diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 59fb706b..4bdb3462 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -609,6 +609,17 @@ search cannot find a *schedule*. *fighters*, so a marked-target counter should ignore turrets; it also rules out `REMAINING OB` being a general kill tally (it ignored ten turret deaths). ❔ Still needs 2–3 `e010` events; one clean 220 s run produced **zero**. +* ✅🔴 **(2026-08-24) Kill-free HUD route works; BE-`u32` assumption REFUTED.** + `ob_by_hud.py` reads the counter off screen (`ob_read.py`) and intersects heap + words equal to it — **no kills needed**. Four readings at HUD=4 narrowed + 6156→4312, then HUD read **11** and the intersection collapsed to **0**. ⇒ within + `0xBD000000–0xBE000000` as **big-endian u32 the counter does not exist**; it may + be u16/u8/LE or outside the region. Both previous hunts assumed BE-u32, so this + eliminates the assumption rather than just failing. 🟡 **The value went UP, 4→11**, + which a pure countdown should not do — candidates: wrong HUD cell, misread digits + (template strip covers only **0 1 2 4 8**; most samples read `00?`/`???`), or a + counter that can rise. **Next: widen the scan to u16/u8 and LE, and beyond the + heap** — one function, no combat cost. Also extend `ob_digits.png`. * ~~🚧 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 4a5b688c..02e154e3 100644 --- a/docs/re/remaining-ob-hunt.md +++ b/docs/re/remaining-ob-hunt.md @@ -156,3 +156,62 @@ minutes. One clean 220 s run this iteration produced **zero** `e010` kills, which is the limit stated plainly. + +--- + +# ✅ A kill-free route via the HUD — and it refutes the u32 assumption (2026-08-24) + +The correlation route is gated on marked-fighter kills, which the pilot gets at +about two per five minutes. But `ob_read.py` already reads the counter off the +screen, so the value can be matched against memory directly — no kills needed. + +`tools/re-capture/ob_by_hud.py`: screenshot → read the digits → keep heap words +equal to that value → intersect across readings. + +``` +t= 7s HUD=4 words==4: 6156 -> candidates 6156 +t= 42s HUD=4 words==4: 6256 -> candidates 5153 +t= 75s HUD=4 words==4: 6327 -> candidates 4620 +t=108s HUD=4 words==4: 6451 -> candidates 4312 +t=142s .. t=312s HUD unreadable ('00?', '??1', '???') +t=347s HUD=11 words==11: 1052 -> candidates 0 +``` + +## 🔴 Refuted: the counter is not a plain big-endian u32 in the entity heap + +Four readings at value 4 narrowed 6156 → 4312 — the expected slow drift. Then the +HUD read **11**, and the intersection collapsed to **zero**. + +A word that genuinely holds this counter must equal 4 at the first four samples +*and* 11 at the last. None does. So within `0xBD000000–0xBE000000`, read as +big-endian `u32`, **the counter does not exist**. It may be `u16`, `u8`, +little-endian, or simply outside that region. + +That is worth having: both hunts so far assumed BE-`u32` in the entity heap, and +that assumption is now eliminated rather than merely unproductive. + +## 🟡 The displayed value went UP, 4 → 11 + +Over ~340 s the counter *increased*. A pure countdown of remaining marked targets +should not do that — unless targets were added, which the deployment work says +does not happen for phase 1 +([mission-phase-deployment.md](mission-phase-deployment.md)). + +Possible readings, none tested: the cell being read is not `REMAINING OB`; the +digits are misread (the template strip only covers **0 1 2 4 8**, so 3/5/6/7/9 +come back as `?` — the many unreadable samples above); or the counter genuinely +counts something that can rise. + +The two clean readings scored 0.95–0.98 against their templates, so a misread of +those specific frames is unlikely — but "4" and "11" use only digits that *are* +in the strip, which is exactly the selection effect that would hide a wrong +reading. + +## Next + +Widen the encoding: search `u16` and `u8`, little-endian as well as big, and +beyond the entity heap. That is a change to one scan function, and unlike the +kill-driven route it costs no combat. + +Also worth extending `ob_digits.png` with the missing digits — most samples were +unreadable, which is why only two data points survived a 480 s run. diff --git a/tools/re-capture/ob_by_hud.py b/tools/re-capture/ob_by_hud.py new file mode 100755 index 00000000..c0fa24f4 --- /dev/null +++ b/tools/re-capture/ob_by_hud.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Locate REMAINING OB from the HUD value instead of from kill events. + +The correlation route works but needs marked-fighter kills, and the pilot gets +about two per five minutes (remaining-ob-hunt.md). The HUD already shows the +number, and ob_read.py already reads it, so match the displayed value against +memory directly -- no kills required. + +Each reading intersects: keep heap words equal to the value the HUD shows at +that moment. A second reading at a DIFFERENT value collapses the set hard; even +repeated readings at the same value help, since unrelated words drift. +""" +import os, sys, time, subprocess, collections +sys.path.insert(0, __file__.rsplit('/', 1)[0]) +import gmem, gworld +import numpy as np +import ob_read + +LO, HI = 0xBD000000, 0xBE000000 +SHOT = '/tmp/ob_hud.png' + +def region(fd): + lo, hi = gmem.va_to_off(LO), gmem.va_to_off(HI) + out, pos = bytearray(), lo + while pos < hi: + n = min(1 << 24, hi - pos); out += os.pread(fd, n, pos); pos += n + return np.frombuffer(bytes(out), dtype='>u4'), lo + +def hud_value(): + subprocess.run(['screenshot', SHOT], capture_output=True, timeout=60) + try: + txt, scores = ob_read.read(SHOT) + except Exception as e: + return None, str(e) + t = (txt or '').strip() + if not t.isdigit(): return None, 'unreadable %r' % txt + return int(t), 'scores %s' % (scores,) # ob_read returns tuples, not floats + +def main(): + secs = int(sys.argv[1]) if len(sys.argv) > 1 else 240 + every = int(sys.argv[2]) if len(sys.argv) > 2 else 25 + w = gworld.World(); fd = w.fd + cand = None; base = None; seen = [] + t0 = time.time() + while time.time() - t0 < secs: + v, note = hud_value() + el = round(time.time() - t0) + if v is None: + print(' t=%4ds HUD unreadable (%s)' % (el, note), flush=True) + else: + r, base = region(fd) + hit = set(np.nonzero(r == v)[0].tolist()) + cand = hit if cand is None else (cand & hit) + seen.append(v) + print(' t=%4ds HUD=%-4d words==%d: %-8d -> candidates %d (%s)' + % (el, v, v, len(hit), len(cand), note), flush=True) + if len(cand) <= 40 and len(set(seen)) >= 2: + break + time.sleep(every) + print('\nHUD values seen: %s' % sorted(set(seen))) + if cand and base is not None: + print('candidates: %d' % len(cand)) + for i in sorted(cand)[:20]: + va = gmem.primary_va(base + i * 4) + print(' va %s' % (('%#010x' % va) if va else '?')) + else: + print('no candidates (HUD never read, or value never matched a u32)') + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/re-capture/obhud_attach.sh b/tools/re-capture/obhud_attach.sh new file mode 100755 index 00000000..0c99929d --- /dev/null +++ b/tools/re-capture/obhud_attach.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)" +pgrep -x xenia_canary >/dev/null || { echo "NO EMULATOR"; exit 1; } +CFG=/tmp/nav-obhud2.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:-400}" /tmp/obhud2-pilot.log 2>&1 & + P=$!; echo "--- pilot re-attached"; break + fi +done +python3 "$SD/ob_by_hud.py" "${1:-400}" "${2:-25}" +[ -n "${P:-}" ] && kill "$P" 2>/dev/null +echo "OBHUD ATTACH DONE" diff --git a/tools/re-capture/obhud_session.sh b/tools/re-capture/obhud_session.sh new file mode 100755 index 00000000..f678e2ed --- /dev/null +++ b/tools/re-capture/obhud_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_by_hud.py" "${1:-240}" "${2:-25}" +[ -n "${P:-}" ] && kill "$P" 2>/dev/null +echo "OBHUD DONE"