//! 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; // X/Y are SIGNED: off-screen animation starts are negative (e.g. -516). 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) as i32, be32(&bytes, blk + 16) as i32, be32(&bytes, blk + 20))); } 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]; // A group is an in → hold → out animation, so neither the first nor the // last keyframe is where the element sits: report the MAX-DWELL one // (longest gap to the next keyframe's time). let shown = if p.is_empty() { "—".into() } else if p.len() == 1 { format!("({},{})", p[0].0, p[0].1) } else { let mut best = (0usize, 0u32); for k in 0..p.len() - 1 { let d = p[k + 1].2.saturating_sub(p[k].2); if d > best.1 { best = (k, d) } } let r = p[best.0]; format!( "rest ({},{}) t={}..{} [{}]", r.0, r.1, r.2, p[best.0 + 1].2, p.iter().map(|(x, y, t)| format!("{t}:{x},{y}")).collect::>().join(" ") ) }; 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 ); } }