re: decode the stage table set — phases, routes, sub-objectives, AI parameters

Following the real stage record (not the _Test template dumped earlier) reaches
the whole mission-parameter layer, all of it in the same self-describing IDXD
container as the squadron roster.

The big one: a stage is divided into Phase_N blocks -- three for Stage 02, each
with its own map path, map mesh, asteroid definition and background -- and
Route_S<NN>.tbl holds the arrival paths, with records named

    Route_<squadron>_p<phase><kind>

tying a UnitGroup squadron id to a phase and to a time-stamped keyframed path of
(time, quat x4, pos x3). Route_ADN101_p1F is 3 frames at t = 0, 20, 30. The
identity len(fields) == FrameCount * 8 + 1 holds for 1449 of 1449 route records
across the 28 stages that have one, and 16/16 for FormationSet_S02.

Also decoded: SUBObjectiveSettings (per-objective bonus points by difficulty,
unlock item id, HUD strings) and AIParams (34 profiles, firing/guard/muster/
counter ranges plus 14 manoeuvre weights for Squad-type AI). The AIParams
numbers are exact original values from static RE and are portable as they are.

Adds tools/re-capture/stagetbl.py, which resolves a stage record by content and
can --follow every table it names, and commits two dumps as evidence.

Refuted and kept: the eight-value keyframe is the common case, not universal.
Formation_Fleet_01 has FrameCount=1 with 136 positional fields and
Formation_Fleet_02 has FrameCount=8 with 32, so a parser must not assume the
stride.

Corrects stage-definition-table.md, which was written from the _Test template
and is missing EnumerateSubobjective, EnumerateAIParams, BackGroundID and the
WingmanIconID fields the real record carries.

Not settled: what advances a phase -- the stage declares Phase_1..3 and routes
are phase-tagged, but nothing static says what ends one. That is a question for
the oracle, not for more static reading. Also open: the route-name kind letters
F/S/A/M/B, what activates a sub-objective, and StageMessageSet_S<NN>.tbl, which
does not resolve in GP_MAIN_GAME_E.pak.
This commit is contained in:
Sylpheed RE agent
2026-08-24 11:15:34 +00:00
parent ecf6887b76
commit 4441ca79ec
6 changed files with 2761 additions and 1 deletions

View File

