15 loadout records, one per flight position x pilot (Bird1-Sandra .. Rhino4-Yoji), each with Arm1/Arm2/Arm3/Nose + UnitID. The same trap as PlayerWeapon, one level up: Arm1/Arm2/Arm3/Nose do NOT name items. They name a per-slot ALLOW-LIST record -- one of 24 whose only named field is Type (the slot kind) -- and the candidate items are that record's positional, unnamed fields, in order. Four hops: Rhino4-Yoji.Arm1 -> STANDARD_ARM1 -> [Falcon_9AM, Condor_105AM, ...] -> item.PlayerWeapon = Turret_NNN -> slot.WeaponID -> Weapon.ID Controls: Arm1/2/3/Nose -> allow-list record 60/60; allow-list positional entries -> arsenal item 70/88, and every one of the 18 misses is the single sentinel No_Equipment -- one of the four WEAPONS-roster values with no item record, i.e. the empty-slot marker. UnitID is two ID spaces at once: 5 rows name a unit Generic.ID (the three -Katana rows are the player -- a _Player craft plus an extra, empty PlayerUnit field), 8 name a character, resolving as Character + the value into the 64-record character table. Two values resolve to nothing, both single rows against 13 that do: Rhino2-Ellen.UnitID = UN_f001_TCAF_DeltaSaber_W exists nowhere (checked as a Generic.ID across every pak and as a record name, 0 hits) while UN_f002_TCAF_DeltaSaber_W does -- consistent with a shipped typo, reported not diagnosed -- and Rhino4-Brandon.UnitID = BRANDON has no CharacterBRANDON among the 64. New structure doc, artefact and regenerator; the other four artefacts regenerate byte-identical.
101 lines
4.2 KiB
Python
101 lines
4.2 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
|
|
|
|
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)
|
|
|
|
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## counts")
|
|
print(" arsenal items %3d loadout records %3d per-slot allow-lists %3d"
|
|
% (len(items), len(loadouts), len(allow)))
|
|
|
|
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()
|