style: rustfmt sweep -- 774 hunks across 154 files -> 0
Some checks failed
CI / Native — linux (pull_request) Successful in 32m7s
CI / WASM — Web (pull_request) Failing after 8m3s
CI / Formatting (pull_request) Successful in 50s

`cargo fmt --all -- --check` has failed on every run in this repository's
history, identically on `main` and on every branch. This is #12.

Mechanical: `cargo fmt --all`, nothing else. 154 files, all `.rs`, no other
extension touched. `cargo check --workspace` exits 0 afterwards, so nothing
changed semantically.

ON THE ORDERING, WHICH WAS THE REAL QUESTION.

HANDOFF-2026-09-06 section 7 warns this is the expensive fix: a whole-tree
reformat before #7 and #8 return "would put a conflict in every file of 861
commits and make the reviews those items exist to enable unreadable".

That is measurably too pessimistic, and it had been reasoned rather than
tested. Measured here by three-way merging a rustfmt'd `main` against both
unmerged branches, file by file:

  file/branch pairs tested   32
  merges CLEAN               28
  merges CONFLICTING          4   (8 conflict hunks total)

    sylpheed-cli/src/main.rs      1 hunk
    sylpheed-export/src/check.rs  1
    sylpheed-export/src/screen.rs 4
    sylpheed-export/src/video.rs  2

All four are against `auto/frame-blend-draw-path` only;
`auto/port-p6-audio` does not conflict anywhere. The earlier framing --
154 dirty files, 133 that cannot collide, 21 that can, the collision set
carrying 147 of 774 hunks (19%) -- reproduces exactly. What it did not say
is that most of the 21 still merge cleanly, because rustfmt's edits and the
branches' edits rarely land on the same lines.

So the cost of sweeping now is 4 files and 8 hunks for one branch, against
a check that is otherwise red forever. Deliberately NOT folded into the
WASM PR: 154 reformatted files would make that one unreviewable.

Closes #12
This commit is contained in:
2026-09-08 20:07:01 +02:00
parent d630bc996f
commit c4c914ff59
154 changed files with 5660 additions and 2106 deletions

View File

@@ -19,22 +19,42 @@ fn main() {
let w = std::env::temp_dir().join(format!("bk_{i}.wav"));
let _ = Command::new("ffmpeg")
.args(["-hide_banner", "-loglevel", "error", "-y", "-i"])
.arg(&p).arg(&w).output();
.arg(&p)
.arg(&w)
.output();
let out = Command::new("ffmpeg")
.args(["-hide_banner", "-v", "info", "-i"])
.arg(&w)
.args(["-af", "astats=measure_perchannel=none", "-f", "null", "-"])
.output().unwrap();
.output()
.unwrap();
let t = String::from_utf8_lossy(&out.stderr).into_owned();
let get = |k: &str| t.lines().find_map(|l| l.split_once(k).map(|x| x.1.trim().to_string()))
.unwrap_or_else(|| "?".into());
let get = |k: &str| {
t.lines()
.find_map(|l| l.split_once(k).map(|x| x.1.trim().to_string()))
.unwrap_or_else(|| "?".into())
};
let dur = Command::new("ffprobe")
.args(["-v","error","-show_entries","format=duration","-of","csv=p=0"])
.arg(&w).output().ok()
.args([
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"csv=p=0",
])
.arg(&w)
.output()
.ok()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_default();
println!(" sub-wave {i}: {:>9} B -> {:>10} s peak {:>10} rms {}",
r.len(), dur, get("Peak level dB:"), get("RMS level dB:"));
println!(
" sub-wave {i}: {:>9} B -> {:>10} s peak {:>10} rms {}",
r.len(),
dur,
get("Peak level dB:"),
get("RMS level dB:")
);
let _ = std::fs::remove_file(&p);
let _ = std::fs::remove_file(&w);
}

View File

