re: crack the UI layout record — the retail UI rebuilds from the disc
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>
This commit is contained in:
55
crates/sylpheed-formats/examples/ui_screen.rs
Normal file
55
crates/sylpheed-formats/examples/ui_screen.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
//! 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)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user