Files
Sylpheed/crates/sylpheed-formats/examples/splash_quad_names.rs
MechaCat02 ccd49ac31f fix(lint): clear the clippy gate across examples and tests
80 findings, not the 14 the first run showed -- clippy stops at the first
failing compilation unit, so `--keep-going` is what makes the list complete.

60 were machine-applicable (`cargo clippy --fix`). The rest by hand:

* five descending `sort_by` -> `sort_by_key(Reverse(..))`
* `chunks_exact(4)` on both sides of four zips, so the compared items stay
  `[u8; 4]` rather than one array against one slice
* three `type` aliases for the census maps and the captured-quad tuple
* `&PathBuf` -> `&Path` in two disc tests
* two range loops; one of them keeps `#[allow(needless_range_loop)]` with the
  reason -- the index is into a map's value, which changes each iteration
* the module doc list in `invert_capture` re-indented to markdown's rules
* `blit`'s eight arguments get `#[allow(too_many_arguments)]`, not a struct

One dead `let off = b.len();` in a `ratc` test is dropped rather than renamed.
The sibling test at :162 is the one that asserts an offset; if this one was
meant to as well, that is a test change and not a lint fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 16:42:41 +02:00

197 lines
7.8 KiB
Rust

//! Name the capture's eight anonymous splash quads, from the disc.
//!
//! `data/splash-quad-timeline.txt` recorded eight distinct NDC rects off the
//! guest's vertex stream and could only call them Q0..Q7 -- a draw capture sees
//! geometry, not names. This predicts each rect from the DECLARED position and
//! the DECODED sprite size in `GP_TITLE.pak` entries 10 and 11, and matches.
//!
//! x_ndc = 2*x/1280 - 1 y_ndc = 1 - 2*y/720 (y down on screen)
//!
//! Instruments: the prediction is ⟨disc⟩ -- `parse_build` + `t8ad::parse`, i.e.
//! OUR reader. The target is ⟨capture⟩ -- the oracle's vertex stream. So an
//! agreement here is not a claim resting on the reader: it VALIDATES the reader
//! on the two splash bundles, which is what `REFUTED.md`'s 🟡 `⟨our-reader⟩`
//! entry on the splash timeline asks for. A disagreement would indict the reader.
//!
//! CONTROL: an assignment is only meaningful if the rects are separable, so this
//! reports the runner-up distance for every sprite. If the best and second-best
//! were comparable, "8/8 matched" would be an artefact of eight similar boxes.
//!
//! cargo run --release -p sylpheed-formats --example splash_quad_names
use std::path::PathBuf;
use sylpheed_formats::{pak::PakArchive, t8ad, ui_layout};
/// One captured quad: (name, x, y, w, h, index).
type CapturedQuad = (&'static str, f64, f64, f64, f64, usize);
/// The oracle. Verbatim from the header of `docs/re/data/splash-quad-timeline.txt`,
/// which read them off the vertex buffer. Quantised to 0.01 by that file.
const CAPTURED: &[CapturedQuad] = &[
// name, x0, x1, y0, y1, draws submitted in
("Q0", -0.520, 0.520, -0.100, 0.080, 111),
("Q1", -0.390, 0.390, 0.350, 0.550, 87),
("Q2", -0.190, 0.190, -0.120, 0.120, 87),
("Q3", -0.300, 0.300, -0.620, -0.250, 87),
("Q4", -0.410, 0.410, 0.320, 0.570, 21),
("Q5", -0.200, 0.210, -0.150, 0.150, 21),
("Q6", -0.320, 0.310, -0.650, -0.220, 21),
("Q7", -0.530, 0.540, -0.130, 0.120, 8),
];
fn main() {
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
// Predict a rect for every sprite-bearing element of the two splash builds.
let mut predicted: Vec<(String, f64, f64, f64, f64)> = Vec::new();
for entry in [10usize, 11] {
let by = ar.read(&ar.entries()[entry]).expect("entry");
let b = ui_layout::parse_build(&by).expect("build");
for el in &b.elements {
let Some(sprite) = el.sprite.as_ref() else {
continue;
};
let Some(&(off, size)) = b.sprites.get(sprite) else {
continue;
};
let Some(img) = t8ad::parse(&by[off..off + size]) else {
continue;
};
// Declared placement is constant across every keyframe on these
// eight elements, so any keyframe gives the same rect; take the first.
let Some(kf) = el.keyframes.first() else {
continue;
};
let (x, y) = (kf.x as f64, kf.y as f64);
let (w, h) = (img.width as f64, img.height as f64);
predicted.push((
sprite.clone(),
2.0 * x / 1280.0 - 1.0,
2.0 * (x + w) / 1280.0 - 1.0,
1.0 - 2.0 * (y + h) / 720.0,
1.0 - 2.0 * y / 720.0,
));
}
}
let dist = |p: &(String, f64, f64, f64, f64), c: &(&str, f64, f64, f64, f64, usize)| {
(p.1 - c.1)
.abs()
.max((p.2 - c.2).abs())
.max((p.3 - c.3).abs())
.max((p.4 - c.4).abs())
};
// The capture rounds to 0.01, so a correct prediction must land inside half
// a step plus the pixel grid: 0.01 NDC is 6.4 px in x, 3.6 px in y.
const TOL: f64 = 0.010;
println!(
"{:<26} {:<4} {:>8} {:>9} {:>7} verdict",
"sprite (disc)", "quad", "max|d|", "runner-up", "draws"
);
let (mut ok, mut bad) = (0, 0);
let mut used: Vec<&str> = Vec::new();
for p in &predicted {
let mut ds: Vec<(f64, &CapturedQuad)> = CAPTURED.iter().map(|c| (dist(p, c), c)).collect();
ds.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let (d0, best) = (ds[0].0, ds[0].1);
let d1 = ds[1].0;
let good = d0 <= TOL && !used.contains(&best.0);
if good {
ok += 1;
used.push(best.0)
} else {
bad += 1
}
println!(
"{:<26} {:<4} {:>8.4} {:>9.4} {:>7} {}",
p.0,
best.0,
d0,
d1,
best.5,
if good { "OK" } else { "MISMATCH" }
);
}
println!(
"\n{ok} named, {bad} unmatched, of {} predicted / {} captured",
predicted.len(),
CAPTURED.len()
);
println!(
"CONTROL: every runner-up above must be far outside the {TOL} tolerance; \
if it is not, the rects are not separable and the naming is luck."
);
}
/// The second question this data answers, appended as its own pass: what IS an
/// `_eff` companion? `splash-quad-timeline.txt` called them "the same rects
/// scaled slightly larger", which is an inference from four rounded NDC numbers.
/// The disc says otherwise, and the difference matters to anyone drawing them.
#[test]
fn eff_is_a_concentric_outset_not_a_scale() {
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
for (entry, pairs) in [
(10usize, &[("palogo_sqex", "palogo_sqex_eff")][..]),
(
11,
&[
("palogo_gamearts", "palogo_gamearts_eff"),
("palogo_seta", "palogo_seta_eff"),
("palogo_anima", "palogo_anima_eff"),
][..],
),
] {
let by = ar.read(&ar.entries()[entry]).expect("entry");
let b = ui_layout::parse_build(&by).expect("build");
for (logo, eff) in pairs {
let g = |n: &str| {
let el = b
.elements
.iter()
.find(|e| e.sprite.as_deref() == Some(&format!("{n}.t32")))
.unwrap();
let &(off, size) = b.sprites.get(&format!("{n}.t32")).unwrap();
let img = t8ad::parse(&by[off..off + size]).unwrap();
let kf = el.keyframes.first().unwrap();
(
kf.x as f64,
kf.y as f64,
img.width as f64,
img.height as f64,
)
};
let (lx, ly, lw, lh) = g(logo);
let (ex, ey, ew, eh) = g(eff);
// concentric?
let dcx = (ex + ew / 2.0) - (lx + lw / 2.0);
let dcy = (ey + eh / 2.0) - (ly + lh / 2.0);
assert!(
dcx.abs() <= 2.0 && dcy.abs() <= 2.0,
"{eff}: centres differ by ({dcx},{dcy}) -- not concentric"
);
// uniform outset, NOT a uniform scale?
let (ox, oy) = ((ew - lw) / 2.0, (eh - lh) / 2.0);
assert!(
(ox - oy).abs() <= 2.0,
"{eff}: outset ({ox},{oy}) is not uniform"
);
assert!(
ox >= 8.0 && ox <= 12.0,
"{eff}: outset {ox} px outside 8..12"
);
// and the scale reading it displaces
let (sx, sy) = (ew / lw, eh / lh);
assert!(
(sx - sy).abs() > 0.05,
"{eff}: scales {sx:.3}/{sy:.3} ARE uniform -- 'scaled larger' would stand"
);
println!(
"{eff:<26} outset {ox:.0}x{oy:.0} px, centre off by \
({dcx:.1},{dcy:.1}), scale {sx:.3}/{sy:.3} (NOT uniform)"
);
}
}
}