//! 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}; /// 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: &[(&str, f64, f64, f64, f64, usize)] = &[ // 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} {}", "sprite (disc)", "quad", "max|d|", "runner-up", "draws", "verdict" ); let (mut ok, mut bad) = (0, 0); let mut used: Vec<&str> = Vec::new(); for p in &predicted { let mut ds: Vec<(f64, &(&str, f64, f64, f64, f64, usize))> = 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)" ); } } }