@@ -24,13 +24,19 @@ fn main() {
let (mut found, mut matches) = (0usize, Vec::new());
for n in 0..=199u32 {
let name = format!("BGM_{n:03}.slb");
let Ok(riffs) = media::sound_bank_riffs(&src, &name) else { continue };
if riffs.is_empty() { continue }
let Ok(riffs) = media::sound_bank_riffs(&src, &name) else {
continue;
};
if riffs.is_empty() {
continue;
}
found += 1;
let sizes: Vec<usize> = riffs.iter().map(|r| r.len()).collect();
// Compare on the DATA payload the port sums, not on the RIFF wrapper:
// a wrapper differs by header bytes and would hide a real collision.
let near = sizes.iter().any(|s| WANT.iter().any(|w| s.abs_diff(*w) < 4096));
let near = sizes
.iter()
.any(|s| WANT.iter().any(|w| s.abs_diff(*w) < 4096));
if near {
matches.push((name.clone(), sizes.clone()));
}
@@ -39,7 +45,10 @@ fn main() {
for (n, s) in &matches {
println!(" {n:<14} wave sizes {s:?}");
}
println!("\n {} bank(s) carry a wave within 4 KiB of {WANT:?}", matches.len());
println!(
"\n {} bank(s) carry a wave within 4 KiB of {WANT:?}",
matches.len()
);
println!(" Exactly 1 means the census EXCLUDES alternatives and is a real third");
println!(" leg. More than 1 means the byte match does not distinguish BGM_103,");
println!(" and \"three legs\" is two. Zero means this reader cannot see the");

View File

@@ -16,27 +16,39 @@
//!
//! This prints what the differences actually look like, so the reading is judged
//! against the names rather than accepted as plausible.
use sylpheed_formats::{pak, ratc, ui_layout};
use std::collections::BTreeSet;
use sylpheed_formats::{pak, ratc, ui_layout};
fn main() {
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let ar = pak::PakArchive::open(format!("{root}/dat/GP_DIALOG.pak")).expect("GP_DIALOG.pak");
let sets: Vec<Option<BTreeSet<String>>> = ar.entries().iter().map(|e| {
let by = ar.read(e).ok()?;
if !ratc::is_ratc(&by) { return None }
let b = ui_layout::parse_build(&by)?;
Some(b.elements.iter().map(|el| el.name.clone()).collect())
}).collect();
let sets: Vec<Option<BTreeSet<String>>> = ar
.entries()
.iter()
.map(|e| {
let by = ar.read(e).ok()?;
if !ratc::is_ratc(&by) {
return None;
}
let b = ui_layout::parse_build(&by)?;
Some(b.elements.iter().map(|el| el.name.clone()).collect())
})
.collect();
let (mut same, mut diff, mut pairs) = (0usize, 0usize, 0usize);
let mut shown = 0;
for i in (0..sets.len().saturating_sub(1)).step_by(2) {
let (Some(a), Some(b)) = (&sets[i], &sets[i + 1]) else { continue };
let (Some(a), Some(b)) = (&sets[i], &sets[i + 1]) else {
continue;
};
pairs += 1;
if a == b {
same += 1;
println!(" entries {i:>3}/{:<3} IDENTICAL sets, {} element(s)", i + 1, a.len());
println!(
" entries {i:>3}/{:<3} IDENTICAL sets, {} element(s)",
i + 1,
a.len()
);
continue;
}
diff += 1;
@@ -49,19 +61,37 @@ fn main() {
if (10..=15).contains(&i) {
let sp = |x: &BTreeSet<String>| x.iter().filter(|n| n.ends_with(".t32")).count();
let stage = |x: &BTreeSet<String>| -> Vec<String> {
let mut v: Vec<String> = x.iter().filter_map(|n| n.strip_prefix("pzstg")
.and_then(|r| r.get(..2)).map(|s| s.to_string())).collect();
v.sort(); v.dedup(); v
let mut v: Vec<String> = x
.iter()
.filter_map(|n| {
n.strip_prefix("pzstg")
.and_then(|r| r.get(..2))
.map(|s| s.to_string())
})
.collect();
v.sort();
v.dedup();
v
};
println!(" entries {i:>3}/{:<3} stages {:?} vs {:?} sprites {} vs {}",
i + 1, stage(a), stage(b), sp(a), sp(b));
println!(
" entries {i:>3}/{:<3} stages {:?} vs {:?} sprites {} vs {}",
i + 1,
stage(a),
stage(b),
sp(a),
sp(b)
);
}
if shown < 3 {
shown += 1;
let only_a: Vec<_> = a.difference(b).cloned().collect();
let only_b: Vec<_> = b.difference(a).cloned().collect();
println!(" entries {i:>3}/{:<3} differ: {} only-in-first, {} only-in-second",
i + 1, only_a.len(), only_b.len());
println!(
" entries {i:>3}/{:<3} differ: {} only-in-first, {} only-in-second",
i + 1,
only_a.len(),
only_b.len()
);
println!(" first : {:?}", &only_a[..only_a.len().min(4)]);
println!(" second : {:?}", &only_b[..only_b.len().min(4)]);
}
@@ -71,12 +101,17 @@ fn main() {
// different dialogs and the whole adjacent-pairing premise is wrong -- which
// is a stronger statement than "the language reading is untested".
let btns = |s: &Option<BTreeSet<String>>| -> usize {
s.as_ref().map_or(0, |x| x.iter().filter(|n| n.contains("btn")).count())
s.as_ref()
.map_or(0, |x| x.iter().filter(|n| n.contains("btn")).count())
};
let mut mismatched = 0;
for i in (0..sets.len().saturating_sub(1)).step_by(2) {
if sets[i].is_none() || sets[i + 1].is_none() { continue }
if btns(&sets[i]) != btns(&sets[i + 1]) { mismatched += 1 }
if sets[i].is_none() || sets[i + 1].is_none() {
continue;
}
if btns(&sets[i]) != btns(&sets[i + 1]) {
mismatched += 1
}
}
println!("\n adjacent pairs whose BUTTON COUNTS differ: {mismatched}");
println!(" A language pair cannot. Every one of these is two different dialogs.");

View File

@@ -26,40 +26,66 @@ fn main() {
// with a different reader, because its whole content is an absence.
const WANT: [i32; 4] = [259, 329, 399, 469];
const TOL: i32 = 6;
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat")).expect("dat/")
.flatten().map(|e| e.path())
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak")).collect();
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
.expect("dat/")
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
.collect();
paks.sort();
let (mut hits, mut scanned) = (0usize, 0usize);
for path in &paks {
let Ok(ar) = pak::PakArchive::open(path) else { continue };
let arch = path.file_name().unwrap().to_string_lossy().to_string();
for (i, e) in ar.entries().iter().enumerate() {
let Ok(by) = ar.read(e) else { continue };
if !ratc::is_ratc(&by) { continue }
let Some(b) = ui_layout::parse_build(&by) else { continue };
scanned += 1;
// Any button-shaped record, not just `pcbtn`: a rival need not share the
// naming convention, and restricting by name would answer a narrower
// question than the one asked.
let mut rows: Vec<(String, i32)> = b.elements.iter()
.filter(|el| el.name.contains("btn"))
.filter_map(|el| el.rest().map(|r| (el.name.clone(), r.y)))
.collect();
if rows.is_empty() { continue }
rows.sort_by(|a, b| a.1.cmp(&b.1));
let ys: Vec<i32> = rows.iter().map(|r| r.1).collect();
let gaps: Vec<i32> = ys.windows(2).map(|w| w[1] - w[0]).collect();
if rows.len() == 4 && ys.iter().zip(WANT.iter()).all(|(a, b)| (a - b).abs() <= TOL) {
hits += 1;
println!(" {arch} entry {i:>2} {} record(s): {}", rows.len(),
rows.iter().map(|r| r.0.as_str()).collect::<Vec<_>>().join(" "));
println!(" rows {ys:?} gaps {gaps:?}");
let Ok(ar) = pak::PakArchive::open(path) else {
continue;
};
let arch = path.file_name().unwrap().to_string_lossy().to_string();
for (i, e) in ar.entries().iter().enumerate() {
let Ok(by) = ar.read(e) else { continue };
if !ratc::is_ratc(&by) {
continue;
}
let Some(b) = ui_layout::parse_build(&by) else {
continue;
};
scanned += 1;
// Any button-shaped record, not just `pcbtn`: a rival need not share the
// naming convention, and restricting by name would answer a narrower
// question than the one asked.
let mut rows: Vec<(String, i32)> = b
.elements
.iter()
.filter(|el| el.name.contains("btn"))
.filter_map(|el| el.rest().map(|r| (el.name.clone(), r.y)))
.collect();
if rows.is_empty() {
continue;
}
rows.sort_by(|a, b| a.1.cmp(&b.1));
let ys: Vec<i32> = rows.iter().map(|r| r.1).collect();
let gaps: Vec<i32> = ys.windows(2).map(|w| w[1] - w[0]).collect();
if rows.len() == 4
&& ys
.iter()
.zip(WANT.iter())
.all(|(a, b)| (a - b).abs() <= TOL)
{
hits += 1;
println!(
" {arch} entry {i:>2} {} record(s): {}",
rows.len(),
rows.iter()
.map(|r| r.0.as_str())
.collect::<Vec<_>>()
.join(" ")
);
println!(" rows {ys:?} gaps {gaps:?}");
}
}
}
}
println!("\n {scanned} build(s) scanned across {} pak(s); {hits} match the",
paks.len());
println!(
"\n {scanned} build(s) scanned across {} pak(s); {hits} match the",
paks.len()
);
println!(" DIFFICULTY row signature within +/-{TOL} px.");
println!(" Expected: exactly 2 -- the EN/JP pair. More means a RIVAL exists and");
println!(" the geometric identification is not unique; fewer means this reader");

View File

@@ -13,26 +13,43 @@ fn main() {
let e = &ar.entries()[4]; // entry 4 = the English title
let bundle = ar.read(e).expect("read");
let b = ui_layout::parse_build(&bundle).expect("parse");
println!("build has {} elements, {} records", b.elements.len(), b.records.len());
println!(
"build has {} elements, {} records",
b.elements.len(),
b.records.len()
);
let mut names: Vec<&String> = b.records.keys().collect();
names.sort();
println!("records: {names:?}");
for el in &b.elements {
if !el.name.starts_with("ptloop") { continue; }
if !el.name.starts_with("ptloop") {
continue;
}
let r = el.rest();
println!("\nPARENT {} sprite={:?} -> rest scale {:?} rot {:?}", el.name, el.sprite,
r.map(|r| (r.scale_x, r.scale_y)), r.map(|r| r.rotation_deg));
println!(
"\nPARENT {} sprite={:?} -> rest scale {:?} rot {:?}",
el.name,
el.sprite,
r.map(|r| (r.scale_x, r.scale_y)),
r.map(|r| r.rotation_deg)
);
if let Some(&(off, size)) = b.records.get(&el.name) {
match ui_layout::parse_build(&bundle[off..off + size]) {
Some(leaf) => {
println!(" LEAF {} parses: {} element(s)", el.name, leaf.elements.len());
println!(
" LEAF {} parses: {} element(s)",
el.name,
leaf.elements.len()
);
for le in &leaf.elements {
let lr = le.rest();
println!(" {:<20} rest scale {:?} rot {:?} pos {:?}",
println!(
" {:<20} rest scale {:?} rot {:?} pos {:?}",
le.name,
lr.map(|r| (r.scale_x, r.scale_y)),
lr.map(|r| r.rotation_deg),
lr.map(|r| (r.x, r.y)));
lr.map(|r| (r.x, r.y))
);
for k in &le.keyframes {
println!(" t={:?} scale=({},{}) rot={} pos=({},{}) fade={:#010x} u4={} u8={}",
k.time, k.scale_x, k.scale_y, k.rotation_deg, k.x, k.y,

View File

@@ -26,15 +26,14 @@
//! and adds the one they could not run: the same two, restricted to the records
//! **this port actually animates**. A disc-wide 0.00 % violation rate says
//! nothing about my six screens if all six sit in the exceptional tail.
use sylpheed_formats::{pak, ratc, ui_layout};
use std::collections::BTreeMap;
use sylpheed_formats::{pak, ratc, ui_layout};
/// The records the port animates: the plate glow, the five menu focus records,
/// and the title's two sweeps. Named rather than pattern-matched, because the
/// point is to check the ones that are shipped, not the ones that match a glob.
const SHIPPED: &[&str] = &[
"ptbtn00f", "ptbtn01f", "ptbtn02f", "ptbtn03f", "ptbtn04f", "ptbtn05f",
"ptloop01", "ptloop02",
"ptbtn00f", "ptbtn01f", "ptbtn02f", "ptbtn03f", "ptbtn04f", "ptbtn05f", "ptloop01", "ptloop02",
];
/// Which header word to read as the loop length. `0x08` is the decoded one;
@@ -44,14 +43,18 @@ const SHIPPED: &[&str] = &[
static mut OFFSET: usize = 8;
fn main() {
let off: usize = std::env::args().find_map(|a| a.strip_prefix("--offset=")
.and_then(|v| v.parse().ok())).unwrap_or(8);
let off: usize = std::env::args()
.find_map(|a| a.strip_prefix("--offset=").and_then(|v| v.parse().ok()))
.unwrap_or(8);
unsafe { OFFSET = off };
println!(" reading the loop length at header +0x{off:02x}");
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat")).expect("dat/")
.flatten().map(|e| e.path())
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak")).collect();
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
.expect("dat/")
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
.collect();
paks.sort();
let (mut total, mut exact, mut holds, mut violations) = (0usize, 0usize, 0usize, 0usize);
@@ -59,14 +62,24 @@ fn main() {
let mut shipped: BTreeMap<String, (i64, i64)> = BTreeMap::new();
for p in &paks {
let Ok(ar) = pak::PakArchive::open(p) else { continue };
let Ok(ar) = pak::PakArchive::open(p) else {
continue;
};
for e in ar.entries() {
let Ok(by) = ar.read(e) else { continue };
if !ratc::is_ratc(&by) { continue }
let Some(b) = ui_layout::parse_build(&by) else { continue };
if !ratc::is_ratc(&by) {
continue;
}
let Some(b) = ui_layout::parse_build(&by) else {
continue;
};
for (rn, &(o, s)) in &b.records {
if o + off + 4 > by.len() || o + s > by.len() { continue }
if &by[o..o + 4] != b"RATC" { continue }
if o + off + 4 > by.len() || o + s > by.len() {
continue;
}
if &by[o..o + 4] != b"RATC" {
continue;
}
// 🔴 THE FALSIFIER IS RUN AT NEIGHBOURING OFFSETS TOO. The
// Decoder's struct-layout control showed that a homogeneous
// repeated table type-checks at every field boundary, so an
@@ -77,15 +90,28 @@ fn main() {
// "confirmation" without asking whether it discriminates the
// OFFSET or merely the file.
let len = u32::from_be_bytes(by[o + off..o + off + 4].try_into().unwrap()) as i64;
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else { continue };
let maxt = lb.elements.iter()
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else {
continue;
};
let maxt = lb
.elements
.iter()
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time))
.max().unwrap_or(0) as i64;
if maxt == 0 { continue } // static: declares no cycle at all
.max()
.unwrap_or(0) as i64;
if maxt == 0 {
continue;
} // static: declares no cycle at all
total += 1;
let slack = len - maxt;
*slack_hist.entry(slack).or_default() += 1;
if slack == 0 { exact += 1 } else if slack > 0 { holds += 1 } else { violations += 1 }
if slack == 0 {
exact += 1
} else if slack > 0 {
holds += 1
} else {
violations += 1
}
let stem = rn.trim_end_matches(".rat");
if SHIPPED.contains(&stem) {
shipped.entry(stem.to_string()).or_insert((len, maxt));
@@ -95,36 +121,68 @@ fn main() {
}
println!("disc-wide, records with timed keyframes: {total}");
println!(" +08 == max t (exact) : {exact:5} {:5.1} %", pc(exact, total));
println!(" +08 > max t (a hold) : {holds:5} {:5.1} %", pc(holds, total));
println!(" +08 < max t <- FALSIFIER : {violations:5} {:5.2} %", pc(violations, total));
println!(
" +08 == max t (exact) : {exact:5} {:5.1} %",
pc(exact, total)
);
println!(
" +08 > max t (a hold) : {holds:5} {:5.1} %",
pc(holds, total)
);
println!(
" +08 < max t <- FALSIFIER : {violations:5} {:5.2} %",
pc(violations, total)
);
println!("\nslack distribution, most common first:");
let mut h: Vec<_> = slack_hist.iter().collect();
h.sort_by_key(|&(_, n)| std::cmp::Reverse(*n));
for (k, n) in h.iter().take(8) { println!(" slack {k:>6} : {n}"); }
for (k, n) in h.iter().take(8) {
println!(" slack {k:>6} : {n}");
}
println!("\nthe records THIS PORT animates:");
println!(" {:<12} {:>6} {:>7} {:>7}", "record", "+0x08", "max t", "slack");
println!(
" {:<12} {:>6} {:>7} {:>7}",
"record", "+0x08", "max t", "slack"
);
let (mut ship_exact, mut ship_hold, mut ship_bad) = (0, 0, 0);
for (n, (len, maxt)) in &shipped {
let slack = len - maxt;
match slack { 0 => ship_exact += 1, s if s > 0 => ship_hold += 1, _ => ship_bad += 1 }
println!(" {n:<12} {len:>6} {maxt:>7} {slack:>7}{}",
if slack < 0 { " 🔴 FALSIFIED" } else { "" });
match slack {
0 => ship_exact += 1,
s if s > 0 => ship_hold += 1,
_ => ship_bad += 1,
}
println!(
" {n:<12} {len:>6} {maxt:>7} {slack:>7}{}",
if slack < 0 { " 🔴 FALSIFIED" } else { "" }
);
}
println!("\n shipped: {ship_exact} exact, {ship_hold} hold, {ship_bad} falsified");
if shipped.len() < SHIPPED.len() {
let missing: Vec<_> = SHIPPED.iter().filter(|s| !shipped.contains_key(**s)).collect();
let missing: Vec<_> = SHIPPED
.iter()
.filter(|s| !shipped.contains_key(**s))
.collect();
println!(" ⚠️ not found on the disc: {missing:?} -- a name the port ships and");
println!(" this control never checked is worse than a violation it found.");
}
println!("\n verdict: {}", if ship_bad > 0 {
"🔴 the reading fails on a record the port animates -- do NOT adopt"
} else if ship_hold == 0 {
"⚠️ every shipped record is exact, so this port cannot tell loop length\n from max keyframe time -- adopting 120 would change nothing here"
} else {
"✅ falsifier clean and the field is non-trivial ON THE SHIPPED SET"
});
println!(
"\n verdict: {}",
if ship_bad > 0 {
"🔴 the reading fails on a record the port animates -- do NOT adopt"
} else if ship_hold == 0 {
"⚠️ every shipped record is exact, so this port cannot tell loop length\n from max keyframe time -- adopting 120 would change nothing here"
} else {
"✅ falsifier clean and the field is non-trivial ON THE SHIPPED SET"
}
);
}
fn pc(n: usize, d: usize) -> f64 { if d == 0 { 0.0 } else { 100.0 * n as f64 / d as f64 } }
fn pc(n: usize, d: usize) -> f64 {
if d == 0 {
0.0
} else {
100.0 * n as f64 / d as f64
}
}

View File

@@ -12,29 +12,47 @@ use sylpheed_formats::{pak, ratc, ui_layout};
fn main() {
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat")).expect("dat/")
.flatten().map(|e| e.path())
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak")).collect();
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
.expect("dat/")
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
.collect();
paks.sort();
let (mut records, mut in_bounds, mut magic, mut parsed, mut timed) = (0, 0, 0, 0, 0);
let (mut untimed, mut all_at_zero) = (0usize, 0usize);
for p in &paks {
let Ok(ar) = pak::PakArchive::open(p) else { continue };
let Ok(ar) = pak::PakArchive::open(p) else {
continue;
};
for e in ar.entries() {
let Ok(by) = ar.read(e) else { continue };
if !ratc::is_ratc(&by) { continue }
let Some(b) = ui_layout::parse_build(&by) else { continue };
if !ratc::is_ratc(&by) {
continue;
}
let Some(b) = ui_layout::parse_build(&by) else {
continue;
};
for (_, &(o, s)) in &b.records {
records += 1;
if o + 12 > by.len() || o + s > by.len() { continue }
if o + 12 > by.len() || o + s > by.len() {
continue;
}
in_bounds += 1;
if &by[o..o + 4] != b"RATC" { continue }
if &by[o..o + 4] != b"RATC" {
continue;
}
magic += 1;
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else { continue };
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else {
continue;
};
parsed += 1;
let maxt = lb.elements.iter()
let maxt = lb
.elements
.iter()
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time))
.max().unwrap_or(0);
.max()
.unwrap_or(0);
// 🔴 `maxt == 0` merges two different populations, and the
// Decoder's cause -- `.max()` returning `Some(0)` -- is only one
// of them. A record with NO timed keyframe has no largest
@@ -43,10 +61,16 @@ fn main() {
// content. Both of us called all 1 530 "the question has no
// meaning"; that is true of one group and an assumption about
// the other.
let any_timed = lb.elements.iter()
let any_timed = lb
.elements
.iter()
.any(|el| el.keyframes.iter().any(|k| k.time.is_some()));
if maxt == 0 {
if any_timed { all_at_zero += 1 } else { untimed += 1 }
if any_timed {
all_at_zero += 1
} else {
untimed += 1
}
continue;
}
timed += 1;
@@ -55,8 +79,10 @@ fn main() {
}
println!(" records declared by parse_build : {records}");
println!(" within the entry's bounds : {in_bounds}");
println!(" carrying the RATC magic : {magic} <- {} dropped here",
in_bounds - magic);
println!(
" carrying the RATC magic : {magic} <- {} dropped here",
in_bounds - magic
);
println!(" parsing as a nested build : {parsed}");
println!(" with a largest keyframe time > 0: {timed}");
println!(" of the {} excluded:", untimed + all_at_zero);

View File

@@ -16,13 +16,25 @@ fn main() {
let (mut total, mut hits, mut multipose) = (0usize, 0usize, 0usize);
for (i, e) in ar.entries().iter().enumerate() {
let Ok(by) = ar.read(e) else { continue };
if !ratc::is_ratc(&by) { continue }
let Some(b) = ui_layout::parse_build(&by) else { continue };
if !ratc::is_ratc(&by) {
continue;
}
let Some(b) = ui_layout::parse_build(&by) else {
continue;
};
for (name, &(o, s)) in &b.records {
if o + 12 > by.len() || o + s > by.len() || &by[o..o + 4] != b"RATC" { continue }
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else { continue };
let maxt = lb.elements.iter()
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time)).max().unwrap_or(0);
if o + 12 > by.len() || o + s > by.len() || &by[o..o + 4] != b"RATC" {
continue;
}
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else {
continue;
};
let maxt = lb
.elements
.iter()
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time))
.max()
.unwrap_or(0);
let len = ui_layout::loop_length_units(&by[o..o + s]).unwrap_or(0);
total += 1;
if maxt == 0 && len > 0 {
@@ -31,10 +43,19 @@ fn main() {
// to move between. All-at-t=0 with a single keyframe per element
// is visually inert however it is played.
let kf: usize = lb.elements.iter().map(|el| el.keyframes.len()).sum();
let multi = lb.elements.iter().filter(|el| el.keyframes.len() > 1).count();
if multi > 0 { multipose += 1 }
println!(" entry {i:>2} {name:<16} {len}-unit cycle, {kf} keyframe(s) \
across {} element(s), {multi} with >1 pose", lb.elements.len());
let multi = lb
.elements
.iter()
.filter(|el| el.keyframes.len() > 1)
.count();
if multi > 0 {
multipose += 1
}
println!(
" entry {i:>2} {name:<16} {len}-unit cycle, {kf} keyframe(s) \
across {} element(s), {multi} with >1 pose",
lb.elements.len()
);
}
}
}

View File

@@ -16,7 +16,11 @@ fn main() {
continue;
};
let riffs = media::voice_region_riffs(&src, s, e).expect("riffs");
println!("{movie}: region [{s}, {e}) = {} bytes, {} chunk(s)", e - s, riffs.len());
println!(
"{movie}: region [{s}, {e}) = {} bytes, {} chunk(s)",
e - s,
riffs.len()
);
for (i, r) in riffs.iter().enumerate() {
let p = std::env::temp_dir().join(format!("vc_{movie}_{i}.xma.wav"));
std::fs::write(&p, r).unwrap();
@@ -28,7 +32,14 @@ fn main() {
.arg(&w)
.output();
let out = Command::new("ffprobe")
.args(["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0"])
.args([
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"csv=p=0",
])
.arg(&w)
.output()
.unwrap();

View File

@@ -169,10 +169,9 @@ pub fn load(authored: &Path) -> Result<Option<Config>> {
#[serde(default)]
voice: BTreeMap<String, serde_json::Value>,
}
let raw = std::fs::read_to_string(&path)
.with_context(|| format!("read {}", path.display()))?;
let file: File = serde_json::from_str(&raw)
.with_context(|| format!("parse {}", path.display()))?;
let raw = std::fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
let file: File =
serde_json::from_str(&raw).with_context(|| format!("parse {}", path.display()))?;
// `_` is the house convention for a prose block explaining the section it
// sits in -- see `authored/flow.json` and `authored/timing.json`. It is
@@ -211,9 +210,12 @@ pub fn load(authored: &Path) -> Result<Option<Config>> {
let size: usize = k
.parse()
.with_context(|| format!("authored/audio.json: voice.stream_weights key {k}"))?;
let w = v.get("weight").and_then(serde_json::Value::as_f64).with_context(|| {
format!("authored/audio.json: voice.stream_weights.{k} has no numeric weight")
})?;
let w = v
.get("weight")
.and_then(serde_json::Value::as_f64)
.with_context(|| {
format!("authored/audio.json: voice.stream_weights.{k} has no numeric weight")
})?;
stream_weights.insert(size, w);
}
}
@@ -282,8 +284,16 @@ fn run_ffmpeg(argv: &[String], out: &Path) -> Result<()> {
// temp name -- it is a hard failure before a byte is written: "Unable to
// choose an output format". `video.rs` already had this shape; this
// function was written from scratch and did not.
let stem = out.file_stem().unwrap_or_default().to_string_lossy().into_owned();
let ext = out.extension().unwrap_or_default().to_string_lossy().into_owned();
let stem = out
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.into_owned();
let ext = out
.extension()
.unwrap_or_default()
.to_string_lossy()
.into_owned();
let partial = out.with_file_name(format!(".{stem}.partial.{ext}"));
let mut argv = argv.to_vec();
let last = argv.len() - 1;
@@ -328,8 +338,12 @@ fn measure(path: &Path) -> (Option<f32>, Option<f32>) {
.find_map(|l| l.split_once(KEY)?.1.trim().parse().ok());
let dur = Command::new("ffprobe")
.args([
"-v", "error", "-show_entries", "format=duration",
"-of", "csv=p=0",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"csv=p=0",
])
.arg(path)
.output()
@@ -368,7 +382,12 @@ pub fn export_cues<S: DiscSource + ?Sized>(
// short read rather than returning a truncated stream, because a
// truncated XMA decodes to plausible-sounding garbage.
let riff = media::se_wave_riff(
source, &cue.bank, offset, cue.packets, cue.channels, cue.rate,
source,
&cue.bank,
offset,
cue.packets,
cue.channels,
cue.rate,
)
.map_err(anyhow::Error::msg)
.with_context(|| format!("assemble the {event} cue"))?;
@@ -376,9 +395,16 @@ pub fn export_cues<S: DiscSource + ?Sized>(
let staged = stage_riff(&dir, event, &riff)?;
let ogg = dir.join(format!("{event}.ogg"));
let argv: Vec<String> = [
"-hide_banner", "-loglevel", "error", "-y",
"-i", &staged.display().to_string(),
"-c:a", "libvorbis", "-q:a", VORBIS_Q,
"-hide_banner",
"-loglevel",
"error",
"-y",
"-i",
&staged.display().to_string(),
"-c:a",
"libvorbis",
"-q:a",
VORBIS_Q,
&ogg.display().to_string(),
]
.iter()
@@ -537,9 +563,15 @@ pub fn export_bgm<S: DiscSource + ?Sized>(
argv.push(format!("{end}"));
}
argv.extend(
["-c:a", "libvorbis", "-q:a", VORBIS_Q, &ogg.display().to_string()]
.iter()
.map(|s| s.to_string()),
[
"-c:a",
"libvorbis",
"-q:a",
VORBIS_Q,
&ogg.display().to_string(),
]
.iter()
.map(|s| s.to_string()),
);
let command = format!("ffmpeg {}", argv.join(" "));
run_ffmpeg(&argv, &ogg)?;
@@ -911,7 +943,10 @@ pub fn export_voice<S: DiscSource + ?Sized>(
// 238-packet late start was found. Applied positionally instead, the
// weights would have gone onto the wrong streams in silence.
let sizes: Vec<usize> = keep.iter().map(|&i| riffs[i].len() - RIFF_HEADER).collect();
let ws: Option<Vec<f64>> = sizes.iter().map(|s| stream_weights.get(s).copied()).collect();
let ws: Option<Vec<f64>> = sizes
.iter()
.map(|s| stream_weights.get(s).copied())
.collect();
match ws {
Some(w) if w.len() == staged.len() => {
// Weights sum to one, so the total is the movie's own and what
@@ -937,9 +972,15 @@ pub fn export_voice<S: DiscSource + ?Sized>(
argv.push("-map".into());
argv.push("[a]".into());
argv.extend(
["-c:a", "libvorbis", "-q:a", VORBIS_Q, &ogg.display().to_string()]
.iter()
.map(|s| s.to_string()),
[
"-c:a",
"libvorbis",
"-q:a",
VORBIS_Q,
&ogg.display().to_string(),
]
.iter()
.map(|s| s.to_string()),
);
let command = format!("ffmpeg {}", argv.join(" "));
run_ffmpeg(&argv, &ogg)?;
@@ -1039,8 +1080,14 @@ pub fn export_voice<S: DiscSource + ?Sized>(
fn probe_channels(path: &Path) -> Option<u8> {
let out = Command::new("ffprobe")
.args([
"-v", "error", "-select_streams", "a:0",
"-show_entries", "stream=channels", "-of", "csv=p=0",
"-v",
"error",
"-select_streams",
"a:0",
"-show_entries",
"stream=channels",
"-of",
"csv=p=0",
])
.arg(path)
.output()
@@ -1056,8 +1103,12 @@ fn probe_channels(path: &Path) -> Option<u8> {
pub fn probe_duration(path: &Path) -> Option<f32> {
let out = Command::new("ffprobe")
.args([
"-v", "error", "-show_entries", "format=duration",
"-of", "csv=p=0",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"csv=p=0",
])
.arg(path)
.output()
@@ -1094,7 +1145,6 @@ fn decoded_chunk(riff: &Path) -> (f32, f32) {
out
}
/// Which channel indices of a decoded stream are not digitally silent.
///
/// `astats` reports per-channel blocks: a `Channel: N` line followed by that

View File

@@ -50,8 +50,9 @@ impl Ctx {
/// name. Anything else means a consumer has to guess, which is the whole thing
/// the format exists to prevent.
fn is_hex32(v: Option<&Value>) -> bool {
v.and_then(Value::as_str)
.is_some_and(|s| s.len() == 10 && s.starts_with("0x") && s[2..].chars().all(|c| c.is_ascii_hexdigit()))
v.and_then(Value::as_str).is_some_and(|s| {
s.len() == 10 && s.starts_with("0x") && s[2..].chars().all(|c| c.is_ascii_hexdigit())
})
}
fn check_pose(c: &mut Ctx, where_: &str, p: &Value) {
@@ -89,12 +90,17 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
// an invented one, so the provenance is mandatory and closed.
match v.get("name_source").and_then(Value::as_str) {
Some("authored") => {
if v.get("name_why").and_then(Value::as_str).is_none_or(str::is_empty) {
if v.get("name_why")
.and_then(Value::as_str)
.is_none_or(str::is_empty)
{
c.err("name_source is `authored` but there is no `name_why`");
}
}
Some("index") => {}
other => c.err(format!("name_source must be `authored` or `index`, got {other:?}")),
other => c.err(format!(
"name_source must be `authored` or `index`, got {other:?}"
)),
}
if let Some(s) = v.get("source") {
for key in ["archive", "entry", "build"] {
@@ -119,9 +125,22 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
let mut indices = Vec::new();
let mut buttons_by_y: Vec<(i64, String)> = Vec::new();
for (i, el) in elements.iter().enumerate() {
let id = el.get("id").and_then(Value::as_str).unwrap_or("<no id>").to_string();
let id = el
.get("id")
.and_then(Value::as_str)
.unwrap_or("<no id>")
.to_string();
let at = format!("element {i} ({id})");
for key in ["index", "id", "declared", "role", "kind_raw", "pivot", "layer_source", "keyframes"] {
for key in [
"index",
"id",
"declared",
"role",
"kind_raw",
"pivot",
"layer_source",
"keyframes",
] {
if el.get(key).is_none() {
c.err(format!("{at}: missing `{key}`"));
}
@@ -131,7 +150,9 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
continue;
};
if idx as usize != i {
c.err(format!("{at}: `index` {idx} does not match its position {i}"));
c.err(format!(
"{at}: `index` {idx} does not match its position {i}"
));
}
indices.push(idx as usize);
@@ -152,15 +173,21 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
match el.get("layer_source").and_then(Value::as_str) {
Some("sprite") | Some("implied") => {
if !is_hex32(el.get("layer")) {
c.err(format!("{at}: layer_source claims a key but `layer` is not one"));
c.err(format!(
"{at}: layer_source claims a key but `layer` is not one"
));
}
}
Some("none") => {
if el.get("layer").is_some() {
c.err(format!("{at}: layer_source `none` but a `layer` is present"));
c.err(format!(
"{at}: layer_source `none` but a `layer` is present"
));
}
}
other => c.err(format!("{at}: layer_source must be sprite/implied/none, got {other:?}")),
other => c.err(format!(
"{at}: layer_source must be sprite/implied/none, got {other:?}"
)),
}
for key in ["sprite", "focus_sprite"] {
@@ -169,7 +196,9 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
if !path.exists() {
c.err(format!("{at}: `{key}` points at {p}, which does not exist"));
} else if let Err(e) = image::open(&path) {
c.err(format!("{at}: `{key}` {p} does not decode as an image: {e}"));
c.err(format!(
"{at}: `{key}` {p} does not decode as an image: {e}"
));
}
}
}
@@ -177,7 +206,11 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
if let Some(r) = el.get("rest") {
check_pose(&mut c, &at, r);
if role == "button" {
if let Some(y) = r.get("pos").and_then(Value::as_array).and_then(|a| a[1].as_i64()) {
if let Some(y) = r
.get("pos")
.and_then(Value::as_array)
.and_then(|a| a[1].as_i64())
{
buttons_by_y.push((y, id.clone()));
}
}
@@ -214,7 +247,10 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
// either drops an element or draws one twice.
match v.get("paint_order").and_then(Value::as_array) {
Some(po) => {
let mut got: Vec<usize> = po.iter().filter_map(|x| x.as_u64().map(|v| v as usize)).collect();
let mut got: Vec<usize> = po
.iter()
.filter_map(|x| x.as_u64().map(|v| v as usize))
.collect();
if got.len() != po.len() {
c.err("`paint_order` holds a non-integer");
}
@@ -256,7 +292,10 @@ fn check_screen(root: &Path, rel: &str, errors: &mut Vec<String>) -> Result<()>
pub fn run(root: &Path) -> Result<usize> {
let manifest_path = root.join("manifest.json");
if !manifest_path.exists() {
bail!("{} has no manifest.json — is that an export tree?", root.display());
bail!(
"{} has no manifest.json — is that an export tree?",
root.display()
);
}
let m: Value = serde_json::from_str(&std::fs::read_to_string(&manifest_path)?)?;
let mut errors = Vec::new();
@@ -328,9 +367,13 @@ fn check_audio(root: &Path, m: &Value, errors: &mut Vec<String>) {
errors.push(format!("manifest.json: audio `{name}` has no `{key}`"));
}
}
let Some(file) = a.get("file").and_then(Value::as_str) else { continue };
let Some(file) = a.get("file").and_then(Value::as_str) else {
continue;
};
if !root.join(file).exists() {
errors.push(format!("manifest.json: lists audio {file}, which does not exist"));
errors.push(format!(
"manifest.json: lists audio {file}, which does not exist"
));
continue;
}
match a.get("peak_dbfs").and_then(Value::as_f64) {

View File

@@ -14,8 +14,8 @@
mod audio;
mod check;
mod video;
mod screen;
mod video;
use anyhow::{Context, Result};
use clap::Parser;
@@ -164,8 +164,7 @@ fn load_names(authored: &Path) -> Result<NameMap> {
#[serde(default)]
also_export: AlsoExport,
}
let raw = std::fs::read_to_string(&path)
.with_context(|| format!("read {}", path.display()))?;
let raw = std::fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
Ok(serde_json::from_str::<File>(&raw)
.with_context(|| format!("parse {}", path.display()))?
.archives)
@@ -173,8 +172,7 @@ fn load_names(authored: &Path) -> Result<NameMap> {
/// Extra pak entries to export that `is_build` does not accept, keyed by
/// archive. AUTHORED, and each carries its own `why`.
type AlsoExport =
std::collections::BTreeMap<String, std::collections::BTreeMap<String, NameEntry>>;
type AlsoExport = std::collections::BTreeMap<String, std::collections::BTreeMap<String, NameEntry>>;
fn load_also_export(authored: &Path) -> Result<AlsoExport> {
let path = authored.join("screen_names.json");
@@ -186,8 +184,7 @@ fn load_also_export(authored: &Path) -> Result<AlsoExport> {
#[serde(default)]
also_export: AlsoExport,
}
let raw = std::fs::read_to_string(&path)
.with_context(|| format!("read {}", path.display()))?;
let raw = std::fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
Ok(serde_json::from_str::<File>(&raw)
.with_context(|| format!("parse {}", path.display()))?
.also_export)
@@ -211,9 +208,10 @@ fn load_also_export(authored: &Path) -> Result<AlsoExport> {
/// exactly four bundles and all four are real screens, with zero fragments. In
/// another archive it would not be, which is why this is an allow-list and not
/// a widened predicate.
fn screen_builds(ar: &PakArchive, also: Option<&std::collections::BTreeMap<String, NameEntry>>)
-> Vec<(usize, Vec<u8>)>
{
fn screen_builds(
ar: &PakArchive,
also: Option<&std::collections::BTreeMap<String, NameEntry>>,
) -> Vec<(usize, Vec<u8>)> {
let mut out = Vec::new();
for (i, e) in ar.entries().iter().enumerate() {
let Ok(bytes) = ar.read(e) else { continue };
@@ -234,7 +232,11 @@ fn main() -> Result<()> {
} => run_export(&disc, &out, &authored),
Cmd::Check { out } => {
let n = check::run(&out)?;
println!("{} screen(s) in {} validate against sylpheed.screen/3", n, out.display());
println!(
"{} screen(s) in {} validate against sylpheed.screen/3",
n,
out.display()
);
Ok(())
}
}
@@ -394,12 +396,7 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
Some(cfg) => {
let source = media::DirectorySource::new(disc);
for a in audio::export_cues(&source, out, &cfg.se)? {
println!(
" se {:<8} -> {} ({})",
a.name,
a.file,
describe(&a)
);
println!(" se {:<8} -> {} ({})", a.name, a.file, describe(&a));
audio.push(ManifestAudio::from(a));
}
for (role, spec) in &cfg.bgm {
@@ -461,7 +458,10 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
// there is no `authored/audio.json` -- the voice binding is decoded,
// so the dialogue exports either way and only the choice defaults.
let want = audio_cfg.as_ref().map(|c| c.voice).unwrap_or_default();
let weights = audio_cfg.as_ref().map(|c| c.stream_weights.clone()).unwrap_or_default();
let weights = audio_cfg
.as_ref()
.map(|c| c.stream_weights.clone())
.unwrap_or_default();
match audio::export_voice(&source, out, stem, *len, want, &weights)? {
Some(a) => {
// 🔴 A TOP-LEVEL WARNING, not just a `why` on the entry. The
@@ -517,7 +517,6 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
Ok(())
}
impl From<audio::Exported> for ManifestAudio {
fn from(a: audio::Exported) -> Self {
ManifestAudio {
@@ -560,7 +559,6 @@ fn describe(a: &audio::Exported) -> String {
}
}
/// Delete anything in `video/` this run did not produce.
///
/// `video/` is the one directory the wholesale wipe spares, so that the

View File

@@ -416,7 +416,6 @@ pub fn export_build(
Ok(true)
}
/// The highlighted twin of a sprite name: `ptbtn01.t32` → `ptbtn01f.t32`.
fn highlight_name(sprite: &str) -> Option<String> {
let (stem, ext) = sprite.rsplit_once('.')?;
@@ -460,7 +459,9 @@ pub fn export_build(
written: &mut std::collections::BTreeMap<String, ()>,
missing: &mut Vec<String>|
-> Result<Option<Focus>> {
let Some(&(off, size)) = b.records.get(rec) else { return Ok(None) };
let Some(&(off, size)) = b.records.get(rec) else {
return Ok(None);
};
let Some(leaf) = ui_layout::parse_build(&bundle[off..off + size]) else {
return Ok(None);
};
@@ -468,8 +469,13 @@ pub fn export_build(
for fe in &leaf.elements {
let sp: &str = fe.sprite.as_deref().unwrap_or(&fe.name);
let mut fsprite = None;
if write_from(&sprite_dir, written, sp, &bundle[off..off + size], &leaf.sprites)?
|| write_from(&sprite_dir, written, sp, bundle, &b.sprites)?
if write_from(
&sprite_dir,
written,
sp,
&bundle[off..off + size],
&leaf.sprites,
)? || write_from(&sprite_dir, written, sp, bundle, &b.sprites)?
{
fsprite = Some(sprite_rel(sp));
} else if sp.ends_with(".t32") {
@@ -481,8 +487,11 @@ pub fn export_build(
declared: fe.name.clone(),
sprite: fsprite,
blend_additive: ui_layout::blend_additive_by_name(
&leaf, &bundle[off..off + size], sp)
.or_else(|| ui_layout::blend_additive_by_name(&b, bundle, sp)),
&leaf,
&bundle[off..off + size],
sp,
)
.or_else(|| ui_layout::blend_additive_by_name(&b, bundle, sp)),
pivot: [fe.pivot_x, fe.pivot_y],
rest: Rest {
pos: [r.x, r.y],
@@ -533,9 +542,13 @@ pub fn export_build(
// into the leaf slice) or in the parent's.
let sp: &str = fe.sprite.as_deref().unwrap_or(&fe.name);
let mut fsprite = None;
if write_from(&sprite_dir, &mut written, sp,
&bundle[off..off + size], &leaf.sprites)?
|| write_from(&sprite_dir, &mut written, sp, bundle, &b.sprites)?
if write_from(
&sprite_dir,
&mut written,
sp,
&bundle[off..off + size],
&leaf.sprites,
)? || write_from(&sprite_dir, &mut written, sp, bundle, &b.sprites)?
{
fsprite = Some(sprite_rel(sp));
} else if sp.ends_with(".t32") {
@@ -547,8 +560,11 @@ pub fn export_build(
declared: fe.name.clone(),
sprite: fsprite,
blend_additive: ui_layout::blend_additive_by_name(
&leaf, &bundle[off..off + size], sp)
.or_else(|| ui_layout::blend_additive_by_name(&b, bundle, sp)),
&leaf,
&bundle[off..off + size],
sp,
)
.or_else(|| ui_layout::blend_additive_by_name(&b, bundle, sp)),
pivot: [fe.pivot_x, fe.pivot_y],
rest: Rest {
pos: [r.x, r.y],
@@ -575,7 +591,9 @@ pub fn export_build(
if !fes.is_empty() {
focus = Some(Focus {
record: rec,
loop_length_units: ui_layout::loop_length_units(&bundle[off..off + size]),
loop_length_units: ui_layout::loop_length_units(
&bundle[off..off + size],
),
elements: fes,
});
}
@@ -704,7 +722,6 @@ pub fn export_build(
})
}
/// The longest interval containing no keyframe time, over TOP-LEVEL elements.
///
/// See [`Screen::settle_window`] for why this is the settled instant and why
@@ -750,12 +767,12 @@ fn settle_window(elements: &[Element]) -> Option<[i64; 3]> {
Some([a, b, (a + b) / 2])
}
/// Alpha of one element at instant `t`, under the linear ramp the port uses.
fn alpha_at(e: &Element, t: i64) -> u8 {
let ks = &e.keyframes;
let a = |k: &Keyframe| (u32::from_str_radix(k.fade_argb.trim_start_matches("0x"), 16)
.unwrap_or(0) >> 24) as i64;
let a = |k: &Keyframe| {
(u32::from_str_radix(k.fade_argb.trim_start_matches("0x"), 16).unwrap_or(0) >> 24) as i64
};
let timed: Vec<&Keyframe> = ks.iter().filter(|k| k.t.is_some()).collect();
if timed.is_empty() {
return 0;
@@ -888,7 +905,9 @@ fn forced_backdrop_first(order: Vec<usize>, elements: &[Element], design: [u32;
.filter_map(|k| k.t)
.map(i64::from)
.collect();
let Some(&lo) = span.first() else { return false };
let Some(&lo) = span.first() else {
return false;
};
// 🔴 COVERAGE IS TESTED AT EACH INSTANT, NOT ONCE FROM `size`.
// Declared size alone is not what the element draws: scale is a
// percent per axis and it animates. `pbafc.prm` is the disc's own
@@ -921,9 +940,10 @@ fn forced_backdrop_first(order: Vec<usize>, elements: &[Element], design: [u32;
return false;
}
// Every OTHER element must be visible somewhere inside that span.
elements.iter().enumerate().all(|(j, o)| {
j == *i || opaque.iter().any(|&t| alpha_at(o, t) > 0)
})
elements
.iter()
.enumerate()
.all(|(j, o)| j == *i || opaque.iter().any(|&t| alpha_at(o, t) > 0))
})
.map(|(i, _)| i)
.collect();

View File

@@ -97,13 +97,22 @@ const DOWNMIX_51: &str = "pan=stereo|FL=0.4142*FL+0.2929*FC+0.2929*BL
fn channels(src: &Path) -> Result<u32> {
let out = Command::new("ffprobe")
.args([
"-v", "error", "-select_streams", "a:0",
"-show_entries", "stream=channels", "-of", "csv=p=0",
"-v",
"error",
"-select_streams",
"a:0",
"-show_entries",
"stream=channels",
"-of",
"csv=p=0",
])
.arg(src)
.output()
.context("run ffprobe -- is it on PATH?")?;
Ok(String::from_utf8_lossy(&out.stdout).trim().parse().unwrap_or(2))
Ok(String::from_utf8_lossy(&out.stdout)
.trim()
.parse()
.unwrap_or(2))
}
/// Duration and frame rate of a finished transcode, straight from the file.
@@ -119,7 +128,8 @@ fn probe_timebase(out: &Path) -> (f64, f64) {
if stream {
c.args(["-select_streams", "v:0"]);
}
c.args(["-show_entries", entries, "-of", "csv=p=0"]).arg(out);
c.args(["-show_entries", entries, "-of", "csv=p=0"])
.arg(out);
c.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_default()
@@ -136,10 +146,20 @@ fn probe_timebase(out: &Path) -> (f64, f64) {
fn args(src: &Path, out: &Path, channels: u32) -> Vec<String> {
let mut v: Vec<String> = [
"-hide_banner", "-loglevel", "error", "-y",
"-i", &src.display().to_string(),
"-c:v", "libtheora", "-q:v", "8",
"-c:a", "libvorbis", "-q:a", "5",
"-hide_banner",
"-loglevel",
"error",
"-y",
"-i",
&src.display().to_string(),
"-c:v",
"libtheora",
"-q:v",
"8",
"-c:a",
"libvorbis",
"-q:a",
"5",
]
.iter()
.map(|s| s.to_string())
@@ -268,7 +288,10 @@ pub fn transcode(disc: &Path, out: &Path, m: &Movie) -> Result<Option<Transcoded
// updates it. The explanation would be correct in the source and absent on
// disc, which is the same shape as every other documented-but-unexercised
// thing this port has had to find the hard way.
if std::fs::read_to_string(&stamp).map(|s| s != want).unwrap_or(true) {
if std::fs::read_to_string(&stamp)
.map(|s| s != want)
.unwrap_or(true)
{
std::fs::write(&stamp, &want)?;
}
let (duration_s, fps) = probe_timebase(&ogv);