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>
124 lines
5.4 KiB
Python
124 lines
5.4 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
|
|
from disc import disc_root
|
|
|
|
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(disc_root() + '/**/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()
|