KB: EXPERIENCE LEVELS decoded -- the 0x25c block is the simulation-mode flags

The egg's per-pilot 'experience' (novice/standard/veteran/expert) is the
pod's simulation-fidelity tier -- stored at BTMission+0xe4 and seeded into
the BTPlayer ctor's flag block @4c0bc8 [T1].  The old 'per-role display
toggles / returnFromDeath' reading was WRONG: the source is
mission->experienceLevel, and the neighboring pair copies
mission->advancedDamageOn (the egg's technician splash/collision switch),
not role floats.  Known consumers: 0x260 = the HEAT-MODEL master switch
(FUN_004ad7d4), 0x25c = the novice sim-lockout (jams/searchlight/powersub),
0x274 = the raw level (FUN_004ac9c8 ==0 novice predicate).

NEW established topic context/experience-levels.md (data path, binary flag
rows per level, manual cross-refs, corrections log) + router row; sweeps in
decomp-reference/gauges-hud/open-questions/pod-hardware/subsystems.
btplayer.cpp/.hpp re-annotated with the corrected semantics; the guarded
role-based seeding stays as a marked [T3] stand-in (slot drift vs the
binary switch documented inline) until the wiring task points it at
BTMission::ExperienceLevel().

(Research by the parallel context session; committed from the glass line.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-18 22:45:37 -05:00
co-authored by Claude Fable 5
parent 171c993147
commit 4e01e83563
12 changed files with 51391 additions and 54 deletions
+1
View File
@@ -67,6 +67,7 @@ precise than anything you can infer.
| Walking, gait, ground model, collision | `context/locomotion.md` |
| Subsystems, the factory, heat/weapons/power | `context/subsystems.md` |
| Damage zones, targeting, firing, death | `context/combat-damage.md` |
| Experience / simulation modes (novice/standard/veteran/expert), heat-model + novice gates | `context/experience-levels.md` |
| Rendering, LODs, materials, sky, shadows, beams | `context/rendering.md` |
| The death/respawn translocation warp (tsphere vortex) | `context/translocation-warp.md` |
| First-person cockpit canopy (*_cop) + the eyepoint camera | `context/cockpit-view.md` |
+15 -1
View File
@@ -113,7 +113,7 @@ valid for reading the decomp, NOT for our compiled layout (see [[reconstruction-
|---|---|---|
| +0x11c / +0x120 | `damageZoneCount` / `damageZones[]` | inherited Entity; Mech ctor populates the array |
| +0x124 / +0x128 | `subsystemCount` / `subsystemArray[]` | the [[subsystem-roster]] (NOT the segment table) |
| +0x190 | **owning `BTPlayer`** (`GetPlayerLink()`) | set by `FUN_0049f624` (mech↔player bind; also `player->playerVehicle(+0x1fc)=mech`). The valve guard (`FUN_004ac9c8 →player+0x274`) + Myomers gate (`FUN_004ad7d4 →player+0x260`) read the owning player's game-mode flags (NOT the messmgr @0x434). `MECH_OWNING_PLAYER`. |
| +0x190 | **owning `BTPlayer`** (`GetPlayerLink()`) | set by `FUN_0049f624` (mech↔player bind; also `player->playerVehicle(+0x1fc)=mech`). The valve guard (`FUN_004ac9c8 →player+0x274` = novice lockout) + heat-model gate (`FUN_004ad7d4 →player+0x260` = veteran/expert) read the owning player's EXPERIENCE flags — see [[experience-levels]] + the BTPlayer flag block below. `MECH_OWNING_PLAYER`. |
| +0x434 (word 0x10d) | `SubsystemMessageManager` cache | the 0xBD3 damage/explosion hub (factory @10142). ✅ task #7 (`afefaee`): `mech.hpp` names this `messageManager`; the `controlsMapper` mislabel is swept — the real mapper is roster slot 0. |
| +0x260 / +0x26c | `localOrigin` = { Point3D linearPosition; Quaternion angularPosition } | ORIGIN.h:15; position@+0x100 in some subsystem views |
| +0x300 | segment table | skeleton `EntitySegment`s (muzzle sites, joints) |
@@ -128,6 +128,20 @@ valid for reading the decomp, NOT for our compiled layout (see [[reconstruction-
`mech+0x100` (a subsystem-view of localOrigin.linearPosition): `.x@+0x100, .z@+0x108`. [T1]
### BTPlayer (0x294) experience/game-mode flag block (ctor @4c0bc8; see [[experience-levels]]) [T1]
Seeded on the master branch from `btMission(+0x1f8)->experienceLevel(+0xe4)` (egg `experience`,
parse @4d2f3e`BTMission+0xe4`) and `advancedDamageOn(+0xf0)`:
| Offset | Value by level (nov/std/vet/exp) | Verified readers |
|---|---|---|
| +0x25c | 0/1/1/1 — "sim live" (novice lockout) | jam roll @4bbfcc, searchlight ToggleLamp @4b860c, powersub handler @4b0efc, mech-ineffective eval part_012.c:9364 |
| +0x260 | 0/0/1/1 — **heat-model master switch** | `FUN_004ad7d4` → Myomers @4b8d18, Emitter, heat.cpp, mislanch, projweap heat |
| +0x264/+0x268 | = mission `advancedDamageOn` (both) | heat-family sim part_013.c:8757 |
| +0x26c | 0/1/1/0 | none located (supercharge-clamp hypothesis [T4]) |
| +0x270 | 0/1/1/1 | none located |
| +0x274 | raw level 0..3 | `FUN_004ac9c8` = (==0) novice predicate (valve guard), powersub @4b21d0 |
### Binary ATTRIBUTE TABLES (2026-07-16, walked from the image) [T1]
**Row format: 16-byte `{id, namePtr, memberOffset+1, 0}`** (MIND THE ALIGNMENT — an 8-off walk
+118
View File
@@ -0,0 +1,118 @@
---
id: experience-levels
title: "Experience levels / simulation modes — novice, standard, veteran, expert"
status: established
source_sections: "decomp part_013.c:10816-10871 (@4c0bc8), part_014.c:7830-7900 (@4d2f3e), FUN_004ad7d4/FUN_004ac9c8/FUN_004bbfcc; BTMSSN.HPP (survived); Tesla40_BT_manual.pdf pp.7,11-14,21,22,24"
related_topics: [subsystems, combat-damage, gauges-hud, pod-hardware, multiplayer, decomp-reference, open-questions]
key_terms: [master, replicant]
open_questions:
- "player+0x26c (std+vet only) and +0x270 (all but novice) consumers not located — 0x26c hypothesis: myomer supercharge clamp (manual p22) [T4]"
- "does a remote pilot's experience matter cross-pod? flags are computed only on the master branch of the BTPlayer ctor [T4]"
- "movement-heat magnitude veteran vs expert (4.0 manual says expert-only; 4.10 gate is vet+expert) [T4]"
---
# 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
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).
## The data path [T1]
1. **Egg**: pilot page key `experience` ∈ {`novice`,`standard`,`veteran`,`expert`}; required unless
`vehicle=camera`; unknown value = hard abort. Parse @4d2f3e (part_014.c:7830-7900, strings
@51e6f1); `tools/eggmodel.py` knows the value set.
2. **BTMission** (`BTMSSN.HPP` survived, T0): stored at `mission+0xe4` (`experienceLevel`, accessor
`ExperienceLevel()`); the neighboring egg key `advancedDamage``mission+0xf0`
(`advancedDamageOn`) — the separate site-technician splash/collision switch (manual p13).
3. **BTPlayer ctor @4c0bc8** (part_013.c:10816-10871): on the MASTER branch
(`(simulationFlags & 0xc) != 4`; the replicant branch instead swaps in the console-update
Performance) it reads `btMission(+0x1f8)->experienceLevel(+0xe4)` and fans it out:
| egg value | +0x25c | +0x260 | +0x26c | +0x270 | +0x274 (raw) |
|---|---|---|---|---|---|
| novice | 0 | 0 | 0 | 0 | 0 |
| standard | 1 | 0 | 1 | 1 | 1 |
| veteran | 1 | 1 | 1 | 1 | 2 |
| expert | 1 | 1 | 0 | 1 | 3 |
then unconditionally `+0x264 = +0x268 = mission->advancedDamageOn(+0xf0)` (both copies). [T1]
## What each flag gates (binary consumers, all reached via `mech+0x190` → BTPlayer) [T1]
- **+0x25c — "sim live", off only for novice.** Consumers: ballistic jam roll `CheckForJam`
@4bbfcc early-returns NO-JAM when 0 (projweap.cpp calls this `LiveFireEnabled`); Searchlight
`ToggleLamp` @4b860c (searchlight.cpp `ControlsAllowLights`); PoweredSubsystem message handler
@4b0efc (powersub.cpp's "message-manager short-event flag" — same flag, misattributed); the mech
combat-ineffective evaluation (part_012.c:9364: mech state ∈ {3,4} && novice is an extra OR-term
setting `mech+0x414`). The `#if 0` block in surviving PPC.CPP:63-95 (novice PPCs deal no damage)
shows the original intent of the tier.
- **+0x260 — the HEAT-MODEL master switch, ON for veteran+expert.** This is `FUN_004ad7d4`
(`*(*(sub+0xD0)+0x190)+0x260`), the single most-called gate in the subsystem family: Myomers
work→heat (movement heat, MyomersSimulation @4b8d18), Emitter/energy-weapon firing heat, missile
launch heat, the heat.cpp simulation, projectile-weapon heat, and the heat-scaled part of the jam
chance. Standard mode has NO heat consequences; that is authentic, not a port gap.
- **+0x274 — the raw level.** `FUN_004ac9c8` = (level == 0), the NOVICE lockout used as the
condenser-valve guard ([[gauges-hud]] task #13) and by the PoweredSubsystem handler @4b21d0
(not-novice gate). The port's `BTPlayerRoleLocksAdvanced` bridge implements this predicate
(correct behavior, wrong seeding — see Port state).
- **+0x264/+0x268 — advancedDamage copies.** Read in the heat-family sim (part_013.c:8757).
Independent of experience.
- **+0x26c / +0x270 — consumers NOT located** via the mech→player hop. 0x26c (standard+veteran
only) best fits the manual-p22 myomer supercharge clamp (standard locked, expert unlocked) [T4].
## Player-facing behavior (Tesla 4.0 manual) [T1 primary source]
Manual pp. 7, 11-14, 21, 22, 24 (the dedicated Simulation Modes page, manual p2, is MISSING from
the `Tesla40_BT_manual.pdf` scan along with p3):
- **Standard**: armor-only damage — no criticals, no generator outages, no jams, no heat concerns;
MechTech limited to the leg-damage audio warning; viewscreen hunting aids (red hot box + guide
arrow, 60° FOV) on; myomer supercharge locked.
- **Veteran**: critical hits (any hit can go internal; likelihood rises as armor thins; a crit can
kill the mech), generators take damage/go offline (reroute power), ballistic jams (manual
clearing), full MechTech audio + smart buttons; viewscreen hunting aids OFF (radar hot box stays).
- **Expert**: veteran + heat as a core mechanic: movement generates heat (accel/decel worst),
heat slows recharge/top speed, incoming fire causes coolant leaks, ammo-bay fires/cook-off;
myomer supercharge allowed.
- Scoring identical in all modes (p4). Splash/collision damage = a separate technician setting.
## 4.0 → 4.10 drift [T1]
- **The viewscreen hunting-aid gate is GONE in 4.10**: part_014 (HUD/btl4vid TU) reads none of the
player flags — the target hotbox/edge arrows (@4cdf6f) are gated on lock state only, every mode.
Fits the 4.10 arcade-cities positioning.
- **Movement heat is veteran+expert in 4.10** (the +0x260 gate), not expert-only as the 4.0 manual
states. Whether a coefficient still differentiates them is open.
## Port state (as of 2026-07-18) — wiring needed [T3]
The reconstruction met this system piecemeal and stubbed the same gates inconsistently:
- `btplayer.cpp` ctor seeds `roleClassIndex` from `scenarioRole->GetReturnFromDeath()` (default 2)
— the binary seeds it from `btMission->ExperienceLevel()`. Bring-up therefore runs as VETERAN.
Two slot errors in the port's switch vs the binary: the binary's switch never touches +0x264
(that's advancedDamage, set after), and expert's `0` lands on +0x26c, not +0x270.
- `HeatModelActive()` stubbed permissive `return 1` (emitter/heat/mislanch/projweap) while
`Myomers::OwnerAdvancedDamage()` returns `False` — the SAME binary gate (+0x260) answered both
ways: heat accrues from weapons but never from movement, and no experience below veteran can
switch it off.
- `LiveFireEnabled` / `ControlsAllowLights` / powersub's short-event flag = the +0x25c not-novice
flag under three names.
- **The fix**: one accessor family on BTPlayer named members (databinding trap — never raw-read
`player+0x260`), seeded from `BTMission::ExperienceLevel()` + `AdvancedDamageOn()`, with all five
stub sites pointed at it; then an egg `experience` line actually selects the sim tier as shipped.
## Corrections (swept 2026-07-18)
- `btplayer.hpp` "per-role display toggles from returnFromDeath" for 0x25c-0x274 was WRONG — the
source is `btMission+0xe4/+0xf0`, not the role; the `showKills`/`roleClassIndex` names date from
the scoring work and don't match the gate semantics (kept in code for now, comments corrected).
- `gauges-hud.md` "ROOKIE-role lockout" for FUN_004ac9c8 → novice-EXPERIENCE lockout.
- `open-questions.md` "true semantic of player+0x260/0x274 not yet pinned" → pinned here.
## Key Relationships
- Feeds: [[subsystems]] (the heat model + jam/valve gates) · [[combat-damage]] (criticals tier intent) · [[gauges-hud]] (valve guard)
- Sourced from: the egg ([[multiplayer]] egg format) · `BTMission` (BTMSSN.HPP, T0)
- Offsets: [[decomp-reference]] §3 (mech+0x190 row + BTPlayer flag block)
- Manual audit: [[pod-hardware]] §Manual
+5 -4
View File
@@ -150,10 +150,11 @@ moves. Diag: `BT_RADAR_LOG` ([radar] ctor probe + 1 Hz [radar-wedge] rot trace).
## Remaining = DATA FEEDS, not widgets (deferred systems)
**✅ The condenser valve CONTROL is LIVE (task #13, 2026-07-11) [T2]:** `MoveValve` (id 4, the
Condenser handler table @0x50E52C — exactly one entry) registered + guarded by the REAL
FUN_004ac9c8 = `player+0x274 roleClassIndex == 0` (the ROOKIE-role lockout; task #12's
`BTPlayerRoleLocksAdvanced` bridge — the old "game-mode flag / likely off in a basic mission"
hedge is superseded: bring-up role = 2 = UNLOCKED, verified live: press → valveState 1→5 →
flow redistribution 1/6 → 5/10). Desktop: 'C' cycles the selected condenser (BT_VALVE_SLOT).
FUN_004ac9c8 = `player+0x274 == 0` (2026-07-18 correction: +0x274 = the egg EXPERIENCE level,
so this is the NOVICE lockout, not a "ROOKIE role" — see [[experience-levels]]; task #12's
`BTPlayerRoleLocksAdvanced` bridge; bring-up seeding = 2 ≈ veteran = UNLOCKED, verified live:
press → valveState 1→5 → flow redistribution 1/6 → 5/10). Desktop: 'C' cycles the selected
condenser (BT_VALVE_SLOT).
The **MessageBoard** feed needs **StatusMessagePool** (a NULL stub) + the per-player status
queue. `SeekVoltageGraph`'s 4 Seek* attrs are a cluster-child (not config-called). [T2]
+28 -21
View File
@@ -151,7 +151,8 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
btl4vid.cpp `MakeMechRenderables` reticle-build/inside pass (== arcade part_014.c:5173, Dynamic,
bound per Searchlight `lightState` via a new `LightStatePtr()` accessor); (3) a toggle input (the
authentic cockpit button-5→`ToggleLamp` dispatch is a controls-family reconstruction; a dev key
stands in). Also flags a deferred **`Mech::ControlsAllowLights()`** (searchlight.hpp:158 stub).
stands in). Also flags a deferred **`Mech::ControlsAllowLights()`** (searchlight.hpp:158 stub;
2026-07-18: identified as the `player+0x25c` not-novice experience flag — [[experience-levels]]).
See [[rendering]] fog section. Verified inert live: BT_FOG_LOG shows zero `SetFogStyle(2/3)`.
- **Factory capability-roster loops 2-4 are STILL DEAD (task #57 discovery).** mech.cpp's
post-roster loops add to `heatableSubsystems`(0x51155c)/`weaponRoster`(0x511830)/
@@ -187,20 +188,24 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
- **✅ `mech+0x190` IDENTIFIED (2026-07): it is the owning `BTPlayer`** (`Mech::GetPlayerLink()`,
`MECH_OWNING_PLAYER`; ENTITY.h:430). Set by `FUN_0049f624` (the mech↔player bind: `mech+0x190 = player`
AND `player->playerVehicle(+0x1fc) = mech`), which resolves the player from the mission player registry
(`app+0x2c+0x54`, `FUN_0041fd18`) by the pilot key. The valve/Myomers "gates" read the owning player's
fields at `player+0x260` and `player+0x274` (the reconstruction maps this block as `showDamageReceived@0x25c
/ showKills@0x260 / showDamageInflicted@0x264 / roleClassIndex@0x274` in btplayer.hpp — from the SCORING
work, so the names may NOT match the gate semantics; the true meaning + the WRITER of `player+0x260/0x274`
are not yet pinned — part_013.c:4553-4660 is the TorsoSimulation, a different object, not the setter). So
there is NO new subsystem to build. **The wiring (small):** point `MechSubsystem::IsDamaged()` (`FUN_004ac9c8`, valve
guard) at `GetPlayerLink()->`(0x274) and `Myomers::OwnerAdvancedDamage()` (`FUN_004ad7d4`) at
`GetPlayerLink()->`(0x260) — via NAMED members (databinding trap: our BTPlayer layout ≠ binary; do NOT
raw-read `player+0x260`). ⚠ Resolve first: (a) a naming conflict — `FUN_004ad7d4` is labeled BOTH
`HeatModelActive` (heat.hpp) AND `OwnerAdvancedDamage` (myomers.cpp); re-verify which body is which; (b)
the true semantic of `player+0x260/0x274` vs the scoring-derived `showKills`/`roleClassIndex` names. NOTE:
these are MODE flags — in the basic test mission the inert Myomers / dormant valve is likely AUTHENTIC
(advanced damage off); wiring makes them RESPOND when a mission enables the mode, not necessarily change
the basic-mission behavior. The MessageBoard feed is separate (StatusMessagePool, below).
(`app+0x2c+0x54`, `FUN_0041fd18`) by the pilot key.
**✅ FLAG SEMANTICS PINNED (2026-07-18) [T1]: the `player+0x25c..0x274` block = the EXPERIENCE
LEVEL (egg `experience` novice/standard/veteran/expert), written by the BTPlayer ctor @4c0bc8
from `btMission(+0x1f8)->experienceLevel(+0xe4)` — full mapping, consumers, and the 4.0-manual
behavior in [[experience-levels]] (+ [[decomp-reference]] §3 flag table).** Resolved en route: the
old "resolve first" items — `FUN_004ad7d4` (labeled both `HeatModelActive` and `OwnerAdvancedDamage`)
is ONE body reading `player+0x260` = the heat-model master switch (veteran+expert); `FUN_004ac9c8`
reads `player+0x274` = the novice lockout; the btplayer.hpp `showKills`/`roleClassIndex` scoring
names indeed do NOT match the gate semantics (comments corrected in place, member names kept).
**The wiring (small, still open):** ONE accessor family on BTPlayer named members seeded from
`BTMission::ExperienceLevel()`/`AdvancedDamageOn()` (databinding trap: do NOT raw-read
`player+0x260`), then point the five stub sites at it (`HeatModelActive` permissive-1 in
emitter/heat/mislanch/projweap vs `OwnerAdvancedDamage` False in myomers — currently the SAME gate
answered both ways; `LiveFireEnabled`/`ControlsAllowLights`/powersub short-event = the +0x25c
flag under three names; ctor seeding from role->returnFromDeath is a bring-up stand-in that runs
every mission as VETERAN). NOTE: these are MODE flags — an egg with `experience=standard` should
authentically have inert movement-heat/jams; wiring makes the tier selectable, not the basic
mission different. The MessageBoard feed is separate (StatusMessagePool, below).
- **✅ Authentic target acquisition RECONSTRUCTED (tasks #36/#39, 2026-07-08)** — the `Reticle`
pick-ray chain is LIVE (see [[combat-damage]] Targeting for the full port map): the crosshair =
**torso boresight** (NO free-aim mouse — the pod stick twisted the torso; you steer to aim) →
@@ -352,17 +357,19 @@ register. ⚠ The audit also flags the damage-economy item as SELF-CONTRADICTOR
tables must be function-local statics INSIDE the accessor (cross-TU static-init
order emptied the table -- see [[reconstruction-gotchas]] #9 last bullet); (b)
**FUN_004ac9c8 is NOT IsDamaged** -- raw body: `owner(+0xD0) -> mech+0x190 player
-> roleClassIndex(+0x274) == 0` = the ROOKIE-role lockout for advanced cockpit
systems (port bridge BTPlayerRoleLocksAdvanced, btplayer.cpp; NULL player =
unlocked [T3]; bring-up role defaults to 2 = unlocked). AUDIT TAIL: the other
-> +0x274 == 0` = the NOVICE-experience lockout for advanced cockpit systems
(2026-07-18 correction: +0x274 = the egg experience level, NOT a role class --
see [[experience-levels]]; port bridge BTPlayerRoleLocksAdvanced, btplayer.cpp;
NULL player = unlocked [T3]; bring-up seeding defaults to 2 ≈ veteran = unlocked).
AUDIT TAIL: the other
powersub.cpp sites annotated FUN_004ac9c8 (the coolant-draw gate ~:427,
ForceShortRecovery ~:444, the Generator/PowerWatcher site ~:1085) still call the
heat-family IsDamaged (simulationState != 0) stand-in -- each needs its raw fn
re-checked and swapped to the role bridge where the binary calls 4ac9c8.
- **Myomers authentic coupling** — the structural un-stub is INERT (mover feed + heat-gen no-op).
Real coupling needs the advanced-damage gate (`OwnerAdvancedDamage`/`FUN_004ad7d4` → the owning
**BTPlayer** `mech+0x190`+0x260, NOT 0xBD3 — see the mech+0x190 item above) + `MoverAttach` routing
into the LIVE JointedMover (must be reconciled with the gait cutover first).
Real coupling needs the heat-model gate (`OwnerAdvancedDamage`/`FUN_004ad7d4` → the owning
**BTPlayer** `mech+0x190`+0x260 = veteran/expert experience, NOT 0xBD3 — see [[experience-levels]])
+ `MoverAttach` routing into the LIVE JointedMover (must be reconciled with the gait cutover first).
- **SeekVoltageGraph** — 4 Seek* attrs unpublished (a cluster-child, not config-called; non-blocking).
- **✅ DAMAGE ECONOMY — AUTHENTIC (task #8, 2026-07-11) [T1/T2].** The whole economy closes:
+4 -1
View File
@@ -154,7 +154,10 @@ every checked control behavior:
button 8 → ChooseNearestPilot); radar = travel-oriented, 60° vision wedge sweeps with twist,
red hot box + callsign on contacts; **map zoom ± buttons** flank the secondary screen.
- **Experience gates the hunting aids**: viewscreen hot box + hunting-guide arrow appear in
STANDARD sim mode only (not veteran/expert); expert mode adds movement heat.
STANDARD sim mode only (not veteran/expert); expert mode adds movement heat. (2026-07-18 [T1]:
4.0→4.10 drift — in OUR 4.10 binary the HUD reads NO experience flag (hotbox/arrows are
lock-gated in every mode) and movement heat is veteran+expert; full wiring in
[[experience-levels]].)
- Cockpit: 5 MFDs (Coolant UL, Engineering top, Hot Box UR, Weapon ×2) + secondary screen with
side button columns + **EJECT button** beside the joystick + per-mech COOLANT LOOP tables
(weapons+generators per loop 1-6) and stat sheets (tonnage/armor/reservoir liters/heat
+2 -2
View File
@@ -57,8 +57,8 @@ Making a base byte-exact GROWS every subclass — they must be re-based TOGETHER
- **✅ DONE (task #7, `afefaee`)** — SubsystemMessageManager 0xBD3 (a
**damage/explosion consolidation hub** cached to `Mech[0x10d]`=0x434; the factory builds the REAL
class and `mech.hpp` names the slot `messageManager` — the old `controlsMapper` mislabel is swept,
the real mapper is roster slot 0. NOT the valve/advanced-damage gate — that's the owning **BTPlayer**
@mech+0x190, see [[combat-damage]]/[[open-questions]]). [T2]
the real mapper is roster slot 0. NOT the valve/heat-model gate — that's the owning **BTPlayer**
@mech+0x190 carrying the EXPERIENCE-level flags, see [[experience-levels]]). [T2]
## The watcher electrical chain (task #57, 2026-07-13 — the torso power gate)
The Watcher branch is POWERED indirectly: a watcher WATCHES another roster subsystem and mirrors
+28 -16
View File
@@ -1303,12 +1303,21 @@ BTPlayer::BTPlayer(
else
{
//
// Master: choose the HUD display toggles from the role class index.
// Master: derive the game-mode flags from the EXPERIENCE level.
//
// TODO(bring-up): scenarioRole is NULL here because the role lookup above
// (btMission->GetRoleRegistry()->Lookup) is still stubbed out. Guard the deref
// so mission load proceeds; default index 2 = show all stats (freeforall).
roleClassIndex = scenarioRole ? scenarioRole->GetReturnFromDeath() : 2; // this[0x9d] <- role+0xe4
// CORRECTED SOURCE (2026-07-18 [T1]): the binary reads
// btMission->experienceLevel (mission+0xe4, the egg's per-pilot
// "experience" 0..3 = novice/standard/veteran/expert) — NOT the role's
// returnFromDeath (the old reading). See context/experience-levels.md.
//
// STAND-IN [T3]: still seeded from the role (default 2 ≈ VETERAN) until
// the wiring task points this at BTMission::ExperienceLevel(). Known
// slot drift vs the binary switch: the binary never touches
// showDamageInflicted(0x264) here (that's the advancedDamage copy, set
// below), and expert's 0 lands on roleReturnDelay2(0x26c), not
// showScore(0x270) — binary rows: nov 0,0,0,0 / std 1,0,1,1 /
// vet 1,1,1,1 / exp 1,1,0,1 for (0x25c,0x260,0x26c,0x270).
roleClassIndex = scenarioRole ? scenarioRole->GetReturnFromDeath() : 2; // this[0x9d] <- mission+0xe4 in the binary
switch (roleClassIndex)
{
case 0:
@@ -1327,9 +1336,11 @@ BTPlayer::BTPlayer(
break;
}
// role+0xf0 ("returnDelay") has no field in the WinTesla ScenarioRole;
// initialised to 0 here. CROSS-FAMILY -- see report. BEST-EFFORT.
roleReturnDelay = roleReturnDelay2 = 0.0f; // this[0x99],[0x9a] <- role+0xf0
// CORRECTED (2026-07-18 [T1]): the binary sets this[0x99]/[0x9a]
// (0x264/0x268) = btMission->advancedDamageOn (mission+0xf0, the egg
// "advancedDamage" technician flag) — an int pair, not role floats.
// 0 here = advanced damage OFF, which matches the bring-up eggs. [T3]
roleReturnDelay = roleReturnDelay2 = 0.0f; // this[0x99],[0x9a] <- mission+0xf0 in the binary
}
killCount = 0; // this[0x9f]
@@ -1589,14 +1600,15 @@ void BTPostKillScore(Entity *victim, Scalar damage) // Step 7: KILL (+ MP deat
//
// FUN_004ac9c8 [T1]: `return *(*(*(sub+0xD0) + 0x190) + 0x274) == 0` -- the
// subsystem's owner Mech -> the owning BTPlayer (mech+0x190, GetPlayerLink)
// -> roleClassIndex (+0x274, role resource +0xE4). TRUE = the ROOKIE role
// (class 0) LOCKS the advanced cockpit systems (generator routing, coolant
// valves); nonzero role classes unlock them. The old reconstruction
// mislabeled this fn "Subsystem::IsDamaged" -- with healthy subsystems at
// simulationState==1, that stand-in gated the power-routing handlers OFF
// permanently. A NULL player (the target dummy / unbound solo mech) would AV
// in the binary (every pod mech has a player); the port reads NULL as
// UNLOCKED so dev rigs work [T3].
// -> +0x274 = the raw EXPERIENCE level (egg "experience", mission+0xe4; the
// old "roleClassIndex / role resource" reading was wrong -- see
// context/experience-levels.md). TRUE = NOVICE experience LOCKS the advanced
// cockpit systems (generator routing, coolant valves); standard and above
// unlock them. The old reconstruction mislabeled this fn
// "Subsystem::IsDamaged" -- with healthy subsystems at simulationState==1,
// that stand-in gated the power-routing handlers OFF permanently. A NULL
// player (the target dummy / unbound solo mech) would AV in the binary (every
// pod mech has a player); the port reads NULL as UNLOCKED so dev rigs work [T3].
//
int BTPlayerRoleLocksAdvanced(void *owner_mech)
{
+15 -9
View File
@@ -356,23 +356,29 @@ class DropZone__ReplyMessage;
suppressConsole; // @0x258 skip console notify for this score event
//
// Per-role display toggles, selected from the role's "returnFromDeath"
// class index (scenarioRole resource +0xe4, values 0..3) at @004c0bc8.
// EXPERIENCE-LEVEL game-mode flags (2026-07-18 [T1] — the old "per-role
// display toggles / returnFromDeath" reading was WRONG). The ctor
// @004c0bc8 seeds this block from btMission(+0x1f8)->experienceLevel
// (+0xe4, the egg's per-pilot "experience" novice/standard/veteran/
// expert) and ->advancedDamageOn(+0xf0) — NOT from the scenario role.
// The member NAMES below date from the scoring work and do not match
// the semantics; kept until the wiring task renames them. Binary
// mapping (nov/std/vet/exp) + consumers: context/experience-levels.md.
//
Logical
showDamageReceived; // @0x25c
showDamageReceived; // @0x25c 0/1/1/1 "sim live" (novice lockout: jams, searchlight, powersub @4b0efc)
Logical
showKills; // @0x260
showKills; // @0x260 0/0/1/1 HEAT-MODEL master switch (FUN_004ad7d4)
Logical
showDamageInflicted;// @0x264
showDamageInflicted;// @0x264 = mission advancedDamageOn (copy 1; NOT part of the level switch)
Logical
showScore; // @0x270
showScore; // @0x270 0/1/1/1 consumer not located
Scalar
roleReturnDelay; // @0x268 (role resource +0xf0)
roleReturnDelay; // @0x268 = mission advancedDamageOn (copy 2; int in the binary)
Scalar
roleReturnDelay2; // @0x26c
roleReturnDelay2; // @0x26c 0/1/1/0 consumer not located (supercharge-clamp hypothesis [T4])
int
roleClassIndex; // @0x274 (role resource +0xe4)
roleClassIndex; // @0x274 raw experience level 0..3 (FUN_004ac9c8 = ==0 novice predicate)
friend int BTPlayerRoleLocksAdvanced(void *); // the FUN_004ac9c8 bridge (task #12)
friend void BTPlayerCountObservedDeath(void *); // observed-death tally (MP DEATHS fix)
File diff suppressed because it is too large Load Diff
+135
View File
@@ -0,0 +1,135 @@
# BT 4.10 source manifest — measured from BTL4OPT.EXE
Generated by `tools/manifest410.py`. Census = the surviving makefiles (BT.MAK 36 TUs, BTL4.MAK 13 TUs + btl4.obj); attribution = in-body assert strings (resolved via section_dump), exporter file= tags, link-order fill.
Totals: **6267 functions** in the image; BT-side: **16 funcs / 18 KB** (bt.lib) + **20 funcs / 19 KB** (btl4.lib+main); boundary-uncertain: 5018; unattributed (outside any anchor span): 483.
`recon @addr hits` = binary function addresses cited in `game/reconstructed/` — a proxy for how much of the TU is already semantically decoded (not a completeness claim).
## bt.lib (link order)
| TU | survives | funcs | KB | addr range | assert-anchored | max assert line | recon @addr hits |
|---|---|---|---|---|---|---|---|
| bt/btmssn.cpp | full | 1 | 0.0 | 49b6bc-49b6d8 | 1 | 0 | 1 |
| bt/messmgr.cpp | — | — | — | — | — | — | 0 |
| bt/mechdmg.cpp | — | — | — | — | — | — | 0 |
| bt/dmgtable.cpp | — | — | — | — | — | — | 0 |
| bt/mech.cpp | — | 1 | 5.7 | 4a1674-4a2d48 | 1 | 0 | 1 |
| bt/mech2.cpp | — | 5 | 10.3 | 4a5028-4a7970 | 4 | 1650 | 4 |
| bt/mech3.cpp | — | — | — | — | — | — | 0 |
| bt/mech4.cpp | — | — | — | — | — | — | 0 |
| bt/mechsub.cpp | — | — | — | — | — | — | 0 |
| bt/mechtech.cpp | — | — | — | — | — | — | 0 |
| bt/heat.cpp | partial(TCP) | 1 | 0.6 | 4adda0-4adfd4 | 1 | 0 | 1 |
| bt/mechmppr.cpp | — | 3 | 0.1 | 4b029c-4b02f0 | 3 | 602 | 3 |
| bt/powersub.cpp | — | 1 | 0.5 | 4b0f74-4b115c | 1 | 626 | 1 |
| bt/sensor.cpp | — | — | — | — | — | — | 0 |
| bt/gnrator.cpp | partial(TCP) | — | — | — | — | — | 0 |
| bt/gyro.cpp | — | — | — | — | — | — | 0 |
| bt/torso.cpp | — | — | — | — | — | — | 0 |
| bt/hud.cpp | — | — | — | — | — | — | 0 |
| bt/myomers.cpp | — | — | — | — | — | — | 0 |
| bt/mechweap.cpp | — | 1 | 0.0 | 4b95ec-4b9608 | 1 | 0 | 1 |
| bt/emitter.cpp | partial(TCP) | — | — | — | — | — | 0 |
| bt/ppc.cpp | full | — | — | — | — | — | 0 |
| bt/projweap.cpp | partial(TCP) | — | — | — | — | — | 0 |
| bt/mislanch.cpp | — | — | — | — | — | — | 0 |
| bt/ammobin.cpp | partial(TCP) | — | — | — | — | — | 0 |
| bt/gauss.cpp | full | — | — | — | — | — | 0 |
| bt/projtile.cpp | partial(TCP) | — | — | — | — | — | 0 |
| bt/misthrst.cpp | — | — | — | — | — | — | 0 |
| bt/seeker.cpp | — | — | — | — | — | — | 0 |
| bt/missile.cpp | partial(TCP) | — | — | — | — | — | 0 |
| bt/btplayer.cpp | — | 3 | 0.8 | 4c0200-4c052c | 3 | 662 | 3 |
| bt/btteam.cpp | full | — | — | — | — | — | 0 |
| bt/btdirect.cpp | — | — | — | — | — | — | 0 |
| bt/btreg.cpp | full | — | — | — | — | — | 0 |
| bt/btcnsl.cpp | full | — | — | — | — | — | 0 |
| bt/bttool.cpp | full | — | — | — | — | — | 0 |
## btl4.lib + main (link order)
| TU | survives | funcs | KB | addr range | assert-anchored | max assert line | recon @addr hits |
|---|---|---|---|---|---|---|---|
| bt_l4/btl4mode.cpp | full | — | — | — | — | — | 0 |
| bt_l4/btl4rdr.cpp | — | 8 | 2.2 | 4c2178-4c2a1d | 2 | 1032 | 8 |
| bt_l4/btl4gaug.cpp | — | 1 | 0.3 | 4c3f6c-4c4074 | 1 | 1674 | 1 |
| bt_l4/btl4gau2.cpp | — | — | — | — | — | — | 0 |
| bt_l4/btl4gau3.cpp | — | — | — | — | — | — | 0 |
| bt_l4/btl4grnd.cpp | — | — | — | — | — | — | 0 |
| bt_l4/btl4galm.cpp | — | — | — | — | — | — | 0 |
| bt_l4/btl4vid.cpp | — | 9 | 13.8 | 4cdac0-4d11e8 | 3 | 3065 | 6 |
| bt_l4/btl4mppr.cpp | — | — | — | — | — | — | 0 |
| bt_l4/btl4arnd.cpp | full | — | — | — | — | — | 0 |
| bt_l4/btl4mssn.cpp | — | 1 | 2.1 | 4d2c30-4d34b0 | 1 | 451 | 1 |
| bt_l4/btl4app.cpp | — | 1 | 0.8 | 4d36f4-4d3a2c | 1 | 400 | 1 |
| bt_l4/btl4pb.cpp | — | — | — | — | — | — | 0 |
| bt_l4/btl4.cpp | — | — | — | — | — | — | 0 |
## Engine/other TUs seen in the image (context only — their source survives)
| TU | funcs | KB |
|---|---|---|
| bt/mechweap.hpp | 1 | 0.0 |
| munga/app.cpp | 22 | 2.7 |
| munga/appmgr.cpp | 1 | 0.4 |
| munga/audcmp.cpp | 51 | 8.7 |
| munga/audio.cpp | 1 | 0.0 |
| munga/audloc.cpp | 1 | 0.4 |
| munga/audlvl.cpp | 1 | 0.4 |
| munga/audmidi.cpp | 2 | 0.1 |
| munga/audseq.cpp | 15 | 1.8 |
| munga/audsrc.cpp | 21 | 3.1 |
| munga/audwthr.cpp | 46 | 3.4 |
| munga/audwthr.hpp | 29 | 8.2 |
| munga/boxsolid.cpp | 1 | 0.0 |
| munga/caminst.cpp | 1 | 0.9 |
| munga/cammgr.cpp | 1 | 0.2 |
| munga/cmpnnt.cpp | 1 | 0.0 |
| munga/controls.cpp | 11 | 0.7 |
| munga/cstr.cpp | 1 | 0.1 |
| munga/event.cpp | 2 | 0.1 |
| munga/explode.cpp | 1 | 0.3 |
| munga/filestrm.hpp | 2 | 0.1 |
| munga/gaugalrm.cpp | 2 | 0.1 |
| munga/gauge.cpp | 16 | 1.4 |
| munga/gaugrend.cpp | 34 | 8.4 |
| munga/graph2d.cpp | 40 | 3.1 |
| munga/hash.cpp | 2 | 0.1 |
| munga/heap.cpp | 1 | 0.5 |
| munga/host.cpp | 1 | 0.1 |
| munga/hostmgr.cpp | 5 | 0.2 |
| munga/icom.cpp | 1 | 0.0 |
| munga/interest.cpp | 28 | 2.5 |
| munga/intorgn.cpp | 3 | 0.1 |
| munga/iterator.cpp | 8 | 0.2 |
| munga/lamp.cpp | 1 | 0.0 |
| munga/memstrm.cpp | 1 | 0.2 |
| munga/network.cpp | 5 | 0.7 |
| munga/objstrm.cpp | 35 | 4.0 |
| munga/player.cpp | 1 | 0.0 |
| munga/registry.cpp | 2 | 0.1 |
| munga/renderer.cpp | 3 | 0.1 |
| munga/scalar.cpp | 1 | 0.1 |
| munga/sfeskt.cpp | 1 | 0.0 |
| munga/socket.cpp | 9 | 0.4 |
| munga/spooler.cpp | 1 | 0.1 |
| munga/srtskt.cpp | 6 | 0.2 |
| munga/table.cpp | 3 | 0.1 |
| munga/tree.cpp | 3 | 0.1 |
| munga/vchain.cpp | 3 | 0.1 |
| munga/watcher.cpp | 5 | 1.0 |
| munga/watcher.hpp | 5 | 0.8 |
| munga_l4/l4app.cpp | 2 | 0.5 |
| munga_l4/l4audhdw.cpp | 15 | 1.3 |
| munga_l4/l4audio.cpp | 15 | 4.7 |
| munga_l4/l4audlvl.cpp | 1 | 0.4 |
| munga_l4/l4audres.cpp | 1 | 1.2 |
| munga_l4/l4audrnd.cpp | 2 | 1.1 |
| munga_l4/l4audwtr.cpp | 1 | 0.6 |
| munga_l4/l4ctrl.cpp | 29 | 7.0 |
| munga_l4/l4gauge.cpp | 1 | 0.0 |
| munga_l4/l4net.cpp | 22 | 8.7 |
| munga_l4/l4splr.cpp | 2 | 0.1 |
| munga_l4/l4video.cpp | 39 | 25.1 |
| munga_l4/l4vidrnd.cpp | 162 | 23.0 |
+342
View File
@@ -0,0 +1,342 @@
#!/usr/bin/env python3
"""
manifest410.py -- per-TU manifest of BTL4OPT.EXE for the 4.10 literal-source
reconstruction (the "size of the mountain" measurement).
Inputs (all already in the repo):
reference/decomp/all/part_*.c -- Ghidra export; every function is headed by
/* @00ADDR file=TAG name=NAME */
reference/decomp/section_dump.txt -- hex dump of the data sections (VA -> bytes);
used to resolve s_*_00ADDR string symbols to
their REAL contents (assert file paths).
game/original/BT/BT.MAK, BT_L4/BTL4.MAK -- the authoritative TU census + link
order for bt.lib (36 TUs) / btl4.lib (13 TUs).
game/reconstructed/*.cpp|*.hpp -- the port; @00ADDR annotations are harvested
to measure which binary ranges are already
semantically decoded.
Attribution signals, strongest first:
1. an assert string inside the body naming the source file (+ line number);
2. the exporter's file= tag;
3. link-order fill: a function between two anchors of the SAME TU inherits it
(confident); between anchors of DIFFERENT TUs it lands in a boundary zone
(uncertain -- counted separately, never claimed).
Outputs:
reference/BT410_SOURCE_MANIFEST.md -- human summary tables
reference/BT410_SOURCE_MANIFEST.json -- machine form (per-function attribution)
Usage: py -3 tools/manifest410.py (from the BT411 repo root)
"""
import json
import os
import re
import sys
from collections import defaultdict
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
DECOMP_DIR = os.path.join(ROOT, "reference", "decomp", "all")
SECTION_DUMP = os.path.join(ROOT, "reference", "decomp", "section_dump.txt")
RECON_DIR = os.path.join(ROOT, "game", "reconstructed")
ORIG_BT = os.path.join(ROOT, "game", "original", "BT")
ORIG_BTL4 = os.path.join(ROOT, "game", "original", "BT_L4")
OUT_MD = os.path.join(ROOT, "reference", "BT410_SOURCE_MANIFEST.md")
OUT_JSON = os.path.join(ROOT, "reference", "BT410_SOURCE_MANIFEST.json")
# ---------------------------------------------------------------- census ----
# The authoritative TU lists, in LINK ORDER, from the surviving makefiles.
BT_LIB_TUS = [ # game/original/BT/BT.MAK BT_OBJS
"btmssn", "messmgr", "mechdmg", "dmgtable", "mech", "mech2", "mech3",
"mech4", "mechsub", "mechtech", "heat", "mechmppr", "powersub", "sensor",
"gnrator", "gyro", "torso", "hud", "myomers", "mechweap", "emitter",
"ppc", "projweap", "mislanch", "ammobin", "gauss", "projtile", "misthrst",
"seeker", "missile", "btplayer", "btteam", "btdirect", "btreg", "btcnsl",
"bttool",
]
BTL4_LIB_TUS = [ # game/original/BT_L4/BTL4.MAK BTL4_OBJS
"btl4mode", "btl4rdr", "btl4gaug", "btl4gau2", "btl4gau3", "btl4grnd",
"btl4galm", "btl4vid", "btl4mppr", "btl4arnd", "btl4mssn", "btl4app",
"btl4pb",
]
MAIN_TUS = ["btl4"] # btl4.obj, linked directly
def tu_key(prefix, name):
return "%s/%s.cpp" % (prefix, name)
CENSUS = (
[tu_key("bt", n) for n in BT_LIB_TUS]
+ [tu_key("bt_l4", n) for n in BTL4_LIB_TUS]
+ [tu_key("bt_l4", n) for n in MAIN_TUS]
)
CENSUS_SET = set(CENSUS)
# ------------------------------------------------------- section dump -------
def load_string_table():
"""Parse section_dump.txt (objdump -s style: ' VA GGGGGGGG x4 ascii')
into a VA->byte map; strings are read lazily via read_cstr()."""
mem = {}
pat = re.compile(r"^ ([0-9a-f]{6,8}) ((?:[0-9a-f]{2,8} ){1,4})", re.I)
with open(SECTION_DUMP, "r", errors="replace") as f:
for line in f:
m = pat.match(line)
if not m:
continue
va = int(m.group(1), 16)
off = 0
for group in m.group(2).split():
if len(group) % 2:
continue
for i in range(0, len(group), 2):
mem[va + off] = int(group[i:i + 2], 16)
off += 1
return mem
def read_cstr(mem, va, maxlen=260):
out = []
for i in range(maxlen):
b = mem.get(va + i)
if b is None or b == 0:
break
out.append(chr(b) if 32 <= b < 127 else "?")
return "".join(out)
PATH_RE = re.compile(r"^[a-z]:[\\/].*\.(cpp|hpp|tcp|thp|c|h|asm)$", re.I)
def normalize_path(p):
r"""d:\tesla_bt\bt_l4\BTL4MSSN.CPP -> bt_l4/btl4mssn.cpp (last two comps)."""
parts = re.split(r"[\\/]+", p.lower())
if len(parts) >= 2:
return "%s/%s" % (parts[-2], parts[-1])
return parts[-1]
# ------------------------------------------------------------ decomp --------
FUNC_HDR = re.compile(r"^/\* @([0-9a-f]{8}) file=(\S+) name=(\S+) \*/")
SYM_REF = re.compile(r"s_\w+?_(00[0-9a-f]{6})\b")
SYM_LINE = re.compile(r"s_\w+?_(00[0-9a-f]{6})\s*,\s*(0x[0-9a-f]+|\d+)\s*[),]")
def parse_decomp(mem):
"""Return sorted list of dicts: addr, name, tag, part, paths{norm: [lines]}."""
funcs = []
for fn in sorted(os.listdir(DECOMP_DIR)):
if not fn.endswith(".c"):
continue
part = fn
cur = None
with open(os.path.join(DECOMP_DIR, fn), "r", errors="replace") as f:
for line in f:
m = FUNC_HDR.match(line)
if m:
cur = {
"addr": int(m.group(1), 16),
"tag": m.group(2),
"name": m.group(3),
"part": part,
"paths": defaultdict(list), # norm path -> [line numbers]
}
funcs.append(cur)
continue
if cur is None:
continue
# line-number pairs first (subset of SYM_REF hits)
consumed = set()
for sm in SYM_LINE.finditer(line):
va = int(sm.group(1), 16)
s = read_cstr(mem, va)
if PATH_RE.match(s):
num = int(sm.group(2), 0)
cur["paths"][normalize_path(s)].append(num)
consumed.add(va)
for sm in SYM_REF.finditer(line):
va = int(sm.group(1), 16)
if va in consumed:
continue
s = read_cstr(mem, va)
if PATH_RE.match(s):
cur["paths"][normalize_path(s)] # touch, no line
funcs.sort(key=lambda d: d["addr"])
return funcs
# ------------------------------------------------------- attribution --------
def attribute(funcs):
"""Set f['file'] (best attribution) + f['how'] in
{assert, tag, fill, boundary, none}."""
# pass 1: direct evidence
for f in funcs:
best = None
if f["paths"]:
# the file MOST referenced in-body (asserts overwhelmingly name
# the owning TU); ties broken by presence of line numbers
best = max(
f["paths"].items(),
key=lambda kv: (len(kv[1]) > 0, len(kv[1]), kv[0]),
)[0]
f["file"], f["how"] = best, "assert"
elif f["tag"] != "?":
f["file"], f["how"] = f["tag"].lower(), "tag"
else:
f["file"], f["how"] = None, "none"
# pass 2: link-order fill between agreeing anchors
anchors = [(i, f["file"]) for i, f in enumerate(funcs) if f["file"]]
for (i0, f0), (i1, f1) in zip(anchors, anchors[1:]):
for j in range(i0 + 1, i1):
if funcs[j]["file"] is None:
if f0 == f1:
funcs[j]["file"], funcs[j]["how"] = f0, "fill"
else:
funcs[j]["file"], funcs[j]["how"] = None, "boundary"
funcs[j]["between"] = (f0, f1)
return funcs
# ------------------------------------------------- reconstructed map --------
ADDR_ANNOT = re.compile(r"@\s?(00[0-9a-f]{6})\b|@([0-9a-f]{6})\b", re.I)
def recon_addresses():
"""Harvest every @00XXXXXX annotation from game/reconstructed sources."""
hits = defaultdict(set) # file -> set of addrs
for fn in sorted(os.listdir(RECON_DIR)):
if not fn.endswith((".cpp", ".hpp", ".md")):
continue
with open(os.path.join(RECON_DIR, fn), "r", errors="replace") as f:
text = f.read()
for m in ADDR_ANNOT.finditer(text):
a = int(m.group(1) or m.group(2), 16)
if 0x400000 <= a <= 0x540000:
hits[fn].add(a)
return hits
# ------------------------------------------------------ survivors -----------
def survivors():
"""Map census key -> 'full' | 'partial(TCP)' from game/original."""
out = {}
for d, prefix in ((ORIG_BT, "bt"), (ORIG_BTL4, "bt_l4")):
if not os.path.isdir(d):
continue
for fn in os.listdir(d):
base, ext = os.path.splitext(fn.lower())
key = tu_key(prefix, base)
if ext == ".cpp" and key in CENSUS_SET:
out[key] = "full"
elif ext == ".tcp":
k2 = tu_key(prefix, base)
if k2 in CENSUS_SET and out.get(k2) != "full":
out[k2] = "partial(TCP)"
return out
# ------------------------------------------------------------- main ---------
def main():
print("loading section dump ...")
mem = load_string_table()
print(" %d bytes mapped" % len(mem))
print("parsing decomp ...")
funcs = parse_decomp(mem)
print(" %d functions" % len(funcs))
attribute(funcs)
# sizes: delta to next function
for f, g in zip(funcs, funcs[1:]):
f["size"] = g["addr"] - f["addr"]
funcs[-1]["size"] = 0
recon = recon_addresses()
recon_all = set().union(*recon.values()) if recon else set()
surv = survivors()
# per-TU aggregation (census TUs + whatever engine files showed up)
tus = defaultdict(lambda: {
"funcs": 0, "bytes": 0, "assert": 0, "fill": 0, "tag": 0,
"lo": None, "hi": None, "max_line": 0, "recon_hits": 0,
})
boundary = 0
unattributed = 0
for f in funcs:
if f["how"] == "boundary":
boundary += 1
continue
if f["file"] is None:
unattributed += 1
continue
t = tus[f["file"]]
t["funcs"] += 1
t["bytes"] += f["size"]
t[f["how"] if f["how"] in ("assert", "fill", "tag") else "tag"] += 1
t["lo"] = f["addr"] if t["lo"] is None else min(t["lo"], f["addr"])
t["hi"] = max(t["hi"] or 0, f["addr"] + f["size"])
for lines in f["paths"].values():
for n in lines:
t["max_line"] = max(t["max_line"], n)
if f["addr"] in recon_all:
t["recon_hits"] += 1
# ------------------------------------------------------------- write ----
def tbl_row(key):
t = tus.get(key)
s = surv.get(key, "")
if not t:
return "| %s | %s | — | — | — | — | — | 0 |" % (key, s or "")
return "| %s | %s | %d | %.1f | %06x-%06x | %d | %d | %d |" % (
key, s or "", t["funcs"], t["bytes"] / 1024.0,
t["lo"], t["hi"], t["assert"], t["max_line"], t["recon_hits"],
)
bt_keys = [tu_key("bt", n) for n in BT_LIB_TUS]
btl4_keys = [tu_key("bt_l4", n) for n in BTL4_LIB_TUS + MAIN_TUS]
engine_keys = sorted(k for k in tus if k not in CENSUS_SET)
bt_total = sum(tus[k]["bytes"] for k in bt_keys if k in tus)
btl4_total = sum(tus[k]["bytes"] for k in btl4_keys if k in tus)
bt_funcs = sum(tus[k]["funcs"] for k in bt_keys if k in tus)
btl4_funcs = sum(tus[k]["funcs"] for k in btl4_keys if k in tus)
hdr = ("| TU | survives | funcs | KB | addr range | assert-anchored | "
"max assert line | recon @addr hits |\n|---|---|---|---|---|---|---|---|")
with open(OUT_MD, "w") as f:
f.write("# BT 4.10 source manifest — measured from BTL4OPT.EXE\n\n")
f.write("Generated by `tools/manifest410.py`. Census = the surviving "
"makefiles (BT.MAK 36 TUs, BTL4.MAK 13 TUs + btl4.obj); "
"attribution = in-body assert strings (resolved via "
"section_dump), exporter file= tags, link-order fill.\n\n")
f.write("Totals: **%d functions** in the image; BT-side: **%d funcs / "
"%.0f KB** (bt.lib) + **%d funcs / %.0f KB** (btl4.lib+main); "
"boundary-uncertain: %d; unattributed (outside any anchor "
"span): %d.\n\n" % (
len(funcs), bt_funcs, bt_total / 1024.0,
btl4_funcs, btl4_total / 1024.0, boundary, unattributed))
f.write("`recon @addr hits` = binary function addresses cited in "
"`game/reconstructed/` — a proxy for how much of the TU is "
"already semantically decoded (not a completeness claim).\n\n")
f.write("## bt.lib (link order)\n\n%s\n" % hdr)
for k in bt_keys:
f.write(tbl_row(k) + "\n")
f.write("\n## btl4.lib + main (link order)\n\n%s\n" % hdr)
for k in btl4_keys:
f.write(tbl_row(k) + "\n")
f.write("\n## Engine/other TUs seen in the image (context only — "
"their source survives)\n\n")
f.write("| TU | funcs | KB |\n|---|---|---|\n")
for k in engine_keys:
t = tus[k]
f.write("| %s | %d | %.1f |\n" % (k, t["funcs"], t["bytes"] / 1024.0))
with open(OUT_JSON, "w") as f:
json.dump({
"census": CENSUS,
"functions": [
{"addr": "%08x" % fu["addr"], "name": fu["name"],
"file": fu["file"], "how": fu["how"], "size": fu["size"],
"lines": {k: v for k, v in fu["paths"].items() if v}}
for fu in funcs
],
}, f, indent=1)
print("wrote %s\nwrote %s" % (OUT_MD, OUT_JSON))
print("BT-side: %d funcs / %.0f KB btl4-side: %d funcs / %.0f KB "
"boundary %d unattributed %d" % (
bt_funcs, bt_total / 1024.0, btl4_funcs, btl4_total / 1024.0,
boundary, unattributed))
if __name__ == "__main__":
sys.exit(main())