Files
Sylpheed/docs/re/mission-phase-membership.md
Sylpheed RE agent 34d86f4ace re: the Turret-biased run was void -- the guest was frozen from t=0
SYLPH_PREFER=Turret, escort at 100%, 1070s, phase field 0 throughout, 4029
Turret-targeting pilot samples. It looked like a clean negative -- 'hunting the
objective squadrons does not advance the phase' -- and it is worthless:
frozen.py reports max_pixel_delta=0, and the pilot's first sample at t=0.0
already has spd=0 with the same yaw/pitch/target/d=7186 it still had at 1070s.
The mission froze on entry to flight and nothing was ever shot, while screen_id
said 'flight' the whole time.

I was one step from writing this up as a fact about the game; running the freeze
test rather than trusting a plausible log is what caught it.

phase_probe.py now calls frozen.py every 60s and prints a GUEST FROZEN banner
inline, so a dead-world reading is labelled in the data instead of discovered
later. Note frozen.frozen() returns a TUPLE (is_frozen, max_delta) -- testing it
directly is always truthy and would have made the witness fire on every check.
Verified against the frozen guest.

Also recorded: the pilot log is itself a freeze witness -- identical
yaw/pitch/target across thousands of samples is a dead world, not patience.
2026-08-25 13:42:54 +00:00

12 KiB
Raw Blame History

Which squadrons fly in which phase — and the ~300 craft are ~150

Status: the phase map, read out of Route_S<NN>.tbl record names; the 387-vs-300 gap explained — the live-craft probe double-counts; what advances a phase (nothing static encodes it).

The phase map is in the route names

stage\Route_S02.tbl has 121 records: 120 routes + 1 roster, and every route name matches Route_<squadron>_p<N><kind>120/120:

Route_ADN101_p1F   Route_TCN003_p2S   Route_ADS351_p3M

The phase lives in the name and nowhere else; a route record's only named field is FrameCount. So the squadron→phase mapping was on the disc all along, one string-parse away, in a table that was being read for fly-in geometry.

108 of 111 S02 squadrons match a route name exactly. The three that do not — TCN207, TCN208, TCS252 — are typos in the route table (Route_TCT207_p2S, Route_TCT208_p2S, Route_TCN252_p2F), plus two orphan routes naming no squadron (Route_ADS352_p3F, Route_kichi_p3F). All three near-misses are phase 2, so phase 1 is unaffected.

Phase partition of Stage 02 (Σn = 387 overall):

phase set squadrons Σn
{1} 33 142
{2} 31 110
{3} 39 118
{1,3} 2 5
{2,3} 1 1
{1,2,3} 2 4
none (the 3 typos) 3 7
per phase 37 / 34 / 44 151 / 115 / 128

The 387-vs-~300 gap: the probe counts each craft TWICE

roster-to-craft-link records four failed attempts to explain how the roster becomes "~300 live craft". The premise was wrong — there were never 300 craft.

Phase 1's member-level n multiset against the measured craft-per-record fan-in (41 records, 300 craft):

phase-1 members (42):   n=1 x25   n=2 x1   n=4 x4   n=9 x12    sum 151
observed craft/record:    2 x24     4 x1     8 x4    18 x12    sum 300
observed / 2:             1 x24     2 x1     4 x4     9 x12    sum 150

Every bucket matches shape for shape. The only discrepancy is 24 vs 25 records at the bottom — the already-documented 41-vs-42 off-by-one, and it is an n=1 record. So 300 = 2 × (151 1). Unit type corroborates independently: the 12 records showing 18 craft are all UN_e007_ADAN_Turret, and phase 1 has exactly 12 turret squadrons at n=9.

The factor of 2 is an artifact of the probe, not real units

Two static arguments, and the second is decisive because it is a hard capacity bound:

  1. Named pilots. Phase 1's eight f001 members all have n=1 and carry distinct identities — Character_Player_Test, ELLEN, RAYMOND, GENE, SANDRA, BILLY, ANTONIUS, CARL. A 2× reading means 16 Delta Sabers for 8 named people.

  2. Formation slots. Squadrons cannot exceed their formation's FrameCount (see unit-group-table):

    reading violations among the 37 phase-1 squadrons
    Σn ≤ FrameCount (1×) 0
    2·Σn ≤ FrameCount (2×) 20

    Including 16 squadrons on Formation_1_only — one slot, n=1 — which a 2× reading would have placing two craft in a one-slot formation.

