`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.
79 lines
3.4 KiB
Rust
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}");
|
|
}
|
|
}
|