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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
Claude (auto)
2026-08-27 21:57:24 +00:00
parent 5c672b11e0
commit 91ba488d47
6 changed files with 230 additions and 10 deletions

View File

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