From 696119b9e6c456c7ad0ee75c330cf283195a679a Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Mon, 24 Aug 2026 22:58:55 +0000 Subject: [PATCH] re: kill-free HUD route for REMAINING OB; big-endian u32 assumption refuted The correlation route is gated on marked-fighter kills, which the pilot manages at about two per five minutes. ob_read.py already reads the counter off the screen, so ob_by_hud.py matches the displayed value against memory directly and needs no kills at all: screenshot, read the digits, keep heap words equal to that value, intersect across readings. Four readings at value 4 narrowed 6156 candidates to 4312, the expected slow drift. Then the HUD read 11 and the intersection collapsed to zero. A word holding this counter must equal 4 at the first four samples and 11 at the last, and none does, so within the entity heap read as big-endian u32 the counter does not exist. It may be u16, u8, little-endian, or outside that region. Both previous hunts assumed big-endian u32 there, so this eliminates the assumption rather than merely failing to find anything. The displayed value also went up, from 4 to 11 over about 340 seconds. A pure countdown of remaining marked targets should not rise, and the deployment work says phase 1 gains no new participants. Three readings are possible and none is tested: the cell being read is not REMAINING OB, the digits are misread, or the counter genuinely counts something that can increase. The two clean readings scored 0.95 to 0.98 against their templates, but 4 and 11 use only digits that are in the strip, which is exactly the selection effect that would hide a wrong reading -- the template set covers 0 1 2 4 8 only, and most samples came back unreadable. Next is widening the scan to u16 and u8 and to little-endian, and beyond the entity heap, which is a change to one function and costs no combat. Extending ob_digits.png with the missing digits would also raise the sample yield, since only two of eleven readings in a 480 s run were usable. --- docs/re/BACKLOG.md | 11 +++++ docs/re/remaining-ob-hunt.md | 59 +++++++++++++++++++++++++ tools/re-capture/ob_by_hud.py | 71 +++++++++++++++++++++++++++++++ tools/re-capture/obhud_attach.sh | 19 +++++++++ tools/re-capture/obhud_session.sh | 19 +++++++++ 5 files changed, 179 insertions(+) create mode 100755 tools/re-capture/ob_by_hud.py create mode 100755 tools/re-capture/obhud_attach.sh create mode 100755 tools/re-capture/obhud_session.sh 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"