#!/usr/bin/env python3 """Value-scan guest memory for a changing counter — used to find REMAINING OB. The mission's objective counter is on the HUD, so it is in RAM, and finding it turns "shoot whatever is nearest" into "shoot what closes the mission" (autopilot-memory-driven.md problem #2). It was found with this, in three passes: ob_scan.py scan # every aligned BE u32 == value ob_scan.py filter # keep those now equal to v ob_scan.py show # offsets as guest VAs Only the FIRST pass needs a 4.8 GB snapshot; every later pass reads just the candidate offsets, so it can run straight against /dev/shm/xenia_memory_*. Two traps this encodes, both paid for: * **Filter on a CHANGE, not on a repeat.** A three-snapshot filter that required 19 -> 19 -> 18 left zero survivors, because the value moved between the memory copy and the screenshot that read it. Scanning one value and filtering on the NEXT distinct value found it immediately. * **Verify across a LATER change.** The first single candidate this produced (0xbc22e83c) matched the transition it was filtered on and was still WRONG — read live it held 26 while the HUD showed 017. A candidate is only believable once it tracks a transition it was not selected by. """ import os, struct, sys sys.path.insert(0, "/work/Syplheed-Reborn/tools/re-capture") import gmem def scan(path, val): """Every 4-byte-aligned offset whose BE u32 == val.""" out = [] pat = struct.pack(">I", val) size = os.path.getsize(path) with open(path, "rb") as f: for start, end in gmem.extents(f.fileno(), size): pos = start & ~3 while pos < end: f.seek(pos) buf = f.read(min(1 << 24, end - pos)) if not buf: break i = buf.find(pat) while i != -1: if (pos + i) % 4 == 0: out.append(pos + i) i = buf.find(pat, i + 1) pos += len(buf) return out def read_at(path, offs, ): vals = {} with open(path, "rb") as f: for o in offs: f.seek(o); b = f.read(4) if len(b) == 4: vals[o] = struct.unpack(">I", b)[0] return vals if __name__ == "__main__": cmd = sys.argv[1] if cmd == "scan": offs = scan(sys.argv[2], int(sys.argv[3])) print(len(offs)) with open(sys.argv[4], "w") as f: for o in offs: f.write(f"{o}\n") elif cmd == "filter": offs = [int(l) for l in open(sys.argv[3])] want = int(sys.argv[4]) vals = read_at(sys.argv[2], offs) keep = [o for o, v in vals.items() if v == want] print(len(keep)) with open(sys.argv[5], "w") as f: for o in keep: f.write(f"{o}\n") elif cmd == "show": for l in open(sys.argv[2]): o = int(l) va = gmem.primary_va(o) print(f"off {o:#013x} va {va:#010x}" if va else f"off {o:#013x}")