This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/tools/re-capture/unit_substructures.py
Sylpheed RE agent 21e710a531 re: the 59-of-131 arsenal question is closed -- an item names a hardpoint
An Arsenal item does not reference a Weapon record.  It references a
Turret_NNN HARDPOINT SLOT on the player craft's own unit table, and the
slot is what carries the WeaponID.  Three hops:

  Arbalest_155KG.PlayerWeapon -> Turret_050  (a slot on
  UN_f001_TCAF_DeltaSaber_T_Player) -> .WeaponID ->
  Weapon_DSaber_P_wep_50_Cannon

Controls, both in the same loop: 0/59 distinct PlayerWeapon values are a
Weapon.ID; 59/59 are a Turret_NNN slot id; the full chain lands on a
Weapon.ID 59/59.  WingmanWeapon resolves identically.  The WEAPONS
roster's 59 = 55 item names + 4 empty-slot sentinels.

Wingmen fly a cheaper gun: following the same 59 slots across craft
variants, the _Player tables give each item its own weapon record (59
distinct) while the AI tables collapse all 59 onto 10 generic classes.
That is most of the 131.

Upgrades yesterday's 'hardpoint catalogue' reading from 21 to adopted,
proved from an independent file, and corrects its '10 distinct WeaponID'
figure -- that was the AI variant, not the player's.

Also adds an __main__ guard to unit_substructures.py so importing
pak_entries from it does not run its report; its artefact is unchanged
and still byte-identical.
2026-08-27 12:49:22 +00:00

123 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""Census the SUB-RECORDS inside the 114 unit tables, and check the counts.
A unit `.tbl` is one IDXD pak entry. Besides `Generic`/`Maneuver`/`Effect` it
carries a `StructureCount` record and a variable number of destructible-part
records: `Turret_NNN`, `Bridge_NNN`, `Hatch_NNN`, `ShieldGenerator_NNN`,
`Thruster_NNN`, `Versatile_NNN`. Regenerates docs/re/data/unit-substructures.txt.
"""
import sys, os, glob, re, collections
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import unitgroup as U
# pak_entries lives in the scratch decoder; fall back to a local copy of the recipe
import struct, zlib
def pak_entries(path):
idx = open(path, 'rb').read()
n = struct.unpack_from('>I', idx, 4)[0]
parts = sorted(glob.glob(re.sub(r'\.pak$', '', path) + '.p[0-9][0-9]'))
data = b''.join(open(p, 'rb').read() for p in parts)
out = []
for i in range(n):
h, off, csz = struct.unpack_from('>III', idx, 16 + i * 12)
s = data[off:off + csz]
if s[:2] == b'Z1':
try: s = zlib.decompress(s[10:])
except Exception: continue
out.append((h, s))
return out
KIND = {'Turret': 'TurretCount', 'Bridge': 'BridgeCount', 'Hatch': 'HatchCount',
'ShieldGenerator': 'ShieldGeneratorCount', 'Thruster': 'ThrusterCount',
'Versatile': 'VersatileCount'}
def blank(n):
return not n.get('Name') and not n.get('NomalModel') and not n.get('Frame')
def units_of(pak):
out = []
for h, b in pak_entries(pak):
if b[:4] != b'IDXD': continue
try: recs = U.parse(b)
except Exception: continue
g = [r for r in recs if r['squadron'] == 'Generic']
if g and 'HP' in U.named(g[0]):
out.append((recs, U.named(g[0])))
return out
def main():
pak = glob.glob('/work/sylph_extract/**/GP_MAIN_GAME_E.pak', recursive=True)[0]
units = units_of(pak)
print("# Sub-records inside the %d unit tables of %s" % (len(units), os.path.basename(pak)))
print("# Regenerate: python3 tools/re-capture/unit_substructures.py")
print("# See docs/re/structures/unit-substructure-records.md")
norm = lambda n: re.sub(r'_\d{3}$', '_NNN', n)
fam = collections.Counter(); per = collections.Counter()
core = collections.defaultdict(collections.Counter)
for recs, gn in units:
seen = collections.Counter()
for r in recs:
n = norm(r['squadron']); fam[n] += 1; seen[n] += 1
for _, fn, _ in r['fields']:
if fn and not re.search(r'_\d{3}$', fn): core[n][fn] += 1
for n, c in seen.items(): per[n] = max(per[n], c)
print("\n## families (records, max per unit, distinct non-indexed field names)")
for n, c in fam.most_common():
print(" %-22s %5d max/unit %3d fields %3d" % (n, c, per[n], len(core[n])))
for n, _ in fam.most_common():
if n in ('Generic', 'Maneuver'): continue
print("\n## %s -- non-indexed fields (records carrying it, of %d)" % (n, fam[n]))
for k, v in sorted(core[n].items(), key=lambda x: (-x[1], x[0])):
print(" %-32s %5d" % (k, v))
res = collections.Counter(); exc = collections.defaultdict(list)
for recs, gn in units:
sc = U.named([r for r in recs if r['squadron'] == 'StructureCount'][0])
for k, f in KIND.items():
subs = [r for r in recs if re.fullmatch(k + r'_\d{3}', r['squadron'])]
w = sc.get(f, '').strip()
if not w.isdigit(): res['no field']+= 1; continue
d = int(w)
if d == len(subs): res['equal']+= 1
elif d == 0 and len(subs) == 1 and blank(U.named(subs[0])):
res['0 declared, 1 blank placeholder'] += 1
else:
res['DIFFERS'] += 1
exc[(k, d, len(subs))].append(gn.get('ID'))
print("\n## CONTROL StructureCount.<Kind>Count == #<Kind>_NNN records")
print("## (a 0 count still emits one blank slot)")
print(" %d comparisons (%d units x %d kinds)" % (len(units) * len(KIND), len(units), len(KIND)))
for k in ('equal', '0 declared, 1 blank placeholder', 'DIFFERS', 'no field'):
print(" %-34s %4d" % (k, res[k]))
print(" exceptions:")
for kk in sorted(exc, key=lambda x: -len(exc[x])):
print(" kind=%-8s declared=%-3d records=%-3d x%d" % (kk[0], kk[1], kk[2], len(exc[kk])))
for i in sorted(exc[kk]): print(" %s" % i)
wids = collections.Counter(); wnames = set()
for h, b in pak_entries(pak):
if b[:4] != b'IDXD': continue
try: recs = U.parse(b)
except Exception: continue
for r in recs:
if r['squadron'] == 'Weapon':
i = U.named(r).get('ID')
if i: wnames.add(i)
for recs, gn in units:
for r in recs:
if re.fullmatch(r'Turret_\d{3}', r['squadron']):
wids[U.named(r).get('WeaponID', '')] += 1
hit = sum(v for k, v in wids.items() if k in wnames)
print("\n## CONTROL Turret_NNN.WeaponID resolves into the Weapon datasheet")
print(" Weapon.ID values: %d" % len(wnames))
print(" turrets: %d, distinct WeaponID: %d" % (sum(wids.values()), len(wids)))
print(" resolve: %d do NOT resolve: %d" % (hit, sum(wids.values()) - hit))
print(" weapons never mounted on a turret: %d" % len(wnames - set(wids)))
if __name__ == "__main__":
main()