style: rustfmt sweep -- 774 hunks across 154 files -> 0
`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:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user