Files
Syplheed-Reborn/docs/re/structures/ui-rat-layout.md
Claude (auto-RE) 8814df843b re: the RATC declaration table IS the screen's draw list, and +32 is a parent index
The doc's open gap was "where does the screen's draw list live" -- the eff/deli/msg
sprites have no .rat and loop1.rat turned out to be an animation. It is the
bundle's own declaration table, which is NOT the child listing: children are
grouped by type (all .t32, then all .rat) while the declaration table names
ELEMENTS in a plausible back-to-front order and contains exactly the missing set.
For the in-mission pause menu it lists 23 elements ending with loop1.rat, and it
omits the focused button variants -- those are reached via each base record's
"opt " link -- so it is the screen's element set, not an inventory.

Also corrects the entry layout: it is FIVE words after the name, not "u32 x4
flags", and word +32 is a PARENT ELEMENT INDEX (0xffffffff = none). pgpeff02a
carries 3 and element 3 is pgpeff02; pgpeff03a carries 5 and element 5 is
pgpeff03. Verified on two independent language builds, no out-of-range value in
any bundle, and the tutorial bundle (no "a" variants) is 0xffffffff throughout.

Bounded honestly: what is drawn and in what order is now answered; WHERE the
eff/deli/msg sprites land is not -- the declaration entry has a pivot but no X/Y.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 21:50:13 +00:00

12 KiB
Raw Blame History

.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

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 screenGP_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 (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

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

  • Half of this gap is now closed. The eff* / deli* / msg sprites have no .rat of their own, and loop1.rat is 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's opt link — so the table is the screen's element set, not a resource inventory.
  • What is still missing is only the position for the elements that have no .rat: the declaration entry carries a pivot but no X/Y. So what is drawn and in what order is answered; where the eff* / deli* / msg sprites land is not.
  • 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 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).