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