This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/tools/re-capture/ob_hunt2.py
Sylpheed RE agent d1154d7ca6 re: REMAINING OB found and verified — big-endian u32 at 0xbdb59668
Ported onto the shared harness, the HUD changed from 4 to 8 and the intersection
collapsed in a single step: one u32be survivor at 0xbdb59668, with the u16be and
u8 hits at 0xbdb5966a and 0xbdb5966b being the low half and low byte of that same
word.

Verified live rather than asserted. Reading screenshot and memory together three
times, the one legible HUD frame showed 012 against mem@0xbdb59668 = 12; the
other two frames were unreadable rather than mismatched. This independently
rediscovers the address the earliest sessions found by digit-transition hunting,
by a completely different method.

It also corrects an earlier conclusion. A previous run intersected HUD readings
of 4 and then 11, got zero survivors in every encoding, and that was written up
as eliminating big-endian u32 for the whole region. This run shows u32be holds
the counter, so the refutation was wrong. The likely cause is the input:
ob_digits.png has templates for 0 1 2 4 8 only, so values containing other
digits are misread rather than rejected, and "11" was probably one of those. A
single bad reading poisons an intersection permanently, because it removes the
true address and nothing later can restore it. The lesson is that an
intersection method needs individually verifiable inputs -- the reader's
confidence scores were printed but never gated on.

One observation reopens the arrival question in a useful way: the counter
increases, 4 then 8 then 12 across about five minutes, measured in memory so not
a digit misread. A count of remaining marked targets that rises means targets
are being added during the mission. That does not contradict the deployment
finding, since the roster is fixed at load, but it does mean the game marks new
objective targets as the mission proceeds. Watching this one address across a
whole mission is now the obvious next experiment and costs almost nothing.
2026-08-25 05:40:30 +00:00

97 lines
3.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""REMAINING OB by HUD value, on the shared harness.
Same idea as ob_by_hud.py -- read the counter off screen, keep heap positions
equal to it, intersect across readings -- but the witness, the incremental save
and the baseline discard now come from probeharness.Probe rather than being
re-implemented (and, twice, forgotten).
Candidates persist to /tmp/ob_candidates.json after EVERY reading, so a chained
attach continues the same intersection on the same mission. Offsets are only
valid within one emulator instance; the session clears the file at launch.
"""
import json, os, subprocess, sys
sys.path.insert(0, __file__.rsplit('/', 1)[0])
from probeharness import Probe
import gmem
import numpy as np
import ob_read
SHOT = '/tmp/ob_hud.png'
CAND = '/tmp/ob_candidates.json'
ENCODINGS = [('u32be', '>u4', 4, 0), ('u32le', '<u4', 4, 0),
('u16be', '>u2', 2, 0), ('u16be@1', '>u2', 2, 1),
('u16le', '<u2', 2, 0), ('u16le@1', '<u2', 2, 1),
('u8', 'u1', 1, 0)]
def hud():
subprocess.run(['screenshot', SHOT], capture_output=True, timeout=60)
try:
txt, _ = ob_read.read(SHOT)
except Exception as e:
return None, str(e)
t = (txt or '').strip()
return (int(t), 'ok') if t.isdigit() else (None, 'unreadable %r' % txt)
def matches(buf, value):
out = {}
for name, dt, w, skew in ENCODINGS:
if value > (1 << (8 * w)) - 1:
continue
n = (len(buf) - skew) // w * w
a = np.frombuffer(buf[skew:skew + n], dtype=dt)
out[name] = (np.nonzero(a == value)[0].astype(np.int64) * w + skew)
return out
def main():
secs = int(sys.argv[1]) if len(sys.argv) > 1 else 300
every = int(sys.argv[2]) if len(sys.argv) > 2 else 25
p = Probe(baseline=116)
if not p.ok:
print(p.why); return 3
print(p.summary(), flush=True)
p.log('t\thud\tencoding\tcandidates\tstalled', '/tmp/ob_hunt2.tsv')
cand, base, seen = None, None, []
if os.environ.get('SYLPH_OB_RESUME') == '1' and os.path.exists(CAND):
raw = json.load(open(CAND))
cand = {k: np.array(v, dtype=np.int64) for k, v in raw['cand'].items()}
seen = raw['seen']
print('resumed: %s' % {k: len(v) for k, v in cand.items()}, flush=True)
while True:
v, note = hud()
buf, base = p.heap()
st = p.status()
if v is None:
print('t=%4ds HUD unreadable (%s)%s' % (p.elapsed, note, st), flush=True)
else:
hit = matches(buf, v)
cand = hit if cand is None else {
k: np.intersect1d(cand[k], hit[k], assume_unique=True)
for k in cand if k in hit}
seen.append(v)
json.dump({'cand': {k: v2.tolist() for k, v2 in cand.items()},
'seen': seen}, open(CAND, 'w'))
desc = ' '.join('%s:%d' % (k, len(cand[k])) for k in cand)
print('t=%4ds HUD=%-4d %s%s' % (p.elapsed, v, desc, st), flush=True)
for k in cand:
p.emit('%d\t%d\t%s\t%d\t%s' % (p.elapsed, v, k, len(cand[k]), st.strip()))
if not p.tick(every, secs):
break
print('\n%s' % p.summary())
print('HUD values seen: %s' % sorted(set(seen)))
if cand:
for k in sorted(cand, key=lambda k: len(cand[k])):
c = cand[k]
print(' %-8s %d' % (k, len(c)))
if 0 < len(c) <= 8 and len(set(seen)) >= 2:
for off in c:
va = gmem.primary_va(base + int(off))
print(' va %s' % (('%#010x' % va) if va else '?'))
return 0
if __name__ == '__main__':
sys.exit(main())