diff --git a/docs/re/structures/ui-rat-layout.md b/docs/re/structures/ui-rat-layout.md index 85e0deb..5617ba0 100644 --- a/docs/re/structures/ui-rat-layout.md +++ b/docs/re/structures/ui-rat-layout.md @@ -19,13 +19,33 @@ The UI is **one pak per screen** — `GP_TITLE`, `GP_PAUSE_MENU`, `GP_READY_ROOM `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: +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 | |---|---| | `.t32` | the sprite ([T8aD](texture-color-k8888.md)) | | `.rat` | that sprite's **layout record** (this document) | -| `loop1.rat` | a screen-level record (larger; not yet decoded) | +| `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 @@ -88,6 +108,11 @@ authored once and only the `.t32` sprites are swapped, which has two consequence - 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 diff --git a/tools/re-capture/ratc_decls.py b/tools/re-capture/ratc_decls.py new file mode 100644 index 0000000..9071955 --- /dev/null +++ b/tools/re-capture/ratc_decls.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""Parse a RATC bundle's header SPRITE DECLARATION TABLE: u32 count at 0x14, then +fixed 60-byte entries of [name, NUL-padded | 4 u32 flags | pivotX | pivotY | 0]. +Check each pivot against half the decoded texture's real dimensions.""" +import struct, sys, glob, re, os + +def decls(d): + n = struct.unpack_from(">I", d, 0x14)[0] + out = [] + for i in range(n): + o = 0x20 + i * 60 + if o + 60 > len(d): break + name = d[o:o+28].split(b"\0")[0].decode("ascii", "replace") + px, py = struct.unpack_from(">II", d, o + 48) + out.append((name, px, py)) + return n, out + +def texmap(prefix): + t = {} + for f in glob.glob(f"pause-tex/{prefix}_*.png"): + m = re.match(rf".*/{prefix}_(.+)\.t32_(\d+)x(\d+)\.png", f) + if m: t[m.group(1) + ".t32"] = (int(m.group(2)), int(m.group(3))) + return t + +for path, prefix in [(sys.argv[1], sys.argv[2])]: + d = open(path, "rb").read() + n, ds = decls(d) + tm = texmap(prefix) + print(f"{os.path.basename(path)}: count={n}, parsed={len(ds)}") + ok = bad = miss = 0 + for name, px, py in ds: + if name in tm: + w, h = tm[name] + hit = (px == w // 2 and py == h // 2) + ok, bad = ok + hit, bad + (not hit) + flag = "OK " if hit else "MISMATCH" + print(f" {flag} {name:28s} tex {w:4d}x{h:<4d} half {w//2:4d},{h//2:<4d} decl {px:4d},{py:<4d}") + else: + miss += 1 + print(f" ? {name:28s} (no decoded texture) decl {px:4d},{py:<4d}") + print(f" => pivot == half(texture): {ok} ok, {bad} mismatch, {miss} unchecked")