Built-in 12 activate_unit returns 0 when the live object is NULL: it registers an object that already exists and cannot create one. So a wave arrival is a craft reaching a point on its route, not a new record, and a count of entity records was never going to move. Stage 02's UnitGroup totals Count = 116 across 111 squadrons, matching the live probe's flat 116. The natural objection is that 116 might match by accident -- it does not: across all 28 stages the roster totals run 2..116, and 116 is the MAXIMUM and unique to Stage 02, with nothing else within 5. Three independent things agree: the handler cannot spawn, the roster totals 116, and the live count sits at 116 while 22 of 113 routed squadrons have a route starting after t=0. Still n=1 for the per-member identity (only a Stage 02 save exists), and this cannot settle timetable-vs-event -- that needs a position probe, not a count.
553 lines
27 KiB
Markdown
553 lines
27 KiB
Markdown
# The arrival timetable, and what the entity table is really counting
|
||
|
||
Status: ✅ the static arrival timetable is in `Route_S<NN>.tbl`;
|
||
✅ **arrivals ARE observable — and the time unit is SECONDS** (2026-08-25, see
|
||
below); 🔴 counting *entity records* does not show them, which is what the
|
||
original negative measured.
|
||
|
||
> ✅ **CORRECTION 2026-08-25.** "Counting entity records does not show wave
|
||
> arrivals" is still true, but the conclusion drawn from it — that arrivals
|
||
> could not be seen — was wrong. The **script VM's own unit table** shows them
|
||
> directly. See [script-runtime-probe](script-runtime-probe.md).
|
||
|
||
## ✅ 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.
|
||
|
||
## ✅ The timetable is confirmed live, and `t` is in SECONDS
|
||
|
||
The three squadrons the phase-1 clear condition polls — ADN110, ADN111, ADN112 —
|
||
all have **first-keyframe time `170`** on their `_p1F` routes:
|
||
|
||
```
|
||
Route_ADN110_p1F FrameCount=3 first field: 170
|
||
Route_ADN111_p1F FrameCount=3 first field: 170
|
||
Route_ADN112_p1F FrameCount=2 first field: 170
|
||
```
|
||
|
||
In a live run (`data/phase-watch-s02.txt`) all three flipped from
|
||
state 1 (not deployed) to state 2 (active) at **~143 s on the probe's clock**.
|
||
The probe starts after flight entry and after the pilot bind, so its zero sits
|
||
roughly 10–20 s into the mission — putting the arrival at about **155–165 s of
|
||
mission time against a predicted 170**.
|
||
|
||
**That pins the unit as seconds.** The alternatives are not close: 170 *frames*
|
||
at 30 fps is 5.7 s, so those squadrons would have been active at the first
|
||
sample, and they demonstrably were not. Three squadrons sharing one timetable
|
||
entry all changing state in the same 5 s sampling window is not coincidence.
|
||
|
||
⚠️ It is a **match, not an exact measurement** — the probe clock is not aligned
|
||
to phase start, so this bounds the unit rather than verifying the value to the
|
||
second. Logging mission start and phase start in the same stream would close
|
||
that.
|
||
|
||
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 — confirmed by composition (2026-08-24)
|
||
|
||
**Promoted from 🟡 to ✅.** The section below argued from a single number,
|
||
116 = 116, and explicitly refused to promote it on that. It is now confirmed on
|
||
a far stronger test: with the records labelled (see the labelling section at the
|
||
end of this file), the **multiset of unit types** matches the static roster
|
||
exactly, not just the total.
|
||
|
||
| unit | static roster | live records |
|
||
|---|---|---|
|
||
| `UN_e007_ADAN_Turret` | 21 | 21 |
|
||
| `UN_e106_ADAN_Destroyer` | 19 | 19 |
|
||
| `UN_f106_TCAF_Destroyer` | 14 | 14 |
|
||
| `UN_f105_TCAF_Cruiser` | 11 | 11 |
|
||
| `UN_e108_ADAN_ASFrigate` | 9 | 9 |
|
||
| `UN_e201_ADAN_ISCMissile` | 9 | 9 |
|
||
| `UN_e010_ADAN_Attacker_S` | 9 | 9 |
|
||
| `UN_e105_ADAN_Cruiser` | 7 | 7 |
|
||
| `UN_f001_TCAF_DeltaSaber_T` | 7 | 7 |
|
||
| `UN_f003_TCAF_ArrowHead` | 6 | 6 |
|
||
|
||
10 of 10 exact, across counts from 21 down to 6. A coincidental total can happen;
|
||
a coincidental *distribution* over ten unit types cannot. The game allocates one
|
||
entity record per `UnitGroup` roster member at mission load, and every one of
|
||
them exists from the first sample — long before the member could have "arrived".
|
||
|
||
## 🟡 116 records, 116 roster members (original argument, superseded above)
|
||
|
||
`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.
|
||
|
||
---
|
||
|
||
# Follow-up: diffing inside the 116 records (2026-08-24)
|
||
|
||
Status: 🟡 supporting evidence for the pre-allocated-roster reading; 🔴 the
|
||
arrival flag was **not** found; 🔴 two defects in my own probe recorded.
|
||
|
||
The count being flat meant an arrival must flip a field *inside* a record, so
|
||
this diffed all 116 records (0x200 bytes each) every 5 s for 170 s of Stage 02
|
||
flight, under the pilot. `tools/re-capture/wave2_probe.py`.
|
||
|
||
## 🟡 Only 10 of 116 records are dynamic
|
||
|
||
Across the whole run, **10 distinct records** ever changed a byte. The other
|
||
**106 never changed at all** — not one word, over 170 s of live mission.
|
||
|
||
That is a second, independent line of support for the reading in the section
|
||
above: if all 116 were live entities they would be moving, and position words
|
||
would churn in most of them every tick. Instead the overwhelming majority are
|
||
inert, which is what a pre-allocated roster of not-yet-activated members looks
|
||
like.
|
||
|
||
**Correction (2026-08-24, from the user).** The first version of this paragraph
|
||
hedged the conclusion by saying Stage 02's roster is heavy with
|
||
`UN_e007_ADAN_Turret` and "a turret does not move even when it is very much
|
||
alive", so inert and not-yet-arrived could not be told apart. **That hedge was
|
||
wrong, and it was wrong because I read the unit's English name instead of its
|
||
data.** "Turret" in the early missions is a *craft* — a distinct enemy type — not
|
||
an anti-aircraft mount on a capital ship. The tables say so plainly:
|
||
|
||
| | |
|
||
|---|---|
|
||
| `UN_e007_ADAN_Turret` | `AI_ADAN_CraftSquadron_Rookie` ×70, `AI_ADAN_CraftSquadron_Veteran` ×19 |
|
||
| `UN_e007_ADAN_Turret_EX4` | `AI_ADAN_CraftSquadron_Ace` ×4 |
|
||
| `AI_Structure` is used by | asteroids, `UN_mn500_ADAN_FloatingMine`, `UN_bf001_TCAF_SchlosBase`, `UN_bf002_TCAF_TransitPlatform`, `UN_be005_ADAN_SpaceFortress` |
|
||
|
||
`AI_ADAN_CraftSquadron_*` is `Type = Squad` in
|
||
[`AIParams`](structures/stage-mission-tables.md), with the full manoeuvre-weight
|
||
block — these things fly. `AI_Structure` exists precisely for the units that do
|
||
not, and no `*_Turret` unit ever uses it. The type also matches "an enemy of the
|
||
first few missions": 93 turret members in total, in S01 (20), S02 (21), S03 (11),
|
||
S04 (6), S05 (12), S06 (19), and then nothing until S27 (4).
|
||
|
||
So the caveat is withdrawn, and removing it makes the evidence **stronger**, not
|
||
weaker: the 106 inert records cannot be explained away as immobile scenery,
|
||
because Stage 02's turrets are craft that should be manoeuvring. The reading
|
||
that they are pre-allocated, not-yet-activated roster entries survives a test it
|
||
would otherwise have dodged.
|
||
|
||
The general lesson, which cost a wrong hedge: **do not infer a unit's behaviour
|
||
from its English name — check its `AIID`.** The roster is self-describing and
|
||
the answer was already in the committed dump.
|
||
|
||
## 🔴 No transition matching the arrival timetable
|
||
|
||
The prediction under test was groups of 3, 3, 3, 2, 1 records changing state at
|
||
t = 90, 120, 170, 210, 240 s. Nothing of the sort appeared. Field-change events
|
||
are spread evenly across ticks (1–7 words per 5 s tick, 1052 in total), with no
|
||
cluster at any predicted time and no field that transitions once for exactly
|
||
three records.
|
||
|
||
So one of these is true, and this run cannot say which:
|
||
|
||
* the timetable's `t` is **not** seconds (at 30 Hz the whole phase-1 schedule
|
||
finishes inside the first 8 s, before the probe's first sample);
|
||
* arrival is not marked in the `0x820af030` record at all;
|
||
* phase 1 was not where the mission actually was during the run.
|
||
|
||
## 🔴 Two defects in this probe
|
||
|
||
* **`label()` failed for all 116 records** — every one resolved to `?`. The
|
||
`object+0x04 → name_record+0x10 → char*` chain from
|
||
[structures/unit-struct-runtime.md](structures/unit-struct-runtime.md) did not
|
||
resolve here. Without labels the records cannot be tied back to squadrons,
|
||
which is exactly what would have made the result decisive: "these three
|
||
records that changed at t=90 are `ADN110`, `ADN111`, `ADN112`" is evidence,
|
||
"records 18, 32, 99" is not. `unit_discover.py` already solves this
|
||
association and should be reused rather than re-derived.
|
||
* **`RECLEN = 0x200` is probably too wide.** The busiest fields by far are
|
||
`+0x1e0 … +0x1fc` — the last eight words of the window — which is what
|
||
spilling into the *next* object looks like. The record's true size was assumed,
|
||
not measured.
|
||
|
||
* **A method error worth keeping:** the first attempt deferred all analysis to
|
||
the end of the run, and the turn's timeout killed it with 240 s of collected
|
||
data still in memory and nothing written. The probe now streams every
|
||
transition to `/tmp/wave2-trans.tsv` and prints a partial ranking every 60 s.
|
||
Combined with the 219 s cold-boot title movie, a run that only reports at the
|
||
end has perhaps 300 s of usable budget and one bad estimate loses everything.
|
||
|
||
## Next
|
||
|
||
Fix the record→squadron association first (reuse `unit_discover.py`), and
|
||
measure the record stride instead of assuming `0x200`. Until a record can be
|
||
named, no diff of these records can confirm or refute an arrival.
|
||
|
||
---
|
||
|
||
# Competing model: waves are event-gated, not clock-driven (2026-08-24)
|
||
|
||
Status: 🟡 plausible and partly supported; ❔ untested. **Raised by the user**,
|
||
and it is a better fit to the evidence than the pure-clock reading I had been
|
||
assuming.
|
||
|
||
## The proposal
|
||
|
||
The next wave may not arrive at a fixed time at all. It may be released when
|
||
some condition is met — a number of enemies destroyed, an objective completed,
|
||
or another event — with the route's first-keyframe time being a *delay after the
|
||
trigger*, or simply the start of the fly-in animation once released.
|
||
|
||
## Why it fits better than the clock model
|
||
|
||
* **My two runs showed no arrivals.** Both were flown by the survival pilot,
|
||
whose job is to stay alive, not to shoot. Under a clock model, arrivals at
|
||
t = 90/120/170 should have happened regardless of what the player did, and
|
||
nothing appeared. Under an event model, a player who kills nothing triggers
|
||
nothing — which is exactly what was observed. This is weak evidence (the
|
||
proxy may simply not show arrivals) but it points the right way.
|
||
* **`DisableInterval` finally has a plausible referent.** The name implies a
|
||
gating *interval* that squadrons can opt out of. A pure timetable in the route
|
||
table would not need a per-squadron flag to disable it.
|
||
|
||
## 🟡 The flag and the delayed routes do not co-occur
|
||
|
||
Cross-tabulating every squadron's `DisableInterval` against the start time of
|
||
its routes, across all stages:
|
||
|
||
| | route t=0 | route t>0 |
|
||
|---|---|---|
|
||
| `DisableInterval = No` | 876 | 68 |
|
||
| `DisableInterval = Yes` | **55** | **0** |
|
||
|
||
No squadron that disables the interval has a delayed route. If the two were
|
||
independent we would expect about 3.7 such routes; the probability of seeing
|
||
zero by chance is roughly **0.02**. That is suggestive, **not** conclusive, and
|
||
it is recorded at 🟡 deliberately — a p ≈ 0.02 association on one derived
|
||
statistic is exactly the kind of near-fit that has already misled this corpus
|
||
once (the "109 of 111" squadron-name base in
|
||
[structures/unit-group-table.md](structures/unit-group-table.md)).
|
||
|
||
### ❌ WITHDRAWN (2026-08-26) — the p ≈ 0.02 does not survive the clustering
|
||
|
||
The caution above was right and did not go far enough. Breaking the same
|
||
cross-tab down **by stage** destroys the result:
|
||
|
||
| stage | No, t=0 | No, t>0 | **Yes, t=0** | **Yes, t>0** |
|
||
|---|---|---|---|---|
|
||
| S04 | 42 | 5 | **10** | 0 |
|
||
| S14 | 22 | 0 | **45** | 0 |
|
||
| the other 9 stages with delayed routes | 836 | 63 | 0 | 0 |
|
||
|
||
All 55 `DisableInterval = Yes` routes live in **two** stages, and **45 of them
|
||
are in S14, which has no delayed route at all** — no squadron there is delayed
|
||
whether it sets the flag or not, so those 45 cannot be evidence of anything.
|
||
Only **one** stage, S04, contains both a `Yes` squadron and a delayed route.
|
||
|
||
The pooled p ≈ 0.02 treated 1 023 routes as independent draws when they are
|
||
really two clusters. The test that actually bears on the hypothesis is within
|
||
S04, and there it is:
|
||
|
||
P(none of S04's 5 delayed routes belongs to one of its 10 Yes squadrons)
|
||
= C(47,5) / C(57,5) = 0.37
|
||
|
||
Expected count under independence: **0.88**, observed **0**. That is entirely
|
||
unremarkable. **The association is not supported by the data**, and the
|
||
"`DisableInterval` finally has a plausible referent" reading above rests on the
|
||
name alone, not on this table.
|
||
|
||
Name matching for this table is sound: for Stage 02, 108 of 111 squadrons have
|
||
routes and 113 route names resolve, so the cross-tab is not an artefact of
|
||
failed joins.
|
||
|
||
## The test that separates the two models
|
||
|
||
Two runs of the same stage, differing only in what the player does:
|
||
|
||
* **Run A — kill nothing.** Evade for ~250 s. Clock model predicts arrivals
|
||
anyway; event model predicts none.
|
||
* **Run B — kill aggressively.** Event model predicts arrivals that track kills,
|
||
not the clock.
|
||
|
||
If Run B produces activity that Run A does not, the schedule is event-gated and
|
||
the route times are post-trigger delays. This needs the record→squadron
|
||
labelling fixed first, otherwise neither run can say *which* squadron arrived —
|
||
the blocker already recorded above.
|
||
|
||
A cheaper precondition worth checking first: whether `REMAINING OB` at
|
||
`0xbdb59668` moves in Run B but not Run A. That is a known-good counter and
|
||
needs no new decoding.
|
||
|
||
|
||
---
|
||
|
||
# Labelling the records, and the real stride (2026-08-24)
|
||
|
||
Status: ✅ both defects from the previous section are fixed and measured.
|
||
|
||
## ✅ The id chain: `record+0x04` → pointer → `+0x00`
|
||
|
||
The previous probe assumed `object+0x04 → name_record+0x10 → char*` and resolved
|
||
**0 of 116**. `tools/re-capture/wave3_probe.py` searches for the chain instead of
|
||
assuming one — for each record it walks the first 24 words, treats any
|
||
guest-range word as a pointer, chases it, and accepts the result only if it
|
||
lands on a `UN_`/`NP_`-prefixed string, optionally through one more indirection.
|
||
|
||
Result: **116 of 116 resolved**, every one by the same chain — pointer at
|
||
`+0x04`, string at delta **`0x00`**, not `0x10`. The `0x10` in
|
||
[structures/unit-struct-runtime.md](structures/unit-struct-runtime.md) is the
|
||
delta for the *definition* object (vtable `0x820af844`); the spawned-entity
|
||
record (`0x820af030`) uses `0x00`. Assuming one from the other is what cost the
|
||
previous run.
|
||
|
||
## ✅ The stride is variable — `0x200` was wrong
|
||
|
||
Measured gaps between consecutive record addresses: **min 32, median 800**, with
|
||
common values 800, 640, 608, 576, 416 and 32. There is no fixed record size, so
|
||
the previous probe's `RECLEN = 0x200` window both truncated large records and
|
||
ran past small ones into their neighbours — which is exactly why its busiest
|
||
"fields" were the last words of the window.
|
||
|
||
Any future diff must bound each record by the *next* record's address rather
|
||
than by a constant.
|
||
|
||
## ❔ `REMAINING OB` at `0xbdb59668` did not read as a counter this run
|
||
|
||
It held `95748078` for the whole run, unchanging. That address is known to be
|
||
run-dependent ([structures/mission-objective-counter.md](structures/mission-objective-counter.md)
|
||
records it recurring in about 5 runs of 7), and this was one of the misses, so
|
||
the run cannot say whether the pilot killed anything. Re-hunting the counter
|
||
with `ob_hunt.py` is a precondition for the kill-versus-no-kill test, not an
|
||
optional extra.
|
||
|
||
---
|
||
|
||
# Hunting run (2026-08-24) — and a withdrawal
|
||
|
||
Status: ✅ a hunting pilot exists and engages; 🔴 the "10 of 116 records are
|
||
dynamic" result is **withdrawn**; ❔ the wave question is still open, and the
|
||
entity scan is too noisy to settle it.
|
||
|
||
The user asked for an actively hunting pilot, with attention to the objectives,
|
||
because the two previous runs used the *survival* pilot and a player who kills
|
||
nothing cannot trigger an event-gated wave.
|
||
|
||
## ✅ `SYLPH_HUNT=1`
|
||
|
||
`pilot.py` gained a hunt mode. The change that matters is which contacts ENGAGE
|
||
is allowed to shoot: it previously skipped every "hard" target with the comment
|
||
*"turrets and hulls are not the objective"*, and kept 2500 units away from
|
||
turrets. That was written on the assumption that an `e007` "Turret" is an
|
||
anti-aircraft mount on a capital ship. It is not — it is a craft, one of the main
|
||
enemy types of the first six missions
|
||
(`AI_ADAN_CraftSquadron_*`, `Type = Squad`), and at 100 HP the cheapest kill on
|
||
the field. Under `SYLPH_HUNT=1` turrets are targets and the keep-out drops to
|
||
600.
|
||
|
||
The run confirms it flies and shoots: steady `ENGAGE`, `fire=1`, committed to
|
||
`e010_ADAN_Attacker_S` at ~2.2 km, hull 1500/1500 and escorted asset at 100 %
|
||
throughout 160 s.
|
||
|
||
## 🔴 Withdrawn: "only 10 of 116 records are dynamic"
|
||
|
||
The previous section reported that 10 of 116 records ever changed a byte in
|
||
170 s, and used it as supporting evidence for the pre-allocated roster. **This
|
||
run measured 41–56 records changing in every single 10 s tick.** The earlier
|
||
figure does not reproduce and is withdrawn.
|
||
|
||
I cannot say why, because I changed two things at once — the record bound (fixed
|
||
`0x200` → bounded by the next record's address, capped at `0x400`) *and* the
|
||
pilot (survival → hunting). Either could explain it: the old window mis-framed
|
||
every record whose true size is not `0x200`, and a hunting pilot flies into
|
||
traffic that an evading one avoids. **That is a design error on my part**: a run
|
||
that changes two variables cannot attribute its own result, and the honest
|
||
outcome is that the old number is retracted without a replacement explanation.
|
||
|
||
The conclusion the retracted number was supporting is unaffected — the
|
||
roster identity now rests on the exact 10-of-10 unit-composition match, which is
|
||
far stronger evidence and was measured independently.
|
||
|
||
## ❔ The entity scan is too noisy to answer the wave question
|
||
|
||
`pilot.py`'s own scanner reports a live entity count each tick. Over the run:
|
||
|
||
```
|
||
t=0s 168 (147 ADAN) t=56s 147 (131) t=101s 145 (129)
|
||
t=20s 171 (147) t=76s 155 (131) t=137s 151 (129)
|
||
t=30s 148 (132) t=86s 147 (131) t=157s 166 (142)
|
||
```
|
||
|
||
ADAN drifts 147 → 129 and back to 142. The late rise is the shape an arrival
|
||
would have, but the sample-to-sample swing is ±10 or more, which is the same
|
||
size as the effect. This is exactly the trap `docker/agent/AGENT.md` warns about
|
||
— polling faster than the guest updates manufactures a curve out of noise — so
|
||
**no wave conclusion is drawn from it.**
|
||
|
||
Two things are needed before this run type can settle the question:
|
||
|
||
1. A *stable* liveness signal — a per-record field that means alive/dead, read
|
||
from the 116 labelled records, rather than a re-scan whose population changes
|
||
between samples.
|
||
2. A kill count. `fc=0` in the pilot's telemetry and the asset at 100 % suggest
|
||
the hunt did not actually destroy anything in 160 s, in which case this run
|
||
does not test the event-gated model either. `REMAINING OB` at `0xbdb59668`
|
||
still did not read as a counter, so that check remains unavailable.
|
||
|
||
## ✅ The timetable's `t` is SECONDS (2026-08-26)
|
||
|
||
This was recorded as "❔ still unmeasured: whether the timetable's `t` is frames
|
||
or seconds". It is seconds, and the evidence is a physical one rather than an
|
||
argument from plausibility.
|
||
|
||
**The method.** A route record is a list of `(time, quat×4, pos×3)` keyframes, so
|
||
each consecutive pair gives a distance and a time delta — an implied speed. The
|
||
flight-speed law was measured **live, against a real-time clock**
|
||
([flight-speed-law.md](flight-speed-law.md)): the player's craft cruises at ~420
|
||
world units/s and tops out at **~1530** under `RT`. That is an independent
|
||
yardstick in known units, so the two readings of `t` can simply be checked
|
||
against it.
|
||
|
||
**The result.** `Route_*` records across all 28 stage tables — 331 records with
|
||
two or more keyframes, **1 104** consecutive pairs:
|
||
|
||
| reading of `t` | median | p99 | max | pairs above 1 530 |
|
||
|---|---|---|---|---|
|
||
| **seconds** | 147 | 959 | **1 310** | **0 of 1 104 (0.0 %)** |
|
||
| frames @30 fps | 4 422 | 28 758 | 39 309 | 984 (89.1 %) |
|
||
| frames @60 fps | 8 844 | 57 516 | 78 617 | 1 037 (93.9 %) |
|
||
|
||
Under **seconds** not one keyframe pair in the game requires a craft to exceed
|
||
the measured player maximum, and the median 147 sits comfortably below cruise —
|
||
which is what waypoint-to-waypoint averages should look like once turns are
|
||
included. Under either **frames** reading, ~90 % of the game's routes would
|
||
demand impossible speeds. There is no overlap to argue about.
|
||
|
||
### ⚠️ Two corrections made while measuring this
|
||
|
||
Both are the same kind of mistake and worth recording:
|
||
|
||
1. **A first pass silently skipped non-numeric fields** while flattening a
|
||
record, which shifts every subsequent value and corrupts the frame split. It
|
||
produced a median implied speed of 65 534 — suspiciously exactly 2^16, which
|
||
is what gave it away. Fixed by rejecting a record outright rather than
|
||
dropping a field from it.
|
||
2. **The remaining outliers were not routes at all.** Seven stage tables gave
|
||
maxima in the tens of thousands; every offending record was named
|
||
`Frame_S<NN>_Asteroid`. The corpus already records that the 8-value frame is
|
||
not universal, and these are the cases. The final measurement therefore
|
||
filters on the record name (`Route_*`, 1 389 of the 1 477 records; the rest
|
||
are 60 `Frame_*` and 28 `Enumeration_*`) rather than excluding whole stages —
|
||
a filter by kind, not a filter that removes inconvenient numbers.
|
||
|
||
### What this does not settle
|
||
|
||
* **331 of the 1 389 `Route_*` records** carry two or more keyframes and pass the
|
||
`FrameCount*8` length identity; the rest are single-keyframe (arrival only) or
|
||
do not fit the 8-value frame. The conclusion rests on those 331.
|
||
* The **~1.25× world-unit factor** noted in `flight-speed-law.md` is not applied
|
||
here; it would move the ceiling, not the 30× and 60× gaps, so it cannot change
|
||
the answer.
|
||
* Whether `t` is measured from **phase start** (as the arrival schedule reads it)
|
||
or from mission start is unchanged by this — that was already settled
|
||
separately.
|
||
|
||
## ✅ The entity-count proxy was not just refuted — there was nothing to see
|
||
|
||
**2026-08-26.** A live count of `0x820af030` entity records stayed **flat at 116
|
||
for 210 s**, spanning Stage 02's scheduled arrivals at t = 90/120/170, and was
|
||
recorded above as refuting the proxy. It is worth saying *why* it was flat,
|
||
because the reason is now established and it is not "the proxy is broken".
|
||
|
||
**Arrivals are not spawns.** Built-in 12 `activate_unit`, read from the
|
||
disassembly (`sub_822659F0`, see
|
||
[structures/isl-builtins.md](structures/isl-builtins.md)), **returns 0
|
||
immediately when the live object `[record+4]` is NULL** — it registers an object
|
||
that already exists and sets its state to *active*. It cannot create one. So the
|
||
mission's craft are instantiated up front and a "wave arrival" is a craft
|
||
reaching a place along its route, not a new record appearing.
|
||
|
||
**The count matches the roster exactly, and the number is distinctive.**
|
||
Stage 02's `UnitGroup` table holds **111 squadron records totalling `Count` =
|
||
116 members** — the same 116 the live probe saw and never left. The obvious
|
||
objection is that 116 might be an unremarkable number that matches by accident.
|
||
It is not:
|
||
|
||
roster member totals, all 28 stages:
|
||
2 4 4 9 11 12 14 17 32 36 37 42 43 45 48 50 53 54 54 62 64 66 69 70 87 90 95 116
|
||
|
||
**116 is the maximum across the whole game and Stage 02 is the only stage with
|
||
it** — no other stage is within ±5. The save we have is a Stage 02 save.
|
||
|
||
So three independent things line up: the handler cannot spawn, the roster totals
|
||
116, and the live count sits at 116 and never moves while 22 of Stage 02's 113
|
||
routed squadrons have a route beginning after t = 0.
|
||
|
||
### What this does and does not settle
|
||
|
||
* ✅ **Arrivals are not spawns**, and the flat count is the expected behaviour
|
||
rather than a failed measurement. Read from the code, not inferred.
|
||
* 🟡 **One pre-allocated record per roster member.** Strong — the mechanism
|
||
explains it and the matching number is unique to this stage — but still
|
||
**n = 1**: only a Stage 02 save exists, so the identity has been checked on one
|
||
mission. The previous entry's "n=1, NOT promoted" caution still applies; what
|
||
has changed is that there is now a mechanism behind the number.
|
||
* ❌ **It does not settle timetable versus event.** Knowing that craft are
|
||
pre-allocated says nothing about *what* starts a route — a clock or a
|
||
trigger. That test still needs a live run, and the entity count can never
|
||
perform it, because there are no spawns for it to count. A different probe is
|
||
required: watch a delayed squadron's **position**, not the record count.
|