pilot: target commitment takes kills from 2 to 9; and the target pointer does not exist
The loop re-scored every contact every tick, so the nose chased whichever fighter scored best that instant and aim error wandered 10-40 deg through a pass. A missile lock is time-on-target, so constant switching is the one thing guaranteed to prevent a kill. Commit to a contact until it dies, passes 6000, sits >90 deg off the nose for 2.5 s, or 14 s elapse. Same guns, same ballistics, same escort weighting, same missile cadence: kills 0000 (five gun-only runs) -> 0002 (missiles) -> 0009 (commitment), with 101 missiles vs 98, and the largest hostile-population fall of any run (134->97). Own hull untouched. The ACROPOLIS still ended at 76.6%, so this is lethality, not the mission outcome. Also records a negative result so it is not re-attempted: the selected target is NOT a raw entity pointer. Three searches came up empty — a +-0x1400 window of the player object, a full-RAM sweep of every entity-pointer word tapped through each button (only thread-stack slots churn, which is frame noise), and a delta tally over all 150 entities of the kind that found the definition pointer at +0x130. The selection must be a handle, an index, or in a subsystem outside the entity object.
This commit is contained in:
BIN
docs/re/captures/kills-target-commitment.png
Normal file
BIN
docs/re/captures/kills-target-commitment.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 156 KiB |
@@ -177,6 +177,43 @@ that the game expects a *lock* — holding the target in the reticle before laun
|
||||
an unlocked launch is wasted. Reading the lock state (or the lock timer) out of RAM is
|
||||
the next step, and it is the same anchoring trick as everything else here.
|
||||
|
||||
## Target commitment — the change that actually moved kills
|
||||
|
||||
The pilot re-scored every contact every tick, so the nose chased whichever fighter was
|
||||
momentarily best-scoring and the aim error wandered 10–40° through a pass. Since a
|
||||
missile lock is time-on-target, constant switching is the one thing guaranteed to
|
||||
prevent a kill. **Commitment**: stay on the chosen contact until it dies, gets beyond
|
||||
6000, sits >90° off the nose for 2.5 s, or 14 s elapse.
|
||||
|
||||
Nothing else changed — same guns, same ballistics, same escort weighting, same missile
|
||||
cadence:
|
||||
|
||||
| run | kills (`WARPLANES`) | missiles | hostiles |
|
||||
|---|---|---|---|
|
||||
| gun-only × 5 | **0000** | 0 | 147→118 … 134→166 |
|
||||
| + guided missiles | **0002** | 98 | 134→104 |
|
||||
| + **target commitment** | **0009** | 101 | **134→97** |
|
||||
|
||||
4.5× the kills for the same ammunition, and the largest fall in hostile population of
|
||||
any run. Our own hull finished untouched at 1500/1500.
|
||||
|
||||
**The escort is still not saved** — the ACROPOLIS finished at 76.6 % — so this improves
|
||||
lethality, not the mission outcome, and the two should not be conflated.
|
||||
|
||||
### Negative result: the selected target is not a raw entity pointer
|
||||
|
||||
Worth recording so it is not re-attempted. `target_probe.py` looked for the selection
|
||||
three ways: (1) every word in a ±0x1400 window of the player object that points at a
|
||||
live entity — **none**; (2) every word in *all* of RAM holding an entity pointer, tapped
|
||||
through each button — only thread-stack slots (`0x70xx_xxxx`) churned, which is frame
|
||||
noise, not selection; (3) a delta tally over all 150 entities looking for a repeated
|
||||
offset holding a pointer to *another* entity, the same trick that found the definition
|
||||
pointer at `+0x130` — **zero candidates**.
|
||||
|
||||
So neither the player nor the AI ships keep a raw pointer to their target near their
|
||||
transform. The selection is a handle, an index, or lives in a targeting subsystem
|
||||
outside the entity object.
|
||||
|
||||
## Reimplementation notes
|
||||
|
||||
- Defeat conditions for an escort stage are readable as: protected-asset
|
||||
|
||||
@@ -119,6 +119,16 @@ class Pilot:
|
||||
HULL_CLEARANCE = 800.0 # clearance ON TOP of a capital ship's own radius
|
||||
CLOSING_WEIGHT = 4.0 # s of closing-rate credit when ranking attackers
|
||||
MY_RANGE_WEIGHT = 0.35 # how much our own distance discounts a target
|
||||
# --- target commitment ---
|
||||
# The loop re-scored every contact every tick, so the nose chased whichever
|
||||
# fighter was momentarily best and the aim error wandered 10-40 deg through
|
||||
# a pass. A missile lock is time-on-target (the OPTIONS screen calls it
|
||||
# Padlock), so switching targets constantly is the one thing guaranteed to
|
||||
# prevent a kill. Stay on the chosen contact until it dies, leaves range, or
|
||||
# sits behind us long enough that chasing it is pointless.
|
||||
COMMIT_MAX = 14.0 # s before we are allowed to reconsider anyway
|
||||
COMMIT_DROP = 6000.0 # ...or it gets this far away
|
||||
COMMIT_BEHIND = 2.5 # ...or stays >90 deg off the nose this long
|
||||
|
||||
def __init__(self, W, pad, dry=False, log=sys.stdout):
|
||||
self.W = W
|
||||
@@ -145,6 +155,9 @@ class Pilot:
|
||||
self.missile_down = False
|
||||
self.missile_t = -1e9
|
||||
self.missiles = 0
|
||||
self.commit_off = None # entity we are committed to
|
||||
self.commit_t = -1e9
|
||||
self.behind_since = None
|
||||
|
||||
def f32(self, off):
|
||||
b = os.pread(self.W.fd, 4, off)
|
||||
@@ -226,6 +239,33 @@ class Pilot:
|
||||
return self.CONE_MAX
|
||||
return min(self.CONE_MAX, max(self.FIRE_CONE, math.atan2(r + SHELL_RADIUS, d)))
|
||||
|
||||
def pick_committed(self, t, me_p, me_v, fwd, hos):
|
||||
"""pick(), but stay on the same contact long enough to actually kill it."""
|
||||
cur = None
|
||||
for off, nm, p, v, r, hard in hos:
|
||||
if off == self.commit_off and not hard:
|
||||
cur = (off, nm, p, v, r)
|
||||
break
|
||||
if cur is not None:
|
||||
off, nm, p, v, r = cur
|
||||
rel = p - me_p
|
||||
d = float(np.linalg.norm(rel))
|
||||
behind = ang(rel, fwd) > math.pi / 2
|
||||
self.behind_since = (self.behind_since if behind else None) or (t if behind else None)
|
||||
stale = (t - self.commit_t > self.COMMIT_MAX
|
||||
or d > self.COMMIT_DROP
|
||||
or (self.behind_since is not None
|
||||
and t - self.behind_since > self.COMMIT_BEHIND))
|
||||
if not stale:
|
||||
lead = self.lead_point(p, v, d)
|
||||
return (off, nm, lead, lead - me_p, d, r)
|
||||
# commit to a fresh one
|
||||
tgt = self.pick(me_p, me_v, fwd, hos)
|
||||
self.commit_off = tgt[0] if tgt else None
|
||||
self.commit_t = t
|
||||
self.behind_since = None
|
||||
return tgt
|
||||
|
||||
def pick(self, me_p, me_v, fwd, hos):
|
||||
"""Nearest *fighter*, weighted by how far off the nose it is."""
|
||||
best, bestscore = None, 1e18
|
||||
@@ -343,7 +383,7 @@ class Pilot:
|
||||
else:
|
||||
self.mode = "ENGAGE"
|
||||
|
||||
tgt = self.pick(me_p, me_v, fwd, hos)
|
||||
tgt = self.pick_committed(t, me_p, me_v, fwd, hos)
|
||||
push, worst = self.av.avoidance(me_p, me_v, me_r, ents, me_off)
|
||||
|
||||
if self.mode == "EVADE":
|
||||
|
||||
173
tools/re-capture/target_probe.py
Normal file
173
tools/re-capture/target_probe.py
Normal file
@@ -0,0 +1,173 @@
|
||||
#!/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 <config.json> [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()
|
||||
Reference in New Issue
Block a user