Three things, all from parsing ptbtn0Nf.rat as a build. 1. THE RING SPINS. Its two keyframes differ in exactly one field: rotation_deg ramps 0 -> 360 with position, scale, alpha and tint all constant. A spin in place, the same shape as the GP_BUNK example already recorded. 2. THE ORACLE CONFIRMS THE GAME RENDERS IT. In the OPTIONS-focused capture the ring's bright head sits in a completely different angular position from the sprite's own -- caught mid-spin. This is a SECOND independent confirmation that rotation_deg is drawn, now on a different screen and a different element from the ptloop sweeps, and it raises rotation's priority: it is not a title-only concern that sits off-screen at rest, it is the main menu's focus marker. NO ANGLE IS QUOTED. A brightest-region centroid says ~250 deg, but the control refuses that precision -- rotating the sprite by a known 30/90/180/270 and re-measuring gives errors up to 19.8 deg. What survives the error bar is that a <=20 deg error cannot manufacture a ~250 deg displacement. 3. WHICH PLACEMENT WINS -- correcting this page's own earlier caveat, which said to use the leaf only for elements the parent does not declare. Right for a BASE record, wrong for an f record: the parent declares NO element for ptbtn0Nf.rat at all (zero of build 5's 16), so the f record's placement comes from its leaf for BOTH elements, label included. The label's (-7,-7) is load-bearing -- the f sprite is 13px larger per axis and -7 keeps them concentric (535+96/2 = 583 vs 542+83/2 = 583.5). Corroborated against the oracle: the focused-minus-unfocused region is x 505..703, and the leaf predicts a right edge near 707 where the parent reading predicts 714. Also exposes UiBuild::records (name -> (offset, size) of a nested .rat leaf). Nested records were parsed into a PRIVATE map, so a consumer holding a UiBuild could not locate a leaf's bytes at all -- which is exactly what blocked the port from reaching the ring.
10 KiB
✅ A focused button is a two-element record, and the second element is the ring
Status: 🟡 DECODED for GP_TITLE, disc-wide check not yet run; the
sprite contents and the ring are ✅ MEASURED against a live capture. Answers the
port's "focus: drawn over the base element, or instead of it?"
⚠️ The element-count field at +20 is read here on GP_TITLE's ten button
records only (five buttons × two language bundles). A disc-wide check —
"does +20 equal the number of resource names the leaf embeds, for every .rat
leaf on the disc?" — is written and was still running when this was committed.
Until it lands, treat +20 as a strong local reading, not a decoded field:
the observable claim below (the focus record carries a second sprite,
ptbtneff01.t32, and the port must draw it) rests on the embedded names and the
capture, not on that word.
Short answer: it does not matter, and that is not the bug. The focused sprite covers the base completely, so over-vs-instead is worth at most 12/255 on ~25 pixels. What is missing is the focus record's second element — a glowing ring that only exists when focused.
The record
A button's base and focused variants are both .rat leaves, and they are not the
same shape:
ptbtn02.rat 164 B strings: RATC, ptbtn02.t32, opt ptbtn02f.rat
ptbtn02f.rat 312 B strings: RATC, ptbtneff01.t32, ptbtn02f.t32, opt ptbtn02b.rat
The header says so explicitly. A .rat leaf begins:
+0 "RATC"
+16 u32 flags 0x00008110 (base) 0x00008112 (focused)
+20 u32 ELEMENT COUNT 1 2
+32 first element name
So the focused record declares two elements — ptbtneff01.t32 first, then
ptbtn0Nf.t32 — where the base declares one. Both f records of both language
bundles read the same way, and the flag word differs only in bit 0x02
alongside the count.
⚠️ The opt link on a .rat leaf is not simply "my focused variant". The
chain runs ptbtn01.rat → ptbtn01f.rat → ptbtn02.rat → ptbtn02f.rat → ptbtn02b.rat, i.e. it threads base and focused records together in order. The
corpus calls it the focus link and for a base record that reading works; do not
generalise it to the f records.
The three sprites
Decoded with sylpheed-cli pak textures, shown over a checkerboard in
button-base-focus-ring-sprites.png:
| sprite | size | what it is |
|---|---|---|
ptbtn02.t32 |
117×43 | the dim label, its underline, and a small dot-in-circle at the underline's left end |
ptbtn02f.t32 |
130×56 | the same label, bright and glowing — a complete replacement, 13 px larger in each axis |
ptbtneff01.t32 |
42×46 | a glowing ring, focus only |
The small dot-in-circle is on every button all the time; the large ring is
the focus marker. They are different things and sit side by side on the focused
row — visible in
live-main-menu-options-focused.png
against
live-main-menu.png.
Over or instead? Measured, and the answer is "unobservable"
Aligning base and focused by normalised cross-correlation of their alpha masks — the true offset is (7,7), not the (6,6) that centring predicts —
| pair | base-visible px | f alpha ≥ base alpha there |
|---|---|---|
ptbtn02 / f (bundle b58a0fe6) |
1 444 | 100.0 % |
ptbtn01 / f (bundle b58a0fe6) |
1 898 | 100.0 % |
ptbtn02 / f (bundle a715f485) |
3 720 | 100.0 % |
⚠️ At the centre alignment it reads 78–84 %, and that number is an artefact. A 1 px shift on strokes this thin manufactures a fifth of a sprite's worth of "the focused art is thinner here". Solve the alignment before trusting a coverage figure.
Coverage is not the same as hiding, though, because f is not fully opaque
everywhere. Compositing both ways over the menu's own background colour:
| pair | max channel difference | px > 8/255 | RMSE over the button rect |
|---|---|---|---|
ptbtn02 |
12.5 | 23 | 1.09 |
ptbtn01 |
12.2 | 27 | 1.08 |
ptbtn02 (JP) |
12.2 | 27 | 1.05 |
So the two hypotheses differ by ~1.1 RMSE inside the button rectangle, on a
couple of dozen pixels — below the ≈ γ 1.4 tone gap
(tone curve) and far below any frame-level RMSE this
corpus can resolve. Either choice is defensible; neither is measurable.
Replacing is the cheaper one and is what the file's structure suggests, since
ptbtn0Nf.t32 is a whole label rather than an overlay.
✅ Where the ring is placed — DECODED 2026-08-29, no authoring needed
This page previously said the per-element placement inside a .rat leaf was not
decoded and that a consumer should eyeball it off a capture. That was wrong by
omission: a leaf needs no new reader. Its first 32 bytes have the same shape as
a bundle header — "RATC", 0x3c declaration-entry size at +4, element count
at +20, design size 1280x720 at +24/+28 — so ui_layout::parse_build
reads it unchanged.
The control is the base record, whose position is known independently: the
parent screen reports ptbtn01.rat resting at (542,162), and parsing the leaf
on its own returns ptbtn01.t32 at (542,162). It reproduces all five.
Positions are absolute design-space top-left, not offsets
(examples/rat_leaf_placement.rs):
| button | base | ring ptbtneff01.t32 |
Δ | label ptbtn0Nf.t32 |
Δ |
|---|---|---|---|---|---|
ptbtn01 |
(542,162) | (500,156) | (−42,−6) | (535,155) | (−7,−7) |
ptbtn02 |
(542,242) | (500,236) | (−42,−6) | (535,235) | (−7,−7) |
ptbtn03 |
(542,322) | (500,316) | (−42,−6) | (535,315) | (−7,−7) |
ptbtn04 |
(542,402) | (500,396) | (−42,−6) | (535,395) | (−7,−7) |
ptbtn05 |
(542,482) | (500,476) | (−42,−6) | (535,475) | (−7,−7) |
The offset is uniform: (−42,−6) for the ring and (−7,−7) for the label on
every button, and identical in the Japanese bundle (pak entry 8).
⚠️ Which placement wins — and an earlier version of this page was misleading.
It said "the parent is what compose honours; treat the leaf's as the source only
for elements the parent does not declare". That is right for a base record and
wrong for an f record, because the parent declares no element for
ptbtn0Nf.rat at all — checked, zero of build 5's 16 elements name it. So the
f record's placement can only come from its own leaf, for both its elements,
the bright label included.
The (−7,−7) on the label is real and load-bearing: ptbtn0Nf.t32 is 13 px
larger in each axis than the base, and −7 keeps the two concentric
(535 + 96/2 = 583 against 542 + 83/2 = 583.5). Drawing the f label at the
base element's position would push it 7 px down-right.
✅ Checked against the oracle. Differencing the OPTIONS-focused capture
against the unfocused one, the changed region is x 505…703, y 397…446.
The leaf predicts ink starting inside the ring's box at x ≥ 500 (measured 505, a
5 px art inset) and the label's right edge near 707 (measured 703); the
parent-position reading predicts 714. Both the right edge and the bottom edge
favour the leaf by ~7 px. ⚠️ Ink-inset reasoning is soft — the decisive argument
is the structural one above, that there is no parent element to inherit from.
The one thing that is duplicated is the base record: ptbtn04's parent
element rests at y 401 while its own leaf says 402 (and in the JP bundle
the leaf says 401 against a parent 401). For a base record the parent is what
compose honours.
✅ The ring SPINS — and the oracle confirms the game draws the rotation
The ring's two keyframes differ in exactly one field:
ptbtneff01.t32 kf0 t=120 pos=(500,156) scale=100%,100% a=255 rot=0
kf1 t=None pos=(500,156) scale=100%,100% a=255 rot=360
Position, scale, alpha and tint are all constant; only rotation_deg ramps,
0 → 360. That is a spin in place — the same shape as the GP_BUNK example
already recorded in ui-keyframe-rotation.md.
✅ And it is actually rendered. In the oracle's OPTIONS-focused frame the
ring's bright head sits in a completely different angular position from the
sprite's own — the game caught it mid-spin:
focus-ring-oracle-vs-sprite.png
(left: the oracle at the ring's declared box; right: the sprite, unrotated).
🔴 Do not quote an angle from this. A brightest-region centroid puts the displacement near 250°, but the control refuses it: rotating the sprite by a known 30/90/180/270° and re-measuring gives errors up to 19.8°, so the estimator is not trustworthy at that precision. What survives is the part the error bar cannot touch — a ≤20° error cannot manufacture a ~250° displacement, so the ring is drawn substantially rotated, and the exact angle is one frame of a continuous spin and not a stable quantity anyway.
⚠️ This raises rotation's priority for a port. It is not a title-only concern
that sits off-screen at rest: the main menu's focus marker spins, so a renderer
that ignores rotation_deg draws a static ring with its highlight in the wrong
place, on the screen the player looks at most.
🟡 The spin's period is not established. t=120 is the first keyframe's time,
and what the untimed second one means for a leaf — as opposed to a screen, where
it is the ~0.4 s exit ramp — was not tested.
⚠️ screen render --focus is blind to the ring, and so was this page
el.focused is name-based on top-level elements, and a screen's buttons are
.rat records whose focused twin is not itself a top-level element — so
rendering build 5 with and without --focus produces an identical image. The
reference renderer has the same blind spot the port reported, for the same
reason: neither walks into the leaf. Fixing it is a renderer change, not a
format question; the format is decoded above.
❔ Not established
ptbtn02b.t32— a third variant,b, exists for button 02 only, same size as the base. Not seen on any capture. Not chased.- Whether a non-title archive uses the same two-element convention. Checked
on
GP_TITLEonly.