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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user