re: crack the UI layout record — the retail UI rebuilds from the disc

Each UI sprite <name>.t32 ships with a <name>.rat companion, and that companion
is the layout record: big-endian u32, a 1280x720 design space at 0x18, and a
placement block of scaleX/scaleY/tint/X/Y. Animated elements repeat the block
with a trailing time field (keyframes); an 'opt ' tag carries a length-prefixed
link to the focused-state record.

Verified in that order, records first: across the four pause buttons exactly one
field varies, stepping 268/338/408/478 at a constant 70px pitch with X fixed at
226; the three items visible in a screenshot of the running game measure
346.0/415.5/485.5, mapping onto those numbers under one constant offset to
within 0.5px. The tutorial build's records are outright framebuffer coordinates,
and compositing its sprites there reproduces the screenshot pixel-accurately.

Also records two mistakes the A/B caught, since a static-only reading would have
shipped both: texture width does not identify a language (Japanese fits English
widths), and the in-mission and tutorial pause menus are separate sprite sets
rather than one set re-packed into slots.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-28 19:05:21 +00:00
parent 07eff85819
commit 621b9dd6e9
5 changed files with 161 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
//! Scratch analysis: inventory one UI screen's pak — every entry, and for RATC
//! entries the children they bundle. Optionally write an entry's decompressed
//! bytes out for hex inspection.
//!
//! Run: cargo run -p sylpheed-formats --example ui_screen -- <PAK> [--dump 0xHASH out.bin]
use sylpheed_formats::pak::{self, PakArchive};
use sylpheed_formats::ratc;
fn main() {
let mut args = std::env::args().skip(1);
let pak = args.next().expect("usage: ui_screen <pak> [--dump 0xHASH out.bin]");
let arc = PakArchive::open(&pak).expect("open pak");
let rest: Vec<String> = args.collect();
if rest.first().map(|s| s == "--dump").unwrap_or(false) {
let h = u32::from_str_radix(rest[1].trim_start_matches("0x"), 16).unwrap();
let bytes = arc.read_by_hash(h).expect("entry present").expect("decompress");
std::fs::write(&rest[2], &bytes).unwrap();
println!("wrote {} bytes to {}", bytes.len(), rest[2]);
return;
}
if rest.first().map(|s| s == "--child").unwrap_or(false) {
// --child 0xHASH <child-name> <out>: carve one RATC child out by its
// listed offset/size, so a 165-byte layout record can be hexdumped alone.
let h = u32::from_str_radix(rest[1].trim_start_matches("0x"), 16).unwrap();
let bytes = arc.read_by_hash(h).expect("entry present").expect("decompress");
let kids = ratc::parse(&bytes).expect("ratc");
let k = kids.iter().find(|k| k.name == rest[2]).expect("child not found");
std::fs::write(&rest[3], &bytes[k.offset..k.offset + k.size]).unwrap();
println!("wrote {} B ({} @ {:#x}) to {}", k.size, k.name, k.offset, rest[3]);
return;
}
println!("{pak}: {} entries", arc.len());
for e in arc.entries() {
let Ok(bytes) = arc.read(e) else {
println!(" {:08x} <decompress failed>", e.name_hash);
continue;
};
let label = pak::inner_format_label(&bytes);
println!(" {:08x} {:>9} B {}", e.name_hash, bytes.len(), label);
if ratc::is_ratc(&bytes) {
if let Some(kids) = ratc::parse(&bytes) {
for k in &kids {
println!(" - {:<40} {:>9} B {}", k.name, k.size, k.kind);
}
if kids.is_empty() {
println!(" (no children found)");
}
}
}
}
}

View File

@@ -22,6 +22,7 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes
| XBG7 mesh | 🟡/❔ | `sylpheed-formats/src/mesh.rs` + `tests/mesh_disc.rs` ([xbg7](structures/xbg7-mesh.md)) | weapons/props: declaration-driven variable stride (36 models), GPU-confirmed. **Stage containers: 5662 sub-models across 22 stages** via content-anchored grouped pools (`stage_models`). Quantized hero bodies (DeltaSaber `f004`) still declined |
| Capital-ship part placement | 🟡 | `sylpheed-formats/src/ship.rs` (static) + [runtime capture](ship-placement-runtime-capture.md) | hull placement static-exact; external parts approximate statically. **Runtime capture** (Canary F10 → VS-constant WorldView) gives ground truth — validated on `e106` destroyer; not yet baked into the viewer |
| Weapon fields defaulted on disc | ✅/🟡 | [runtime DATA SHEET](weapon-datasheet-runtime.md) | The Arsenal Gallery-Mode panel reads the live record: `Ammo Capacity` = `LoadingCount` ✅, `Max. Lock Ons` = `TriggerShotCount` ✅. Recovers `TriggerShotCount` for `wep_05`/`wep_60` (both **4**); brackets defaulted `Power`/`MaximumRange` via the letter classes. 9 of ~61 weapons reachable at 5 % save progress |
| UI screen layout (`.rat`) | ✅/🟡 | [ui-rat-layout](structures/ui-rat-layout.md) | One pak per UI screen; each RATC = one (context × language) build; every `<name>.t32` sprite has a `<name>.rat` **layout record** (BE u32; 1280×720 design space; scale/tint/X/Y, keyframes for animated elements, `opt ` link to the focused state). **The tutorial PAUSE menu rebuilds pixel-accurately from the disc.** `loop1.rat` (screen-level draw order) not yet decoded |
## Functions / code paths

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

View File

