re: the screen parse generalises to the ARSENAL, plus kind 0x4 and the component model
examples/screen_layout.rs dumps a bundle's declaration table and placement region together. It reproduces the tutorial pause menu exactly and reads the ARSENAL the same way -- 23 elements that match the running game: eight buttons prbtn1..8.rat at X=242 evenly spaced (the config declares WEAPON_CATEGORIES = 8, and eight categories are what the Arsenal shows), prexp3.t32 declared SEVEN times at X=726 34px apart (the DATA SHEET rows), prexp1 sliding (726,143)->(1286,143) with prexp1a parented to it, and prmsg at (151,645). Two additions to the format: kind = 0x4 marks a REPEATED INSTANCE of a sprite -- prexp3.t32 appears once with 0x0 then six times with 0x4, each with its own placement. So the element name is not a key; the declaration index is. A screen composes from named .prt components. GP_HANGAR_ARSENAL.pak has 510 RATC entries because its config names components (Menu = prmain_scr.prt, etc.) and they resolve under the config's own PATH prefix: prmain_scr.prt is absent, eng\prmain_scr.prt is present -- the same <lang>+<member> convention the movie table uses. A sub-component reads identically: psselect_win1 declares 4 elements, three parented to element 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
95
crates/sylpheed-formats/examples/screen_layout.rs
Normal file
95
crates/sylpheed-formats/examples/screen_layout.rs
Normal file
@@ -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 -- <PAK> [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 <pak> [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<(u32, u32)>> = 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
|
||||
);
|
||||
}
|
||||
}
|
||||
25
docs/re/captures/arsenal-main-screen-layout.txt
Normal file
25
docs/re/captures/arsenal-main-screen-layout.txt
Normal file
@@ -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)
|
||||
13
docs/re/captures/pause-tutorial-screen-layout.txt
Normal file
13
docs/re/captures/pause-tutorial-screen-layout.txt
Normal file
@@ -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)
|
||||
@@ -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 `<lang>+<member>` 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.
|
||||
|
||||
Reference in New Issue
Block a user