diff --git a/docs/re/captures/ob-turret-control-timeline.png b/docs/re/captures/ob-turret-control-timeline.png new file mode 100644 index 00000000..b17a2eb2 Binary files /dev/null and b/docs/re/captures/ob-turret-control-timeline.png differ diff --git a/docs/re/ob-counts-marked-attackers.md b/docs/re/ob-counts-marked-attackers.md index de0b6199..886afb2b 100644 --- a/docs/re/ob-counts-marked-attackers.md +++ b/docs/re/ob-counts-marked-attackers.md @@ -341,3 +341,62 @@ distinct positions carry the player definition. Not investigated. A run in which a **marked** attacker demonstrably dies — which means logging the per-class roster before and after each kill, not just the ADAN total. The pilot already prefers `e010`; what is missing is the bookkeeping to prove it hit one. + +--- + +# 2026-08-27 (third run) — ✅ the turret CONTROL passes, arrival timing at n=3 + +`pilot.py` with `SYLPH_PREFER=e010` for 320 s (312 `fire=1` samples), with a +per-class live count logged every 11 s beside the HUD +([`class_count.py`](../../tools/re-capture/class_count.py)) and the mission clock +sampled throughout ([capture](captures/ob-turret-control-timeline.png)). + +## ✅ Killing turrets does NOT move the counter — control confirmed + +The live turret population fell monotonically while the counter only ever rose: + +``` +sample 0: e007=108 e010=16 OB 004 +sample 9: e007=105 e010=16 OB 008 +sample 21: e007=102 e010=16 OB 012 +sample 25: e007=101 e010=16 OB 012 +``` + +**Seven `e007` turrets died and `REMAINING OB` never decremented once.** This was +previously only *weakly* supported — inferred from a run whose kill log happened +to be turrets. It is now a measured control with the classes counted directly. + +## ✅ Arrival timing reproduces a third time + +| mission `TIME` | `OB` | | +|---|---|---| +| 01:48.73 → **02:05.54** | `004` → **`008`** | step in (108.7 s, 125.5 s] ∋ **120** | +| 03:24.16 → **03:41.70** | `008` → **`012`** | step in (204.2 s, 221.7 s] ∋ **210** | + +**n = 3.** Both predicted times are inside their brackets on every run so far. + +## ✅ The live `e010` baseline is exactly phase 1's roster + +The count sat at **16** in 20 of 26 samples — precisely the phase-1 `e010` +roster (`ADT102`, `ADT107`, `ADT113`, `ADS151`, each n=4). That is an independent +runtime corroboration of the static roster, and it locates the counter's 12 as +the three `A`-route squadrons within that 16. + +⚠️ **Measurement artifact, stated:** six samples read 17–28. The spikes are always +*upward* and never persist, and the counter dedups by a position triple read a +moment after the pattern scan, so an entity written between the two reads can be +counted twice. The stable floor of 16 is the signal; the spikes are noise in my +tool, not arrivals. + +## 🟡 The decrement is STILL unproven + +No marked attacker died in this run either: the `e010` floor never dropped below +16 despite 312 `fire=1` samples. So the counter had no opportunity to decrement, +and its stillness says nothing about the claim. + +Three runs have now failed to kill one `e010`. The blocker is combat +effectiveness, not instrumentation — `pilot-never-fires.md` and the +`SYLPH_PREFER` notes already record that this pilot lands few attacker kills +because turrets are nearer and far more numerous. ❔ Options not yet tried: +pick a lone attacker while the squadron is still inbound, or wait for the +wingmen/ACROPOLIS guns to kill one (which counts just as well). diff --git a/tools/re-capture/class_count.py b/tools/re-capture/class_count.py new file mode 100644 index 00000000..4c84c87e --- /dev/null +++ b/tools/re-capture/class_count.py @@ -0,0 +1,32 @@ +"""Fast per-class live count: search ONE definition pointer, dedup by position.""" +import sys, os, struct, time +sys.path.insert(0, '/tmp/rc') +import gmem, gworld, entities2 as E + +w = gworld.World() +defs = E.definitions(w) +want = {nm: pat for pat, nm in defs.items() + if nm in ('UN_e010_ADAN_Attacker_S', 'UN_e007_ADAN_Turret')} +lo, hi = gmem.va_to_off(E.ENT_VA_LO), gmem.va_to_off(E.ENT_VA_HI) + +def count(pat): + seen = set() + for a, b in gmem.extents(w.fd, w.size): + a, b = max(a, lo), min(b, hi) + if b <= a: continue + blob = os.pread(w.fd, b - a, a); i = 0 + while True: + j = blob.find(pat, i) + if j < 0: break + off = a + j + if off % 4 == 0: + p = off - 0x130 + d = os.pread(w.fd, 12, p) + if len(d) == 12: seen.add(struct.unpack('>fff', d)) + i = j + 4 + return len(seen) + +for n in range(int(sys.argv[1])): + row = ' '.join('%s=%d' % (k.split('_')[1], count(v)) for k, v in sorted(want.items())) + print('%2d %s' % (n, row), flush=True) + time.sleep(float(sys.argv[2]))