Files
Sylpheed/tools/re-capture/hangar_loadouts.py
sim ac0ad579fd chore: retire the last dead paths and names from the consolidation
Nothing here changes what a tool computes; it changes where tools look.

- tools/re-capture: 33 censuses globbed /work/sylph_extract, a path that has
  existed nowhere since /work became a clone, so they matched nothing and
  printed empty results. They now resolve the disc through a new disc.py
  from $SYLPHEED_DISC and exit loudly without it (the #44 fix, generalised).
  Nine scripts that imported siblings from the retired Reborn checkout or an
  old session scratchpad now import from their own directory. unitgroup.py
  only needs the variable when --pak is not given.
- sylpheed-xex: the loader only ever uses the XEX2 retail key. The dead
  devkit key and a doc comment claiming a devkit fallback that does not
  exist are gone; Project Sylpheed is a retail XEX2, so no XEX1 key either.
- sylpheed-viewer: real_font_rasterizes looked for /tmp/sylph_extract and so
  always skipped. It reads $SYLPHEED_DISC now, and passes against the disc.
- Comments and docs that named xenia-rs, the Reborn repository or /work/*.pe
  as places to look now name sylpheed.db, Canary's ppc_context.h and the
  flat .pe; docs/re/README.md no longer says the native Canary build does not
  run.

Historical records keep their original paths: findings that were measured
against /work/xenia-rs/sylpheed.db still say so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 22:30:28 +02:00

130 lines
5.8 KiB
Python

#!/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
from disc import disc_root
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(disc_root() + '/**/GP_HANGAR_ARSENAL.pak', recursive=True)[0]
unit_ids, char_ids = set(), set()
for pk in sorted(glob.glob(disc_root() + '/**/*.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()