Closes the reach caveat the `opt ` name fix left behind: 60 of 18 002 RATC children carry no `opt ` block, and it was not established whether they lack one or sit past our 128-byte window. Neither. They are not children. `examples/ratc_optless_children.rs` re-runs `ratc::parse`'s own guards over the disc and reports which one fired: all 60 are "tag beyond the window", none is rejected by length, gap or charset, none is child #0, and all 60 live in six bundles of one archive. Within a bundle the distances back to the nearest tag are an exact arithmetic progression, step 60 600 -- ten different records finding the SAME tag, because there is only one. Reading a bundle directly: children 1..10 are equal-size T8aD blocks under a single `opt ` name, `pb_f15_eg_anm.tan`. `.tan` is a FRAME SEQUENCE. One block declares the resource; its payload is a run of T8aD frames. Disc-wide, over all 18 718 `opt ` names in all 33 paks: a RATC bundle names exactly six kinds of resource -- `.t32` 14 756, `.rat` 3 311, `.prm` 367, `.tbm` 224, `.sbo` 54, `.tan` 6. Six `.tan`, ten frames each = 60, the entire population with nothing left over. The negative is closed, not narrowed. Consequence recorded but deliberately not fixed: `ratc::parse` over-reports there, listing a `.tan`'s frames as anonymous children. Nothing in the menu milestone reads a `.tan` -- it occurs only in GP_READY_ROOM, which S1 ruled out -- so no screen the port draws changes. Also a METHOD entry for this container OOM-killing `slb_leading_segment_disc` under default test parallelism (SIGKILL, no assertion; 8/8 pass with --test-threads=1).
142 lines
6.6 KiB
Markdown
142 lines
6.6 KiB
Markdown
# 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:
|
||
|
||
```text
|
||
"opt " | BE32 length | name | NUL | 3 bytes | <child magic>
|
||
```
|
||
|
||
Two children of the main menu bundle (`GP_TITLE.pak` entry 8), raw:
|
||
|
||
```text
|
||
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
|
||
|
||
```rust
|
||
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`](../../../crates/sylpheed-formats/examples/ratc_child_names.rs)
|
||
compares the two readings for every RATC child in all 33 `dat/*.pak`
|
||
([data](../data/ratc-child-name-audit.txt)):
|
||
|
||
| | |
|
||
|---|---|
|
||
| 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 — closed 2026-08-29.** 60 children (0.3 %) have **no** `opt ` block
|
||
within 128 bytes and fall back to the scan. **They are not children.** They are
|
||
the ten frames of the disc's only `.tan` resource, `pb_f15_eg_anm.tan`, in the
|
||
six language copies of one `GP_READY_ROOM` bundle — 6 × 10 = 60, the whole
|
||
population with nothing left over. One `opt ` block names the whole run, which is
|
||
why nine of the ten find no block of their own. None is on the five menu screens.
|
||
[`ratc-tan-frame-sequence.md`](ratc-tan-frame-sequence.md)
|
||
|
||
## 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](../data/ratc-name-fix-render-effect.txt)):
|
||
|
||
| 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`](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`](../../../crates/sylpheed-formats/examples/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.
|