mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
3207d52491
- extract_proto_exe.py: subdir-aware proto main-EXE extraction (reuses iso9660); extracted
sep8 SLUS_007.26 (413,696 B) + aug31 MUSASHI/USA_DEMO.EXE (415,744 B, base 0x80018000)
- ghidra_import.sh: reusable headless import; both protos imported into the bfm project
(sep8 1726 funcs, aug31 1737 funcs, both PsyQ 4.0.0), R9-verified
- DumpFunctionSignatures.java + match_protos.py: 3-tier (exact/norm/seq) cross-binary
correspondence; MIPS normalizer validated by anchors
- FINDINGS: (T2) no debug symbols in either proto — Hidden Palace "no symbols" VERIFIED;
recon "Sep-8 less-stripped" REFUTED by per-file byte compare. (T3) Sep-8 99.6% byte-identical
to retail (only 3 funcs differ: DebugMenuHandler, CdReadSectorReadyCB, SaveLoadRoutine);
Aug-31 66% norm-identical, 862 1:1 correspondences. (T4) demo shares retail's 18-entry
game-mode dispatch architecture but the handler code diverged — Q#10 resolved
- VERDICT (docs/proto-correspondence.md): NO-GO as a Gen1 label/symbol accelerator; GO to retain
Sep-8 (Phase-6 compiler corroborant) + both (Gen2 assets); proto-side labels in
config/symbols.proto-{sep8,demo}.txt (R13-tagged, never feed symbols.us.txt)
- reorg extracted/ into per-ROM subfolders (extracted/retail/, extracted/proto/); nested
gitignore allowlist; extractor defaults updated; manifest --verify PASS
- memory-map.md: Phase-3.5 block + demo dispatch anchors; Q#10 RESOLVED; §5 proto notes VERIFIED
- ghidra_mcp_start.sh: PROG arg to serve a prototype
- rules R13 (proto-provenance/confidence tagging), R14 (verify recon counts vs bytes)
- bumps project version 1.3.0 -> 1.3.1
141 lines
6.5 KiB
Python
141 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Cross-binary function correspondence for the BFM prototype spike (Phase 3.5).
|
|
|
|
Reads the per-function signature dumps produced by
|
|
``tools/ghidra_scripts/DumpFunctionSignatures.java`` (``.run/sig.<prog>.jsonl``)
|
|
and reports, for each prototype vs retail, how many retail functions have a
|
|
counterpart at three tiers:
|
|
|
|
exact : identical raw instruction bytes (strict)
|
|
norm : identical MIPS-normalized stream (same source, relocation-insensitive)
|
|
seq : identical mnemonic-only sequence (loose; same ops, any regalloc/consts)
|
|
|
|
Counts are reported at two size thresholds -- all functions (>=1 instruction) and
|
|
"substantial" functions (>= MIN_SUBSTANTIAL instructions) -- so trivial-stub
|
|
collisions (e.g. ``jr ra; nop``) don't inflate the headline number. The trustworthy
|
|
output is the set of **1:1 unique h_norm correspondences** (a retail function whose
|
|
normalized stream matches exactly one proto function and vice-versa); these are
|
|
written to ``.run/correspondence.<proto>-vs-retail.tsv`` for T4/T6.
|
|
|
|
A small anchor sanity-check confirms the normalizer by checking known retail
|
|
functions (LZSS decoder, game-mode dispatch, loader) map into each proto.
|
|
|
|
Pure stdlib. Read-only. R12: outputs under ``.run/``.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
RUN = Path(__file__).resolve().parents[1] / ".run" # tools/ -> repo/.run
|
|
CONFIG = Path(__file__).resolve().parents[1] / "config" # committed symbol records
|
|
MIN_SUBSTANTIAL = 8
|
|
|
|
RETAIL = "SLUS_007.26"
|
|
PROTOS = ["sep8_SLUS_007.26", "aug31_USA_DEMO.EXE"]
|
|
SHORT = {"sep8_SLUS_007.26": "sep8", "aug31_USA_DEMO.EXE": "demo"}
|
|
|
|
# Known retail anchors (lowercase 0x) for the normalizer sanity check.
|
|
ANCHORS = {
|
|
"0x80018730": "LzssDecodeSector",
|
|
"0x80010b40": "GameModeDispatch",
|
|
"0x80011144": "DebugMenuHandler",
|
|
"0x8001971c": "LoaderInitFileTable",
|
|
"0x80019a24": "CdReadRequest",
|
|
"0x8002b154": "SaveLoadRoutine",
|
|
}
|
|
|
|
|
|
def load(prog: str) -> list[dict]:
|
|
p = RUN / f"sig.{prog}.jsonl"
|
|
rows = [json.loads(line) for line in p.read_text().splitlines() if line.strip()]
|
|
return [r for r in rows if r["nins"] > 0] # drop GTE-macro stubs / empty
|
|
|
|
|
|
def index(rows: list[dict], key: str) -> dict[str, list[dict]]:
|
|
d: dict[str, list[dict]] = defaultdict(list)
|
|
for r in rows:
|
|
d[r[key]].append(r)
|
|
return d
|
|
|
|
|
|
def emit_proto_symbols(proto: str, pairs: list[tuple[dict, dict]]) -> tuple[Path, int]:
|
|
"""Write config/symbols.proto-<short>.txt: retail names mapped onto the proto via
|
|
1:1 h_norm correspondence. PROTO-SIDE ONLY, provenance-tagged (R13) -- these are
|
|
inherited-from-retail labels to make the proto navigable for Phase-6/Gen2, and are
|
|
UNVERIFIED against the proto until independently confirmed. They are NOT retail
|
|
symbols and must never feed config/symbols.us.txt. Only meaningful (non-default)
|
|
retail names are emitted; FUN_/LAB_ defaults are noise and dropped."""
|
|
short = SHORT.get(proto, proto)
|
|
named = [(r, p) for r, p in pairs
|
|
if not (r["name"].startswith("FUN_") or r["name"].startswith("LAB_"))]
|
|
named.sort(key=lambda x: int(x[1]["addr"], 16))
|
|
lines = [
|
|
f"// config/symbols.proto-{short}.txt — retail symbol names mapped onto the {proto} prototype.",
|
|
"// Phase 3.5: generated by tools/match_protos.py from 1:1 h_norm correspondences (NOT byte-matches).",
|
|
"// PROVENANCE (R13): each name is INHERITED FROM RETAIL via normalized-instruction equality and is",
|
|
"// UNVERIFIED against the proto until independently confirmed. Proto-side navigation aid for",
|
|
"// Phase-6/Gen2 ONLY. These are NOT retail symbols; never merge into config/symbols.us.txt.",
|
|
f"// count={len(named)}",
|
|
"",
|
|
]
|
|
for r, p in named:
|
|
lines.append(f'{r["name"]:32} = 0x{int(p["addr"], 16):08X}; // func <- retail {r["addr"]} (norm-1to1, unverified)')
|
|
out = CONFIG / f"symbols.proto-{short}.txt"
|
|
out.write_text("\n".join(lines) + "\n")
|
|
return out, len(named)
|
|
|
|
|
|
def main() -> int:
|
|
retail = load(RETAIL)
|
|
print(f"retail {RETAIL}: {len(retail)} functions (nins>0)\n")
|
|
|
|
for proto in PROTOS:
|
|
prows = load(proto)
|
|
print(f"===== {proto} vs retail =====")
|
|
print(f" proto functions (nins>0): {len(prows)}")
|
|
for tier in ("h_exact", "h_norm", "h_seq"):
|
|
pset = {r[tier] for r in prows}
|
|
for thr, label in ((1, "all"), (MIN_SUBSTANTIAL, f">={MIN_SUBSTANTIAL}ins")):
|
|
rsub = [r for r in retail if r["nins"] >= thr]
|
|
m = sum(1 for r in rsub if r[tier] in pset)
|
|
pct = 100 * m / max(1, len(rsub))
|
|
print(f" {tier:8} {label:9}: {m:5}/{len(rsub):5} retail funcs matched ({pct:5.1f}%)")
|
|
|
|
# 1:1 unique h_norm correspondences (the trustworthy pairs)
|
|
rN, pN = index(retail, "h_norm"), index(prows, "h_norm")
|
|
pairs = [(rs[0], ps[0]) for h, rs in rN.items()
|
|
if (ps := pN.get(h)) and len(rs) == 1 and len(ps) == 1]
|
|
sub_pairs = [(r, p) for r, p in pairs if r["nins"] >= MIN_SUBSTANTIAL]
|
|
print(f" 1:1 unique h_norm correspondences: {len(pairs)} (>= {MIN_SUBSTANTIAL}ins: {len(sub_pairs)})")
|
|
|
|
out = RUN / f"correspondence.{proto}-vs-retail.tsv"
|
|
with out.open("w") as fh:
|
|
fh.write("retail_addr\tretail_name\tproto_addr\tproto_name\tnins\ttier\n")
|
|
for r, p in sorted(pairs, key=lambda x: x[0]["addr"]):
|
|
fh.write(f'{r["addr"]}\t{r["name"]}\t{p["addr"]}\t{p["name"]}\t{r["nins"]}\tnorm-1to1\n')
|
|
print(f" -> {out}")
|
|
sym_out, nsym = emit_proto_symbols(proto, pairs)
|
|
print(f" -> {sym_out} ({nsym} named retail funcs mapped onto the proto)")
|
|
|
|
# anchor sanity check
|
|
pN_all, pE_all = index(prows, "h_norm"), index(prows, "h_exact")
|
|
by_addr = {r["addr"].lower(): r for r in retail}
|
|
print(" anchor sanity (known retail func -> proto match?):")
|
|
for a, nm in ANCHORS.items():
|
|
r = by_addr.get(a)
|
|
if not r:
|
|
print(f" {a} {nm:20}: (not in retail dump)")
|
|
continue
|
|
em, nmatch = pE_all.get(r["h_exact"]), pN_all.get(r["h_norm"])
|
|
tag = "EXACT" if em else ("norm " if nmatch else " - ")
|
|
where = (em or nmatch or [{}])[0].get("addr", "")
|
|
print(f" {a} {nm:20} nins={r['nins']:4} [{tag}] {('-> ' + where) if where else '(no match)'}")
|
|
print()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|