@@ -296,7 +296,28 @@ search cannot find a *schedule*.
hash, and an earlier "109/111" reading of the squadron-id string base was an
artefact of the uniform 7-byte id stride (it shifted every name by three).
**First step (next):** find where the arrival *interval values* live.
***The rest of the mission-parameter layer is decoded** (2026-08-24) —
[`structures/stage-mission-tables.md`](structures/stage-mission-tables.md),
tool `tools/re-capture/stagetbl.py --follow S02`. The *real* stage record (the
one dumped earlier was the `_Test` template) splits a stage into **`Phase_1..3`**
blocks and names `Route_S<NN>.tbl`, `SUBObjectiveSettings_S<NN>.tbl`,
`AIParams_S<NN>.tbl`, `FormationSet_S<NN>.tbl`, `nameplate_S<NN>.tbl` and more.
**`Route_S<NN>.tbl` is the arrival schedule**: records named
`Route_<squadron>_p<phase><kind>` holding time-stamped keyframes
`(time, quat x4, pos x3)`, tying a `UnitGroup` squadron to a phase and a path —
self-checked `FrameCount*8+1` on **1449/1449** route records across 28 stages.
`AIParams` carries directly portable combat tuning (firing/guard/muster ranges
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.
**First step (next):** find what *advances a phase* — the stage declares
`Phase_1..3` and routes are phase-tagged, but nothing static found so far says
what ends a phase. This is the point where the oracle should be measured rather
than reasoned about: fly Stage 02 and watch for the phase transition. Also open:
the route-name kind letters `F/S/A/M/B`, what activates a sub-objective, and
`StageMessageSet_S<NN>.tbl` which does not resolve in `GP_MAIN_GAME_E.pak`.
**Superseded first step:** find where the arrival *interval values* live.
`DisableInterval` is only a per-squadron flag (`Yes` for 31 of 1160); the
durations, spawn triggers and arrival positions are not in `UnitGroup`. The
stage record names two untouched candidates: `Formation_*.tbl` (formation

View File

@@ -0,0 +1,568 @@
=== stage\AIParams_S02.tbl (35 records) ===
-- 'AI_ADAN_CraftSquadron_Ace' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.20
224f8336 CounterManeuverRatio = 0.50
240dca2c BootDefManeuverRatio_Aimed = 0.16
2e136f2c BootDefManeuverRatio_Fired = 0.40
429c4495 CounterLength = 6000.0
4913db76 DefManeuverRatio_BoostAway = 0.88
55f76dfe GuardLength = 10000.0
7e2e5018 BootDefManeuverRatio_Missiled = 0.40
98aa858f OffManeuverRatio_HoldPosition = 3.50
9d93b0b6 OffManeuverRatio_Through = 3.50
a17ea8fc DefManeuverRatio_BarrelRoll = 1.75
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 2.63
c11cad36 FiringLength = 3000.0
cbc80716 DefManeuverRatio_NoAction = 3.00
ce666dd1 DefManeuverRatio_SideRoll = 0.88
d746db41 OffManeuverRatio_NoAction = 3.00
e2fe30cd MusterLength = 10000.0
eb41fdfb DefManeuverRatio_TurnAway = 0.88
eece0777 AutoGuardLength = 2500.0
-- 'Enumerate_AIs' (34 fields)
01cc57f8 AI_ADAN_CraftSquadron_Ace =
077eabc4 AI_ADAN_Elan_GR_Violeta =
125f8586 AI_TCAF_Acropolis =
1b1de94f AI_ADAN_Fleet_Escort =
250e4f4d AI_TCAF_Fleet_Escort =
253b577f AI_CraftSquadron_Test =
336ccfb9 AI_ADAN_Phantom =
344ee4e3 AI_ADAN_BomberSquadron_Veteran =
459f847a AI_TCAF_BirdFlight =
48037b3b AI_ADAN_ElanPlus_NightRavens =
4b7b7485 AI_Fleet_Test_ADAN =
4c8979df AI_ADAN_Fleet =
557a76f0 AI_Fleet_Test_TCAF =
5652c286 AI_TCAF_Fleet =
61f55143 AI_ADAN_CraftSquadron_Rookie =
7c6328b5 AI_ADAN_Attacker_B_HF_Wayne =
7cffa74f AI_ADAN_ElanPlus_NightRavens_Veteran =
89dbd2f2 AI_Test =
8d6c7b26 AI_ADAN_Attacker_S_HF_Wayne =
91b17697 AI_ADAN_Frigates =
9545b087 AI_TCAF_YatsukaSquadron =
9bfcd7d3 AI_ADAN_Vindicator_Margras =
a33de123 AI_TCAF_CraftSquadron =
a41681e7 AI_ADAN_Elan_NightRavens =
acc3a108 AI_ADAN_ElanPlus_Margras =
b307b2f2 AI_TCAF_RhinoLeader =
b9b6ed54 AI_ADAN_ElanPlus_Taskent =
ba7b2b69 AI_Structure =
c4459ddb AI_TCAF_RhinoFlight =
c8588f28 AI_ADAN_BomberSquadron_Rookie =
cd111420 AI_ADAN_CraftSquadron_Veteran =
d80617f1 AI_Fleet_Test =
e7a21320 AI_TraceRoute =
fbb168ce AI_ADAN_ElanTypeQ_Margras =
-- 'AI_ADAN_Elan_GR_Violeta' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.25
224f8336 CounterManeuverRatio = 0.50
240dca2c BootDefManeuverRatio_Aimed = 0.20
2e136f2c BootDefManeuverRatio_Fired = 0.50
429c4495 CounterLength = 6000.0
4913db76 DefManeuverRatio_BoostAway = 2.81
55f76dfe GuardLength = 10000.0
7e2e5018 BootDefManeuverRatio_Missiled = 0.50
98aa858f OffManeuverRatio_HoldPosition = 2.67
9d93b0b6 OffManeuverRatio_Through = 3.33
a17ea8fc DefManeuverRatio_BarrelRoll = 0.94
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 0.94
c11cad36 FiringLength = 3000.0
cbc80716 DefManeuverRatio_NoAction = 2.50
ce666dd1 DefManeuverRatio_SideRoll = 0.94
d746db41 OffManeuverRatio_NoAction = 4.00
e2fe30cd MusterLength = 10000.0
eb41fdfb DefManeuverRatio_TurnAway = 1.88
eece0777 AutoGuardLength = 2500.0
-- 'AI_TCAF_Acropolis' (6 fields)
429c4495 CounterLength = 25000.0
55f76dfe GuardLength = 10000.0
a2797b39 Type = Fleet
c11cad36 FiringLength = 20000.0
e2fe30cd MusterLength = 30000.0
eece0777 AutoGuardLength = 10000.0
-- 'AI_ADAN_Fleet_Escort' (6 fields)
429c4495 CounterLength = 30000.0
55f76dfe GuardLength = 13000.0
a2797b39 Type = Fleet
c11cad36 FiringLength = 20000.0
e2fe30cd MusterLength = 20000.0
eece0777 AutoGuardLength = 4000.0
-- 'AI_TCAF_Fleet_Escort' (6 fields)
429c4495 CounterLength = 25000.0
55f76dfe GuardLength = 10000.0
a2797b39 Type = Fleet
c11cad36 FiringLength = 20000.0
e2fe30cd MusterLength = 30000.0
eece0777 AutoGuardLength = 10000.0
-- 'AI_CraftSquadron_Test' (6 fields)
429c4495 CounterLength = 6000.0
55f76dfe GuardLength = 10000.0
a2797b39 Type = Squad
c11cad36 FiringLength = 3000.0
e2fe30cd MusterLength = 10000.0
eece0777 AutoGuardLength = 2500.0
-- 'AI_ADAN_Phantom' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.15
224f8336 CounterManeuverRatio = 0.50
240dca2c BootDefManeuverRatio_Aimed = 0.12
2e136f2c BootDefManeuverRatio_Fired = 0.30
429c4495 CounterLength = 6000.0
4913db76 DefManeuverRatio_BoostAway = 2.44
55f76dfe GuardLength = 10000.0
7e2e5018 BootDefManeuverRatio_Missiled = 0.30
98aa858f OffManeuverRatio_HoldPosition = 3.00
9d93b0b6 OffManeuverRatio_Through = 3.00
a17ea8fc DefManeuverRatio_BarrelRoll = 0.81
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 0.81
c11cad36 FiringLength = 3000.0
cbc80716 DefManeuverRatio_NoAction = 3.50
ce666dd1 DefManeuverRatio_SideRoll = 0.81
d746db41 OffManeuverRatio_NoAction = 4.00
e2fe30cd MusterLength = 10000.0
eb41fdfb DefManeuverRatio_TurnAway = 1.63
eece0777 AutoGuardLength = 2500.0
-- 'AI_ADAN_BomberSquadron_Veteran' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.15
224f8336 CounterManeuverRatio = 0.50
240dca2c BootDefManeuverRatio_Aimed = 0.12
2e136f2c BootDefManeuverRatio_Fired = 0.30
429c4495 CounterLength = 6000.0
4913db76 DefManeuverRatio_BoostAway = 2.17
55f76dfe GuardLength = 10000.0
7e2e5018 BootDefManeuverRatio_Missiled = 0.30
98aa858f OffManeuverRatio_HoldPosition = 2.00
9d93b0b6 OffManeuverRatio_Through = 4.00
a17ea8fc DefManeuverRatio_BarrelRoll = 1.44
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 0.72
c11cad36 FiringLength = 3000.0
cbc80716 DefManeuverRatio_NoAction = 3.50
ce666dd1 DefManeuverRatio_SideRoll = 0.72
d746db41 OffManeuverRatio_NoAction = 4.00
e2fe30cd MusterLength = 10000.0
eb41fdfb DefManeuverRatio_TurnAway = 1.44
eece0777 AutoGuardLength = 2500.0
-- 'AI_TCAF_BirdFlight' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.30
224f8336 CounterManeuverRatio = 0.50
240dca2c BootDefManeuverRatio_Aimed = 0.24
2e136f2c BootDefManeuverRatio_Fired = 0.60
429c4495 CounterLength = 6000.0
4913db76 DefManeuverRatio_BoostAway = 0.73
55f76dfe GuardLength = 10000.0
7e2e5018 BootDefManeuverRatio_Missiled = 0.60
98aa858f OffManeuverRatio_HoldPosition = 6.40
9d93b0b6 OffManeuverRatio_Through = 1.60
a17ea8fc DefManeuverRatio_BarrelRoll = 1.45
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 3.64
c11cad36 FiringLength = 3000.0
cbc80716 DefManeuverRatio_NoAction = 2.00
ce666dd1 DefManeuverRatio_SideRoll = 0.73
d746db41 OffManeuverRatio_NoAction = 2.00
e2fe30cd MusterLength = 10000.0
eb41fdfb DefManeuverRatio_TurnAway = 1.45
eece0777 AutoGuardLength = 2500.0
-- 'AI_ADAN_ElanPlus_NightRavens' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.20
224f8336 CounterManeuverRatio = 0.50
240dca2c BootDefManeuverRatio_Aimed = 0.16
2e136f2c BootDefManeuverRatio_Fired = 0.40
429c4495 CounterLength = 6000.0
4913db76 DefManeuverRatio_BoostAway = 2.33
55f76dfe GuardLength = 10000.0
7e2e5018 BootDefManeuverRatio_Missiled = 0.40
98aa858f OffManeuverRatio_HoldPosition = 3.43
9d93b0b6 OffManeuverRatio_Through = 2.57
a17ea8fc DefManeuverRatio_BarrelRoll = 0.78
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 1.56
c11cad36 FiringLength = 3000.0
cbc80716 DefManeuverRatio_NoAction = 3.00
ce666dd1 DefManeuverRatio_SideRoll = 0.78
d746db41 OffManeuverRatio_NoAction = 4.00
e2fe30cd MusterLength = 10000.0
eb41fdfb DefManeuverRatio_TurnAway = 1.56
eece0777 AutoGuardLength = 2500.0
-- 'AI_Fleet_Test_ADAN' (6 fields)
429c4495 CounterLength = 30000.0
55f76dfe GuardLength = 13000.0
a2797b39 Type = Fleet
c11cad36 FiringLength = 20000.0
e2fe30cd MusterLength = 20000.0
eece0777 AutoGuardLength = 4000.0
-- 'AI_ADAN_Fleet' (6 fields)
429c4495 CounterLength = 30000.0
55f76dfe GuardLength = 13000.0
a2797b39 Type = Fleet
c11cad36 FiringLength = 20000.0
e2fe30cd MusterLength = 20000.0
eece0777 AutoGuardLength = 4000.0
-- 'AI_Fleet_Test_TCAF' (6 fields)
429c4495 CounterLength = 25000.0
55f76dfe GuardLength = 10000.0
a2797b39 Type = Fleet
c11cad36 FiringLength = 12000.0
e2fe30cd MusterLength = 30000.0
eece0777 AutoGuardLength = 4000.0
-- 'AI_TCAF_Fleet' (6 fields)
429c4495 CounterLength = 25000.0
55f76dfe GuardLength = 10000.0
a2797b39 Type = Fleet
c11cad36 FiringLength = 20000.0
e2fe30cd MusterLength = 30000.0
eece0777 AutoGuardLength = 10000.0
-- 'AI_ADAN_CraftSquadron_Rookie' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.10
224f8336 CounterManeuverRatio = 0.50
240dca2c BootDefManeuverRatio_Aimed = 0.08
2e136f2c BootDefManeuverRatio_Fired = 0.20
429c4495 CounterLength = 6000.0
4913db76 DefManeuverRatio_BoostAway = 2.25
55f76dfe GuardLength = 10000.0
7e2e5018 BootDefManeuverRatio_Missiled = 0.20
98aa858f OffManeuverRatio_HoldPosition = 1.00
9d93b0b6 OffManeuverRatio_Through = 4.00
a17ea8fc DefManeuverRatio_BarrelRoll = 0.75
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 0.75
c11cad36 FiringLength = 3000.0
cbc80716 DefManeuverRatio_NoAction = 4.00
ce666dd1 DefManeuverRatio_SideRoll = 0.75
d746db41 OffManeuverRatio_NoAction = 5.00
e2fe30cd MusterLength = 10000.0
eb41fdfb DefManeuverRatio_TurnAway = 1.50
eece0777 AutoGuardLength = 2500.0
-- 'AI_ADAN_Attacker_B_HF_Wayne' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.25
224f8336 CounterManeuverRatio = 0.50
240dca2c BootDefManeuverRatio_Aimed = 0.20
2e136f2c BootDefManeuverRatio_Fired = 0.50
429c4495 CounterLength = 6000.0
4913db76 DefManeuverRatio_BoostAway = 0.75
55f76dfe GuardLength = 10000.0
7e2e5018 BootDefManeuverRatio_Missiled = 0.50
98aa858f OffManeuverRatio_HoldPosition = 4.57
9d93b0b6 OffManeuverRatio_Through = 3.43
a17ea8fc DefManeuverRatio_BarrelRoll = 1.50
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 1.50
c11cad36 FiringLength = 3000.0
cbc80716 DefManeuverRatio_NoAction = 2.50
ce666dd1 DefManeuverRatio_SideRoll = 2.25
d746db41 OffManeuverRatio_NoAction = 2.00
e2fe30cd MusterLength = 10000.0
eb41fdfb DefManeuverRatio_TurnAway = 1.50
eece0777 AutoGuardLength = 2500.0
-- 'AI_ADAN_ElanPlus_NightRavens_Veteran' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.25
224f8336 CounterManeuverRatio = 0.50
240dca2c BootDefManeuverRatio_Aimed = 0.20
2e136f2c BootDefManeuverRatio_Fired = 0.50
429c4495 CounterLength = 6000.0
4913db76 DefManeuverRatio_BoostAway = 2.05
55f76dfe GuardLength = 10000.0
7e2e5018 BootDefManeuverRatio_Missiled = 0.50
98aa858f OffManeuverRatio_HoldPosition = 4.67
9d93b0b6 OffManeuverRatio_Through = 2.33
a17ea8fc DefManeuverRatio_BarrelRoll = 1.36
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 2.05
c11cad36 FiringLength = 3000.0
cbc80716 DefManeuverRatio_NoAction = 2.50
ce666dd1 DefManeuverRatio_SideRoll = 0.68
d746db41 OffManeuverRatio_NoAction = 3.00
e2fe30cd MusterLength = 10000.0
eb41fdfb DefManeuverRatio_TurnAway = 1.36
eece0777 AutoGuardLength = 2500.0
-- 'AI_Test' (6 fields)
429c4495 CounterLength = 15000.0
55f76dfe GuardLength = 5000.0
a2797b39 Type = Squad
c11cad36 FiringLength = 10000.0
e2fe30cd MusterLength = 7500.0
eece0777 AutoGuardLength = 3000.0
-- 'AI_ADAN_Attacker_S_HF_Wayne' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.20
224f8336 CounterManeuverRatio = 0.50
240dca2c BootDefManeuverRatio_Aimed = 0.16
2e136f2c BootDefManeuverRatio_Fired = 0.40
429c4495 CounterLength = 6000.0
4913db76 DefManeuverRatio_BoostAway = 1.40
55f76dfe GuardLength = 10000.0
7e2e5018 BootDefManeuverRatio_Missiled = 0.40
98aa858f OffManeuverRatio_HoldPosition = 3.11
9d93b0b6 OffManeuverRatio_Through = 3.89
a17ea8fc DefManeuverRatio_BarrelRoll = 0.70
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 1.40
c11cad36 FiringLength = 3000.0
cbc80716 DefManeuverRatio_NoAction = 3.00
ce666dd1 DefManeuverRatio_SideRoll = 2.10
d746db41 OffManeuverRatio_NoAction = 3.00
e2fe30cd MusterLength = 10000.0
eb41fdfb DefManeuverRatio_TurnAway = 1.40
eece0777 AutoGuardLength = 2500.0
-- 'AI_ADAN_Frigates' (6 fields)
429c4495 CounterLength = 10000.0
55f76dfe GuardLength = 13000.0
a2797b39 Type = Fleet
c11cad36 FiringLength = 6000.0
e2fe30cd MusterLength = 20000.0
eece0777 AutoGuardLength = 4000.0
-- 'AI_TCAF_YatsukaSquadron' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.25
224f8336 CounterManeuverRatio = 0.50
240dca2c BootDefManeuverRatio_Aimed = 0.20
2e136f2c BootDefManeuverRatio_Fired = 0.50
429c4495 CounterLength = 6000.0
4913db76 DefManeuverRatio_BoostAway = 1.88
55f76dfe GuardLength = 10000.0
7e2e5018 BootDefManeuverRatio_Missiled = 0.50
98aa858f OffManeuverRatio_HoldPosition = 4.80
9d93b0b6 OffManeuverRatio_Through = 1.20
a17ea8fc DefManeuverRatio_BarrelRoll = 0.94
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 1.88
c11cad36 FiringLength = 3000.0
cbc80716 DefManeuverRatio_NoAction = 2.50
ce666dd1 DefManeuverRatio_SideRoll = 0.94
d746db41 OffManeuverRatio_NoAction = 4.00
e2fe30cd MusterLength = 10000.0
eb41fdfb DefManeuverRatio_TurnAway = 1.88
eece0777 AutoGuardLength = 2500.0
-- 'AI_ADAN_Vindicator_Margras' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.40
224f8336 CounterManeuverRatio = 0.50
240dca2c BootDefManeuverRatio_Aimed = 0.32
2e136f2c BootDefManeuverRatio_Fired = 0.80
429c4495 CounterLength = 6000.0
4913db76 DefManeuverRatio_BoostAway = 0.69
55f76dfe GuardLength = 10000.0
7e2e5018 BootDefManeuverRatio_Missiled = 0.80
98aa858f OffManeuverRatio_HoldPosition = 7.20
9d93b0b6 OffManeuverRatio_Through = 1.80
a17ea8fc DefManeuverRatio_BarrelRoll = 2.08
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 3.46
c11cad36 FiringLength = 3000.0
cbc80716 DefManeuverRatio_NoAction = 1.00
ce666dd1 DefManeuverRatio_SideRoll = 1.38
d746db41 OffManeuverRatio_NoAction = 1.00
e2fe30cd MusterLength = 10000.0
eb41fdfb DefManeuverRatio_TurnAway = 1.38
eece0777 AutoGuardLength = 2500.0
-- 'AI_TCAF_CraftSquadron' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.30
224f8336 CounterManeuverRatio = 0.50
240dca2c BootDefManeuverRatio_Aimed = 0.24
2e136f2c BootDefManeuverRatio_Fired = 0.60
429c4495 CounterLength = 6000.0
4913db76 DefManeuverRatio_BoostAway = 2.29
55f76dfe GuardLength = 10000.0
7e2e5018 BootDefManeuverRatio_Missiled = 0.60
98aa858f OffManeuverRatio_HoldPosition = 3.33
9d93b0b6 OffManeuverRatio_Through = 1.67
a17ea8fc DefManeuverRatio_BarrelRoll = 1.14
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 1.14
c11cad36 FiringLength = 3000.0
cbc80716 DefManeuverRatio_NoAction = 2.00
ce666dd1 DefManeuverRatio_SideRoll = 1.14
d746db41 OffManeuverRatio_NoAction = 5.00
e2fe30cd MusterLength = 10000.0
eb41fdfb DefManeuverRatio_TurnAway = 2.29
eece0777 AutoGuardLength = 2500.0
-- 'AI_ADAN_Elan_NightRavens' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.15
224f8336 CounterManeuverRatio = 0.50
240dca2c BootDefManeuverRatio_Aimed = 0.12
2e136f2c BootDefManeuverRatio_Fired = 0.30
429c4495 CounterLength = 6000.0
4913db76 DefManeuverRatio_BoostAway = 2.44
55f76dfe GuardLength = 10000.0
7e2e5018 BootDefManeuverRatio_Missiled = 0.30
98aa858f OffManeuverRatio_HoldPosition = 2.22
9d93b0b6 OffManeuverRatio_Through = 2.78
a17ea8fc DefManeuverRatio_BarrelRoll = 0.81
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 0.81
c11cad36 FiringLength = 3000.0
cbc80716 DefManeuverRatio_NoAction = 3.50
ce666dd1 DefManeuverRatio_SideRoll = 0.81
d746db41 OffManeuverRatio_NoAction = 5.00
e2fe30cd MusterLength = 10000.0
eb41fdfb DefManeuverRatio_TurnAway = 1.63
eece0777 AutoGuardLength = 2500.0
-- 'AI_ADAN_ElanPlus_Margras' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.25
224f8336 CounterManeuverRatio = 0.50
240dca2c BootDefManeuverRatio_Aimed = 0.20
2e136f2c BootDefManeuverRatio_Fired = 0.50
429c4495 CounterLength = 6000.0
4913db76 DefManeuverRatio_BoostAway = 2.05
55f76dfe GuardLength = 10000.0
7e2e5018 BootDefManeuverRatio_Missiled = 0.50
98aa858f OffManeuverRatio_HoldPosition = 3.00
9d93b0b6 OffManeuverRatio_Through = 3.00
a17ea8fc DefManeuverRatio_BarrelRoll = 0.68
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 2.05
c11cad36 FiringLength = 3000.0
cbc80716 DefManeuverRatio_NoAction = 2.50
ce666dd1 DefManeuverRatio_SideRoll = 1.36
d746db41 OffManeuverRatio_NoAction = 4.00
e2fe30cd MusterLength = 10000.0
eb41fdfb DefManeuverRatio_TurnAway = 1.36
eece0777 AutoGuardLength = 2500.0
-- 'AI_TCAF_RhinoLeader' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.15
224f8336 CounterManeuverRatio = 0.90
240dca2c BootDefManeuverRatio_Aimed = 0.10
2e136f2c BootDefManeuverRatio_Fired = 0.10
429c4495 CounterLength = 500000.0
4913db76 DefManeuverRatio_BoostAway = 0.22
55f76dfe GuardLength = 10000.1
7e2e5018 BootDefManeuverRatio_Missiled = 0.10
98aa858f OffManeuverRatio_HoldPosition = 7.20
9d93b0b6 OffManeuverRatio_Through = 1.80
a17ea8fc DefManeuverRatio_BarrelRoll = 0.11
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 0.33
c11cad36 FiringLength = 500000.1
cbc80716 DefManeuverRatio_NoAction = 9.00
ce666dd1 DefManeuverRatio_SideRoll = 0.11
d746db41 OffManeuverRatio_NoAction = 1.00
e2fe30cd MusterLength = 500000.0
eb41fdfb DefManeuverRatio_TurnAway = 0.22
eece0777 AutoGuardLength = 5000.1
-- 'AI_ADAN_ElanPlus_Taskent' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.20
224f8336 CounterManeuverRatio = 0.50
240dca2c BootDefManeuverRatio_Aimed = 0.16
2e136f2c BootDefManeuverRatio_Fired = 0.40
429c4495 CounterLength = 6000.0
4913db76 DefManeuverRatio_BoostAway = 2.33
55f76dfe GuardLength = 10000.0
7e2e5018 BootDefManeuverRatio_Missiled = 0.40
98aa858f OffManeuverRatio_HoldPosition = 2.22
9d93b0b6 OffManeuverRatio_Through = 2.78
a17ea8fc DefManeuverRatio_BarrelRoll = 0.78
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 1.56
c11cad36 FiringLength = 3000.0
cbc80716 DefManeuverRatio_NoAction = 3.00
ce666dd1 DefManeuverRatio_SideRoll = 0.78
d746db41 OffManeuverRatio_NoAction = 5.00
e2fe30cd MusterLength = 10000.0
eb41fdfb DefManeuverRatio_TurnAway = 1.56
eece0777 AutoGuardLength = 2500.0
-- 'AI_Structure' (6 fields)
429c4495 CounterLength = 20000.0
55f76dfe GuardLength = 5000.0
a2797b39 Type = Fleet
c11cad36 FiringLength = 15000.0
e2fe30cd MusterLength = 10.0
eece0777 AutoGuardLength = 3000.0
-- 'AI_TCAF_RhinoFlight' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.25
224f8336 CounterManeuverRatio = 0.90
240dca2c BootDefManeuverRatio_Aimed = 0.10
2e136f2c BootDefManeuverRatio_Fired = 0.10
429c4495 CounterLength = 500000.0
4913db76 DefManeuverRatio_BoostAway = 0.13
55f76dfe GuardLength = 10000.1
7e2e5018 BootDefManeuverRatio_Missiled = 0.10
98aa858f OffManeuverRatio_HoldPosition = 7.20
9d93b0b6 OffManeuverRatio_Through = 1.80
a17ea8fc DefManeuverRatio_BarrelRoll = 0.13
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 0.38
c11cad36 FiringLength = 500000.1
cbc80716 DefManeuverRatio_NoAction = 9.00
ce666dd1 DefManeuverRatio_SideRoll = 0.13
d746db41 OffManeuverRatio_NoAction = 1.00
e2fe30cd MusterLength = 500000.0
eb41fdfb DefManeuverRatio_TurnAway = 0.25
eece0777 AutoGuardLength = 5000.1
-- 'AI_ADAN_BomberSquadron_Rookie' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.05
224f8336 CounterManeuverRatio = 0.50
240dca2c BootDefManeuverRatio_Aimed = 0.04
2e136f2c BootDefManeuverRatio_Fired = 0.10
429c4495 CounterLength = 6000.0
4913db76 DefManeuverRatio_BoostAway = 2.06
55f76dfe GuardLength = 10000.0
7e2e5018 BootDefManeuverRatio_Missiled = 0.10
98aa858f OffManeuverRatio_HoldPosition = 1.00
9d93b0b6 OffManeuverRatio_Through = 4.00
a17ea8fc DefManeuverRatio_BarrelRoll = 0.69
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 0.69
c11cad36 FiringLength = 3000.0
cbc80716 DefManeuverRatio_NoAction = 4.50
ce666dd1 DefManeuverRatio_SideRoll = 0.69
d746db41 OffManeuverRatio_NoAction = 5.00
e2fe30cd MusterLength = 10000.0
eb41fdfb DefManeuverRatio_TurnAway = 1.38
eece0777 AutoGuardLength = 2500.0
-- 'AI_ADAN_CraftSquadron_Veteran' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.15
224f8336 CounterManeuverRatio = 0.50
240dca2c BootDefManeuverRatio_Aimed = 0.12
2e136f2c BootDefManeuverRatio_Fired = 0.30
429c4495 CounterLength = 6000.0
4913db76 DefManeuverRatio_BoostAway = 1.95
55f76dfe GuardLength = 10000.0
7e2e5018 BootDefManeuverRatio_Missiled = 0.30
98aa858f OffManeuverRatio_HoldPosition = 2.00
9d93b0b6 OffManeuverRatio_Through = 4.00
a17ea8fc DefManeuverRatio_BarrelRoll = 0.65
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 1.30
c11cad36 FiringLength = 3000.0
cbc80716 DefManeuverRatio_NoAction = 3.50
ce666dd1 DefManeuverRatio_SideRoll = 1.30
d746db41 OffManeuverRatio_NoAction = 4.00
e2fe30cd MusterLength = 10000.0
eb41fdfb DefManeuverRatio_TurnAway = 1.30
eece0777 AutoGuardLength = 2500.0
-- 'AI_Fleet_Test' (6 fields)
429c4495 CounterLength = 30000.0
55f76dfe GuardLength = 5000.0
a2797b39 Type = Fleet
c11cad36 FiringLength = 15000.0
e2fe30cd MusterLength = 20000.0
eece0777 AutoGuardLength = 3000.0
-- 'AI_TraceRoute' (6 fields)
429c4495 CounterLength = 20000.0
55f76dfe GuardLength = 5000.0
a2797b39 Type = Fleet
c11cad36 FiringLength = 15000.0
e2fe30cd MusterLength = 10.0
eece0777 AutoGuardLength = 3000.0
-- 'AI_ADAN_ElanTypeQ_Margras' (20 fields)
08dabbb5 ManeuverRatio_Cutoff = 0.30
224f8336 CounterManeuverRatio = 0.50
240dca2c BootDefManeuverRatio_Aimed = 0.24
2e136f2c BootDefManeuverRatio_Fired = 0.60
429c4495 CounterLength = 6000.0
4913db76 DefManeuverRatio_BoostAway = 1.45
55f76dfe GuardLength = 10000.0
7e2e5018 BootDefManeuverRatio_Missiled = 0.60
98aa858f OffManeuverRatio_HoldPosition = 4.67
9d93b0b6 OffManeuverRatio_Through = 2.33
a17ea8fc DefManeuverRatio_BarrelRoll = 0.73
a2797b39 Type = Squad
b1eb847b DefManeuverRatio_TurnAttack = 2.91
c11cad36 FiringLength = 3000.0
cbc80716 DefManeuverRatio_NoAction = 2.00
ce666dd1 DefManeuverRatio_SideRoll = 1.45
d746db41 OffManeuverRatio_NoAction = 3.00
e2fe30cd MusterLength = 10000.0
eb41fdfb DefManeuverRatio_TurnAway = 1.45
eece0777 AutoGuardLength = 2500.0

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,14 @@ Status: ✅ for the record vocabulary and the stage→table wiring;
This is the answer to "where does the game say *which* enemies appear in a
mission, and when". It is data, not code, and it lives in the language paks.
> **Correction (2026-08-24):** the record dumped below is the `_Test` *template*,
> not a real stage record. The real `Stage_S02` record has six sub-records —
> including `Phase_1..3` — and 19 `StageResource` fields, several of which the
> template does not have at all (`EnumerateSubobjective`, `EnumerateAIParams`,
> `BackGroundID`, `WingmanIconID_0..3`). See
> [stage-mission-tables.md](stage-mission-tables.md) for the real one. The
> stage→`UnitGroup` wiring and the hash table below are unaffected.
## ✅ The per-stage definition record
`GP_MAIN_GAME_<lang>.pak` contains one IDXD object per stage. Dumped with

View File

@@ -0,0 +1,144 @@
# The stage table set — phases, routes, sub-objectives and AI parameters
Status: ✅ the table set and how the stage record reaches it, validated across
all 28 stages; ✅ the route/formation keyframe layout; ❔ what the route-name
phase suffixes mean and where a phase *advances*.
This is the layer that answers "which enemies and objectives come into play,
and when". It is all data, in `GP_MAIN_GAME_<lang>.pak`, in the same
self-describing IDXD container as the squadron roster
([unit-group-table.md](unit-group-table.md)).
Tool: `tools/re-capture/stagetbl.py` (pure static, runs no emulator).
```
python3 tools/re-capture/stagetbl.py --follow S02 # stage record + every table it names
python3 tools/re-capture/stagetbl.py --name 'stage\AIParams_S02.tbl'
```
Committed dumps: [`../data/stage-s02-tables.txt`](../data/stage-s02-tables.txt)
and [`../data/aiparams-s02.txt`](../data/aiparams-s02.txt).
## ✅ The real stage record is much richer than the `_Test` template
[stage-definition-table.md](stage-definition-table.md) was written from the
`_Test` template record, which is a stripped stand-in. The **real** `Stage_S02`
record has six sub-records, not one, and 19 fields in its `StageResource` rather
than the template's 13:
```
StageResource EnumerateLocalString = EnumLocalString_S02.tbl
EnumerateWeapon = EnumWeapon_Test.tbl
CollisionMeshes = CollisionSet_S02.bin
EnumerateNullFrame = Route_S02.tbl
EnumerateCharacter = EnumCharacter_S02.tbl
EnumerateUnit = EnumUnit_S02.tbl
EnumerateFormation = FormationSet_S02.tbl
BackGroundID = Lebendorf
WingmanIconID_0..3 = "", PGHUD_WING4, "", ""
NamePlate = nameplate_S02.tbl
EnumerateSubobjective = SUBObjectiveSettings_S02.tbl
BackGroundPackage = game:\hidden\Resource3D\BG_Lebendorf_near.xpr
EnumerateAIParams = AIParams_S02.tbl
StageResourcePackage = game:\hidden\Resource3D\Stage_S02.xpr
MessageSet = StageMessageSet_S02.tbl
EnumerateSquadron = UnitGroup_S02.tbl
LodResource LodEnumTable = DefTables.pak+EnumLODSet_test.tbl
Phase_1 MapPath = mapmesh_box_500km.rgn, MapMesh = mapmesh_box_500km.col,
Phase_2 AsteroidDefinition = "", BackgroundResourceID = ""
Phase_3
MotionResource MotionEnumTable = DefTables.pak+EnumGameModel_test.tbl
```
**A stage is divided into `Phase_N` blocks** — three for Stage 02 — each naming
its own map path, map mesh, asteroid definition and background. `--follow`
resolves every table above except `StageMessageSet_S02.tbl`, which is not in
`GP_MAIN_GAME_E.pak` under that name (❔, probably localised elsewhere).
## ✅ `Route_S<NN>.tbl` — the arrival paths, bound to squadron *and* phase
121 records for Stage 02, 1449 across the corpus. Records are named
**`Route_<squadron>_p<phase><kind>`**, e.g. `Route_ADN101_p1F`,
`Route_ADT102_p1A`, `Route_ADN310_p3F` — tying a route to a squadron id from
[`UnitGroup`](unit-group-table.md) and to a phase from the stage record.
A record is a **time-stamped keyframed path**: `FrameCount` frames of eight
values each,
```
(time, quat.x, quat.y, quat.z, quat.w, pos.x, pos.y, pos.z)
```
`Route_ADN101_p1F` is 3 frames at t = 0, 20, 30, running from
(-2500, -100, -2500) to (4241.8, 100, -1017.7).
**Self-check: `len(fields) == FrameCount * 8 + 1` holds for 1449 of 1449 route
records** across the 28 stages that have a `Route_S<NN>.tbl` (S17 has none, as
it has no stage record or `UnitGroup` either). It also holds 16/16 for
`FormationSet_S02.tbl` and 9/9 for `Null_Test.tbl`.
Values are stored as *text* in the string pool (`"0"`, `"-0.011578"`), not as
binary floats.
Phase/kind suffix counts across the corpus:
| | F | S | A | M | B |
|---|---|---|---|---|---|
| p1 | 397 | 404 | 54 | 72 | 9 |
| p2 | 125 | 107 | 17 | 59 | |
| p3 | 48 | 48 | | 19 | |
❔ What `F`, `S`, `A`, `M`, `B` stand for is **not settled**. Every phase has F
and S; A and B appear only in phases 12.
### 🔴 The eight-value frame is the common case, not universal
`Formation_Test.tbl` breaks it: `Formation_Fleet_01` has `FrameCount = 1` with
136 positional fields, and `Formation_Fleet_02` has `FrameCount = 8` with 32
(four per frame). So `FrameCount` does not always index an 8-tuple, and any
parser must not assume it. `Formation_100` (100 frames, 800 fields) and all of
`FormationSet_S02` do follow the 8-value layout.
## ✅ `SUBObjectiveSettings_S<NN>.tbl` — the sub-objectives
Stage 02 declares nine (`SUBOBJ_005``SUBOBJ_014`), each with:
| field | example |
|---|---|
| `BonusPoint_EASY` / `_NORMAL` / `_HARD` | 500 / 1000 / 2000 |
| `UNLockItemID` | 0, or 13 for `SUBOBJ_013` |
| `HUDString01` / `HUDString02` | `SUBOBJ_010_Mes_L1` / `_L2` |
This is the objective *reward and presentation* table. It does **not** say what
completes an objective or when one becomes active — that trigger is still ❔.
## ✅ `AIParams_S<NN>.tbl` — combat tuning, directly portable
34 AI profiles for Stage 02, referenced by `AIID` in `UnitGroup`. Two shapes:
* `Type = Fleet` — 6 fields: `FiringLength`, `GuardLength`, `AutoGuardLength`,
`MusterLength`, `CounterLength`, `Type`.
* `Type = Squad` — those six plus 14 manoeuvre weights:
`ManeuverRatio_Cutoff`, `CounterManeuverRatio`,
`BootDefManeuverRatio_{Aimed,Fired,Missiled}`,
`DefManeuverRatio_{BoostAway,BarrelRoll,TurnAttack,TurnAway,SideRoll,NoAction}`,
`OffManeuverRatio_{HoldPosition,Through,NoAction}`.
e.g. `AI_ADAN_CraftSquadron_Ace` fires at 3000.0, guards at 10000.0, musters at
10000.0, and weights `DefManeuverRatio_BoostAway = 0.88` against
`AI_ADAN_Elan_GR_Violeta`'s 2.81. Full table in
[`../data/aiparams-s02.txt`](../data/aiparams-s02.txt).
These are exact original values obtained by static RE, so they are usable in the
port directly.
## What is still open
* **The phase advance condition.** The stage record declares `Phase_1..3` and
routes are tagged by phase, but nothing found so far says what *ends* a phase.
* The route-name kind letters `F`, `S`, `A`, `M`, `B`.
* What activates or completes a sub-objective.
* `StageMessageSet_S<NN>.tbl` does not resolve in `GP_MAIN_GAME_E.pak`.
* The `UnitGroup` record key and its member field `n` remain ❔ from
[unit-group-table.md](unit-group-table.md). `n` is now a little more
suspicious: routes are per-squadron, so `n` is unlikely to be a spawn count.

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Dump any `stage\\*.tbl` mission table from GP_MAIN_GAME_<lang>.pak.
Pure static work -- reads the extracted disc, runs no emulator. The container is
the same self-describing IDXD format decoded for the squadron roster; see
docs/re/structures/unit-group-table.md and tools/re-capture/unitgroup.py.
python3 tools/re-capture/stagetbl.py Stage_S02 # by stage record
python3 tools/re-capture/stagetbl.py --name 'stage\\AIParams_S02.tbl'
python3 tools/re-capture/stagetbl.py --follow S02 # stage record + all
# tables it names
"""
import argparse, re, sys
sys.path.insert(0, __file__.rsplit('/', 1)[0])
from unitgroup import read_entry, name_hash, parse, named
PAK = '/work/sylph_extract/dat/GP_MAIN_GAME_E.pak'
def stage_record_hash(stage):
"""The per-stage definition record is not name-addressed; find it by content."""
import struct, glob, os, zlib
idx = open(PAK, 'rb').read()
base = PAK[:-4]
data = b''.join(open(p, 'rb').read() for p in sorted(glob.glob(base + '.p[0-9][0-9]')))
want = ('UnitGroup_%s.tbl' % stage).encode() # the _Test template names
# Stage_S02.xpr too, so key
# off the squadron table
for i in range(struct.unpack_from('>I', idx, 4)[0]):
h, off, cs = struct.unpack_from('>III', idx, 16 + i * 12)
s = data[off:off + cs]
try: b = zlib.decompress(s[10:]) if s[:2] == b'Z1' else s
except Exception: continue
if b[:4] == b'IDXD' and want in b and b'EnumerateSquadron' in b:
return h
raise KeyError('no stage record for ' + stage)
def dump(label, recs, limit=None):
print('=== %s (%d records) ===' % (label, len(recs)))
for r in recs[:limit]:
print('-- %r (%d fields)' % (r['squadron'], len(r['fields'])))
for tag, fn, v in r['fields']:
print(' %08x %-30s = %s' % (tag, fn if fn else '(pos)', v))
if limit and len(recs) > limit:
print(' ... %d more records not shown' % (len(recs) - limit))
def main():
ap = argparse.ArgumentParser()
ap.add_argument('target', nargs='?', default='Stage_S02')
ap.add_argument('--name', help=r'archive name, e.g. stage\AIParams_S02.tbl')
ap.add_argument('--follow', metavar='SNN', help='stage record plus every .tbl it names')
ap.add_argument('--limit', type=int, default=None)
a = ap.parse_args()
if a.name:
dump(a.name, parse(read_entry(PAK, name_hash(a.name))), a.limit); return 0
stage = a.follow or (a.target[6:] if a.target.startswith('Stage_') else a.target)
recs = parse(read_entry(PAK, stage_record_hash(stage)))
dump('Stage_%s definition record' % stage, recs)
if not a.follow: return 0
seen = set()
for r in recs:
for _, fn, v in r['fields']:
if not v.endswith('.tbl') or v in seen: continue
seen.add(v)
src, n = PAK, 'stage\\' + v.split('+')[-1]
if '+' in v and 'DefTables' in v:
src, n = '/work/sylph_extract/hidden/DefTables.pak', v.split('+')[-1]
print()
try: dump('%s (via %s)' % (n, fn), parse(read_entry(src, name_hash(n))), a.limit)
except Exception as e: print('=== %s -> unresolved: %s' % (n, e))
return 0
if __name__ == '__main__':
sys.exit(main())