Two threads had converged on needing one capture this container cannot take, so this iteration records that and finishes something reachable. BLOCKED, written into MISSION.md rather than worked around: Q1's keyframe time association and the rest() rule for plateau-less elements both now hinge on a running capture of GP_TITLE build 7, the Japanese title. The console language is not settable here -- user_language appears only as DECLARE_int32 at four call sites with no DEFINE anywhere in the tree, and it is absent from the registered cvars in xenia-canary.config.toml. There is no flag to pass, and guessing one is specifically unsafe: run-canary's own header records that xenia calls ShowSimpleMessageBox from ParseLaunchArguments before logging starts, so a bad flag blocks forever with an empty log. Rebuilding canary to add the cvar would be improvising around the blocker; it needs a human decision. Neither question blocks the five menu screens. FINISHED: the disc-wide top-level rotation count, left running four iterations ago as a shell loop over `screen info --geometry` that never completed (it decodes every texture per build). Walking the placement region directly takes seconds. top-level elements with a keyframe group 15 493 carrying a non-zero rotation 2 152 (13.89 %) Both controls pass: GP_TITLE build 4 reports 0 (its rotations are the nested ptloop records) and GP_DIALOG build 0 reports the expected two. The control earned its place -- the first version indexed the pak with a `screen list` BUILD number and got 0 for a screen that has two, because GP_DIALOG build 0 is entry 2. GP_TITLE maps 1:1, which is how the assumption survived. METHOD line added. Two free corroborations of the rotation decode. The rotated population is dominated by tactical-map ship icons -- pbb_destroyer 444, pbr_destroyer 402, pbr_fighter 276 -- i.e. markers rotated to heading, the single largest use of the field on the disc. And GP_TITLE entry 7's Japanese wordmark pieces settle from ALTERNATING tilts: ptlogo3a r = 0, -14, -4, -1, 0, ... ptlogo3b r = 0, +14, +4, +1, 0, ... ptlogo3c r = 0, -14, -4, -1, 0, ... Same magnitudes, opposite signs, all decaying to upright. A misread field does not produce that.
93 lines
4.2 KiB
Python
93 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
"""How many TOP-LEVEL elements carry a non-zero keyframe rotation (`+12`)?
|
|
|
|
The nested-record census (kf_rotation_census.py) scans raw bytes and so counts
|
|
rotations wherever they live. This one walks the top-level placement region the
|
|
way ui_layout.rs does, so the difference between the two is exactly the
|
|
population that lives in nested `.rat` leaf records.
|
|
|
|
An earlier attempt at this drove `sylpheed-cli screen info --geometry` in a
|
|
shell loop; it decodes every texture per build and never finished. Same answer,
|
|
seconds instead of hours.
|
|
|
|
CONTROL: GP_TITLE build 4 (the English title) must report ZERO -- its rotations
|
|
are the nested ptloop records -- while GP_DIALOG build 0 must report the two
|
|
`pceff03`/`pceff04` elements that ramp 90 -> 0.
|
|
|
|
⚠ A `screen list` BUILD index is not a pak ENTRY index. GP_TITLE happens to
|
|
map 1:1 (16 builds, entries 0..15), GP_DIALOG does not: its build 0 is **entry
|
|
2**. Indexing the pak with a build number silently reads a different bundle, and
|
|
the first version of this control did exactly that and reported 0 rotations for a
|
|
screen that plainly has two. `screen list` prints the mapping.
|
|
"""
|
|
import struct, zlib, glob, os, sys, 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 i, (zlib.decompress(st[10:]) if st[:2] == b"Z1" else st)
|
|
except Exception: continue
|
|
|
|
def rotated(d):
|
|
"""[(element index, name, [rotations])] for top-level elements with any r != 0."""
|
|
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, total = [], DECL_AT + count * DECL_ENTRY, 0
|
|
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
|
|
rots = []
|
|
for k in range(frames):
|
|
blk = first + k * KEYFRAME
|
|
if blk + 36 > len(d) or blk + 36 > end: break
|
|
rots.append(struct.unpack_from(">i", d, blk + 12)[0])
|
|
total += 1
|
|
if any(r != 0 for r in rots): out.append((idx, names[idx], rots))
|
|
pos = end
|
|
return out, total
|
|
|
|
# ---- CONTROL -------------------------------------------------------------
|
|
t = dict(entries("/work/sylph_extract/dat/GP_TITLE"))
|
|
g = dict(entries("/work/sylph_extract/dat/GP_DIALOG"))
|
|
ct, _ = rotated(t[4]); cg, _ = rotated(g[2]) # build 0 == entry 2, see the note above
|
|
print(f"CONTROL GP_TITLE build 4 (nested rotations only): {len(ct)} top-level rotated (want 0)")
|
|
print(f"CONTROL GP_DIALOG build 0: {len(cg)} top-level rotated (want 2)")
|
|
for i, n, r in cg: print(f" [{i}] {n} r = {r}")
|
|
assert len(ct) == 0 and len(cg) == 2, "control failed"
|
|
|
|
# ---- census --------------------------------------------------------------
|
|
tot = rot = 0
|
|
per_pak = collections.Counter(); names = collections.Counter()
|
|
for pak in sorted(glob.glob("/work/sylph_extract/dat/GP_*.pak")):
|
|
for i, d in entries(pak[:-4]):
|
|
r = rotated(d)
|
|
if not r: continue
|
|
hits, n = r
|
|
tot += n; rot += len(hits)
|
|
if hits:
|
|
per_pak[os.path.basename(pak)] += len(hits)
|
|
for _, nm, _rr in hits: names[nm] += 1
|
|
print(f"\ntop-level elements with a keyframe group, disc-wide: {tot}")
|
|
print(f" carrying a non-zero rotation: {rot} ({100*rot/max(tot,1):.2f} %)")
|
|
print("\nby archive:")
|
|
for k, c in per_pak.most_common(): print(f" {c:5d} {k}")
|
|
print("\nmost common rotated element names:")
|
|
for k, c in names.most_common(10): print(f" {c:5d} {k}")
|