Files
Sylpheed/tools/re-capture/ob_sample.py
Sylpheed RE agent 12d516729e tools: sample a candidate REMAINING OB address against the HUD in one run
ob_sample.py pairs a live read of a guest VA with a screenshot and a crop of the
HUD counter, and reads the word again AFTER the shot so a sample whose two reads
disagree can be thrown away instead of believed -- that race is what left the
first three-snapshot filter with zero survivors.

ob_session.sh is fly_session.sh plus that sampler, because the evidence this
needs is a transition, and an unattended craft is dead in about a minute. Its
header records that launch_mission.sh does not yet finish unattended on restored
state, so nobody discovers that halfway through a boot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-23 17:15:36 +00:00

80 lines
2.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Sample a candidate `REMAINING OB` address paired with a HUD screenshot.
`structures/mission-objective-counter.md` found the counter at a guest VA in one
Stage 02 run and left cross-run stability untested. Testing it needs the RAM word
and the HUD digits from the SAME moment, which is the trap that already cost one
pass: the value moves between the memory read and the screenshot that is supposed
to confirm it. So every sample reads the word, takes the shot, and reads the word
AGAIN — a row whose two reads disagree is thrown away rather than believed.
The HUD counter is cropped out of each shot (the region is fixed: the plate sits
in the top-right, x >= 0.72 W, 0.32 H .. 0.45 H) so the digits can be read without
squinting at a 1280x720 frame.
Usage: ob_sample.py <va> <out.csv> <shot-dir> <tag> [interval_s] [count]
"""
import os
import struct
import subprocess
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import gmem # noqa: E402
CROP = (0.72, 0.32, 1.0, 0.45) # HUD "REMAINING OB" plate, fractions of W/H
def read_word(path, off):
try:
with open(path, "rb") as f:
f.seek(off)
b = f.read(4)
return struct.unpack(">I", b)[0] if len(b) == 4 else None
except OSError:
return None
def crop_hud(shot, out):
try:
from PIL import Image
except ImportError:
return False
im = Image.open(shot)
w, h = im.size
c = im.crop((int(CROP[0] * w), int(CROP[1] * h), int(CROP[2] * w), int(CROP[3] * h)))
c.resize((c.width * 2, c.height * 2)).save(out)
return True
def main():
va = int(sys.argv[1], 0)
out_csv, shot_dir, tag = sys.argv[2], sys.argv[3], sys.argv[4]
interval = float(sys.argv[5]) if len(sys.argv) > 5 else 30.0
count = int(sys.argv[6]) if len(sys.argv) > 6 else 8
off = gmem.va_to_off(va)
mem = gmem.mem_path()
os.makedirs(shot_dir, exist_ok=True)
t0 = time.time()
with open(out_csv, "w") as f:
f.write("t_s,ram_before,ram_after,stable,shot\n")
for i in range(count):
before = read_word(mem, off)
shot = os.path.join(shot_dir, f"{tag}-{i:02d}.png")
subprocess.run(["screenshot", shot], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
after = read_word(mem, off)
crop_hud(shot, os.path.join(shot_dir, f"{tag}-{i:02d}-ob.png"))
row = (f"{time.time()-t0:.1f},{before},{after},"
f"{int(before is not None and before == after)},{shot}")
f.write(row + "\n")
f.flush()
print(row, flush=True)
time.sleep(max(0.0, interval - (time.time() - t0) % interval))
if __name__ == "__main__":
main()