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:
@@ -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