formats: ui_layout — reassemble UI screens from .rat records
Parse each RATC bundle's .rat layout records (sprite name @0x20, placement block [scale,tint,X,Y]) and composite the .t32 sprites back into the screen image. Validated: GP_PAUSE_MENU rebuilds pixel-accurately (btn X=226, Y=268/337/407/478 — the documented 70px pitch); focus records land 42px left/8px up. Compositor confirmed by rendering the pause menu from disc alone. Format doc: docs/re/structures/ui-rat-layout.md (agent RE).
This commit is contained in:
118
crates/sylpheed-formats/examples/rat_inspect.rs
Normal file
118
crates/sylpheed-formats/examples/rat_inspect.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
//! Scratch: confirm the `.rat` layout record offsets against a real screen pak.
|
||||
//! Run: cargo run -p sylpheed-formats --example rat_inspect -- <GP_SCREEN.pak>
|
||||
use sylpheed_formats::{pak::PakArchive, ratc, t8ad};
|
||||
|
||||
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 path = std::env::args().nth(1).expect("pak path");
|
||||
let ar = PakArchive::open(&path).expect("open pak");
|
||||
println!("entries: {}", ar.len());
|
||||
|
||||
let mut decoded: Vec<Vec<u8>> = Vec::new();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let d = ar.read(e).unwrap_or_default();
|
||||
let magic = String::from_utf8_lossy(&d[..4.min(d.len())]).to_string();
|
||||
let (nt, nr) = if ratc::is_ratc(&d) {
|
||||
let k = ratc::parse(&d).unwrap_or_default();
|
||||
(
|
||||
k.iter().filter(|c| c.kind == "T8aD").count(),
|
||||
k.iter()
|
||||
.filter(|c| c.name.to_lowercase().ends_with(".rat"))
|
||||
.count(),
|
||||
)
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
println!(
|
||||
" entry[{:2}] {:>8} B magic={:4} t32={:2} rat={:2}",
|
||||
i,
|
||||
d.len(),
|
||||
magic,
|
||||
nt,
|
||||
nr
|
||||
);
|
||||
decoded.push(d);
|
||||
}
|
||||
// Pick the entry with the most children as "build0".
|
||||
let bi = (0..decoded.len())
|
||||
.filter(|&i| ratc::is_ratc(&decoded[i]))
|
||||
.max_by_key(|&i| ratc::parse(&decoded[i]).map(|k| k.len()).unwrap_or(0))
|
||||
.unwrap();
|
||||
println!("=> inspecting richest build: entry[{}]", bi);
|
||||
let bundle = &decoded[bi];
|
||||
let kids = ratc::parse(bundle).unwrap_or_default();
|
||||
let t32: Vec<_> = kids.iter().filter(|c| c.kind == "T8aD").collect();
|
||||
let rat: Vec<_> = kids
|
||||
.iter()
|
||||
.filter(|c| c.name.to_lowercase().ends_with(".rat"))
|
||||
.collect();
|
||||
println!(
|
||||
"build0: {} children, {} t32 sprites, {} rat records",
|
||||
kids.len(),
|
||||
t32.len(),
|
||||
rat.len()
|
||||
);
|
||||
for c in t32.iter().take(5) {
|
||||
let b = &bundle[c.offset..(c.offset + c.size).min(bundle.len())];
|
||||
if let Some(img) = t8ad::parse(b) {
|
||||
println!(" T8aD {:28} {}x{}", c.name, img.width, img.height);
|
||||
}
|
||||
}
|
||||
println!(" -- .rat records --");
|
||||
for c in rat.iter().take(10) {
|
||||
let r = &bundle[c.offset..(c.offset + c.size).min(bundle.len())];
|
||||
if r.len() < 0x60 {
|
||||
println!(" .rat {:20} (len {}, too short)", c.name, r.len());
|
||||
continue;
|
||||
}
|
||||
let (dw, dh) = (be32(r, 0x18), be32(r, 0x1c));
|
||||
let sprite = {
|
||||
let s = &r[0x20..0x30.min(r.len())];
|
||||
let end = s.iter().position(|&b| b == 0).unwrap_or(s.len());
|
||||
String::from_utf8_lossy(&s[..end]).to_string()
|
||||
};
|
||||
let (pvx, pvy) = (be32(r, 0x50), be32(r, 0x54));
|
||||
// scan for placement block [scaleX=100, scaleY=100, tint, X<dw, Y<dh]
|
||||
let mut placement = None;
|
||||
let mut o = 0x58;
|
||||
while o + 20 <= r.len() {
|
||||
if be32(r, o) == 100 && be32(r, o + 4) == 100 {
|
||||
let (tint, x, y) = (be32(r, o + 8), be32(r, o + 12), be32(r, o + 16));
|
||||
if x < dw && y < dh {
|
||||
placement = Some((o, tint, x, y));
|
||||
break;
|
||||
}
|
||||
}
|
||||
o += 4;
|
||||
}
|
||||
println!(
|
||||
" .rat {:22} {}x{} sprite={:16} pivot=({},{}) place={:x?}",
|
||||
c.name, dw, dh, sprite, pvx, pvy, placement
|
||||
);
|
||||
}
|
||||
|
||||
// End-to-end: composite the build and write it out as a PNG.
|
||||
if let Some(screen) = sylpheed_formats::ui_layout::compose_build(bundle, false) {
|
||||
println!(
|
||||
"compose: {}x{}, drew {} records: {:?}",
|
||||
screen.width,
|
||||
screen.height,
|
||||
screen.drawn.len(),
|
||||
screen.drawn
|
||||
);
|
||||
if let Some(out) = std::env::args().nth(2) {
|
||||
// Dependency-free PPM (P6, RGB — alpha already composited over the backdrop).
|
||||
let mut buf = format!("P6\n{} {}\n255\n", screen.width, screen.height).into_bytes();
|
||||
for px in screen.rgba.chunks_exact(4) {
|
||||
buf.extend_from_slice(&px[..3]);
|
||||
}
|
||||
std::fs::write(&out, buf).unwrap();
|
||||
println!("wrote {out}");
|
||||
}
|
||||
} else {
|
||||
println!("compose: build produced no image");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user