diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 998a4a6..832d75d 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -429,6 +429,13 @@ authored version can be deleted. **Three independent observables — animation timing, static composites and the per-frame draw log — all support: plateau where there is one, last keyframe where there is not.** + 📊 **Structurally, `last` is defensible for 2 293 of the 2 305 (99.5 %)**: 1 618 + end invisible (a transient, gone at rest), 675 end at maximum alpha (faded in + and stopped — the final pose *is* settled), and only **12** end visible below + full alpha, which are genuinely unclear. The dwell rule returns a mid-movement + frame by construction. ⚠️ The 1 618 rest on an assumption worth seeing: that + such an element's animation has finished by the time the screen settles — shown + by the draw log for the two splash glows, unshown for the rest. 📊 The disc-wide blast radius, for whoever decides: the rules **differ on 82.3 %** of those 2 305, so "either is fine" is not available — and the current rule returns a **zero-scale** (collapsed, pre-roll) pose for **195** of them against diff --git a/docs/re/data/plateauless-endstate.txt b/docs/re/data/plateauless-endstate.txt new file mode 100644 index 0000000..fa9ef52 --- /dev/null +++ b/docs/re/data/plateauless-endstate.txt @@ -0,0 +1,16 @@ +# tools/re-capture/plateauless_endstate.py over dat/GP_*.pak -- 2026-08-29 + +plateau-less, multi-keyframe, ending VISIBLE: 687 + alpha never decreases (a fade/slide IN that stops): 472 (68.7 %) + ends at its maximum alpha but dips on the way : 203 (29.5 %) + ends visible but NOT at maximum alpha : 12 (1.7 %) + +examples of the monotone kind (alpha sequence, then scale pairs): + GP_BUNK.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)] + GP_BUNK.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)] + GP_DEBRIEFING_PILOTLOG.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)] + GP_DEBRIEFING_PILOTLOG.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)] + GP_DEBRIEFING_PILOTLOG.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)] + GP_DEBRIEFING_PILOTLOG.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)] + GP_MAIN_GAME_D2D.pak pghud_speed_cut.t32 [0, 255] [(100, 100), (100, 100)] + GP_MAIN_GAME_D2D.pak pgmsg_restart_sub.rat [255, 255] [(106, 106), (100, 100)] diff --git a/docs/re/structures/ui-resting-pose.md b/docs/re/structures/ui-resting-pose.md index e4147ea..eecfaa3 100644 --- a/docs/re/structures/ui-resting-pose.md +++ b/docs/re/structures/ui-resting-pose.md @@ -574,3 +574,32 @@ produces, and why it scored +0.9982 and +0.9758. ✅ So the picture is coherent across three independent observables — animation timing, static composites, and the per-frame draw log — and the rule it supports is: **plateau where there is one, last keyframe where there is not.** + +### ✅ The structural case for `last`, disc-wide — 2 293 of 2 305 + +Only two ambiguous elements could be *scored* against a capture. But the +entry → hold → exit model makes a prediction that can be checked on all 2 305: +what does each element's **final** keyframe look like? +([`tools/re-capture/plateauless_endstate.py`](../../../tools/re-capture/plateauless_endstate.py), +[data](../data/plateauless-endstate.txt)) + +| final keyframe | count | is `last` the right answer? | +|---|---|---| +| **invisible** (`a = 0`) | **1 618** | the element is a transient; at any settled moment it is *gone* — ✅ model-consistent, and confirmed by the draw log for the two observed | +| **visible, at maximum alpha** | **675** | it faded in and stopped — the final pose *is* the settled one ✅ | +| visible, **below** maximum alpha | **12** | ❔ genuinely unclear | + +Of the 687 that end visible, **472 have monotonically non-decreasing alpha** — a +plain fade-in that stops, `[0, 255]` over two keyframes in the commonest case +(`pjex_eff.rat`, `pghud_speed_cut.t32`) — and another 203 end at their maximum +after dipping. Only **12 elements in 2 305** end visible at less than full alpha. + +So `last` is **structurally defensible for 2 293 of 2 305 (99.5 %)**, against a +dwell rule that returns a mid-movement frame by construction. It is *observed* +correct for 2, *structural* for 675, *model-consistent* for 1 618, and unclear +for 12. + +⚠️ The 1 618 carry the weight of an assumption, and it should be visible: that a +plateau-less element's animation has finished by the time the screen is settled. +The draw log establishes this for the two splash glows (drawn on frames 94–115, +with the logos on 116–211) and nothing establishes it for the rest. diff --git a/tools/re-capture/plateauless_endstate.py b/tools/re-capture/plateauless_endstate.py new file mode 100755 index 0000000..80b819c --- /dev/null +++ b/tools/re-capture/plateauless_endstate.py @@ -0,0 +1,62 @@ +"""Plateau-less elements whose LAST keyframe is visible -- do they contradict +the entry->hold->exit model, or are they 'slides in and stops'?""" +import struct, zlib, glob, os, collections +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 (zlib.decompress(st[10:]) if st[:2]==b"Z1" else st) + except Exception: continue +def elements(d): + if d[:4]!=b"RATC": return [] + count=struct.unpack_from(">I",d,0x14)[0] + if not (0len(d): return [] + names.append(d[o:o+28].split(b"\0")[0].decode("ascii","replace")) + out,pos=[],DECL_AT+count*DECL_ENTRY + 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 + poses=[] + for k in range(frames): + b=first+k*KEYFRAME + if b+36>len(d) or b+36>end: break + poses.append(struct.unpack_from(">I",d,b)+struct.unpack_from(">II",d,b+16)+struct.unpack_from(">ii",d,b+28)) + if poses: out.append((names[idx],poses)) + pos=end + return out +alpha=lambda q:(q[0]>>24)&0xff +mono=stuck=other=0 +ex=[] +for pak in sorted(glob.glob("/work/sylph_extract/dat/GP_*.pak")): + for d in entries(pak[:-4]): + for nm,p in elements(d): + if len(p)==1: continue + if any(p[i]==p[i+1] for i in range(len(p)-1)): continue + if alpha(p[-1])==0: continue # ends invisible: transient + a=[alpha(q) for q in p] + if all(a[i]<=a[i+1] for i in range(len(a)-1)): # alpha never decreases + mono+=1 + if len(ex)<8: ex.append((os.path.basename(pak), nm, a, [(q[1],q[2]) for q in p])) + elif a[-1]==max(a): stuck+=1 + else: other+=1 +tot=mono+stuck+other +print(f"plateau-less, multi-keyframe, ending VISIBLE: {tot}") +print(f" alpha never decreases (a fade/slide IN that stops): {mono} ({100*mono/max(tot,1):.1f} %)") +print(f" ends at its maximum alpha but dips on the way : {stuck} ({100*stuck/max(tot,1):.1f} %)") +print(f" ends visible but NOT at maximum alpha : {other} ({100*other/max(tot,1):.1f} %)") +print("\nexamples of the monotone kind (alpha sequence, then scale pairs):") +for e in ex: print(" ", e[0], e[1], e[2], e[3])