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

90 lines
3.8 KiB
Python

"""Structural census of the keyframe block's angle words (+4/+8/+12), disc-wide.
`+12` is the screen-plane ROTATION in degrees (see
docs/re/structures/ui-keyframe-rotation.md). This is the disc-wide check behind
that decode, and it exists because the FIRST version of it was wrong in a way
that hid the very blocks the decode rests on.
⚠️ The blocks are **not 4-byte aligned**. A nested leaf record's `RATC` blob can
start at an odd offset (`ptloop01.rat` sits at 0xbb5966), so its keyframe blocks
inherit that alignment. An earlier scan filtered candidates on `%4 == 0`, found
0/3 of its own control blocks, and under-counted the corpus by 16 341 blocks —
every one of them inside a nested record. The CONTROL below is not decoration:
it must print 3/3 with +12 = {30} and {-45} or the numbers mean nothing.
A keyframe block is 40 bytes: fade(ARGB) | w1 w2 w3 | sx sy | tint | x y | t.
The filter keys on SHAPE, not on a count: >=2 consecutive blocks whose fade is
0x??ffffff, whose tint is 0xffffffff, and whose scale words are 1..4000.
"""
import struct, zlib, glob, os, sys, collections, re
from disc import disc_root
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 h,(zlib.decompress(st[10:]) if st[:2]==b'Z1' else st)
except Exception: continue
def blocks(d):
"""Yield offsets of the first block of each run of >=2 keyframe blocks."""
n=len(d)
U=lambda p: struct.unpack_from('>I',d,p)[0]
seen=set()
cands=sorted({m.start()-24 for m in re.finditer(b'\xff\xff\xff\xff',d)
if m.start()>=24})
for o in cands:
if o in seen or o+80>n: continue
if (U(o)&0x00ffffff)==0x00ffffff and U(o+24)==0xffffffff \
and 0<U(o+16)<=4000 and 0<U(o+20)<=4000:
k=0
while o+40*(k+1)<=n and (U(o+40*k)&0x00ffffff)==0x00ffffff \
and U(o+40*k+24)==0xffffffff \
and 0<U(o+40*k+16)<=4000 and 0<U(o+40*k+20)<=4000:
k+=1
if k>=2:
for j in range(k):
seen.add(o+40*j); yield o+40*j
S=lambda d,p: struct.unpack_from('>i',d,p)[0]
# --- control: the two known ptloop blocks must be found, with 30 / -45 ---
d4=open('/tmp/build4.bin','rb').read()
found={o for o in blocks(d4)}
for name,base,want in [("ptloop01",0xbb5966,30),("ptloop02",0xbb5a82,-45)]:
hits=[b for b in (base+0x68+40*k for k in range(3)) if b in found]
vals={S(d4,b+12) for b in hits}
print(f"CONTROL {name}: {len(hits)}/3 blocks found, +12 = {vals} (want {want})")
if not all(True for _ in [0]): sys.exit(1)
hist=collections.Counter(); nz=collections.Counter(); total=0
examples=collections.defaultdict(list)
for pak in sorted(glob.glob(disc_root() + '/dat/GP_*.pak')):
base=pak[:-4]
for h,d in entries(base):
if b'RATC' not in d[:4] and d[:4]!=b'RATC': pass
for o in blocks(d):
total+=1
for lbl,off in (("+4",4),("+8",8),("+12",12)):
v=S(d,o+off)
if v!=0:
nz[lbl]+=1
hist[(lbl,v)]+=1
if len(examples[lbl])<8: examples[lbl].append((os.path.basename(base),f"{h:08x}",hex(o),v))
print(f"\nblocks scanned disc-wide: {total}")
for lbl in ("+4","+8","+12"):
print(f" {lbl}: non-zero in {nz[lbl]:6d} ({100*nz[lbl]/max(total,1):5.2f} %)")
print("\nvalue histogram (non-zero), top 25:")
for (lbl,v),c in hist.most_common(25):
print(f" {lbl} = {v:>8} x{c}")
print("\nexamples:")
for lbl,ex in examples.items():
for e in ex[:4]: print(" ",lbl,e)