KB sweep: the game is NOT '1995' -- era witnesses say BT 4.10 shipped later, and the 1995 belief traced to the MUNGA library RCS strings inside BTL4OPT.EXE (library code date, not the game; the exe has no game-copyright year). ~120 era references across CLAUDE.md/context/glossary/handout rewritten to 'the original'/'the shipped machine'; genuine dated facts kept (RCS $Id strings, Borland 1994, the 30-Nov-1995 asset timestamp); dating note + evidence added to project-overview.md

This commit is contained in:
Joe DiPrima
2026-08-12 11:50:56 -05:00
parent 535dc8bfa7
commit 6ac62dff6f
22 changed files with 131 additions and 122 deletions
+5 -5
View File
@@ -5,7 +5,7 @@
> detail is preserved verbatim in **`docs/PROGRESS_LOG.md`** (the old monolithic CLAUDE.md) — the
> ultimate fallback if a topic digest is thin. Migration history: `phases/phase-01-context-restructure.md`.
**Project:** Port VWE's 1995 arcade **BattleTech** (BT) pod game (MUNGA engine, Tesla platform
**Project:** Port VWE's arcade **BattleTech** (BT) pod game (MUNGA engine, Tesla platform
4.10) to modern Windows + the pod hardware, by reconstructing the missing BT game logic from the
`BTL4OPT.EXE` binary on top of the working WinTesla engine.
**Repo of record:** the top-level `CMakeLists.txt` + `README.md` build `btl4.exe`. Layout:
@@ -24,13 +24,13 @@ POLISH list (#156 fade, weapon-state presentation). Details + what's-next:
## YOU ARE THE BATTLETECH PORT EXPERT
**You (Claude) are the expert on this port** — the 1995 MUNGA/BT engine, its asset formats, the
**You (Claude) are the expert on this port** — the original MUNGA/BT engine, its asset formats, the
decompilation, and the reconstruction state. The `context/*.md` files, `reference/glossary.yaml`,
`reference/decomp/` (the raw pseudocode), and the `docs/*.md` ledgers are YOUR knowledge base.
**ALWAYS consult the context system before answering a question about the engine, formats, a
subsystem, or the reconstruction state — and before writing reconstruction code.** Do not rely on
training data; this is a specific, reverse-engineered 1995 codebase. The knowledge here is more
training data; this is a specific, reverse-engineered 1990s codebase. The knowledge here is more
precise than anything you can infer.
### How to answer a question
@@ -93,7 +93,7 @@ precise than anything you can infer.
| Offsets, ClassIDs, addresses, resource types, env vars | `context/decomp-reference.md` |
| What we don't know / deferred systems | `context/open-questions.md` |
| Term / acronym definitions | `reference/glossary.yaml` |
| The ORIGINAL 1995 player manual (controls, per-mech stats, coolant loops) | `reference/manual/Tesla40_BT_manual.pdf` (+ alignment audit in `context/pod-hardware.md` §Manual) |
| The ORIGINAL player manual (controls, per-mech stats, coolant loops) | `reference/manual/Tesla40_BT_manual.pdf` (+ alignment audit in `context/pod-hardware.md` §Manual) |
| The complete verbatim detail (fallback) | `docs/PROGRESS_LOG.md` (the old 2236-line CLAUDE.md) |
| Detailed running ledgers | `docs/RECONCILE.md`, `docs/GAUGE_COMPOSITE.md`, `docs/HARD_PROBLEMS.md`, `docs/SUBSYS_PLAN.md`, `docs/P3_LOCOMOTION.md`, `docs/RESOURCE_AUDIT.md`, `docs/VEHICLE_SUBSYSTEMS.md`, `docs/BGF_FORMAT.md`, `docs/ASSET_PIPELINE.md`, `docs/BT_SOURCE_STATUS.md`, `docs/WAVE_PLAN.md`, `docs/GLASS_COCKPIT.md`, `docs/REVOLVING_DOOR_PLAN.md`, `docs/INPUT_PATH_AUDIT.md`, `docs/RESPAWN_REARM_PLAN.md`, `docs/KD_SCOREBOARD_PLAN.md`, `docs/DIST_LAYOUT_PLAN.md`, `docs/MOUSELOOK_PLAN.md`, `docs/GHOST_MECH_ANALYSIS.md`, `docs/AUDIO_FIDELITY.md`; multi-agent audit ledgers (2026-08-11): `docs/KD_CHAIN_AUDIT_20260811.md`, `docs/MISSION_END_AUDIT_20260811.md`, `docs/RAM_LEAK_AUDIT_20260811.md`, `docs/GEN_WARNING_AUDIT_20260811.md` |
@@ -119,7 +119,7 @@ Tag individual claims (not sections) inline: `[T1]`, `[T2]`, … A claim inherit
---
## Conventions (CRITICAL — full detail in `context/reconstruction-gotchas.md`)
- **Never raw-read a compiled object's offsets** (`*(T*)(obj+0xNN)`) — our layout ≠ the 1995 binary.
- **Never raw-read a compiled object's offsets** (`*(T*)(obj+0xNN)`) — our layout ≠ the original binary.
Use named members / accessors / a **bridge** fn in a complete-type TU. (databinding trap)
- **Never re-declare an engine-base field** — it shadows the base (reads `0xCDCDCDCD`) + mis-offsets.
- **`/FORCE` hides unresolved symbols** — an unresolved external → runtime AV near `__ImageBase`, not
+1 -1
View File
@@ -101,7 +101,7 @@ One shared 128×64 texmap: `bmap:` → `VIDEO/MAT/BMAP.BMF` → 6 `TEXTURE` reco
`LoadBitSliceTexture` merges a 1bpp egg BitMap into it; `FlushBitSliceTexture`
(L4VIDEO.cpp:5682, stubbed in 2007) did `dpl_LookupTexture("bmap:bmap1_tex")` +
`dpl_TexmapTexels2D(texmap, buf, 128, 64, 4)` — one upload rewrites ALL SIX slices. So the
1995 engine painted the **egg's callsign + ordinal bitmaps** onto these plates every mission;
original engine painted the **egg's callsign + ordinal bitmaps** onto these plates every mission;
the baked "PLAYER n" text is only the shipped DEFAULT. Two stacked 128×32 large-name bitmaps
per slice = exactly the PNAME v-halves; two stacked 128×32 ordinal bitmaps ("1st|2nd") = exactly
the PLACE 2×2 grid. Corollaries:
+3 -3
View File
@@ -86,7 +86,7 @@ run\run.cmd [EGG] # default DEV.EGG; cd's to content\ and runs btl4.exe -eg
chain end including a normal quit — it primes players to report quits as crashes; #157 ("did not
exit cleanly") was exactly this echo. The arcade-authentic round-end (mission process exits →
menu relaunch) itself reads as a crash to first-night players.
- **Why this is a BT411-only hazard [T1].** The 1995 pod shipped ONE folder — `BTL4OPT.EXE` sits
- **Why this is a BT411-only hazard [T1].** The original pod shipped ONE folder — `BTL4OPT.EXE` sits
next to `BTL4.RES`/`VIDEO\`/`GAUGE\`/`AUDIO\` (it is still there in `content\`) — and RP411/RP412
keep that shape (`pack-dist.ps1` copies the exe and every asset dir into one dist root). BT411's
two-folder split (`build\Release\btl4.exe` + `content\`) is not a design decision: `mkdist.py`
@@ -97,7 +97,7 @@ run\run.cmd [EGG] # default DEV.EGG; cd's to content\ and runs btl4.exe -eg
authors.** The cwd guard stays either way (it protects the developer tree, where the split is
real and permanent).
- Interactive: ⚠ under the GLASS profile (`content\bindings.txt`, the default since the cyd
merge) drive is the **1995 throttle-lever scheme**, NOT WASD: **SHIFT** = throttle up (lever
merge) drive is the **original throttle-lever scheme**, NOT WASD: **SHIFT** = throttle up (lever
STICKS where you leave it), **CTRL** = throttle down, **ALT** = reverse thrust, **X** =
all-stop, **BACKTICK** = view toggle. ("WASD drive" was the pre-glass dev profile — telling a
glass user W is the confirmed way to watch a mech stand perfectly still, 2026-07-29.)
@@ -183,7 +183,7 @@ stamps `build=4.11.<n> (<hash>)`, so an archived PDB matches a tester's log exac
- **`tools/`** — btconsole.py, disas2.py, map/res scanners. **`context/`** — this knowledge base.
## Versioning (2026-07-18)
`4.10` = the 1995 arcade release; `4.11` = this win32 reconstruction; dev builds =
`4.10` = the arcade release; `4.11` = this win32 reconstruction; dev builds =
**`4.11.<git commit count>` + short hash**, `+` suffix = built from an uncommitted tree
(e.g. `4.11.311 (980c9cd+)`). Stamped every build by `tools/btversion.cmake`
`build/btversion.h` (`BT_VERSION_*` macros); shown in the boot banner (day-log head) and
+17 -17
View File
@@ -53,7 +53,7 @@ section). Counting the enum gives `ThermalSightClassID = 0xBD9` and `TurretClass
(0xBD6) and Searchlight (0xBD8) but disagrees on MechTech (enum 0xBDD vs CLASSMAP 0xBDC), so the
port's `VDATA.h` ordering has drifted from the shipped binary somewhere in this range. CLASSMAP is
binary-derived and wins for what the runtime does; the true numeric value of `TurretClassID` in the
1995 binary is **unknown**. Resolve by disassembling the ClassID each ctor stores before relying on it.
original binary is **unknown**. Resolve by disassembling the ClassID each ctor stores before relying on it.
## No AI — BT is PvP-only (verified, not a recovery gap)
The mission system is NETWORKED and BT never had NPCs/AI. Every mech is driven by a `BTPlayer` (or a
@@ -85,10 +85,10 @@ unlocated** [T4 candidates]: the un-exported stretch `0x4a1674-0x4a2d48` (touche
together near entry `0x4a2971`, which is called from `0x494483`), the two unidentified Mech vtable
overrides `+0x18=0x4a122c` / `+0x1c=0x4a0c2c` (both big switch functions), and lone sites
`0x4877a4` (byte read via the mission entity table `0x5015c8`), `0x4b0097`, `0x45fb14-24`.
**RESOLVED (same dig, deeper): there IS no software pick writer — the 1995 pick was a DPL SCENE
**RESOLVED (same dig, deeper): there IS no software pick writer — the original pick was a DPL SCENE
INTERSECTION** [T0 + T1 converging]. Evidence: (a) the WinTesla renderer still carries the result
members `dplHitInstance/dplHitDCS/dplHitGeoGroup/dplHitGeometry` + `vehicleReticle` (L4VIDEO.cpp
ctor, all NULL-init); (b) the stubbed 1995-era renderable constructors each took
ctor, all NULL-init); (b) the stubbed original-era renderable constructors each took
**`dpl_isect_mode_obj` ("type of intersections to do on this object") + an intersection MASK** —
per-renderable intersection configuration against the scene; (c) Auric's account ("the pod's
division card cast from the view"); (d) VGL Lynx's LOD warning reads as firsthand knowledge — the
@@ -97,7 +97,7 @@ binary or the pseudocode ever writes `rayIntersection/targetEntity/targetDamageZ
only reads, initializes (the Mech ctor @0x4a1674 constructs the embedded Reticle), gates
(`FUN_004afd10`, the look-state machine, toggles `pickPointingOn` per view with the π rear case),
and SUBMITS the reticle to the board for drawing (`FUN_00460a7c` packages +0x37c/+0x390 into dpl).
**Aimed fire in 1995 had PER-PART precision**: scene ray → struck triangle on the current LOD →
**Aimed fire in the original had PER-PART precision**: scene ray → struck triangle on the current LOD →
the DCS = the struck SEGMENT → its `dzone` (the SKL segment→zone map, `GetSegmentIndex@49db20`) =
`targetDamageZone`. The cylinder lottery (STEP 6) was only ever the UNAIMED path. The port's
whole-mech box pick + "STEP-6 zone under the boresight" funnels AIMED fire through the unaimed
@@ -127,7 +127,7 @@ patch CENTROIDS so the zone walker can exercise per-panel. Bench: [[test-harnes
drawn geometry -- the any-object SPHERE fallback and the caller's whole-mech AABB fallback are
gone (sphere only for a mesh the reader cannot parse -- none exist, counters clean; AABB only as
the pre-tree replicant grace, `BTMechSegmentPick` returns 1/0/-1). They were port stand-ins the
1995 card never had, and their signature regime was a LEVEL boresight over a SHORT mech: the
original card never had, and their signature regime was a LEVEL boresight over a SHORT mech: the
blackhawk's mesh tops out below eye-ray height, so rays cleared every triangle while threading
the fat cull spheres -- lock ring lit with the reticle above its head (night-12 field report,
operator-witnessed on the sweep bench). Instruments: `BT_LOCK_SWEEP` (torso-pan lock envelope,
@@ -156,7 +156,7 @@ stale). **Selection is SPECIFICITY-FIRST: among pierced spheres the SMALLEST rad
mesh's sphere (r≈4.1 on the MadCat vs shoulders r≈1.0) envelops the mech, so nearest-entry always
faces you AND normalized distance rewards the giant sphere (a 0.45u-off-axis ray scores 0.012 vs
it). Limbs nest inside the envelope; smallest-pierced picks the most specific part on the aim line
— the per-part semantic the 1995 mesh intersection produced. `mech4.cpp` tries the segment pick
— the per-part semantic the original mesh intersection produced. `mech4.cpp` tries the segment pick
per candidate (box `PickRayHit` + zone 1 → the unaimed lottery survives only as fallback:
no tree / wrecked / structure occlusion), and the winner's zone rides `MECH_TARGET_SUBIDX` +
`targetReticle.targetDamageZone` into `SendDamageMessage` — aimed hits now dispatch a REAL zone.
@@ -333,7 +333,7 @@ an enclosing box), no crash. The interactive boresight (`BTGetAimRay`) can't be
## Firing arc + muzzles
There is NO firing-arc field in the binary (`Emitter::FireWeapon` fires whenever HasActiveTarget) —
the 1995 game got away with it because it was **cockpit-only** (a beam behind you is off-camera). The
the original game got away with it because it was **cockpit-only** (a beam behind you is off-camera). The
port's external camera exposes it, so the arc is gated on the **torso twist range**
(`Torso::GetHorizontalReach`, 0 for the twist-disabled Blackhawk) + a base aim tolerance
(`BT_FIRE_ARC`, a labeled PORT presentation param). Beams fire from the mech's real gun-port SITE
@@ -549,13 +549,13 @@ measuring a PORT bug. The truth [T1, raw disasm @0x4ac7bb + live-verified]:
own error strings).
Consequences: `MechSubsystem::TakeDamage @0x4ac0bc` (now real, was a stub) accumulates authored
per-type damage on subsystems; crits land and destroy subsystems; and **the Myomers un-powered
self-repair `@004b8bb9` was LIVE in 1995 and is live in the port now** — the earlier "dead code in
self-repair `@004b8bb9` was LIVE in the original and is live in the port now** — the earlier "dead code in
the original too" note in [[subsystems]] WAVE 6 is superseded. `DistributeCriticalHit`'s direct
`damageLevel = 1.0f` pin remains the ammo-explosion path, not the general mechanism.
## ⚠ ZONE SELECTION IS A WEIGHTED LOTTERY — pixel-precise limb damage does not exist (2026-07-29) [T1]
Issue #73 ("fired only at the left arm, damage credited elsewhere, no crits") is **largely the
authored 1995 model, not a bug**. The chain (`dmgtable.cpp`, binary-faithful, byte-verified stream
authored original model, not a bug**. The chain (`dmgtable.cpp`, binary-faithful, byte-verified stream
format): impact point → `WorldToLocal`**height layer** (`floor(layerCount·y/heightRef)`) →
**pie slice** (`atan2(local.z, local.x)`, optionally rotated by live torso twist —
`rotateWithTorso` per layer) → `DamageZonePercentTable::SelectZone()` = **`RandomUnit()` against
@@ -616,8 +616,8 @@ btplayer.hpp, static_assert-locked; `scoreAward`=APPLIED tally in all three):
`killCount++`: **the #134 panic penalty, live** (bench: `type=2 award=-39.00 kills=0`).
- **B @0x4a05d9, not newly killed && tally≠0** → type 0 to the shooter: the **per-hit INFLICTED
credit**, and it is LIVE. ⚠ **CORRECTED 2026-08-07** — this entry previously read "the ONLY
registered 0x16 receiver Verify-rejects type 0 → 1995 folded an UNINITIALIZED stack float into
the shooter's score on every non-lethal hit (real 1995 bug — @0x4c0200 … is in NO table entry:
registered 0x16 receiver Verify-rejects type 0 → the original folded an UNINITIALIZED stack float into
the shooter's score on every non-lethal hit (real original-binary bug — @0x4c0200 … is in NO table entry:
dead code)". **That was wrong**, and build 787 retired the port's per-hit crediting on the
strength of it (the scoring regression players reported on 4.11.817). `BTPlayer` overrides
**`Dispatch` — vtable `@00513300` slot 3 = `FUN_004bffa0`** — and splits type 0 off BEFORE base
@@ -636,7 +636,7 @@ btplayer.hpp, static_assert-locked; `scoreAward`=APPLIED tally in all three):
Reports carry the LOOP-ENTRY zone (msg+0x24, never rewritten mid-loop), the vital-wreck flag, and
`inflictingSubsystemID` (msg+0x5c, engine T0 name).
**Score model consequence** [T1, ⚠ REWRITTEN 2026-08-07]: 1995 pod scoring = **per-hit inflicted
**Score model consequence** [T1, ⚠ REWRITTEN 2026-08-07]: original pod scoring = **per-hit inflicted
credit + kill awards + received-damage penalties + death costs**. The previous text here claimed
"No per-hit inflicted credit … inventions riding the dead @0x4c0200 channel; both retired" — that
followed from the dead-code misreading corrected in report B above, and retiring the credit IS the
@@ -1235,10 +1235,10 @@ code involved; grammar = the binary ctors @0x49ea48/@0x49e5e4). Results:
W1|W2 SEAM. (Corrects the earlier #92 comment that put frontal hits in W6.)
* **⚠ PORT FRAME ADAPTER (#124, fixed 2026-08-03; corrected same day by the
MUZZLE-ANCHOR probe)**: that +Z-forward (and +X-right, from the W0/W7
"Right*" names) is the **1995 resolver frame**. OUR engine frame, measured
"Right*" names) is the **original resolver frame**. OUR engine frame, measured
with the mech's own asymmetric geometry as the anchor (`BT_ASPECT_TEST`:
four world-cardinal self-impacts + one at each weapon's physical muzzle):
**RIGHT = +X, same as 1995; FORWARD = Z, flipped** — the LEFT missile pod
**RIGHT = +X, same as the original; FORWARD = Z, flipped** — the LEFT missile pod
sits at raw local x=2.32, the RIGHT at +2.32, and `WorldToLocal` is a clean
R(yaw) with no hidden terms (verified at yaws 30°/150°/40°). The frames
differ by a **Z-NEGATION ONLY — a reflection** (the first commit's π
@@ -1268,7 +1268,7 @@ the per-frame UpdateDeathState and a same-frame direct+splash lethal pair re-ran
block (duplicate kill credit + duplicate VehicleDead) — caught by the 4-node bench, fixed by
running the once-per-death transition inline at the death tail (1b6d247). Full class write-up:
[[reconstruction-gotchas]] §25; stress verification: kd4_bench 9/9/9/9 + kd8_stress 26/26/26/26,
SWALLOWED=0 both. The receiver-side dedup remains a TRIPWIRE only (1995 has none; +0x290 never
SWALLOWED=0 both. The receiver-side dedup remains a TRIPWIRE only (the original has none; +0x290 never
written) — if it ever fires again, a new duplicate source exists upstream.
## Key Relationships
@@ -1278,7 +1278,7 @@ written) — if it ever fires again, a new duplicate source exists upstream.
## The K/D-score AUTHORITY MODEL -- settled once and for all (2026-08-11, #162) [T1]
The 1995 chain has ONE emitter and NO receiver dedup; every counter is single-owner.
The original chain has ONE emitter and NO receiver dedup; every counter is single-owner.
Fixed twice piecemeal (#150 kills, #162 deaths) before the full audit settled it -- do
not touch any single limb of this chain again without re-reading this section.
@@ -1307,7 +1307,7 @@ part_013.c:10519) -- the #81-era port put it in the VehicleDead arm by misattrib
disarming the port's defensive latch (corrected 2026-08-11).
**Replication:** the player update record carries currentScore(+0x1c8) +
dropZoneLocation ONLY. killCount/deaths never cross the wire in 1995; each node's
dropZoneLocation ONLY. killCount/deaths never cross the wire in the original; each node's
scoreboard columns are locally accumulated (the port's SBMIRROR is a deliberate
deviation that makes boards consistent).
+6 -6
View File
@@ -299,7 +299,7 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
Score@0x4c02e4** (the type-1/2 switch — BT shadows the engine id with its override), **0x17
VehicleDead@0x4c05c4, 0x18 MissionStarting@0x4bfbe8, 0x19 MissionEnding@0x4bfc20, 0x1a
ScoreUpdate@0x4c02a8** (= Player::NextMessageID). **@0x4c0200 ("ScoreInflicted", type-0-only,
Verify line 0x18b) is in NO entry — dead code in 1995.** The 1995 type-0 path therefore lands in
Verify line 0x18b) is in NO entry — dead code in the original.** The original type-0 path therefore lands in
@0x4c02e4's Verify arm and folds an UNINITIALIZED [ebp-0xc] into currentScore (real bug; port
banks 0). @0x4c02e4 internals [T1 raw]: `+0x40==4` MissionEnding gate; registry-find msg+0x34;
case 1 = `(ownTonnage/senderTonnage) × role->CalcDamageReceivedScore(basis)` + the
@@ -329,7 +329,7 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
call to engine Player::ScoreMessageHandler @0x42da20 (never dispatched, never on the wire —
no matchlog receipt exists; the port's `[deathcost]` DEBUG line is the only witness).
**deathPending(+0x290) is NEVER written non-zero anywhere in BTL4OPT.EXE** (three writes,
all zero-stores: ctor @0x4c0c07, respawn helper @0x4c01e5, DropZoneReply tail) — 1995 has NO
all zero-stores: ctor @0x4c0c07, respawn helper @0x4c01e5, DropZoneReply tail) — the original has NO
receiver-side death dedup; the sender's one-shot edge is the whole guarantee. The clear pair
`{+0x290=0, +0x258=0}` lives in **FUN_004c012c = DropZoneReply**'s tail (part_013.c:10519-23)
— NOT the VehicleDead arm (the #81-era port misattribution, corrected 2026-08-11). Latch
@@ -342,7 +342,7 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
tail; gotcha §25). `advancedDamageOn(+0x264)` ← the mission egg's
per-pilot `advancedDamage=` key (every stock egg stamps 1 on every page); Player update
record replicates ONLY currentScore(+0x1c8) + dropZoneLocation — killCount/deaths never
cross the 1995 wire (the port's #45 record extension mirrors them owner→replicant; benched
cross the original wire (the port's #45 record extension mirrors them owner→replicant; benched
convergent at 4 and 8 nodes, kd4_bench/kd8_stress).
- **HeatableSubsystem** @0x50E41C: {3, "ToggleCooling"→@004ad6f8}. **Disassembled 2026-07-20**
(`tools/disas2.py 0x4ad6f8`): a per-subsystem coolant on/off TOGGLE (NOT a multi-level "cycle
@@ -360,7 +360,7 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
GeneratorA-D — unreconstructed).
- **Searchlight** @0x51117C: {3, "ToggleLamp"→**@004b838c**}; **ThermalSight** @0x51120C: {3,
"ToggleLamp"→**@004b860c**}. ⚠ **CORRECTED 2026-07-25 (#61)** — these were swapped here (and in
`searchlight.cpp:51`, `hud.cpp:282`), which propagated a false "1995 latent bug" across four
`searchlight.cpp:51`, `hud.cpp:282`), which propagated a false "original latent bug" across four
context files; see the WAVE-4 note in [[subsystems]]. The two TUs emit PARALLEL shared-data blocks
with identical stride — msg entry, +0x5C attribute entries, +0x84 Performance triple:
| TU | msg entry | handler | attributes | Performance |
@@ -500,7 +500,7 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
warning; respawn-with-tripped-gen RTIS (4,0) FIRES it (ghost warning during
respawn); crit-death fires enter-Idle one frame late; 4→1 (cooled while switched
off) double-fires. The generators' inv+Start is the ONLY inverse-Start fault
trigger in the game (census) — a 1995 authoring slip [T4 intent]. Port machinery
trigger in the game (census) — an original authoring slip [T4 intent]. Port machinery
is faithful end-to-end. **DELIBERATE DEVIATION shipped 2026-08-11 (user-approved):**
the AudioStateTrigger ctor (AUDWTHR.cpp) flips the GeneratorState (trigState=4,
inv=1) record to inv=0 at stream time — warning now fires at the TRIP, silent at
@@ -560,7 +560,7 @@ From the weapon `.SUB` records + the charge-curve `.data` constants (PE-parsed a
with `scoreAward = ScenarioRole::specialCaseDeathPenalty` (role+0x20, the 4.10-only
key; restored to SCNROLE.h/.cpp — record slot [1] after KillBonus, fail-hard read).
Direct call, NOT dispatched — bypasses the BT handlers' type Verifies. NUANCE [T4]:
the 1995 engine adds at Player+0x1c8 while every BT scoreboard reads +0x278 — the
the original engine adds at Player+0x1c8 while every BT scoreboard reads +0x278 — the
pod's death cost may never have displayed; our single-cell port shows it. Shipped
content authors NO role keys, so the cost is 0 in the field. **CORRECTED 2026-08-07 —
that sentence was wrong on both halves.** The role's scoring fields are not authored via
+1 -1
View File
@@ -14,7 +14,7 @@ open_questions:
# Experience levels / simulation modes
The egg's per-pilot `experience` field is the pod's **simulation-fidelity tier** — the thing the
1995 site operator set per player at sign-up. It is NOT the BAS/MID/ADV control mode (a free
original site operator set per player at sign-up. It is NOT the BAS/MID/ADV control mode (a free
in-cockpit choice, [[locomotion]]/[[pod-hardware]]) and NOT the scenario role (scoring modifiers).
Researched end-to-end 2026-07-18; this file supersedes the older "role display toggles" /
"ROOKIE-role lockout" readings (see Corrections below).
+4 -4
View File
@@ -333,7 +333,7 @@ screen paint over the shared Eng plane, pinning it on the highest screen). Pixel
lists SCORING players (flag cleared when the mech links its player, mech.cpp:688, all
nodes). Both loops now use `Player::IsScoringPlayer()`; also closes a `pilotIDs[1]` heap
overrun the undercount exposed.
2. **Latch race**: the binary's build-once latch assumes 1995 synchronous pod loads; over
2. **Latch race**: the binary's build-once latch assumes the original's synchronous pod loads; over
async relay TCP the faster node ticks InterpretControls before the peer's replicants
arrive (rig-proven: staggered node logged roster 1 → then peer arrival). [T3
accommodation, demand-latch precedent]: rebuild whenever the scoring CENSUS changes —
@@ -641,7 +641,7 @@ and every instrument is now live [T2]:**
(class key 0x46) = the engine's **`CameraShipHUDRenderable`**, the camera/broadcast seat's
followed-name banner + ranking window, which uses PNAME **and** PLACE1-8 as sorted
`[ordinal][callsign]` rows and is **already live** as screen quads (see [[multiplayer]] camera
seat). Its 1995 source is likewise commented out in `L4VIDRND.cpp:2562-2876` — the literal
seat). Its original source is likewise commented out in `L4VIDRND.cpp:2562-2876` — the literal
`dpl_*` calls, a Rosetta stone for the binary's node API.
- **Simple-X mode** (PrimaryHudOn off, mask 0x20): the minimal reticle — a small green cross
(±0.02..0.08 arms) riding the aim translate (ctor @4689-4705 [T1]) — swapped for the full HUD
@@ -674,7 +674,7 @@ BITMAPS from the egg, not text — format, renderers, and the operator-console g
**The gauge blit addresses SOURCE rows bottom-up, and the vertical strip artwork is authored
to match.** `Video16BitBuffered::DrawBitMapOpaque` converts the incoming source rect with
`sTop = map_max_y - sTop; sBottom = map_max_y - sBottom` and walks rows upward
(`engine/MUNGA_L4/L4VB16.cpp:3846-3850`, `UP_BITMAP` :2155) [T0]; the 1995 display blit
(`engine/MUNGA_L4/L4VB16.cpp:3846-3850`, `UP_BITMAP` :2155) [T0]; the original display blit
@0046bdfc performs the IDENTICAL flip [T1 disasm], and the PCC→BitMap loader stores the PCX
top scanline at memory row 0 unflipped (`GRAPH2D.cpp:27-174`). Net: in `OneOfSeveral::Execute`
`sy0 = row*frameHeight` selects frames counting from the **BOTTOM** of the image as authored.
@@ -780,7 +780,7 @@ Or/Xor ignore the mask entirely and And clears foreign planes -- all audited):
entry=0xffffff00 mask=0x3f`, at exactly the artifact positions in the
operator's screenshot. FIX: zero the table in the ctor + cycle the in-plane
pattern across entries [2^bits..255] (high-index art degrades to its
index-mod-2^bits colour IN-PLANE, can never leak). The 1995 binary ships the
index-mod-2^bits colour IN-PLANE, can never leak). The original binary ships the
SAME 64-entry fill and relied on 6-bit art discipline -- garbage is not a
preservable behaviour, so the cycle-fill is a guarded PORT deviation.
+5 -5
View File
@@ -387,10 +387,10 @@ It lands on BT's addresses better than it deserves to: `1-4`+`QWER` are the ENTI
(Condensers 1-6, flush, balance), `F6`/`F7` the display/control-mode cycles, `F9`-`F12` Generators
A-D, `F4` the crouch button.
**What it costs, deliberately.** A bound key is removed from the authentic 1995 typed-hotkey
**What it costs, deliberately.** A bound key is removed from the authentic original typed-hotkey
channel to stop double-dispatch, and this board binds nearly everything — so `5` (Quad page), `z`
(Eng1), `t/y/u/i/o` (pilot select) and `+`/`-` (target zoom) are given up. Unbind a key to get its
1995 meaning back.
original meaning back.
**New rule: a bindings row WINS over a built-in convenience key** (`PadRIO::KeyHasBinding`).
`V` and `J/K/L` are board buttons now, so the view-toggle and preset-cycle polls stand down for
@@ -518,7 +518,7 @@ assigned axes excluded), derives invert from the move direction (throttle from F
position), auto-adds the hat look cluster, and rewrites bindings.txt between
`# >>> BT_JOYCONFIG` markers (rest of the file preserved). Sign conventions: JoystickX + =
twist right, JoystickY + = aim up (wizard binds flight-style forward=down via invert), Turn +
= right. Legacy `L4DINPUT.cpp DIJoystick` (L4CONTROLS=DIJOYSTICK, 1995 `Joystick` iface)
= right. Legacy `L4DINPUT.cpp DIJoystick` (L4CONTROLS=DIJOYSTICK, legacy `Joystick` iface)
left untouched. Verified: grammar accept/reject by line, no-device + XInput-only polling
clean 30 s in-mission, zero DI cost with no joy rows. NOT yet verified: real stick axes
(no hardware here) — needs a tester with a stick.
@@ -590,7 +590,7 @@ copied from them.
(right-click = hold latch) — the merged default profile lives in `L4PADBINDINGS.cpp`
(auto-writes `content\bindings.txt`; W/S/A/D/Q/E/R/F/X drive, 1-4/Space/Ctrl fire, M/N/H/C =
pod buttons 0x18/0x15/0x2C/0x2F, `/V view toggle + J/K/L preset cycles hardcoded in PadRIO).
Bound keys are SUPPRESSED from the typed 1995 channel (`PadRIO::SuppressKey`, the btinput
Bound keys are SUPPRESSED from the typed original channel (`PadRIO::SuppressKey`, the btinput
pattern — ended the glass double-dispatch: 'w'=pilot-select, numpad/F-key key-up aliases);
unbound keys keep their authentic meanings (5/z presets, t-o pilot select, +/- zoom).
In BAS control mode the pod turns with the STICK (Q/E) and pedals are inactive — authentic;
@@ -654,7 +654,7 @@ copied from them.
streamed Eng-page `.CTL` dispatches subsystem msg **id 3 / 0xb** to whatever subsystem is shown,
and a **weapon (Emitter)** registers only PoweredSubsystem 4-8 + MechWeapon 9-10 — so id 3 is a
gap below `entryCount`, and `Receiver::MessageHandlerSet::Build`'s `new HandlerEntry[]` left it
as heap garbage → `(this->*garbage)(msg)`. The 1995 binary has the identical non-zeroing
as heap garbage → `(this->*garbage)(msg)`. The original binary has the identical non-zeroing
`new[]` and survived only on fresh-heap-zero luck (id 3 on a weapon was always meant to be
IGNORED — it's a Condenser/Reservoir action in a different subsystem branch; the uniform
Eng-page button template makes buttons for unimplemented actions authentically inert). The new
+2 -2
View File
@@ -185,7 +185,7 @@ Declared-separate members don't see each other's writes — alias them.
Replicants (task #50): travel = engine DeadReckon; leg channel driven joints-only from the replicated
worldLinearVelocity -> speedDemand (mech4.cpp REPLICANT MOTION) — detail in [[multiplayer]]. [T2]
## The AUTHENTIC 1995 ground model (default-on, `BT_GROUND_REAL`)
## The AUTHENTIC original ground model (default-on, `BT_GROUND_REAL`)
Decoded by the 10-agent `ground-model-decode` workflow (binary `FUN_004a9b5c` = the master perf).
**NO GRAVITY anywhere** — accelerations zeroed every frame. Vertical position = an ABSOLUTE per-frame
ground SNAP:
@@ -462,7 +462,7 @@ trap never fired while the value changed) resets the sim cell between damage eve
1 Hz bench harness it restarted the warning before the 1.8 s voice note; sporadic real-play
damage is unaffected. Needs a cdb write-watchpoint dig (candidates: a recon raw +0x2c-family
write or a struct copy spanning it).
### The gimp cell is REPLICATED STATE — peers limped for free in 1995 (#82, fixed 2026-07-29)
### The gimp cell is REPLICATED STATE — peers limped for free in the original (#82, fixed 2026-07-29)
Field: "peers see a limping mech **skating**" (first-person limp + voice correct; bystanders saw a
normal walk sliding at limp speed; a stop/start did not clear it). Root cause chain, all [T1/T2]:
- The binary's one `mech+0x40` **is** `Simulation::simulationState`, which
+12 -12
View File
@@ -357,7 +357,7 @@ phase). Plan: `~/.claude/plans/partitioned-snuggling-piglet.md`.
— the relay fans out server-side (kills the mesh's N1× duplication at the seam where
`EntityBroadcastToReplicants` already serialized once). Verified: relay `tcp rx==tx`, one
copy per broadcast.
- **UDP channel = the 1995 NETNUB reliable/unreliable split RESTORED (not invented)**:
- **UDP channel = the original NETNUB reliable/unreliable split RESTORED (not invented)**:
`Receiver::Message` defaults `messageFlags=ReliableFlag` (RECEIVER.h:195); the ~60Hz update
path sets flags=0 (ENTITY.cpp:590); `Mode(UnreliableMode)` at LoadingMission (APP.cpp:704,
the 2007 port's `Mode()` was a no-op — now STORES the mode). The routing gate is
@@ -397,7 +397,7 @@ phase). Plan: `~/.claude/plans/partitioned-snuggling-piglet.md`.
SEAT_REQUEST branch (btconsole.py). Verified: 8/8 stub tests (distinct seats, FULL on
exhaustion, claim clears reserve, duplicate-HELLO refused) + 2-node localhost e2e with NO
BT_SELF on either pod → both seated, full ladder, launch, UDP flowing.
- **WALK-UP CALLSIGN/MECH REQUEST (2026-07-22) [T2]**: the 1995 front-desk conversation, over
- **WALK-UP CALLSIGN/MECH REQUEST (2026-07-22) [T2]**: the original front-desk conversation, over
the wire. join.bat/join_lan.bat set `BT_FE_JOIN=1` and launch ZERO-ARG -> the FE menu in JOIN
trim (btl4fe.cpp `FeJoinOnly()`: CALLSIGN edit + the 18-mech list + JOIN; screenshot-verified)
-> `BTFeLaunchJoinRelay` relaunches with the universal join args + `BT_CALLSIGN`/`BT_MECH` env
@@ -438,7 +438,7 @@ phase). Plan: `~/.claude/plans/partitioned-snuggling-piglet.md`.
default case `Fail()`ed = abort = a SILENT crash (c0000409, no WER record; cdb stack:
ucrtbase!abort <- RunMissionMessageHandler <- RoutePacket; 3/3 reproducible in the
console-shaped run, and the field report's 464 crash died mid-LOD-load with the same
signature). The 1995 console never hit this state (operator-paced launches). FIX
signature). The original console never hit this state (operator-paced launches). FIX
(L4APP.cpp): the default case now DEFERS -- re-posts the message to itself at +1s (the
VehicleDead re-post pattern) until the ladder reaches WaitingForLaunch; bounded at 120
deferrals so a wedged load still surfaces the original Fail. `[launch] ... deferred +1s`
@@ -513,9 +513,9 @@ phase). Plan: `~/.claude/plans/partitioned-snuggling-piglet.md`.
is the console pad -- the game socket doesn't exist yet, so the first fix's RelayGameDown
hook could never fire (caught by the verification itself). Detection now lives in
`HostDisconnectedMessageHandler` case ConsoleHostType (relayMode && !scene-presented ->
`BTRelayRejoinNow`); mesh keeps 1995 console-less behavior. Verified 2x in one run.
`BTRelayRejoinNow`); mesh keeps the original console-less behavior. Verified 2x in one run.
2. **Post-egg pre-mission relay loss**: RelayGameDown -> rejoin (the original fix). ✓
3. **Mid-mission relay loss = forever-mission**: the STOP is relay-sent (1995: console owns
3. **Mid-mission relay loss = forever-mission**: the STOP is relay-sent (original design: console owns
the clock), so a pod losing its relay mid-match played alone forever. Now RelayGameDown
(scene presented) posts `StopMissionMessage` at +15s -> normal end path -> lobby relaunch
-> patient wait. Verified: scene LIVE -> relay killed -> 15s -> clean RunMissions exit.
@@ -598,14 +598,14 @@ phase). Plan: `~/.claude/plans/partitioned-snuggling-piglet.md`.
drives the pod countdown + the final-30s ranking window; the relay now SENDS
`Application::StopMissionMessage` (clientID 4, msgID 6, exitCode 0) at length+2s grace, or on
the operator's ⏹ END MISSION button (stdin `stop`). Was half-implemented — nothing fired it in
1995 the console owned the stop. Pod runs the authentic fade → mission-loop exit → clean
the original — the console owned the stop. Pod runs the authentic fade → mission-loop exit → clean
process exit. Verified live (40s test mission ended on the clock).
> **The console/relay now has its own topic: [[operator-console]]** (`context/operator-console.md`)
> — which of the two programs is which, ports, the seat model, the full round/launch lifecycle and
> its wedge states, plus liveness. Read that first for anything console-side; the notes below are
> the historical record of how it got built. Sysop how-to: `docs/OPERATOR_GUIDE.md`.
- **OPERATOR CONSOLE (2026-07-18)**: `tools/btoperator.py` (PySide6) — the lost 1995 operator
- **OPERATOR CONSOLE (2026-07-18)**: `tools/btoperator.py` (PySide6) — the lost original operator
station recreated: mission editor with ALL values validated live from BTL4.RES
(`tools/eggmodel.py`: maps=type14∩26, colors/badges/patches from the type-25 vehicletable,
vehicles from ModelLists), relay/mesh mode switch, live pod-status lights parsed from the
@@ -637,7 +637,7 @@ phase). Plan: `~/.claude/plans/partitioned-snuggling-piglet.md`.
— the egg's `[mission] length=` (600 = the 10-min pod mission) drives the pod-side countdown
(`secondsRemainingInGame`, APP.cpp:652) and the ranking-window display for the final 30s
(DIRECTOR.cpp:113), and the full end chain exists (StopMission id 6 → EndingMission → player
fade → Stop) — but NOTHING fired it: in 1995 the CONSOLE sent StopMission at expiry (mission
fade → Stop) — but NOTHING fired it: in the original the CONSOLE sent StopMission at expiry (mission
length enforcement is console-side). Now reconstructed: the relay arms the clock at RunMission
#2 and sends `Application::StopMissionMessage` (clientID 4, msgID 6, +int exitCode 0) at
length+2s grace; the operator app gets an ⏹ END MISSION button (stdin `stop`, like `launch`).
@@ -713,7 +713,7 @@ carousel, Phase 2 = true drop-in). The durable engine facts found:
existing ROSTER host to OnLine whenever it connects — arrival order/timing is free (this is
how slow loaders already work), and the relay's PEER_UP exchange is bidirectional for
newcomers (btconsole.py:1202-1209).
- **Late ENTITY sync was never finished in 1995**: entity MakeMessages are broadcast ONCE at
- **Late ENTITY sync was never finished in the original**: entity MakeMessages are broadcast ONCE at
creation and never retained. `InterestManager::NotifyOfEntityCreation` (INTEREST.cpp:694-763
→ DynamicEntityCreation :839+) sends immediately; the interest-zone bookkeeping is
`#if 0 "HACK - Partial implementation"`; and `HostManager::NotifyOfReplacementEntityCreation`
@@ -769,7 +769,7 @@ transition, HUD all landed since P6): console egg → mesh → RunningMission on
deathPending); without one → engine base @0042db80 (PLAYER.cpp:214: closest non-"win"
DropZone → AssignDropZone → 2s re-post probe) → DropZoneReply → CreatePlayerVehicle (a NEW
mech; `deathCount = 0`) + viewpoint relink (Verify(viewpointEntity==NULL) is compiled out at
DEBUG_LEVEL 0 — release overwrites, same as the 1995 build). @0049fe0c = roster-wide
DEBUG_LEVEL 0 — release overwrites, same as the original build). @0049fe0c = roster-wide
slot-11 DeathShutdown broadcast (the death transition already runs that pass).
CORRECTIONS to the earlier scouting: BTL4.RES **type 12 = InternalAudioStream** (the engine's
own resource-type table) — `*int.scp` are internal-AUDIO scripts, NOT per-mech intro scripts;
@@ -895,7 +895,7 @@ transition, HUD all landed since P6): console egg → mesh → RunningMission on
## Authentic coupled peer motion — DONE (task #50, 2026-07-14) [T1]
Decomp-verified (workflows w0odszxro/wh1h5gnmc, 3 make-or-break claims adversarially CONFIRMED; committed c52a1ad + default-promoted). The 1995 peer per-frame path FUN_004ab430 -> FUN_004ab1c8 (IntegrateMotion) drives motion SINGLE-SOURCE:
Decomp-verified (workflows w0odszxro/wh1h5gnmc, 3 make-or-break claims adversarially CONFIRMED; committed c52a1ad + default-promoted). The original peer per-frame path FUN_004ab430 -> FUN_004ab1c8 (IntegrateMotion) drives motion SINGLE-SOURCE:
- LINEAR position = GAIT. IntegrateMotion advances the BODY gait channel FUN_004a5678 (FUN_004a5028 is DEAD CODE, zero call sites), turns its cycleDistance into velocity `{0,0,-cd/dt}`, rotates by heading, integrates into projectedOrigin.linear@0x260 (part_012.c:14994/14997/15008); localOrigin.linear@0x100 is copied VERBATIM from it (FUN_00408440, part_012.c:15131 -- NOT a lerp). updateVelocity.linear is never used for position.
- ANGULAR/heading = replicated-velocity slerp (the ONLY thing Mover::DeadReckon-style velocity extrapolation drives): projectedOrigin.angular@0x26c built from updateVelocity.angular@0x2d4 (FUN_004ab188), localOrigin.angular slerped toward it.
- 0x260 aliasing: "motionDelta@0x260" and "projectedOrigin@0x260" are LITERALLY THE SAME field (base Mover::projectedOrigin, Origin 0x1c); "worldPose@0x26c" is its quaternion half. Our reconstruction's shadow names hid this and drove the false velocity-vs-gait "contradiction".
@@ -913,7 +913,7 @@ The scoring authority model (full map: [[combat-damage]] + `docs/KD_CHAIN_AUDIT_
the VICTIM's master emits everything on its death edge (kill credit to the shooter via
replicant-Dispatch reroute, received penalty to itself, one VehicleDead); the killer's master
increments `killCount` when the rerouted credit lands; **killCount/deathTally never crossed the
1995 wire** — the port's #45 update-record extension mirrors them one-way owner→replicant
original wire** — the port's #45 update-record extension mirrors them one-way owner→replicant
(read guarded replicant-only, mixed-version length guard, 2s heartbeat re-dirty). Bystander
nodes learn tallies ONLY through that mirror. Verified: `kd4_bench.sh` (bystander convergence +
no-mirror-exceeds-authority, receipt-derived per player) and `kd8_stress.sh` (8-node all-vs-all
+13 -13
View File
@@ -29,7 +29,7 @@ authentic path scoped.
(`FUN_0048d4d4` = "flush_material", dpl command opcode 100 — siblings flush_ramp/flush_texture).
The card-side shading (how material colour selected/scaled the 256-row material ramps → final
texels) is i860 firmware, NOT in BTL4OPT.EXE — so the PERCEPTUAL damage-darkening curve of the
1995 pods cannot be recovered from this binary. Our port applies the host's linear factor at
original pods cannot be recovered from this binary. Our port applies the host's linear factor at
draw (TFACTOR modulate, L4D3D). Field testimony ("a single missile leaves visible armor
damage") is arithmetically consistent with the linear curve — one 25-pt hit snaps a 68-90-pt
zone by 0.28-0.37 (measured live: amt=25 → rarm 0→0.3247) — but if the card's ramp mapping was
@@ -64,7 +64,7 @@ authentic path scoped.
relay session — replicated player links (`bmp=1+veh`), director locks the goal (30s timer),
camera ship tracks the moving mech (`sees=1`) and CUTS between authored cameras
(screenshots). Egg: pilot page `hostType=1` + `vehicle=camera`. **RANKING WINDOW LIVE (same day)**: the stubbed
1995 overlay reimplemented as screen-space quads in `CameraShipHUDRenderable::Render` — the
original overlay reimplemented as screen-space quads in `CameraShipHUDRenderable::Render` — the
followed player's callsign banner (bottom center, the Execute index is ALREADY 0-based) + a
rank-ordered row per scoring player ([ordinal][callsign], right of center), visibility = the
Director's flash logic (10s on/15s off, solid final 30s), live `playerRank` pointers re-sort
@@ -162,7 +162,7 @@ New unaccounted functionality no prior list knew (addresses verified absent from
launcher's `ExplosionModelFile`. The MissileThruster parser `FUN_004bf8ec` writes `"SplashRadius"` to
+0x50 (size-0x54 record). Burst falloff `baseBurst / dist^exp` floored at 1 (arcade 1.25 / WinTesla
`EXPLODE.cpp:209` 1.2f). Only missiles splash (AC tracer is not a Missile). The engine `EXPLODE.cpp`
diff is DONE — the T0 source confirmed the model; the only 1995-vs-WinTesla drift is the exponent.
diff is DONE — the T0 source confirmed the model; the only original-vs-WinTesla drift is the exponent.
Port: `BTResolveSplashRadius`/`BTApplySplashDamage` (mech4.cpp), see [[combat-damage]]. **Remaining
T3:** the per-player enable sub-gate `missile+0x360 = BTPlayer+0x264` (its writers read as a per-frame
toggle, not a config flag; the `showDamageInflicted@0x264` label is a guess) — port treats authored
@@ -181,7 +181,7 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
the port now implements it byte-grounded ([[subsystems]] WAVE 6 correction). LESSON: a
"no callers in the decomp" claim is only as good as the export coverage — sweep the raw image
for the operand pattern before declaring a data path dead ([[reconstruction-gotchas]]).
Original entry below for the record:** The 1995 manual (p20/p22 + stat sheets) says seek changes TOP SPEED (gear 4 =
Original entry below for the record:** The original manual (p20/p22 + stat sheets) says seek changes TOP SPEED (gear 4 =
"Supercharge", expert-only; Loki/Thor print Normal 143 / Super Charged 182 / Gimped 40 kph).
The 4.10 binary provably has NO runtime myomer→speed coupling (full data-flow closure [T1]:
demand fn @004afd10 all branches, all speedDemand writers, AvailableOutput's 2 callers, the
@@ -193,7 +193,7 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
authorized multiplication in the demand path. [[subsystems]] [[pod-hardware]]
- **Environment gravity unwired — the myomer CLIMB-heat term is inert (found 2026-07-31, #85).**
`Mover::localEnvironment` (MOVER.h:271) is declared and never populated anywhere in the port;
`GetEnvironment()->gravityConstant` is the named analog of the 1995 Mover's gravity POINTER at
`GetEnvironment()->gravityConstant` is the named analog of the original Mover's gravity POINTER at
`+0x250` (`FUN_00421e2c` does `vy -= **(+0x250)` per tick), which the myomer drive-heat
integrator uses for its `m·g·|vy|·dt` climb-work term. The bridge (`BTMechMyomerMotionSample`)
null-guards it, so climbing currently generates no myomer heat (flat maps: moot; cavern slopes:
@@ -239,9 +239,9 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
`SetFogStyle(searchLightOn/OffFogStyle)` with the binary's inverted-cache seed (authentic DARK
night start), plus the external spot.bgf beam cone (btfx `brighten` additive veil) on the
searchlight site joint, MP-replicated. Historical attribution notes kept below — the
"ORIGINAL 1995 LATENT BUG" premise was ❌ RETRACTED 2026-07-25 (#61).** The arcade swaps fog between `fog=` (lights on) and
"ORIGINAL LATENT BUG" premise was ❌ RETRACTED 2026-07-25 (#61).** The arcade swaps fog between `fog=` (lights on) and
`nosearchlightfog=` (off) via `PullFogRenderable` watching the Searchlight's `lightState`.
**The old entry claimed the 1995 binary itself could never light the lamp. That was wrong** — it
**The old entry claimed the original binary itself could never light the lamp. That was wrong** — it
compared Searchlight's Performance (@004b841c, reads `requestedOn`@0x1E0) against **ThermalSight's**
`ToggleLamp` (@004b860c`0x1DC`), i.e. two different classes, and so invented a missing
"0x1DC→0x1E0 bridge". Searchlight's real handler is **@004b838c** (table @0x51117C) and toggles
@@ -251,7 +251,7 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
carries no novice gate. All T1.]
Searchlight's handler set is WIRED and **verified live**: pad `0x14``requestedOn 0→1`
`lightState 0→1`; the fog watcher + beam cone landed 2026-08-05 (see the header above), so no
remaining work rides this entry. (NB the 1995 searchlight carries NO novice gate — that
remaining work rides this entry. (NB the original searchlight carries NO novice gate — that
lockout is ThermalSight's; raw disasm @004b838c.)
- **`HandleMessage` is vtable slot 8/9 in the binary but NON-virtual across the reconstruction --
FILED AS GITEA #65
@@ -305,7 +305,7 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
- **PlayerStatus (the mission-review panels) reads the compiled player at RAW BINARY OFFSETS --
FILED AS GITEA #63 (2026-07-25, found via #48) [T1 code-read].** btl4gau3.cpp `PlayerStatus::Execute`:
`player+0x1FC` (vehicle), `+0x1C8` (score, drawn as a numeric), `+0x1C4` (alive/dead -- a
state flip draws a 160x240 filled box) -- all 1995 offsets, garbage on our compiled layout
state flip draws a 160x240 filled box) -- all original-binary offsets, garbage on our compiled layout
(the databinding trap). DORMANT in-game (the review context no longer survives the viewpoint
swap -- the #48 teardown fix) but LIVE the moment the mission-review screen actually runs at
round end. Bridge all three through btplayer.cpp accessors before enabling the review screen.
@@ -702,7 +702,7 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
- **Subsystem-panel online/offline gate — fields unidentified [T4] (2026-07-12).** The binary's
SubsystemCluster draw-state reads `*(subsystem+0x40)==1` and `*(subsystem+0x278)!=4`; neither
1995 field is identified (weapon simulationState is 0/2/3/4 -- +0x40 isn't it). The port pins
original field is identified (weapon simulationState is 0/2/3/4 -- +0x40 isn't it). The port pins
the panel ONLINE (GetDrawState()==1) because the old raw reads were layout NOISE that flapped
the state and black-filled the panel over the recharge arc (the frozen-PPC-dial bug; the
WeaponCluster repaint-heal also landed). Identify both fields to restore the authentic
@@ -948,7 +948,7 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
- **Collision-death tail fallthrough is inspection-tier [T3]:** the divert now falls to the death
tail (VehicleDead + blast) per @0x4a0375, but no live wall-death bench has run; field
wall-deaths exercise it nightly (watch for DEAD_NOTIFY on collision deaths).
- **The 1995 type-0 uninit-award bug** (documented in [[decomp-reference]] §BTPlayer table): the
- **The original type-0 uninit-award bug** (documented in [[decomp-reference]] §BTPlayer table): the
shipped binary folds an uninitialized stack float into the shooter's score on every non-lethal
hit's type-0 report. Era scoreboards looked sane — worth understanding WHY (dispatch path may
deterministically leave ~0 at [ebp-0xc]; or replicant-player reroute delivery differs). Port
@@ -979,7 +979,7 @@ readout being wrong — it is a real behavioural gap, not a cosmetic one.
(`btfx:firesmoke1_scr_tex`) — all effects share the radial-masked grit sheet; decoding FIRE.BSL
would give the authored sheet; (c) beams draw before particles, so a beam through thick smoke
reads too bright (pass-ordering); (d) `colorWarp/alphaWarp` applied as `t^(1/warp)` [T4 convention];
(e) emission uses rate×releasePeriod batching [T4 convention vs the exact 1995 emitter].
(e) emission uses rate×releasePeriod batching [T4 convention vs the exact original emitter].
- Replacement-LOD selection for NON-additive multi-LOD models (needs the board's hot-spot/
reference-point semantics; leads: `s_dplobject.lod_ranges[16]/lod_hot_spot`, 0x2047/0x2048).
- Day/night MATERIAL path priority (BMF/IMG) — infrastructure done, gated `BT_MATPRI` (over-applies
@@ -1104,7 +1104,7 @@ Trace it live with `BT_AMMO_LOG=1` (+ `BT_PROJ_LOG=1` for per-tick heat/recoil).
## Night-9 playtest intelligence (build 4.11.693, 2026-08-01)
**A MEMBER OF THE ORIGINAL TEAM IS NOW TESTING.** "Ronin" (log user `torak`,
machine TOWER_OF_TERROR) worked on the 1995 game and has played the pods
machine TOWER_OF_TERROR) worked on the original game and has played the pods
recently, including in Japan. He is a **primary source** — treat his comparisons
against the pods as high-value evidence, above reconstruction inference. Two
contributions already:
+3 -3
View File
@@ -14,7 +14,7 @@ open_questions:
# The Operator Console + Relay
The system-operator station: build a mission, run a session, watch pods arrive, launch rounds.
This is the piece the 1995 archive lost and we rebuilt.
This is the piece the original archive lost and we rebuilt.
**What survives of the original, and why so little:** the console APPLICATION is absent from every
archive (and Route A is closed). What we have is the pod's half of the conversation --
@@ -22,7 +22,7 @@ archive (and Route A is closed). What we have is the pod's half of the conversat
defining the Console* messages the game sends TO the console (MechKilled / MechDamaged /
ScoreUpdate / TeamScoreUpdate / DeathWithoutHonor / EndMission) -- plus the engine's receive-side
handlers (egg / RunMission / StopMission). Its changelog line *"06/03/95 GAH -- Added
corresponding Macintosh message definitions"* implies the 1995 sysop station was a **Macintosh
corresponding Macintosh message definitions"* implies the original sysop station was a **Macintosh
application**, a separate codebase that was never in the pod build tree -- which is why no archive
has it. `btconsole.py`/`btoperator.py` are therefore a wire-faithful RECREATION built from the
surviving parser side (+ BT_NET_PROBE against the real exe), not a port of the lost program.
@@ -292,7 +292,7 @@ no UDP fan-out, no control channel). All sends now go through `_send_all_guarded
- **Relay (internet)**`btconsole.py --relay <port> <egg>`; pods dial out. The normal mode.
- **Mesh (LAN legacy)**`btconsole.py <egg> <host:port> ...`; the console dials OUT to each pod's
`-net` port. The authentic 1995 cabinet direction.
`-net` port. The authentic original cabinet direction.
- **Remote relay / PARKED RELAY** — the GUI drives an already-running relay over the control port
instead of spawning a child. **This is a COMPLETE, SUPPORTED deployment mode** (fixed 2026-07-26 —
do not re-derive it; see §Parked relay below). The intended production shape: the relay lives
+9 -9
View File
@@ -63,7 +63,7 @@ Searchlight + Searchlight2 `ToggleLamp` (id 3, table @0x51117C fn @004b838c)**
2026-07-25 (#61)**, previously default-constructed blackholes; verified live (0x14 →
`lightState 0→1`, 0x12 → `thermalActive 0→1`). ⚠ The fn addresses were **swapped** in this file
before #61 (@004b860c is ThermalSight's) — see [[decomp-reference]] for the table evidence — and
that swap had propagated a false "1995 latent bug" (retracted, [[subsystems]] WAVE 4).
that swap had propagated a false "original latent bug" (retracted, [[subsystems]] WAVE 4).
An unhandled message is SILENTLY ignored
(Receiver::Receive finds no handler and does nothing — dead buttons produce NO log).
0x10/0x11 = map ZoomIn/Out, 0x15 = CycleDisplayMode, 0x18 = CycleControlMode (#6). Buttons
@@ -77,7 +77,7 @@ Full decode: [[gauges-hud]] §preset pages; the complete 72-button census + veri
button <addr>|axis <axis> deflect|rate <n>|keypad pilot|external <0-15>|pckey <char>|action
<name>`; `pad <button>` same targets; `padaxis <src> axis ... [invert] [deadzone d] [rate n]`.
Axes: Throttle (lever, rate-walks + sticks), LeftPedal/RightPedal/Pedals (turn, springs),
JoystickX (torso twist), **JoystickY (torso ELEVATION — pitch aim; every 1995 control mode
JoystickX (torso twist), **JoystickY (torso ELEVATION — pitch aim; every original control mode
routes it to Torso::SetAnalogElevationAxis; keys R/F + pad LeftStickY in the default map;
verified to the authentic VerticalLimitTop clamp, 20° on the Blackhawk; aims the GUNS — reads
on the HUD elevation tape, the eyepoint stays level)**.
@@ -96,7 +96,7 @@ dynamically (xinput1_4 → 9_1_0; disconnected-pad probing rate-limited to 1 Hz)
a binding are SUPPRESSED from the legacy WM_CHAR/KEYUP feed** (`BTInputSuppressKey`, hook at
L4CTRL.cpp keyboard read) — this ended the historic DOUBLE-DISPATCH: 'w' drove AND selected pilot
0; F5's key-up value 0x74 aliased to the 't' hotkey; letter key-ups (uppercase) fed the developer
fake-event dispatcher. Unbound keys keep their authentic 1995 dispatcher meaning (reachable from
fake-event dispatcher. Unbound keys keep their authentic original dispatcher meaning (reachable from
any key via `pckey`). The old dual-use 'V' (view toggle + look-behind) is split: V = ViewToggle,
B = LookBehind. Verified live: bindings load, W/NumPad8 drive (spd rises), X all-stop, aux-button
emission ([input] 0x2f PRESS), suppression both directions ('r' delivered / 'w' swallowed),
@@ -227,7 +227,7 @@ each mono CRT exactly its picture. Grouping follows `L4GAUGE.CFG`: *VGA Port A*
Mfd2, *VGA Port B* = Mfd1 + Mfd3; the radar keeps real colour on its own port.
`SVGA16::ExpandPlaneToBGRA` does the per-plane extraction, and `BT_POD_RGB` implies
`BT_POD_SURFACES`. Two companion gates came out of the bring-up: **`BT_POD_CHANMAP=<Port>=<colour>,…`**
remaps a surface's colour line when a cab's harness disagrees with the 1995 config (it did — see
remaps a surface's colour line when a cab's harness disagrees with the original config (it did — see
§ALPHA-MR), and **`BT_POD_IDENT`** / **`BT_POD_CHANTEST`** paint identifying/cycling test patterns
so you can tell which physical CRT is which without trusting the desktop layout.
**SETTLED:** Nick's cart IS splitter-wired — its three 640×480 "displays" are VGA outputs, each
@@ -235,7 +235,7 @@ feeding a three-monitor splitter, and the composite is what lit the glass correc
## MFD PANELS ON REAL HARDWARE — the bring-up path (2026-08-06) [T2 local / T4 on-pod]
Nick's crash cart (pod hardware + Chrome Remote Desktop on a burner account) is the first chance
to drive the real panels. **The 1995 display path is NOT the way in.** That rig spanned the five
to drive the real panels. **The original display path is NOT the way in.** That rig spanned the five
MFDs as ONE 1280×480 surface via **NVIDIA Horizontal Span, which every driver after XP dropped**;
`SVGA16::BuildWindows` also wants an exclusive-fullscreen D3D device per adapter, which is
fragile on modern drivers and over a remote session. Both are still in-tree and still the
@@ -341,13 +341,13 @@ colour planes (§THE RGB SPLIT). Desktop-coordinate order says NOTHING about phy
the first assignment, made by sorting monitors left-to-right, was wrong. Only eyes on the glass
settle it.
**Two deviations from the 1995 config, both real, both this cab's wiring:**
**Two deviations from the original config, both real, both this cab's wiring:**
- **`BT_POD_CHANMAP=Comm=blue,Heat=red`** — kills/deaths and coolant loops came up on each
other's panel. `L4GAUGE.CFG` has Comm on red and Heat on blue; THIS cab's splitter harness
wires them the other way. The env override remaps a port's colour channel without touching the
authentic config file. [T2]
- **`BT_GAUGE_SEC_ROT=0`** — the radar here is a LANDSCAPE LCD, so the 640×480 source needs no
rotation. The 1995 pod's 270° rotation existed because its secondary CRT was mounted PORTRAIT;
rotation. The original pod's 270° rotation existed because its secondary CRT was mounted PORTRAIT;
it is a mounting fact, not a format fact. (En route: 180° looked plausible and was upside down.)
**Frozen in `content/environ.ini`**, not in a launcher — `scratchpad/pod/podprofile.ini` is the
@@ -515,7 +515,7 @@ newest-install auto-resolve worked, no hand-edits. The cab genuinely upgrades li
second FTDI (A995JTOYA); **two ghost Prolific PL2303 entries also claim "COM1"** from earlier
plug-ins (clean them in Device Manager). When the adapter's **USB side** drops mid-operation:
brief drop = analog polls go mute (the field "encoder dropouts", revived by hand-jostle /
re-enumeration — NOT by button protocol); drop with a write in flight = the 1995 code's
re-enumeration — NOT by button protocol); drop with a write in flight = the original code's
**unbounded `GetOverlappedResult` wait in `PCSerialPacket::SendPacket` parked the whole game at
0 CPU forever** (three boots on 2026-08-10; stack: NtWaitForSingleObject ← GetOverlappedResult ←
SendPacket ← RIO ctor). **Hardened in 4.11.881**: 500 ms bounded write + CancelIo + 3 s sick
@@ -557,7 +557,7 @@ banner still says "hardware RIO" (the banner is the PROFILE, not the init result
the BT411Run task an idempotent restart — but any hand-rolled launcher on a future cab needs the
same line, and a mid-mission `/run` now RESTARTS the game rather than failing quietly.
## The 1995 player manual — alignment audit (2026-07-18) [T1, primary source]
## The original player manual — alignment audit (2026-07-18) [T1, primary source]
`reference/manual/Tesla40_BT_manual.pdf` (34pp, from Nick). CONFIRMS the reconstruction on
every checked control behavior:
- **Control modes are named BAS / MID / ADV** (basic/middle/advanced; our Basic/Standard/Veteran
+12 -3
View File
@@ -9,11 +9,20 @@ key_terms: [BT, RP, MUNGA, Tesla-platform, WinTesla, pod]
# Project Overview
Port VWE's arcade **pod** games (Tesla platform, release 4.10, ~1995-96) to modern Windows and,
Port VWE's arcade **pod** games (Tesla platform, release 4.10; 1990s, exact release year unestablished) to modern Windows and,
ultimately, the actual arcade pod hardware. Primary target: **BattleTech (BT)**; **Red Planet
(RP)** shares the engine. Full detail: `docs/PROGRESS_LOG.md §1-3`.
The ORIGINAL 1995-96 team — the initials in the source headers matched to names by VWE veterans
**Dating note (2026-08-12):** the KB previously called this "the 1995 game" — WRONG, per era
witnesses (playtesters who ran the original pods say BT 4.10 shipped later). The 1995 belief
came from the MUNGA asset-reader library RCS `$Id ... 1995/..` strings inside `BTL4OPT.EXE`,
which date the *library code*, not the game; the exe carries no game-copyright year (its PE
link timestamp is stamped 2019 — a later re-link/tool artifact, not a release date), and a
content-file timestamp of 30-Nov-1995 likewise dates assets, not the release. Until a real
date surfaces (ask Nick / era witnesses), say "the original" / "the shipped machine" — never a
year. [T2 for the library strings; T4 for any specific release year]
The ORIGINAL team — the initials in the source headers matched to names by VWE veterans
(dev team ~8, art ~6, MUNGA by a smaller internal team) — is credited in **`docs/CREDITS.md`**.
## The stack
@@ -42,7 +51,7 @@ content → `content/`, raw decomp → `reference/decomp/`). [T2]
## Current state (2026-08-06) — core gameplay reconstruction COMPLETE; polish phase
Field-tested nightly at 4-8 pods over Steam (builds 4.11.622→801). Authentic + benched: the full
SP+MP loop; per-panel mesh-true targeting/damage (+ crits, cylinder lottery, armour darkening);
the 1995 SCORING model (kill awards + received penalties + panic cost — the id-0x16 report tail);
the original SCORING model (kill awards + received penalties + panic cost — the id-0x16 report tail);
death (blast/splash, wreck, burial) + respawn (full re-arm audit); locomotion (two-channel gait,
gimp limp, knockdowns, CROUCH F4); the night kit (fog-swap SEARCHLIGHT F5 + beam cone); heat/
power/myomers (incl. the seek-4 freeze); replication (masters/replicants, kill/death columns,
+16 -16
View File
@@ -12,7 +12,7 @@ open_questions:
# 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
classes are NOT byte-identical to the original 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.
@@ -65,7 +65,7 @@ every hit is either dead code or a vanished cache.
## 3. Databinding trap — raw offsets read garbage
Our compiled layout != the 1995 binary, so `*(T*)(obj+0xNN)` reads garbage for ANY object we
Our compiled layout != the original 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
@@ -133,7 +133,7 @@ unresolved-by-design until bridged per-module ([[open-questions]]). **Rule: when
wall exists, `tail` on the build output hides the fleet — always grep the FULL log; and verify a
stub's member TYPE against `dumpbin /symbols` of the defining obj, not against what looks right.**
**Duplicate-GLOBAL corollary (glass per-display windows, 2026-07-20):** a global DEFINED in two
libs (the 1995 headers declare free globals without `inline`/`extern`, so `application`,
libs (the original headers declare free globals without `inline`/`extern`, so `application`,
`ghWnd`, … exist in BOTH `munga_engine` and `bt410_l4`) links under `/FORCE:MULTIPLE` with
per-object binding that is **non-deterministic across links** — a given `.obj` can resolve
`application` to the copy the game ASSIGNS (correct) or to the other copy (stays NULL). A
@@ -203,7 +203,7 @@ real PoweredSubsystem). Keep the alloc SIZE + special-cache when swapping a fact
always use it. Related trap: the dense handler table (Build indexes slots by id-1) leaves
GAP slots (skipped ids) as uninitialized heap -- the NAME-based `Find(const char*)`
strcmp-walks every slot and AVs on a gap's garbage entryName (the id-based Find is safe).
The 1995 binary's own tables carry the same holes. [T2]
The original binary's own tables carry the same holes. [T2]
## 10. Container-Execute must override (gauges)
@@ -242,7 +242,7 @@ Trigger: the streamed Eng-page `.CTL` dispatches subsystem msg **id 3 / 0xb** to
subsystem is shown on that MFD page; a **weapon (Emitter)** registers only PoweredSubsystem 4-8 +
MechWeapon 9-10, so id 3 (slot[2], < entryCount 10) is a gap. Clicking the glass Engineering
panel button 0x21 on an Eng page reached it live (`eip=cdcdcdcd` debug / `0x01048748` release).
**The 1995 binary has the IDENTICAL non-zeroing `new[]`** (part_002.c Build, `FUN_004022b0` =
**The original binary has the IDENTICAL non-zeroing `new[]`** (part_002.c Build, `FUN_004022b0` =
operator new) and only survived on fresh-OS-heap-zero luck (a zero slot == NullHandler ==
ignored) — a weapon receiving id 3 was *always meant to ignore it* (id 3 = a Condenser/Reservoir
action in a different subsystem branch; the Eng-page button template is uniform, so buttons for
@@ -262,7 +262,7 @@ Receive`, suspect a gap slot, not a missing symbol. [T2]
## 12. Frame-pacing trap — the binary assumes a LOCKED 60 fps (task #11)
The 1995 pod ran frame-locked; reconstructed per-frame logic can carry HIDDEN frame-rate
The original pod ran frame-locked; reconstructed per-frame logic can carry HIDDEN frame-rate
assumptions that variable dt violates. Archetype: the Emitter Loading tick — the charge
integrates toward the generator's 10000V and the Loading→Loaded transition only fires while
`rechargeLevel` crosses the ±0.01 snap window around seekV (~0.25s of travel ≈ 15 pod frames —
@@ -366,10 +366,10 @@ corrupts the metric.
canopies are open lattices. Namespace edge keys by patch identity (and remember l/r patches are
MIRRORED — winding handedness flips, so no global winding choice can be right; orient per-face).
## 16. Engine-facility drift: 2007 terrain-solids amplify 1995 per-contact physics (the MP ram one-shot)
## 16. Engine-facility drift: 2007 terrain-solids amplify original per-contact physics (the MP ram one-shot)
`Mover::StaticBounce` [T0] MUTATES `worldLinearVelocity` (`+= delta_v`, a ×(1+e) reflection) on
every call, and `ProcessCollisionList` calls it once PER CONTACTED SOLID in the frame. In the 1995
every call, and `ProcessCollisionList` calls it once PER CONTACTED SOLID in the frame. In the original
binary the ground was a heightfield probe (FUN_0040e5f0 lineage) — never a collision-list entry —
so a mech's list held ~one solid and the mutation was harmless. The 2007 WinTesla engine models
TERRAIN AS COLLISION SOLIDS: a mech touching ground + rock + another mech reflects 2-4× in ONE
@@ -387,7 +387,7 @@ one-shots (mp_a.log:32651, 2026-07-12: a pristine mech killed by a walking bump)
~13-62), CONSTANT repeated values (a stable grind oscillation), or spikes scaling with how many
solids surround the contact. Damage = `0.0005 × (1e²) × impact² × moverMass` [T0 MOVER.cpp] —
invert it to read the implied impact speed; >100 m/s means amplified/garbage velocity, not motion.
- **Class rule:** when a 1995 per-event computation reads MUTABLE engine state, audit what ELSE the
- **Class rule:** when an original per-event computation reads MUTABLE engine state, audit what ELSE the
2007 engine feeds that state within the same event batch. (Family of gotcha #12's frame-pacing
trap: the binary's physics assumed its own engine's event granularity.)
@@ -435,7 +435,7 @@ DontExecute). **Rule:** zero every hand-built resource struct; never rely on a p
the tester's `'\'` dev key instantly "crashed the game". **Cause:** `Entity::Dispatch`
(ENTITY.cpp:236) WRITES `entityID` + `interestZoneID` into the incoming message at
`Entity::Message` offsets before routing. A bare `ReceiverDataMessageOf<T>` (Receiver-sized) on
the stack gets written PAST ITS END → the caller's frame is corrupted → wild jump. The 1995
the stack gets written PAST ITS END → the caller's frame is corrupted → wild jump. The original
binary performs the identical overwrite and survived on stack-layout luck. Subsystem/Receiver
`Dispatch` does NOT stamp — only the Entity level. **Rule:** any message dispatched at an
`Entity` must have `Entity::Message`-sized backing (derive from `Entity::Message`, or placement-
@@ -554,7 +554,7 @@ The Ghidra export (`reference/decomp/`) has coverage gaps (#60), and BTL4.RES co
appears in it at all — so "no readers", "no caller", "no such clip/asset", and "unreconstructed"
claims made by grepping the export or the port ALONE are structurally unsound. This class has
produced four wrong conclusions, two of them [T1]-tagged at the time:
1. **2026-07-25 searchlight "1995 latent bug"** — compared Searchlight's Performance against
1. **2026-07-25 searchlight "original latent bug"** — compared Searchlight's Performance against
THERMALSIGHT's handler (wrong class) and invented a missing bridge; retracted (#61).
2. **2026-07-2x `DuckState` "has no CODE consumer, authentic [T1]" + "no SQUAT clip survives"**
the consumer was the un-exported master-perf posture block (@0x4aa011), and the clips lived in
@@ -596,7 +596,7 @@ was never a regression -- it shipped with the feature.)
**The bug class:** a piece of state is installed around a resource load, mutates the load's
*output*, and is then torn down — so any LATER re-load of that resource silently produces a
different (wrong) result. Our port re-loads things the 1995 engine loaded once, which is
different (wrong) result. Our port re-loads things the original engine loaded once, which is
exactly where this bites.
**The archetype — mech paint.** The per-pilot colour/badge/patch is applied by REWRITING
@@ -755,7 +755,7 @@ treat the disagreement as a hole in YOUR evidence first, not in theirs.
## §22 — The SPLIT CELL: one binary offset, two port members (only one gets written)
**(2026-07-31, gitea #86 "destroyed weapons keep firing".)** The binary's weapon fire gates test
`subsystem+0x40`. In the 1995 layout that offset is *inside* the embedded status alarm
`subsystem+0x40`. In the original layout that offset is *inside* the embedded status alarm
(`statusAlarm@0x2C` + the indicator's level at `+0x14` = `0x40`) — **one cell**, written by
`ForceCriticalFailure` when a zone's crit-cascade kills the subsystem. The port models the same
address as TWO independent members: `AlarmIndicator statusAlarm` **and** a plain
@@ -773,13 +773,13 @@ base field): same failure shape — two cells where the binary has one, and the
dead one.
## §23 — Scaling `D3DMATERIAL9` does NOTHING for BGF geometry (the vertex-colour diffuse source)
**(2026-07-31, gitea #87 "mech armour panels don't darken".)** The 1995 armour damage darkens a
**(2026-07-31, gitea #87 "mech armour panels don't darken".)** The original armour damage darkens a
mech by scaling its MATERIALS' colour terms (`dpl_SetMaterialAmbient/Emissive/Diffuse/Specular`).
The obvious port translation — scale the draw op's `D3DMATERIAL9` before `SetMaterial` — renders
**byte-identically**: measured **0 changed pixels** at a full 0.1x scale.
**Why:** D3D9 defaults `D3DRS_DIFFUSEMATERIALSOURCE` to **`D3DMCS_COLOR1`** (and `D3DRS_COLORVERTEX`
to TRUE), so when a vertex carries a diffuse colour the material's Diffuse is never consulted.
Every BGF vertex carries a baked colour (that IS the 1995 shading model — no-normal geometry is
Every BGF vertex carries a baked colour (that IS the original shading model — no-normal geometry is
unlit and coloured by vertex/ramp), so for essentially all world geometry `SetMaterial` is inert
for colour. The material only matters for lit, normal-bearing meshes.
**Fix pattern:** modulate the FINAL fragment instead — `D3DRS_TEXTUREFACTOR` + a
@@ -876,7 +876,7 @@ whole segment table marking every entry dirty, then clears the joint flag.
The binary agrees exactly. `MechWeapon::GetMuzzlePoint @004b9948` ends in
`FUN_00424da8(owner, segment, out)`, which IS `GetSegmentToWorld`
instruction-for-instruction (`GetJointSubsystem``if (AreJointsModified())`
mark all → `ModifyJoints(False)``× localToWorld`). **So in the 1995 image
mark all → `ModifyJoints(False)``× localToWorld`). **So in the original image
every muzzle query performs the joints→segments refresh.** [T1]
Four port sites had replaced that with `mw.Multiply(seg->GetSegmentToEntity(),
+5 -5
View File
@@ -73,7 +73,7 @@ DECLOUDS must stay in the sky pass). Authored TEXTURE **SCROLL** (BMF TEXTURE ta
`SetFogStyle(searchLightOn/OffFogStyle)`). TWO independent reasons it never fires:
(1) **Port:** `PullFogRenderable` is NEVER CONSTRUCTED (grep: only its .h decl + .cpp def; no `new`);
`searchLightOn/Off` is called from nowhere else (BT_FOG_LOG: over 22s only `SetFogStyle(0)`, zero 2/3).
(2) ~~**Original 1995 LATENT BUG**~~ — ❌ **RETRACTED 2026-07-25 (#61). There is no 1995 bug.** The
(2) ~~**Original LATENT BUG**~~ — ❌ **RETRACTED 2026-07-25 (#61). There is no original-binary bug.** The
claim rested on a **swapped table attribution**: @004b860c is **ThermalSight's** `ToggleLamp`
(table @0x51120C), not Searchlight's, so pitting its `0x1DC` write against Searchlight's
`SearchlightSimulation` (@004b841c, reads `requestedOn`@0x1E0) compared two different classes and
@@ -154,7 +154,7 @@ The per-weapon impact visuals are fully AUTHORED in `content/BTDPL.INI` `[effect
CannonHit/GroundHit/MinorHit/MajordHit/Fireball — "old-style" (version-1) descriptor sections
(`texture/type/size/velocity/v_bias/y_off/cook(3f)/varience/gravity/cool(2f)/count/repeats`),
most on `btfx:firesmoke1_scr_tex` (MissileHit size 12 ×7; Fireball size 14, gravity 1.5 rises;
Chunks1/2 = ballistic debris vel 40-55 grav 34-35; Sparks NOTEX). [T1: the 1995 binary parses
Chunks1/2 = ballistic debris vel 40-55 grav 34-35; Sparks NOTEX). [T1: the original binary parses
them — part_008.c:1546-1590, field strings @004f25e4-004f2663 → installer FUN_00490210 packs a
0x44-byte board command via FUN_0048e2c0.] **Simulation semantics (type/cook/cool) live in the
i860 firmware** `content/VREND.MNG` (same decode workflow as the task-#55 'damageize' handler).
@@ -331,7 +331,7 @@ A mech's armour panels **darken as their damage zone takes damage** — a per-zo
colour ramp-down, NOT a texture swap, a filter, or geometry. Players reported the port showing only
the limb swap ("the actual enemy mech in external view is not showing darkened armor panels").
**The 1995 chain** [T1]:
**The original chain** [T1]:
- **`.DZM`** (`VIDEO\<mech>SKIN.DZM`, 40 shipped) lists, per damage zone, the materials that zone
paints: `[dz_ltorso] material=avaskin:avat2_dz_ltorso_mtl`. One file per SKIN VARIANT — the
`dzm`/`dzms`/`dzmt`/`dzmo`/`dzma`/`dzmb`/`dzmc`/`dzmd` key family (same suffix set as the
@@ -400,7 +400,7 @@ the VISUALS were the gap, decoded from MakeMechRenderables @004cef28 `case 0xbd8
reads as a modest push-back of the night murk at mid/long range, NOT a flashlight. Geometry
brightness comes from the page's ambient= + ramps and never changes with the lamp -- a close
wall is identical lit/unlit. First eyeball matched exactly ("kinda sorta illuminates the
area... didn't illuminate the wall" -- that IS the 1995 behavior). No illuminating dynamic
area... didn't illuminate the wall" -- that IS the original behavior). No illuminating dynamic
light exists anywhere in BT's render path: ChildLightRenderable has ZERO instantiations
engine-wide, and the FUN_0048d060 cluster (@4568f8/@456a0c/@456b4c/@4586d0) is light-POINT
sprites (beacons/nav strobes, palette-tinted), not geometry lights. Set player expectations
@@ -409,7 +409,7 @@ the VISUALS were the gap, decoded from MakeMechRenderables @004cef28 `case 0xbd8
the searchlight SITE joint (mount = subsystem `mountSegment`@0x1DC == resource segmentIndex;
madcat 20, loki 21), shown/hidden per frame from the replicated LightOn (@0045612c watcher ->
the SetDrawObj swap). Site segments now build geometry-less DCS children (posed + parentable --
the 1995 graph gave every site a DCS node; our tree used to skip sites entirely; this also
the original graph gave every site a DCS node; our tree used to skip sites entirely; this also
enables future site-hung effects). Wrecks go dark through the authentic gate (host shutdown
forces lightState 0). Cross-node verified: B presses, A logs `[spot] cone SHOWN (seg 20)`.
- Benches: `scratchpad/night12/searchfog.sh` (solo cockpit chain + red-fog end-to-end probe),
+5 -5
View File
@@ -6,7 +6,7 @@ source_sections: "PROGRESS_LOG.md §5a; docs/BT_SOURCE_STATUS.md"
related_topics: [wintesla-port, reconstruction-method, decomp-reference, open-questions]
key_terms: [BTL4OPT, decomp, oracle, WinTesla, BT]
open_questions:
- "Backfill the missing 1995 engine headers/TUs (vdata.hpp first) by back-dating the WinTesla/BT412 versions — the compile-closure blocker"
- "Backfill the missing original engine headers/TUs (vdata.hpp first) by back-dating the WinTesla/BT412 versions — the compile-closure blocker"
---
# Source Completeness — the gating blocker
@@ -20,7 +20,7 @@ This is THE gating fact that shapes the whole project. Full map: `docs/BT_SOURCE
`.cpp` (PLAYER, NETWORK, GRAPH2D, OBJSTRM, SCNROLE, EXPLODE, L4SPLR, L4APP + ~15 smaller) AND at
least one missing engine HEADER (`vdata.hpp`, included by plug.hpp — exists only as the drifted
WinTesla/BT412 `VDATA.h`). `CODE/BT/MUNGA` is a PARTIAL BT-drift overlay (69 files) over the
fuller `CODE/RP/MUNGA` (305); a 1995 build needs the merged BT-over-RP view. [T1]
fuller `CODE/RP/MUNGA` (305); an original-era build needs the merged BT-over-RP view. [T1]
- **BT game `.hpp` headers: PARTIAL, not complete** (2026-07-19 correction — the old "all headers
present" claim was WRONG): only 17 of ~36 bt-side `.hpp` survive (mech.hpp, mech2-4, heat.hpp,
mechmppr.hpp, torso.hpp, gyro.hpp, hud.hpp, myomers.hpp, powersub.hpp, btplayer.hpp are
@@ -78,11 +78,11 @@ projtile, missile); toolchain = **Borland BCC/tlib/tlink32** (from the makefiles
targets** for future drive dumps. Full log: `phases/phase-03-bt410-source-manifest.md`.
## The literal 4.10 tree — `TeslaRel410/restoration/source410/` (started 2026-07-18)
The archival-grade recreation of the missing TUs in 1995 house style against the surviving
1995 engine headers (distinct from OUR WinTesla port). Rules + status in its README; every
The archival-grade recreation of the missing TUs in the original house style against the surviving
original engine headers (distinct from OUR WinTesla port). Rules + status in its README; every
file carries a `.NOTES.md` evidence sidecar; binary `Fail()` file:line records are treated
as LINE CONSTRAINTS. **Pilot complete: `BT_L4/BTL4APP.CPP`** — 12/12 functions, all decomp
constants resolved to named 1995 expressions (`ApplicationID` BTL4=1 + an unreleased `BTW4`
constants resolved to named original expressions (`ApplicationID` BTL4=1 + an unreleased `BTW4`
slot in APPMSG.HPP; `ModeInitial`=0x50421; `AddModeMask` inlines byte-exact), Fail lands on
its recorded line 400 exactly. [T1/T3 per the sidecar]
+3 -3
View File
@@ -48,7 +48,7 @@ Making a base byte-exact GROWS every subclass — they must be re-based TOGETHER
default-constructed blackholes (systemic cause #1, `docs/INPUT_PATH_AUDIT.md`). Each now chains
`PowerWatcher::GetMessageHandlers()` with its own id-3 `ToggleLamp`. **Verified live**: pad `0x14`
`requestedOn 0→1``lightState 0→1`; pad `0x12``requestedOn 0→1``thermalActive 0→1`.
- ❌ **RETRACTED: the "ORIGINAL 1995 latent bug" (old task #63) NEVER EXISTED.** It was an artifact of
- ❌ **RETRACTED: the "ORIGINAL latent bug" (old task #63) NEVER EXISTED.** It was an artifact of
a **swapped table attribution**: the claim compared Searchlight's Performance (@004b841c, reads
`requestedOn`@0x1E0) against **ThermalSight's** `ToggleLamp` (@004b860c, toggles `0x1DC`) and
concluded "no 0x1DC→0x1E0 bridge, so the lamp can never light". @004b860c is not Searchlight's.
@@ -131,7 +131,7 @@ Making a base byte-exact GROWS every subclass — they must be re-based TOGETHER
thermalMass=2.5e5. Live-verified (solo arena, BT_MYO_LOG): standstill ≈ 0 generation, hard
circling drove T 77→1225 (past degradation) and the coolant loop pulled it back to ~520
equilibrium on slowing. ⚠ Two carried caveats: (1) the kinetic work term has **no dt** in the
binary (per-tick accumulate, a 1995 fixed-frame assumption) → heat/s scales mildly with frame
binary (per-tick accumulate, an original fixed-frame assumption) → heat/s scales mildly with frame
rate; kept verbatim, field-calibrate against pod veterans. (2) the climb term is wired but
inert: `Mover::localEnvironment` is never populated in the port (EnvironmentZone res type 23
unwired), so g reads 0 — see [[open-questions]].
@@ -160,7 +160,7 @@ Making a base byte-exact GROWS every subclass — they must be re-based TOGETHER
thermal curve and zone damage; `@004b8ceb` run the inner integrator **only when `outputVoltage > 0`**.
Verified live: healthy `outV=10000 speed=1`, un-powered `outV=0 speed=0`, and 96/96 torso samples at
`elec=4` across two death/respawn cycles.
✅ **CORRECTED 2026-07-29 (#80): `@004b8bb9` — the un-powered self-repair — is LIVE, in 1995 and
✅ **CORRECTED 2026-07-29 (#80): `@004b8bb9` — the un-powered self-repair — is LIVE, in the original and
now in the port.** The 2026-07-28 "dead code in the original too" verdict here was wrong about the
original: the frozen-at-0.6 experiment was measuring a PORT bug (the subsystem ctor wrote the
zone's armour/scales through the `ReconDamageZone` proxy at struct offsets +4/+8 instead of the
+2 -2
View File
@@ -12,7 +12,7 @@ open_questions:
# Translocation warp (the respawn vortex)
The "blue whirlwind" respawn effect from the 1995 pod: on death the pilot is sucked into a spinning
The "blue whirlwind" respawn effect from the original pod: on death the pilot is sucked into a spinning
energy vortex; on respawn it blows outward and the reborn mech's world snaps back. VWE calls it
**translocate** (not "whirlwind" — that keyword search is why an earlier scout wrongly declared it
"not a shipped asset"). It is a pure **render-side** effect watching the player's `SimulationState`
@@ -37,7 +37,7 @@ The loader **doubles** every tri (fwd `a b c` + rev `a c b`) → 80 tris (`emitT
`tessellateWarpCone()` subdivides 4:1 × `BT_WARP_TESS` (default 3) projecting each new midpoint onto
the true cone surface (radius `10.5*(1-|z|/21)`) → ~5120 smooth tris. Untessellated, the 12 flat
facets read as a "12-sided funnel"; tessellation is what makes the swirl smooth on a modern display.
(The 1995 low-res CRT + phosphor + fast spin hid the facets for free.)
(The original low-res CRT + phosphor + fast spin hid the facets for free.)
## The material — `btfx:tsphere_mtl` (in `VIDEO/MAT/<tod>/BTFX.BMF`) [T1]
`tsphere_mtl` → texture **`tsphere_scr_tex`** (a scrolling texture, SPECIAL `SCROLL 0.0 0.0 0.1 0.5`) →
+2 -2
View File
@@ -50,7 +50,7 @@ on top of it. Full detail: `docs/PROGRESS_LOG.md §5b, §8`.
`alDeleteSources`) is working. A dropped voice there competes with ~256 already sounding, so it is
very likely sub-perceptual — treat the log noise as a symptom to watch, **not** as the old bug.
Mechanism if it ever does matter: `alcCreateContext(device, NULL)` passes no attribute list, so
OpenAL Soft applies its DEFAULT 256 mono sources (an OpenAL default, unrelated to the 1995 audio
OpenAL Soft applies its DEFAULT 256 mono sources (an OpenAL default, unrelated to the original audio
hardware), while a busy match logs ~7200 explosions × 3 `DPLIndependantEffect` voices.
**Raising the cap is deliberately OPT-IN (`BT_AUDIO_SOURCES=<n>`, no rebuild needed)** — the 256
ceiling doubles as a **governor**. The steal loop only steals when the incoming source outranks a
@@ -224,7 +224,7 @@ on top of it. Full detail: `docs/PROGRESS_LOG.md §5b, §8`.
(3) Failed acquisitions stranded partial sets forever — dropped transients are never released by
anything (the dtor didn't release; maintenance only touches running sources). Fix: handback via
`ReleaseChannels()` at both failure sites (StartRequest + dormant-resume) + a dtor backstop.
Context: the port's `RequestAudioChannels` has NO channel budget (the 1995 per-card budget is
Context: the port's `RequestAudioChannels` has NO channel budget (the original per-card budget is
the commented-out block) — it alGenSources until OpenAL Soft's ~256 cap; on failure a transient
is silently dropped (StartRequest early-return), a sustained goes dormant — which is exactly the
"cutting in and out" presentation. Priority-steal (`RequestAudioResources`) only engages at cap.
+2 -2
View File
@@ -114,7 +114,7 @@ ALSO (#152 rider): did Mid/Adv auto-recentre the torso in the pod, or only
-----------------------------------------------------------------------------
D. DESIGN CALLS -- group opinions wanted
-----------------------------------------------------------------------------
#151 Panic eject costs -499: verified REAL 1995 scoring (your death scored
#151 Panic eject costs -499: verified REAL original scoring (your death scored
as suicide + death penalty). Keep authentic, or house-rule a flat cost?
#23 Non-glass screen layout: Oracle proposes widescreen-aware sizing --
world view wider + taller, moved up, allowed to overlap the radar.
@@ -147,7 +147,7 @@ F. INTERNAL ENGINEERING BACKLOG (nothing to test)
#14 Glass panel unreconstructed button handler backlog (census)
#17 Cavern upper-butte halves lack a collision stream (authenticity TBD)
#28 Confirm the vital-subsystem-crit death path can fire (machinery in)
#63 PlayerStatus reads raw 1995 offsets (databinding trap, dormant)
#63 PlayerStatus reads raw original-binary offsets (databinding trap, dormant)
#64 damageZone shadow field (half-fixed; consequences disarmed)
#65 HandleMessage non-virtual across 10 classes (latent dispatch trap)
#72 BTLoadPump swallows WM_QUIT during load (latent)
+3 -3
View File
@@ -26,7 +26,7 @@ L4:
BT:
kind: concept
definition: BattleTech — the primary port target. VWE arcade pod game, Tesla platform release 4.10 (~1995-96). Game logic = CODE/BT (mostly MISSING as source; reconstructed from the binary).
definition: BattleTech — the primary port target. VWE arcade pod game, Tesla platform release 4.10 (1990s; exact release year unestablished). Game logic = CODE/BT (mostly MISSING as source; reconstructed from the binary).
related_terms: [RP, MUNGA, BTL4OPT, source-completeness]
related_topics: [project-overview, source-completeness]
@@ -344,7 +344,7 @@ shadow-field:
databinding-trap:
kind: concept
definition: Our compiled class layout != the 1995 binary, so a RAW offset read *(T*)(obj+0xNN) reads garbage. Use compiled named members / accessors / a bridge fn in a complete-type TU. The systemic reason raw-offset reads fail.
definition: Our compiled class layout != the original binary, so a RAW offset read *(T*)(obj+0xNN) reads garbage. Use compiled named members / accessors / a bridge fn in a complete-type TU. The systemic reason raw-offset reads fail.
related_terms: [shadow-field, bridge, Wword-trap]
related_topics: [reconstruction-gotchas]
@@ -422,7 +422,7 @@ TakeDamage:
ground-model:
kind: concept
definition: The authentic 1995 vertical-position model (FUN_004a9b5c). NO GRAVITY — an ABSOLUTE per-frame ground SNAP via the zone BoundingBoxTree (FindBoundingBoxUnder), lift = 5% of volume X-width. Walls block by FULL FRAME REJECTION; crushable props pass with a crunch.
definition: The authentic original vertical-position model (FUN_004a9b5c). NO GRAVITY — an ABSOLUTE per-frame ground SNAP via the zone BoundingBoxTree (FindBoundingBoxUnder), lift = 5% of volume X-width. Walls block by FULL FRAME REJECTION; crushable props pass with a crunch.
related_terms: [SLD, BoundingBoxTree, MoveAndCollide]
related_topics: [locomotion]