re: ui — the RATC bundle header is the screen's element declaration table
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>
This commit is contained in:
@@ -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 |
|
||||
|---|---|
|
||||
| `<name>.t32` | the sprite ([T8aD](texture-color-k8888.md)) |
|
||||
| `<name>.rat` | that sprite's **layout record** (this document) |
|
||||
| `<screen>loop1.rat` | a screen-level record (larger; not yet decoded) |
|
||||
| `<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
|
||||
@@ -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
|
||||
|
||||
41
tools/re-capture/ratc_decls.py
Normal file
41
tools/re-capture/ratc_decls.py
Normal file
@@ -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")
|
||||
Reference in New Issue
Block a user