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/mission_scoring.py
Claude (auto) 26b7728ceb re: the rest of the stage-settings object -- cameras, player limits, difficulty
53 objects in GP_MAIN_GAME_E carry a Phase_1 record, in TWO families: 29 are
the resource manifest the corpus already owns (Phase_N = 4 fields) and 24 are
the settings table (Phase_N = 31-90 fields).  That answers the 24-vs-28 puzzle
left open by the scoring entry -- the 29 is a different table, not the settings.

Camera: three chase rigs in metres, 13 of 14 fields identical in every stage --
Nose (0, 4.5, 7), Near (0, 10, 40), Far (0, 15, 80), FOV 0.92; only CameraFar
varies, once.  Player: BulletLimit 512 / HomingLimit 256 / LaserLimit 32 and
the three 0.30 axis adjustments are constant, GravityFactor is non-zero in 4 of
24 stages, and IsBoss16Enable appears in exactly ONE object -- the first
per-stage handle for a family whose filenames do not resolve.

Difficulty_Easy/Normal/Hard is a SECOND difficulty record (8 damage and
guidance multipliers), separate from Score_*.

New doc structures/stage-settings-table.md; mission_scoring.py extended.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-27 18:35:05 +00:00

131 lines
4.8 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, 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]))
print()
print('the commonest Normal record:')
for k, v in sorted(dict(base).items()):
print(' %-32s %s' % (k, v))
if __name__ == '__main__':
main()