Dumping the remaining Arsenal components turned up placements reading as 4294966780 and 4294966856 -- negative as i32 (-516, -440). X and Y are signed; off-screen animation starts are negative, and read as unsigned a reimplementation would draw them four billion pixels off. The raw region then showed the rest of it. The trailing word is a TIME, and a group is an in -> hold -> out animation: prselect_win1 runs t=4:-516 -> 6:-71 -> 7:81 -> 8:127 -> 23:134 -> 24:134 -> 25:127 -> 27:81 -> 31:-71 -> 1:-516. So NEITHER the first nor the last keyframe is where the element sits -- both are off-screen. The resting position is the max-dwell keyframe, (127,155)..(134,155) here, which is where the weapon-list panel actually appears. This corrects my own note from earlier today, which reported "final=" positions. Static elements were unaffected (the pause menu's buttons have identical keyframes) but animated ones were misread. screen_layout.rs now reports the max-dwell pose plus the full timeline, and both captures are regenerated. Also seen while dumping: kind 0x2, and a .sbo child (scrollbar) parented to the list window. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
112 lines
4.4 KiB
Rust
112 lines
4.4 KiB
Rust
//! 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;
|
|
// X/Y are SIGNED: off-screen animation starts are negative (e.g. -516).
|
|
let mut placements: Vec<Vec<(i32, i32, 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) 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::<Vec<_>>().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
|
|
);
|
|
}
|
|
}
|