Files
Sylpheed/docs/re/structures/ui-button-focus-record.md
Sylpheed RE agent 76653ca41b re(ui): the focus ring SPINS, the game draws it, and the leaf owns the f record
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.
2026-08-29 08:46:53 +00:00

10 KiB
Raw Permalink Blame History

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 7884 %, 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_TITLE only.