u32 count at 0x14, then fixed 60-byte entries of [name | 4 flag words | pivotX | pivotY | 0]. The table lists both .t32 sprites and .rat records, so it is the screen's element list. Verified: for all 7 .t32 entries in the tutorial pause bundle the declared pivot is exactly half the decoded texture's dimensions, 7/7 with no mismatch. That also settles what the pair at 0x50/0x54 of a .rat record is — a .rat simply opens with one of these declarations. The language split is visible inside one bundle: the English build's .t32 declarations carry correct English pivots while its .rat declarations carry Japanese-derived ones, so the sprite table is regenerated per language and the layout layer is inherited from the Japanese master. Adds tools/re-capture/ratc_decls.py (parse the table, check pivots against the decoded textures). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9.2 KiB
.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.
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 bundle is one (context × language) build of that screen. Its own header is the screen's element declaration table, and the elements themselves follow as children:
| child | what it is |
|---|---|
<name>.t32 |
the sprite (T8aD) |
<name>.rat |
that sprite's layout record (this document) |
<screen>loop1.rat |
a looping sprite animation (see below) |
The bundle header — element declaration table
0x14 u32 entry count
0x20 entry[count], 60 bytes each:
+0 char[28] element name, NUL-padded ("pgp_ttrl_eff10.t32", "pgp_ttrl_btn10.rat")
+28 u32 ×4 flags (0xffffffff / 0xffffffff / 0 / 0xffffffff on every entry seen)
+48 u32 pivot X
+52 u32 pivot Y
+56 u32 0
The table lists both sprites and .rat records — it is the screen's element list.
pgp_ttrl declares 11: six eff*, msg, and four .rats (title, btn10..12).
✅ Verified: for all 7 .t32 entries the declared pivot is exactly half the decoded
texture's dimensions — eff10 408×120 → 204,60; eff21 428×360 → 214,180; msg 381×38 →
190,19; and so on, 7/7 with no mismatch (tools/re-capture/ratc_decls.py).
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), and tag-driven: 4-char ASCII tags (opt ,
PRMD, end ) mark sections, so a record is a stream of blocks rather than a fixed struct.
A minimal record (a static button) is 165 bytes:
0x00 "RATC" magic — a RATC bundle reused as a data record
0x08 u32 payload size
0x14 u32 entry count (loop1.rat: 3, matching its 3 sprite names)
0x18 u32 design width = 1280
0x1c u32 design height = 720
0x20 char[16] the sprite this record places, e.g. "pgpbtn00.t32"
0x50 u32 pivot X = texture width / 2
0x54 u32 pivot Y = texture height / 2
...
── placement block ──
u32 scale X = 100 (percent)
u32 scale Y = 100
u32 tint = 0xffffffff (RGBA, white = untinted)
u32 X ← top-left position
u32 Y ←
u32 time (keyframe records only)
...
"opt " u32 len char[len] link to another record, e.g. "pgpbtn00f.rat"
- X/Y is the sprite's top-left, not its centre: compositing at these coordinates reproduces the screenshot, which drawing centred on them would not.
- The pivot at 0x50/0x54 is half the texture size — 4 of the 5 plain sprites match
exactly (
pgpbtn0086×42 → 43,21;pgpbtn05221×42 → 110,21;pgptitle202×73 → 101,36;pgpbtn04172×43 → 86,22). It is a rotation/scale centre, not a draw offset. - Animated elements are a keyframe list.
pgptitle.rat(752 B) is the same placement block repeated with a varying trailingtimefield — the PAUSE title's fly-in. optcarries a length-prefixed record name. Onpgpbtn00.ratit points atpgpbtn00f.rat, i.e. normal state → focused state. Focused records place their sprite 42 px left and 8 px up of the base, because the focused art includes the selection ring that hangs off the left edge; their pivot is a constant (21,25) rather than half-size.<screen>loop1.ratis not a screen composition — it is a looping sprite animation: a 3-name table (pgpeff34/35/36.t32) plus ~30 keyframes all at one position.- The small (380 B) top-level entries are
PRMDprimitives, not sprites: a colour and four explicit corner coordinates(0,0) (1280,0) (0,720) (1280,720)— the full-screen quad that dims the scene behind the pause menu — terminated byend.
The records are language-independent
pgpbtn00.rat is byte-identical in the English and Japanese bundles. The layout is
authored once and only the .t32 sprites are swapped, which has two consequences:
- The baked pivot belongs to whichever build the record was authored from, not to the
sprite actually shipped beside it. That is why
pgpbtn01's pivot (113 → a 226 px wide texture) matches neither the English sprite (207) nor the Japanese one (148). - The split is visible inside a single bundle: in the English tutorial build, every
.t32declaration carries the correct English pivot (7/7), while the.ratdeclarations carry Japanese-derived ones (btn10→ 43,21 = 86/2, the Japanese sprite). So the.t32table is regenerated per language and the.ratlayer is inherited from the Japanese master. - Do not infer anything about a language from a texture size — see the traps below.
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.
- Differential. Across
pgpbtn00/01/04/05.rat, exactly one field varies and it steps268 → 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. - Absolute placement — the decisive test. The tutorial build's records give 546/288, 546/358, 546/428 and 540/119. Compositing its sprites at exactly those numbers, with no offset and no fitting, reproduces the screenshot (image above).
- Pivot. 4 of 5 plain sprites carry exactly half their texture's dimensions at 0x50/0x54 (above).
A caution on step 2, because the first pass here got it subtly wrong: the screenshot is of the tutorial pause menu, so only the
pgp_ttrl_*records can be checked against it. The in-mission records (X = 226) also map onto the same screenshot under a single constant offset — but that only works because both builds share the 70 px pitch, and it proves nothing. The in-mission coordinates remain unverified: confirming them needs a screenshot of a pause during an actual mission.
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 records being language-independent makes this worse: a record's baked pivot implies a texture width that matches no shipped sprite.) - 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 isRESUME / 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
- The
eff*/deli*/msgplacements are still missing — those sprites have no.ratof their own, andloop1.ratturned out to be an animation, not a composition. So the screen's draw list lives somewhere not yet found (the parent RATC's own header region, or title code). That is the gap between the rebuild above and a complete screen. - The same method should now unroll the other screens directly;
GP_HANGAR_ARSENAL.pak(789 T8aD + 510 RATC) is the big one, and the ARSENALDATA SHEETpanel documented in 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).
