#!/usr/bin/env python3 """The Hangar loadout system: loadout -> per-slot allow-list -> arsenal item. A loadout record's `Arm1`/`Arm2`/`Arm3`/`Nose` do NOT name items. They name a per-slot ALLOW-LIST record whose positional (unnamed) fields are the ordered candidate item names. Regenerates docs/re/data/hangar-loadouts.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 ITEMF = {'Dependency', 'MissionObjective', 'Model', 'Package', 'PlayerWeapon', 'Points', 'Power', 'Range'} SLOTS = ('Arm1', 'Arm2', 'Arm3', 'Nose') def main(): ars = glob.glob('/work/sylph_extract/**/GP_HANGAR_ARSENAL.pak', recursive=True)[0] unit_ids, char_ids = set(), set() for pk in sorted(glob.glob('/work/sylph_extract/**/*.pak', recursive=True)): for h, b in pak_entries(pk): if b[:4] != b'IDXD': continue try: recs = U.parse(b) except Exception: continue for r in recs: if r['squadron'] != 'Generic': continue n = U.named(r) if 'HP' in n and n.get('ID'): unit_ids.add(n['ID']) elif 'SideID' in n and n.get('ID'): char_ids.add(n['ID']) items, loadouts, allow, roster = {}, {}, {}, None for h, b in pak_entries(ars): if b[:4] != b'IDXD': continue try: recs = U.parse(b) except Exception: continue for r in recs: n = U.named(r); nm = r['squadron'] if nm == 'WEAPONS' and roster is None: roster = r if ITEMF <= set(n): items.setdefault(nm, r) elif set(SLOTS) <= set(n): loadouts.setdefault(nm, r) elif set(n) == {'Type'}: allow.setdefault(nm, r) idxd = [(h, U.parse(b)) for h, b in pak_entries(ars) if b[:4] == b'IDXD'] withu = [(h, r) for h, r in idxd if any(x['squadron'] == 'UNITS' for x in r)] rosters = collections.Counter() for h, recs in withu: for r in recs: if r['squadron'] == 'UNITS': rosters[tuple(sorted(n for t, n, v in r['fields']))] += 1 print("# The Hangar loadout system") print("# Regenerate: python3 tools/re-capture/hangar_loadouts.py") print("# See docs/re/structures/hangar-loadout-system.md") print("\n## the pak is STAGE-SCOPED -- one config entry per stage per language") print(" total pak entries %4d IDXD entries %3d" % (len(pak_entries(ars)), len(idxd))) print(" entries carrying a UNITS roster %3d = %d stages x 6 languages" % (len(withu), len(withu) // 6)) print(" entries without one %3d = %d x 6 (the item table + siblings)" % (len(idxd) - len(withu), (len(idxd) - len(withu)) // 6)) print(" every roster count is a multiple of 6: %s ; they sum to %d stages" % (all(c % 6 == 0 for c in rosters.values()), sum(c // 6 for c in rosters.values()))) print("\n## the %d distinct UNITS rosters -- who is in the flight, per stage" % len(rosters)) for t, c in rosters.most_common(): print(" %2d stage(s) %d pilots: %s" % (c // 6, len(t), ", ".join(t))) print("\n## counts, UNION over all entries (each name first-seen)") print(" arsenal items %3d loadout records %3d per-slot allow-lists %3d" % (len(items), len(loadouts), len(allow))) var = collections.defaultdict(set) for h, recs in idxd: for r in recs: if set(U.named(r)) == {'Type'}: var[r['squadron']].add(tuple(v for t, n, v in r['fields'] if n is None)) print("\n## allow-list CONTENT VARIES BY ENTRY -- distinct contents per name") for k in sorted(var): print(" %-18s %d" % (k, len(var[k]))) print("\n## loadout records") for nm in sorted(loadouts): n = U.named(loadouts[nm]) u = n.get('UnitID', '') kind = 'unit' if u in unit_ids else ('character' if 'Character' + u in char_ids else '???') print(" %-16s %-16s %-16s %-16s %-16s UnitID=%-34s (%s)%s" % (nm, n.get('Arm1'), n.get('Arm2'), n.get('Arm3'), n.get('Nose'), u, kind, ' PlayerUnit' if 'PlayerUnit' in n else '')) tot = hit = 0 for r in loadouts.values(): n = U.named(r) for f in SLOTS: tot += 1; hit += n.get(f, '').strip() in allow print("\n## CONTROL loadout.Arm1/2/3/Nose names an ALLOW-LIST record") print(" %d/%d" % (hit, tot)) ptot = phit = 0 miss = collections.Counter() for r in allow.values(): for t, n, v in r['fields']: if n is not None: continue ptot += 1 if v in items: phit += 1 else: miss[v] += 1 rset = {v for _, _, v in roster['fields']} print("\n## CONTROL allow-list positional entries name an ARSENAL ITEM") print(" %d/%d" % (phit, ptot)) print(" misses: %s" % miss.most_common()) print(" every miss is a WEAPONS-roster sentinel with no item record: %s" % (set(miss) <= (rset - set(items)))) print("\n## CONTROL loadout.UnitID") u = [U.named(r).get('UnitID', '').strip() for r in loadouts.values()] print(" %d loadouts: %d name a unit Generic.ID, %d name a character (as 'Character'+value), %d neither" % (len(u), sum(1 for x in u if x in unit_ids), sum(1 for x in u if 'Character' + x in char_ids), sum(1 for x in u if x not in unit_ids and 'Character' + x not in char_ids))) print(" neither: %s" % sorted({x for x in u if x not in unit_ids and 'Character' + x not in char_ids})) print("\n## the allow-lists, in order") for nm in sorted(allow): vs = [v for t, n, v in allow[nm]['fields'] if n is None] print(" %-16s Type=%-5s %2d: %s" % (nm, U.named(allow[nm]).get('Type'), len(vs), ", ".join(vs))) if __name__ == "__main__": main()