"""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 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=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('/work/sylph_extract/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)