re: find the arrival timetable; refute the entity-count proxy for waves

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.
This commit is contained in:
Sylpheed RE agent
2026-08-24 11:57:16 +00:00
parent a0a0214b24
commit c193b44133
4 changed files with 184 additions and 0 deletions

View File

@@ -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

View File

@@ -0,0 +1,84 @@
# The arrival timetable, and what the entity table is really counting
Status: ✅ the static arrival timetable is in `Route_S<NN>.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<NN>.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.

75
tools/re-capture/wave_probe.py Executable file
View File

@@ -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())

View File

@@ -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" </dev/null >/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"