diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index e5d582dc..6da08d37 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -310,6 +310,16 @@ search cannot find a *schedule*. and 14 manoeuvre weights per profile). 🔴 Refuted: the 8-value frame is *not* universal — `Formation_Fleet_01/02` use 136 and 4 values per frame. +* ✅🔴 **The arrival timetable is found; the entity-count proxy is refuted** + (2026-08-24) — [`mission-wave-arrivals.md`](mission-wave-arrivals.md). Route + records' first-keyframe time is the **arrival schedule**, measured from the + start of that route's *phase* (phase 2 has all 37 routes at t=0). Stage 02 + phase 1: 25 at t=0, then 3/3/3/2/1 at t=90/120/170/210/240. 🔴 A live count of + `0x820af030` entity records is FLAT at 116 for 210 s — no arrivals visible. + 🟡 116 is exactly Stage 02's roster member count, suggesting one pre-allocated + record per member (n=1, NOT promoted; the refutation needs another stage's + save, which we do not have — only slot 01 / Stage 02 exists). + ❔ Still unmeasured: whether the timetable's `t` is frames or seconds. * 🔴 **Probed 2026-08-24 and refuted the cheap hypothesis** — the phase state is NOT adjacent to the loaded table strings; see [`mission-phase-runtime.md`](mission-phase-runtime.md). The run did confirm diff --git a/docs/re/mission-wave-arrivals.md b/docs/re/mission-wave-arrivals.md new file mode 100644 index 00000000..c3e1954d --- /dev/null +++ b/docs/re/mission-wave-arrivals.md @@ -0,0 +1,84 @@ +# The arrival timetable, and what the entity table is really counting + +Status: ✅ the static arrival timetable is in `Route_S.tbl`; 🟡 the runtime +entity table appears to be one record per roster member, pre-allocated (n = 1); +🔴 counting entity records does **not** show wave arrivals; ❔ the timetable's +time unit is still unmeasured. + +## ✅ The arrival timetable is the routes' first keyframe time + +Each record in `Route_S.tbl` starts with a time +([structures/stage-mission-tables.md](structures/stage-mission-tables.md)), and +that time is **not** always zero. For Stage 02, grouping the 120 route records +by phase and by first-frame time gives a timetable: + +| phase | t=0 | 50 | 60 | 90 | 120 | 150 | 170 | 210 | 240 | +|---|---|---|---|---|---|---|---|---|---| +| 1 | 25 | | | 3 | 3 | | 3 | 2 | 1 | +| 2 | 37 | | | | | | | | | +| 3 | 36 | 1 | 1 | 3 | | 1 | | 4 | | + +Phase 2 has **every** route at t = 0, which is the useful detail: it means `t` +is measured **from the start of its phase**, not from the start of the mission. +Entering a phase releases that phase's t = 0 group, and the rest arrive on the +offsets above. + +That is the "algorithm with parameters" this line of work was after: the +schedule is data — squadron, phase, arrival time, and the path flown in — spread +across `UnitGroup` (who) and `Route` (when and from where). Stage 02 phase 1 +spawn points span x ∈ [-47371, 20800], y ∈ [-18856, 27395], z ∈ [-25349, 25036]. + +❔ **The time unit is not measured.** At 30 Hz, t = 240 is 8 s; as seconds it is +4 min. Distinguishing them was the point of the run below, and the run did not +manage it. + +## 🔴 Counting entity records does not show arrivals + +One Stage 02 flight, 210 s sampled every 15 s, counting aligned occurrences of +the spawned-entity vtable `0x820af030` +([structures/unit-struct-runtime.md](structures/unit-struct-runtime.md)) in an +8.3 MB span of guest RAM: + +``` +t=0s 116 t=45s 116 t=90s 116 t=135s 116 t=180s 116 t=210s 116 +``` + +Perfectly flat. No steps at 90, 120, 170, 210 or anywhere else. The hypothesis +that a live entity count would step up at the timetable's offsets is **refuted +for this proxy** — but the reason matters more than the refutation. + +## 🟡 116 records, 116 roster members + +`UnitGroup_S02.tbl` has 111 squadrons whose `Count` fields sum to **116** +members. The entity table holds **116** records, from the first sample onwards. + +The natural reading is that the game allocates **one entity record per roster +member when the mission loads**, and that arriving on a route makes an existing +record active rather than creating a new one. That would explain the flat count +exactly, and it would mean `0x820af030` records are a *roster*, not a live +population — consistent with the existing note in `unit-struct-runtime.md` that +these records are "not live state". + +**This is one data point and must not be promoted on it.** 116 = 116 is a +striking match but n = 1, and the obvious refutation has not been run: load a +*different* stage and check the record count against that stage's member sum +(S01 = 42, S10 = 14, S16 = 2, S29 = 95). S16 with 2 members would be decisive +and cheap if a save for it existed — the only save slot available is Stage 02, +which is the blocker. + +## What this run did not settle + +* The timetable's time unit (frames vs seconds). +* Whether arrivals are visible at all from memory, and where. The live/dead flag + is presumably a field *inside* the 116 records; diffing those records over a + mission is the obvious next probe, and it is well scoped now that the record + set is bounded and located. +* Whether the 116↔116 match generalises. + +## Operational note + +Cold, the boot spent **204 s** in the title movie before the first Ⓐ — the +`wait_screen` oracle correctly refused to tap through it. A 300 s probe plus that +boot exceeded a 580 s turn budget and the first attempt was killed with its +output still in the pipe. Log runs to a file (`> /tmp/x.log 2>&1`) rather than +piping to `tail`, or a timeout loses the evidence as well as the run. diff --git a/tools/re-capture/wave_probe.py b/tools/re-capture/wave_probe.py new file mode 100755 index 00000000..f7ba489c --- /dev/null +++ b/tools/re-capture/wave_probe.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Count live spawned-entity records over a mission and look for wave arrivals. + +Route_S02.tbl gives a per-phase arrival timetable: phase 1 has 25 routes at +t=0 then groups of 3, 3, 3, 2, 1 at t = 90, 120, 170, 210, 240 (units unknown -- +that is what this measures). If those are seconds of phase time, a live entity +count should step up at roughly those offsets. If they are frames at 30 Hz, +every arrival lands inside the first ~8 s and the count is flat afterwards. + +Counts occurrences of the spawned-entity vtable 0x820af030 +(docs/re/structures/unit-struct-runtime.md). +""" +import sys, time, struct, importlib.util + +SD = __file__.rsplit('/', 1)[0] +spec = importlib.util.spec_from_file_location('gmem', SD + '/gmem.py') +gmem = importlib.util.module_from_spec(spec); spec.loader.exec_module(gmem) + +VT = struct.pack('>I', 0x820AF030) + +def scan(fd, size, span=None): + """Count aligned occurrences of VT; return (count, lo_off, hi_off).""" + import os + f = os.fdopen(os.dup(fd), 'rb', closefd=True) + n = 0; lo = hi = None + ext = [span] if span else gmem.extents(fd, size) + for start, end in ext: + pos = start + while pos < end: + f.seek(pos) + buf = f.read(min(1 << 24, end - pos)) + if not buf: break + i = buf.find(VT) + while i != -1: + off = pos + i + if off % 4 == 0: + n += 1 + if lo is None or off < lo: lo = off + if hi is None or off > hi: hi = off + i = buf.find(VT, i + 1) + pos += len(buf) + f.close() + return n, lo, hi + +def main(): + secs = int(sys.argv[1]) if len(sys.argv) > 1 else 300 + every = int(sys.argv[2]) if len(sys.argv) > 2 else 10 + path = gmem.mem_path() + import os + fd = os.open(path, os.O_RDONLY) + size = os.fstat(fd).st_size + n, lo, hi = scan(fd, size) + if not n: + print('NO ENTITY RECORDS -- not in a mission'); return 2 + span = (max(0, lo - (1 << 20)), min(size, hi + (1 << 20))) + print('initial entities: %d span 0x%x..0x%x (%.1f MB)' + % (n, span[0], span[1], (span[1] - span[0]) / 2**20)) + t0 = time.time(); series = [] + while True: + el = time.time() - t0 + if el > secs: break + c, _, _ = scan(fd, size, span) + series.append((round(el), c)) + print(' t=%4ds entities=%d' % (round(el), c), flush=True) + time.sleep(max(0, every - (time.time() - t0 - el))) + print('\n--- steps up (arrivals) ---') + for i in range(1, len(series)): + d = series[i][1] - series[i-1][1] + if d > 0: + print(' t=%4ds +%d (%d -> %d)' % (series[i][0], d, series[i-1][1], series[i][1])) + print('\nseries:', series) + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/re-capture/wave_session.sh b/tools/re-capture/wave_session.sh new file mode 100755 index 00000000..5ed2cb23 --- /dev/null +++ b/tools/re-capture/wave_session.sh @@ -0,0 +1,15 @@ +#!/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:-300}"; EVERY="${2:-10}" +CFG=/tmp/nav-wave.json +"$SD/launch_mission.sh" fly || { echo "BOOT FAILED"; exit 1; } +if python3 "$SD/entities2.py" self 0x130 "$CFG" >/dev/null 2>&1; then + nohup python3 "$SD/pilot.py" "$CFG" "$SECS" /tmp/wave-pilot.log 2>&1 & + PILOT=$!; echo "--- pilot flying" +else PILOT=""; echo "--- BIND FAILED, unattended craft"; fi +python3 "$SD/wave_probe.py" "$SECS" "$EVERY"; rc=$? +[ -n "$PILOT" ] && kill "$PILOT" 2>/dev/null +echo "WAVE SESSION DONE rc=$rc"