Immediately after the declaration table the RATC bundle holds a placement region, one group per element: a header of (u32 element index, u32 keyframe count) followed by that many 40-byte keyframes carrying scale/tint/X/Y. The element index is stated in the header, so the correspondence is read, not inferred from order. Tutorial pause bundle: 11 groups for 11 elements, every header index and count matching the blocks present. The values are self-evidently right -- three menu buttons at X=546 exactly 70 px apart (288/358/428), title (540,119), message (451,545), and the eff* sprites carrying multi-position fly-ins. Cross-checked: pgp_ttrl_btn10.rat places its own sprite at (546,288), identical to its inline group, so the inline region is the same placement data and it covers the eff*/deli*/msg elements that have no record of their own. So a screen is fully reconstructible from its bundle alone: element list and order from the declaration table, placement and animation from this region, plus the sprites, with "opt " supplying focused states. This retires the caveat added earlier today that only the order was known. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
13 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 0 on every entry seen
+32 u32 PARENT element index, or 0xffffffff for "none" ← see below
+36 u32 0xffffffff on every entry seen
+40 u32 kind flags 0 for a plain sprite, 0x3002 for a button record,
1 for an element that has a parent
+44 u32 0xffffffff, but 0 on button records
+48 u32 pivot X
+52 u32 pivot Y
+56 u32 0
Corrected 2026-08-11: this was previously written as "+28 u32 ×4 flags" — it
is five words, and the second of them is not a flag.
+32 is a parent element index. In the in-mission pause bundle,
pgpeff02a.t32 carries 3 and element 3 is pgpeff02.t32; pgpeff03a.t32
carries 5 and element 5 is pgpeff03.t32 — each …a variant naming its base
element. Verified on two independent language builds of that screen (4 links, no
out-of-range value anywhere in any bundle), and the tutorial bundle, which has no
…a variants, carries 0xffffffff throughout.
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.
It generalizes — the title screen
The same method run against GP_TITLE.pak reproduces the main menu, which is a
different screen with a different item count and a different pitch:
ptbtn01..05.rat give X = 542 for all five and Y = 162 / 242 / 322 / 402 / 482 — an
80 px pitch, where the pause menu used 70. Measured against the screenshot, the sprite
tops land at a constant +46 px for all five (one reads 45, a 1-px edge-detection
wobble), and 46 is exactly the 45 px of Xenia window chrome plus one. So the record's Y is
the sprite's top edge in the guest framebuffer, to the pixel, on a second screen.
GP_TITLE.pak also splits by sub-screen the way the pause pak splits by context:
ptbtn00 alone (the PRESS Ⓐ BUTTON prompt), ptbtn01..05 (main menu), ptbtn11..13
(the EXTRAS submenu), plus pgloading_* for the loading screen.
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
-
Half of this gap is now closed. The
eff*/deli*/msgsprites have no.ratof their own, andloop1.ratis an animation, so the question was where the screen's draw list lives. It is the bundle's own declaration table (above): that table is not the child listing — the children are grouped by type (every.t32, then every.rat), while the declaration table names elements, in a plausible back-to-front order, and it is exactly the missing set that appears there. For the in-mission pause menu it lists 23:eff11/12/10,eff02(+a),eff03(+a),eff01,title.rat,btn00/01/04/05.rat,msg,deli1…4,eff30…33,loop1.rat. Note what it does not list: the focused button variants, which are reached through each base record'soptlink — so the table is the screen's element set, not a resource inventory. -
And the position is there too — the gap is closed. Immediately after the declaration table the bundle carries a placement region, one group per element:
group header: u32 element index (0,1,2… — the correspondence is stated, not inferred) u32 keyframe count then `count` keyframes, 40 bytes apart, each holding the familiar block: u32 scale X = 100 u32 scale Y = 100 u32 tint = 0xffffffff u32 X u32 Y (+ a trailing time field)For the tutorial pause bundle this yields 11 groups for 11 elements, and every group's header index and count match the blocks actually present. The values are self-evidently right: the three menu buttons sit at X=546, 70 px apart (288 / 358 / 428), the title at (540,119), the message at (451,545), and the
eff*sprites carry multi-position fly-ins.Cross-checked against the records themselves:
pgp_ttrl_btn10.ratplaces its sprite at (546,288) — identical to its inline group. So the inline region is the same placement data, and it covers theeff*/deli*/msgelements that have no record of their own.A screen is therefore fully reconstructible from its bundle alone: element list and order (declaration table) + placement and animation (this region) + sprites, with
optsupplying focused states. -
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).

