72 records, 94 distinct field names, 31 in every one (post-processing core, five Fog*, three ScreenColor*, SpaceSize, five Supply*, UnderCommandSquadron, the first BGOperate slot). Optional families sit in clean tiers: Nebura_* and ColorLayer* in 69/72, DOF_*/UnsharpMask_*/ExposureKey_* in 15/72, FinalPass* in 3/72. REFUTED: BGOperateFrameCount is NOT the number of BGOperateFrameName_i slots -- 36 of 72. What holds is Count <= slots, 72/72: a fixed slot array with a live count, the same shape as MessageCount under the 32-slot clamp. 68 of the fields common to all three phases NEVER differ in any of the 24 objects. The phase block is a per-stage environment block copied three times; what a phase change is actually for is the backdrop animation (BGOperate*) and the supply/command squadron assignment. MapPath is not in this family -- its 87 records per pack are 29 x 3, the resource manifest's Phase_N. Also fixed a non-determinism the verify loop caught: most_common() over a set iteration ordered ties differently per run; now sorted by (-count, name). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
179 lines
6.9 KiB
Python
179 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Regenerate docs/re/data/mission-scoring.txt.
|
|
|
|
The per-stage mission scoring / rank-threshold tables: 24 IDXD objects per
|
|
language pack, each holding Score_Easy / Score_Normal / Score_Hard with one
|
|
22-field schema. Loader: sub_8230D1F8.
|
|
|
|
Run: python3 mission_scoring.py > ../../docs/re/data/mission-scoring.txt
|
|
"""
|
|
import sys, os, re, glob, collections
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from unit_substructures import pak_entries
|
|
import unitgroup as U
|
|
|
|
DAT = '/work/sylph_extract/dat'
|
|
KEY = 'RankScore_S'
|
|
DIFFS = ('Score_Easy', 'Score_Normal', 'Score_Hard')
|
|
|
|
|
|
def main():
|
|
percopy = {}
|
|
for p in sorted(glob.glob(os.path.join(DAT, '*.pak'))):
|
|
n = sum(1 for _, s in pak_entries(p)
|
|
if s[:4] == b'IDXD' and KEY.encode() in s)
|
|
if n:
|
|
percopy[os.path.basename(p)] = n
|
|
|
|
P = os.path.join(DAT, 'GP_MAIN_GAME_E.pak')
|
|
tables = []
|
|
for h, s in pak_entries(P):
|
|
if s[:4] != b'IDXD' or KEY.encode() not in s:
|
|
continue
|
|
try:
|
|
recs = U.parse(s)
|
|
except Exception:
|
|
continue
|
|
tables.append((h, {r['squadron']: U.named(r) for r in recs
|
|
if r['squadron'] in DIFFS}))
|
|
tables.sort()
|
|
|
|
print('# Mission scoring / rank thresholds (regenerated by mission_scoring.py)')
|
|
print()
|
|
print('IDXD objects carrying %s, per pack: %s' % (KEY, percopy))
|
|
print('total across the disc : %d' % sum(percopy.values()))
|
|
print('difficulty records per object : %s' % sorted(
|
|
{tuple(sorted(t)) for _, t in tables}))
|
|
|
|
schemas = collections.Counter()
|
|
for _, t in tables:
|
|
for d in DIFFS:
|
|
schemas[tuple(sorted(t[d]))] += 1
|
|
print('distinct field sets : %d' % len(schemas))
|
|
fields = sorted(schemas.most_common(1)[0][0])
|
|
print('records : %d (%d fields each)' % (
|
|
sum(schemas.values()), len(fields)))
|
|
|
|
# how much does each field actually vary?
|
|
print()
|
|
print('value census per field (over %d records):' % sum(schemas.values()))
|
|
for f in fields:
|
|
c = collections.Counter(t[d][f] for _, t in tables for d in DIFFS)
|
|
vals = ' '.join('%s=%d' % (k, v) for k, v in sorted(
|
|
c.items(), key=lambda kv: (-kv[1], kv[0])))
|
|
print(' %-32s %2d distinct %s' % (f, len(c), vals[:110]))
|
|
|
|
# does difficulty move anything?
|
|
print()
|
|
print('fields that differ between the three difficulty records:')
|
|
moved = collections.Counter()
|
|
for _, t in tables:
|
|
for f in fields:
|
|
if len({t[d][f] for d in DIFFS}) > 1:
|
|
moved[f] += 1
|
|
print(' %s' % (dict(moved) if moved else 'NONE — Easy/Normal/Hard are identical everywhere'))
|
|
|
|
# per-object variation
|
|
print()
|
|
print('objects whose Normal record differs from the commonest Normal record:')
|
|
base = collections.Counter(tuple(sorted(t['Score_Normal'].items()))
|
|
for _, t in tables).most_common(1)[0][0]
|
|
odd = [(h, t) for h, t in tables
|
|
if tuple(sorted(t['Score_Normal'].items())) != base]
|
|
print(' %d of %d objects' % (len(odd), len(tables)))
|
|
for h, t in odd:
|
|
diff = {f: t['Score_Normal'][f] for f in fields
|
|
if t['Score_Normal'][f] != dict(base)[f]}
|
|
print(' %#010x %s' % (h, diff))
|
|
|
|
# the rest of the settings object: Camera / Player / SplinterCell / Difficulty_*
|
|
OTHER = ('Camera', 'Player', 'SplinterCell',
|
|
'Difficulty_Easy', 'Difficulty_Normal', 'Difficulty_Hard')
|
|
full = []
|
|
for h, s2 in pak_entries(P):
|
|
if s2[:4] != b'IDXD' or KEY.encode() not in s2:
|
|
continue
|
|
try:
|
|
full.append((h, {r['squadron']: r for r in U.parse(s2)}))
|
|
except Exception:
|
|
pass
|
|
full.sort()
|
|
print()
|
|
print('the rest of the settings object (%d objects):' % len(full))
|
|
shapes = collections.Counter(tuple(sorted(d)) for _, d in full)
|
|
print(' record-name sets: %d' % len(shapes))
|
|
for t, c in shapes.most_common():
|
|
print(' x%-3d %s' % (c, list(t)))
|
|
for rn in OTHER:
|
|
vals = collections.defaultdict(collections.Counter)
|
|
n = 0
|
|
for _, d in full:
|
|
if rn not in d:
|
|
continue
|
|
n += 1
|
|
for k, v in U.named(d[rn]).items():
|
|
vals[k][v] += 1
|
|
print()
|
|
print(' %s — present in %d/%d objects, %d fields' % (rn, n, len(full), len(vals)))
|
|
for k in sorted(vals):
|
|
c = vals[k]
|
|
print(' %-36s %2d distinct %s' % (
|
|
k, len(c), ' '.join('%s=%d' % (a, b) for a, b in c.most_common(5))[:70]))
|
|
|
|
# the Phase_1/2/3 block of the settings family
|
|
prec = [(h, i, U.named(d['Phase_%d' % i])) for h, d in full for i in (1, 2, 3)
|
|
if 'Phase_%d' % i in d]
|
|
allf = collections.Counter()
|
|
for _, _, n in prec:
|
|
for f in n:
|
|
allf[f] += 1
|
|
core = sorted(f for f, c in allf.items() if c == len(prec))
|
|
print()
|
|
print('the Phase_1/2/3 block (%d records over %d objects):' % (len(prec), len(full)))
|
|
print(' distinct field names : %d' % len(allf))
|
|
print(' present in EVERY record : %d' % len(core))
|
|
print(' %s' % ' '.join(core))
|
|
print(' field-count histogram : %s' % dict(sorted(
|
|
collections.Counter(len(n) for _, _, n in prec).items())))
|
|
|
|
le = 0
|
|
slots = collections.Counter()
|
|
cnts = collections.Counter()
|
|
for _, _, n in prec:
|
|
c = int(n['BGOperateFrameCount'])
|
|
k = sum(1 for f in n if re.fullmatch(r'BGOperateFrameName_\d+', f))
|
|
slots[k] += 1
|
|
cnts[c] += 1
|
|
le += (c <= k)
|
|
eq = sum(1 for _, _, n in prec
|
|
if int(n['BGOperateFrameCount'])
|
|
== sum(1 for f in n if re.fullmatch(r'BGOperateFrameName_\d+', f)))
|
|
print(' BGOperateFrameCount == slots: %d/%d (REFUTED)' % (eq, len(prec)))
|
|
print(' BGOperateFrameCount <= slots: %d/%d' % (le, len(prec)))
|
|
print(' slot histogram %s ; Count histogram %s' % (
|
|
dict(sorted(slots.items())), dict(sorted(cnts.items()))))
|
|
|
|
moved = collections.Counter()
|
|
same = collections.Counter()
|
|
for h, d in full:
|
|
ns = [U.named(d['Phase_%d' % i]) for i in (1, 2, 3) if 'Phase_%d' % i in d]
|
|
if len(ns) != 3:
|
|
continue
|
|
for f in set(ns[0]) & set(ns[1]) & set(ns[2]):
|
|
(moved if len({n[f] for n in ns}) > 1 else same)[f] += 1
|
|
never = sorted(f for f in same if f not in moved)
|
|
print(' fields that DIFFER between phases (of %d objects):' % len(full))
|
|
for f, c in sorted(moved.items(), key=lambda kv: (-kv[1], kv[0])):
|
|
print(' %-28s %2d' % (f, c))
|
|
print(' fields that NEVER differ between phases: %d' % len(never))
|
|
print(' %s' % ' '.join(never))
|
|
|
|
print()
|
|
print('the commonest Normal record:')
|
|
for k, v in sorted(dict(base).items()):
|
|
print(' %-32s %s' % (k, v))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|