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

204 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ✅ 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:
```text
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:
```text
+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`](../captures/ui-layout/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`](../captures/title-builds/live-main-menu-options-focused.png)
against
[`live-main-menu.png`](../captures/title-builds/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](ui-render-tone-curve.md)) 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`](../../../crates/sylpheed-formats/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:
```text
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`](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`](../captures/ui-layout/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.