Three things from one Stage 02 run. The address recurs a third time: HUD=4 RAM=4 at 0xbdb59668, so 3 of the 5 runs measured put the counter exactly there. The counter is NOT a live class head-count. With the counter at 4 the typed entity list was 8 attackers, 7 friendly Delta Sabers, 7 turrets and the player - no class has 4 members and no pair of them sums to 4. That sharpens the corpus's existing "012 against 118 live ADAN" note from "not the hostile count" to "not the count of any class this enumeration can see". The flag experiment itself proves nothing, and why is the useful part. It found 20 offsets where exactly 4 of 23 entities agree, then reported "the counter never moved" for 600 s. The guest had stopped advancing ten seconds into flight: pilot.py logged 724 s of identical speed/yaw/pitch, and two screenshots six seconds apart were byte-identical, max delta 0 over 863325 pixels - while screen_id said "flight", the emulator burned 212% CPU and every liveness check passed. So that was a fact about a dead world. Withdrawn along with it: the claim in ob_session.sh that the counter climbs on its own in the first minutes, which one advancing run supports and this one cannot. frozen.py makes it a single call, checked in both directions (0 on the frozen pair, 254 on two frames of a live run), and ob_hunt/ob_flag now say GUEST FROZEN rather than waiting out their timeouts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
112 lines
4.2 KiB
Python
Executable File
112 lines
4.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Find `REMAINING OB` in RAM by value-scanning against the HUD, automatically.
|
|
|
|
The method is `ob_scan.py`'s, and the two traps it records are what this
|
|
automates away, because both were paid twice by hand:
|
|
|
|
* **Confirm the HUD on BOTH sides of the scan.** The scan itself takes ~0.9 s,
|
|
but the counter climbs 004 -> 008 -> 012 within the first minutes of Stage 02,
|
|
and a scan taken across a step selects nothing. A pass whose before- and
|
|
after-readings disagree is thrown away and retried rather than used.
|
|
* **Verify across a transition you did NOT select on.** The first filter only
|
|
narrows; the SECOND one is the evidence. Two hand attempts each managed one
|
|
filter and then lost the counter — once because the pilot stopped killing
|
|
anything, once because the mission ended.
|
|
|
|
The HUD is read by `ob_read.py`, which returns `?` rather than a wrong digit, so
|
|
an unreadable frame (explosion, GAME OVER) stalls the hunt instead of poisoning
|
|
it.
|
|
|
|
Usage: ob_hunt.py <out-dir> [transitions] [timeout_s]
|
|
"""
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import frozen # noqa: E402
|
|
import gmem # noqa: E402
|
|
import ob_read # noqa: E402
|
|
import ob_scan # noqa: E402
|
|
|
|
|
|
def hud(shot):
|
|
subprocess.run(["screenshot", shot], stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL)
|
|
txt, _ = ob_read.read(shot)
|
|
return int(txt) if txt.isdigit() else None
|
|
|
|
|
|
def main():
|
|
out = sys.argv[1]
|
|
want = int(sys.argv[2]) if len(sys.argv) > 2 else 2
|
|
deadline = time.time() + (float(sys.argv[3]) if len(sys.argv) > 3 else 900)
|
|
os.makedirs(out, exist_ok=True)
|
|
mem = gmem.mem_path()
|
|
log = lambda *a: print(*a, flush=True)
|
|
|
|
# --- the scan, with the HUD confirmed on both sides -----------------------
|
|
cands = None
|
|
while cands is None and time.time() < deadline:
|
|
v0 = hud(f"{out}/scan-before.png")
|
|
if v0 is None:
|
|
time.sleep(3)
|
|
continue
|
|
offs = ob_scan.scan(mem, v0)
|
|
v1 = hud(f"{out}/scan-after.png")
|
|
if v1 != v0:
|
|
log(f"scan at {v0} discarded: HUD moved to {v1} across it")
|
|
continue
|
|
cands = offs
|
|
log(f"scanned at {v0:03d}: {len(cands)} candidates")
|
|
if cands is None:
|
|
log("never got a clean scan")
|
|
return 1
|
|
|
|
# --- filter on each following transition ---------------------------------
|
|
prev, done, results = v0, 0, []
|
|
stuck = 0
|
|
while done < want and time.time() < deadline:
|
|
time.sleep(5)
|
|
v = hud(f"{out}/poll.png")
|
|
if v is None or v == prev:
|
|
# "the counter never moved" is a claim about the GAME; a frozen
|
|
# guest makes it a claim about a dead world instead, and the run
|
|
# then burns its whole timeout saying nothing. Measured once: the
|
|
# world stopped ~10 s into flight and everything downstream --
|
|
# screen_id, the liveness check, the CPU -- stayed happy.
|
|
stuck += 1
|
|
if stuck % 12 == 0 and frozen.frozen(6.0)[0]:
|
|
log("GUEST FROZEN — the world stopped advancing; abandoning")
|
|
break
|
|
continue
|
|
stuck = 0
|
|
# Confirm the new value before spending a filter on it.
|
|
time.sleep(1.5)
|
|
if hud(f"{out}/poll2.png") != v:
|
|
continue
|
|
vals = ob_scan.read_at(mem, cands)
|
|
cands = [o for o, x in vals.items() if x == v]
|
|
done += 1
|
|
kind = "SELECTED ON" if done == 1 else "verification (unselected)"
|
|
log(f"{prev:03d} -> {v:03d} [{kind}] {len(cands)} survive")
|
|
results.append({"from": prev, "to": v, "survivors": len(cands)})
|
|
prev = v
|
|
if not cands:
|
|
log("no survivors — the scan value or a reading was wrong")
|
|
break
|
|
|
|
payload = {"scan_value": v0, "transitions": results,
|
|
"survivors": [{"off": o, "va": gmem.primary_va(o)} for o in sorted(cands)]}
|
|
json.dump(payload, open(f"{out}/hunt.json", "w"), indent=1)
|
|
for o in sorted(cands):
|
|
log(f" va {gmem.primary_va(o):#x}")
|
|
log(f"wrote {out}/hunt.json")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|