The mission's objective counter is a big-endian u32 at guest VA 0xbdb59668 in a Stage 02 run on the upstream baseline. It was selected on an 18->24 transition and then tracked 24->23->22 against the HUD on its own - four readings, two changes it was not filtered on. That last point is the whole discipline here, because the first attempt failed it. An earlier differential over 19->18 also produced exactly one candidate, 0xbc22e83c, which matched the transition it was selected on and was still wrong: read live it held 26 while the HUD showed 017. One matching transition is not evidence. A second trap is recorded too: a three-snapshot filter requiring 19 -> 19 -> 18 left ZERO survivors, because the value moves between the memory copy and the screenshot that reads it. Filtering on the next DISTINCT value instead found the counter on the first try. ob_scan.py carries the method: scan one snapshot, then filter the candidate set against live /dev/shm/xenia_memory_* at each new value, so only the first pass needs a 4.8 GB copy. Stated plainly as unsettled: the ADDRESS is from one run and cross-run stability is untested, so the durable result is the method rather than the number. And what the counter counts - whether every OB-badged entity is one of them, and whether that badge is a flag in the entity object - is the follow-on the pilot actually needs to CHOOSE targets rather than just know how many remain.
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, "/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}")
|