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>
79 lines
3.0 KiB
Python
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}")
|