diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 1f7eb16..39bf7fc 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -347,8 +347,17 @@ search cannot find a *schedule*. Fan-in: only **41 of 116** records have craft (2×24, 4×1, 8×4, 18×12 = 300). ⇒ **an arrival is craft appearing for a record that had none; a kill is that count decreasing — both attributable to a named squadron**, hence to a route. - **Next:** watch PER-RECORD craft counts over a mission (not the global total); - that is the kill-vs-no-kill test with squadron resolution. + 🔴 **BLOCKER (2026-08-24): the per-record measurement does not reproduce.** + [`mission-per-record-strength.md`](mission-per-record-strength.md) — one run + gives 116 records/300 craft, the next 42/170, same disc, save and script. Save + drift is REFUTED (savedata untouched since 2026-08-23) and the guest was not + frozen (pilot telemetry shows live engagement). Also: **zero arrivals and zero + losses in 190 s of hunting** — weak evidence against clock arrivals at + t=90/120/170 s, and no test of event-gating at all because nothing was killed + (`fc=0`, hull never moved). **Next: (1)** sample the roster-record count + repeatedly WITHIN one run to tell a load race from a run-specific constant; + **(2)** get a confirmed kill — the pilot reaches 259 m and still misses, so the + gap is accuracy, not engagement. * 🟡 **COMPETING MODEL (user, 2026-08-24): waves are event-gated, not clock-driven** — released by kills/objectives rather than at a fixed time, with the route's start time being a post-trigger delay. Fits the evidence better: diff --git a/docs/re/mission-per-record-strength.md b/docs/re/mission-per-record-strength.md new file mode 100644 index 0000000..2a351f2 --- /dev/null +++ b/docs/re/mission-per-record-strength.md @@ -0,0 +1,74 @@ +# Per-record craft strength — the measurement works, the run does not reproduce + +Status: ✅ the per-record measurement is internally consistent; 🔴 it does **not** +reproduce across runs, and that is now the blocker; 🔴 zero arrivals and zero +losses in 190 s of hunting, which does not test the event-gated model. + +Follows [roster-to-craft-link.md](roster-to-craft-link.md): craft point at +`roster_base + 0x08`, so a member's live strength is how many craft point at it, +and an arrival should be a `0 → n` transition on one record. + +## ✅ The measurement itself is sound + +`tools/re-capture/wave5_probe.py` counted craft per roster record every ~20 s. +The first sample: + +``` +craft=170 deployed records=24/42 strength histogram [(2, 11), (8, 2), (12, 11)] +``` + +11·2 + 2·8 + 11·12 = **170**, exactly the craft total. The per-record decomposition +is internally consistent, and 24 of 42 records deployed with 18 holding nothing +is the same shape as the previous run's 41 of 116. + +## 🔴 But the numbers do not reproduce across runs + +| run | roster records | craft | deployed | strengths | +|---|---|---|---|---| +| link2 (earlier) | **116** | **300** | 41 | 2, 4, 8, **18** | +| wave5 (this one) | **42** | **170** | 24 | 2, 8, **12** | + +Same disc, same save slot, same launch script, same stage. + +**The save has not drifted** — that was the first suspicion and it is refuted: +`content/.../game01/savedata` is unmodified since 2026-08-23 16:37; only the +profile `.gpd` files were written today. + +**The guest was not frozen** — the pilot's telemetry over the same 190 s shows +speed varying across dozens of values and a live engagement (`tgt=e007_ADAN_Turret` +at 259–680 m), so the game was advancing normally. + +So the discrepancy is real and unexplained. Candidates, none tested: + +* the probe samples at a different point in the mission's deployment and the + roster-record set is still being built (argued against by the counts being + *completely static* for 190 s afterwards — if it were still loading, it should + have grown toward 116); +* `gmem.extents()` races the emulator's allocation, so the vtable scan sees a + different set of extents between runs; +* deployment strength genuinely varies per run (turret squadrons at 18 vs 12). + +Until this is understood, **per-record counts cannot be used as a time series**, +and no cross-run comparison of them means anything. This is the same discipline +already recorded for the global craft count, now needed one level down. + +## 🔴 Zero arrivals, zero losses in 190 s + +Every sample was identical: 170 craft, 24 deployed, no `0 → n`, no `n → n−1`. + +* **Against the clock model**, weakly: if phase-1 arrivals were at t = 90/120/170 + seconds, a `0 → n` should have appeared, and none did. +* **It does not test the event-gated model at all**, because nothing was killed. + The pilot engaged continuously — closing to 259 m on turrets — but `fc=0`, no + record lost strength, and the player's own hull never moved off 1500. It shot + and missed, which `pilot.py`'s own notes say has happened before. + +## What is needed next + +1. **Explain the 42-vs-116 discrepancy** before trusting any per-record series. + Cheapest test: sample the roster-record count repeatedly *within one run*, + starting the instant flight is detected. If it climbs, it is a load race; if + it is flat at a run-specific value, it is not. +2. **Get a confirmed kill.** Without one, the kill-versus-no-kill experiment + cannot run. The pilot reaches firing range and does not destroy anything, so + the gap is accuracy, not engagement. diff --git a/tools/re-capture/wave5_probe.py b/tools/re-capture/wave5_probe.py new file mode 100755 index 0000000..285b4f0 --- /dev/null +++ b/tools/re-capture/wave5_probe.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Per-record craft counts over a mission: catch arrivals and kills by squadron. + +The link is a pointer at roster_base + 0x08, verified 300/300 by independent +unit-type agreement (roster-to-craft-link.md). So a roster member's live +strength is simply how many craft point at it: + + 0 -> n the member deployed (an ARRIVAL) + n -> n-1 one of its craft died (a KILL) + +Both are attributable to a UnitGroup member, which is what ties an observation +back to Route_S.tbl. This watches those counts instead of any global total. +""" +import os, sys, time, struct, collections, importlib.util +SD = __file__.rsplit('/', 1)[0] +sys.path.insert(0, SD) +import gmem, gworld, entities2 +_w3 = importlib.util.spec_from_file_location('w3', SD + '/wave3_probe.py') +wave3 = importlib.util.module_from_spec(_w3); _w3.loader.exec_module(wave3) + +ROSTER_VT = struct.pack('>I', 0x820AF030) +DELTA, WIN, LINK = 0x130, 0x400, 0x08 + +def scan_vt(fd, size, vt): + out = [] + for a, b in gmem.extents(fd, size): + pos = a + while pos < b: + n = min(1 << 24, b - pos) + blob = os.pread(fd, n, pos) + i = blob.find(vt) + while i != -1: + if (pos + i) % 4 == 0: out.append(pos + i) + i = blob.find(vt, i + 1) + pos += n + return sorted(out) + +def counts(fd, defs, want): + """craft per roster VA, this instant""" + lo, hi = gmem.va_to_off(entities2.ENT_VA_LO), gmem.va_to_off(entities2.ENT_VA_HI) + per = collections.Counter(); pos = lo; n = 0 + while pos < hi: + m = min(1 << 24, hi - pos) + blob = os.pread(fd, m, pos) + for k in range(0, len(blob) - 3, 4): + if blob[k:k+4] in defs: + base = pos + k - DELTA + n += 1 + head = os.pread(fd, WIN, base) + for j in range(0, len(head) - 3, 4): + (p,) = struct.unpack_from('>I', head, j) + if p in want: + per[want[p]] += 1 + break + pos += m + return per, n + +def main(): + secs = int(sys.argv[1]) if len(sys.argv) > 1 else 200 + every = int(sys.argv[2]) if len(sys.argv) > 2 else 15 + w = gworld.World(); fd = w.fd + defs = entities2.definitions(w) + if not defs: print('NOT IN A MISSION'); return 2 + roster = scan_vt(fd, w.size, ROSTER_VT) + f = os.fdopen(os.dup(fd), 'rb') + label, want = {}, {} + for o in roster: + va = gmem.primary_va(o) + if va is None: continue + label[va] = wave3.resolve_id(f, w.size, o)[0] or '?' + want[va + LINK] = va + print('roster records: %d' % len(label)) + + log = open('/tmp/wave5-per-record.tsv', 'w') + log.write('t\tva\tunit\tfrom\tto\n') + prev, total0 = None, None + t0 = time.time() + while time.time() - t0 < secs: + per, n = counts(fd, defs, want) + el = round(time.time() - t0) + if prev is None: + print('t=%4ds craft=%d deployed records=%d/%d strength histogram %s' + % (el, n, len(per), len(label), + sorted(collections.Counter(per.values()).items()))) + else: + arr = [(va, per[va]) for va in per if prev.get(va, 0) == 0 and per[va] > 0] + kil = [(va, prev[va], per[va]) for va in prev + if per.get(va, 0) < prev[va]] + print('t=%4ds craft=%d deployed=%d ARRIVALS=%d losses=%d' + % (el, n, len(per), len(arr), len(kil)), flush=True) + for va, c in arr: + print(' ARRIVAL %#010x %-30s 0 -> %d' % (va, label.get(va, '?'), c)) + log.write('%d\t%#x\t%s\t0\t%d\n' % (el, va, label.get(va, '?'), c)) + for va, a, b in kil: + print(' loss %#010x %-30s %d -> %d' % (va, label.get(va, '?'), a, b)) + log.write('%d\t%#x\t%s\t%d\t%d\n' % (el, va, label.get(va, '?'), a, b)) + log.flush() + prev = per + time.sleep(max(0, every - (time.time() - t0 - el))) + log.close() + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/re-capture/wave5_session.sh b/tools/re-capture/wave5_session.sh new file mode 100755 index 0000000..a35d47e --- /dev/null +++ b/tools/re-capture/wave5_session.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -u +export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98 +export PYTHONPATH=/sylph-home/.local/lib/python3.12/site-packages +SD="$(cd "$(dirname "$0")" && pwd)" +SECS="${1:-180}"; EVERY="${2:-10}"; HUNT="${3:-1}" +CFG=/tmp/nav-live.json +"$SD/launch_mission.sh" fly || { echo "BOOT FAILED"; exit 1; } +if python3 "$SD/entities2.py" self 0x130 "$CFG" >/dev/null 2>&1; then + SYLPH_HUNT="$HUNT" SYLPH_KILL_TURRETS=1 nohup python3 "$SD/pilot.py" "$CFG" "$SECS" \ + /tmp/live-pilot.log 2>&1 & + PILOT=$!; echo "--- pilot (SYLPH_HUNT=$HUNT)" +else PILOT=""; echo "--- BIND FAILED, no pilot"; fi +python3 "$SD/wave5_probe.py" ; rc=$? +[ -n "$PILOT" ] && kill "$PILOT" 2>/dev/null +echo "LIVENESS DONE rc=$rc"