Files
Sylpheed/tools/re-capture/rotation_toplevel_census.py
sim e909c7c133 chore: retire the last dead paths and names from the consolidation
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>
2026-09-16 22:30:28 +02:00

94 lines
4.2 KiB
Python

#!/usr/bin/env python3
"""How many TOP-LEVEL elements carry a non-zero keyframe rotation (`+12`)?
The nested-record census (kf_rotation_census.py) scans raw bytes and so counts
rotations wherever they live. This one walks the top-level placement region the
way ui_layout.rs does, so the difference between the two is exactly the
population that lives in nested `.rat` leaf records.
An earlier attempt at this drove `sylpheed-cli screen info --geometry` in a
shell loop; it decodes every texture per build and never finished. Same answer,
seconds instead of hours.
CONTROL: GP_TITLE build 4 (the English title) must report ZERO -- its rotations
are the nested ptloop records -- while GP_DIALOG build 0 must report the two
`pceff03`/`pceff04` elements that ramp 90 -> 0.
⚠ A `screen list` BUILD index is not a pak ENTRY index. GP_TITLE happens to
map 1:1 (16 builds, entries 0..15), GP_DIALOG does not: its build 0 is **entry
2**. Indexing the pak with a build number silently reads a different bundle, and
the first version of this control did exactly that and reported 0 rotations for a
screen that plainly has two. `screen list` prints the mapping.
"""
import struct, zlib, glob, os, sys, collections
from disc import disc_root
DECL_AT, DECL_ENTRY, KEYFRAME = 0x20, 60, 40
def entries(base):
stub = open(base + ".pak", "rb").read()
if stub[:4] != b"IPFB": return
n = struct.unpack_from(">I", stub, 4)[0]
segs = sorted(glob.glob(base + ".p[0-9][0-9]"))
if not segs: return
blob = b"".join(open(s, "rb").read() for s in segs)
for i in range(n):
h, off, sz = struct.unpack_from(">III", stub, 0x10 + 12 * i)
st = blob[off:off + sz]
if len(st) < 10: continue
try: yield i, (zlib.decompress(st[10:]) if st[:2] == b"Z1" else st)
except Exception: continue
def rotated(d):
"""[(element index, name, [rotations])] for top-level elements with any r != 0."""
if d[:4] != b"RATC": return []
count = struct.unpack_from(">I", d, 0x14)[0]
if not (0 < count < 4096): return []
names = []
for i in range(count):
o = DECL_AT + i * DECL_ENTRY
if o + DECL_ENTRY > len(d): return []
names.append(d[o:o+28].split(b"\0")[0].decode("ascii", "replace"))
out, pos, total = [], DECL_AT + count * DECL_ENTRY, 0
while True:
if pos + 8 > len(d): break
idx, frames = struct.unpack_from(">II", d, pos)
if idx >= count or frames == 0 or frames > 4096: break
first = pos + 12; end = first + frames * KEYFRAME - 4
rots = []
for k in range(frames):
blk = first + k * KEYFRAME
if blk + 36 > len(d) or blk + 36 > end: break
rots.append(struct.unpack_from(">i", d, blk + 12)[0])
total += 1
if any(r != 0 for r in rots): out.append((idx, names[idx], rots))
pos = end
return out, total
# ---- CONTROL -------------------------------------------------------------
t = dict(entries(disc_root() + "/dat/GP_TITLE"))
g = dict(entries(disc_root() + "/dat/GP_DIALOG"))
ct, _ = rotated(t[4]); cg, _ = rotated(g[2]) # build 0 == entry 2, see the note above
print(f"CONTROL GP_TITLE build 4 (nested rotations only): {len(ct)} top-level rotated (want 0)")
print(f"CONTROL GP_DIALOG build 0: {len(cg)} top-level rotated (want 2)")
for i, n, r in cg: print(f" [{i}] {n} r = {r}")
assert len(ct) == 0 and len(cg) == 2, "control failed"
# ---- census --------------------------------------------------------------
tot = rot = 0
per_pak = collections.Counter(); names = collections.Counter()
for pak in sorted(glob.glob(disc_root() + "/dat/GP_*.pak")):
for i, d in entries(pak[:-4]):
r = rotated(d)
if not r: continue
hits, n = r
tot += n; rot += len(hits)
if hits:
per_pak[os.path.basename(pak)] += len(hits)
for _, nm, _rr in hits: names[nm] += 1
print(f"\ntop-level elements with a keyframe group, disc-wide: {tot}")
print(f" carrying a non-zero rotation: {rot} ({100*rot/max(tot,1):.2f} %)")
print("\nby archive:")
for k, c in per_pak.most_common(): print(f" {c:5d} {k}")
print("\nmost common rotated element names:")
for k, c in names.most_common(10): print(f" {c:5d} {k}")