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
This commit is contained in:
103
tools/re-capture/ob_neighbourhood.py
Executable file
103
tools/re-capture/ob_neighbourhood.py
Executable file
@@ -0,0 +1,103 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user