#!/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 [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()