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>
90 lines
3.8 KiB
Python
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)
|