So the true phase-1 population is ~150, and the 296 / 298 / 300 spread is 2 × a 148 / 149 / 150 population after kills. That all three measurements are even is a further tell a 1:1 count would not produce half the time.

Best explanation of the missing 42nd record: the player's own tuple (TCN001 member 0, Character_Player_Test), which plausibly is not in the AI-craft pool the +0x130 def-pointer scan walks. That makes the chain exact: 300 = 2 × (151 1 player).

⚠️ This is a static argument about a dynamic measurement. It says the probe's number cannot mean what it was taken to mean; it does not identify what the probe is double-counting. That needs a run — the counting rule in the craft scan is the place to look, not the game.

🔴 Refuted along the way

  • UnitGroup carries no phase, spawn time or arrival delay. The raw field dump accounts for every slot: 1019 = sum(Count)·4 + 5·111, with positional entries tagged by sequential index (0x0,0x1,0x2,…), not hashes. There is no spare field.
  • DisableInterval cannot explain Stage 02 — it is No for all 111 squadrons. Corpus-wide its 31 Yes records sit in just two stages, S04 (5) and S14 (26), all on GNN*** squadrons.
  • stage\EnumSquadron_Test.tbl does not exist (0x213f4a7e is not in the TOC). It was listed as a candidate for the arrival-interval values; it is dead.
  • AIParams_S02.tbl is keyed by AI profile, not squadron, and carries only combat tuning. SUBObjectiveSettings_S02.tbl is keyed SUBOBJ_005..014 with only points/unlock/HUD strings — no squadron, phase or trigger.

Bonus: the UnitGroup record key, confirmed corpus-wide

A squadron record's key is tag_hash(squadron_id)1160/1160 across all 28 stages (ADN1010x659aff47). Same hash as idxd-tag-hash; this is an independent confirmation of it on a second kind of table.

Still open: what advances a phase

Nothing static encodes a trigger. Swept the stage record and every table it names — Route, AIParams, SUBObjectiveSettings, FormationSet, nameplate, EnumUnit, EnumCharacter, EnumLocalString, EnumWeapon — plus the message\ family, for field names matching interval|time|phase|delay|wave|spawn|arrival|trigger|start|appear|event|condition. The only hits anywhere are FrameCount and PresetMessage_Phase1/2/3 in message\UnitMessageSet_S02.tbl. The trigger is almost certainly in the executable, not the data.

Falsifiable predictions for the next run

  • Per-record craft counts, in probe units (2×): 4 records at 8 = ADS151, ADT102, ADT107, ADT113 (all UN_e010_ADAN_Attacker_S); 1 record at 4 = TCN105 (UN_f106_TCAF_Destroyer); 24 records at 2 = the remaining phase-1 n=1 members.
  • A phase advance should move the deployed set toward phase 2: 34 squadrons, 36 members, Σn 115 (≈230 in probe units).
  • TCN207, TCN208 and TCS252 may fail to arrive in phase 2, because their routes are misnamed.

🟡 2026-08-25 — first long live observation: no phase advance, and a GAME OVER

Flew Stage 02 with an actively hunting pilot for 936 s while polling the runtime phase mirror [*(0x828F35F8) + 236] every 3 s (phase_probe.py).

The mirror never left 0. That is what the static work predicts rather than a contradiction: CScriptInterpreter::ChangePhase (opcode 995) is only posted when the ordinal is > 1 (the guard in mission-bytecode opcode 0x83), so a run that stays in phase 1 never writes the mirror. It does mean the mirror is useless as a "which phase am I in" readout for phase 1 — it reads 0, not 1. [ScriptMission+40] is the real counter and needs a debugger.

The run ended in GAME OVER — and the pilot's hull was still 1500/1500, so the player did not die. A script lose-branch fired, and the pilot log says which one is likely: the tracked asset fell from 100.8 % to 23.4 %, with ASSET-HIT events logged, while the pilot repeatedly AVOIDed f101_TCAF_Acropolis — a friendly capital ship — 2588 times.

