mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
1431b523a4
- recovered the engine actor/player struct cold via PCSX-Redux (R10/R11): base 0x80078E00, ~154 fields over 0x24C bytes, 32 pointer fields; HP/BP/gold/day/hour/position/BP-drain all live-verified and anchored to the Phase-3 player state + the walk diff. -> docs/actor-struct.md - tools/ram_probe.py: PCSX-Redux RAM capture/diff/read/ptr-scan workhorse for field-typing - FINDING (the matching verdict): feeding the struct to m2c as --context is BYTE-NEUTRAL. m2c adopts it (arg0->f_X) but structural-miss sample scores 0 better / 10 same / 2 worse vs m2c-alone (full code+RAM pointer set). The misses are 50-98% mismatched = m2c decompiler limits, not missing types. T4 fn-ptr context unblocks compilation but functions stay structural. - conclusion: rich context (struct/type recovery) is a comprehension / decomp.me win, NOT a byte-match lever -> Phase-16 wall re-confirmed from the type angle. struct-core-pivot.md updated. - emulator field-naming pass deferred (comprehension, off the matching path); NEXT: T6 permuter validation on the 146 genuine near-misses, then T7 go/no-go.
118 lines
5.2 KiB
Python
118 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""ram_probe.py — PCSX-Redux RAM capture / diff / read for actor-struct field-typing (T5; R10/R11).
|
|
|
|
Fetches the 2 MB main RAM over the PCSX-Redux web API (Windows-native emulator, bridged to WSL).
|
|
Dump offset i maps to PS1 vaddr 0x80000000 + i. Snapshots persist under .run/ram/ so we can diff
|
|
across the controlled scenarios Drew runs (walk/jump/swing/charge/damage/zone-change) and, per R10,
|
|
type each actor field (pointer vs scalar) + name it by which action moves it.
|
|
|
|
tools/ram_probe.py snapshot still # capture RAM -> .run/ram/still.bin
|
|
tools/ram_probe.py diff still walked --lo 0x80120000 --hi 0x80200000 # changed words A->B
|
|
tools/ram_probe.py read still 0x801a0014 --n 8 --width 4 # read values
|
|
tools/ram_probe.py ptrs still --lo 0x801a0000 --hi 0x801a0260 # offsets holding a RAM ptr
|
|
tools/ram_probe.py field still walked jumped --base 0x801a0000 # values at the inferred
|
|
# actor offsets across snaps
|
|
|
|
Default host 172.17.208.1:8081 (R11 NAT gateway); pass --host if the bridge IP changed (check SETUP §3).
|
|
"""
|
|
import argparse, os, sys, struct, urllib.request
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
RAMDIR = os.path.join(REPO, ".run/ram")
|
|
BASE = 0x80000000
|
|
RAM_SIZE = 2 * 1024 * 1024
|
|
PTR_LO, PTR_HI = 0x80010000, 0x80200000 # plausible main-RAM pointer range
|
|
|
|
|
|
def fetch(host):
|
|
url = f"http://{host}/api/v1/cpu/ram/raw"
|
|
with urllib.request.urlopen(url, timeout=15) as r:
|
|
data = r.read()
|
|
if len(data) != RAM_SIZE:
|
|
sys.stderr.write(f"[warn] got {len(data)} bytes (expected {RAM_SIZE})\n")
|
|
return data
|
|
|
|
|
|
def load(name):
|
|
p = os.path.join(RAMDIR, name + ".bin")
|
|
if not os.path.exists(p):
|
|
sys.exit(f"no snapshot {name} ({p})")
|
|
return open(p, "rb").read()
|
|
|
|
|
|
def vrange(a, lo, hi):
|
|
lo = max(lo - BASE, 0); hi = min(hi - BASE, len(a))
|
|
return lo, hi
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("cmd", choices=["snapshot", "diff", "read", "ptrs", "field"])
|
|
ap.add_argument("args", nargs="*")
|
|
ap.add_argument("--host", default="172.17.208.1:8081")
|
|
ap.add_argument("--lo", type=lambda x: int(x, 0), default=BASE)
|
|
ap.add_argument("--hi", type=lambda x: int(x, 0), default=BASE + RAM_SIZE)
|
|
ap.add_argument("--n", type=int, default=4)
|
|
ap.add_argument("--width", type=int, default=4, choices=(1, 2, 4))
|
|
ap.add_argument("--base", type=lambda x: int(x, 0), help="actor base vaddr (field cmd)")
|
|
a = ap.parse_args()
|
|
os.makedirs(RAMDIR, exist_ok=True)
|
|
|
|
if a.cmd == "snapshot":
|
|
name = a.args[0] if a.args else "snap"
|
|
try:
|
|
data = fetch(a.host)
|
|
except Exception as e:
|
|
sys.exit(f"FETCH FAILED ({e}). Is PCSX-Redux running BFM with the web server on {a.host}?")
|
|
open(os.path.join(RAMDIR, name + ".bin"), "wb").write(data)
|
|
print(f"captured {len(data)} B -> .run/ram/{name}.bin")
|
|
|
|
elif a.cmd == "diff":
|
|
x, y = load(a.args[0]), load(a.args[1])
|
|
lo, hi = vrange(x, a.lo, a.hi)
|
|
w = a.width
|
|
changed = []
|
|
for off in range(lo, hi - w + 1, w):
|
|
if x[off:off + w] != y[off:off + w]:
|
|
xv = int.from_bytes(x[off:off + w], "little")
|
|
yv = int.from_bytes(y[off:off + w], "little")
|
|
changed.append((BASE + off, xv, yv))
|
|
print(f"{len(changed)} changed {w}-byte words in [0x{a.lo:X},0x{a.hi:X}) ({a.args[0]} -> {a.args[1]})")
|
|
for v, xv, yv in changed[:80]:
|
|
print(f" 0x{v:08X}: {xv:#010x} -> {yv:#010x} ({xv} -> {yv})")
|
|
if len(changed) > 80:
|
|
print(f" ... +{len(changed)-80} more")
|
|
|
|
elif a.cmd == "read":
|
|
x = load(a.args[0]); va = int(a.args[1], 0); off = va - BASE; w = a.width
|
|
for i in range(a.n):
|
|
o = off + i * w
|
|
v = int.from_bytes(x[o:o + w], "little")
|
|
tag = " <ptr?>" if PTR_LO <= v < PTR_HI else ""
|
|
print(f" 0x{BASE+o:08X} (+0x{i*w:X}): {v:#0{2+w*2}x} ({v}){tag}")
|
|
|
|
elif a.cmd == "ptrs":
|
|
x = load(a.args[0]); lo, hi = vrange(x, a.lo, a.hi)
|
|
for off in range(lo, hi - 3, 4):
|
|
v = int.from_bytes(x[off:off + 4], "little")
|
|
if PTR_LO <= v < PTR_HI:
|
|
print(f" 0x{BASE+off:08X}: -> {v:#010x}")
|
|
|
|
elif a.cmd == "field":
|
|
if not a.base:
|
|
sys.exit("field: pass --base <actor vaddr>")
|
|
snaps = [(n, load(n)) for n in a.args]
|
|
# the inferred actor offsets (from .run/wall_taxonomy struct inference); fallback to a dense scan
|
|
offs = list(range(0, 0x250, 4))
|
|
print(f"actor @ 0x{a.base:08X}; values across {', '.join(n for n,_ in snaps)} (w=4):")
|
|
for fo in offs:
|
|
o = a.base - BASE + fo
|
|
vals = [int.from_bytes(d[o:o + 4], "little") for _, d in snaps]
|
|
if len(set(vals)) > 1 or any(PTR_LO <= v < PTR_HI for v in vals):
|
|
tag = " <ptr?>" if any(PTR_LO <= v < PTR_HI for v in vals) else (" *changes*" if len(set(vals)) > 1 else "")
|
|
print(f" +0x{fo:03X}: " + " ".join(f"{v:#010x}" for v in vals) + tag)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|