diff --git a/crates/sylpheed-formats/examples/screen_layout.rs b/crates/sylpheed-formats/examples/screen_layout.rs new file mode 100644 index 0000000..99c67db --- /dev/null +++ b/crates/sylpheed-formats/examples/screen_layout.rs @@ -0,0 +1,95 @@ +//! Dump a UI screen's full layout from one RATC bundle: the element declaration +//! table (what is drawn, in back-to-front order, with pivots and parent links) +//! plus the placement region that follows it (per element: a keyframe group of +//! scale / tint / X / Y). +//! +//! See docs/re/structures/ui-rat-layout.md. Run: +//! cargo run -p sylpheed-formats --example screen_layout -- [0xHASH] +//! With no hash, the largest RATC entry in the pak is used. + +use sylpheed_formats::pak::PakArchive; + +fn be32(b: &[u8], o: usize) -> u32 { + u32::from_be_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]) +} + +fn main() { + let mut args = std::env::args().skip(1); + let pak = args.next().expect("usage: screen_layout [0xHASH]"); + let want = args.next(); + let arc = PakArchive::open(&pak).expect("open pak"); + + let bytes = match want { + Some(h) if h.starts_with("0x") => { + let h = u32::from_str_radix(h.trim_start_matches("0x"), 16).expect("hash"); + arc.read_by_hash(h).expect("entry").expect("decompress") + } + Some(name) => arc.read_by_name(&name).expect("entry present").expect("decompress"), + None => arc + .entries() + .iter() + .filter_map(|e| arc.read(e).ok()) + .filter(|b| b.len() > 16 && &b[..4] == b"RATC") + .max_by_key(|b| b.len()) + .expect("no RATC entry"), + }; + assert_eq!(&bytes[..4], b"RATC", "not a RATC bundle"); + + let count = be32(&bytes, 0x14) as usize; + let mut names = Vec::with_capacity(count); + let mut meta = Vec::with_capacity(count); + for i in 0..count { + let e = &bytes[0x20 + i * 60..0x20 + (i + 1) * 60]; + let name = String::from_utf8_lossy(&e[..28]) + .trim_end_matches('\0') + .trim_end_matches(char::from(0)) + .to_string(); + let parent = be32(e, 32); + let kind = be32(e, 40); + let (px, py) = (be32(e, 48), be32(e, 52)); + names.push(name); + meta.push((parent, kind, px, py)); + } + + // Placement region: groups of (u32 element index, u32 keyframe count) then + // `count` 40-byte keyframes; the X/Y sit 12 bytes into the scale/tint block. + let mut pos = 0x20 + count * 60; + let mut placements: Vec> = vec![Vec::new(); count]; + for _ in 0..count { + if pos + 8 > bytes.len() { break } + let idx = be32(&bytes, pos) as usize; + let frames = be32(&bytes, pos + 4) as usize; + if idx >= count || frames == 0 || frames > 4096 { break } + let first = pos + 28; // header + lead-in, verified on the pause bundles + let mut group = Vec::with_capacity(frames); + for k in 0..frames { + let blk = first + k * 40; + if blk + 20 > bytes.len() { break } + group.push((be32(&bytes, blk + 12), be32(&bytes, blk + 16))); + } + placements[idx] = group; + pos = first + frames * 40 - 20; + } + + println!("{} elements", count); + println!("{:<3} {:<30} {:>7} {:>8} {:>12} {:>4} placement", "#", "element", "parent", "kind", "pivot", "kf"); + for i in 0..count { + let (parent, kind, px, py) = meta[i]; + let p = &placements[i]; + let shown = match (p.first(), p.last()) { + (Some(f), Some(l)) if p.len() > 1 && f != l => format!("{:?} → {:?}", f, l), + (Some(f), _) => format!("{:?}", f), + _ => "—".into(), + }; + println!( + "{:<3} {:<30} {:>7} {:>8} {:>12} {:>4} {}", + i, + names[i], + if parent == u32::MAX { "-".into() } else { parent.to_string() }, + format!("{kind:#x}"), + format!("({px},{py})"), + p.len(), + shown + ); + } +} diff --git a/docs/re/captures/arsenal-main-screen-layout.txt b/docs/re/captures/arsenal-main-screen-layout.txt new file mode 100644 index 0000000..b3d68b0 --- /dev/null +++ b/docs/re/captures/arsenal-main-screen-layout.txt @@ -0,0 +1,25 @@ +23 elements +# element parent kind pivot kf placement +0 preff01.t32 - 0x0 (164,156) 5 (256, 214) +1 preff02.t32 - 0x0 (155,175) 7 (305, 240) +2 preff03.t32 - 0x0 (155,175) 7 (225, 150) +3 preff04.t32 - 0x0 (247,237) 6 (173, 133) +4 prwinbase.t32 - 0x0 (136,130) 5 (284, 240) +5 prbtn1.rat - 0x3002 (28,14) 5 (242, 166) +6 prbtn2.rat - 0x3002 (43,14) 5 (242, 220) +7 prbtn3.rat - 0x3002 (58,14) 5 (242, 276) +8 prbtn4.rat - 0x3002 (89,14) 5 (242, 331) +9 prbtn5.rat - 0x3002 (89,14) 5 (242, 385) +10 prbtn6.rat - 0x3002 (104,14) 5 (242, 441) +11 prbtn7.rat - 0x3002 (58,14) 5 (242, 496) +12 prbtn8.rat - 0x3002 (30,14) 5 (242, 551) +13 prexp1.t32 - 0x0 (277,30) 8 (726, 143) → (1286, 143) +14 prexp1a.t32 13 0x1 (277,30) 3 (1126, 143) +15 prexp3.t32 - 0x0 (205,5) 5 (726, 381) +16 prmsg.t32 - 0x0 (330,19) 5 (151, 645) +17 prexp3.t32 - 0x4 (205,5) 5 (726, 415) +18 prexp3.t32 - 0x4 (205,5) 5 (726, 449) +19 prexp3.t32 - 0x4 (205,5) 5 (726, 483) +20 prexp3.t32 - 0x4 (205,5) 5 (726, 517) +21 prexp3.t32 - 0x4 (205,5) 5 (726, 551) +22 prexp3.t32 - 0x4 (205,5) 5 (726, 585) diff --git a/docs/re/captures/pause-tutorial-screen-layout.txt b/docs/re/captures/pause-tutorial-screen-layout.txt new file mode 100644 index 0000000..6930e32 --- /dev/null +++ b/docs/re/captures/pause-tutorial-screen-layout.txt @@ -0,0 +1,13 @@ +11 elements +# element parent kind pivot kf placement +0 pgp_ttrl_eff11.t32 - 0x0 (34,33) 7 (603, 121) → (493, 121) +1 pgp_ttrl_eff12.t32 - 0x0 (34,33) 7 (609, 121) → (719, 121) +2 pgp_ttrl_eff10.t32 - 0x0 (204,60) 5 (436, 94) +3 pgp_ttrl_eff22.t32 - 0x0 (133,130) 5 (480, 225) → (490, 225) +4 pgp_ttrl_eff23.t32 - 0x0 (138,130) 5 (524, 271) → (514, 271) +5 pgp_ttrl_eff21.t32 - 0x0 (214,180) 5 (426, 198) +6 pgp_ttrl_title.rat - 0x0 (101,36) 6 (540, 119) +7 pgp_ttrl_btn10.rat - 0x3002 (43,21) 5 (546, 288) +8 pgp_ttrl_btn11.rat - 0x3002 (86,22) 5 (546, 358) +9 pgp_ttrl_btn12.rat - 0x3002 (110,21) 5 (546, 428) +10 pgp_ttrl_msg.t32 - 0x0 (190,19) 5 (451, 545) diff --git a/docs/re/structures/ui-rat-layout.md b/docs/re/structures/ui-rat-layout.md index b84646c..fdab7e7 100644 --- a/docs/re/structures/ui-rat-layout.md +++ b/docs/re/structures/ui-rat-layout.md @@ -227,3 +227,37 @@ recording them because a static-only reading would have shipped them: [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). + +## It generalises: the ARSENAL screen, and how a multi-component screen composes + +**2026-08-11.** [`examples/screen_layout.rs`](../../../crates/sylpheed-formats/examples/screen_layout.rs) +dumps a bundle's declaration table and placement region together. It reproduces +the tutorial pause menu exactly, and it reads the ARSENAL the same way +([capture](../captures/arsenal-main-screen-layout.txt)) — 23 elements that match +the running game: + +- **Eight buttons** `prbtn1…8.rat` at **X=242**, Y `166, 220, 276, 331, 385, 441, + 496, 551` — evenly spaced, and eight is exactly what the screen's own config + declares with `WEAPON_CATEGORIES = 8` (GUN, BEAM, LASER, MPM, ASM, B/R, CANNON, + SPECIAL, the list photographed in the Arsenal). +- **`prexp3.t32` declared seven times** at X=726, 34 px apart (`381 … 585`) — the + DATA SHEET's rows. +- `prexp1.t32` animates `(726,143) → (1286,143)`, sliding off the right edge, with + `prexp1a.t32` **parented to it** (`parent = 13`). +- `prmsg.t32` at (151,645) — the description line along the bottom. + +### Two things this adds + +- **`kind = 0x4` marks a repeated instance.** `prexp3.t32` appears once with + `kind 0x0` and then six more times with `0x4`, each with its own placement — the + game repeats one row template rather than shipping seven sprites. So the element + *name* is not a key; the declaration index is. +- **A screen is composed of named components.** The pause menu is one bundle, but + `GP_HANGAR_ARSENAL.pak` holds **510** RATC entries because its screens are built + from the `.prt` components its config names (`Menu = prmain_scr.prt`, + `Select_Window = prselect_scr.prt`, `Detail_Window_Known = prselect_win3.prt`, …). + Those resolve **under the config's own `PATH` prefix**: `prmain_scr.prt` is not + present, `eng\prmain_scr.prt` is — the same `+` convention the + [movie table](../movie-subtitle-link.md) uses. A sub-component reads identically: + `psselect_win1` declares 4 elements, three of them **parented to element 0**, + which is the parent-index field doing real work on an independent pak.