Files
Sylpheed/tools/re-capture/ob_scan.py
sim e909c7c133 chore: retire the last dead paths and names from the consolidation
Nothing here changes what a tool computes; it changes where tools look.

- tools/re-capture: 33 censuses globbed /work/sylph_extract, a path that has
  existed nowhere since /work became a clone, so they matched nothing and
  printed empty results. They now resolve the disc through a new disc.py
  from $SYLPHEED_DISC and exit loudly without it (the #44 fix, generalised).
  Nine scripts that imported siblings from the retired Reborn checkout or an
  old session scratchpad now import from their own directory. unitgroup.py
  only needs the variable when --pak is not given.
- sylpheed-xex: the loader only ever uses the XEX2 retail key. The dead
  devkit key and a doc comment claiming a devkit fallback that does not
  exist are gone; Project Sylpheed is a retail XEX2, so no XEX1 key either.
- sylpheed-viewer: real_font_rasterizes looked for /tmp/sylph_extract and so
  always skipped. It reads $SYLPHEED_DISC now, and passes against the disc.
- Comments and docs that named xenia-rs, the Reborn repository or /work/*.pe
  as places to look now name sylpheed.db, Canary's ppc_context.h and the
  flat .pe; docs/re/README.md no longer says the native Canary build does not
  run.

Historical records keep their original paths: findings that were measured
against /work/xenia-rs/sylpheed.db still say so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 22:30:28 +02:00

79 lines
3.0 KiB
Python

#!/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 <snapshot> <value> <out> # every aligned BE u32 == value
ob_scan.py filter <live-or-snap> <in> <v> <out> # keep those now equal to v
ob_scan.py show <candidates> # 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, os.path.dirname(os.path.abspath(__file__)))
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}")