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
+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())