So the escort was being destroyed throughout, and the mission failed with the player untouched. "Escort lost" is now a measured candidate rather than a guess, though the final asset value was 23.4 %, not 0, so the exact threshold (or whether a different unit's loss ended it) is not established.

So a phase advance still has not been observed. Two runs now — 530 s and 936 s — neither reached phase 2. Combined with the static reading that phase 1 polls unit_state on ADN110/ADN111/ADN112, the likeliest explanation is that those three squadrons were never destroyed; the pilot hunts whatever is nearest, not the objective. Next: bias the pilot at those squadrons' craft and re-run.

🔴 Refuted: sub_8226D740 does not read the flag register file

Chasing what reads the 32-entry flag file at [phase+120], the promising hit was three addi rN, r31, 120 inside sub_8226D740, the per-frame engine→script trigger edge. It is not the flag file. In that function r31 is a local trigger-record buffer whose fields are handed to sub_8226E220 as out-parameters (+80, +84, +88, +92, +112, +120), and lfd f1,120(r31) reads a double from the record. The phase is in r26/r29.

Offsets 88 and 120 are far too common (2091 and 5228 instructions image-wide, mostly stack frames) for an offset search to isolate the register files. Who reads the flags is still unknown, so the chain set_flag(8) → … → END_PHASE has a gap in the middle.

2026-08-25 — Stage 02 phase 1: destroy 27 Turrets (ADN110/111/112)

Resolving the three squadrons the phase-1 condition polls (isl-builtins, unit_state at 0xF524) against the roster:

squadron phases AI unit n
ADN110 1 AI_ADAN_CraftSquadron_Rookie UN_e007_ADAN_Turret 9
ADN111 1 AI_ADAN_CraftSquadron_Veteran UN_e007_ADAN_Turret 9
ADN112 1 AI_ADAN_CraftSquadron_Rookie UN_e007_ADAN_Turret 9

All three are phase 1 (from the Route_S02 name map), all fly the same craft, all n = 9 — so the condition watches 27 Turrets, and all three squadrons share Formation_ADAN_Turret09_30.

Three independent layers agree here, which is the point worth recording: the route names say these are phase-1 squadrons, the roster says what they fly and how many, and the script polls exactly these three before latching set_flag(8).

Confirms "Turret" is a craft type, not ship AA

The user's correction stands on the data: UN_e007_ADAN_Turret is flown by AI_ADAN_CraftSquadron_Rookie/_Veteran — a craft squadron AI, in a 30-slot craft formation — never AI_Structure. It is an enemy the player fights, not a gun bolted to a capital ship.

🔴 2026-08-25 — the Turret-biased run was VOID: the guest was frozen

Ran it: SYLPH_PREFER=Turret, escort intact (ast=100.0%), 1070 s. The phase field read 0 throughout and the pilot logged 4029 Turret-targeting samples. That looked like a clean negative — "hunting the objective squadrons does not advance the phase" — and it is worthless.

frozen.py says max_pixel_delta=0: the guest was frozen, and the pilot's very first sample at t=0.0 already shows spd=0 with the same yaw, pitch, target and d=7186 that it still had at 1070 s. The mission froze on entry to flight; nothing was ever shot. screen_id said flight the whole time, which is precisely the trap mission-freeze-resume-spin documents.

I was one step from writing this up as a result about the game. What saved it was running the freeze test rather than trusting a plausible-looking log — the same lesson that note already carries, arrived at again from the other side.

Fixed so it cannot recur silently

phase_probe.py now calls frozen.py every 60 s and prints *** GUEST FROZEN *** inline, so a dead-world reading is labelled in the data rather than discovered afterwards. ⚠️ frozen.frozen() returns a tuple (is_frozen, max_delta) — testing it directly is always truthy and would have made the witness fire on every check; it must be unpacked. Verified against the frozen guest: the banner fires, and the unpacked value is (True, 0).

The pilot log is itself a witness and was not being read as one: identical yaw/pitch/target across thousands of samples means a dead world, not a patient pilot.

🟡 The prediction to test

If phase 1 clears on those three squadrons dying, then destroying 27 Turrets while the escort survives should push [ScriptMission+40] to 2 and make ChangePhase write the mirror at [*(0x828F35F8)+236]. Two runs so far killed whatever was nearest and never advanced.