# `.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**. 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](texture-color-k8888.md)) |
| `<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 `.rat`s (`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 (`pgpbtn00` 86×42 → 43,21; `pgpbtn05` 221×42 → 110,21; `pgptitle` 202×73 →
  101,36; `pgpbtn04` 172×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 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*. 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.rat` is **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 **`PRMD` primitives**, 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 by `end `.

### 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
  `.t32` declaration carries the correct English pivot (7/7), while the `.rat` declarations
  carry Japanese-derived ones (`btn10` → 43,21 = 86/2, the *Japanese* sprite). So the
  `.t32` table is regenerated per language and the `.rat` layer 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.

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. **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).
3. **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:

![main menu real vs rebuilt](captures/ui-layout/title-mainmenu-real-vs-rebuilt.png)

`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 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

- **The `eff*` / `deli*` / `msg` placements are still missing** — those sprites have no
  `.rat` of their own, and `loop1.rat` turned 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 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).
