#!/usr/bin/env python3 """Watch REMAINING OB (big-endian u32 at 0xbdb59668) across a mission. The address is confirmed (remaining-ob-hunt.md), so this costs one 4-byte read per sample instead of a 32 MB scan -- cheap enough to sample often and run long without provoking the freezes heavy probes cause. Two things this does that earlier probes did not: * **verify the address before trusting it.** It was once recorded as run-dependent, so the watcher requires at least one confident HUD reading to agree with memory before it reports anything. * **gate the HUD reader on confidence.** ob_read returns (best, second) per digit and those scores were printed but never checked; one misread poisoned an entire intersection. Accept a reading only if every digit scores >= 0.80 with a >= 0.05 margin -- the rule ob_read's own docstring states. """ import os, struct, subprocess, sys, time sys.path.insert(0, __file__.rsplit('/', 1)[0]) from probeharness import Probe import gmem, ob_read OB_VA = 0xBDB59668 SHOT = '/tmp/ob_watch.png' FLOOR, MARGIN = 0.80, 0.05 def hud_confident(): subprocess.run(['screenshot', SHOT], capture_output=True, timeout=60) try: txt, scores = ob_read.read(SHOT) except Exception: return None t = (txt or '').strip() if not t.isdigit() or not scores: return None for s in scores: best, second = (s if isinstance(s, (tuple, list)) else (s, 0.0))[:2] if best < FLOOR or (best - second) < MARGIN: return None return int(t) def main(): secs = int(sys.argv[1]) if len(sys.argv) > 1 else 400 every = int(sys.argv[2]) if len(sys.argv) > 2 else 5 p = Probe(baseline=116) if not p.ok: print(p.why); return 3 print(p.summary(), flush=True) off = gmem.va_to_off(OB_VA) if off is None: print('OB VA does not map'); return 4 read = lambda: struct.unpack('>I', os.pread(p.fd, 4, off))[0] # confirm the address on this run before reporting anything from it ok = False for _ in range(6): h = hud_confident() m = read() if h is not None: print('confirm: HUD=%d mem=%d %s' % (h, m, 'MATCH' if h == m else 'MISMATCH'), flush=True) if h == m: ok = True break time.sleep(5) if not ok: # The address is run-dependent -- 0xbdb59668 held 3165285888 on a fresh # launch while the HUD showed 4. So find it on THIS run before watching: # intersect heap positions equal to the confidently-read HUD value until # one survives. Same method as ob_hunt2, inline, so the watcher is # self-sufficient instead of depending on a lucky address. import numpy as np print('address not valid this run -- hunting it', flush=True) cand = None while p.tick(every=20, secs=secs): h = hud_confident() if h is None: print('t=%4ds HUD not confidently readable%s' % (p.elapsed, p.status()), flush=True) continue buf, hbase = p.heap() a = np.frombuffer(buf, dtype='>u4') hit = np.nonzero(a == h)[0].astype(np.int64) * 4 cand = hit if cand is None else np.intersect1d(cand, hit, assume_unique=True) print('t=%4ds HUD=%-4d candidates=%d%s' % (p.elapsed, h, len(cand), p.status()), flush=True) if len(cand) == 1: off = hbase + int(cand[0]) va = gmem.primary_va(off) print('FOUND this run: va %s' % (('%#010x' % va) if va else '?'), flush=True) read = lambda: struct.unpack('>I', os.pread(p.fd, 4, off))[0] ok = True break if len(cand) == 0: print('intersection empty -- a HUD reading disagreed with every ' 'candidate; restarting the hunt', flush=True) cand = None if not ok: print('ADDRESS NOT FOUND on this run -- not reporting a series it ' 'might not describe.') return 5 p.log('t\tob\tstalled', '/tmp/ob_watch.tsv') prev = read() print('t= 0s OB=%d' % prev, flush=True) p.emit('0\t%d\t' % prev) while p.tick(every, secs): v = read() if v != prev: print('t=%4ds OB %d -> %d (%+d)%s' % (p.elapsed, prev, v, v - prev, p.status()), flush=True) prev = v p.emit('%d\t%d\t%s' % (p.elapsed, v, p.status().strip())) print('\n%s' % p.summary()) print('final OB=%d' % prev) return 0 if __name__ == '__main__': sys.exit(main())