@@ -0,0 +1,105 @@
# `.rat` — the UI element layout / animation record
**Status:**`CONFIRMED` for placement (2026-07-28). The retail UI can be
**reassembled from the disc**: the tutorial PAUSE menu rebuilds pixel-accurately from its
sprites placed at the coordinates in their `.rat` records — no fitting, no manual nudging.
![real vs rebuilt](captures/ui-layout/pause-tutorial-real-vs-rebuilt.png)
*Left: the running game (Canary screenshot). Right: rebuilt from `GP_PAUSE_MENU.pak` alone.
The remaining differences are the animated frame/glow sprites (`*eff*`) that were not
placed, and the live 3D background.*
## Screen composition
The UI is **one pak per screen**`GP_TITLE`, `GP_PAUSE_MENU`, `GP_READY_ROOM`,
`GP_SAVE_LOAD`, `GP_MISSION_SELECT`, `GP_OPTIONS`, `GP_SYSTEM`, `GP_TUTORIAL`,
`GP_STAGE_CLEAR`, `GP_MOVIE_THEATER`, `GP_DEBRIEFING_PILOTLOG`, `GP_LEADERBOARD`,
`GP_MISSION_LOG`, `GP_BUNK`, `GP_DIALOG`, `GP_GAMEOVER`, `GP_CHALLENGE`,
`GP_HANGAR_ARSENAL`.
Inside a screen pak, each top-level [RATC](../INDEX.md) bundle is **one (context × language)
build of that screen**, and its children come in pairs:
| child | what it is |
|---|---|
| `<name>.t32` | the sprite ([T8aD](texture-color-k8888.md)) |
| `<name>.rat` | that sprite's **layout record** (this document) |
| `<screen>loop1.rat` | a screen-level record (larger; not yet decoded) |
`GP_PAUSE_MENU.pak`'s six bundles are `{in-mission, tutorial} × {English, Japanese}`, with
the two in-mission builds each present twice at identical size. The purpose of that
duplicate is **NEEDS-HUMAN** (resolution or aspect variant?), and where the other four
shipped languages live is likewise unresolved — this pak holds only EN and JP.
Naming is transparent: `pgp` = pause screen, `pgp_ttrl_` = its tutorial context, `btnNN` =
menu item, `btnNNf` = that item's **focused** sprite, `eff*` = frame/glow decoration,
`deli*` = the item divider, `title`, `msg`.
## Record layout
Big-endian u32 throughout (Xbox 360). A minimal record (a static button) is 165 bytes:
```
0x00 "RATC" magic — a RATC bundle reused as a data record
0x08 u32 payload size
0x18 u32 design width = 1280
0x1c u32 design height = 720
0x20 char[] the sprite this record places, e.g. "pgpbtn00.t32"
...
── placement block ──
u32 scale X = 100 (percent)
u32 scale Y = 100
u32 tint = 0xffffffff (RGBA, white = untinted)
u32 X ← position
u32 Y ←
u32 time (keyframe records only)
...
"opt " u32 len char[len] link to another record, e.g. "pgpbtn00f.rat"
```
- **Animated elements are a keyframe list.** `pgptitle.rat` (752 B) is the same placement
block repeated with a varying trailing `time` field — the PAUSE title's fly-in.
- **`opt `** carries a length-prefixed record name. On `pgpbtn00.rat` it points at
`pgpbtn00f.rat`, i.e. *normal state → focused state*.
- Two fields (at 0x50/0x54 in the button records) vary per button and are **not identified**.
## Evidence
Positions were read out of the records and checked against a screenshot of the running
game, twice, in that order — the records were never fitted to the picture.
1. **Differential.** Across `pgpbtn00/01/04/05.rat`, exactly one field varies and it steps
`268 → 338 → 408 → 478` — a constant 70 px pitch — while the field before it is 226 in
all four. A vertical menu: constant X, evenly spaced Y.
2. **Against the game.** The three visible items measured at screenshot y-centres
346.0 / 415.5 / 485.5 — spacing 70.0 and 70.0, and a *single* constant offset maps all
three onto 268 / 338 / 408 to within 0.5 px.
3. **Absolute placement.** The tutorial build's records give 546/288, 546/358, 546/428 and
540/119, which are framebuffer coordinates directly: compositing the sprites there
reproduces the screenshot (image above).
## Two traps this caught
Both were mistakes made during this analysis, caught by comparing against the real game —
recording them because a static-only reading would have shipped them:
- **Texture width does not identify a language.** English `RESUME` (166 px) and Japanese
`再開` (86 px) differ hugely, but Japanese `通信ログ` (148 px) is within a few px of an
English label. The first language assignment made here was wrong; rendering the sprites
is the only reliable check.
- **The in-mission and tutorial pause menus are different sprite sets, not one set
re-packed.** In-mission is `RESUME / RADIO LOG / OPTIONS / BACK TO TITLE` (4 items,
`pgpbtnNN`); the tutorial is `RESUME / OPTIONS / BACK TO MENU` (3 items,
`pgp_ttrl_btn1N`). Matching the 3-item screenshot against the 4-item set suggests a
runtime slot-packing rule that does not exist.
## Next
- `<screen>loop1.rat` (1320 B) is the screen-level record — decoding it should give the
draw order and the `eff*`/`deli*` placements that are missing from the rebuild above.
- The same method should now unroll the other screens directly; `GP_HANGAR_ARSENAL.pak`
(789 T8aD + 510 RATC) is the big one, and the ARSENAL `DATA SHEET` panel documented in
[weapon-datasheet-runtime.md](../weapon-datasheet-runtime.md) is a ready-made oracle for it.
- Tooling: `crates/sylpheed-formats/examples/ui_screen.rs` (inventory a screen pak, carve a
named RATC child), `sylpheed-cli pak textures` (decode every sprite).