#!/usr/bin/env python3 """Find the SELECTED TARGET inside the player object, and the input that changes it. The OPTIONS key-config screen lists `Change Target` and `Padlock Mode Toggle` as real bindable actions (docs/re/flight-controls-runtime.md), but a button sweep that watched the *ammo counters* could not see either — neither action fires a weapon. Screenshots of the reticle were no better: the view keeps moving, so "did the selection change" is not legible frame to frame. Guest memory is legible. If the craft holds a selected target, it holds a **pointer to that target's object**, and every live entity's address is already known from the entity scan. So: 1. enumerate live entities and their addresses; 2. read a window of the player object and keep every word that points at one of them (allowing a small fixed delta, since a pointer to an object's base is not a pointer to its transform); 3. tap each candidate input and see which of those words switches to a *different* entity. The word that follows the button is the selection, and the button that moves it is `Change Target`. Both answers come out of the same run, and neither depends on reading pixels. Usage: target_probe.py [seconds_per_button] """ import json import os import struct import sys import time import numpy as np sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import gmem # noqa: E402 import navigator # noqa: E402 from flight_probe import Pad # noqa: E402 BACK, FWD = 0x400, 0x1000 MAX_DELTA = 0x400 # how far below its transform an object's base may sit BUTTONS = ["LB", "X", "B", "A", "LS", "RS", "BACK", "START", "Y", "RB"] def main(): cfg = json.load(open(sys.argv[1])) dwell = float(sys.argv[2]) if len(sys.argv) > 2 else 1.2 W = navigator.World(cfg) ents = W.scan() me = [(off, va) for off, va in ents if "Player" in W.defs[va]] if not me: sys.exit("player entity not found — not in flight?") me_off = me[0][0] print(f"# player object at {gmem.primary_va(me_off):#010x}, " f"{len(ents)} live entities") # every address that could be a pointer to some entity ptr_map = {} for off, va in ents: pos_va = gmem.primary_va(off) if pos_va is None: continue for d in range(0, MAX_DELTA, 4): ptr_map.setdefault(pos_va - d, (W.defs[va], d, off)) def window(): b = os.pread(W.fd, BACK + FWD, me_off - BACK) return np.frombuffer(b, dtype=">u4").copy() # ---- global mode: the selection need not live in the player object at all. # Scan every mapped extent for words that hold an entity pointer, then see # which of THOSE follow a button. A word that is an entity pointer before # AND after, pointing at a different entity, is a selection by construction. keys = np.array(sorted(ptr_map), dtype=np.uint32) def global_ptrs(): out = {} for a0, b0 in gmem.extents(W.fd, W.size): n = (b0 - a0) // 4 * 4 if n < 64: continue arr = np.frombuffer(os.pread(W.fd, n, a0), dtype=">u4").astype(np.uint32) idx = np.flatnonzero(np.isin(arr, keys)) for k in idx: out[a0 + int(k) * 4] = int(arr[k]) return out # ---- delta mode: WHERE does an entity keep its target? # The AI ships clearly hold pointers to other entities, so the field is a # fixed offset from the transform — exactly the situation entities2.py # solved for the definition pointer. Tally, over many entities, the delta at # which a word points at *another* entity; the offset that repeats is the # field, and reading it on the PLAYER gives our selected target. if "--delta" in sys.argv: from collections import Counter votes, examples = Counter(), {} for off, va in ents: blob = os.pread(W.fd, 0x1000, max(0, off - 0x800)) arr = np.frombuffer(blob[:len(blob) // 4 * 4], dtype=">u4") for i, w in enumerate(arr): hit = ptr_map.get(int(w)) if hit and hit[2] != off: d = i * 4 - 0x800 votes[d] += 1 examples.setdefault(d, (W.defs[va], hit[0])) print(f"# target-pointer delta candidates over {len(ents)} entities:") for d, n in votes.most_common(10): src, dst = examples[d] print(f" pos{d:+#07x} seen {n:4d} e.g. {src[:26]:<26} -> {dst}") best = votes.most_common(1) if best: d = best[0][0] pw = struct.unpack(">I", os.pread(W.fd, 4, me_off + d))[0] hit = ptr_map.get(pw) print(f"\n# PLAYER at that delta: {pw:#010x} -> " f"{hit[0] if hit else 'not an entity pointer'}") return a = window() cands = [] for i, w in enumerate(a): hit = ptr_map.get(int(w)) if hit and hit[2] != me_off: # a self-pointer is not a target cands.append((i, hit[0], hit[1])) print(f"# {len(cands)} word(s) in the player object point at a live entity") for i, nm, d in cands[:30]: print(f" pos{i * 4 - BACK:+#07x} -> {nm} (entity_va - {d:#x})") if not cands: print("# none — widen BACK/FWD or MAX_DELTA, or nothing is selected") # ---- which input moves the selection? pad = Pad() if "--global" in sys.argv: print("\n# GLOBAL scan: every word in RAM that holds an entity pointer") g0 = global_ptrs() print(f"# {len(g0)} entity-pointer words in RAM") for btn in BUTTONS: pad.f.write(f"tap {btn} 200\n") time.sleep(dwell) g1 = global_ptrs() moved = [(o, g0[o], g1[o]) for o in g0 if o in g1 and g1[o] != g0[o]] named = [] for o, v0, v1 in moved[:4]: n0 = ptr_map.get(v0, ("?",))[0] n1 = ptr_map.get(v1, ("?",))[0] named.append(f"{gmem.primary_va(o):#010x} {n0[:16]}->{n1[:16]}") print(f" [{btn:<5}] {len(moved):4d} switched " + " ".join(named), flush=True) g0 = g1 pad.reset() return print("\n# tapping each input; a word that switches to a DIFFERENT entity is" " the selection") for btn in BUTTONS: before = window() pad.f.write(f"tap {btn} 200\n") time.sleep(dwell) after = window() moved = [] for i, nm, d in cands: if before[i] == after[i]: continue hit = ptr_map.get(int(after[i])) moved.append((i, nm, hit[0] if hit else f"{int(after[i]):#010x}")) tag = " ".join(f"pos{i * 4 - BACK:+#x} {o[:18]}->{n[:18]}" for i, o, n in moved[:3]) print(f" [{btn:<5}] {len(moved):2d} changed {tag}", flush=True) pad.reset() if __name__ == "__main__": main()