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:
@@ -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` |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user