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_neighbourhood.py
Sylpheed RE agent ac17398515 formats+tools: the counter's neighbours are its own rendered digits
With the per-entity searches refuted at word and bit level, the question became
which object owns the counter. Sampling +-0x200 around it across a 4->8
transition: the control interval moved 0 of 256 words, and the step moved nine -
the counter plus four words holding ASCII '4' -> '8' NUL-padded, and four
pointers into 0xbcad2xxx that swap with them. Read live at HUD 008, all four
character slots hold '8'.

So the neighbourhood is the HUD's rendered text for this counter, which reframes
the address: it is the HUD widget's value rather than "the mission's own
objective counter" as this file called it.

Recorded against that, because it is already measured: there is no separate
mission-side copy moving on the same step. ob_hunt scans all of guest memory and
requires a match across two transitions, and it left exactly one address.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 09:48:57 +00:00

104 lines
3.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Which words NEXT TO `REMAINING OB` move with it?
Both the word-level and the bit-level per-entity searches are refuted
(`mission-freeze-and-ob-flag.md`): nothing in an entity object tracks the
counter. So the counter belongs to something else — and it sits at `0xbdb59668`,
inside the entity heap window, which means the object that owns it is right
there to be read.
This samples a window around the counter across one of its transitions and
reports the words that changed **with** it. A mission-script object should have
neighbours that move together: a total, a wave index, a timer.
The discipline is the same as everywhere else here: the counter's own address is
confirmed against the HUD first, and a word is only interesting if it changes on
the same step the counter does — a word that changes every sample is noise.
Usage: ob_neighbourhood.py <out.json> [radius_bytes] [timeout_s]
"""
import json
import os
import struct
import subprocess
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import gmem # noqa: E402
import ob_read # noqa: E402
KNOWN_VAS = [0xBDB59668, 0xBDB49668, 0xBDB58668]
def hud(shot):
subprocess.run(["screenshot", shot], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
txt, _ = ob_read.read(shot)
return int(txt) if txt.isdigit() else None
def main():
out = sys.argv[1]
radius = int(sys.argv[2], 0) if len(sys.argv) > 2 else 0x200
deadline = time.time() + (float(sys.argv[3]) if len(sys.argv) > 3 else 420)
fd = os.open(gmem.mem_path(), os.O_RDONLY)
v = None
for _ in range(8):
v = hud("/tmp/obn.png")
if v is not None:
break
time.sleep(3)
va = next((a for a in KNOWN_VAS
if struct.unpack(">I", os.pread(fd, 4, gmem.va_to_off(a)))[0] == v), None)
if v is None or va is None:
print(f"HUD={v}: no known address holds it — re-scan with ob_hunt.py")
return 2
base = gmem.va_to_off(va) - radius
print(f"counter {v} at {va:#x}; window {va - radius:#x}..{va + radius:#x}",
flush=True)
def window():
b = os.pread(fd, radius * 2, base)
return [struct.unpack_from(">I", b, i)[0] for i in range(0, len(b), 4)]
a = window()
n0 = a[radius // 4]
# A word that changes on EVERY sample is noise, not a neighbour: take a
# mid-sample while the counter is still n0 and discard anything that moved.
time.sleep(8)
mid = window()
noisy = {i for i, (x, y) in enumerate(zip(a, mid)) if x != y}
print(f"words that move even while the counter is still {n0}: {len(noisy)}",
flush=True)
while time.time() < deadline:
time.sleep(4)
cur = window()
if cur[radius // 4] != n0:
break
else:
print("counter never moved")
return 1
n1 = cur[radius // 4]
vb = hud("/tmp/obn2.png")
print(f"counter {n0} -> {n1} (HUD {vb})", flush=True)
moved = [i for i, (x, y) in enumerate(zip(mid, cur)) if x != y and i not in noisy]
rows = [{"va": va - radius + i * 4, "delta_from_counter": (i * 4) - radius,
"before": mid[i], "after": cur[i]} for i in moved]
print(f"words that moved WITH the counter (excluding the noisy ones): {len(rows)}")
for r in rows[:40]:
print(f" {r['va']:#010x} counter{r['delta_from_counter']:+#07x} "
f"{r['before']:#010x} -> {r['after']:#010x}"
f" ({r['before']} -> {r['after']})")
json.dump({"counter_va": va, "n0": n0, "n1": n1, "hud_b": vb,
"noisy_words": len(noisy), "moved": rows}, open(out, "w"), indent=1)
print(f"wrote {out}")
return 0
if __name__ == "__main__":
raise SystemExit(main())