`ratc::parse` named each child by scanning backwards for the last printable run of bytes before its magic. The format states the name explicitly instead, in an `opt ` block: `"opt " | BE32 len | name | NUL | 3 bytes | magic` -- the same block `ui_layout::opt_link` already read for a button's focus link. The scan agrees with it 17 918 times out of 17 942 and is wrong 24 times, every one the same failure: the 3 trailing payload bytes are themselves printable and beat the real name. For `pteff05.t32` those bytes are `38 41 58` = `8AX`, so the full-resolution background of all five menu screens registered under a name no element declares, resolved to no sprite, and `compose` dropped it through an early `continue` that -- unlike the two arms above it -- records nothing. The screen lost its background and `screen render` still reported "all resolved". `8AX` was never a name. Docs that treated it as one are corrected here. Disc-wide, and the control is the 17 918 the scan already got right: the `opt ` reading reproduces every one of them. Effect on the five screens is the signature of the same art at twice the resolution -- mean brightness unmoved, high-frequency detail x1.15..x1.30 -- which is what the separately-measured `ui-8ax-fullres-background` result said the game draws. Also closes a long-standing dangling reference: `pmbase.t32`, recorded as "on the disc nowhere", is the `GP_STAGE_CLEAR` child the scan called `8AX`. RATC sibling references now resolve 10 148 of 10 148. Verified: 114/114 sylpheed-formats unit tests (including two new ones pinning the `8AX` case byte for byte and the no-block fallback), and every disc-gated integration suite in sylpheed-formats/sylpheed-cli.
6.3 KiB
A RATC child's name is stated by an opt block, not by the bytes before it
Status: ✅ DECODED, with a disc-wide check. This fixes a decoder defect that
silently dropped the full-resolution background from all five menu screens.
The field
Immediately before nearly every child of a RATC bundle — 17 942 of the disc's
18 002 — sits an opt block:
"opt " | BE32 length | name | NUL | 3 bytes | <child magic>
Two children of the main menu bundle (GP_TITLE.pak entry 8), raw:
opt 00 00 00 0a p t b a s e . t 3 2 00 0e 10 a4 T8aD
opt 00 00 00 0b p t e f f 0 5 . t 3 2 00 38 41 58 T8aD
^^^^^^^^
"8AX"
This is the same opt block ui_layout::opt_link already decodes for a
button's focus link. Nothing new had to be discovered to read it — only noticed.
The defect it fixes
ratc::parse named each child with name_before: the last printable run in the
96 bytes before the child's magic. That is a heuristic, and it is usually right
— ptbase.t32's three trailing bytes are 0e 10 a4, not printable, so the scan
walks back to the real name.
But pteff05.t32's trailing three bytes are 38 41 58, which is "8AX" in
ASCII. The scan takes those, the child is registered under the name 8AX, the
element that declares pteff05.t32 matches nothing in the sprite table, and
compose drops it through
let Some(sprite) = el.sprite.as_ref() else { continue };
— before the arm that records a missing sprite. So the screen's background
vanished with no diagnostic at all: screen render reported "all resolved".
⚠️ 8AX was carried in docs/ as though it were a name the game uses — the old
text read "the T8aD behind its opt link is registered under the name 8AX".
It is not a name. It is three bytes of the preceding record's payload.
The disc-wide check
examples/ratc_child_names.rs
compares the two readings for every RATC child in all 33 dat/*.pak
(data):
| RATC children scanned | 18 002 |
carrying an opt block |
17 942 |
| scanned name agrees with it | 17 918 |
| scanned name differs | 24 |
Every one of the 24 is the same failure: a 3-byte printable tail beating a real name.
| scanned | actual | count | where |
|---|---|---|---|
8AX |
pbbg.t32 |
12 | GP_READY_ROOM |
8AX |
pteff05.t32 |
4 | GP_TITLE 5, 6, 8, 9 — the menus |
8AX |
pmbase.t32 |
4 | GP_STAGE_CLEAR |
8AX |
pteff04.t32 |
2 | GP_TITLE 4, 7 — the title |
'OX |
po_keys_win1.t32 |
2 | GP_OPTIONS |
⚠️ What I checked about the 24, precisely. That the recovered name is the one
the bundle actually wants is verified for the ten GP_TITLE and GP_STAGE_CLEAR
cases: on the title screens pteff04.t32/pteff05.t32 are declared elements
that previously resolved to nothing and now resolve, and pmbase.t32 is the
target of GP_STAGE_CLEAR's long-standing dangling reference (below). For the 12
GP_READY_ROOM (pbbg.t32) and 2 GP_OPTIONS (po_keys_win1.t32) cases I
checked only that no element in those bundles is left unresolved afterwards —
which is consistent with, not proof of, the same story. None of the 14 is on the
five menu screens.
The control is the 17 918 the heuristic already got right: the opt reading
reproduces every one of them. A reading that fixed the 24 but disturbed the rest
would be a different rule, not this one.
🟡 Reach. 60 children (0.3 %) have no opt block within 128 bytes and
still fall back to the scan. None of them is on the five menu screens. Whether
they genuinely lack the block or sit past the search window is not established.
What it changes in the composite
Resolving the name makes the element resolve, so compose now draws it. On all
five port screens (before/after):
| build | screen | mean brightness | high-frequency detail |
|---|---|---|---|
| 4 | title | 72.77 → 72.81 | ×1.15 |
| 5 | main menu (JP) | 42.73 → 42.74 | ×1.27 |
| 6 | EXTRAS (JP) |
44.08 → 44.09 | ×1.27 |
| 8 | main menu | 41.57 → 41.58 | ×1.30 |
| 9 | EXTRAS |
42.99 → 43.00 | ×1.29 |
The brightness is unmoved and the detail is up by a quarter — which is exactly
the signature of the same artwork at twice the resolution replacing a 2×
upscale, and not of new content appearing. That it should be the full-res art
was settled separately and against the running game, in
ui-8ax-fullres-background.md; this page only
supplies the name that lets the renderer find it.
✅ Nothing is covered. The background is opaque and full-screen, and on the
menu it paints 4th of 16, so the worry is real. Measured at each element's
resting rect, before vs after: ptbtn01 sd 59.98 → 60.03, ptbtn03 63.40 →
63.45, ptbtn05 66.54 → 66.58, ptframe1 51.16 → 51.20, ptmsg 56.11 → 56.12.
Everything survives; only the two loop* elements paint beneath it, and those
are excluded from the default composite anyway.
⚠️ Both backgrounds are now drawn — ptbase.t32 upscaled 2×, then the
full-res one opaquely over it. Correct output, wasted fill. The port should draw
only the full-res one, and take its timing from ptbase's element, which is
the one carrying the keyframes.
What is NOT decoded
❔ No declaration word points at the child. Before reading the bytes I tested
whether the 60-byte element declaration indexes the T8aD child table. Its unread
words are +28, +36, +44 and +56; the control is the resolved elements,
whose child index is known. On the main menu, of 13 controls the words reproduce
the child index 1, 0, 0 and 1 times — and both 1s are the trivial index-0
case. There is no pointer; the association is by name, and the name is the opt
string. decl_word_probe.rs
✅ Incidental, from the same probe: +44 is a button ordinal. It is 1…5 on
exactly the five ptbtn0N.rat elements of the main menu, in order, and −1 on
every other element. Not needed for anything open, and recorded rather than
chased.