From 7c5a4f84d46235fc3bbfc487627027de60b03e37 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Tue, 25 Aug 2026 06:02:33 +0000 Subject: [PATCH] re: OB address is run-dependent; watcher hunts it, HUD reader gated on confidence ob_watch.py verifies the address against a confidence-gated HUD reading before reporting anything, and two consecutive fresh launches settle the question: 0xbdb59668 held 3165285888 against a HUD of 4 on one, and exactly 4 on the next. The address is not stable across launches, the old note that it recurs in about five runs of seven was right, and the gate did its job by refusing to report a series from an address that did not describe that run. The watcher now hunts the address on the current run when confirmation fails, using the same intersection method, so it no longer depends on a lucky launch. The HUD reader is also gated now. ob_read returns a best and second score per digit and those were printed but never checked, which is how one misread poisoned an intersection and produced a wrong refutation of big-endian u32. A reading is accepted only if every digit scores at least 0.80 with a margin of at least 0.05, the rule ob_read's own docstring states. The measurement itself is a negative. With a clean witness, zero stalled samples of fifty, OB held at 4 for 250 seconds while the pilot targeted e010 for 1964 ticks and fired on 1635 of them. Constant fire at the marked attackers and not one decrement, so it destroyed none, which matches the roughly two marked kills per five minutes measured earlier. The fire rate itself rose from 4.6 % of ticks in an earlier diagnosis to 83 % here without producing more kills. Recorded as unreproduced rather than explained away: the run that found the address saw the counter rise 4, 8, 12 over five minutes, and that reading was confirmed against the HUD. This run was flat over a comparable window. Both observations are sound and they disagree, so the rise is not a stable property of the mission's first five minutes and presumably depends on progress this run never reached. --- docs/re/BACKLOG.md | 13 +++ docs/re/remaining-ob-hunt.md | 59 ++++++++++++++ tools/re-capture/ob_watch.py | 119 ++++++++++++++++++++++++++++ tools/re-capture/obwatch_session.sh | 19 +++++ 4 files changed, 210 insertions(+) create mode 100755 tools/re-capture/ob_watch.py create mode 100755 tools/re-capture/obwatch_session.sh diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index b711b52b..df4689dc 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -657,6 +657,19 @@ search cannot find a *schedule*. objective targets are being **added during the mission** — the arrival question again, now with a one-word signal instead of a 32 MB scan. **Next: watch this address across a whole mission.** +* ✅🔴 **(2026-08-25) OB address is RUN-DEPENDENT; watcher now self-sufficient.** + Two fresh launches: `mem@0xbdb59668` = **3165285888 (MISMATCH)** then **4 + (MATCH)** against HUD=4. The confirmation gate refused to report from the bad + one — the old "recurs in ~5 of 7 runs" note is right. `ob_watch.py` now **hunts + the address on the current run** when confirmation fails. ✅ HUD reader is now + **confidence-gated** (every digit ≥0.80, margin ≥0.05 — the rule `ob_read`'s own + docstring states), closing the hole that produced the wrong `u32be` refutation. + 🔴 **OB flat at 4 for 250 s** (witness clean, 0/50 stalled) while the pilot + fired on **1635 of 1964 ticks** at marked attackers — constant fire, zero + decrements, so it destroyed none. Fire rate itself rose from 4.6% to 83% with + no more kills. ❔ **The earlier 4→8→12 rise is NOT reproduced** — both readings + were HUD-confirmed, so the rise is not a stable property of the first five + minutes; recorded as unreproduced rather than explained away. * ~~🚧 BLOCKER: t=210/240 unreachable in one turn~~ — **superseded, see above**; it rested on an untested assumption that a turn is one shell call. 595 s shell cap − ~220 s boot (a ~190 s title movie that cannot be tapped through) − ~25 s startup = **~350 s observation ≈ 193 game-seconds**. diff --git a/docs/re/remaining-ob-hunt.md b/docs/re/remaining-ob-hunt.md index 9f344de4..c783bdf2 100644 --- a/docs/re/remaining-ob-hunt.md +++ b/docs/re/remaining-ob-hunt.md @@ -328,3 +328,62 @@ targets as the mission proceeds. Watching this one address across a mission is now the obvious next experiment, and it costs almost nothing. + +--- + +# The address is run-dependent; the watcher now finds it itself (2026-08-25) + +## ✅ Confirmed run-dependent + +`ob_watch.py` verifies the address against a **confidence-gated** HUD reading +before reporting anything. Two consecutive fresh launches: + +``` +run A confirm: HUD=4 mem@0xbdb59668=3165285888 MISMATCH +run B confirm: HUD=4 mem@0xbdb59668=4 MATCH +``` + +So `0xbdb59668` is **not stable across launches** — it held garbage on one and +the true counter on the next. The old note that it "recurs in about 5 runs of 7" +was right, and the gate did its job: run A refused to report a series from an +address that did not describe it. + +The watcher now **hunts the address on the current run** when confirmation fails, +by the same intersection method, so it is self-sufficient rather than depending +on a lucky launch. + +## ✅ The confidence gate is in + +`ob_read` returns `(best, second)` per digit and those scores were printed but +never checked — one misread poisoned an entire intersection and produced a wrong +refutation. A reading is now accepted only if **every digit scores ≥ 0.80 with a +≥ 0.05 margin**, which is the rule `ob_read`'s own docstring states. + +## 🔴 OB did not move in 250 s, and the pilot fired 1635 times + +Run B, witness clean (0 stalled samples of 50): + +``` +OB = 4 for all 50 samples +pilot: 1964 ticks targeting e010, fire=1 on 1635 of them +``` + +Constant fire at the marked attackers and **not one decrement**. The simplest +reading is that it destroyed none of them — consistent with the combat limit +recorded in [mission-objectives-text.md](mission-objectives-text.md), and with +the earlier measurement of roughly two marked kills per five minutes. + +Worth noting the fire rate itself changed: an earlier diagnosis measured firing +on **4.6 %** of ticks, this run on **83 %**. Whatever drives that, more shooting +did not produce more kills. + +## ❔ The 4 → 8 → 12 rise is not reproduced + +The run that found the address saw the counter go 4 → 8 → 12 over five minutes, +and that reading was itself confirmed against the HUD (`012` vs `mem=12`). This +run was flat at 4 for a comparable window. + +Both observations are sound and they disagree, so the rise is **not a stable +property of the first five minutes**. It presumably depends on mission progress +this run never reached. Recorded as unreproduced rather than folded into either +story. diff --git a/tools/re-capture/ob_watch.py b/tools/re-capture/ob_watch.py new file mode 100755 index 00000000..79583366 --- /dev/null +++ b/tools/re-capture/ob_watch.py @@ -0,0 +1,119 @@ +#!/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()) diff --git a/tools/re-capture/obwatch_session.sh b/tools/re-capture/obwatch_session.sh new file mode 100755 index 00000000..c8572f93 --- /dev/null +++ b/tools/re-capture/obwatch_session.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -u +export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98 +export PYTHONPATH=/sylph-home/.local/lib/python3.12/site-packages +SD="$(cd "$(dirname "$0")" && pwd)" +"$SD/launch_mission.sh" fly || { echo "BOOT FAILED"; exit 1; } +CFG=/tmp/nav-obhud.json +for try in 1 2 3; do + python3 "$SD/pad.py" set "rt=1" >/dev/null 2>&1 || true; sleep 3 + python3 "$SD/pad.py" clear >/dev/null 2>&1 || true + if python3 "$SD/entities2.py" self 0x130 "$CFG" >/dev/null 2>&1; then + SYLPH_HUNT=1 SYLPH_KILL_TURRETS=1 SYLPH_KEEPOUT=1400 SYLPH_PREFER=e010 \ + nohup python3 "$SD/pilot.py" "$CFG" "${1:-240}" /tmp/obhud-pilot.log 2>&1 & + P=$!; echo "--- pilot flying"; break + fi +done +python3 "$SD/ob_watch.py" "${1:-240}" "${2:-25}" +[ -n "${P:-}" ] && kill "$P" 2>/dev/null +echo "OBHUD DONE"