mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
7d10fd7e20
parse_config's docstring said 'the trailing run of {data,.rodata} pieces after
the last c piece'; its code took data_pieces[0], the first such piece anywhere in
the file. Those agree on 171 configs and disagree on 42: the md_* modules open
with the 154-A leading island - [0x0, .rodata, md_XXX] BEFORE their c piece, so
apply()'s splice lines[:lo] + region + lines[hi:] deleted the c line and wrote the
yaml to disk before the tool errored out for unrelated reasons.
Fixed by deriving the region from the last c piece, plus an R43 guard that refuses
outright if any c piece lands inside the window apply() rewrites wholesale.
Also, correcting what S58's blanket refusal had lumped together:
* md_* (42 configs) was the whole corruption class.
* main was never in it - its pieces are already [all c ..., data, .rodata, data].
main's real defects were the config PATH (there is no config/splat.main.yaml,
it is splat.us.exe.yaml) and the FILE BASE: the EXE has a 0x800 header, so the
delta is 0x8000F800, not the yaml's first vram: 0x80010000, and with the naive
value payload_word silently read 0x800 early. Both fixed; the base comes from
the single derivation in family_remap.vram_of (R33).
The cfg_path class refusal is lifted and replaced by an operation-level one: a carve
whose table lies below the data region is in the leading island, which a tail carve
cannot reach, so build_carve refuses and names the island-split lane (R43).
tools/test_jtbl_parse_config.py proves all three, read-only:
NC-1 regression 171/171 configs byte-unchanged by the fix
NC-2 defect 42/42 md_* lose a c line under the historical derivation; 42/42
keep every c line under the fix
NC-3 main cfg=splat.us.exe.yaml base=0x8000F800, region starts after the last c
157 lines
7.4 KiB
Python
157 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
"""test_jtbl_parse_config.py — the regression + negative control for jtbl_carve.parse_config.
|
|
|
|
WHY THIS EXISTS (P31 S58/S59). `parse_config`'s docstring said "the trailing run of {data,
|
|
.rodata} pieces after the last `c` piece" and its code said `data_pieces[0]` — the first
|
|
data/.rodata piece anywhere in the file. Those agree on 171 configs and disagree on 42, because
|
|
the md_* modules open with the §154-A LEADING ISLAND `- [0x0, .rodata, md_XXX]` before their `c`
|
|
piece. On those, apply()'s splice `lines[:lo] + region + lines[hi:]` DELETED the `c` line and
|
|
wrote the yaml to disk before the tool errored out for unrelated reasons. S58 responded with a
|
|
blanket md_*/main refusal in cfg_path; S59 fixed the derivation and lifted it.
|
|
|
|
Three checks, all read-only (no config on disk is touched — the splice is simulated in memory):
|
|
|
|
NC-1 REGRESSION — on every config that is NOT in the corruption class, the fixed derivation
|
|
returns EXACTLY what the historical one returned. The overlay carve pipeline is byte-gated
|
|
and green; a "fix" that moves it is a regression, not a fix. The historical derivation is
|
|
re-implemented below (`_legacy_region_lo`) precisely so this comparison is possible.
|
|
|
|
NC-2 THE DEFECT — on every md_* config, the historical derivation is SHOWN to delete a `c`
|
|
piece line (the defect is demonstrated, not asserted from the failure signature), and the
|
|
fixed one is shown to preserve every `c` line.
|
|
|
|
NC-3 CLASS FACTS — main resolves to config/splat.us.exe.yaml with file base 0x8000F800 (not the
|
|
yaml's first `vram:` 0x80010000, which reads 0x800 early), and main is NOT in the
|
|
corruption class: its pieces are already [all c ..., data, .rodata, data].
|
|
|
|
Run: .venv/bin/python tools/test_jtbl_parse_config.py
|
|
"""
|
|
import glob
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import jtbl_carve as jc # noqa: E402
|
|
|
|
REPO = jc.REPO
|
|
FAIL = []
|
|
|
|
|
|
def alias_of(cfg):
|
|
"""binary alias from a config path — 'config/splat.<alias>.yaml', with the EXE's exception."""
|
|
base = os.path.basename(cfg)
|
|
return "main" if base == "splat.us.exe.yaml" else base[len("splat."):-len(".yaml")]
|
|
|
|
|
|
def pieces_of(lines):
|
|
"""[(line_idx, indent, off, kind, name)] — the same scan parse_config does."""
|
|
out = []
|
|
for i, ln in enumerate(lines):
|
|
m = jc.PIECE_RE.match(ln)
|
|
if m:
|
|
out.append((i, m.group(1), int(m.group(2), 16), m.group(3), m.group(4)))
|
|
return out
|
|
|
|
|
|
def _legacy_region_lo(pieces):
|
|
"""THE HISTORICAL DERIVATION (pre-S59), kept only so NC-1 can compare against it.
|
|
|
|
`data_pieces[0]` — the first data/.rodata piece anywhere in the file, regardless of where the
|
|
`c` pieces are."""
|
|
data = [p for p in pieces if p[3] in ("data", ".rodata")]
|
|
return data[0][0] if data else None
|
|
|
|
|
|
def check(cond, msg):
|
|
if not cond:
|
|
FAIL.append(msg)
|
|
return cond
|
|
|
|
|
|
def main():
|
|
cfgs = sorted(glob.glob(os.path.join(REPO, "config/splat.*.yaml")))
|
|
legacy_class, held_class = [], []
|
|
for cfg in cfgs:
|
|
lines = open(cfg).read().splitlines()
|
|
pieces = pieces_of(lines)
|
|
if not [p for p in pieces if p[3] in ("data", ".rodata")]:
|
|
continue # no data region at all — not our subject
|
|
c_idxs = [p[0] for p in pieces if p[3] == "c"]
|
|
lo_legacy = _legacy_region_lo(pieces)
|
|
(legacy_class if (c_idxs and lo_legacy < c_idxs[-1]) else held_class).append(cfg)
|
|
|
|
print(f"configs with a data region : {len(legacy_class) + len(held_class)}")
|
|
print(f" corruption class (md_*) : {len(legacy_class)}")
|
|
print(f" contract already held : {len(held_class)}")
|
|
check(len(legacy_class) == 42, f"NC: expected 42 configs in the corruption class, got {len(legacy_class)}")
|
|
check(all(os.path.basename(c).startswith("splat.md_") for c in legacy_class),
|
|
"NC: the corruption class must be exactly the md_* modules")
|
|
|
|
# ---- NC-1: the fix moves nothing outside the corruption class ------------------------------
|
|
moved = []
|
|
for cfg in held_class:
|
|
ov = alias_of(cfg)
|
|
lines = open(cfg).read().splitlines()
|
|
lo_legacy = _legacy_region_lo(pieces_of(lines))
|
|
lo_new = jc.parse_config(ov)[2]
|
|
if lo_legacy != lo_new:
|
|
moved.append((ov, lo_legacy, lo_new))
|
|
check(not moved, f"NC-1 REGRESSION: the region start moved on {moved}")
|
|
print(f"NC-1 regression : {len(held_class)}/{len(held_class)} configs unchanged by the fix")
|
|
|
|
# ---- NC-2: the defect, demonstrated, and the fix, demonstrated -----------------------------
|
|
demonstrated, preserved = 0, 0
|
|
for cfg in legacy_class:
|
|
ov = alias_of(cfg)
|
|
lines = open(cfg).read().splitlines()
|
|
pieces = pieces_of(lines)
|
|
c_lines = {ln for i, ln in enumerate(lines) if i in {p[0] for p in pieces if p[3] == "c"}}
|
|
|
|
_, _, lo_new, hi_new, tail_start, _, _, _ = jc.parse_config(ov)
|
|
lo_legacy = _legacy_region_lo(pieces)
|
|
|
|
# apply() splices lines[:lo] + <regenerated region> + lines[hi:]; a `c` line inside the
|
|
# replaced window is GONE from the file. hi is the same under both derivations.
|
|
legacy_after = set(lines[:lo_legacy]) | set(lines[hi_new:])
|
|
new_after = set(lines[:lo_new]) | set(lines[hi_new:])
|
|
lost_legacy = c_lines - legacy_after
|
|
lost_new = c_lines - new_after
|
|
if lost_legacy:
|
|
demonstrated += 1
|
|
if not lost_new:
|
|
preserved += 1
|
|
check(lost_legacy, f"NC-2 {ov}: the historical derivation was expected to delete a `c` line, it did not")
|
|
check(not lost_new, f"NC-2 {ov}: the FIXED derivation still deletes {sorted(lost_new)}")
|
|
# and the leading island must sit OUTSIDE the region the fix will rewrite
|
|
island = [p for p in pieces if p[3] == ".rodata" and p[2] == 0]
|
|
check(all(p[0] < lo_new for p in island),
|
|
f"NC-2 {ov}: the leading island piece is inside the rewritten region")
|
|
check(tail_start > 0, f"NC-2 {ov}: tail_start 0x{tail_start:x} — the region still starts at the island")
|
|
print(f"NC-2 defect : {demonstrated}/{len(legacy_class)} md_* configs LOSE a `c` line under the "
|
|
f"historical derivation; {preserved}/{len(legacy_class)} keep every `c` line under the fix")
|
|
|
|
# ---- NC-3: main's class facts --------------------------------------------------------------
|
|
check(jc.cfg_path("main").endswith("config/splat.us.exe.yaml"),
|
|
f"NC-3: main resolved to {jc.cfg_path('main')}")
|
|
base = jc.overlay_vram_base("main")
|
|
check(base == 0x8000F800, f"NC-3: main file base is 0x{base:x}, expected 0x8000F800")
|
|
check(all(alias_of(c) != "main" for c in legacy_class), "NC-3: main must NOT be in the corruption class")
|
|
lo_main = jc.parse_config("main")[2]
|
|
lines_main = open(jc.cfg_path("main")).read().splitlines()
|
|
c_last = max(p[0] for p in pieces_of(lines_main) if p[3] == "c")
|
|
check(lo_main > c_last, f"NC-3: main region starts at line {lo_main + 1}, last `c` at {c_last + 1}")
|
|
print(f"NC-3 main : cfg=config/splat.us.exe.yaml base=0x{base:x} "
|
|
f"region starts line {lo_main + 1} (last `c` line {c_last + 1})")
|
|
|
|
if FAIL:
|
|
print("\nFAILED:")
|
|
for f in FAIL:
|
|
print(" -", f)
|
|
return 1
|
|
print("\nALL CHECKS PASS")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|