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"