Files
Sylpheed/crates/sylpheed-formats/examples/ratc_child_names.rs
Sylpheed RE agent 0ed33bcd38 re(ui): a RATC child's name is stated, not inferred -- and it was hiding every menu background
`ratc::parse` named each child by scanning backwards for the last printable run
of bytes before its magic. The format states the name explicitly instead, in an
`opt ` block: `"opt " | BE32 len | name | NUL | 3 bytes | magic` -- the same
block `ui_layout::opt_link` already read for a button's focus link.

The scan agrees with it 17 918 times out of 17 942 and is wrong 24 times, every
one the same failure: the 3 trailing payload bytes are themselves printable and
beat the real name. For `pteff05.t32` those bytes are `38 41 58` = `8AX`, so the
full-resolution background of all five menu screens registered under a name no
element declares, resolved to no sprite, and `compose` dropped it through an
early `continue` that -- unlike the two arms above it -- records nothing. The
screen lost its background and `screen render` still reported "all resolved".

`8AX` was never a name. Docs that treated it as one are corrected here.

Disc-wide, and the control is the 17 918 the scan already got right: the `opt `
reading reproduces every one of them. Effect on the five screens is the
signature of the same art at twice the resolution -- mean brightness unmoved,
high-frequency detail x1.15..x1.30 -- which is what the separately-measured
`ui-8ax-fullres-background` result said the game draws.

Also closes a long-standing dangling reference: `pmbase.t32`, recorded as "on
the disc nowhere", is the `GP_STAGE_CLEAR` child the scan called `8AX`. RATC
sibling references now resolve 10 148 of 10 148.

Verified: 114/114 sylpheed-formats unit tests (including two new ones pinning
the `8AX` case byte for byte and the no-block fallback), and every disc-gated
integration suite in sylpheed-formats/sylpheed-cli.
2026-08-29 07:26:56 +00:00

79 lines
3.4 KiB
Rust

//! Is a RATC child's name the printable run before its magic, or the `opt ` block?
//!
//! `ratc::parse` names each child by scanning backwards for the ASCII run that
//! precedes its magic. That is usually right, but it is a HEURISTIC, and the
//! real format states the name explicitly: immediately before each child sits
//!
//! "opt " | BE32 length | name | NUL | 3 bytes | <child magic>
//!
//! -- the same `opt ` block `ui_layout::opt_link` already decodes for a button's
//! focus link. When those 3 trailing bytes happen to be printable the heuristic
//! reads THEM as the name: the title screens' full-resolution background comes
//! out as `8AX` (bytes 38 41 58) instead of `pteff05.t32`, its element then
//! resolves to no sprite, and `compose` drops the screen's background.
//!
//! This compares the two readings for every RATC child in the paks given.
//!
//! cargo run -p sylpheed-formats --example ratc_child_names -- <pak>...
use sylpheed_formats::{pak, ratc};
/// The name stated by the `opt ` block that ends just before `at`.
fn opt_name(buf: &[u8], at: usize) -> Option<String> {
// The block is short; search back a bounded window for the tag.
let lo = at.saturating_sub(128);
let win = &buf[lo..at];
let pos = lo + win.windows(4).rposition(|w| w == b"opt ")?;
let len = u32::from_be_bytes(buf[pos + 4..pos + 8].try_into().ok()?) as usize;
if len == 0 || len > 64 || pos + 8 + len > at {
return None;
}
let s = String::from_utf8_lossy(&buf[pos + 8..pos + 8 + len]).to_string();
// It must be THIS child's block: name, NUL, then a short run to the magic.
if at - (pos + 8 + len) > 8 {
return None;
}
(!s.is_empty() && s.chars().all(|c| c.is_ascii_graphic())).then_some(s)
}
fn main() {
let mut children = 0usize;
let mut with_opt = 0usize;
let mut agree = 0usize;
let mut disagree: Vec<(String, usize, String, String)> = Vec::new();
for path in std::env::args().skip(1) {
let Ok(ar) = pak::PakArchive::open(&path) else { continue };
let short = path.rsplit('/').next().unwrap_or(&path).to_string();
let entries: Vec<_> = ar.entries().to_vec();
for (i, e) in entries.iter().enumerate() {
let Ok(bytes) = ar.read(e) else { continue };
let Some(kids) = ratc::parse(&bytes) else { continue };
for c in &kids {
children += 1;
let Some(o) = opt_name(&bytes, c.offset) else { continue };
with_opt += 1;
if o == c.name {
agree += 1;
} else {
disagree.push((short.clone(), i, c.name.clone(), o));
}
}
}
}
println!("RATC children scanned : {children}");
println!(" with an `opt ` block: {with_opt}");
println!(" scanned name AGREES : {agree}");
println!(" scanned name DIFFERS : {}", disagree.len());
let mut by_pair: std::collections::BTreeMap<(String, String), usize> = Default::default();
for (_, _, scanned, opt) in &disagree {
*by_pair.entry((scanned.clone(), opt.clone())).or_default() += 1;
}
println!("\ndistinct disagreements (scanned -> opt), with counts:");
for ((s, o), n) in &by_pair {
println!(" {s:24} -> {o:24} x{n}");
}
println!("\nfirst 20 occurrences:");
for (p, i, s, o) in disagree.iter().take(20) {
println!(" {p} entry {i:4} {s:20} -> {o}");
}
}