Each UI sprite <name>.t32 ships with a <name>.rat companion, and that companion is the layout record: big-endian u32, a 1280x720 design space at 0x18, and a placement block of scaleX/scaleY/tint/X/Y. Animated elements repeat the block with a trailing time field (keyframes); an 'opt ' tag carries a length-prefixed link to the focused-state record. Verified in that order, records first: across the four pause buttons exactly one field varies, stepping 268/338/408/478 at a constant 70px pitch with X fixed at 226; the three items visible in a screenshot of the running game measure 346.0/415.5/485.5, mapping onto those numbers under one constant offset to within 0.5px. The tutorial build's records are outright framebuffer coordinates, and compositing its sprites there reproduces the screenshot pixel-accurately. Also records two mistakes the A/B caught, since a static-only reading would have shipped both: texture width does not identify a language (Japanese fits English widths), and the in-mission and tutorial pause menus are separate sprite sets rather than one set re-packed into slots. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
56 lines
2.4 KiB
Rust
56 lines
2.4 KiB
Rust
//! Scratch analysis: inventory one UI screen's pak — every entry, and for RATC
|
|
//! entries the children they bundle. Optionally write an entry's decompressed
|
|
//! bytes out for hex inspection.
|
|
//!
|
|
//! Run: cargo run -p sylpheed-formats --example ui_screen -- <PAK> [--dump 0xHASH out.bin]
|
|
|
|
use sylpheed_formats::pak::{self, PakArchive};
|
|
use sylpheed_formats::ratc;
|
|
|
|
fn main() {
|
|
let mut args = std::env::args().skip(1);
|
|
let pak = args.next().expect("usage: ui_screen <pak> [--dump 0xHASH out.bin]");
|
|
let arc = PakArchive::open(&pak).expect("open pak");
|
|
|
|
let rest: Vec<String> = args.collect();
|
|
if rest.first().map(|s| s == "--dump").unwrap_or(false) {
|
|
let h = u32::from_str_radix(rest[1].trim_start_matches("0x"), 16).unwrap();
|
|
let bytes = arc.read_by_hash(h).expect("entry present").expect("decompress");
|
|
std::fs::write(&rest[2], &bytes).unwrap();
|
|
println!("wrote {} bytes to {}", bytes.len(), rest[2]);
|
|
return;
|
|
}
|
|
|
|
if rest.first().map(|s| s == "--child").unwrap_or(false) {
|
|
// --child 0xHASH <child-name> <out>: carve one RATC child out by its
|
|
// listed offset/size, so a 165-byte layout record can be hexdumped alone.
|
|
let h = u32::from_str_radix(rest[1].trim_start_matches("0x"), 16).unwrap();
|
|
let bytes = arc.read_by_hash(h).expect("entry present").expect("decompress");
|
|
let kids = ratc::parse(&bytes).expect("ratc");
|
|
let k = kids.iter().find(|k| k.name == rest[2]).expect("child not found");
|
|
std::fs::write(&rest[3], &bytes[k.offset..k.offset + k.size]).unwrap();
|
|
println!("wrote {} B ({} @ {:#x}) to {}", k.size, k.name, k.offset, rest[3]);
|
|
return;
|
|
}
|
|
|
|
println!("{pak}: {} entries", arc.len());
|
|
for e in arc.entries() {
|
|
let Ok(bytes) = arc.read(e) else {
|
|
println!(" {:08x} <decompress failed>", e.name_hash);
|
|
continue;
|
|
};
|
|
let label = pak::inner_format_label(&bytes);
|
|
println!(" {:08x} {:>9} B {}", e.name_hash, bytes.len(), label);
|
|
if ratc::is_ratc(&bytes) {
|
|
if let Some(kids) = ratc::parse(&bytes) {
|
|
for k in &kids {
|
|
println!(" - {:<40} {:>9} B {}", k.name, k.size, k.kind);
|
|
}
|
|
if kids.is_empty() {
|
|
println!(" (no children found)");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|