diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index e1bd15a..c3d696c 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -353,7 +353,17 @@ search cannot find a *schedule*. distinct VAs (116 == 116), so the VA-aliasing explanation is 🔴 refuted too. 🟡 The single 42/170 run stays unexplained; rule adopted: **discard a run that disagrees with 116 rather than interpreting it, and reproduce any finding in - ≥2 runs.** ❔ No arrival seen in any run yet. + ≥2 runs.** +* ✅🔴 **Six runs, no arrival — plus an accidental control** + ([`mission-arrival-watch.md`](mission-arrival-watch.md), 2026-08-24). Deployment + reproduces byte-for-byte (116 records, 300 craft, 41 deployed, strengths + 2×24/4×1/8×4/18×12). ✅ **Losses require the player**: an unpiloted run held at + exactly 300 craft for 240 s / 22 samples, vs 16–20 losses in each piloted run — + so NPC crossfire destroys nothing on its own. 🔴 **Zero `0→n` arrivals in either + condition**, ~15 min cumulative. ❔ **Prime suspect: the phase clock may not be + advancing at all** — never checked. **Next: find the mission timer** (HUD digits + or a fixed-rate counter); if it is stopped, every arrival result so far is + measuring a stopped clock. Earlier framing: [`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 diff --git a/docs/re/mission-arrival-watch.md b/docs/re/mission-arrival-watch.md new file mode 100644 index 0000000..9883248 --- /dev/null +++ b/docs/re/mission-arrival-watch.md @@ -0,0 +1,70 @@ +# Six runs, no arrival — and an accidental control + +Status: ✅ the deployment structure reproduces exactly; ✅ losses require the +player; 🔴 **no arrival has ever been observed**, across six runs; ❔ whether +mission/phase time is advancing at all is now the prime suspect and is untested. + +## ✅ The deployment structure reproduces byte-for-byte + +`tools/re-capture/wave6_probe.py` refuses to interpret a run whose roster count +is not the reproduced baseline of 116 (the discard rule from +[mission-per-record-strength.md](mission-per-record-strength.md)). This run +passed, and its first sample is *identical* to the earlier link run: + +``` +roster records: 116 craft=300 deployed=41/116 +strengths [(2, 24), (4, 1), (8, 4), (18, 12)] +``` + +Same 41 deployed of 116, same discrete strength histogram, same total. The +deployment is deterministic at mission start. + +## ✅ Losses require the player — an accidental control run + +The pilot failed to bind this run (`BIND FAILED, no pilot`), so the craft sat +unattended. That is the control condition the kill-versus-no-kill experiment +needed, and it arrived by accident: + +| condition | duration | losses | +|---|---|---| +| hunting pilot | 168 s | 300 → 288 | +| hunting pilot | 168 s | 296 → 280 | +| **no pilot** | **240 s** | **300 → 300, zero** | + +With nobody flying, **not one craft was destroyed in four minutes** — the count +held at exactly 300 for all 22 samples. So the 16–20 losses in the piloted runs +are attributable to the player being in the fight, and **NPC crossfire does not +by itself destroy anything**. That was an open question two iterations ago and +it is now answered. + +## 🔴 No arrival, in either condition, in six runs + +Zero `0 → n` transitions. Not with a hunting pilot, not without one, across +roughly fifteen minutes of cumulative Stage 02 flight and windows up to 240 s. +The 75 records that hold no craft at mission start still hold none at the end. + +Set against `Route_S02.tbl`, which schedules phase-1 arrivals at +t = 90, 120, 170, 210, 240 in groups of 3, 3, 3, 2, 1, this is now a strong +negative rather than a null result. Three readings survive: + +1. **The timetable's `t` is not seconds.** At 30 Hz the whole phase-1 schedule + completes inside 8 s — before any probe's first sample — and everything that + was going to arrive already had. +2. **Arrivals are event-gated** and no run supplied the trigger. The control run + supplied nothing at all; the piloted runs killed 16–20 craft, which may be + below a threshold or of the wrong squadrons. +3. **The mission is not advancing its phase clock**, so no schedule ever fires. + +## ❔ The prime suspect is now (3), and it is untested + +Nothing in six runs has confirmed that mission or phase time is advancing at +all. The craft count is frozen without a pilot; `REMAINING OB` has never read as +a counter; no clock has been located. Every "no arrival" observation is +consistent with a mission whose scheduler is simply not running under these +conditions — and that possibility has never been checked, which makes it the +cheapest thing to eliminate next. + +**Next: find the mission timer.** The HUD shows elapsed mission time, so a +digit-recognition read of the clock region, or a memory scan for a counter that +advances at a fixed rate, would settle whether phase time moves. If it does not, +every arrival conclusion so far is measuring a stopped clock. diff --git a/tools/re-capture/wave6_probe.py b/tools/re-capture/wave6_probe.py new file mode 100755 index 0000000..92b6b16 --- /dev/null +++ b/tools/re-capture/wave6_probe.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Per-record arrival/loss watch against the verified 116/300 baseline. + +Supersedes wave5_probe.py, which reported 42 records in a run nobody could +reproduce. Changes: + * refuses to interpret a run whose roster count is not the reproduced + baseline (mission-per-record-strength.md: discard, do not interpret); + * keys records by file offset rather than by primary VA; + * samples faster and for longer, since losses appear at ~1 per 10 s and no + arrival has been seen in five runs. + +An arrival is a record going 0 -> n; a loss is n -> n-1. +""" +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 +BASELINE = 116 + +def scan_vt(fd, size, vt): + out = [] + for a, b in gmem.extents(fd, size): + pos = a + while pos < b: + m = min(1 << 24, b - pos) + blob = os.pread(fd, m, pos) + i = blob.find(vt) + while i != -1: + if (pos + i) % 4 == 0: out.append(pos + i) + i = blob.find(vt, i + 1) + pos += m + return sorted(out) + +def sample(fd, defs, want): + lo, hi = gmem.va_to_off(entities2.ENT_VA_LO), gmem.va_to_off(entities2.ENT_VA_HI) + per = collections.Counter(); tot = 0; pos = lo + 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] not in defs: continue + tot += 1 + head = os.pread(fd, WIN, pos + k - DELTA) + 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, tot + +def main(): + secs = int(sys.argv[1]) if len(sys.argv) > 1 else 240 + every = int(sys.argv[2]) if len(sys.argv) > 2 else 10 + 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) + print('roster records: %d (baseline %d)' % (len(roster), BASELINE)) + if len(roster) != BASELINE: + print('DISCARD: roster count is not the reproduced baseline. ' + 'Per mission-per-record-strength.md this run is not interpreted.') + return 3 + f = os.fdopen(os.dup(fd), 'rb') + label, want = {}, {} + for o in roster: + va = gmem.primary_va(o) + if va is None: continue + label[o] = wave3.resolve_id(f, w.size, o)[0] or '?' + want[va + LINK] = o + log = open('/tmp/wave6.tsv', 'w'); log.write('t\tkind\toff\tunit\tfrom\tto\n') + prev = None; t0 = time.time(); arr_n = loss_n = 0 + while time.time() - t0 < secs: + per, tot = sample(fd, defs, want) + el = round(time.time() - t0) + if prev is None: + print('t=%4ds craft=%d deployed=%d/%d strengths %s' + % (el, tot, len(per), len(roster), + sorted(collections.Counter(per.values()).items())), flush=True) + else: + arr = [(o, per[o]) for o in per if prev.get(o, 0) == 0 < per[o]] + los = [(o, prev[o], per.get(o, 0)) for o in prev if per.get(o, 0) < prev[o]] + arr_n += len(arr); loss_n += len(los) + print('t=%4ds craft=%d deployed=%d ARRIVALS=%d losses=%d (cum %d/%d)' + % (el, tot, len(per), len(arr), len(los), arr_n, loss_n), flush=True) + for o, c in arr: + print(' ARRIVAL %-30s 0 -> %d' % (label.get(o, '?'), c), flush=True) + log.write('%d\tarrival\t%#x\t%s\t0\t%d\n' % (el, o, label.get(o, '?'), c)) + for o, a, b in los: + print(' loss %-30s %d -> %d' % (label.get(o, '?'), a, b), flush=True) + log.write('%d\tloss\t%#x\t%s\t%d\t%d\n' % (el, o, label.get(o, '?'), a, b)) + log.flush() + prev = per + time.sleep(max(0, every - (time.time() - t0 - el))) + log.close() + print('\nTOTAL arrivals=%d losses=%d over %ds' % (arr_n, loss_n, secs)) + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/re-capture/wave6_session.sh b/tools/re-capture/wave6_session.sh new file mode 100755 index 0000000..830204a --- /dev/null +++ b/tools/re-capture/wave6_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/wave6_probe.py" ; rc=$? +[ -n "$PILOT" ] && kill "$PILOT" 2>/dev/null +echo "LIVENESS DONE rc=$rc"