From 2456d28f1a334a6fcb75385c9a9ec3c6d95535bb Mon Sep 17 00:00:00 2001 From: "Claude (auto)" Date: Thu, 27 Aug 2026 21:57:24 +0000 Subject: [PATCH] re: AIParams disc-wide, and a correction to my own last entry CORRECTION FIRST. Last iteration I recorded sub_8233C368 as unblocking a NEEDS-HUMAN item -- "the corpus carries the AI tail of Maneuver as NEEDS-HUMAN/runtime; it is statically reachable after all." That was wrong. stage-mission-tables.md already documents AIParams_S02.tbl as exact original values obtained by static RE, directly portable, listing all 20 field names and both shapes. I grepped FiringLength and saw the file but did not read the section. Finding the owning doc is not reading it. The only genuinely new part was the loader's name. What is new: the census generalises Stage 02 to the disc. 23 AIParams objects, identical in all six GP_MAIN_GAME_* paks, sharing ONE declared-name set of 34 profiles; 782 profile records = 23 x 34; 0 declared names without a record in their own object. So "34 AI profiles" is not a Stage-02 fact -- every stage carries the same 34 and only the values move. The roster is declared by an Enumerate_AIs record whose field names are the profile names, the same declaration-table mechanism that closed DefTables. Type predicts the field count with exactly two exceptions: Fleet -> 6 fields is 253/253 zero partials; Squad -> 20 fields is 483/529. The 46-record residual in full: AI_Test and AI_CraftSquadron_Test, both Type = Squad with only the six base fields, in all 23 objects. No profile's shape varies between objects. New regenerator aiparams_census.py, 45-line artefact, byte-identical across two runs; the other fourteen verify unchanged. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE --- docs/re/BACKLOG.md | 41 ++++++++++ docs/re/INDEX.md | 2 +- docs/re/data/aiparams-census.txt | 45 +++++++++++ docs/re/structures/player-tuning-tables.md | 18 ++--- docs/re/structures/stage-mission-tables.md | 46 +++++++++++ tools/re-capture/aiparams_census.py | 88 ++++++++++++++++++++++ 6 files changed, 230 insertions(+), 10 deletions(-) create mode 100644 docs/re/data/aiparams-census.txt create mode 100644 tools/re-capture/aiparams_census.py diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 714211c..fd5235b 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -5125,3 +5125,44 @@ cross-linking the S16 `Guardian` object), `sub_822AFA50` 🟑 Not settled: the index says what each function *names*, not what each *means* β€” nothing above was opened. And the 107 low-confidence rows were flagged, not re-solved with a stricter method. + +--- + +## βœ…βš οΈ 2026-08-27 β€” `AIParams` disc-wide, and a correction to my own last entry + +Item (a), "open `sub_8233C368`, the AI table". New regenerator +`tools/re-capture/aiparams_census.py` β†’ `docs/re/data/aiparams-census.txt` +(45 lines, byte-identical across two runs). + +⚠️ **CORRECTION FIRST.** Last iteration I recorded `sub_8233C368` as unblocking a +NEEDS-HUMAN item β€” *"the corpus carries the AI tail of `Maneuver` as +NEEDS-HUMAN/runtime; it is statically reachable after all."* **That was wrong.** +`stage-mission-tables.md` already documents `AIParams_S02.tbl` as βœ… *"exact +original values obtained by static RE… usable in the port directly"*, listing all +20 field names and both shapes. I grepped `FiringLength` and saw the file, but +did not read the section. **Finding the owning doc is not reading it** β€” the same +lesson this corpus has paid for before. The only genuinely new part was the +**loader's name**. + +βœ… **What is new: the census generalises Stage 02 to the disc.** 23 `AIParams` +objects, identical in all six `GP_MAIN_GAME_*` paks, sharing **one** declared-name +set of **34** profiles; **782** profile records = 23 Γ— 34; **0** declared names +without a record in their own object. So "34 AI profiles" is not a Stage-02 fact β€” +**every stage carries the same 34 and only the values move.** + +πŸ”‘ The roster is declared by an **`Enumerate_AIs` record whose field names are the +profile names** β€” the same declaration-table mechanism that closed `DefTables`. + +⚠️ **`Type` predicts the field count, with exactly two exceptions.** +`Fleet` β†’ 6 fields is **253/253, zero partials**; `Squad` β†’ 20 fields is +**483/529**. The 46-record residual, in full: **`AI_Test` and +`AI_CraftSquadron_Test`**, both `Type = Squad` with only the six base fields, in +all 23 objects (2 Γ— 23 = 46) β€” the family's `_Test` templates again. **No +profile's shape varies between objects (0).** + +βœ… **The loader**: `sub_8233C368`, `r28`, base `0x8208583C`, 20 names β€” exactly +the field set above and nothing else. + +🟑 Not settled: which of the 23 objects belongs to which stage (only +`AIParams_S02.tbl` is named by the corpus; the disc-wide harvest was not re-run +against the other 22), and what consumes an `AIID` at runtime. diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index c2c12f6..08796c9 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -121,7 +121,7 @@ files, which is how the same ground got covered twice. | [`structures/savegame-format.md`](structures/savegame-format.md) | Save file (`savedata`) β€” container βœ… exact, 3 fields named βœ…, rest ❔ (2026-08-11) | βœ… CONFIRMED for the container and the chunk layout β€” parsed off the | | [`structures/sound-slb.md`](structures/sound-slb.md) | Sound bank audio β€” `sound.pak` / `.slb` / XMA1 | β€” | | [`structures/stage-definition-table.md`](structures/stage-definition-table.md) | Stage definition table and the squadron (`UnitGroup`) roster | βœ… for the record vocabulary and the stageβ†’table wiring; | -| [`structures/stage-mission-tables.md`](structures/stage-mission-tables.md) | The stage table set β€” phases, routes, sub-objectives and AI parameters | βœ… the table set and how the stage record reaches it, validated across | +| [`structures/stage-mission-tables.md`](structures/stage-mission-tables.md) | The stage table set β€” phases, routes, sub-objectives and AI parameters | βœ… the table set and how the stage record reaches it, validated across; **`AIParams` disc-wide: 23 objects, one shared 34-profile roster (782 records), loader `sub_8233C368`; `Type`β†’field-count holds except the two `_Test` templates** | | [`structures/texture-color-k8888.md`](structures/texture-color-k8888.md) | Texture colour interpretation β€” `k_8_8_8_8` (32bpp UI/HUD textures) | β€” | | [`structures/ui-composable-bundles.md`](structures/ui-composable-bundles.md) | A screen build is not the only thing `compose` can draw | βœ… CONFIRMED by measurement over the disc, with the artifact to | | [`structures/ui-focus-and-effect-elements.md`](structures/ui-focus-and-effect-elements.md) | `_eff` glow layers are not focused-state records | βœ… CONFIRMED by measurement over all 965 screen builds on the disc, | diff --git a/docs/re/data/aiparams-census.txt b/docs/re/data/aiparams-census.txt new file mode 100644 index 0000000..8c421c6 --- /dev/null +++ b/docs/re/data/aiparams-census.txt @@ -0,0 +1,45 @@ +# AIParams across the whole disc +# Regenerate: python3 tools/re-capture/aiparams_census.py +# See docs/re/structures/stage-mission-tables.md + +## CONTROL the six language copies + GP_MAIN_GAME_D.pak 23 AIParams objects + GP_MAIN_GAME_E.pak 23 AIParams objects + GP_MAIN_GAME_F.pak 23 AIParams objects + GP_MAIN_GAME_I.pak 23 AIParams objects + GP_MAIN_GAME_J.pak 23 AIParams objects + GP_MAIN_GAME_S.pak 23 AIParams objects + +## The roster + objects: 23 distinct declared-name sets: 1 roster size: [34] + declared names with no record in their own object: 0 + profile records total: 782 (= 23 objects x 34) + +## Does `Type` predict the field count? + Fleet 6 fields x253 + Squad 6 fields x46 + Squad 20 fields x483 + profiles breaking the rule, in full: 2 ['AI_CraftSquadron_Test', 'AI_Test'] + profiles whose (Type, field count) varies between objects: 0 [] + +## Field census over every profile record + x782 AutoGuardLength + x782 CounterLength + x782 FiringLength + x782 GuardLength + x782 MusterLength + x782 Type + x483 BootDefManeuverRatio_Aimed + x483 BootDefManeuverRatio_Fired + x483 BootDefManeuverRatio_Missiled + x483 CounterManeuverRatio + x483 DefManeuverRatio_BarrelRoll + x483 DefManeuverRatio_BoostAway + x483 DefManeuverRatio_NoAction + x483 DefManeuverRatio_SideRoll + x483 DefManeuverRatio_TurnAttack + x483 DefManeuverRatio_TurnAway + x483 ManeuverRatio_Cutoff + x483 OffManeuverRatio_HoldPosition + x483 OffManeuverRatio_NoAction + x483 OffManeuverRatio_Through diff --git a/docs/re/structures/player-tuning-tables.md b/docs/re/structures/player-tuning-tables.md index 22ce18a..88540c0 100644 --- a/docs/re/structures/player-tuning-tables.md +++ b/docs/re/structures/player-tuning-tables.md @@ -226,15 +226,15 @@ substructures (`sub_823479B8` β†’ `ParentStructureID`), and six camera/fog reade (`sub_825F2CF0`, `sub_825F2F88`, `sub_8247DFC0`, `sub_823B2620`, `sub_82222E70`, `sub_822C7480`). -**πŸ”‘ The find: `sub_8233C368` reads the AI behaviour table.** `r28`, base -`0x8208583C`, 20 names β€” **`Enumerate_AIs`**, `FiringLength`, `GuardLength`, -`AutoGuardLength`, `CounterLength`, `MusterLength`, … [[stage-mission-tables]] -owns those *field* names on the data side; **`Enumerate_AIs` appears in no -document, and no reader was known** β€” the corpus carries the AI tail of -`Maneuver` as 🟑 NEEDS-HUMAN/runtime. It is statically reachable after all. -The same base `0x8208583C` also serves `sub_82338EE0` (97 names, -`Weapon TargetType SpecialWeaponType ReticleType IsCharging …`) β€” the **weapon** -datasheet loader, likewise not previously named. +**πŸ”‘ The find: `sub_8233C368` is the `AIParams` loader.** `r28`, base +`0x8208583C`, 20 names β€” `Enumerate_AIs`, `FiringLength`, `GuardLength`, +`AutoGuardLength`, `CounterLength`, `MusterLength` and the 14 manoeuvre weights. +⚠️ **Corrected:** I first wrote that this unblocked a NEEDS-HUMAN item. +[[stage-mission-tables]] already had `AIParams` as βœ… static and directly +portable, with every field name; **only the loader was unnamed.** The disc-wide +census that followed is in that document. The same base `0x8208583C` also serves +`sub_82338EE0` (97 names, `Weapon TargetType SpecialWeaponType ReticleType +IsCharging …`) β€” the **weapon** datasheet loader, likewise not previously named. **Other unowned blocks the index surfaces** (measured, not investigated): `sub_822215D0` (205/206, `PGHUD_*` / `PGREMAIN_NUM%d` / `PGTIMER_NUM%d` β€” HUD diff --git a/docs/re/structures/stage-mission-tables.md b/docs/re/structures/stage-mission-tables.md index 1612ddc..e1e36f6 100644 --- a/docs/re/structures/stage-mission-tables.md +++ b/docs/re/structures/stage-mission-tables.md @@ -132,6 +132,52 @@ e.g. `AI_ADAN_CraftSquadron_Ace` fires at 3000.0, guards at 10000.0, musters at These are exact original values obtained by static RE, so they are usable in the port directly. +## βœ… `AIParams` across the whole disc, and its loader (2026-08-27) + +- **Regenerate:** `python3 tools/re-capture/aiparams_census.py` β†’ [`../data/aiparams-census.txt`](../data/aiparams-census.txt) + +The section above measured Stage 02. The disc holds **23** `AIParams` objects, +identically in all six `GP_MAIN_GAME_*` paks, and they share **one roster**: + +| | | +|---|---:| +| `AIParams` objects per pak | **23** | +| distinct declared-name sets across them | **1** | +| profiles per object | **34** | +| profile records total | **782** = 23 Γ— 34 | +| declared names with no record in their own object | **0** | + +βœ… So "34 AI profiles" is not a Stage-02 fact β€” **every stage carries the same 34 +profiles and only the values move.** πŸ”‘ The roster is declared the same way +`DefTables` names its tables: an **`Enumerate_AIs` record whose field names are +the profile names** (see [[archive-naming]] route 2). + +### ⚠️ `Type` predicts the field count β€” with exactly two exceptions + +| | | +|---|---| +| `Type = Fleet` β†’ 6 fields | **253 / 253**, zero partials | +| `Type = Squad` β†’ 20 fields | **483 / 529** | + +The 46-record residual, in full: **`AI_Test` and `AI_CraftSquadron_Test`** β€” both +`Type = Squad` but carrying only the six base fields, in all 23 objects +(2 Γ— 23 = 46). They are the family's `_Test` templates, the same trap the stage +tables sprang before. **No profile's shape varies between objects (0).** + +### βœ… The loader: `sub_8233C368` + +Found by the base-solver ([[player-tuning-tables]]): `r28`, base `0x8208583C`, +20 names β€” `Enumerate_AIs`, `FiringLength`, `GuardLength`, `AutoGuardLength`, +`CounterLength`, `MusterLength` and the 14 manoeuvre weights. That is exactly the +field set above, so the loader names the whole schema and nothing else. + +> ⚠️ **Correcting my own note from the previous iteration.** I recorded this as +> "the AI behaviour table, which the corpus carried as NEEDS-HUMAN/runtime, is +> statically reachable". **Wrong** β€” this document already had `AIParams` as βœ… +> *"exact original values obtained by static RE, directly portable"*, with all +> 20 field names. The only thing that was actually missing is the **loader's +> name**, plus the disc-wide generalisation above. + ## What is still open * **The phase advance condition.** The stage record declares `Phase_1..3` and diff --git a/tools/re-capture/aiparams_census.py b/tools/re-capture/aiparams_census.py new file mode 100644 index 0000000..8d344c4 --- /dev/null +++ b/tools/re-capture/aiparams_census.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Census every AIParams object on the disc, not just Stage 02's. + +`stage-mission-tables.md` documents `AIParams_S02.tbl` -- 34 profiles, two +shapes. This checks that against the whole disc: how many such objects exist, +whether the 34-name roster is shared, and whether `Type` really predicts the +field count. Regenerates docs/re/data/aiparams-census.txt. +""" +import sys, os, glob, collections + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +import unitgroup as U +from unit_substructures import pak_entries + +def objects(pak): + out = [] + for h, b in pak_entries(pak): + if b[:4] != b'IDXD' or b'Enumerate_AIs' not in b: + continue + try: + recs = U.parse(b) + except Exception: + continue + if 'Enumerate_AIs' not in {r['squadron'] for r in recs}: + continue + decl = [n for r in recs if r['squadron'] == 'Enumerate_AIs' + for _t, n, _v in r['fields'] if n] + prof = [(r['squadron'], dict((n, v) for _t, n, v in r['fields'] if n)) + for r in recs if r['squadron'] != 'Enumerate_AIs'] + out.append((h, decl, prof)) + return out + +def main(): + paks = sorted(p for p in glob.glob('/work/sylph_extract/**/*.pak', recursive=True) + if os.path.basename(p).startswith('GP_MAIN_GAME_') + and '2D' not in os.path.basename(p)) + print("# AIParams across the whole disc") + print("# Regenerate: python3 tools/re-capture/aiparams_census.py") + print("# See docs/re/structures/stage-mission-tables.md") + + print("\n## CONTROL the six language copies") + per = {} + for pk in paks: + o = objects(pk) + per[os.path.basename(pk)] = o + print(" %-24s %2d AIParams objects" % (os.path.basename(pk), len(o))) + + objs = per[os.path.basename(paks[0])] + sets = {tuple(sorted(d)) for _h, d, _p in objs} + print("\n## The roster") + print(" objects: %d distinct declared-name sets: %d roster size: %s" + % (len(objs), len(sets), sorted({len(s) for s in sets}))) + absent = sum(1 for _h, d, p in objs + for n in d if n not in {s for s, _f in p}) + print(" declared names with no record in their own object: %d" % absent) + print(" profile records total: %d (= %d objects x %d)" + % (sum(len(p) for _h, _d, p in objs), len(objs), + len(objs[0][2]) if objs else 0)) + + ct = collections.Counter() + byname = collections.defaultdict(set) + for _h, _d, p in objs: + for s, f in p: + ct[(f.get('Type'), len(f))] += 1 + byname[s].add((f.get('Type'), len(f))) + print("\n## Does `Type` predict the field count?") + for k, v in sorted(ct.items(), key=lambda kv: (str(kv[0][0]), kv[0][1])): + print(" %-6s %2d fields x%d" % (k[0], k[1], v)) + odd = sorted(s for s, v in byname.items() + if v & {('Squad', n) for n in range(30) if n != 20} + or v & {('Fleet', n) for n in range(30) if n != 6}) + print(" profiles breaking the rule, in full: %d %s" % (len(odd), odd)) + var = sorted(s for s, v in byname.items() if len(v) > 1) + print(" profiles whose (Type, field count) varies between objects: %d %s" + % (len(var), var)) + + fc = collections.Counter() + for _h, _d, p in objs: + for _s, f in p: + for k in f: + fc[k] += 1 + print("\n## Field census over every profile record") + for k, v in sorted(fc.items(), key=lambda kv: (-kv[1], kv[0])): + print(" x%-4d %s" % (v, k)) + +if __name__ == "__main__": + main()