re(ui): finish the top-level rotation census; record the JP-capture blocker
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.
This commit is contained in:
@@ -77,6 +77,37 @@ alongside it.
|
||||
| **Q10** | **What are a music bank's sub-waves?** `BGM_001.slb` is three sub-waves — 10 KB, 4.47 MB, 4.67 MB — and we currently **concatenate them blindly** into one 347 s track. Two near-equal halves could be intro + loop, or two variations, or two halves of one piece. A menu that loops its music needs to know which | The role of each sub-wave, established for at least the menu BGM. "Concatenate" is a decision, not a default — right now it is a default nobody chose |
|
||||
| **S1** | ~~**Ready Room probe.**~~ **DONE 2026-08-28 — [no-go](../re/ready-room-probe.md).** It is 2D and enumerates fine, but the pak is briefing/tactical-map content, not the Ready Room menu | ✅ go/no-go written |
|
||||
|
||||
## 🔴 Blocked in this container — a Japanese-locale capture
|
||||
|
||||
Recorded rather than worked around, per "do not improvise around a blocker".
|
||||
|
||||
Two questions have converged on needing **one capture we cannot take**: a running
|
||||
capture of `GP_TITLE` **build 7**, the Japanese title screen.
|
||||
|
||||
* Q1's keyframe-time association. The shifted reading is favoured 26× by a
|
||||
calibration-free measurement, and the only render it changes on the whole disc
|
||||
is build 7 ([`ui-keyframe-time-unit.md`](../re/ui-keyframe-time-unit.md)).
|
||||
* What `rest()` should return for a plateau-less element. The one element that
|
||||
discriminates, `ptlogo_eff3.t32`, is also in build 7
|
||||
([`structures/ui-resting-pose.md`](../re/structures/ui-resting-pose.md)).
|
||||
|
||||
**Why the container cannot do it:** the console language is not settable. In this
|
||||
canary tree `user_language` appears only as `DECLARE_int32` — four call sites,
|
||||
no `DEFINE` — and it is absent from the registered cvars in
|
||||
`xenia-canary.config.toml`. There is no flag to pass, and passing an unknown one
|
||||
is specifically dangerous here: `run-canary`'s own header records that xenia
|
||||
calls `ShowSimpleMessageBox` from `ParseLaunchArguments` *before* logging starts,
|
||||
so a bad flag blocks forever on an SDL dialog with an empty log.
|
||||
|
||||
Rebuilding canary to add the cvar would be improvising around this, so it has not
|
||||
been done. **It needs a human decision**: either add the cvar and rebuild, or
|
||||
accept that both questions stay 🟡 and the port authors those values by hand.
|
||||
|
||||
⚠️ Neither question blocks the five menu screens. Q1's *interpolation law* is
|
||||
settled and only multi-keyframe absolute timing is open; `rest()` differs from
|
||||
its alternative on **one** element across all five screens, and the current
|
||||
answer there is the defensible one.
|
||||
|
||||
## Known unknowns — say so, do not fill them in
|
||||
|
||||
Some of these may turn out to be undecodable. That is a valid, useful answer, and
|
||||
|
||||
@@ -419,3 +419,10 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the
|
||||
diff was then all zeros, because `cmp` had read a half-written PNG. Compare
|
||||
only after the job signals done, or skip the newest file. A "difference" that
|
||||
vanishes on re-examination is usually a race, not a flake.
|
||||
* **A `screen list` BUILD index is not a pak ENTRY index.** `GP_TITLE` happens to
|
||||
map 1:1 (16 builds, entries 0–15), which is how the assumption survived;
|
||||
`GP_DIALOG` does not — its build 0 is **entry 2**. Indexing a pak directly with
|
||||
a build number silently reads a different bundle and reports a confident wrong
|
||||
answer: a census control asked for the two rotated elements of `GP_DIALOG`
|
||||
build 0 and got zero, from a bundle that genuinely has none. `screen list`
|
||||
prints the mapping; use it rather than assuming identity.
|
||||
|
||||
32
docs/re/data/rotation-toplevel-census.txt
Normal file
32
docs/re/data/rotation-toplevel-census.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
CONTROL GP_TITLE build 4 (nested rotations only): 0 top-level rotated (want 0)
|
||||
CONTROL GP_DIALOG build 0: 2 top-level rotated (want 2)
|
||||
[4] pceff03.t32 r = [90, 30, 10, 3, 0, 0, 0]
|
||||
[5] pceff04.t32 r = [90, 30, 10, 3, 0, 0, 0]
|
||||
|
||||
top-level elements with a keyframe group, disc-wide: 15493
|
||||
carrying a non-zero rotation: 2152 (13.89 %)
|
||||
|
||||
by archive:
|
||||
1868 GP_READY_ROOM.pak
|
||||
40 GP_DIALOG.pak
|
||||
37 GP_MAIN_GAME_D2D.pak
|
||||
37 GP_MAIN_GAME_E2D.pak
|
||||
37 GP_MAIN_GAME_F2D.pak
|
||||
37 GP_MAIN_GAME_I2D.pak
|
||||
37 GP_MAIN_GAME_J2D.pak
|
||||
37 GP_MAIN_GAME_S2D.pak
|
||||
10 GP_GAMEOVER.pak
|
||||
8 GP_DEBRIEFING_PILOTLOG.pak
|
||||
4 GP_TITLE.pak
|
||||
|
||||
most common rotated element names:
|
||||
444 pbb_destroyer.t32
|
||||
402 pbr_destroyer.t32
|
||||
276 pbr_fighter.t32
|
||||
174 pbb_fighter.t32
|
||||
168 pbb_aircraft.t32
|
||||
84 pb_warp_eff.t32
|
||||
48 pbtab.t32
|
||||
35 gbp012i.t32
|
||||
32 pzeff02.t32
|
||||
30 gbp001i.t32
|
||||
@@ -177,3 +177,51 @@ expressed in degrees rather than of a free angle. 🟡 That is a hypothesis abou
|
||||
* `+4` and `+8` are **not** claimed as X/Y rotation. Three adjacent signed
|
||||
degree-valued words invite that reading; only `+12` has been observed to do
|
||||
anything.
|
||||
|
||||
## ✅ The top-level census, finished (2026-08-28)
|
||||
|
||||
The earlier attempt drove `sylpheed-cli screen info --geometry` in a shell loop
|
||||
and never finished — it decodes every texture per build.
|
||||
[`tools/re-capture/rotation_toplevel_census.py`](../../../tools/re-capture/rotation_toplevel_census.py)
|
||||
walks the top-level placement region the way `ui_layout.rs` does; seconds instead
|
||||
of hours. Output: [`data/rotation-toplevel-census.txt`](../data/rotation-toplevel-census.txt).
|
||||
|
||||
Both controls pass — `GP_TITLE` build 4 reports **0** (its rotations are the
|
||||
nested `ptloop` records) and `GP_DIALOG` build 0 reports the **2** expected
|
||||
`pceff03`/`pceff04` elements.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| top-level elements with a keyframe group | 15 493 |
|
||||
| carrying a non-zero rotation | **2 152 (13.89 %)** |
|
||||
|
||||
By archive: `GP_READY_ROOM` **1 868**, `GP_DIALOG` 40, each of the six
|
||||
`GP_MAIN_GAME_*2D` 37, `GP_GAMEOVER` 10, `GP_DEBRIEFING_PILOTLOG` 8,
|
||||
`GP_TITLE` **4**.
|
||||
|
||||
### Two more corroborations, both free
|
||||
|
||||
**The rotated population is tactical-map ship icons.** The most-rotated names are
|
||||
`pbb_destroyer` (444), `pbr_destroyer` (402), `pbr_fighter` (276),
|
||||
`pbb_fighter` (174), `pbb_aircraft` (168) — map markers, in the archive that
|
||||
holds the tactical map. Rotating a ship icon to its heading is what the field is
|
||||
*for*, and it is the single largest use of it on the disc.
|
||||
|
||||
**`GP_TITLE` entry 7's four rotated elements carry their own control.** Three of
|
||||
them are the Japanese wordmark's pieces, and they settle to upright from
|
||||
**alternating** tilts:
|
||||
|
||||
```
|
||||
ptlogo3a.t32 r = 0, -14, -4, -1, 0, 0, 0, 0
|
||||
ptlogo3b.t32 r = 0, +14, +4, +1, 0, 0, 0, 0
|
||||
ptlogo3c.t32 r = 0, -14, -4, -1, 0, 0, 0, 0
|
||||
```
|
||||
|
||||
Same magnitudes, alternating sign, all decaying to 0 — three sibling elements
|
||||
tilting opposite ways and straightening. A misread field does not produce that.
|
||||
(The fourth is `ptlogo_eff3.t32`, `r = 0, 0, 80, 150` — the transient bloom
|
||||
discussed in [ui-resting-pose](ui-resting-pose.md).)
|
||||
|
||||
⚠️ **This is not subtractable from the byte-scan census.** That one counts
|
||||
*blocks* (12 164 of 83 862 with a non-zero `+12`); this one counts *elements*.
|
||||
The two are different units and the difference is not "the nested population".
|
||||
|
||||
92
tools/re-capture/rotation_toplevel_census.py
Normal file
92
tools/re-capture/rotation_toplevel_census.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/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}")
|
||||
Reference in New Issue
Block a user