Files
Sylpheed/tools/re-capture/plateau_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

140 lines
6.2 KiB
Python

"""Disc-wide: which elements reach rest()'s dwell fallback, i.e. have no plateau?
Reimplements the placement-region walk from ui_layout.rs::parse_placements so the
census runs in seconds instead of via `screen info --geometry` (which decodes
every texture). CONTROL: it must reproduce the three known fallback elements of
GP_TITLE build 7 (indices 8, 13, 14) and find ptlogo_eff3 among them.
"""
import struct, zlib, glob, os, 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, h, (zlib.decompress(st[10:]) if st[:2] == b"Z1" else st)
except Exception: continue
def elements(d):
"""(index, name, [poses]) per element with a keyframe group."""
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 = [], DECL_AT + count * DECL_ENTRY
for _ in range(count):
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
poses = []
for k in range(frames):
blk = first + k * KEYFRAME
if blk + 36 > len(d) or blk + 36 > end: break
poses.append(struct.unpack_from(">IiiIIi i", d, blk)[:1] +
struct.unpack_from(">II", d, blk + 16) +
struct.unpack_from(">ii", d, blk + 28))
if poses: out.append((idx, names[idx], poses))
pos = end
return out
def has_plateau(poses):
return any(poses[i] == poses[i+1] for i in range(len(poses) - 1))
# ---- CONTROL -------------------------------------------------------------
ctl = {i: (n, p) for i, n, p in elements(
dict((i, d) for i, h, d in entries(disc_root() + "/dat/GP_TITLE"))[7])}
fb = sorted(i for i, (n, p) in ctl.items() if not has_plateau(p))
print(f"CONTROL GP_TITLE build 7: fallback elements {fb} (want [8, 13, 14])")
print(f" index 8 is {ctl[8][0]!r} (want ptlogo_eff3.t32)")
assert fb == [8, 13, 14] and ctl[8][0].startswith("ptlogo_eff3"), "control failed"
# ---- census --------------------------------------------------------------
tot = fb_n = 0
by_name = collections.Counter()
worst = []
for pak in sorted(glob.glob(disc_root() + "/dat/GP_*.pak")):
base = pak[:-4]
for i, h, d in entries(base):
for idx, name, poses in elements(d):
tot += 1
if not has_plateau(poses):
fb_n += 1
by_name[name] += 1
worst.append((os.path.basename(base), i, idx, name, len(poses)))
print(f"\nelements with a keyframe group, disc-wide: {tot}")
print(f" no plateau -> rest pose is GUESSED: {fb_n} ({100*fb_n/max(tot,1):.2f} %)")
print(f"\nmost common guessed elements:")
for n, c in by_name.most_common(12): print(f" {c:5d} {n}")
# ---- candidate rules for a plateau-less element --------------------------
# pose tuple = (fade, scale_x, scale_y, x, y); times read separately.
def dwell_pick(times, poses):
best = (0, -1)
for k in range(len(times) - 1):
if times[k] is None or times[k + 1] is None: continue
d = times[k + 1] - times[k]
if d >= best[1]: best = (k, d)
return best[0]
def elements_t(d):
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 = [], DECL_AT + count * DECL_ENTRY
for _ in range(count):
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
poses, times = [], []
for k in range(frames):
blk = first + k * KEYFRAME
if blk + 36 > len(d) or blk + 36 > end: break
poses.append(struct.unpack_from(">I", d, blk) +
struct.unpack_from(">II", d, blk + 16) +
struct.unpack_from(">ii", d, blk + 28))
times.append(struct.unpack_from(">i", d, blk + 36)[0] if blk + 40 <= end else None)
if poses: out.append((idx, names[idx], times, poses))
pos = end
return out
alpha = lambda p: (p[0] >> 24) & 0xff
stats = collections.Counter()
for pak in sorted(glob.glob(disc_root() + "/dat/GP_*.pak")):
for i, h, d in entries(pak[:-4]):
for idx, name, times, poses in elements_t(d):
if has_plateau(poses): continue
cur = poses[dwell_pick(times, poses)]
last = poses[-1]
stats["n"] += 1
stats["cur_invisible"] += alpha(cur) == 0
stats["last_invisible"] += alpha(last) == 0
stats["cur_zero_scale"] += (cur[1] == 0 or cur[2] == 0)
stats["agree"] += cur == last
n = stats["n"]
print(f"\n--- the {n} plateau-less elements, by what each candidate rule returns")
print(f" current rule (longest dwell) returns an INVISIBLE pose : {stats['cur_invisible']:5d} ({100*stats['cur_invisible']/n:5.1f} %)")
print(f" current rule returns a ZERO-SCALE (degenerate) pose : {stats['cur_zero_scale']:5d} ({100*stats['cur_zero_scale']/n:5.1f} %)")
print(f" 'rest = last keyframe' returns an INVISIBLE pose : {stats['last_invisible']:5d} ({100*stats['last_invisible']/n:5.1f} %)")
print(f" the two rules agree : {stats['agree']:5d} ({100*stats['agree']/n:5.1f} %)")