A resolve-check on HANDOFF's own rows. Q8 read "SE audio is undecodable
from the disc -- no XACT container exists anywhere". menu-audio-cues.md
retracted exactly that ("### Retracting 'cannot be extracted'") and
locates three cues in Static.slb that decode to PCM: d-pad move 0x1ec0
(4 packets), (B) back 0x0ec0 (2), (A) confirm 0x5d6c0 (6), all mono
48 kHz. The retraction landed in docs/re/ and the page the port reads
kept the superseded text -- the fourth time in this corpus.
Writing the rule down has not worked, so there is a tool now.
handoff_lint.py flags every HANDOFF line making a strong negative claim
that links a doc containing retraction language. First run: found the Q8
row, plus one benign false positive (Q3 links a doc whose retraction is
about a sprite count, not about the tie-break -- checked, and HANDOFF
repeats none of the retracted figures). The lint also caught its own bug
first: it reported existing docs as missing because it joined a guessed
repo root, so it now resolves links relative to the file as markdown does.
Separately, EXTRAS's paint-order risk narrows twice more. Of its 15 tied
pairs only 2 overlap, and of those, ptloop01 x ptloop02 are loop*
animations compose skips by default -- so exactly ONE tie can be drawn:
ptframe3 x ptframe4, overlapping 102x132 px. Against live-extras.png that
contested region correlates +0.9622, better than the whole frame (+0.9440)
and inside the range of regions where order cannot matter (+0.8502 /
+0.9903). Consistent with our order, not proof: correlation cannot see a
swap between locally similar art.
15 -> 2 -> 1 -> consistent is now the whole paint-order risk on the five
screens, and HANDOFF says so.
137 lines
6.6 KiB
Rust
137 lines
6.6 KiB
Rust
//! Does the DERIVED paint order reproduce the ones measured from the game?
|
|
//!
|
|
//! `compose` uses a measured order for the three builds that have one and falls
|
|
//! back to `derived_paint_order` (a sort on each sprite's layer key) everywhere
|
|
//! else. The doc comment claims the derived order "reproduces both measured
|
|
//! orders up to ties" — this checks that claim against all three, and says what
|
|
//! the ties actually cost.
|
|
//!
|
|
//! cargo run -p sylpheed-formats --example paint_order_audit -- <GP_TITLE.pak>
|
|
use sylpheed_formats::{pak, ui_layout};
|
|
|
|
fn measured(names: &[&str]) -> Option<(&'static str, Vec<usize>)> {
|
|
const TITLE: [&str; 24] = [
|
|
"ptlogo1.t32", "ptlogo2.t32", "ptlogo1.t32", "ptlogo2.t32", "ptlogo1.t32",
|
|
"ptlogo2.t32", "pteff01.t32", "ptlogo_tm.t32", "pteff00.prm", "ptbase2.t32",
|
|
"pteff04.t32", "ptloop01.rat", "ptloop02.rat", "pteff02.prm",
|
|
"ptlogo_back2eff1.t32", "ptlogo_back2eff2.t32", "ptlogo_back2eff3.t32",
|
|
"ptlogo_back2eff4.t32", "ptlogo_back2eff5.t32", "ptlogo_back2.t32",
|
|
"ptlogo_back2eff.t32", "ptcopyright.t32", "ptlogoall_eff.t32",
|
|
"ptlogoall_eff2.t32",
|
|
];
|
|
const SPLASH: [&str; 7] = [
|
|
"palogo_eff0.prm", "palogo_gamearts.t32", "palogo_gamearts_eff.t32",
|
|
"palogo_seta.t32", "palogo_seta_eff.t32", "palogo_anima.t32",
|
|
"palogo_anima_eff.t32",
|
|
];
|
|
const MENU: [&str; 16] = [
|
|
"pteff00.prm", "ptbase.t32", "pteff05.t32", "ptloop01.rat",
|
|
"ptloop02.rat", "pteff02.prm", "ptframe1.t32", "ptframe2.t32",
|
|
"pteff10.t32", "pteff12.t32", "ptbtn01.rat", "ptbtn02.rat",
|
|
"ptbtn03.rat", "ptbtn04.rat", "ptbtn05.rat", "ptmsg.t32",
|
|
];
|
|
if names == TITLE {
|
|
return Some(("title", vec![9,11,12,10,13,6,20,19,14,15,18,16,17,0,2,4,7,1,3,5,22,23,21,8]));
|
|
}
|
|
if names == SPLASH { return Some(("splash", vec![0,2,4,6,1,3,5])); }
|
|
if names == MENU {
|
|
return Some(("main menu", vec![1,3,4,2,5,8,9,6,7,15,10,11,12,13,14,0]));
|
|
}
|
|
None
|
|
}
|
|
|
|
fn main() {
|
|
let path = std::env::args().nth(1).expect("usage: paint_order_audit <pak>");
|
|
let ar = pak::PakArchive::open(&path).expect("open pak");
|
|
let mut checked = 0;
|
|
let entries: Vec<_> = ar.entries().to_vec();
|
|
for (i, e) in entries.iter().enumerate() {
|
|
let Ok(bytes) = ar.read(e) else { continue };
|
|
let Some(build) = ui_layout::parse_build(&bytes) else { continue };
|
|
let names: Vec<&str> = build.elements.iter().map(|e| e.name.as_str()).collect();
|
|
// Every build: how exposed is it to tie-breaking? A tie between
|
|
// OVERLAPPING elements is where a derived order can go visibly wrong.
|
|
let keys_all: Vec<u32> = build.elements.iter()
|
|
.map(|e| ui_layout::sprite_layer_key(&build, &bytes, e).unwrap_or(u32::MAX))
|
|
.collect();
|
|
let mut tie_pairs = 0;
|
|
for a in 0..keys_all.len() {
|
|
for b in (a + 1)..keys_all.len() {
|
|
if keys_all[a] == keys_all[b] && keys_all[a] != u32::MAX { tie_pairs += 1; }
|
|
}
|
|
}
|
|
// Of the tied pairs, how many OVERLAP? Only those can paint visibly
|
|
// differently under an arbitrary tie-break. Rect from the declared
|
|
// pivot (= half the sprite for a .t32) at the resting placement.
|
|
let rect = |e: &ui_layout::Element| -> Option<(i32,i32,i32,i32)> {
|
|
let kf = e.rest()?;
|
|
let (w, h) = ((e.pivot_x * 2) as i32, (e.pivot_y * 2) as i32);
|
|
if w == 0 || h == 0 { return None; }
|
|
Some((kf.x, kf.y, w, h))
|
|
};
|
|
let mut tie_overlap = 0;
|
|
for a in 0..keys_all.len() {
|
|
for b in (a + 1)..keys_all.len() {
|
|
if keys_all[a] != keys_all[b] || keys_all[a] == u32::MAX { continue; }
|
|
let (Some(ra), Some(rb)) = (rect(&build.elements[a]), rect(&build.elements[b]))
|
|
else { continue };
|
|
let ox = (ra.0 + ra.2).min(rb.0 + rb.2) - ra.0.max(rb.0);
|
|
let oy = (ra.1 + ra.3).min(rb.1 + rb.3) - ra.1.max(rb.1);
|
|
if ox > 0 && oy > 0 { tie_overlap += 1; }
|
|
}
|
|
}
|
|
let Some((label, want)) = measured(&names) else {
|
|
println!("entry {i:2} (no measured order) {} elements, {tie_pairs} tied pairs, \
|
|
{tie_overlap} of them OVERLAPPING", build.elements.len());
|
|
// Name them: these are the only pairs whose order can show.
|
|
for a in 0..keys_all.len() {
|
|
for b in (a + 1)..keys_all.len() {
|
|
if keys_all[a] != keys_all[b] || keys_all[a] == u32::MAX { continue; }
|
|
let (Some(ra), Some(rb)) = (rect(&build.elements[a]), rect(&build.elements[b]))
|
|
else { continue };
|
|
let ox = (ra.0 + ra.2).min(rb.0 + rb.2) - ra.0.max(rb.0);
|
|
let oy = (ra.1 + ra.3).min(rb.1 + rb.3) - ra.1.max(rb.1);
|
|
if ox > 0 && oy > 0 {
|
|
println!(" overlapping tie: [{a}] {} x [{b}] {} key {} rect {:?} / {:?} overlap {}x{}",
|
|
build.elements[a].name, build.elements[b].name, keys_all[a], ra, rb, ox, oy);
|
|
}
|
|
}
|
|
}
|
|
continue;
|
|
};
|
|
checked += 1;
|
|
let got = ui_layout::derived_paint_order(&build, &bytes);
|
|
let keys: Vec<u32> = build.elements.iter()
|
|
.map(|e| ui_layout::sprite_layer_key(&build, &bytes, e).unwrap_or(u32::MAX))
|
|
.collect();
|
|
let exact = got == want;
|
|
// How many adjacent pairs in the MEASURED order does derived get wrong,
|
|
// and of those, how many are between elements sharing a layer key (a
|
|
// tie the sort cannot resolve) versus a genuine key-order conflict?
|
|
let pos_got: Vec<usize> = {
|
|
let mut p = vec![0; got.len()];
|
|
for (r, &e) in got.iter().enumerate() { p[e] = r; }
|
|
p
|
|
};
|
|
let (mut inv, mut tied) = (0, 0);
|
|
for a in 0..want.len() {
|
|
for b in (a + 1)..want.len() {
|
|
let (x, y) = (want[a], want[b]);
|
|
if pos_got[x] > pos_got[y] {
|
|
inv += 1;
|
|
if keys[x] == keys[y] { tied += 1; }
|
|
}
|
|
}
|
|
}
|
|
println!("entry {i:2} {label:10} {} elements", want.len());
|
|
println!(" derived == measured : {}", if exact { "YES" } else { "NO" });
|
|
println!(" inverted pairs : {inv} (of which same-layer-key ties: {tied})");
|
|
if !exact {
|
|
println!(" measured: {want:?}");
|
|
println!(" derived : {got:?}");
|
|
println!(" keys : {keys:?}");
|
|
}
|
|
}
|
|
println!("\n{checked} build(s) with a measured order were checked");
|
|
}
|