From 27e0b6ebc12e9f067fed4b337999766623d19663 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Mon, 24 Aug 2026 11:57:16 +0000 Subject: [PATCH] re: find the arrival timetable; refute the entity-count proxy for waves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The routes' first-keyframe time is the arrival schedule. It is not always zero, and grouping Stage 02's 120 route records by phase and first-frame time gives a timetable: phase 1 releases 25 routes at t=0 then 3, 3, 3, 2, 1 at t = 90, 120, 170, 210, 240. Phase 2 has every one of its 37 routes at t=0, which is what pins the meaning: t is measured from the start of its phase, not of the mission. Entering a phase releases that phase's t=0 group and the rest follow on the offsets. That completes the data side of the question this line of work started from -- the schedule is data, split across UnitGroup (who) and Route (when, and the path flown in), with no fixed enemy count anywhere. Refuted: counting spawned-entity records does not reveal arrivals. One Stage 02 flight, 210 s sampled every 15 s, counting aligned 0x820af030 in an 8.3 MB span: flat at 116 throughout, no step at 90, 120, 170, 210 or anywhere. The reason looks more useful than the refutation. UnitGroup_S02's Count fields sum to exactly 116 members, and there are exactly 116 records from the first sample on, so the game most likely allocates one record per roster member at mission load and a route arrival activates an existing record rather than creating one. Kept at 🟡, not promoted: n=1, and the obvious refutation -- check another stage's record count against its member sum (S01=42, S16=2, S29=95) -- needs a save for another stage, and only slot 01 / Stage 02 exists. Noted as the blocker rather than worked around. Not settled: whether the timetable's t is frames or seconds (at 30 Hz t=240 is 8 s; as seconds it is 4 min), and where an arrival is observable in memory. The live flag is presumably a field inside those 116 records, which is a well-scoped next probe now that the record set is bounded and located. Operational note recorded: cold boot spent 204 s in the title movie, so a 300 s probe overran the turn and the first attempt died with its output still in the pipe. Log to a file rather than piping to tail. --- docs/re/BACKLOG.md | 10 ++++ docs/re/mission-wave-arrivals.md | 84 ++++++++++++++++++++++++++++++++ tools/re-capture/wave_probe.py | 75 ++++++++++++++++++++++++++++ tools/re-capture/wave_session.sh | 15 ++++++ 4 files changed, 184 insertions(+) create mode 100644 docs/re/mission-wave-arrivals.md create mode 100755 tools/re-capture/wave_probe.py create mode 100755 tools/re-capture/wave_session.sh 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"