diff --git a/crates/sylpheed-cli/src/main.rs b/crates/sylpheed-cli/src/main.rs index 877ef2df..4d71fd74 100644 --- a/crates/sylpheed-cli/src/main.rs +++ b/crates/sylpheed-cli/src/main.rs @@ -305,7 +305,7 @@ async fn main() -> Result<()> { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::from_default_env() - .add_directive("sylpheed=info".parse().unwrap()) + .add_directive("sylpheed=info".parse().unwrap()), ) .init(); @@ -313,24 +313,33 @@ async fn main() -> Result<()> { match cli.command { Commands::Extract { iso, output } => cmd_extract(&iso, &output).await, - Commands::List { iso, filter } => cmd_list(&iso, filter).await, + Commands::List { iso, filter } => cmd_list(&iso, filter).await, Commands::Sniff { dir, unknown_only } => cmd_sniff(&dir, unknown_only), Commands::Texture { cmd } => match cmd { - TextureCommands::Info { file } => cmd_texture_info(&file), + TextureCommands::Info { file } => cmd_texture_info(&file), TextureCommands::Export { file, output } => cmd_texture_export(&file, &output), }, Commands::Mesh { cmd } => match cmd { MeshCommands::Info { file } => cmd_mesh_info(&file), - MeshCommands::Render { file, output, size, yaw, pitch, dist, row, only } => { - cmd_mesh_render(&file, &output, size, yaw, pitch, dist, row, only) - } + MeshCommands::Render { + file, + output, + size, + yaw, + pitch, + dist, + row, + only, + } => cmd_mesh_render(&file, &output, size, yaw, pitch, dist, row, only), }, Commands::Pak { cmd } => match cmd { - PakCommands::List { pak, idxd_only } => cmd_pak_list(&pak, idxd_only), - PakCommands::Dump { pak, hash } => cmd_pak_dump(&pak, &hash), - PakCommands::Textures { pak, output, verbose } => { - cmd_pak_textures(&pak, &output, verbose) - } + PakCommands::List { pak, idxd_only } => cmd_pak_list(&pak, idxd_only), + PakCommands::Dump { pak, hash } => cmd_pak_dump(&pak, &hash), + PakCommands::Textures { + pak, + output, + verbose, + } => cmd_pak_textures(&pak, &output, verbose), }, Commands::Audio { cmd } => match cmd { AudioCommands::Info { file } => cmd_audio_info(&file), @@ -481,7 +490,9 @@ fn cmd_screen_info(pak: &Path, want: Option, geometry: bool, all: bool) - "{:<3} {:<30} {:>7} {:>8} {:>12} {:>4} {rest}", el.index, el.name, - el.parent.map(|p| p.to_string()).unwrap_or_else(|| "-".into()), + el.parent + .map(|p| p.to_string()) + .unwrap_or_else(|| "-".into()), format!("{:#x}", el.kind), format!("({},{})", el.pivot_x, el.pivot_y), el.keyframes.len(), @@ -623,7 +634,10 @@ fn cmd_screen_render( screen.height ); if !screen.missing.is_empty() { - println!(" sprites that did not resolve/decode: {:?}", screen.missing); + println!( + " sprites that did not resolve/decode: {:?}", + screen.missing + ); } let undrawn: Vec<&str> = b .elements @@ -640,9 +654,7 @@ fn cmd_screen_render( // ── save file ──────────────────────────────────────────────────────────────── fn cmd_save_info(file: &Path, all: bool) -> Result<()> { - use sylpheed_formats::savegame::{ - self, Confidence, DevelopState, FieldKind, GHAD_LAYOUT, - }; + use sylpheed_formats::savegame::{self, Confidence, DevelopState, FieldKind, GHAD_LAYOUT}; let raw = std::fs::read(file).context("read save")?; let save = savegame::parse(&raw).map_err(|e| anyhow::anyhow!("{e}"))?; @@ -692,7 +704,11 @@ fn cmd_save_info(file: &Path, all: bool) -> Result<()> { " {} +{:<3} {:<14} {}", mark(f.confidence), f.offset, - if f.name.is_empty() { "(unnamed)" } else { f.name }, + if f.name.is_empty() { + "(unnamed)" + } else { + f.name + }, value ); if all && !f.note.is_empty() { @@ -701,7 +717,10 @@ fn cmd_save_info(file: &Path, all: bool) -> Result<()> { } let dev = save.develop_state(); - let owned = dev.iter().filter(|d| **d == DevelopState::Developed).count(); + let owned = dev + .iter() + .filter(|d| **d == DevelopState::Developed) + .count(); let ready = dev .iter() .filter(|d| **d == DevelopState::Developable) @@ -745,16 +764,28 @@ fn cmd_audio_info(file: &Path) -> Result<()> { println!("{} {}", "Audio:".green().bold(), file.display()); println!(" Codec : {}", info.codec.label().yellow()); let opt = |v: Option| v.unwrap_or_else(|| "—".dimmed().to_string()); - println!(" Channels : {}", opt(info.channels.map(|c| c.to_string()))); - println!(" Sample rate: {}", opt(info.sample_rate.map(|r| format!("{r} Hz")))); - println!(" Bit depth : {}", opt(info.bits_per_sample.map(|b| format!("{b}-bit")))); + println!( + " Channels : {}", + opt(info.channels.map(|c| c.to_string())) + ); + println!( + " Sample rate: {}", + opt(info.sample_rate.map(|r| format!("{r} Hz"))) + ); + println!( + " Bit depth : {}", + opt(info.bits_per_sample.map(|b| format!("{b}-bit"))) + ); if let Some(d) = info.duration_secs { println!(" Duration : {d:.2} s"); } if let Some(p) = info.xma_packets { println!(" XMA packets: {} (2048 B each)", p.to_string().yellow()); } - println!(" Size : {} bytes", info.size_bytes.to_string().yellow()); + println!( + " Size : {} bytes", + info.size_bytes.to_string().yellow() + ); if info.codec.needs_decoder() { println!( " {} decode not supported (needs an XMA2 decoder + the sound-bank descriptor)", @@ -787,7 +818,7 @@ async fn cmd_extract(iso_path: &Path, output_dir: &Path) -> Result<()> { ProgressStyle::default_bar() .template("{spinner:.cyan} [{bar:40.cyan/blue}] {pos}/{len} {msg}") .unwrap() - .progress_chars("█▉▊▋▌▍▎▏ ") + .progress_chars("█▉▊▋▌▍▎▏ "), ); let stats = reader.extract_all(output_dir).await?; @@ -816,7 +847,11 @@ async fn cmd_extract(iso_path: &Path, output_dir: &Path) -> Result<()> { // ── list ─────────────────────────────────────────────────────────────────── async fn cmd_list(iso_path: &Path, filter: Option) -> Result<()> { - println!("{} {}", "Listing".green().bold(), iso_path.display().to_string().cyan()); + println!( + "{} {}", + "Listing".green().bold(), + iso_path.display().to_string().cyan() + ); let mut reader = sylpheed_formats::xiso::open_iso(iso_path).await?; let files = reader.list_all_files().await?; @@ -861,7 +896,9 @@ fn cmd_sniff(dir: &Path, unknown_only: bool) -> Result<()> { let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new(); for file in &files { - let Ok(bytes) = assets.read(file) else { continue; }; + let Ok(bytes) = assets.read(file) else { + continue; + }; let fmt = identify_format(&bytes); let label = fmt.extension_hint(); *counts.entry(label).or_insert(0) += 1; @@ -882,9 +919,10 @@ fn cmd_sniff(dir: &Path, unknown_only: bool) -> Result<()> { let hex_preview = if label == "bin" && bytes.len() >= 8 { format!( " {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X} {:02X}", - bytes[0], bytes[1], bytes[2], bytes[3], - bytes[4], bytes[5], bytes[6], bytes[7] - ).dimmed().to_string() + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7] + ) + .dimmed() + .to_string() } else { String::new() }; @@ -898,11 +936,7 @@ fn cmd_sniff(dir: &Path, unknown_only: bool) -> Result<()> { let mut summary: Vec<_> = counts.into_iter().collect(); summary.sort_by_key(|&(_, count)| std::cmp::Reverse(count)); for (fmt, count) in summary { - println!( - " {:>6} .{}", - count.to_string().yellow(), - fmt - ); + println!(" {:>6} .{}", count.to_string().yellow(), fmt); } Ok(()) @@ -911,8 +945,7 @@ fn cmd_sniff(dir: &Path, unknown_only: bool) -> Result<()> { // ── texture info ────────────────────────────────────────────────────────── fn cmd_texture_info(file: &Path) -> Result<()> { - let bytes = std::fs::read(file) - .with_context(|| format!("Cannot read {}", file.display()))?; + let bytes = std::fs::read(file).with_context(|| format!("Cannot read {}", file.display()))?; use sylpheed_formats::texture::X360Texture; let tex = X360Texture::from_xpr2(&bytes) @@ -920,16 +953,27 @@ fn cmd_texture_info(file: &Path) -> Result<()> { let d = tex.format.desc(); println!("{} {}", "Texture:".green().bold(), file.display()); - println!(" Resolution : {}×{}", tex.width.to_string().yellow(), tex.height.to_string().yellow()); + println!( + " Resolution : {}×{}", + tex.width.to_string().yellow(), + tex.height.to_string().yellow() + ); println!( " Format : {} ({:?}) · {} bpp, {}", tex.format.gpu_name().yellow(), tex.format, d.bpp, - if d.compressed { "compressed" } else { "uncompressed" }, + if d.compressed { + "compressed" + } else { + "uncompressed" + }, ); println!(" Mip levels : {}", tex.mip_levels); - println!(" Data size : {} bytes", tex.data.len().to_string().yellow()); + println!( + " Data size : {} bytes", + tex.data.len().to_string().yellow() + ); Ok(()) } @@ -937,13 +981,12 @@ fn cmd_texture_info(file: &Path) -> Result<()> { // ── texture export ──────────────────────────────────────────────────────── fn cmd_texture_export(file: &Path, output: &Path) -> Result<()> { - let bytes = std::fs::read(file) - .with_context(|| format!("Cannot read {}", file.display()))?; + let bytes = std::fs::read(file).with_context(|| format!("Cannot read {}", file.display()))?; use sylpheed_formats::texture::X360Texture; let tex = X360Texture::from_xpr2(&bytes)?; - let rgba = decode_to_rgba8(&tex) - .with_context(|| format!("decoding {:?} texture", tex.format))?; + let rgba = + decode_to_rgba8(&tex).with_context(|| format!("decoding {:?} texture", tex.format))?; image::save_buffer( output, @@ -960,7 +1003,11 @@ fn cmd_texture_export(file: &Path, output: &Path) -> Result<()> { tex.width, tex.height, tex.format, - if tex.is_cubemap { " (cubemap face 0)" } else { "" }, + if tex.is_cubemap { + " (cubemap face 0)" + } else { + "" + }, output.display().to_string().cyan(), ); Ok(()) @@ -1066,7 +1113,11 @@ fn cmd_mesh_info(file: &Path) -> Result<()> { .map(|t| edge(t[0], t[1]).max(edge(t[1], t[2])).max(edge(t[0], t[2]))) .collect(); maxedges.sort_by(|a, b| a.partial_cmp(b).unwrap()); - let median = maxedges.get(maxedges.len() / 2).copied().unwrap_or(1.0).max(1e-6); + let median = maxedges + .get(maxedges.len() / 2) + .copied() + .unwrap_or(1.0) + .max(1e-6); let spanning = maxedges.iter().filter(|&&e| e > 6.0 * median).count(); println!( " sub{si}: {nv} v, {} tris | degenerate {degen}, unref-verts {unref}, spanning>6×med {spanning}, idx_max {imax}/{}{}", @@ -1186,10 +1237,16 @@ fn cmd_mesh_render( (lo[2] + hi[2]) * 0.5, ]; let (scale, cell) = if multi { - let extent = (hi[0] - lo[0]).max(hi[1] - lo[1]).max(hi[2] - lo[2]).max(1e-3); + let extent = (hi[0] - lo[0]) + .max(hi[1] - lo[1]) + .max(hi[2] - lo[2]) + .max(1e-3); let col = i % cols; let row = i / cols; - (CELL / extent, [col as f32 * grid_pitch, -(row as f32) * grid_pitch, 0.0]) + ( + CELL / extent, + [col as f32 * grid_pitch, -(row as f32) * grid_pitch, 0.0], + ) } else { (1.0, [0.0, 0.0, 0.0]) }; @@ -1255,7 +1312,9 @@ fn cmd_mesh_render( }; for place in &mine { let f = |i: usize| { - let p = place.map(|pl| pl.apply(sub.positions[i])).unwrap_or(sub.positions[i]); + let p = place + .map(|pl| pl.apply(sub.positions[i])) + .unwrap_or(sub.positions[i]); [ (p[0] - center[0]) * scale * mirror[0] + cell[0], (p[1] - center[1]) * scale * mirror[1] + cell[1], @@ -1646,8 +1705,10 @@ fn emit_t8ad( } match t8ad::parse(slice) { Some(img) => { - let out = - output.join(format!("{hash:08x}_{stem}_{}x{}.png", img.width, img.height)); + let out = output.join(format!( + "{hash:08x}_{stem}_{}x{}.png", + img.width, img.height + )); image::save_buffer( &out, &img.rgba, @@ -1669,8 +1730,7 @@ fn cmd_pak_textures(pak: &Path, output: &Path, verbose: bool) -> Result<()> { use sylpheed_formats::{lsta, ratc, t8ad}; let arc = PakArchive::open(pak).with_context(|| format!("opening {}", pak.display()))?; - std::fs::create_dir_all(output) - .with_context(|| format!("creating {}", output.display()))?; + std::fs::create_dir_all(output).with_context(|| format!("creating {}", output.display()))?; println!( "{} {} → {}", @@ -1738,7 +1798,14 @@ fn cmd_pak_textures(pak: &Path, output: &Path, verbose: bool) -> Result<()> { } else { safe_name(&child.name) }; - emit_t8ad(&payload[child.offset..end], hash, &stem, output, verbose, &mut stats)?; + emit_t8ad( + &payload[child.offset..end], + hash, + &stem, + output, + verbose, + &mut stats, + )?; } } continue; diff --git a/crates/sylpheed-export/examples/bank_chunks.rs b/crates/sylpheed-export/examples/bank_chunks.rs index d17c54ae..d24f687c 100644 --- a/crates/sylpheed-export/examples/bank_chunks.rs +++ b/crates/sylpheed-export/examples/bank_chunks.rs @@ -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); } diff --git a/crates/sylpheed-export/examples/bgm_size_census.rs b/crates/sylpheed-export/examples/bgm_size_census.rs index 878e654d..bb93187f 100644 --- a/crates/sylpheed-export/examples/bgm_size_census.rs +++ b/crates/sylpheed-export/examples/bgm_size_census.rs @@ -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 = 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"); diff --git a/crates/sylpheed-export/examples/dialog_pairs.rs b/crates/sylpheed-export/examples/dialog_pairs.rs index 261e32e2..22d5b056 100644 --- a/crates/sylpheed-export/examples/dialog_pairs.rs +++ b/crates/sylpheed-export/examples/dialog_pairs.rs @@ -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>> = 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>> = 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| x.iter().filter(|n| n.ends_with(".t32")).count(); let stage = |x: &BTreeSet| -> Vec { - let mut v: Vec = 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 = 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>| -> 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."); diff --git a/crates/sylpheed-export/examples/dialog_rows.rs b/crates/sylpheed-export/examples/dialog_rows.rs index a42c5f8b..568847a9 100644 --- a/crates/sylpheed-export/examples/dialog_rows.rs +++ b/crates/sylpheed-export/examples/dialog_rows.rs @@ -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 = rows.iter().map(|r| r.1).collect(); - let gaps: Vec = 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::>().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 = rows.iter().map(|r| r.1).collect(); + let gaps: Vec = 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::>() + .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"); diff --git a/crates/sylpheed-export/examples/rat_leaf.rs b/crates/sylpheed-export/examples/rat_leaf.rs index 908e3132..7bfc7eef 100644 --- a/crates/sylpheed-export/examples/rat_leaf.rs +++ b/crates/sylpheed-export/examples/rat_leaf.rs @@ -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, diff --git a/crates/sylpheed-export/examples/record_loop_control.rs b/crates/sylpheed-export/examples/record_loop_control.rs index bbee584d..87d177d7 100644 --- a/crates/sylpheed-export/examples/record_loop_control.rs +++ b/crates/sylpheed-export/examples/record_loop_control.rs @@ -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 = 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 + } +} diff --git a/crates/sylpheed-export/examples/record_population.rs b/crates/sylpheed-export/examples/record_population.rs index e186b7db..933ffe92 100644 --- a/crates/sylpheed-export/examples/record_population.rs +++ b/crates/sylpheed-export/examples/record_population.rs @@ -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); diff --git a/crates/sylpheed-export/examples/static_with_cycle.rs b/crates/sylpheed-export/examples/static_with_cycle.rs index 0e8b114c..6ca5d394 100644 --- a/crates/sylpheed-export/examples/static_with_cycle.rs +++ b/crates/sylpheed-export/examples/static_with_cycle.rs @@ -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() + ); } } } diff --git a/crates/sylpheed-export/examples/voice_chunks.rs b/crates/sylpheed-export/examples/voice_chunks.rs index 0c630168..998bcb34 100644 --- a/crates/sylpheed-export/examples/voice_chunks.rs +++ b/crates/sylpheed-export/examples/voice_chunks.rs @@ -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(); diff --git a/crates/sylpheed-export/src/audio.rs b/crates/sylpheed-export/src/audio.rs index 598778c8..a8495f85 100644 --- a/crates/sylpheed-export/src/audio.rs +++ b/crates/sylpheed-export/src/audio.rs @@ -169,10 +169,9 @@ pub fn load(authored: &Path) -> Result> { #[serde(default)] voice: BTreeMap, } - 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> { 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, Option) { .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( // 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( let staged = stage_riff(&dir, event, &riff)?; let ogg = dir.join(format!("{event}.ogg")); let argv: Vec = [ - "-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( 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( // 238-packet late start was found. Applied positionally instead, the // weights would have gone onto the wrong streams in silence. let sizes: Vec = keep.iter().map(|&i| riffs[i].len() - RIFF_HEADER).collect(); - let ws: Option> = sizes.iter().map(|s| stream_weights.get(s).copied()).collect(); + let ws: Option> = 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( 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( fn probe_channels(path: &Path) -> Option { 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 { pub fn probe_duration(path: &Path) -> Option { 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 diff --git a/crates/sylpheed-export/src/check.rs b/crates/sylpheed-export/src/check.rs index 82f71b1c..21c9852f 100644 --- a/crates/sylpheed-export/src/check.rs +++ b/crates/sylpheed-export/src/check.rs @@ -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) -> 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) -> 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("").to_string(); + let id = el + .get("id") + .and_then(Value::as_str) + .unwrap_or("") + .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) -> 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) -> 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) -> 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) -> 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) -> 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 = po.iter().filter_map(|x| x.as_u64().map(|v| v as usize)).collect(); + let mut got: Vec = 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) -> Result<()> pub fn run(root: &Path) -> Result { 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) { 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) { diff --git a/crates/sylpheed-export/src/main.rs b/crates/sylpheed-export/src/main.rs index ef982b7e..18357873 100644 --- a/crates/sylpheed-export/src/main.rs +++ b/crates/sylpheed-export/src/main.rs @@ -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 { #[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::(&raw) .with_context(|| format!("parse {}", path.display()))? .archives) @@ -173,8 +172,7 @@ fn load_names(authored: &Path) -> Result { /// 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>; +type AlsoExport = std::collections::BTreeMap>; fn load_also_export(authored: &Path) -> Result { let path = authored.join("screen_names.json"); @@ -186,8 +184,7 @@ fn load_also_export(authored: &Path) -> Result { #[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::(&raw) .with_context(|| format!("parse {}", path.display()))? .also_export) @@ -211,9 +208,10 @@ fn load_also_export(authored: &Path) -> Result { /// 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>) - -> Vec<(usize, Vec)> -{ +fn screen_builds( + ar: &PakArchive, + also: Option<&std::collections::BTreeMap>, +) -> Vec<(usize, Vec)> { 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 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 diff --git a/crates/sylpheed-export/src/screen.rs b/crates/sylpheed-export/src/screen.rs index e505f239..bd85e4d9 100644 --- a/crates/sylpheed-export/src/screen.rs +++ b/crates/sylpheed-export/src/screen.rs @@ -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 { let (stem, ext) = sprite.rsplit_once('.')?; @@ -460,7 +459,9 @@ pub fn export_build( written: &mut std::collections::BTreeMap, missing: &mut Vec| -> Result> { - 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, 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, 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(); diff --git a/crates/sylpheed-export/src/video.rs b/crates/sylpheed-export/src/video.rs index 54751487..44227934 100644 --- a/crates/sylpheed-export/src/video.rs +++ b/crates/sylpheed-export/src/video.rs @@ -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 { 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 { let mut v: Vec = [ - "-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>()); +fn main() { + let disc = std::env::var("SYLPHEED_DISC").unwrap(); + let pak = PakArchive::open(format!("{disc}/dat/GP_HANGAR_ARSENAL.pak")).unwrap(); + let a = gd::load_arsenal(&pak); + for (hp, list) in [ + ("NOSE", &a.nose), + ("ARM1", &a.arm1), + ("ARM2", &a.arm2), + ("ARM3", &a.arm3), + ] { + println!( + "{hp} ({}): {:?}", + list.len(), + list.iter().take(8).collect::>() + ); } } diff --git a/crates/sylpheed-formats/examples/battle.rs b/crates/sylpheed-formats/examples/battle.rs index be624ba7..2286d908 100644 --- a/crates/sylpheed-formats/examples/battle.rs +++ b/crates/sylpheed-formats/examples/battle.rs @@ -1,18 +1,38 @@ use sylpheed_formats::{game_data as gd, PakArchive}; -fn main(){ - let disc=std::env::var("SYLPHEED_DISC").unwrap(); - let pak=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); - let units=gd::load_units(&pak); let vessels=gd::load_vessels(&pak); - let hp=|id:&str|->String{ - units.iter().find(|u|u.id.as_deref()==Some(id)).and_then(|u|u.hp).map(|h|format!("{h:.0}hp")) - .or_else(||vessels.iter().find(|v|v.id.as_deref()==Some(id)).and_then(|v|v.hp).map(|h|format!("{h:.0}HP"))) +fn main() { + let disc = std::env::var("SYLPHEED_DISC").unwrap(); + let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); + let units = gd::load_units(&pak); + let vessels = gd::load_vessels(&pak); + let hp = |id: &str| -> String { + units + .iter() + .find(|u| u.id.as_deref() == Some(id)) + .and_then(|u| u.hp) + .map(|h| format!("{h:.0}hp")) + .or_else(|| { + vessels + .iter() + .find(|v| v.id.as_deref() == Some(id)) + .and_then(|v| v.hp) + .map(|h| format!("{h:.0}HP")) + }) .unwrap_or("·".into()) }; - let rosters=gd::load_unit_rosters(&pak); - for r in rosters.iter().filter(|r|r.stage.is_some()).take(4){ - println!("\n{} — {} combatants:", r.stage.as_deref().unwrap(), r.units.len()); - for u in r.units.iter().filter(|u|u.contains("ADAN")).take(6){ - let short=u.trim_start_matches("UN_").split('_').skip(1).collect::>().join("_"); + let rosters = gd::load_unit_rosters(&pak); + for r in rosters.iter().filter(|r| r.stage.is_some()).take(4) { + println!( + "\n{} — {} combatants:", + r.stage.as_deref().unwrap(), + r.units.len() + ); + for u in r.units.iter().filter(|u| u.contains("ADAN")).take(6) { + let short = u + .trim_start_matches("UN_") + .split('_') + .skip(1) + .collect::>() + .join("_"); println!(" {short:32} {}", hp(u)); } } diff --git a/crates/sylpheed-formats/examples/bench_ships.rs b/crates/sylpheed-formats/examples/bench_ships.rs index dc7c89c5..9ce65b48 100644 --- a/crates/sylpheed-formats/examples/bench_ships.rs +++ b/crates/sylpheed-formats/examples/bench_ships.rs @@ -8,13 +8,22 @@ fn main() { let bytes = std::fs::read(&a[1]).unwrap(); let names = sylpheed_formats::mesh::xbg7_resource_names(&bytes); let ids: Vec = { - let mut v: Vec = names.iter().filter(|n| is_base_part(n)) - .filter_map(|n| ship_id_of(n).map(|s| s.to_string())).collect(); - v.sort(); v.dedup(); v.truncate(5); v + let mut v: Vec = names + .iter() + .filter(|n| is_base_part(n)) + .filter_map(|n| ship_id_of(n).map(|s| s.to_string())) + .collect(); + v.sort(); + v.dedup(); + v.truncate(5); + v }; for id in &ids { - let want: HashSet = names.iter() - .filter(|n| ship_id_of(n) == Some(id.as_str())).cloned().collect(); + let want: HashSet = names + .iter() + .filter(|n| ship_id_of(n) == Some(id.as_str())) + .cloned() + .collect(); let t = Instant::now(); let got = Xbg7Model::models_named(&bytes, &want, &|| false); println!("{id}: {} models in {:?}", got.len(), t.elapsed()); diff --git a/crates/sylpheed-formats/examples/better_home.rs b/crates/sylpheed-formats/examples/better_home.rs index af01e0ff..c73b4675 100644 --- a/crates/sylpheed-formats/examples/better_home.rs +++ b/crates/sylpheed-formats/examples/better_home.rs @@ -28,7 +28,10 @@ fn main() { std::process::exit(1); }; let (vc, ic) = markers[0]; - println!("{name}: {} marker(s), first = {vc} verts / {ic} indices, stride {stride}", markers.len()); + println!( + "{name}: {} marker(s), first = {vc} verts / {ic} indices, stride {stride}", + markers.len() + ); // Where did the decoder put it? let ours = Xbg7Model::stage_models(&bytes) @@ -38,7 +41,10 @@ fn main() { println!("our anchor: {ours:?}"); let starts = debug_vertex_run_starts(&bytes, stride); - println!("{} candidate vertex-run starts for stride {stride}", starts.len()); + println!( + "{} candidate vertex-run starts for stride {stride}", + starts.len() + ); // Score every (start, pad): degenerate triangles, winding against the stored // normals, and whether the run covers the pool exactly. @@ -67,7 +73,10 @@ fn main() { ] }) .collect(); - if pos.iter().any(|p| p.iter().any(|c| !c.is_finite() || c.abs() > 1e6)) { + if pos + .iter() + .any(|p| p.iter().any(|c| !c.is_finite() || c.abs() > 1e6)) + { continue; } let mut degen = 0usize; @@ -82,8 +91,16 @@ fn main() { // stand-in so this stays declaration-agnostic: a consistent mesh // has all faces pointing away from the centroid on a convex-ish // hull. Weak, so degeneracy leads the sort. - let e1 = [pos[y][0] - pos[x][0], pos[y][1] - pos[x][1], pos[y][2] - pos[x][2]]; - let e2 = [pos[z][0] - pos[x][0], pos[z][1] - pos[x][1], pos[z][2] - pos[x][2]]; + let e1 = [ + pos[y][0] - pos[x][0], + pos[y][1] - pos[x][1], + pos[y][2] - pos[x][2], + ]; + let e2 = [ + pos[z][0] - pos[x][0], + pos[z][1] - pos[x][1], + pos[z][2] - pos[x][2], + ]; let f = [ e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], @@ -104,12 +121,19 @@ fn main() { agree += 1; } } - let w = if counted == 0 { 0.0 } else { agree as f32 / counted as f32 }; + let w = if counted == 0 { + 0.0 + } else { + agree as f32 / counted as f32 + }; rows.push((degen, w.max(1.0 - w), vb, pad, max_idx, max_idx + 1 == vc)); } } rows.sort_by(|a, b| a.0.cmp(&b.0).then(b.1.total_cmp(&a.1))); - println!("\n{} in-range candidates; best 12 by (degenerate, winding):", rows.len()); + println!( + "\n{} in-range candidates; best 12 by (degenerate, winding):", + rows.len() + ); for (d, w, vb, pad, mx, cov) in rows.iter().take(12) { let mark = if Some(*vb) == ours { " <-- ours" } else { "" }; println!( diff --git a/crates/sylpheed-formats/examples/bounds_in_descriptor.rs b/crates/sylpheed-formats/examples/bounds_in_descriptor.rs index c3c0bf7b..8b4a25d6 100644 --- a/crates/sylpheed-formats/examples/bounds_in_descriptor.rs +++ b/crates/sylpheed-formats/examples/bounds_in_descriptor.rs @@ -7,8 +7,8 @@ //! those float values. //! //! Usage: bounds_in_descriptor ... -use sylpheed_formats::mesh::{xbg7_descriptor_range, Xbg7Model}; use std::collections::HashSet; +use sylpheed_formats::mesh::{xbg7_descriptor_range, Xbg7Model}; fn main() { let a: Vec = std::env::args().collect(); @@ -24,7 +24,9 @@ fn main() { } } } - let Some((d0, d1)) = xbg7_descriptor_range(&bytes, &m.name) else { continue }; + let Some((d0, d1)) = xbg7_descriptor_range(&bytes, &m.name) else { + continue; + }; println!( "{} descriptor 0x{d0:x}..0x{d1:x} ({} bytes), box lo{:?} hi{:?}", m.name, @@ -34,8 +36,12 @@ fn main() { ); // Where in the descriptor does each bound value appear (±0.01)? let targets: Vec<(&str, f32)> = vec![ - ("lo.x", lo[0]), ("lo.y", lo[1]), ("lo.z", lo[2]), - ("hi.x", hi[0]), ("hi.y", hi[1]), ("hi.z", hi[2]), + ("lo.x", lo[0]), + ("lo.y", lo[1]), + ("lo.z", lo[2]), + ("hi.x", hi[0]), + ("hi.y", hi[1]), + ("hi.z", hi[2]), ]; for (label, v) in targets { let mut at: Vec = Vec::new(); @@ -47,7 +53,10 @@ fn main() { } o += 4; } - println!(" {label:5} {v:10.3} at descriptor offsets {:x?}", &at[..at.len().min(6)]); + println!( + " {label:5} {v:10.3} at descriptor offsets {:x?}", + &at[..at.len().min(6)] + ); } } } diff --git a/crates/sylpheed-formats/examples/campaign.rs b/crates/sylpheed-formats/examples/campaign.rs index 4b4bc460..25747e65 100644 --- a/crates/sylpheed-formats/examples/campaign.rs +++ b/crates/sylpheed-formats/examples/campaign.rs @@ -1,24 +1,39 @@ use sylpheed_formats::{game_data, localization::TextIndex, PakArchive}; -fn main(){ - let disc=std::env::var("SYLPHEED_DISC").unwrap(); - let pak=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); - let text=TextIndex::build(&pak); - let mut stages=game_data::load_stages(&pak); - stages.retain(|s|s.id.starts_with('S') && s.id.len()==3 && s.id[1..].parse::().map(|n|n<=16).unwrap_or(false)); - stages.sort_by(|a,b|a.id.cmp(&b.id)); +fn main() { + let disc = std::env::var("SYLPHEED_DISC").unwrap(); + let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); + let text = TextIndex::build(&pak); + let mut stages = game_data::load_stages(&pak); + stages.retain(|s| { + s.id.starts_with('S') + && s.id.len() == 3 + && s.id[1..].parse::().map(|n| n <= 16).unwrap_or(false) + }); + stages.sort_by(|a, b| a.id.cmp(&b.id)); println!("═══ CAMPAIGN (S01–S16) ═══"); - for s in &stages{ - let obj=text.objectives(&s.id,1); + for s in &stages { + let obj = text.objectives(&s.id, 1); println!("\n{} · {}", s.id, s.location.as_deref().unwrap_or("?")); - for o in obj.iter().take(2){ println!(" ▸ {o}"); } + for o in obj.iter().take(2) { + println!(" ▸ {o}"); + } } // roster with real names - let mut chars=game_data::load_characters(&pak); - chars.retain(|c|c.faction.as_deref()==Some("TCAF") && c.unique==Some(true) && c.faces.len()>=4); + let mut chars = game_data::load_characters(&pak); + chars.retain(|c| { + c.faction.as_deref() == Some("TCAF") && c.unique == Some(true) && c.faces.len() >= 4 + }); println!("\n═══ PRINCIPAL CAST (TCAF, named) ═══"); - for c in &chars{ - let id=c.id.as_deref().unwrap_or(""); - let name=text.character_name(id.trim_start_matches("Character")).or_else(||c.name_key.as_deref().and_then(|k|text.get(k))).unwrap_or("?"); - println!(" {name:12} ({} portraits) [{}]", c.faces.len(), id.trim_start_matches("Character")); + for c in &chars { + let id = c.id.as_deref().unwrap_or(""); + let name = text + .character_name(id.trim_start_matches("Character")) + .or_else(|| c.name_key.as_deref().and_then(|k| text.get(k))) + .unwrap_or("?"); + println!( + " {name:12} ({} portraits) [{}]", + c.faces.len(), + id.trim_start_matches("Character") + ); } } diff --git a/crates/sylpheed-formats/examples/capture_ib_truth.rs b/crates/sylpheed-formats/examples/capture_ib_truth.rs index 19a39253..78d02be8 100644 --- a/crates/sylpheed-formats/examples/capture_ib_truth.rs +++ b/crates/sylpheed-formats/examples/capture_ib_truth.rs @@ -17,9 +17,9 @@ //! //! Usage: //! cargo run --release --example capture_ib_truth -- ... +use std::collections::HashMap; use sylpheed_formats::mesh::{debug_resource_params, xbg7_resource_names, Xbg7Model}; use sylpheed_formats::ship_capture::{parse_capture, CapturedDraw}; -use std::collections::HashMap; fn q(v: f32) -> i64 { (v as f64 * 1e4).round() as i64 @@ -60,7 +60,13 @@ fn main() { let mut o = 0usize; while o + 12 <= bytes.len() { let (x, y, z) = (be(o), be(o + 4), be(o + 8)); - if x.is_finite() && y.is_finite() && z.is_finite() && x.abs() < 1e6 && y.abs() < 1e6 && z.abs() < 1e6 { + if x.is_finite() + && y.is_finite() + && z.is_finite() + && x.abs() < 1e6 + && y.abs() < 1e6 + && z.abs() < 1e6 + { index.entry((q(x), q(y), q(z))).or_default().push(o as u32); } o += 4; @@ -72,7 +78,9 @@ fn main() { for dx in -1..=1i64 { for dy in -1..=1i64 { for dz in -1..=1i64 { - let Some(cands) = index.get(&(k.0 + dx, k.1 + dy, k.2 + dz)) else { continue }; + let Some(cands) = index.get(&(k.0 + dx, k.1 + dy, k.2 + dz)) else { + continue; + }; for &off in cands { for stride in (12..=64).step_by(4) { let ok = (1..4).all(|j| { @@ -96,7 +104,9 @@ fn main() { eprintln!("no draw could be placed in this container"); std::process::exit(1); }; - println!("container load constant: vbase - file_offset = 0x{base_delta:X} ({n} buffers agree)"); + println!( + "container load constant: vbase - file_offset = 0x{base_delta:X} ({n} buffers agree)" + ); // ── Our decoder's view of the same container. let models = Xbg7Model::stage_models(&bytes); @@ -104,14 +114,19 @@ fn main() { for m in &models { for sm in &m.meshes { if let Some(off) = sm.vbuf_offset { - by_off - .entry(off) - .or_default() - .push((m.name.clone(), sm.positions.len(), sm.indices.len())); + by_off.entry(off).or_default().push(( + m.name.clone(), + sm.positions.len(), + sm.indices.len(), + )); } } } - println!("decoded {} resources, {} distinct vertex offsets\n", models.len(), by_off.len()); + println!( + "decoded {} resources, {} distinct vertex offsets\n", + models.len(), + by_off.len() + ); // Declared-but-not-decoded resources, indexed by their first marker's // (vertex, index) counts. A drawn buffer our decoder cannot name is the one @@ -133,13 +148,21 @@ fn main() { } // ── The report: one row per drawn BUFFER, aggregating its index batches. - let mut per_buf: HashMap, u32)> = - HashMap::new(); + let mut per_buf: HashMap< + u32, + ( + usize, + Vec, + u32, + ), + > = HashMap::new(); for (delta, voff, d) in &hits { if *delta != base_delta { continue; } - let e = per_buf.entry(d.vbase).or_insert((*voff, Vec::new(), d.vcount)); + let e = per_buf + .entry(d.vbase) + .or_insert((*voff, Vec::new(), d.vcount)); let ib = d.ib.unwrap(); if !e.1.contains(&ib) { e.1.push(ib); @@ -147,7 +170,8 @@ fn main() { } let (mut pad0, mut pad_small, mut pad_off, mut unnamed) = (0usize, 0usize, 0usize, 0usize); - let (mut cover_exact, mut cover_short, mut idx_equal, mut idx_partial) = (0usize, 0usize, 0usize, 0usize); + let (mut cover_exact, mut cover_short, mut idx_equal, mut idx_partial) = + (0usize, 0usize, 0usize, 0usize); let mut rows: Vec<(usize, String)> = Vec::new(); for (_, (voff, ibs, vcount)) in per_buf.iter() { let batches = ibs.len(); @@ -157,8 +181,11 @@ fn main() { let umax = ibs.iter().map(|i| i.imax).max().unwrap(); let gap = *voff as i64 - hi; // bytes from the end of the index data to the vertex buffer let names = by_off.get(voff); - let dec_idx = names - .and_then(|v| v.iter().find(|(_, p, _)| *p as u32 == *vcount).map(|(_, _, i)| *i as u32)); + let dec_idx = names.and_then(|v| { + v.iter() + .find(|(_, p, _)| *p as u32 == *vcount) + .map(|(_, _, i)| *i as u32) + }); // The decoder's assumption, scored: it expects the whole index buffer at // `vb - 2*idx_count - pad`, pad ≤ 3. let dec_pad = dec_idx.map(|i| *voff as i64 - (i as i64) * 2 - lo); @@ -218,5 +245,7 @@ fn main() { println!( "index extent: our idx_count == sum of captured batches for {idx_equal} buffers, differs for {idx_partial}" ); - println!("vertex-pool coverage by the union of batches: exact {cover_exact} · short {cover_short}"); + println!( + "vertex-pool coverage by the union of batches: exact {cover_exact} · short {cover_short}" + ); } diff --git a/crates/sylpheed-formats/examples/capture_index_bytes.rs b/crates/sylpheed-formats/examples/capture_index_bytes.rs index 13aaf169..879566e3 100644 --- a/crates/sylpheed-formats/examples/capture_index_bytes.rs +++ b/crates/sylpheed-formats/examples/capture_index_bytes.rs @@ -12,9 +12,9 @@ //! //! Usage: //! cargo run --release --example capture_index_bytes -- ... +use std::collections::HashMap; use sylpheed_formats::mesh::Xbg7Model; use sylpheed_formats::ship_capture::{parse_capture, CapturedDraw}; -use std::collections::HashMap; fn q(v: f32) -> i64 { (v as f64 * 1e4).round() as i64 @@ -34,7 +34,10 @@ fn main() { let text = std::fs::read_to_string(log).expect("log"); for d in parse_capture(&text) { let k = d.ib.map(|i| (i.ibase, i.icount)).unwrap_or((0, 0)); - if d.ib.map_or(false, |i| i.head_len > 0) && d.pos.len() >= 4 && seen.insert((log.clone(), d.vbase, k)) { + if d.ib.map_or(false, |i| i.head_len > 0) + && d.pos.len() >= 4 + && seen.insert((log.clone(), d.vbase, k)) + { draws.push(d); } } @@ -48,7 +51,13 @@ fn main() { let mut o = 0usize; while o + 12 <= bytes.len() { let (x, y, z) = (be(o), be(o + 4), be(o + 8)); - if x.is_finite() && y.is_finite() && z.is_finite() && x.abs() < 1e6 && y.abs() < 1e6 && z.abs() < 1e6 { + if x.is_finite() + && y.is_finite() + && z.is_finite() + && x.abs() < 1e6 + && y.abs() < 1e6 + && z.abs() < 1e6 + { index.entry((q(x), q(y), q(z))).or_default().push(o as u32); } o += 4; @@ -60,7 +69,9 @@ fn main() { for dx in -1..=1i64 { for dy in -1..=1i64 { for dz in -1..=1i64 { - let Some(cands) = index.get(&(k.0 + dx, k.1 + dy, k.2 + dz)) else { continue }; + let Some(cands) = index.get(&(k.0 + dx, k.1 + dy, k.2 + dz)) else { + continue; + }; for &off in cands { for stride in (12..=64).step_by(4) { let ok = (1..4).all(|j| { @@ -92,10 +103,11 @@ fn main() { for m in &models { for sm in &m.meshes { if let Some(off) = sm.vbuf_offset { - by_off - .entry(off) - .or_default() - .push((m.name.clone(), sm.positions.len(), sm.indices.clone())); + by_off.entry(off).or_default().push(( + m.name.clone(), + sm.positions.len(), + sm.indices.clone(), + )); } } } @@ -112,7 +124,10 @@ fn main() { continue; } let ibase = d.ib.unwrap().ibase as i64; - ib_start.entry(d.vbase).and_modify(|e| *e = (*e).min(ibase)).or_insert(ibase); + ib_start + .entry(d.vbase) + .and_modify(|e| *e = (*e).min(ibase)) + .or_insert(ibase); } let (mut agree, mut disagree, mut unmatched, mut nooverlap) = (0usize, 0usize, 0usize, 0usize); diff --git a/crates/sylpheed-formats/examples/capture_match.rs b/crates/sylpheed-formats/examples/capture_match.rs index 37ca6a92..6fb42611 100644 --- a/crates/sylpheed-formats/examples/capture_match.rs +++ b/crates/sylpheed-formats/examples/capture_match.rs @@ -1,21 +1,37 @@ //! Identify which stage + ship a capture log came from: match the capture's //! draw vertex counts against every resource (incl. LODs) of every Stage_SNN.xpr. //! cargo run --release --example capture_match -- +use std::collections::{BTreeMap, HashSet}; use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model}; use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog}; -use std::collections::{BTreeMap, HashSet}; fn main() { let a: Vec = std::env::args().collect(); let text = std::fs::read_to_string(&a[1]).unwrap(); let mut draws = parse_capture(&text); - if draws.is_empty() { draws = parse_drawlog(&text); } - let caps: HashSet = draws.iter().map(|d| d.vcount).filter(|v| *v >= 100).collect(); - eprintln!("{} draws, {} distinct vcounts>=100", draws.len(), caps.len()); + if draws.is_empty() { + draws = parse_drawlog(&text); + } + let caps: HashSet = draws + .iter() + .map(|d| d.vcount) + .filter(|v| *v >= 100) + .collect(); + eprintln!( + "{} draws, {} distinct vcounts>=100", + draws.len(), + caps.len() + ); - let mut entries: Vec<_> = std::fs::read_dir(&a[2]).unwrap().filter_map(|e| e.ok()) - .filter(|e| { let n = e.file_name().to_string_lossy().to_string(); n.starts_with("Stage_") && n.ends_with(".xpr") }) - .map(|e| e.path()).collect(); + let mut entries: Vec<_> = std::fs::read_dir(&a[2]) + .unwrap() + .filter_map(|e| e.ok()) + .filter(|e| { + let n = e.file_name().to_string_lossy().to_string(); + n.starts_with("Stage_") && n.ends_with(".xpr") + }) + .map(|e| e.path()) + .collect(); entries.sort(); for path in entries { let bytes = std::fs::read(&path).unwrap(); @@ -28,12 +44,18 @@ fn main() { let vc: usize = m.meshes.iter().map(|s| s.positions.len()).sum(); if vc >= 100 && caps.contains(&(vc as u32)) { let id = m.name.get(..4).unwrap_or("?").to_string(); - hits.entry(id).or_default().push((m.name.clone(), vc as u32)); + hits.entry(id) + .or_default() + .push((m.name.clone(), vc as u32)); } } let total: usize = hits.values().map(|v| v.len()).sum(); if total >= 3 { - println!("== {} : {} matching resources ==", path.file_name().unwrap().to_string_lossy(), total); + println!( + "== {} : {} matching resources ==", + path.file_name().unwrap().to_string_lossy(), + total + ); for (id, v) in &hits { let s: Vec = v.iter().map(|(n, c)| format!("{n}({c})")).collect(); println!(" {id}: {}", s.join(" ")); diff --git a/crates/sylpheed-formats/examples/capture_truth_scan.rs b/crates/sylpheed-formats/examples/capture_truth_scan.rs index 6b62adfe..c75153d8 100644 --- a/crates/sylpheed-formats/examples/capture_truth_scan.rs +++ b/crates/sylpheed-formats/examples/capture_truth_scan.rs @@ -10,8 +10,8 @@ //! `Stage_S01` gave us. //! //! Usage: capture_truth_scan ... -use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog, CapturedDraw}; use std::collections::HashMap; +use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog, CapturedDraw}; fn q(v: f32) -> i64 { (v as f64 * 1e4).round() as i64 @@ -47,7 +47,9 @@ fn main() { let mut placed = 0usize; for f in &files { - let Ok(bytes) = std::fs::read(f) else { continue }; + let Ok(bytes) = std::fs::read(f) else { + continue; + }; // Index quantised position triples. Junk floats (NaN/huge) are skipped, // which prunes most of a texture-heavy container. let be = |at: usize| f32::from_be_bytes(bytes[at..at + 4].try_into().unwrap()); @@ -55,7 +57,12 @@ fn main() { let mut o = 0usize; while o + 12 <= bytes.len() { let (x, y, z) = (be(o), be(o + 4), be(o + 8)); - if x.is_finite() && y.is_finite() && z.is_finite() && x.abs() < 1e6 && y.abs() < 1e6 && z.abs() < 1e6 + if x.is_finite() + && y.is_finite() + && z.is_finite() + && x.abs() < 1e6 + && y.abs() < 1e6 + && z.abs() < 1e6 { index.entry((q(x), q(y), q(z))).or_default().push(o as u32); } @@ -77,9 +84,8 @@ fn main() { let ok = (1..4).all(|j| { let at = off as usize + j * stride; at + 12 <= bytes.len() - && (0..3).all(|c| { - (be(at + c * 4) - d.pos[j][c]).abs() <= 1e-4 - }) + && (0..3) + .all(|c| (be(at + c * 4) - d.pos[j][c]).abs() <= 1e-4) }); if ok { deltas diff --git a/crates/sylpheed-formats/examples/capture_verify.rs b/crates/sylpheed-formats/examples/capture_verify.rs index 6638d77d..a1b909c9 100644 --- a/crates/sylpheed-formats/examples/capture_verify.rs +++ b/crates/sylpheed-formats/examples/capture_verify.rs @@ -71,15 +71,27 @@ fn main() { for p in &base_parts { let mut vcounts = Vec::new(); let mut pos = Vec::new(); - for cand in [p.clone(), format!("{p}_m"), format!("{p}_l"), format!("{p}_d")] { + for cand in [ + p.clone(), + format!("{p}_m"), + format!("{p}_l"), + format!("{p}_d"), + ] { if let Some(m) = models.iter().find(|m| m.name == cand) { - let mp: Vec<[f32; 3]> = - m.meshes.iter().flat_map(|s| s.positions.iter().copied()).collect(); + let mp: Vec<[f32; 3]> = m + .meshes + .iter() + .flat_map(|s| s.positions.iter().copied()) + .collect(); vcounts.push(mp.len() as u32); pos.extend(mp); } } - parts.push(Part { base: p.clone(), vcounts, pos }); + parts.push(Part { + base: p.clone(), + vcounts, + pos, + }); } // Validate a draw against a part (direct or X-mirrored), like ship_capture. @@ -88,7 +100,9 @@ fn main() { return None; } let near = |a: &[f32; 3], b: &[f32; 3]| { - (a[0] - b[0]).abs() <= 1e-2 && (a[1] - b[1]).abs() <= 1e-2 && (a[2] - b[2]).abs() <= 1e-2 + (a[0] - b[0]).abs() <= 1e-2 + && (a[1] - b[1]).abs() <= 1e-2 + && (a[2] - b[2]).abs() <= 1e-2 }; let (mut direct, mut mirror) = (0usize, 0usize); for q in &d.pos { @@ -144,7 +158,11 @@ fn main() { } } } - eprintln!("{log}: {} validated draws, {} bdy_04 refs", labeled.len(), refs.len()); + eprintln!( + "{log}: {} validated draws, {} bdy_04 refs", + labeled.len(), + refs.len() + ); } // Cluster per part (greedy, 8-unit radius), print clusters with ≥3 samples. diff --git a/crates/sylpheed-formats/examples/challenge_map.rs b/crates/sylpheed-formats/examples/challenge_map.rs index eee6fc61..e0a77ce9 100644 --- a/crates/sylpheed-formats/examples/challenge_map.rs +++ b/crates/sylpheed-formats/examples/challenge_map.rs @@ -22,14 +22,19 @@ fn main() { let mut rows = vec![]; for e in arc.entries() { let Ok(b) = arc.read(e) else { continue }; - let Ok(o) = IdxdObject::parse(&b) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { + continue; + }; if o.schema_hash != 0x3c9ae32e { continue; } let t = o.tokens(); let stage = t .iter() - .find_map(|s| s.strip_prefix("EnumUnit_").map(|x| x.trim_end_matches(".tbl").to_string())) + .find_map(|s| { + s.strip_prefix("EnumUnit_") + .map(|x| x.trim_end_matches(".tbl").to_string()) + }) .unwrap_or("?".into()); let bg = o.get_raw("BackGroundID").unwrap_or("?").to_string(); rows.push((stage, bg, t.len())); @@ -47,11 +52,20 @@ fn main() { let mut all_tokens: Vec<(u32, Vec)> = vec![]; for e in ch.entries() { let Ok(b) = ch.read(e) else { continue }; - let Ok(o) = IdxdObject::parse(&b) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { + continue; + }; *by_schema.entry(o.schema_hash).or_default() += 1; - all_tokens.push((o.schema_hash, o.tokens().iter().map(|s| s.to_string()).collect())); + all_tokens.push(( + o.schema_hash, + o.tokens().iter().map(|s| s.to_string()).collect(), + )); } - println!(" {} entries, {} IDXD objects", ch.entries().len(), all_tokens.len()); + println!( + " {} entries, {} IDXD objects", + ch.entries().len(), + all_tokens.len() + ); for (h, n) in &by_schema { println!(" schema {h:08x} x{n}"); } @@ -66,10 +80,16 @@ fn main() { let mut hits: std::collections::BTreeSet = Default::default(); for e in arc.entries() { let Ok(b) = arc.read(e) else { continue }; - let Ok(o) = IdxdObject::parse(&b) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { + continue; + }; for t in o.tokens() { let l = t.to_ascii_lowercase(); - if l.contains("challenge") || t.ends_with("_EX") || t.contains("_EX4") || t.contains("_EX5") { + if l.contains("challenge") + || t.ends_with("_EX") + || t.contains("_EX4") + || t.contains("_EX5") + { hits.insert(format!("{:08x} {t}", o.schema_hash)); } } diff --git a/crates/sylpheed-formats/examples/challenge_screen.rs b/crates/sylpheed-formats/examples/challenge_screen.rs index a861bd6d..264715ed 100644 --- a/crates/sylpheed-formats/examples/challenge_screen.rs +++ b/crates/sylpheed-formats/examples/challenge_screen.rs @@ -44,7 +44,9 @@ fn main() { paks.sort(); for p in &paks { - let Ok(arc) = PakArchive::open(p) else { continue }; + let Ok(arc) = PakArchive::open(p) else { + continue; + }; for (i, e) in arc.entries().iter().enumerate() { let Ok(b) = arc.read(e) else { continue }; if KEYS.iter().filter(|k| find(&b, k.as_bytes())).count() < KEYS.len() { diff --git a/crates/sylpheed-formats/examples/composite_geometry.rs b/crates/sylpheed-formats/examples/composite_geometry.rs index d63e8d0f..76e08e07 100644 --- a/crates/sylpheed-formats/examples/composite_geometry.rs +++ b/crates/sylpheed-formats/examples/composite_geometry.rs @@ -15,7 +15,9 @@ fn main() { files.sort(); let (mut total, mut composite, mut composite_named) = (0usize, 0usize, 0usize); for f in &files { - let Ok(bytes) = std::fs::read(f) else { continue }; + let Ok(bytes) = std::fs::read(f) else { + continue; + }; for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) { total += 1; let has_nodes = !scene_world_nodes(&bytes, &m.name).is_empty(); diff --git a/crates/sylpheed-formats/examples/consensus_check.rs b/crates/sylpheed-formats/examples/consensus_check.rs index 25089c53..101a2f52 100644 --- a/crates/sylpheed-formats/examples/consensus_check.rs +++ b/crates/sylpheed-formats/examples/consensus_check.rs @@ -8,8 +8,8 @@ //! shift chain after the exact-coverage fix. //! //! Usage: consensus_check [--list] -use sylpheed_formats::mesh::Xbg7Model; use std::collections::{BTreeMap, HashMap}; +use sylpheed_formats::mesh::Xbg7Model; fn main() { let dir = std::env::args().nth(1).expect("resource3d dir"); @@ -25,7 +25,9 @@ fn main() { // name -> [(container, verts, tris, span)] let mut seen: BTreeMap> = BTreeMap::new(); for f in &files { - let Ok(bytes) = std::fs::read(f) else { continue }; + let Ok(bytes) = std::fs::read(f) else { + continue; + }; let where_ = f.file_name().unwrap().to_string_lossy().to_string(); for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) { let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]); @@ -47,7 +49,9 @@ fn main() { (hi[1] - lo[1]).round() as i64, (hi[2] - lo[2]).round() as i64, ]; - seen.entry(m.name.clone()).or_default().push((where_.clone(), v, t, span)); + seen.entry(m.name.clone()) + .or_default() + .push((where_.clone(), v, t, span)); } } @@ -81,7 +85,5 @@ fn main() { println!("{r}"); } } - println!( - "{minority} minority decodes across {resources} resources that have a majority" - ); + println!("{minority} minority decodes across {resources} resources that have a majority"); } diff --git a/crates/sylpheed-formats/examples/correlate_capture.rs b/crates/sylpheed-formats/examples/correlate_capture.rs index 871129ea..e9771878 100644 --- a/crates/sylpheed-formats/examples/correlate_capture.rs +++ b/crates/sylpheed-formats/examples/correlate_capture.rs @@ -21,14 +21,14 @@ //! cargo run --release --example correlate_capture -- \ //! xenia_ship_capture.log Stage_S01 e106 bdy_04 --emit +use std::collections::HashSet; +use std::path::Path; use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model}; use sylpheed_formats::ship::{is_base_part, ship_id_of}; use sylpheed_formats::ship_capture::{ correlate, parse_capture, parse_drawlog, serialize_table, PartKey, }; use sylpheed_formats::xiso::open_iso; -use std::collections::HashSet; -use std::path::Path; fn main() { let args: Vec = std::env::args().collect(); @@ -58,10 +58,15 @@ fn main() { // Decode the ship's base parts AND their LOD copies (vcount + leading // positions are the match keys; LODs share the base part's local frame). let bytes = { - let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); rt.block_on(async { let mut r = open_iso(Path::new(&iso)).await.unwrap(); - r.read_file(&format!("hidden/resource3d/{stage}.xpr")).await.unwrap() + r.read_file(&format!("hidden/resource3d/{stage}.xpr")) + .await + .unwrap() }) }; let names = xbg7_resource_names(&bytes); @@ -83,7 +88,12 @@ fn main() { let models = Xbg7Model::models_named(&bytes, &want, &|| false); let positions_of = |name: &str| -> Option> { let m = models.iter().find(|m| m.name == name)?; - Some(m.meshes.iter().flat_map(|s| s.positions.iter().copied()).collect()) + Some( + m.meshes + .iter() + .flat_map(|s| s.positions.iter().copied()) + .collect(), + ) }; // One PartKey per (part, variant-vcount present in the capture) — correlate @@ -94,18 +104,33 @@ fn main() { // variant is the same part in the same local frame. let mut keys: Vec = Vec::new(); for part in &base_parts { - let variants = - [part.clone(), format!("{part}_m"), format!("{part}_l"), format!("{part}_d")]; - let union: Vec<[f32; 3]> = - variants.iter().filter_map(|v| positions_of(v)).flatten().collect(); + let variants = [ + part.clone(), + format!("{part}_m"), + format!("{part}_l"), + format!("{part}_d"), + ]; + let union: Vec<[f32; 3]> = variants + .iter() + .filter_map(|v| positions_of(v)) + .flatten() + .collect(); let mut any = false; for cand in &variants { if let Some(pos) = positions_of(cand) { let vcount = pos.len() as u32; if draws.iter().any(|d| d.vcount == vcount) { - let lod = if cand == part { "full" } else { cand.rsplit('_').next().unwrap_or("?") }; + let lod = if cand == part { + "full" + } else { + cand.rsplit('_').next().unwrap_or("?") + }; eprintln!(" {part:20} try vcount={vcount:6} [{lod}]"); - keys.push(PartKey { part: part.clone(), vcount, ref_pos: union.clone() }); + keys.push(PartKey { + part: part.clone(), + vcount, + ref_pos: union.clone(), + }); any = true; } } @@ -120,9 +145,15 @@ fn main() { return; }; - eprintln!("\nreference = {} → ship-relative placement:", ship.reference); + eprintln!( + "\nreference = {} → ship-relative placement:", + ship.reference + ); for p in &ship.parts { - eprintln!(" {:18} T=[{:9.1}{:9.1}{:9.1}]", p.part, p.t[0], p.t[1], p.t[2]); + eprintln!( + " {:18} T=[{:9.1}{:9.1}{:9.1}]", + p.part, p.t[0], p.t[1], p.t[2] + ); } for part in &base_parts { if !ship.parts.iter().any(|p| &p.part == part) { diff --git a/crates/sylpheed-formats/examples/correlate_frames.rs b/crates/sylpheed-formats/examples/correlate_frames.rs index 4cca47ad..6bf37494 100644 --- a/crates/sylpheed-formats/examples/correlate_frames.rs +++ b/crates/sylpheed-formats/examples/correlate_frames.rs @@ -16,19 +16,23 @@ //! SYLPHEED_ISO=... cargo run --release --example correlate_frames -- \ //! [ref_part_substr] [--min-parts N] +use std::collections::{BTreeMap, HashSet}; +use std::path::Path; use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model}; use sylpheed_formats::ship::{is_base_part, ship_id_of}; use sylpheed_formats::ship_capture::{ correlate, parse_capture, parse_drawlog, segment_frames, PartKey, }; use sylpheed_formats::xiso::open_iso; -use std::collections::{BTreeMap, HashSet}; -use std::path::Path; fn median(mut v: Vec) -> f32 { v.sort_by(|a, b| a.partial_cmp(b).unwrap()); let n = v.len(); - if n % 2 == 1 { v[n / 2] } else { 0.5 * (v[n / 2 - 1] + v[n / 2]) } + if n % 2 == 1 { + v[n / 2] + } else { + 0.5 * (v[n / 2 - 1] + v[n / 2]) + } } fn main() { @@ -54,13 +58,22 @@ fn main() { draws = parse_drawlog(&text); } let frames = segment_frames(&draws); - println!("{} draws → {} camera-consistent blocks", draws.len(), frames.len()); + println!( + "{} draws → {} camera-consistent blocks", + draws.len(), + frames.len() + ); let bytes = { - let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); rt.block_on(async { let mut r = open_iso(Path::new(&iso)).await.unwrap(); - r.read_file(&format!("hidden/resource3d/{stage}.xpr")).await.unwrap() + r.read_file(&format!("hidden/resource3d/{stage}.xpr")) + .await + .unwrap() }) }; let names = xbg7_resource_names(&bytes); @@ -81,7 +94,12 @@ fn main() { let models = Xbg7Model::models_named(&bytes, &want, &|| false); let positions_of = |name: &str| -> Option> { let m = models.iter().find(|m| m.name == name)?; - Some(m.meshes.iter().flat_map(|s| s.positions.iter().copied()).collect()) + Some( + m.meshes + .iter() + .flat_map(|s| s.positions.iter().copied()) + .collect(), + ) }; // part -> [T per frame], and how many frames placed it at all. @@ -91,20 +109,33 @@ fn main() { for (fi, fr) in frames.iter().enumerate() { let mut keys: Vec = Vec::new(); for part in &base_parts { - let variants = - [part.clone(), format!("{part}_m"), format!("{part}_l"), format!("{part}_d")]; - let union: Vec<[f32; 3]> = - variants.iter().filter_map(|v| positions_of(v)).flatten().collect(); + let variants = [ + part.clone(), + format!("{part}_m"), + format!("{part}_l"), + format!("{part}_d"), + ]; + let union: Vec<[f32; 3]> = variants + .iter() + .filter_map(|v| positions_of(v)) + .flatten() + .collect(); for cand in &variants { if let Some(pos) = positions_of(cand) { let vcount = pos.len() as u32; if fr.iter().any(|d| d.vcount == vcount) { - keys.push(PartKey { part: part.clone(), vcount, ref_pos: union.clone() }); + keys.push(PartKey { + part: part.clone(), + vcount, + ref_pos: union.clone(), + }); } } } } - let Some(ship) = correlate(id, fr, &keys, ref_sub) else { continue }; + let Some(ship) = correlate(id, fr, &keys, ref_sub) else { + continue; + }; if ship.parts.len() < min_parts { continue; } @@ -114,7 +145,10 @@ fn main() { // Averaging them together is what makes an otherwise clean result look // like it disagrees by exactly the distance between the two references. if !ship.reference.contains(ref_sub) { - println!(" block {fi:2}: skipped — reference fell back to {}", ship.reference); + println!( + " block {fi:2}: skipped — reference fell back to {}", + ship.reference + ); continue; } used_frames += 1; @@ -173,7 +207,8 @@ fn main() { let spread: Vec = (0..3) .map(|a| { let v: Vec = cl.iter().map(|t| t[a]).collect(); - v.iter().cloned().fold(f32::MIN, f32::max) - v.iter().cloned().fold(f32::MAX, f32::min) + v.iter().cloned().fold(f32::MIN, f32::max) + - v.iter().cloned().fold(f32::MAX, f32::min) }) .collect(); let verdict = if cl.len() < 2 { @@ -187,14 +222,21 @@ fn main() { // (turret aiming, engine gimballing) does not — which is what separates // "the assembler has the rotation wrong" from "the part moved". let all_ms = rots.get(part).cloned().unwrap_or_default(); - let ms: Vec<[[f32; 3]; 3]> = - cl_idx.iter().filter_map(|&i| all_ms.get(i).copied()).collect(); + let ms: Vec<[[f32; 3]; 3]> = cl_idx + .iter() + .filter_map(|&i| all_ms.get(i).copied()) + .collect(); // Keep a rotation from INSIDE the cluster: the first sample overall can // belong to another instance, and diffing static against that reads as a // rotation error that is really an instance mix-up. consensus.insert( part.clone(), - (med, ms.first().copied().unwrap_or([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])), + ( + med, + ms.first() + .copied() + .unwrap_or([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]), + ), ); let rot_var = ms .iter() @@ -219,8 +261,12 @@ fn main() { // re-expressed in the reference part's frame before comparing — and the // rotation is compared too, because "wrong orientation" is half of the // reported viewer symptom and a translation-only check cannot see it. - let Some(si) = args.iter().position(|a| a == "--static") else { return }; - let Some(spath) = args.get(si + 1) else { return }; + let Some(si) = args.iter().position(|a| a == "--static") else { + return; + }; + let Some(spath) = args.get(si + 1) else { + return; + }; let sbytes = std::fs::read(spath).expect("read stage container"); // `include_external = true` — the engine cluster, the bridge and cross-id // turrets live in SEPARATE composites (`e_rou_e106_eng`, 3 nodes) that the @@ -269,12 +315,19 @@ fn main() { .fold(0.0f32, f32::max); worst_t = worst_t.max(dtm); worst_r = worst_r.max(drm); - let mark = if dtm < 1.0 && drm < 0.02 { "MATCH" } else { "DIFFERS" }; + let mark = if dtm < 1.0 && drm < 0.02 { + "MATCH" + } else { + "DIFFERS" + }; println!( " {part:18} static=[{:9.1}{:9.1}{:9.1}] dT={dtm:7.2} dR={drm:6.3} {mark}", r[0], r[1], r[2] ); } - println!("\nworst dT={worst_t:.2} worst dR={worst_r:.3} ({} static parts, {} captured)", - scene.len(), samples.len()); + println!( + "\nworst dT={worst_t:.2} worst dR={worst_r:.3} ({} static parts, {} captured)", + scene.len(), + samples.len() + ); } diff --git a/crates/sylpheed-formats/examples/coverage_audit.rs b/crates/sylpheed-formats/examples/coverage_audit.rs index e49512f1..baa37caa 100644 --- a/crates/sylpheed-formats/examples/coverage_audit.rs +++ b/crates/sylpheed-formats/examples/coverage_audit.rs @@ -6,8 +6,8 @@ //! is well founded. This measures the slack on every block that DOES decode: if //! real geometry always covers its pool, under-coverage is good evidence of a //! wrong candidate and the gate stands. -use sylpheed_formats::mesh::Xbg7Model; use std::collections::BTreeMap; +use sylpheed_formats::mesh::Xbg7Model; fn main() { let dir = std::env::args().nth(1).expect("resource3d dir"); @@ -22,7 +22,9 @@ fn main() { let mut hist: BTreeMap = BTreeMap::new(); let mut worst: Vec<(i64, String)> = Vec::new(); for f in &files { - let Ok(bytes) = std::fs::read(f) else { continue }; + let Ok(bytes) = std::fs::read(f) else { + continue; + }; for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) { for sub in &m.meshes { if sub.positions.is_empty() || sub.indices.is_empty() { @@ -32,14 +34,21 @@ fn main() { let slack = sub.positions.len() as i64 - 1 - max_idx; *hist.entry(slack.min(20)).or_default() += 1; if slack > 4 { - worst.push((slack, format!("{} in {}", m.name, f.file_name().unwrap().to_string_lossy()))); + worst.push(( + slack, + format!("{} in {}", m.name, f.file_name().unwrap().to_string_lossy()), + )); } } } } println!("unreferenced tail vertices (vtx_count − 1 − max index), over decoded sub-meshes:"); for (slack, n) in &hist { - println!(" {:>3}{} : {n}", slack, if *slack == 20 { "+" } else { " " }); + println!( + " {:>3}{} : {n}", + slack, + if *slack == 20 { "+" } else { " " } + ); } worst.sort_by_key(|(s, _)| std::cmp::Reverse(*s)); for (s, w) in worst.iter().take(5) { diff --git a/crates/sylpheed-formats/examples/cue_unit_check.rs b/crates/sylpheed-formats/examples/cue_unit_check.rs index 1b8c7aa6..61985764 100644 --- a/crates/sylpheed-formats/examples/cue_unit_check.rs +++ b/crates/sylpheed-formats/examples/cue_unit_check.rs @@ -12,7 +12,13 @@ use sylpheed_formats::{movie_subtitle, PakArchive}; fn movie_secs(path: &str) -> Option { let out = Command::new("ffprobe") .args([ - "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", path, + "-v", + "error", + "-show_entries", + "format=duration", + "-of", + "csv=p=0", + path, ]) .output() .ok()?; @@ -27,7 +33,9 @@ fn main() { let mut over = 0; let mut worst: Vec<(String, f32, f32)> = Vec::new(); let dir = format!("{disc}/dat/movie"); - let Ok(rd) = std::fs::read_dir(&dir) else { return }; + let Ok(rd) = std::fs::read_dir(&dir) else { + return; + }; for e in rd.flatten() { let p = e.path(); if p.extension().is_none_or(|x| x != "wmv") { @@ -39,7 +47,9 @@ fn main() { continue; } let last = cues.iter().map(|(_, t)| *t).fold(0.0f32, f32::max); - let Some(secs) = movie_secs(&p.to_string_lossy()) else { continue }; + let Some(secs) = movie_secs(&p.to_string_lossy()) else { + continue; + }; checked += 1; if last > secs { over += 1; diff --git a/crates/sylpheed-formats/examples/decl_word_probe.rs b/crates/sylpheed-formats/examples/decl_word_probe.rs index 61766952..0cda3869 100644 --- a/crates/sylpheed-formats/examples/decl_word_probe.rs +++ b/crates/sylpheed-formats/examples/decl_word_probe.rs @@ -16,12 +16,16 @@ use sylpheed_formats::{pak, ratc, ui_layout}; const OFFS: [usize; 4] = [28, 36, 44, 56]; fn be32(b: &[u8], o: usize) -> u32 { - if o + 4 > b.len() { return 0; } + if o + 4 > b.len() { + return 0; + } u32::from_be_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]) } fn main() { - let path = std::env::args().nth(1).expect("usage: decl_word_probe [entry]"); + let path = std::env::args() + .nth(1) + .expect("usage: decl_word_probe [entry]"); let want: Option = std::env::args().nth(2).and_then(|s| s.parse().ok()); let ar = pak::PakArchive::open(&path).expect("open pak"); let entries: Vec<_> = ar.entries().to_vec(); @@ -29,34 +33,62 @@ fn main() { let mut hit = [0usize; 4]; let mut tot = 0usize; for (i, e) in entries.iter().enumerate() { - if want.is_some_and(|w| w != i) { continue; } - let Ok(bytes) = ar.read(e) else { continue }; - let Some(build) = ui_layout::parse_build(&bytes) else { continue }; - let Some(kids) = ratc::parse(&bytes) else { continue }; - // Index space to test against: the T8aD children, in child order. - let t8: Vec<&ratc::RatcChild> = kids.iter().filter(|c| c.kind == "T8aD").collect(); - if build.elements.iter().all(|el| el.sprite.is_some() || el.kind & 0x10 != 0) { + if want.is_some_and(|w| w != i) { continue; } - println!("== entry {i} ({} elements, {} T8aD children)", build.elements.len(), t8.len()); - for (n, c) in t8.iter().enumerate() { println!(" child[{n:2}] {}", c.name); } + let Ok(bytes) = ar.read(e) else { continue }; + let Some(build) = ui_layout::parse_build(&bytes) else { + continue; + }; + let Some(kids) = ratc::parse(&bytes) else { + continue; + }; + // Index space to test against: the T8aD children, in child order. + let t8: Vec<&ratc::RatcChild> = kids.iter().filter(|c| c.kind == "T8aD").collect(); + if build + .elements + .iter() + .all(|el| el.sprite.is_some() || el.kind & 0x10 != 0) + { + continue; + } + println!( + "== entry {i} ({} elements, {} T8aD children)", + build.elements.len(), + t8.len() + ); + for (n, c) in t8.iter().enumerate() { + println!(" child[{n:2}] {}", c.name); + } for el in &build.elements { - if el.kind & 0x10 != 0 { continue; } + if el.kind & 0x10 != 0 { + continue; + } let d = &bytes[0x20 + el.index * 60..0x20 + (el.index + 1) * 60]; let words: Vec = OFFS.iter().map(|&o| be32(d, o)).collect(); // The control: for a RESOLVED element, which T8aD child is it? - let truth = el.sprite.as_ref() + let truth = el + .sprite + .as_ref() .and_then(|s| t8.iter().position(|c| &c.name == s)); if let Some(t) = truth { tot += 1; for (k, w) in words.iter().enumerate() { - if *w as usize == t { hit[k] += 1; } + if *w as usize == t { + hit[k] += 1; + } } } println!( " [{:2}] {:26} sprite={:?} child={:?} +28={} +36={} +44={} +56={}", - el.index, el.name, el.sprite, truth, - words[0] as i32, words[1] as i32, words[2] as i32, words[3] as i32 + el.index, + el.name, + el.sprite, + truth, + words[0] as i32, + words[1] as i32, + words[2] as i32, + words[3] as i32 ); } } diff --git a/crates/sylpheed-formats/examples/deep_map.rs b/crates/sylpheed-formats/examples/deep_map.rs index ca288679..99035d23 100644 --- a/crates/sylpheed-formats/examples/deep_map.rs +++ b/crates/sylpheed-formats/examples/deep_map.rs @@ -1,59 +1,108 @@ -use sylpheed_formats::{ratc, idxd::IdxdObject, PakArchive}; use std::collections::BTreeMap; -fn main(){ - let disc=std::env::var("SYLPHEED_DISC").unwrap(); +use sylpheed_formats::{idxd::IdxdObject, ratc, PakArchive}; +fn main() { + let disc = std::env::var("SYLPHEED_DISC").unwrap(); // 1. RATC child-type census across the big RATC paks - let mut childtypes:BTreeMap=BTreeMap::new(); - let mut ratc_unparsed=0u64; - for pk in ["GP_READY_ROOM","GP_MOVIE_THEATER","GP_DIALOG","GP_DEBRIEFING_PILOTLOG","GP_TITLE","GP_BUNK"]{ - let Ok(arc)=PakArchive::open(format!("{disc}/dat/{pk}.pak")) else{continue}; - for e in arc.entries(){ - let Ok(b)=arc.read(e) else{continue}; - if !ratc::is_ratc(&b){continue;} - match ratc::parse(&b){ - Some(kids)=> for k in kids{ - let ext=k.name.rsplit('.').next().unwrap_or("?").to_lowercase(); - *childtypes.entry(ext).or_default()+=1; - }, - None=>ratc_unparsed+=1, + let mut childtypes: BTreeMap = BTreeMap::new(); + let mut ratc_unparsed = 0u64; + for pk in [ + "GP_READY_ROOM", + "GP_MOVIE_THEATER", + "GP_DIALOG", + "GP_DEBRIEFING_PILOTLOG", + "GP_TITLE", + "GP_BUNK", + ] { + let Ok(arc) = PakArchive::open(format!("{disc}/dat/{pk}.pak")) else { + continue; + }; + for e in arc.entries() { + let Ok(b) = arc.read(e) else { continue }; + if !ratc::is_ratc(&b) { + continue; + } + match ratc::parse(&b) { + Some(kids) => { + for k in kids { + let ext = k.name.rsplit('.').next().unwrap_or("?").to_lowercase(); + *childtypes.entry(ext).or_default() += 1; + } + } + None => ratc_unparsed += 1, } } } println!("=== RATC child-type census (big RATC paks) ==="); - let mut cv:Vec<_>=childtypes.into_iter().collect(); cv.sort_by_key(|x|std::cmp::Reverse(x.1)); - for (e,c) in &cv{ println!(" .{e:8} ×{c}"); } + let mut cv: Vec<_> = childtypes.into_iter().collect(); + cv.sort_by_key(|x| std::cmp::Reverse(x.1)); + for (e, c) in &cv { + println!(" .{e:8} ×{c}"); + } println!(" (RATC bundles that failed to parse: {ratc_unparsed})"); // 2. The 00000002 mystery format (GP_MAIN_GAME_E) - let arc=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); - for e in arc.entries(){ - let Ok(b)=arc.read(e) else{continue}; - if b.len()>=4 && b[0..4]==[0,0,0,2]{ - let hx:String=b[..48.min(b.len())].iter().map(|x|format!("{x:02x}")).collect::>().join(" "); - let asc:String=b[..64.min(b.len())].iter().map(|&x|if(0x20..0x7f).contains(&x){x as char}else{'.'}).collect(); - println!("\n=== 00000002 format sample ({}B) ===\n{hx}\n{asc}",b.len()); + let arc = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); + for e in arc.entries() { + let Ok(b) = arc.read(e) else { continue }; + if b.len() >= 4 && b[0..4] == [0, 0, 0, 2] { + let hx: String = b[..48.min(b.len())] + .iter() + .map(|x| format!("{x:02x}")) + .collect::>() + .join(" "); + let asc: String = b[..64.min(b.len())] + .iter() + .map(|&x| { + if (0x20..0x7f).contains(&x) { + x as char + } else { + '.' + } + }) + .collect(); + println!( + "\n=== 00000002 format sample ({}B) ===\n{hx}\n{asc}", + b.len() + ); break; } } // 3. Sample the top undecoded IDXD schemas: first tokens (guess semantics) println!("\n=== top undecoded IDXD schemas — sample tokens (semantic hints) ==="); - let targets:[u32;6]=[0xb412e6d8,0x026379ab,0x43faa517,0x3c5b0549,0x6ab4825a,0x0426e81d]; - for want in targets{ - let mut shown=false; - for e in arc.entries(){ - if shown{break;} - let Ok(b)=arc.read(e) else{continue}; - if b.len()<12 || &b[0..4]!=b"IDXD"{continue;} - let s=u32::from_be_bytes([b[8],b[9],b[10],b[11]]); - if s!=want{continue;} - if let Ok(o)=IdxdObject::parse(&b){ - let t=o.tokens(); - let sample:Vec=t.iter().take(14).map(|s|{let s=s.chars().take(18).collect::();s}).collect(); + let targets: [u32; 6] = [ + 0xb412e6d8, 0x026379ab, 0x43faa517, 0x3c5b0549, 0x6ab4825a, 0x0426e81d, + ]; + for want in targets { + let mut shown = false; + for e in arc.entries() { + if shown { + break; + } + let Ok(b) = arc.read(e) else { continue }; + if b.len() < 12 || &b[0..4] != b"IDXD" { + continue; + } + let s = u32::from_be_bytes([b[8], b[9], b[10], b[11]]); + if s != want { + continue; + } + if let Ok(o) = IdxdObject::parse(&b) { + let t = o.tokens(); + let sample: Vec = t + .iter() + .take(14) + .map(|s| { + let s = s.chars().take(18).collect::(); + s + }) + .collect(); println!(" {want:08x}: {sample:?}"); - shown=true; + shown = true; } } - if !shown{ println!(" {want:08x}: (parse failed / not found)"); } + if !shown { + println!(" {want:08x}: (parse failed / not found)"); + } } } diff --git a/crates/sylpheed-formats/examples/default_owners.rs b/crates/sylpheed-formats/examples/default_owners.rs index c27a03b6..c1f64db2 100644 --- a/crates/sylpheed-formats/examples/default_owners.rs +++ b/crates/sylpheed-formats/examples/default_owners.rs @@ -13,8 +13,11 @@ fn is_number(s: &str) -> bool { } fn is_key(s: &str) -> bool { !s.is_empty() - && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') - && s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_') + && s.chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + && s.chars() + .next() + .is_some_and(|c| c.is_ascii_alphabetic() || c == '_') && !is_number(s) } fn is_value(s: &str) -> bool { @@ -29,7 +32,9 @@ fn main() { for e in arc.entries() { let Ok(bytes) = arc.read(e) else { continue }; - let Ok(obj) = IdxdObject::parse(&bytes) else { continue }; + let Ok(obj) = IdxdObject::parse(&bytes) else { + continue; + }; let toks = obj.tokens().to_vec(); let mut hits: Vec = vec![]; for (i, t) in toks.iter().enumerate() { @@ -44,7 +49,12 @@ fn main() { }); } if !hits.is_empty() { - println!("0x{:08x} {:<44} {}", obj.schema_hash, obj.identity(), hits.join(" ")); + println!( + "0x{:08x} {:<44} {}", + obj.schema_hash, + obj.identity(), + hits.join(" ") + ); } } } diff --git a/crates/sylpheed-formats/examples/defaulted_fields.rs b/crates/sylpheed-formats/examples/defaulted_fields.rs index b7801505..3a554eea 100644 --- a/crates/sylpheed-formats/examples/defaulted_fields.rs +++ b/crates/sylpheed-formats/examples/defaulted_fields.rs @@ -20,7 +20,9 @@ fn is_key(s: &str) -> bool { !s.is_empty() && s.chars() .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') - && s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_') + && s.chars() + .next() + .is_some_and(|c| c.is_ascii_alphabetic() || c == '_') && !is_number(s) } @@ -71,7 +73,11 @@ fn main() { } let prev = if i == 0 { None } else { Some(&toks[i - 1]) }; let valued = prev.map(|p| is_value(p)).unwrap_or(false); - let v = if valued { Some(toks[i - 1].clone()) } else { None }; + let v = if valued { + Some(toks[i - 1].clone()) + } else { + None + }; seen_here.entry(t.clone()).or_insert(v); } for (k, v) in seen_here { @@ -107,10 +113,7 @@ fn main() { 0xb412_e6d8 => "MESSAGE", _ => "?", }; - let defaulted: Vec<_> = keys - .iter() - .filter(|(_, s)| s.1 > 0) - .collect(); + let defaulted: Vec<_> = keys.iter().filter(|(_, s)| s.1 > 0).collect(); if defaulted.is_empty() { continue; } diff --git a/crates/sylpheed-formats/examples/desc_dump.rs b/crates/sylpheed-formats/examples/desc_dump.rs index f77a67eb..17547ebd 100644 --- a/crates/sylpheed-formats/examples/desc_dump.rs +++ b/crates/sylpheed-formats/examples/desc_dump.rs @@ -49,12 +49,16 @@ fn main() { if r + 12 > desc.len() { break; } - let (o, code, usage) = (be32(desc, r), be32(desc, r + 4), be32(desc, r + 8) >> 16); + let (o, code, usage) = + (be32(desc, r), be32(desc, r + 4), be32(desc, r + 8) >> 16); if o == 0x00FF_0000 || code == 0xFFFF_FFFF || o > 0x1000 { println!(" end marker @0x{r:X}: off=0x{o:X} code=0x{code:X}"); break; } - println!(" off {o:>3} code 0x{:06X} usage {usage}", code & 0xFF_FFFF); + println!( + " off {o:>3} code 0x{:06X} usage {usage}", + code & 0xFF_FFFF + ); stride = stride.max(o + 4); r += 12; } diff --git a/crates/sylpheed-formats/examples/dialogue.rs b/crates/sylpheed-formats/examples/dialogue.rs index 888114a0..a49dbc09 100644 --- a/crates/sylpheed-formats/examples/dialogue.rs +++ b/crates/sylpheed-formats/examples/dialogue.rs @@ -1,13 +1,30 @@ use sylpheed_formats::{game_data, localization::TextIndex, PakArchive}; -fn main(){ - let disc=std::env::var("SYLPHEED_DISC").unwrap(); - let pak=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); - let text=TextIndex::build(&pak); - let msgs=game_data::load_demo_messages(&pak); +fn main() { + let disc = std::env::var("SYLPHEED_DISC").unwrap(); + let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); + let text = TextIndex::build(&pak); + let msgs = game_data::load_demo_messages(&pak); println!("{} dialogue lines total\n", msgs.len()); - for m in msgs.iter().filter(|m|m.character.is_some()&&!m.page_keys.is_empty()).take(8){ - let who=m.character.as_deref().unwrap_or("?").trim_start_matches("Character"); - let line:String=m.page_keys.iter().filter_map(|k|text.get(k)).collect::>().join(" "); - println!(" {who:10} [{}] “{}”", m.voice_clip.as_deref().unwrap_or("-"), line.chars().take(64).collect::()); + for m in msgs + .iter() + .filter(|m| m.character.is_some() && !m.page_keys.is_empty()) + .take(8) + { + let who = m + .character + .as_deref() + .unwrap_or("?") + .trim_start_matches("Character"); + let line: String = m + .page_keys + .iter() + .filter_map(|k| text.get(k)) + .collect::>() + .join(" "); + println!( + " {who:10} [{}] “{}”", + m.voice_clip.as_deref().unwrap_or("-"), + line.chars().take(64).collect::() + ); } } diff --git a/crates/sylpheed-formats/examples/dm_extract.rs b/crates/sylpheed-formats/examples/dm_extract.rs index e6ed04d2..511ddf6a 100644 --- a/crates/sylpheed-formats/examples/dm_extract.rs +++ b/crates/sylpheed-formats/examples/dm_extract.rs @@ -1,29 +1,60 @@ -use sylpheed_formats::{idxd::IdxdObject, PakArchive}; use std::collections::BTreeMap; -fn main(){ - let disc=std::env::var("SYLPHEED_DISC").unwrap(); - let arc=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); - let tables:[(u32,&str);4]=[(0x0426e81d,"Player / physics+scoring"),(0x6ab4825a,"Weapon"),(0x43faa517,"Unit / craft"),(0x3c5b0549,"Vessel / capital ship")]; - for (want,label) in tables{ +use sylpheed_formats::{idxd::IdxdObject, PakArchive}; +fn main() { + let disc = std::env::var("SYLPHEED_DISC").unwrap(); + let arc = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); + let tables: [(u32, &str); 4] = [ + (0x0426e81d, "Player / physics+scoring"), + (0x6ab4825a, "Weapon"), + (0x43faa517, "Unit / craft"), + (0x3c5b0549, "Vessel / capital ship"), + ]; + for (want, label) in tables { // collect all records of this schema - let mut recs:Vec=vec![]; - for e in arc.entries(){ let Ok(b)=arc.read(e) else{continue}; let Ok(o)=IdxdObject::parse(&b) else{continue}; if o.schema_hash==want{recs.push(o);} } + let mut recs: Vec = vec![]; + for e in arc.entries() { + let Ok(b) = arc.read(e) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { + continue; + }; + if o.schema_hash == want { + recs.push(o); + } + } // field-union (explicit-valued keys), with occurrence count - let mut cols:BTreeMap=BTreeMap::new(); - for o in &recs{ for (k,_) in o.resolved_fields(){ *cols.entry(k.into()).or_default()+=1; } } + let mut cols: BTreeMap = BTreeMap::new(); + for o in &recs { + for (k, _) in o.resolved_fields() { + *cols.entry(k.into()).or_default() += 1; + } + } println!("\n╔══ {label} [{want:08x}] {} records ══", recs.len()); - let mut cv:Vec<_>=cols.into_iter().collect(); cv.sort_by_key(|x|std::cmp::Reverse(x.1)); - let colstr:String=cv.iter().take(28).map(|(k,c)|format!("{k}({c})")).collect::>().join(" "); + let mut cv: Vec<_> = cols.into_iter().collect(); + cv.sort_by_key(|x| std::cmp::Reverse(x.1)); + let colstr: String = cv + .iter() + .take(28) + .map(|(k, c)| format!("{k}({c})")) + .collect::>() + .join(" "); println!("║ numeric/enum fields: {colstr}"); // dump 2 sample records: identity + explicit fields - for o in recs.iter().take(2){ - let id=o.get_raw("ID").unwrap_or("?"); - let name=o.get_raw("Name").unwrap_or(""); + for o in recs.iter().take(2) { + let id = o.get_raw("ID").unwrap_or("?"); + let name = o.get_raw("Name").unwrap_or(""); print!("║ • {id}"); - if !name.is_empty(){print!(" «{name}»");} + if !name.is_empty() { + print!(" «{name}»"); + } println!(); - let fields:Vec=o.resolved_fields().iter().map(|(k,v)|format!("{k}={v}")).collect(); - for chunk in fields.chunks(5){ println!("║ {}", chunk.join(" ")); } + let fields: Vec = o + .resolved_fields() + .iter() + .map(|(k, v)| format!("{k}={v}")) + .collect(); + for chunk in fields.chunks(5) { + println!("║ {}", chunk.join(" ")); + } } } } diff --git a/crates/sylpheed-formats/examples/dm_roster.rs b/crates/sylpheed-formats/examples/dm_roster.rs index 816665ff..273183ba 100644 --- a/crates/sylpheed-formats/examples/dm_roster.rs +++ b/crates/sylpheed-formats/examples/dm_roster.rs @@ -1,11 +1,29 @@ -use sylpheed_formats::{game_data, PakArchive}; use std::collections::BTreeMap; -fn main(){ - let disc=std::env::var("SYLPHEED_DISC").unwrap(); - let pak=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); - let chars=game_data::load_characters(&pak); - let mut byfac:BTreeMap>=Default::default(); - for c in &chars{ byfac.entry(c.faction.clone().unwrap_or("·".into())).or_default().push(format!("{}({})",c.id.clone().unwrap_or_default().trim_start_matches("Character"),c.faces.len())); } - println!("{} characters across {} factions:",chars.len(),byfac.len()); - for (f,mut v) in byfac{ v.sort(); println!(" [{f}] {}", v.join(" ")); } +use sylpheed_formats::{game_data, PakArchive}; +fn main() { + let disc = std::env::var("SYLPHEED_DISC").unwrap(); + let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); + let chars = game_data::load_characters(&pak); + let mut byfac: BTreeMap> = Default::default(); + for c in &chars { + byfac + .entry(c.faction.clone().unwrap_or("·".into())) + .or_default() + .push(format!( + "{}({})", + c.id.clone() + .unwrap_or_default() + .trim_start_matches("Character"), + c.faces.len() + )); + } + println!( + "{} characters across {} factions:", + chars.len(), + byfac.len() + ); + for (f, mut v) in byfac { + v.sort(); + println!(" [{f}] {}", v.join(" ")); + } } diff --git a/crates/sylpheed-formats/examples/dm_rows.rs b/crates/sylpheed-formats/examples/dm_rows.rs index 76a72fd0..be99a05b 100644 --- a/crates/sylpheed-formats/examples/dm_rows.rs +++ b/crates/sylpheed-formats/examples/dm_rows.rs @@ -1,14 +1,83 @@ use sylpheed_formats::{idxd::IdxdObject, PakArchive}; -fn short(s:&str)->String{ s.trim_start_matches("Weapon_").trim_start_matches("UN_").trim_start_matches("UnitName_UN_").into() } -fn g<'a>(o:&'a IdxdObject,k:&str)->String{ o.get_f32(k).map(|v|{if v==v.trunc(){format!("{}",v as i64)}else{format!("{v}")}}).or_else(||o.get_raw(k).filter(|x|x.chars().next().map(|c|c.is_ascii_digit()).unwrap_or(false)).map(|s|s.to_string())).unwrap_or("·".into()) } -fn main(){ - let disc=std::env::var("SYLPHEED_DISC").unwrap(); - let arc=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); - let recs=|want:u32|->Vec{ arc.entries().iter().filter_map(|e|arc.read(e).ok()).filter_map(|b|IdxdObject::parse(&b).ok()).filter(|o|o.schema_hash==want).collect() }; - println!("### WEAPONS (name | target | load | int | power | vel | range | trig)"); - for o in recs(0x6ab4825a).iter().take(16){ println!("{:<34}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", short(o.get_raw("ID").unwrap_or("?")), o.get_raw("TargetType").unwrap_or("·"), g(o,"LoadingCount"),g(o,"Interval"),g(o,"Power"),g(o,"Velocity"),g(o,"MaximumRange"),g(o,"TriggerShotCount")); } - println!("\n### UNITS/CRAFT (name | HP | cruise | accel | radar | turrets | score)"); - for o in recs(0x43faa517).iter().take(16){ println!("{:<38}\t{}\t{}\t{}\t{}\t{}\t{}", short(o.get_raw("ID").unwrap_or("?")), g(o,"HP"),g(o,"CruisingVelocity"),g(o,"Acceleration"),g(o,"RadarRange"),g(o,"TurretCount"),g(o,"ScorePoint")); } - println!("\n### VESSELS/CAPITAL SHIPS (name | HP | Sz_X | Sz_Z | radar | turrets | bridges | hatches | shieldgen | thrusters)"); - for o in recs(0x3c5b0549).iter(){ println!("{:<30}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", short(o.get_raw("ID").unwrap_or("?")), g(o,"HP"),g(o,"Size_X"),g(o,"Size_Z"),g(o,"RadarRange"),g(o,"TurretCount"),g(o,"BridgeCount"),g(o,"HatchCount"),g(o,"ShieldGeneratorCount"),g(o,"ThrusterCount")); } +fn short(s: &str) -> String { + s.trim_start_matches("Weapon_") + .trim_start_matches("UN_") + .trim_start_matches("UnitName_UN_") + .into() +} +fn g<'a>(o: &'a IdxdObject, k: &str) -> String { + o.get_f32(k) + .map(|v| { + if v == v.trunc() { + format!("{}", v as i64) + } else { + format!("{v}") + } + }) + .or_else(|| { + o.get_raw(k) + .filter(|x| { + x.chars() + .next() + .map(|c| c.is_ascii_digit()) + .unwrap_or(false) + }) + .map(|s| s.to_string()) + }) + .unwrap_or("·".into()) +} +fn main() { + let disc = std::env::var("SYLPHEED_DISC").unwrap(); + let arc = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); + let recs = |want: u32| -> Vec { + arc.entries() + .iter() + .filter_map(|e| arc.read(e).ok()) + .filter_map(|b| IdxdObject::parse(&b).ok()) + .filter(|o| o.schema_hash == want) + .collect() + }; + println!("### WEAPONS (name | target | load | int | power | vel | range | trig)"); + for o in recs(0x6ab4825a).iter().take(16) { + println!( + "{:<34}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + short(o.get_raw("ID").unwrap_or("?")), + o.get_raw("TargetType").unwrap_or("·"), + g(o, "LoadingCount"), + g(o, "Interval"), + g(o, "Power"), + g(o, "Velocity"), + g(o, "MaximumRange"), + g(o, "TriggerShotCount") + ); + } + println!("\n### UNITS/CRAFT (name | HP | cruise | accel | radar | turrets | score)"); + for o in recs(0x43faa517).iter().take(16) { + println!( + "{:<38}\t{}\t{}\t{}\t{}\t{}\t{}", + short(o.get_raw("ID").unwrap_or("?")), + g(o, "HP"), + g(o, "CruisingVelocity"), + g(o, "Acceleration"), + g(o, "RadarRange"), + g(o, "TurretCount"), + g(o, "ScorePoint") + ); + } + println!("\n### VESSELS/CAPITAL SHIPS (name | HP | Sz_X | Sz_Z | radar | turrets | bridges | hatches | shieldgen | thrusters)"); + for o in recs(0x3c5b0549).iter() { + println!( + "{:<30}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + short(o.get_raw("ID").unwrap_or("?")), + g(o, "HP"), + g(o, "Size_X"), + g(o, "Size_Z"), + g(o, "RadarRange"), + g(o, "TurretCount"), + g(o, "BridgeCount"), + g(o, "HatchCount"), + g(o, "ShieldGeneratorCount"), + g(o, "ThrusterCount") + ); + } } diff --git a/crates/sylpheed-formats/examples/dossier.rs b/crates/sylpheed-formats/examples/dossier.rs index 35b3322c..ed1b6a10 100644 --- a/crates/sylpheed-formats/examples/dossier.rs +++ b/crates/sylpheed-formats/examples/dossier.rs @@ -1,15 +1,21 @@ use sylpheed_formats::{game_data, localization::TextIndex, PakArchive}; -fn main(){ - let disc=std::env::var("SYLPHEED_DISC").unwrap(); - let pak=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); - let text=TextIndex::build(&pak); - for n in 1..=16u32{ - let sid=format!("S{n:02}"); +fn main() { + let disc = std::env::var("SYLPHEED_DISC").unwrap(); + let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); + let text = TextIndex::build(&pak); + for n in 1..=16u32 { + let sid = format!("S{n:02}"); // primary objective across phases (first that resolves) - let obj:Vec=(1..=3).flat_map(|p|text.objectives(&sid,p)).map(|s|s.to_string()).collect(); - let lose:Vec=(1..=3).flat_map(|p|text.lose_conditions(&sid,p)).map(|s|s.to_string()).collect(); - let full_obj=obj.join(" "); - let full_lose=lose.into_iter().take(2).collect::>().join(" "); + let obj: Vec = (1..=3) + .flat_map(|p| text.objectives(&sid, p)) + .map(|s| s.to_string()) + .collect(); + let lose: Vec = (1..=3) + .flat_map(|p| text.lose_conditions(&sid, p)) + .map(|s| s.to_string()) + .collect(); + let full_obj = obj.join(" "); + let full_lose = lose.into_iter().take(2).collect::>().join(" "); println!("{sid}\t{full_obj}\t{full_lose}"); } } diff --git a/crates/sylpheed-formats/examples/edge_cap_sweep.rs b/crates/sylpheed-formats/examples/edge_cap_sweep.rs index c948ba20..de7bcb8a 100644 --- a/crates/sylpheed-formats/examples/edge_cap_sweep.rs +++ b/crates/sylpheed-formats/examples/edge_cap_sweep.rs @@ -3,8 +3,8 @@ //! `XBG7_EDGE_CAP` sets the cap; this reports, for one setting, how much //! geometry decodes and how self-consistent it is across containers — the two //! numbers any change to the cap has to trade off. Run it once per cap value. -use sylpheed_formats::mesh::Xbg7Model; use std::collections::BTreeMap; +use sylpheed_formats::mesh::Xbg7Model; fn main() { let dir = std::env::args().nth(1).expect("resource3d dir"); @@ -20,7 +20,9 @@ fn main() { let mut seen: BTreeMap> = BTreeMap::new(); let (mut models, mut verts) = (0usize, 0usize); for f in &files { - let Ok(bytes) = std::fs::read(f) else { continue }; + let Ok(bytes) = std::fs::read(f) else { + continue; + }; for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) { let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]); for s in &m.meshes { diff --git a/crates/sylpheed-formats/examples/envelope_screen.rs b/crates/sylpheed-formats/examples/envelope_screen.rs index 00884b0d..ecbe482f 100644 --- a/crates/sylpheed-formats/examples/envelope_screen.rs +++ b/crates/sylpheed-formats/examples/envelope_screen.rs @@ -8,13 +8,16 @@ //! however big, is part of the silhouette. //! //! Usage: envelope_screen [protrusion_fraction] +use std::collections::{BTreeSet, HashSet}; use sylpheed_formats::mesh::Xbg7Model; use sylpheed_formats::ship::{assemble_ship, ship_id_of}; -use std::collections::{BTreeSet, HashSet}; fn main() { let dir = std::env::args().nth(1).expect("resource3d dir"); - let limit: f32 = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(0.35); + let limit: f32 = std::env::args() + .nth(2) + .and_then(|s| s.parse().ok()) + .unwrap_or(0.35); let mut files: Vec<_> = std::fs::read_dir(&dir) .unwrap() .flatten() @@ -25,7 +28,9 @@ fn main() { let mut flagged = 0usize; for f in &files { - let Ok(bytes) = std::fs::read(f) else { continue }; + let Ok(bytes) = std::fs::read(f) else { + continue; + }; let ids: BTreeSet = Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) .iter() .filter_map(|m| ship_id_of(&m.name).map(|s| s.to_string())) @@ -40,7 +45,9 @@ fn main() { // World box per placement. let mut boxes: Vec<(String, [f32; 3], [f32; 3])> = Vec::new(); for p in &placed { - let Some(m) = models.iter().find(|m| m.name == p.resource) else { continue }; + let Some(m) = models.iter().find(|m| m.name == p.resource) else { + continue; + }; let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]); for s in &m.meshes { for q in &s.positions { @@ -102,5 +109,8 @@ fn main() { } } } - println!("{flagged} parts protrude more than {:.0}% of their ship's size", 100.0 * limit); + println!( + "{flagged} parts protrude more than {:.0}% of their ship's size", + 100.0 * limit + ); } diff --git a/crates/sylpheed-formats/examples/filter_consistency.rs b/crates/sylpheed-formats/examples/filter_consistency.rs index 8374ec51..63f0de48 100644 --- a/crates/sylpheed-formats/examples/filter_consistency.rs +++ b/crates/sylpheed-formats/examples/filter_consistency.rs @@ -5,8 +5,8 @@ //! assembler and the viewer) can reach a different answer from a whole-container //! decode. This measures that directly. //! Usage: filter_consistency [resource...] -use sylpheed_formats::mesh::Xbg7Model; use std::collections::HashSet; +use sylpheed_formats::mesh::Xbg7Model; fn main() { let a: Vec = std::env::args().collect(); let bytes = std::fs::read(&a[1]).expect("container"); @@ -27,9 +27,16 @@ fn main() { if off(f) != off(s) { differ += 1; if differ <= 10 { - println!("{n}: full decode at 0x{:x}, filtered at 0x{:x}", off(f), off(s)); + println!( + "{n}: full decode at 0x{:x}, filtered at 0x{:x}", + off(f), + off(s) + ); } } } - println!("{differ} of {} resources decode differently when filtered", names.len()); + println!( + "{differ} of {} resources decode differently when filtered", + names.len() + ); } diff --git a/crates/sylpheed-formats/examples/find_mirror.rs b/crates/sylpheed-formats/examples/find_mirror.rs index 8914bc44..30e2bd46 100644 --- a/crates/sylpheed-formats/examples/find_mirror.rs +++ b/crates/sylpheed-formats/examples/find_mirror.rs @@ -7,8 +7,8 @@ //! mirrored pair. //! //! Usage: find_mirror ... -use sylpheed_formats::mesh::Xbg7Model; use std::collections::HashSet; +use sylpheed_formats::mesh::Xbg7Model; fn main() { let a: Vec = std::env::args().collect(); @@ -18,7 +18,12 @@ fn main() { let be = |at: usize| f32::from_be_bytes(bytes[at..at + 4].try_into().unwrap()); for m in &models { - let pos: Vec<[f32; 3]> = m.meshes.iter().flat_map(|s| s.positions.clone()).take(8).collect(); + let pos: Vec<[f32; 3]> = m + .meshes + .iter() + .flat_map(|s| s.positions.clone()) + .take(8) + .collect(); if pos.len() < 8 { continue; } diff --git a/crates/sylpheed-formats/examples/flights.rs b/crates/sylpheed-formats/examples/flights.rs index c806d366..d23ad4d9 100644 --- a/crates/sylpheed-formats/examples/flights.rs +++ b/crates/sylpheed-formats/examples/flights.rs @@ -1,16 +1,22 @@ use sylpheed_formats::{game_data as gd, PakArchive}; -fn main(){ - let disc=std::env::var("SYLPHEED_DISC").unwrap(); - let pak=PakArchive::open(format!("{disc}/dat/GP_HANGAR_ARSENAL.pak")).unwrap(); - let rosters=gd::load_pilot_rosters(&pak); +fn main() { + let disc = std::env::var("SYLPHEED_DISC").unwrap(); + let pak = PakArchive::open(format!("{disc}/dat/GP_HANGAR_ARSENAL.pak")).unwrap(); + let rosters = gd::load_pilot_rosters(&pak); // distinct rosters by their pilot set - let mut seen=std::collections::BTreeSet::new(); let mut shown=0; + let mut seen = std::collections::BTreeSet::new(); + let mut shown = 0; println!("{} pilot-roster configs; distinct line-ups:", rosters.len()); - for r in &rosters{ - let key:String=r.pilots().iter().map(|(c,p)|format!("{c}:{p}")).collect::>().join(","); - if seen.insert(key) && shown<8 { - shown+=1; - let flt:Vec=r.pilots().iter().map(|(c,p)|format!("{c}={p}")).collect(); + for r in &rosters { + let key: String = r + .pilots() + .iter() + .map(|(c, p)| format!("{c}:{p}")) + .collect::>() + .join(","); + if seen.insert(key) && shown < 8 { + shown += 1; + let flt: Vec = r.pilots().iter().map(|(c, p)| format!("{c}={p}")).collect(); println!(" {}", flt.join(" ")); } } diff --git a/crates/sylpheed-formats/examples/gate_histogram.rs b/crates/sylpheed-formats/examples/gate_histogram.rs index e08d2585..5948ff67 100644 --- a/crates/sylpheed-formats/examples/gate_histogram.rs +++ b/crates/sylpheed-formats/examples/gate_histogram.rs @@ -1,11 +1,14 @@ //! Which gate stops the resources that never decode? //! Usage: gate_histogram [max_resources] -use sylpheed_formats::mesh::{debug_best_rejection, xbg7_resource_names, Xbg7Model}; use std::collections::{BTreeMap, HashSet}; +use sylpheed_formats::mesh::{debug_best_rejection, xbg7_resource_names, Xbg7Model}; fn main() { let dir = std::env::args().nth(1).expect("resource3d dir"); - let cap: usize = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(usize::MAX); + let cap: usize = std::env::args() + .nth(2) + .and_then(|s| s.parse().ok()) + .unwrap_or(usize::MAX); let mut files: Vec<_> = std::fs::read_dir(&dir) .unwrap() .flatten() @@ -20,7 +23,9 @@ fn main() { if done >= cap { break; } - let Ok(bytes) = std::fs::read(f) else { continue }; + let Ok(bytes) = std::fs::read(f) else { + continue; + }; let names = xbg7_resource_names(&bytes); if names.is_empty() { continue; diff --git a/crates/sylpheed-formats/examples/grouped_report.rs b/crates/sylpheed-formats/examples/grouped_report.rs index 1361ae46..6bbbe50a 100644 --- a/crates/sylpheed-formats/examples/grouped_report.rs +++ b/crates/sylpheed-formats/examples/grouped_report.rs @@ -1,2 +1,7 @@ -fn main(){let a:Vec=std::env::args().collect();let b=std::fs::read(&a[1]).unwrap(); -for l in sylpheed_formats::mesh::debug_grouped_report(&b,&a[2],a[3].parse().unwrap()){println!("{l}")}} +fn main() { + let a: Vec = std::env::args().collect(); + let b = std::fs::read(&a[1]).unwrap(); + for l in sylpheed_formats::mesh::debug_grouped_report(&b, &a[2], a[3].parse().unwrap()) { + println!("{l}") + } +} diff --git a/crates/sylpheed-formats/examples/hokyu_demo_resolve.rs b/crates/sylpheed-formats/examples/hokyu_demo_resolve.rs index ceb3f994..1f97f6b3 100644 --- a/crates/sylpheed-formats/examples/hokyu_demo_resolve.rs +++ b/crates/sylpheed-formats/examples/hokyu_demo_resolve.rs @@ -1,43 +1,144 @@ -use sylpheed_formats::{hash::name_hash, movie_manifest, movie_subtitle as ms, movie_voice, slb, PakArchive}; -use std::fs;use std::io::{Read,Seek,SeekFrom};use std::process::Command; -fn rg(disc:&str,g:u64,n:usize)->Vec{let mut segs=vec![];let mut cum=0u64;for i in 0..5{let p=format!("{disc}/dat/sound.p{i:02}");if let Ok(m)=fs::metadata(&p){segs.push((cum,m.len(),p));cum+=m.len();}}let mut out=vec![];let(mut need,mut pos)=(n,g);for(base,len,path)in &segs{if need==0||pos>=base+len||pos<*base{continue;}let local=pos-base;let take=need.min((len-local)as usize);let mut f=fs::File::open(path).unwrap();f.seek(SeekFrom::Start(local)).unwrap();let mut b=vec![0u8;take];f.read_exact(&mut b).unwrap();out.extend_from_slice(&b);need-=take;pos+=take as u64;}out} -fn ct(h:&[u8],n:&[u8])->bool{h.windows(n.len()).any(|w|w==n)} -fn dur(w:&str)->String{let o=Command::new("ffprobe").args(["-v","error","-show_entries","format=duration","-of","default=nw=1:nk=1",w]).output().unwrap();String::from_utf8_lossy(&o.stdout).trim().to_string()} -fn main(){ - let disc=std::env::var("SYLPHEED_DISC").unwrap(); - let tpak=PakArchive::open(format!("{disc}/dat/tables.pak")).unwrap(); - let man=tpak.entries().iter().find_map(|e|tpak.read(e).ok().filter(|b|movie_manifest::is_manifest(b))).unwrap(); - let lpak=PakArchive::open(format!("{disc}/dat/movie/eng.pak")).unwrap(); - let reg=tpak.entries().iter().find_map(|e|tpak.read(e).ok().filter(|b|ct(b,b"eng\\Movie\\VOICE_ADV.slb"))).unwrap(); - let ids=movie_voice::registry_voice_ids(®); - let stoc=fs::read(format!("{disc}/dat/sound.pak")).unwrap(); - let ents=PakArchive::parse_toc(&stoc).unwrap(); +use std::fs; +use std::io::{Read, Seek, SeekFrom}; +use std::process::Command; +use sylpheed_formats::{ + hash::name_hash, movie_manifest, movie_subtitle as ms, movie_voice, slb, PakArchive, +}; +fn rg(disc: &str, g: u64, n: usize) -> Vec { + let mut segs = vec![]; + let mut cum = 0u64; + for i in 0..5 { + let p = format!("{disc}/dat/sound.p{i:02}"); + if let Ok(m) = fs::metadata(&p) { + segs.push((cum, m.len(), p)); + cum += m.len(); + } + } + let mut out = vec![]; + let (mut need, mut pos) = (n, g); + for (base, len, path) in &segs { + if need == 0 || pos >= base + len || pos < *base { + continue; + } + let local = pos - base; + let take = need.min((len - local) as usize); + let mut f = fs::File::open(path).unwrap(); + f.seek(SeekFrom::Start(local)).unwrap(); + let mut b = vec![0u8; take]; + f.read_exact(&mut b).unwrap(); + out.extend_from_slice(&b); + need -= take; + pos += take as u64; + } + out +} +fn ct(h: &[u8], n: &[u8]) -> bool { + h.windows(n.len()).any(|w| w == n) +} +fn dur(w: &str) -> String { + let o = Command::new("ffprobe") + .args([ + "-v", + "error", + "-show_entries", + "format=duration", + "-of", + "default=nw=1:nk=1", + w, + ]) + .output() + .unwrap(); + String::from_utf8_lossy(&o.stdout).trim().to_string() +} +fn main() { + let disc = std::env::var("SYLPHEED_DISC").unwrap(); + let tpak = PakArchive::open(format!("{disc}/dat/tables.pak")).unwrap(); + let man = tpak + .entries() + .iter() + .find_map(|e| tpak.read(e).ok().filter(|b| movie_manifest::is_manifest(b))) + .unwrap(); + let lpak = PakArchive::open(format!("{disc}/dat/movie/eng.pak")).unwrap(); + let reg = tpak + .entries() + .iter() + .find_map(|e| { + tpak.read(e) + .ok() + .filter(|b| ct(b, b"eng\\Movie\\VOICE_ADV.slb")) + }) + .unwrap(); + let ids = movie_voice::registry_voice_ids(®); + let stoc = fs::read(format!("{disc}/dat/sound.pak")).unwrap(); + let ents = PakArchive::parse_toc(&stoc).unwrap(); // demo->token from bound hokyu - let hok:Vec<_>=movie_manifest::parse(&man).into_iter().filter(|m|m.movie.starts_with("hokyu_")).collect(); - let resolve=|movie:&str|->Option{ - movie_manifest::voice_token(&man,movie).or_else(||{ - let want=ms::track_voice_cues(&lpak,movie).first().map(|&(d,_)|d)?; - hok.iter().find_map(|e|{let t=e.voice_token.clone().filter(|_|e.movie.starts_with("hokyu_"))?; ms::track_voice_cues(&lpak,&e.movie).iter().any(|&(d,_)|d==want).then_some(t)}) + let hok: Vec<_> = movie_manifest::parse(&man) + .into_iter() + .filter(|m| m.movie.starts_with("hokyu_")) + .collect(); + let resolve = |movie: &str| -> Option { + movie_manifest::voice_token(&man, movie).or_else(|| { + let want = ms::track_voice_cues(&lpak, movie) + .first() + .map(|&(d, _)| d)?; + hok.iter().find_map(|e| { + let t = e + .voice_token + .clone() + .filter(|_| e.movie.starts_with("hokyu_"))?; + ms::track_voice_cues(&lpak, &e.movie) + .iter() + .any(|&(d, _)| d == want) + .then_some(t) + }) }) }; - for e in &hok{ - let mv=&e.movie; - let bound=e.voice_token.is_some(); - let tok=resolve(mv); - let demo=ms::track_voice_cues(&lpak,mv).first().map(|&(d,_)|d); - let mut d="—".to_string(); - if let Some(tok)=&tok{ - if let Some(&id)=ids.get(tok){ - if let Some(anchor)=["Movie","etc","Voice"].iter().find_map(|dir|{let h=name_hash(&format!("eng\\{dir}\\{tok}.slb"));ents.binary_search_by_key(&h,|x|x.name_hash).ok().map(|i|ents[i].offset as u64)}){ - let ws=(anchor.saturating_sub(2*1024*1024))&!3; let win=rg(&disc,ws,8*1024*1024); - if let Some(el)=movie_voice::find_descriptor(&win,id){let end=ws+el as u64; - let start=movie_voice::find_descriptor(&win,id.wrapping_sub(1)).or_else(||movie_voice::find_descriptor_before(&win,el)).map(|o|ws+o as u64).filter(|&s|s {:11} voice={d}s", if bound{"BOUND"}else{"unbound"}, demo, tok.unwrap_or("(silent)".into())); + println!( + "{mv:16} {:8} demo={:?} -> {:11} voice={d}s", + if bound { "BOUND" } else { "unbound" }, + demo, + tok.unwrap_or("(silent)".into()) + ); } } diff --git a/crates/sylpheed-formats/examples/hokyu_final.rs b/crates/sylpheed-formats/examples/hokyu_final.rs index ef3ea232..cc9e5022 100644 --- a/crates/sylpheed-formats/examples/hokyu_final.rs +++ b/crates/sylpheed-formats/examples/hokyu_final.rs @@ -1,36 +1,148 @@ +use std::fs; +use std::io::{Read, Seek, SeekFrom}; +use std::process::Command; use sylpheed_formats::{hash::name_hash, movie_manifest, movie_voice, slb, PakArchive}; -use std::fs;use std::io::{Read,Seek,SeekFrom};use std::process::Command; -fn rg(disc:&str,g:u64,n:usize)->Vec{let mut segs=vec![];let mut cum=0u64;for i in 0..5{let p=format!("{disc}/dat/sound.p{i:02}");if let Ok(m)=fs::metadata(&p){segs.push((cum,m.len(),p));cum+=m.len();}}let mut out=vec![];let(mut need,mut pos)=(n,g);for(base,len,path)in &segs{if need==0||pos>=base+len||pos<*base{continue;}let local=pos-base;let take=need.min((len-local)as usize);let mut f=fs::File::open(path).unwrap();f.seek(SeekFrom::Start(local)).unwrap();let mut b=vec![0u8;take];f.read_exact(&mut b).unwrap();out.extend_from_slice(&b);need-=take;pos+=take as u64;}out} -fn contains(h:&[u8],n:&[u8])->bool{h.windows(n.len()).any(|w|w==n)} -fn dur(w:&str)->String{let o=Command::new("ffprobe").args(["-v","error","-show_entries","format=duration","-of","default=nw=1:nk=1",w]).output().unwrap();String::from_utf8_lossy(&o.stdout).trim().to_string()} -fn hokyu_fallback(m:&str)->Option{if !m.starts_with("hokyu_"){return None}let ls=m.contains("_LS_");let ds=m.contains("_DS_");let a=m.ends_with('A');let h=m.ends_with('H');Some(match(ls,ds,a,h){(true,_,true,_)=>"VOICE_D_450",(true,_,_,true)=>"VOICE_D_453",(_,true,true,_)=>"VOICE_D_452",(_,true,_,true)=>"VOICE_D_454",_=>return None}.into())} -fn main(){ - let disc=std::env::var("SYLPHEED_DISC").unwrap(); - let tpak=PakArchive::open(format!("{disc}/dat/tables.pak")).unwrap(); - let manifest=tpak.entries().iter().find_map(|e|tpak.read(e).ok().filter(|b|movie_manifest::is_manifest(b))).unwrap(); - let code="eng"; - let registry=tpak.entries().iter().find_map(|e|tpak.read(e).ok().filter(|b|contains(b,format!("{code}\\Movie\\VOICE_ADV.slb").as_bytes()))).unwrap(); - let ids=movie_voice::registry_voice_ids(®istry); - let stoc=fs::read(format!("{disc}/dat/sound.pak")).unwrap(); - let entries=PakArchive::parse_toc(&stoc).unwrap(); - let hokyu:Vec=movie_manifest::parse(&manifest).into_iter().filter(|m|m.movie.starts_with("hokyu_")).map(|m|m.movie).collect(); - for movie in &hokyu{ - let bound=movie_manifest::voice_token(&manifest,movie); - let token=bound.clone().or_else(||hokyu_fallback(movie)); - let Some(token)=token else{println!("{movie:16} NO TOKEN"); continue}; - let src = if bound.is_some(){"bound"}else{"fallback"}; - let Some(&id)=ids.get(&token) else{println!("{movie:16} {token} not in registry");continue}; - let Some(anchor)=["Movie","etc","Voice"].iter().find_map(|d|{let h=name_hash(&format!("{code}\\{d}\\{token}.slb"));entries.binary_search_by_key(&h,|e|e.name_hash).ok().map(|i|entries[i].offset as u64)}) else{println!("{movie:16} no anchor");continue}; - let win_start=(anchor.saturating_sub(2*1024*1024))&!3; - let window=rg(&disc,win_start,8*1024*1024); - let Some(el)=movie_voice::find_descriptor(&window,id) else{println!("{movie:16} desc not found");continue}; - let end=win_start+el as u64; - let start=movie_voice::find_descriptor(&window,id.wrapping_sub(1)).or_else(||movie_voice::find_descriptor_before(&window,el)).map(|o|win_start+o as u64).filter(|&s|s Vec { + let mut segs = vec![]; + let mut cum = 0u64; + for i in 0..5 { + let p = format!("{disc}/dat/sound.p{i:02}"); + if let Ok(m) = fs::metadata(&p) { + segs.push((cum, m.len(), p)); + cum += m.len(); + } + } + let mut out = vec![]; + let (mut need, mut pos) = (n, g); + for (base, len, path) in &segs { + if need == 0 || pos >= base + len || pos < *base { + continue; + } + let local = pos - base; + let take = need.min((len - local) as usize); + let mut f = fs::File::open(path).unwrap(); + f.seek(SeekFrom::Start(local)).unwrap(); + let mut b = vec![0u8; take]; + f.read_exact(&mut b).unwrap(); + out.extend_from_slice(&b); + need -= take; + pos += take as u64; + } + out +} +fn contains(h: &[u8], n: &[u8]) -> bool { + h.windows(n.len()).any(|w| w == n) +} +fn dur(w: &str) -> String { + let o = Command::new("ffprobe") + .args([ + "-v", + "error", + "-show_entries", + "format=duration", + "-of", + "default=nw=1:nk=1", + w, + ]) + .output() + .unwrap(); + String::from_utf8_lossy(&o.stdout).trim().to_string() +} +fn hokyu_fallback(m: &str) -> Option { + if !m.starts_with("hokyu_") { + return None; + } + let ls = m.contains("_LS_"); + let ds = m.contains("_DS_"); + let a = m.ends_with('A'); + let h = m.ends_with('H'); + Some( + match (ls, ds, a, h) { + (true, _, true, _) => "VOICE_D_450", + (true, _, _, true) => "VOICE_D_453", + (_, true, true, _) => "VOICE_D_452", + (_, true, _, true) => "VOICE_D_454", + _ => return None, + } + .into(), + ) +} +fn main() { + let disc = std::env::var("SYLPHEED_DISC").unwrap(); + let tpak = PakArchive::open(format!("{disc}/dat/tables.pak")).unwrap(); + let manifest = tpak + .entries() + .iter() + .find_map(|e| tpak.read(e).ok().filter(|b| movie_manifest::is_manifest(b))) + .unwrap(); + let code = "eng"; + let registry = tpak + .entries() + .iter() + .find_map(|e| { + tpak.read(e) + .ok() + .filter(|b| contains(b, format!("{code}\\Movie\\VOICE_ADV.slb").as_bytes())) + }) + .unwrap(); + let ids = movie_voice::registry_voice_ids(®istry); + let stoc = fs::read(format!("{disc}/dat/sound.pak")).unwrap(); + let entries = PakArchive::parse_toc(&stoc).unwrap(); + let hokyu: Vec = movie_manifest::parse(&manifest) + .into_iter() + .filter(|m| m.movie.starts_with("hokyu_")) + .map(|m| m.movie) + .collect(); + for movie in &hokyu { + let bound = movie_manifest::voice_token(&manifest, movie); + let token = bound.clone().or_else(|| hokyu_fallback(movie)); + let Some(token) = token else { + println!("{movie:16} NO TOKEN"); + continue; + }; + let src = if bound.is_some() { "bound" } else { "fallback" }; + let Some(&id) = ids.get(&token) else { + println!("{movie:16} {token} not in registry"); + continue; + }; + let Some(anchor) = ["Movie", "etc", "Voice"].iter().find_map(|d| { + let h = name_hash(&format!("{code}\\{d}\\{token}.slb")); + entries + .binary_search_by_key(&h, |e| e.name_hash) + .ok() + .map(|i| entries[i].offset as u64) + }) else { + println!("{movie:16} no anchor"); + continue; + }; + let win_start = (anchor.saturating_sub(2 * 1024 * 1024)) & !3; + let window = rg(&disc, win_start, 8 * 1024 * 1024); + let Some(el) = movie_voice::find_descriptor(&window, id) else { + println!("{movie:16} desc not found"); + continue; + }; + let end = win_start + el as u64; + let start = movie_voice::find_descriptor(&window, id.wrapping_sub(1)) + .or_else(|| movie_voice::find_descriptor_before(&window, el)) + .map(|o| win_start + o as u64) + .filter(|&s| s < end && end - s < 1_500_000) + .unwrap_or(anchor); + let region = rg(&disc, start, (end - start) as usize); + let mut riffs = slb::to_xma_riffs(®ion); + if riffs.is_empty() { + riffs = slb::to_xma_riff_best(®ion).into_iter().collect(); + } + let mut d = "?".into(); + if let Some(r) = riffs.first() { + let xp = format!("/tmp/hf_{movie}.xma.wav"); + let wp = format!("/tmp/hf_{movie}.wav"); + fs::write(&xp, r).unwrap(); + let _ = Command::new("ffmpeg") + .args(["-hide_banner", "-v", "error", "-y", "-i", &xp, &wp]) + .status(); + d = dur(&wp); + } + let mv = dur(&format!("{disc}/dat/movie/{movie}.wmv")); println!("{movie:16} {src:8} {token:12} id={id} voice={d}s movie={mv}s"); } } diff --git a/crates/sylpheed-formats/examples/idxd_tokens.rs b/crates/sylpheed-formats/examples/idxd_tokens.rs index 4cde36cb..d3e6084f 100644 --- a/crates/sylpheed-formats/examples/idxd_tokens.rs +++ b/crates/sylpheed-formats/examples/idxd_tokens.rs @@ -35,7 +35,9 @@ fn is_enum_value(s: &str) -> bool { fn is_key_like(s: &str) -> bool { !s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') - && s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_') + && s.chars() + .next() + .is_some_and(|c| c.is_ascii_alphabetic() || c == '_') && !is_number(s) && !is_enum_value(s) } @@ -55,15 +57,18 @@ fn main() { let pak = PakArchive::open(&pak_path).expect("open pak"); for entry in pak.entries() { let Ok(bytes) = pak.read(entry) else { continue }; - let Ok(obj) = IdxdObject::parse(&bytes) else { continue }; + let Ok(obj) = IdxdObject::parse(&bytes) else { + continue; + }; let toks = obj.tokens(); // Only entries that actually declare one of the requested sub-record // types (the pool-start heuristic can prefix one stray byte, so match a // short suffix rather than equality -- same rule as the splitter below). - if !toks - .iter() - .any(|t| types.iter().any(|ty| t == ty || (t.ends_with(ty.as_str()) && t.len() <= ty.len() + 2))) - { + if !toks.iter().any(|t| { + types + .iter() + .any(|ty| t == ty || (t.ends_with(ty.as_str()) && t.len() <= ty.len() + 2)) + }) { continue; } diff --git a/crates/sylpheed-formats/examples/index_hash_dump.rs b/crates/sylpheed-formats/examples/index_hash_dump.rs index 8d6f67bd..743f05dc 100644 --- a/crates/sylpheed-formats/examples/index_hash_dump.rs +++ b/crates/sylpheed-formats/examples/index_hash_dump.rs @@ -12,7 +12,9 @@ fn main() { .collect(); files.sort(); for f in &files { - let Ok(bytes) = std::fs::read(f) else { continue }; + let Ok(bytes) = std::fs::read(f) else { + continue; + }; let w = f.file_name().unwrap().to_string_lossy().to_string(); for m in Xbg7Model::stage_models(&bytes) { for (k, sm) in m.meshes.iter().enumerate() { diff --git a/crates/sylpheed-formats/examples/index_pad_check.rs b/crates/sylpheed-formats/examples/index_pad_check.rs index adca8a1d..17af7d32 100644 --- a/crates/sylpheed-formats/examples/index_pad_check.rs +++ b/crates/sylpheed-formats/examples/index_pad_check.rs @@ -36,8 +36,16 @@ fn score(idx: &[u32], pos: &[[f32; 3]], nrm: &[[f32; 3]]) -> (usize, f32, usize) if a >= pos.len() || b >= pos.len() || c >= pos.len() { continue; } - let e1 = [pos[b][0] - pos[a][0], pos[b][1] - pos[a][1], pos[b][2] - pos[a][2]]; - let e2 = [pos[c][0] - pos[a][0], pos[c][1] - pos[a][1], pos[c][2] - pos[a][2]]; + let e1 = [ + pos[b][0] - pos[a][0], + pos[b][1] - pos[a][1], + pos[b][2] - pos[a][2], + ]; + let e2 = [ + pos[c][0] - pos[a][0], + pos[c][1] - pos[a][1], + pos[c][2] - pos[a][2], + ]; let f = [ e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], @@ -53,7 +61,11 @@ fn score(idx: &[u32], pos: &[[f32; 3]], nrm: &[[f32; 3]]) -> (usize, f32, usize) agree += 1; } } - let frac = if counted == 0 { 0.0 } else { agree as f32 / counted as f32 }; + let frac = if counted == 0 { + 0.0 + } else { + agree as f32 / counted as f32 + }; (degen, frac.max(1.0 - frac), counted) } @@ -82,7 +94,8 @@ fn main() { if n < 6 || sm.normals.is_empty() || vb < n * 2 + 2 { continue; } - let read_at = |start: usize| -> Vec { (0..n).map(|k| be16(&bytes, start + k * 2)).collect() }; + let read_at = + |start: usize| -> Vec { (0..n).map(|k| be16(&bytes, start + k * 2)).collect() }; let l0 = read_at(vb - n * 2); let l2 = read_at(vb - n * 2 - 2); // Sanity: l0 must be what the decoder emitted, or the assumption that we diff --git a/crates/sylpheed-formats/examples/invert_capture.rs b/crates/sylpheed-formats/examples/invert_capture.rs index 29842061..6018f852 100644 --- a/crates/sylpheed-formats/examples/invert_capture.rs +++ b/crates/sylpheed-formats/examples/invert_capture.rs @@ -18,11 +18,11 @@ //! [top_n] [--all] //! `--all` lists every capture vcount, not just the `top_n` (default 40) largest. +use std::collections::{HashMap, HashSet}; +use std::path::Path; use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model}; use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog}; use sylpheed_formats::xiso::open_iso; -use std::collections::{HashMap, HashSet}; -use std::path::Path; fn main() { let args: Vec = std::env::args().collect(); @@ -47,10 +47,15 @@ fn main() { // Decode EVERY geometry resource in the stage container, not just one ship's. let bytes = { - let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); rt.block_on(async { let mut r = open_iso(Path::new(&iso)).await.unwrap(); - r.read_file(&format!("hidden/resource3d/{stage}.xpr")).await.unwrap() + r.read_file(&format!("hidden/resource3d/{stage}.xpr")) + .await + .unwrap() }) }; let names = xbg7_resource_names(&bytes); @@ -101,7 +106,11 @@ fn main() { 100.0 * matched_draws as f64 / draws.len().max(1) as f64 ); - let shown = if all { vcounts.len() } else { top_n.min(vcounts.len()) }; + let shown = if all { + vcounts.len() + } else { + top_n.min(vcounts.len()) + }; println!("\nlargest capture vcounts (draws / distinct vbufs) → matching resources:"); for &v in vcounts.iter().take(shown) { let n = draw_count[&v]; @@ -134,9 +143,19 @@ fn main() { .collect(); rows.sort_by(|a, b| a.0.cmp(&b.0)); let drawn = rows.iter().filter(|r| r.2 > 0).count(); - println!("\n{id} resources in {stage}.xpr ({drawn}/{} with a drawn vcount):", rows.len()); + println!( + "\n{id} resources in {stage}.xpr ({drawn}/{} with a drawn vcount):", + rows.len() + ); for (name, v, n) in rows { - println!(" {name:28} vcount {v:6} {}", if n > 0 { format!("DRAWN ×{n}") } else { "—".into() }); + println!( + " {name:28} vcount {v:6} {}", + if n > 0 { + format!("DRAWN ×{n}") + } else { + "—".into() + } + ); } } } @@ -145,12 +164,24 @@ fn main() { // whether the capture ever drew that many vertices. let mut sizes: Vec<(u32, String)> = models .iter() - .map(|m| (m.meshes.iter().map(|s| s.positions.len()).sum::() as u32, m.name.clone())) + .map(|m| { + ( + m.meshes.iter().map(|s| s.positions.len()).sum::() as u32, + m.name.clone(), + ) + }) .collect(); sizes.sort_unstable_by(|a, b| b.0.cmp(&a.0)); println!("\nlargest resources in {stage}.xpr → drawn in the capture?"); for (v, name) in sizes.iter().take(top_n.min(sizes.len())) { let n = draw_count.get(v).copied().unwrap_or(0); - println!(" {name:28} vcount {v:6} {}", if n > 0 { format!("DRAWN ×{n}") } else { "not drawn".into() }); + println!( + " {name:28} vcount {v:6} {}", + if n > 0 { + format!("DRAWN ×{n}") + } else { + "not drawn".into() + } + ); } } diff --git a/crates/sylpheed-formats/examples/iso_map.rs b/crates/sylpheed-formats/examples/iso_map.rs index 78d4a03d..00eedf1c 100644 --- a/crates/sylpheed-formats/examples/iso_map.rs +++ b/crates/sylpheed-formats/examples/iso_map.rs @@ -1,50 +1,125 @@ -use sylpheed_formats::PakArchive; use std::collections::BTreeMap; -fn be32(b:&[u8],o:usize)->u32{ if o+4<=b.len(){u32::from_be_bytes([b[o],b[o+1],b[o+2],b[o+3]])}else{0} } -fn magic(b:&[u8])->String{ - if b.len()<4 {return "(<4B)".into();} - let m=&b[0..4]; - if m.iter().all(|&c|(0x20..0x7f).contains(&c)){ String::from_utf8_lossy(m).into() } - else if m==b"\x89PNG"{"PNG".into()} else if m==[0,1,0,0]{"ttf".into()} - else { format!("{:02x}{:02x}{:02x}{:02x}",m[0],m[1],m[2],m[3]) } +use sylpheed_formats::PakArchive; +fn be32(b: &[u8], o: usize) -> u32 { + if o + 4 <= b.len() { + u32::from_be_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]) + } else { + 0 + } } -fn main(){ - let disc=std::env::var("SYLPHEED_DISC").unwrap(); +fn magic(b: &[u8]) -> String { + if b.len() < 4 { + return "(<4B)".into(); + } + let m = &b[0..4]; + if m.iter().all(|&c| (0x20..0x7f).contains(&c)) { + String::from_utf8_lossy(m).into() + } else if m == b"\x89PNG" { + "PNG".into() + } else if m == [0, 1, 0, 0] { + "ttf".into() + } else { + format!("{:02x}{:02x}{:02x}{:02x}", m[0], m[1], m[2], m[3]) + } +} +fn main() { + let disc = std::env::var("SYLPHEED_DISC").unwrap(); // Known/understood IDXD schemas (semantic parsers we have) - let known_schema:BTreeMap=[(0x067025b9,"ADVERTISE_MOVIE (movie_manifest)"),(0x13cb84ba,"sound registry / sounds.tbl")].into(); - let mut fmt_count:BTreeMap=BTreeMap::new(); // fmt -> (count, bytes) - let mut schema_census:BTreeMap=BTreeMap::new(); // schema -> (count,bytes,sample pak) - let mut unknown_magics:BTreeMap)>=BTreeMap::new(); - let paks:Vec={let mut v=vec![]; for e in std::fs::read_dir(format!("{disc}/dat")).unwrap(){let p=e.unwrap().path(); if p.extension().map(|x|x=="pak").unwrap_or(false){v.push(p.file_stem().unwrap().to_string_lossy().into());}} v.sort(); v}; - println!("pak entries decompressed formats"); - for pk in &paks{ - let Ok(arc)=PakArchive::open(format!("{disc}/dat/{pk}.pak")) else{continue}; - let mut per:BTreeMap=BTreeMap::new(); - let mut tot=0u64; let n=arc.entries().len(); - for e in arc.entries(){ - let Ok(b)=arc.read(e) else{continue}; - tot+=b.len() as u64; - let mut f=magic(&b); - if f=="IDXD"{ let s=be32(&b,8); schema_census.entry(s).or_insert((0,0,pk.clone())).0+=1; schema_census.get_mut(&s).unwrap().1+=b.len() as u64; f=format!("IDXD:{s:08x}"); } - else if !["XPR2","IXUD","LSTA","RATC","RIFF","XBG7","OTTO","PNG","ttf","DDS ","T8AD"].contains(&f.as_str()){ - let ent=unknown_magics.entry(f.clone()).or_insert((0,vec![])); ent.0+=1; if ent.1.len()<3 && !ent.1.contains(pk){ent.1.push(pk.clone());} + let known_schema: BTreeMap = [ + (0x067025b9, "ADVERTISE_MOVIE (movie_manifest)"), + (0x13cb84ba, "sound registry / sounds.tbl"), + ] + .into(); + let mut fmt_count: BTreeMap = BTreeMap::new(); // fmt -> (count, bytes) + let mut schema_census: BTreeMap = BTreeMap::new(); // schema -> (count,bytes,sample pak) + let mut unknown_magics: BTreeMap)> = BTreeMap::new(); + let paks: Vec = { + let mut v = vec![]; + for e in std::fs::read_dir(format!("{disc}/dat")).unwrap() { + let p = e.unwrap().path(); + if p.extension().map(|x| x == "pak").unwrap_or(false) { + v.push(p.file_stem().unwrap().to_string_lossy().into()); } - *per.entry(if f.starts_with("IDXD:"){"IDXD".into()}else{f.clone()}).or_default()+=1; - let g=fmt_count.entry(if f.starts_with("IDXD:"){"IDXD".into()}else{f}).or_insert((0,0)); g.0+=1; g.1+=b.len() as u64; } - let mut fs:Vec<_>=per.into_iter().collect(); fs.sort_by_key(|x|std::cmp::Reverse(x.1)); - let top:String=fs.iter().take(4).map(|(k,v)|format!("{k}×{v}")).collect::>().join(" "); - println!("{pk:26} {n:>7} {:>10}KB {top}", tot/1024); + v.sort(); + v + }; + println!("pak entries decompressed formats"); + for pk in &paks { + let Ok(arc) = PakArchive::open(format!("{disc}/dat/{pk}.pak")) else { + continue; + }; + let mut per: BTreeMap = BTreeMap::new(); + let mut tot = 0u64; + let n = arc.entries().len(); + for e in arc.entries() { + let Ok(b) = arc.read(e) else { continue }; + tot += b.len() as u64; + let mut f = magic(&b); + if f == "IDXD" { + let s = be32(&b, 8); + schema_census.entry(s).or_insert((0, 0, pk.clone())).0 += 1; + schema_census.get_mut(&s).unwrap().1 += b.len() as u64; + f = format!("IDXD:{s:08x}"); + } else if ![ + "XPR2", "IXUD", "LSTA", "RATC", "RIFF", "XBG7", "OTTO", "PNG", "ttf", "DDS ", + "T8AD", + ] + .contains(&f.as_str()) + { + let ent = unknown_magics.entry(f.clone()).or_insert((0, vec![])); + ent.0 += 1; + if ent.1.len() < 3 && !ent.1.contains(pk) { + ent.1.push(pk.clone()); + } + } + *per.entry(if f.starts_with("IDXD:") { + "IDXD".into() + } else { + f.clone() + }) + .or_default() += 1; + let g = fmt_count + .entry(if f.starts_with("IDXD:") { + "IDXD".into() + } else { + f + }) + .or_insert((0, 0)); + g.0 += 1; + g.1 += b.len() as u64; + } + let mut fs: Vec<_> = per.into_iter().collect(); + fs.sort_by_key(|x| std::cmp::Reverse(x.1)); + let top: String = fs + .iter() + .take(4) + .map(|(k, v)| format!("{k}×{v}")) + .collect::>() + .join(" "); + println!("{pk:26} {n:>7} {:>10}KB {top}", tot / 1024); } println!("\n=== FORMAT TOTALS (across all paks) ==="); - let mut fv:Vec<_>=fmt_count.into_iter().collect(); fv.sort_by_key(|x|std::cmp::Reverse(x.1.1)); - for (f,(c,b)) in &fv{ println!(" {f:12} {c:>6} entries {:>8}KB", b/1024); } - println!("\n=== IDXD SCHEMA CENSUS ({} distinct schemas) ===", schema_census.len()); - let mut sv:Vec<_>=schema_census.into_iter().collect(); sv.sort_by_key(|x|std::cmp::Reverse(x.1.1)); - for (s,(c,b,pk)) in &sv{ - let tag=known_schema.get(s).copied().unwrap_or("??? UNDECODED"); - println!(" {s:08x} {c:>5} ent {:>7}KB e.g. {pk:22} {tag}", b/1024); + let mut fv: Vec<_> = fmt_count.into_iter().collect(); + fv.sort_by_key(|x| std::cmp::Reverse(x.1 .1)); + for (f, (c, b)) in &fv { + println!(" {f:12} {c:>6} entries {:>8}KB", b / 1024); + } + println!( + "\n=== IDXD SCHEMA CENSUS ({} distinct schemas) ===", + schema_census.len() + ); + let mut sv: Vec<_> = schema_census.into_iter().collect(); + sv.sort_by_key(|x| std::cmp::Reverse(x.1 .1)); + for (s, (c, b, pk)) in &sv { + let tag = known_schema.get(s).copied().unwrap_or("??? UNDECODED"); + println!( + " {s:08x} {c:>5} ent {:>7}KB e.g. {pk:22} {tag}", + b / 1024 + ); } println!("\n=== UNKNOWN / UNCLASSIFIED MAGICS ==="); - for (m,(c,pks)) in &unknown_magics{ println!(" {m:12} ×{c:<5} in {pks:?}"); } + for (m, (c, pks)) in &unknown_magics { + println!(" {m:12} ×{c:<5} in {pks:?}"); + } } diff --git a/crates/sylpheed-formats/examples/live_offsets.rs b/crates/sylpheed-formats/examples/live_offsets.rs index 8db238f3..0572f651 100644 --- a/crates/sylpheed-formats/examples/live_offsets.rs +++ b/crates/sylpheed-formats/examples/live_offsets.rs @@ -15,7 +15,10 @@ fn main() { let disc = a.next().expect("disc root"); let dump = a.next().expect("live dump"); let pairs: Vec<(String, String)> = a - .filter_map(|s| s.split_once('=').map(|(i, v)| (i.to_string(), v.to_string()))) + .filter_map(|s| { + s.split_once('=') + .map(|(i, v)| (i.to_string(), v.to_string())) + }) .collect(); // live: va -> offset -> f32 @@ -28,8 +31,7 @@ fn main() { } let f: Vec<&str> = line.split_whitespace().collect(); if f.len() >= 4 && f[1].starts_with('+') { - if let (Ok(off), Ok(val)) = - (usize::from_str_radix(&f[1][1..], 16), f[3].parse::()) + if let (Ok(off), Ok(val)) = (usize::from_str_radix(&f[1][1..], 16), f[3].parse::()) { live.entry(cur.clone()).or_default().insert(off, val); } @@ -42,9 +44,15 @@ fn main() { let mut units = 0usize; for entry in pak.entries() { let Ok(bytes) = pak.read(entry) else { continue }; - let Ok(obj) = IdxdObject::parse(&bytes) else { continue }; - let Some(id) = obj.get_raw("ID") else { continue }; - let Some((_, va)) = pairs.iter().find(|(i, _)| i == id) else { continue }; + let Ok(obj) = IdxdObject::parse(&bytes) else { + continue; + }; + let Some(id) = obj.get_raw("ID") else { + continue; + }; + let Some((_, va)) = pairs.iter().find(|(i, _)| i == id) else { + continue; + }; let Some(words) = live.get(va) else { continue }; units += 1; let mut numeric = 0usize; @@ -58,7 +66,11 @@ fn main() { let mut found = false; for (off, w) in words { if (*w - v).abs() <= v.abs() * 1e-6 { - *votes.entry(key.clone()).or_default().entry(*off).or_default() += 1; + *votes + .entry(key.clone()) + .or_default() + .entry(*off) + .or_default() += 1; found = true; } } diff --git a/crates/sylpheed-formats/examples/miss_targets.rs b/crates/sylpheed-formats/examples/miss_targets.rs index 432ffc24..d34cc4e8 100644 --- a/crates/sylpheed-formats/examples/miss_targets.rs +++ b/crates/sylpheed-formats/examples/miss_targets.rs @@ -13,8 +13,8 @@ //! fixing, but the geometry is already recoverable, so a capture is not needed. //! //! Usage: miss_targets -use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model}; use std::collections::{BTreeMap, BTreeSet}; +use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model}; fn main() { let dir = std::env::args().nth(1).expect("resource3d dir"); @@ -30,14 +30,18 @@ fn main() { let mut ok: BTreeMap> = BTreeMap::new(); let mut miss: BTreeMap> = BTreeMap::new(); for f in &files { - let Ok(bytes) = std::fs::read(f) else { continue }; + let Ok(bytes) = std::fs::read(f) else { + continue; + }; let where_ = f.file_name().unwrap().to_string_lossy().to_string(); let declared: BTreeSet = xbg7_resource_names(&bytes).into_iter().collect(); if declared.is_empty() { continue; } - let decoded: BTreeSet = - Xbg7Model::stage_models(&bytes).into_iter().map(|m| m.name).collect(); + let decoded: BTreeSet = Xbg7Model::stage_models(&bytes) + .into_iter() + .map(|m| m.name) + .collect(); for n in &declared { if decoded.contains(n) { ok.entry(n.clone()).or_default().insert(where_.clone()); @@ -52,7 +56,10 @@ fn main() { let recoverable: Vec<&String> = miss.keys().filter(|n| ok.contains_key(*n)).collect(); println!("resources that decode NOWHERE: {}", never.len()); - println!("resources that miss somewhere but decode elsewhere: {}\n", recoverable.len()); + println!( + "resources that miss somewhere but decode elsewhere: {}\n", + recoverable.len() + ); // Rank containers by how many never-decoding resources they hold. let mut per_container: BTreeMap<&String, Vec<&String>> = BTreeMap::new(); @@ -73,7 +80,10 @@ fn main() { // reachable through whatever loads that container. let excl: Vec<(&&String, &&BTreeSet)> = never.iter().filter(|(_, w)| w.len() == 1).collect(); - println!("\nof the never-decoding, {} live in exactly one container", excl.len()); + println!( + "\nof the never-decoding, {} live in exactly one container", + excl.len() + ); let mut by_one: BTreeMap<&String, Vec<&String>> = BTreeMap::new(); for (n, w) in &excl { by_one.entry(w.iter().next().unwrap()).or_default().push(n); @@ -81,6 +91,15 @@ fn main() { let mut b: Vec<_> = by_one.iter().collect(); b.sort_by_key(|(_, v)| std::cmp::Reverse(v.len())); for (w, v) in b.iter().take(10) { - println!(" {:<24} {:>3} {}", w, v.len(), v.iter().take(5).map(|s| s.as_str()).collect::>().join(", ")); + println!( + " {:<24} {:>3} {}", + w, + v.len(), + v.iter() + .take(5) + .map(|s| s.as_str()) + .collect::>() + .join(", ") + ); } } diff --git a/crates/sylpheed-formats/examples/mission_map.rs b/crates/sylpheed-formats/examples/mission_map.rs index 24772c59..db073f9b 100644 --- a/crates/sylpheed-formats/examples/mission_map.rs +++ b/crates/sylpheed-formats/examples/mission_map.rs @@ -1,46 +1,96 @@ use sylpheed_formats::{idxd::IdxdObject, PakArchive}; -fn main(){ - let disc=std::env::var("SYLPHEED_DISC").unwrap(); - let arc=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); +fn main() { + let disc = std::env::var("SYLPHEED_DISC").unwrap(); + let arc = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); // 1. All stages: BackGroundID + phases + stage tag (from EnumUnit_SNN) println!("=== STAGES (StageResource 3c9ae32e) ==="); - let mut rows=vec![]; - for e in arc.entries(){ - let Ok(b)=arc.read(e) else{continue}; let Ok(o)=IdxdObject::parse(&b) else{continue}; - if o.schema_hash!=0x3c9ae32e {continue;} - let t=o.tokens(); - let bg=o.get_raw("BackGroundID").unwrap_or("?").to_string(); - let stage=t.iter().find_map(|s|s.strip_prefix("EnumUnit_").map(|x|x.trim_end_matches(".tbl").to_string())).unwrap_or("?".into()); - let phases=t.iter().filter(|s|s.starts_with("Phase_")).count(); - let unitgrp=t.iter().any(|s|s.starts_with("UnitGroup_")); - let route=t.iter().any(|s|s.starts_with("Route_")); - let formation=t.iter().any(|s|s.starts_with("FormationSet_")); - rows.push((stage,bg,phases,unitgrp,route,formation)); + let mut rows = vec![]; + for e in arc.entries() { + let Ok(b) = arc.read(e) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { + continue; + }; + if o.schema_hash != 0x3c9ae32e { + continue; + } + let t = o.tokens(); + let bg = o.get_raw("BackGroundID").unwrap_or("?").to_string(); + let stage = t + .iter() + .find_map(|s| { + s.strip_prefix("EnumUnit_") + .map(|x| x.trim_end_matches(".tbl").to_string()) + }) + .unwrap_or("?".into()); + let phases = t.iter().filter(|s| s.starts_with("Phase_")).count(); + let unitgrp = t.iter().any(|s| s.starts_with("UnitGroup_")); + let route = t.iter().any(|s| s.starts_with("Route_")); + let formation = t.iter().any(|s| s.starts_with("FormationSet_")); + rows.push((stage, bg, phases, unitgrp, route, formation)); } rows.sort(); - for (s,bg,p,ug,rt,fm) in &rows{ println!(" {s:8} location={bg:14} phases={p} [squadrons:{} route:{} formations:{}]", if *ug{"Y"}else{"·"},if *rt{"Y"}else{"·"},if *fm{"Y"}else{"·"}); } + for (s, bg, p, ug, rt, fm) in &rows { + println!( + " {s:8} location={bg:14} phases={p} [squadrons:{} route:{} formations:{}]", + if *ug { "Y" } else { "·" }, + if *rt { "Y" } else { "·" }, + if *fm { "Y" } else { "·" } + ); + } // 2. Parse objectives table into (stage,phase) -> key counts println!("\n=== OBJECTIVES (033b5b7e) — per stage/phase key groups ==="); - for e in arc.entries(){ - let Ok(b)=arc.read(e) else{continue}; let Ok(o)=IdxdObject::parse(&b) else{continue}; - if o.schema_hash!=0x033b5b7e {continue;} - let t=o.tokens(); - let mut groups:std::collections::BTreeMap=Default::default(); - for tok in t{ if let Some(p)=tok.find("_Objective_").or_else(||tok.find("_Lose_")).or_else(||tok.find("_Hint_")){ let key=&tok[..p]; *groups.entry(key.trim_start_matches(|c:char|!c.is_ascii_alphabetic()).into()).or_default()+=1; } } - let stages:std::collections::BTreeSet=groups.keys().filter_map(|k|k.get(..3).map(|s|s.to_string())).collect(); + for e in arc.entries() { + let Ok(b) = arc.read(e) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { + continue; + }; + if o.schema_hash != 0x033b5b7e { + continue; + } + let t = o.tokens(); + let mut groups: std::collections::BTreeMap = Default::default(); + for tok in t { + if let Some(p) = tok + .find("_Objective_") + .or_else(|| tok.find("_Lose_")) + .or_else(|| tok.find("_Hint_")) + { + let key = &tok[..p]; + *groups + .entry( + key.trim_start_matches(|c: char| !c.is_ascii_alphabetic()) + .into(), + ) + .or_default() += 1; + } + } + let stages: std::collections::BTreeSet = groups + .keys() + .filter_map(|k| k.get(..3).map(|s| s.to_string())) + .collect(); println!(" covers {} stages: {:?}", stages.len(), stages); - println!(" sample S01_P1 group counts: {:?}", groups.iter().filter(|(k,_)|k.starts_with("S01_P1")).collect::>()); + println!( + " sample S01_P1 group counts: {:?}", + groups + .iter() + .filter(|(k, _)| k.starts_with("S01_P1")) + .collect::>() + ); break; } // 3. Resolve objective TEXT: search all pak entries for the key "S01_P1_Objective_00" as an ID with English text println!("\n=== OBJECTIVE TEXT resolution probe ==="); - for e in arc.entries(){ - let Ok(b)=arc.read(e) else{continue}; let Ok(o)=IdxdObject::parse(&b) else{continue}; - let t=o.tokens(); - if let Some(i)=t.iter().position(|s|s=="S01_P1_Objective_00"){ - let lo=i.saturating_sub(1); let hi=(i+3).min(t.len()); + for e in arc.entries() { + let Ok(b) = arc.read(e) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { + continue; + }; + let t = o.tokens(); + if let Some(i) = t.iter().position(|s| s == "S01_P1_Objective_00") { + let lo = i.saturating_sub(1); + let hi = (i + 3).min(t.len()); println!(" schema {:08x}: ...{:?}...", o.schema_hash, &t[lo..hi]); } } diff --git a/crates/sylpheed-formats/examples/name_resolution.rs b/crates/sylpheed-formats/examples/name_resolution.rs index e87149a1..9c9ba7ef 100644 --- a/crates/sylpheed-formats/examples/name_resolution.rs +++ b/crates/sylpheed-formats/examples/name_resolution.rs @@ -10,7 +10,9 @@ use sylpheed_formats::{pak, ui_layout}; fn main() { - let path = std::env::args().nth(1).expect("usage: name_resolution [entry]"); + let path = std::env::args() + .nth(1) + .expect("usage: name_resolution [entry]"); let want: Option = std::env::args().nth(2).and_then(|s| s.parse().ok()); let ar = pak::PakArchive::open(&path).expect("open pak"); let entries: Vec<_> = ar.entries().to_vec(); @@ -19,16 +21,24 @@ fn main() { continue; } let Ok(bytes) = ar.read(e) else { continue }; - let Some(build) = ui_layout::parse_build(&bytes) else { continue }; + let Some(build) = ui_layout::parse_build(&bytes) else { + continue; + }; let unresolved: Vec<&ui_layout::Element> = build .elements .iter() .filter(|el| el.sprite.is_none() && el.kind & 0x10 == 0) .collect(); - let claimed: std::collections::HashSet<&str> = - build.elements.iter().filter_map(|e| e.sprite.as_deref()).collect(); - let unclaimed: Vec<&String> = - build.sprites.keys().filter(|k| !claimed.contains(k.as_str())).collect(); + let claimed: std::collections::HashSet<&str> = build + .elements + .iter() + .filter_map(|e| e.sprite.as_deref()) + .collect(); + let unclaimed: Vec<&String> = build + .sprites + .keys() + .filter(|k| !claimed.contains(k.as_str())) + .collect(); if want.is_none() && unresolved.is_empty() && unclaimed.is_empty() { continue; } @@ -39,12 +49,21 @@ fn main() { .collect(); println!( "entry {i:3} {:2} elements UNRESOLVED {:?} UNCLAIMED {:?}", - build.elements.len(), un, uc + build.elements.len(), + un, + uc ); if want.is_some() { for el in &build.elements { - let mark = if el.sprite.is_none() && el.kind & 0x10 == 0 { " <-- UNRESOLVED" } else { "" }; - println!(" [{:2}] kind {:#06x} {:28} -> {:?}{mark}", el.index, el.kind, el.name, el.sprite); + let mark = if el.sprite.is_none() && el.kind & 0x10 == 0 { + " <-- UNRESOLVED" + } else { + "" + }; + println!( + " [{:2}] kind {:#06x} {:28} -> {:?}{mark}", + el.index, el.kind, el.name, el.sprite + ); } } } diff --git a/crates/sylpheed-formats/examples/node_dump.rs b/crates/sylpheed-formats/examples/node_dump.rs index 7c1ceada..d51c14ea 100644 --- a/crates/sylpheed-formats/examples/node_dump.rs +++ b/crates/sylpheed-formats/examples/node_dump.rs @@ -40,7 +40,11 @@ fn main() { j += 1; } let name = std::str::from_utf8(&d[i..j]).unwrap_or("?").to_string(); - if name.ends_with("_col") || name.ends_with("_spc") || name.ends_with("_gls") || name.ends_with("_lum") { + if name.ends_with("_col") + || name.ends_with("_spc") + || name.ends_with("_gls") + || name.ends_with("_lum") + { i = j.max(i + 1); continue; } @@ -74,10 +78,18 @@ fn main() { if p == 0 || p + 24 > d.len() { continue; } - let a = if p >= 0x48 { be_f64(d, p - 0x48) } else { f64::NAN }; + let a = if p >= 0x48 { + be_f64(d, p - 0x48) + } else { + f64::NAN + }; let b = be_f64(d, p + 8); let head = if p >= 0x50 { be32(d, p - 0x50) } else { 0 }; - let t_a = if p >= 0x50 { be_f64(d, p - 0x50) } else { f64::NAN }; + let t_a = if p >= 0x50 { + be_f64(d, p - 0x50) + } else { + f64::NAN + }; let t_b = be_f64(d, p); println!( " slot{k:2} @{p:#x} head={head:08X}: A={a:12.5} (t={t_a:8.3}) B={b:12.5} (t={t_b:8.3})" diff --git a/crates/sylpheed-formats/examples/node_scale.rs b/crates/sylpheed-formats/examples/node_scale.rs index e966ba17..cfb196d0 100644 --- a/crates/sylpheed-formats/examples/node_scale.rs +++ b/crates/sylpheed-formats/examples/node_scale.rs @@ -12,9 +12,15 @@ fn main() { println!( "{:<26} s[{:7.3}{:7.3}{:7.3}] |m rows|[{:7.3}{:7.3}{:7.3}] t[{:9.1}{:9.1}{:9.1}]", n.resource, - n.s[0], n.s[1], n.s[2], - norm(n.m[0]), norm(n.m[1]), norm(n.m[2]), - n.t[0], n.t[1], n.t[2] + n.s[0], + n.s[1], + n.s[2], + norm(n.m[0]), + norm(n.m[1]), + norm(n.m[2]), + n.t[0], + n.t[1], + n.t[2] ); } } diff --git a/crates/sylpheed-formats/examples/pad_shift_audit.rs b/crates/sylpheed-formats/examples/pad_shift_audit.rs index d26606d2..b2004495 100644 --- a/crates/sylpheed-formats/examples/pad_shift_audit.rs +++ b/crates/sylpheed-formats/examples/pad_shift_audit.rs @@ -22,8 +22,16 @@ fn winding(idx: &[u32], pos: &[[f32; 3]], nrm: &[[f32; 3]]) -> f32 { if a == b || b == c || a == c || a.max(b).max(c) >= pos.len() || a >= nrm.len() { continue; } - let e1 = [pos[b][0] - pos[a][0], pos[b][1] - pos[a][1], pos[b][2] - pos[a][2]]; - let e2 = [pos[c][0] - pos[a][0], pos[c][1] - pos[a][1], pos[c][2] - pos[a][2]]; + let e1 = [ + pos[b][0] - pos[a][0], + pos[b][1] - pos[a][1], + pos[b][2] - pos[a][2], + ]; + let e2 = [ + pos[c][0] - pos[a][0], + pos[c][1] - pos[a][1], + pos[c][2] - pos[a][2], + ]; let f = [ e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], @@ -63,7 +71,9 @@ fn main() { let mut weak_wind: Vec<(f32, f32, String)> = Vec::new(); let mut worst: Vec<(usize, String, String)> = Vec::new(); for f in &files { - let Ok(bytes) = std::fs::read(f) else { continue }; + let Ok(bytes) = std::fs::read(f) else { + continue; + }; let where_ = f.file_name().unwrap().to_string_lossy().to_string(); let mut c_moved = 0usize; for m in Xbg7Model::stage_models(&bytes) { @@ -102,7 +112,9 @@ fn main() { // noise between two equally clean readings. let w0 = winding(&pad0, &sm.positions, &sm.normals); let w2: f32 = { - let l2: Vec = (0..n).map(|k| be16(&bytes, vb - n * 2 - 2 + k * 2)).collect(); + let l2: Vec = (0..n) + .map(|k| be16(&bytes, vb - n * 2 - 2 + k * 2)) + .collect(); winding(&l2, &sm.positions, &sm.normals) }; weak_wind.push((w0, w2, m.name.clone())); @@ -115,9 +127,17 @@ fn main() { } println!("\n{moved} of {total} single-block resources moved off the pad-0 run"); println!("{still_degen} decoded runs still contain a degenerate triangle"); - println!("of the moved, {weak} had a pad-0 run with no degenerate triangle (moved on winding alone)"); - let clear = weak_wind.iter().filter(|(w0, w2, _)| *w2 - *w0 > 0.15).count(); - let close = weak_wind.iter().filter(|(w0, w2, _)| (*w2 - *w0).abs() <= 0.05).count(); + println!( + "of the moved, {weak} had a pad-0 run with no degenerate triangle (moved on winding alone)" + ); + let clear = weak_wind + .iter() + .filter(|(w0, w2, _)| *w2 - *w0 > 0.15) + .count(); + let close = weak_wind + .iter() + .filter(|(w0, w2, _)| (*w2 - *w0).abs() <= 0.05) + .count(); println!(" of those: {clear} show the shift signature (pad-2 winding > pad-0 by >0.15), {close} are within 0.05 (noise)"); for (w0, w2, n) in weak_wind.iter().take(8) { println!(" {n}: pad0 wind {w0:.3} -> pad2 {w2:.3}"); diff --git a/crates/sylpheed-formats/examples/paint_order_audit.rs b/crates/sylpheed-formats/examples/paint_order_audit.rs index 69cd1f31..79689082 100644 --- a/crates/sylpheed-formats/examples/paint_order_audit.rs +++ b/crates/sylpheed-formats/examples/paint_order_audit.rs @@ -11,84 +11,151 @@ use sylpheed_formats::{pak, ui_layout}; fn measured(names: &[&str]) -> Option<(&'static str, Vec)> { 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", + "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_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", + "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])); + 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 == 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])); + 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 "); + let path = std::env::args() + .nth(1) + .expect("usage: paint_order_audit "); 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 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 = build.elements.iter() + let keys_all: Vec = 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; } + 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 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; } + 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; } + 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 }; + 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; } + 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()); + 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; } + 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 }; + 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 { @@ -101,7 +168,9 @@ fn main() { }; checked += 1; let got = ui_layout::derived_paint_order(&build, &bytes); - let keys: Vec = build.elements.iter() + let keys: Vec = build + .elements + .iter() .map(|e| ui_layout::sprite_layer_key(&build, &bytes, e).unwrap_or(u32::MAX)) .collect(); let exact = got == want; @@ -110,7 +179,9 @@ fn main() { // tie the sort cannot resolve) versus a genuine key-order conflict? let pos_got: Vec = { let mut p = vec![0; got.len()]; - for (r, &e) in got.iter().enumerate() { p[e] = r; } + for (r, &e) in got.iter().enumerate() { + p[e] = r; + } p }; let (mut inv, mut tied) = (0, 0); @@ -119,12 +190,17 @@ fn main() { let (x, y) = (want[a], want[b]); if pos_got[x] > pos_got[y] { inv += 1; - if keys[x] == keys[y] { tied += 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!( + " derived == measured : {}", + if exact { "YES" } else { "NO" } + ); println!(" inverted pairs : {inv} (of which same-layer-key ties: {tied})"); if !exact { println!(" measured: {want:?}"); diff --git a/crates/sylpheed-formats/examples/params.rs b/crates/sylpheed-formats/examples/params.rs index ce8eaea8..0eb304a8 100644 --- a/crates/sylpheed-formats/examples/params.rs +++ b/crates/sylpheed-formats/examples/params.rs @@ -1,13 +1,18 @@ -fn main(){ - let a:Vec=std::env::args().collect(); - let bytes=std::fs::read(&a[1]).unwrap(); - let filter=a.get(2).cloned().unwrap_or_default(); +fn main() { + let a: Vec = std::env::args().collect(); + let bytes = std::fs::read(&a[1]).unwrap(); + let filter = a.get(2).cloned().unwrap_or_default(); for n in sylpheed_formats::mesh::xbg7_resource_names(&bytes) { - if !filter.is_empty() && !n.contains(&filter) { continue } - if let Some((m,stride))=sylpheed_formats::mesh::debug_resource_params(&bytes,&n) { + if !filter.is_empty() && !n.contains(&filter) { + continue; + } + if let Some((m, stride)) = sylpheed_formats::mesh::debug_resource_params(&bytes, &n) { let strides = sylpheed_formats::mesh::debug_decl_strides(&bytes, &n); - println!("{n:<28} decl-stride {stride} markers {:?} per-sub-mesh strides {:?}", - &m[..m.len().min(4)], strides); + println!( + "{n:<28} decl-stride {stride} markers {:?} per-sub-mesh strides {:?}", + &m[..m.len().min(4)], + strides + ); } } } diff --git a/crates/sylpheed-formats/examples/plateauless_suppression.rs b/crates/sylpheed-formats/examples/plateauless_suppression.rs index e8ef1890..57c489cd 100644 --- a/crates/sylpheed-formats/examples/plateauless_suppression.rs +++ b/crates/sylpheed-formats/examples/plateauless_suppression.rs @@ -15,7 +15,9 @@ use sylpheed_formats::{pak, ui_layout}; fn main() { - let pak_path = std::env::args().nth(1).expect("usage: ..."); + let pak_path = std::env::args() + .nth(1) + .expect("usage: ..."); let ar = pak::PakArchive::open(&pak_path).expect("open"); let entries: Vec<_> = ar.entries().to_vec(); let outdir = std::env::args().nth(2).expect("outdir"); @@ -23,30 +25,43 @@ fn main() { for spec in std::env::args().skip(3) { let idx: usize = spec.parse().unwrap(); let bytes = ar.read(&entries[idx]).expect("read"); - let Some(build) = ui_layout::parse_build(&bytes) else { continue }; + let Some(build) = ui_layout::parse_build(&bytes) else { + continue; + }; // plateau-less = rest() had to guess: no two adjacent keyframes share a pose // Default: suppress plateau-less elements. With SUPPRESS_SUBSTR set, // suppress every element whose NAME contains it instead — used to test // the entry→hold→exit model's prediction that the splash glows are all // finished by the moment the logos are up. let by_name = std::env::var("SUPPRESS_SUBSTR").ok(); - let mask: Vec = build.elements.iter().map(|e| { - if let Some(sub) = &by_name { - return !e.name.to_lowercase().contains(sub.as_str()); - } - let k = &e.keyframes; - (0..k.len().saturating_sub(1)).any(|i| { - k[i].fade == k[i+1].fade && k[i].scale_x == k[i+1].scale_x - && k[i].scale_y == k[i+1].scale_y && k[i].x == k[i+1].x && k[i].y == k[i+1].y + let mask: Vec = build + .elements + .iter() + .map(|e| { + if let Some(sub) = &by_name { + return !e.name.to_lowercase().contains(sub.as_str()); + } + let k = &e.keyframes; + (0..k.len().saturating_sub(1)).any(|i| { + k[i].fade == k[i + 1].fade + && k[i].scale_x == k[i + 1].scale_x + && k[i].scale_y == k[i + 1].scale_y + && k[i].x == k[i + 1].x + && k[i].y == k[i + 1].y + }) }) - }).collect(); + .collect(); let suppressed = mask.iter().filter(|m| !**m).count(); let opts = ui_layout::ComposeOptions::default(); let a = ui_layout::compose(&build, &bytes, opts, None); let b = ui_layout::compose(&build, &bytes, opts, Some(&mask)); std::fs::write(format!("{outdir}/entry{idx:02}_asis.raw"), &a.rgba).unwrap(); std::fs::write(format!("{outdir}/entry{idx:02}_suppressed.raw"), &b.rgba).unwrap(); - println!("entry {idx:2} {}x{} elements {:2} plateau-less suppressed {suppressed}", - a.width, a.height, build.elements.len()); + println!( + "entry {idx:2} {}x{} elements {:2} plateau-less suppressed {suppressed}", + a.width, + a.height, + build.elements.len() + ); } } diff --git a/crates/sylpheed-formats/examples/pool_window.rs b/crates/sylpheed-formats/examples/pool_window.rs index 1cd2dd78..7d1d0dad 100644 --- a/crates/sylpheed-formats/examples/pool_window.rs +++ b/crates/sylpheed-formats/examples/pool_window.rs @@ -10,9 +10,13 @@ fn main() { let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); for e in pak.entries() { let Ok(b) = pak.read(e) else { continue }; - let Ok(o) = IdxdObject::parse(&b) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { + continue; + }; let Some(id) = o.get_raw("ID") else { continue }; - if !id.contains(&want) { continue; } + if !id.contains(&want) { + continue; + } let t = o.tokens(); println!("=== {id} get_f32({key}) = {:?}", o.get_f32(&key)); for (i, tok) in t.iter().enumerate() { @@ -20,7 +24,11 @@ fn main() { let lo = i.saturating_sub(6); let hi = (i + 7).min(t.len()); for j in lo..hi { - println!(" [{j}]{} {:?}", if j == i { " <-- key" } else { " " }, t[j]); + println!( + " [{j}]{} {:?}", + if j == i { " <-- key" } else { " " }, + t[j] + ); } println!(); } diff --git a/crates/sylpheed-formats/examples/probe_anchor.rs b/crates/sylpheed-formats/examples/probe_anchor.rs index 5c378a28..7b00684a 100644 --- a/crates/sylpheed-formats/examples/probe_anchor.rs +++ b/crates/sylpheed-formats/examples/probe_anchor.rs @@ -2,5 +2,8 @@ fn main() { let a: Vec = std::env::args().collect(); let bytes = std::fs::read(&a[1]).unwrap(); let vb: usize = a[3].parse().unwrap(); - println!("{:?}", sylpheed_formats::mesh::debug_try_anchor(&bytes, &a[2], vb, 3)); + println!( + "{:?}", + sylpheed_formats::mesh::debug_try_anchor(&bytes, &a[2], vb, 3) + ); } diff --git a/crates/sylpheed-formats/examples/rat_leaf_placement.rs b/crates/sylpheed-formats/examples/rat_leaf_placement.rs index 9bac6e26..3eaa0f53 100644 --- a/crates/sylpheed-formats/examples/rat_leaf_placement.rs +++ b/crates/sylpheed-formats/examples/rat_leaf_placement.rs @@ -21,7 +21,9 @@ fn main() { for (ei, e) in entries.iter().enumerate() { let Ok(bytes) = ar.read(e) else { continue }; - let Some(kids) = ratc::parse(&bytes) else { continue }; + let Some(kids) = ratc::parse(&bytes) else { + continue; + }; // Only the title-family bundles carry ptbtn records. if !kids.iter().any(|c| c.name == "ptbtn01f.rat") { continue; diff --git a/crates/sylpheed-formats/examples/ratc_child_names.rs b/crates/sylpheed-formats/examples/ratc_child_names.rs index 77668af9..eb0a737a 100644 --- a/crates/sylpheed-formats/examples/ratc_child_names.rs +++ b/crates/sylpheed-formats/examples/ratc_child_names.rs @@ -41,15 +41,21 @@ fn main() { let mut agree = 0usize; let mut disagree: Vec<(String, usize, String, String)> = Vec::new(); for path in std::env::args().skip(1) { - let Ok(ar) = pak::PakArchive::open(&path) else { continue }; + let Ok(ar) = pak::PakArchive::open(&path) else { + continue; + }; let short = path.rsplit('/').next().unwrap_or(&path).to_string(); let entries: Vec<_> = ar.entries().to_vec(); for (i, e) in entries.iter().enumerate() { let Ok(bytes) = ar.read(e) else { continue }; - let Some(kids) = ratc::parse(&bytes) else { continue }; + let Some(kids) = ratc::parse(&bytes) else { + continue; + }; for c in &kids { children += 1; - let Some(o) = opt_name(&bytes, c.offset) else { continue }; + let Some(o) = opt_name(&bytes, c.offset) else { + continue; + }; with_opt += 1; if o == c.name { agree += 1; diff --git a/crates/sylpheed-formats/examples/ratc_optless_children.rs b/crates/sylpheed-formats/examples/ratc_optless_children.rs index 7f9e7755..5c722632 100644 --- a/crates/sylpheed-formats/examples/ratc_optless_children.rs +++ b/crates/sylpheed-formats/examples/ratc_optless_children.rs @@ -70,15 +70,21 @@ fn main() { let mut rows: Vec<(String, usize, usize, String, String, Why)> = Vec::new(); let mut first_child_of_bundle = 0usize; for path in std::env::args().skip(1) { - let Ok(ar) = pak::PakArchive::open(&path) else { continue }; + let Ok(ar) = pak::PakArchive::open(&path) else { + continue; + }; let short = path.rsplit('/').next().unwrap_or(&path).to_string(); let entries: Vec<_> = ar.entries().to_vec(); for (ei, e) in entries.iter().enumerate() { let Ok(bytes) = ar.read(e) else { continue }; - let Some(kids) = ratc::parse(&bytes) else { continue }; + let Some(kids) = ratc::parse(&bytes) else { + continue; + }; for (ci, c) in kids.iter().enumerate() { total += 1; - let Some(why) = classify(&bytes, c.offset) else { continue }; + let Some(why) = classify(&bytes, c.offset) else { + continue; + }; if ci == 0 { first_child_of_bundle += 1; } diff --git a/crates/sylpheed-formats/examples/resolve_all.rs b/crates/sylpheed-formats/examples/resolve_all.rs index 871a3b6e..c300f1ed 100644 --- a/crates/sylpheed-formats/examples/resolve_all.rs +++ b/crates/sylpheed-formats/examples/resolve_all.rs @@ -1,36 +1,136 @@ +use std::fs; +use std::io::{Read, Seek, SeekFrom}; +use std::process::Command; use sylpheed_formats::{hash::name_hash, movie_manifest, movie_voice, slb, PakArchive}; -use std::fs;use std::io::{Read,Seek,SeekFrom};use std::process::Command; -fn rg(disc:&str,g:u64,n:usize)->Vec{let mut segs=vec![];let mut cum=0u64;for i in 0..5{let p=format!("{disc}/dat/sound.p{i:02}");if let Ok(m)=fs::metadata(&p){segs.push((cum,m.len(),p));cum+=m.len();}}let mut out=vec![];let(mut need,mut pos)=(n,g);for(base,len,path)in &segs{if need==0||pos>=base+len||pos<*base{continue;}let local=pos-base;let take=need.min((len-local)as usize);let mut f=fs::File::open(path).unwrap();f.seek(SeekFrom::Start(local)).unwrap();let mut b=vec![0u8;take];f.read_exact(&mut b).unwrap();out.extend_from_slice(&b);need-=take;pos+=take as u64;}out} -fn contains(h:&[u8],n:&[u8])->bool{h.windows(n.len()).any(|w|w==n)} -fn dur(w:&str)->String{let o=Command::new("ffprobe").args(["-v","error","-show_entries","format=duration","-of","default=nw=1:nk=1",w]).output().unwrap();String::from_utf8_lossy(&o.stdout).trim().to_string()} -fn main(){ - let disc=std::env::var("SYLPHEED_DISC").unwrap(); - let tpak=PakArchive::open(format!("{disc}/dat/tables.pak")).unwrap(); - let manifest=tpak.entries().iter().find_map(|e|tpak.read(e).ok().filter(|b|movie_manifest::is_manifest(b))).unwrap(); - let code="eng"; - let registry=tpak.entries().iter().find_map(|e|tpak.read(e).ok().filter(|b|contains(b,format!("{code}\\Movie\\VOICE_ADV.slb").as_bytes()))).unwrap(); - let ids=movie_voice::registry_voice_ids(®istry); - let stoc=fs::read(format!("{disc}/dat/sound.pak")).unwrap(); - let entries=PakArchive::parse_toc(&stoc).unwrap(); - for movie in ["ADV","RT01A","RT01B","RT01C_1","S00A","S01A","hokyu_LS_s02A","hokyu_LS_s09A","hokyu_LS_s02H","hokyu_DS_s07H"]{ - let Some(token)=movie_manifest::voice_token(&manifest,movie) else { println!("{movie:16} no token"); continue }; - let Some(&id)=ids.get(&token) else { println!("{movie:16} token {token} not in registry"); continue }; - let Some(anchor)=["Movie","etc","Voice"].iter().find_map(|d|{let h=name_hash(&format!("{code}\\{d}\\{token}.slb"));entries.binary_search_by_key(&h,|e|e.name_hash).ok().map(|i|entries[i].offset as u64)}) else { println!("{movie:16} no anchor for {token}"); continue }; - let win_start=(anchor.saturating_sub(2*1024*1024))&!3; - let window=rg(&disc,win_start,8*1024*1024); - let Some(end_local)=movie_voice::find_descriptor(&window,id) else { println!("{movie:16} id {id}: desc NOT FOUND"); continue }; - let end=win_start+end_local as u64; - let start=movie_voice::find_descriptor(&window,id.wrapping_sub(1)) - .or_else(||movie_voice::find_descriptor_before(&window,end_local)) - .map(|o|win_start+o as u64) - .filter(|&s|s Vec { + let mut segs = vec![]; + let mut cum = 0u64; + for i in 0..5 { + let p = format!("{disc}/dat/sound.p{i:02}"); + if let Ok(m) = fs::metadata(&p) { + segs.push((cum, m.len(), p)); + cum += m.len(); + } + } + let mut out = vec![]; + let (mut need, mut pos) = (n, g); + for (base, len, path) in &segs { + if need == 0 || pos >= base + len || pos < *base { + continue; + } + let local = pos - base; + let take = need.min((len - local) as usize); + let mut f = fs::File::open(path).unwrap(); + f.seek(SeekFrom::Start(local)).unwrap(); + let mut b = vec![0u8; take]; + f.read_exact(&mut b).unwrap(); + out.extend_from_slice(&b); + need -= take; + pos += take as u64; + } + out +} +fn contains(h: &[u8], n: &[u8]) -> bool { + h.windows(n.len()).any(|w| w == n) +} +fn dur(w: &str) -> String { + let o = Command::new("ffprobe") + .args([ + "-v", + "error", + "-show_entries", + "format=duration", + "-of", + "default=nw=1:nk=1", + w, + ]) + .output() + .unwrap(); + String::from_utf8_lossy(&o.stdout).trim().to_string() +} +fn main() { + let disc = std::env::var("SYLPHEED_DISC").unwrap(); + let tpak = PakArchive::open(format!("{disc}/dat/tables.pak")).unwrap(); + let manifest = tpak + .entries() + .iter() + .find_map(|e| tpak.read(e).ok().filter(|b| movie_manifest::is_manifest(b))) + .unwrap(); + let code = "eng"; + let registry = tpak + .entries() + .iter() + .find_map(|e| { + tpak.read(e) + .ok() + .filter(|b| contains(b, format!("{code}\\Movie\\VOICE_ADV.slb").as_bytes())) + }) + .unwrap(); + let ids = movie_voice::registry_voice_ids(®istry); + let stoc = fs::read(format!("{disc}/dat/sound.pak")).unwrap(); + let entries = PakArchive::parse_toc(&stoc).unwrap(); + for movie in [ + "ADV", + "RT01A", + "RT01B", + "RT01C_1", + "S00A", + "S01A", + "hokyu_LS_s02A", + "hokyu_LS_s09A", + "hokyu_LS_s02H", + "hokyu_DS_s07H", + ] { + let Some(token) = movie_manifest::voice_token(&manifest, movie) else { + println!("{movie:16} no token"); + continue; + }; + let Some(&id) = ids.get(&token) else { + println!("{movie:16} token {token} not in registry"); + continue; + }; + let Some(anchor) = ["Movie", "etc", "Voice"].iter().find_map(|d| { + let h = name_hash(&format!("{code}\\{d}\\{token}.slb")); + entries + .binary_search_by_key(&h, |e| e.name_hash) + .ok() + .map(|i| entries[i].offset as u64) + }) else { + println!("{movie:16} no anchor for {token}"); + continue; + }; + let win_start = (anchor.saturating_sub(2 * 1024 * 1024)) & !3; + let window = rg(&disc, win_start, 8 * 1024 * 1024); + let Some(end_local) = movie_voice::find_descriptor(&window, id) else { + println!("{movie:16} id {id}: desc NOT FOUND"); + continue; + }; + let end = win_start + end_local as u64; + let start = movie_voice::find_descriptor(&window, id.wrapping_sub(1)) + .or_else(|| movie_voice::find_descriptor_before(&window, end_local)) + .map(|o| win_start + o as u64) + .filter(|&s| s < end && end - s < 1_500_000) .unwrap_or(anchor); - let region=rg(&disc,start,(end-start) as usize); - let mut riffs=slb::to_xma_riffs(®ion); - if riffs.is_empty(){ riffs=slb::to_xma_riff_best(®ion).into_iter().collect(); } - let mut d="?".into(); - if let Some(r)=riffs.first(){ let xp=format!("/tmp/ra2_{movie}.xma.wav");let wp=format!("/tmp/ra2_{movie}.wav");fs::write(&xp,r).unwrap();let _=Command::new("ffmpeg").args(["-hide_banner","-v","error","-y","-i",&xp,&wp]).status();d=dur(&wp);} - let mv=dur(&format!("{disc}/dat/movie/{movie}.wmv")); - println!("{movie:16} id={id:5} region={}KB riffs={} voice={d}s movie={mv}s", (end-start)/1024, riffs.len()); + let region = rg(&disc, start, (end - start) as usize); + let mut riffs = slb::to_xma_riffs(®ion); + if riffs.is_empty() { + riffs = slb::to_xma_riff_best(®ion).into_iter().collect(); + } + let mut d = "?".into(); + if let Some(r) = riffs.first() { + let xp = format!("/tmp/ra2_{movie}.xma.wav"); + let wp = format!("/tmp/ra2_{movie}.wav"); + fs::write(&xp, r).unwrap(); + let _ = Command::new("ffmpeg") + .args(["-hide_banner", "-v", "error", "-y", "-i", &xp, &wp]) + .status(); + d = dur(&wp); + } + let mv = dur(&format!("{disc}/dat/movie/{movie}.wmv")); + println!( + "{movie:16} id={id:5} region={}KB riffs={} voice={d}s movie={mv}s", + (end - start) / 1024, + riffs.len() + ); } } diff --git a/crates/sylpheed-formats/examples/roster_target.rs b/crates/sylpheed-formats/examples/roster_target.rs index 9ce42373..9e3e0391 100644 --- a/crates/sylpheed-formats/examples/roster_target.rs +++ b/crates/sylpheed-formats/examples/roster_target.rs @@ -6,10 +6,10 @@ //! harvested CSV. //! //! Usage: roster_target +use std::collections::{BTreeMap, BTreeSet}; use sylpheed_formats::hash::name_hash; use sylpheed_formats::idxd::IdxdObject; use sylpheed_formats::pak::PakArchive; -use std::collections::{BTreeMap, BTreeSet}; fn main() { let a: Vec = std::env::args().collect(); @@ -31,15 +31,22 @@ fn main() { let stage = format!("S{i:02}"); for pre in ["", "unit\\", "battle\\", "stage\\", "enemy\\"] { for suf in [".tbl", ""] { - by_hash.insert(name_hash(&format!("{pre}EnumUnit_{stage}{suf}")), stage.clone()); + by_hash.insert( + name_hash(&format!("{pre}EnumUnit_{stage}{suf}")), + stage.clone(), + ); } } } let mut rows: Vec<(usize, String, Vec)> = Vec::new(); for e in pak.entries() { - let Some(stage) = by_hash.get(&e.name_hash).cloned() else { continue }; + let Some(stage) = by_hash.get(&e.name_hash).cloned() else { + continue; + }; let Ok(b) = pak.read(e) else { continue }; - let Ok(o) = IdxdObject::parse(&b) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { + continue; + }; let mut units: Vec = Vec::new(); for id in o.tokens().iter().filter(|s| s.starts_with("UN_")) { let prop = id.contains("Asteroid") || id.contains("cmesh") || id.contains("_Box"); @@ -47,7 +54,11 @@ fn main() { units.push(id.clone()); } } - let missing: Vec = units.iter().filter(|u| !have.contains(*u)).cloned().collect(); + let missing: Vec = units + .iter() + .filter(|u| !have.contains(*u)) + .cloned() + .collect(); rows.push((missing.len(), stage, missing)); } rows.sort_by_key(|(n, _, _)| std::cmp::Reverse(*n)); diff --git a/crates/sylpheed-formats/examples/screen_configs.rs b/crates/sylpheed-formats/examples/screen_configs.rs index 331b08d4..3091a31a 100644 --- a/crates/sylpheed-formats/examples/screen_configs.rs +++ b/crates/sylpheed-formats/examples/screen_configs.rs @@ -1,18 +1,37 @@ //! RE probe: dump tables.pak screen-config records that mention a given token. //! Usage: screen_configs use sylpheed_formats::{idxd::IdxdObject, pak::PakArchive}; -fn main(){ - let a:Vec=std::env::args().collect(); - let needle=a.get(2).cloned().unwrap_or_else(||"CHALLENGE".into()); - let arc=PakArchive::open(format!("{}/dat/tables.pak",a[1])).unwrap(); - for (i,e) in arc.entries().iter().enumerate(){ - let Ok(b)=arc.read(e) else{continue}; - let Ok(o)=IdxdObject::parse(&b) else{continue}; - let t=o.tokens(); - if !t.iter().any(|s|s.to_lowercase().contains(&needle.to_lowercase())){continue} - let path=t.iter().find(|s|s.contains(".pak+")).cloned().unwrap_or_default(); - if !path.is_empty() && !path.contains("+eng"){continue} - println!("\n=== entry #{i} schema {:08x} [{path}] {} tokens ===",o.schema_hash,t.len()); - for (j,tok) in t.iter().enumerate(){println!(" {j:3} {tok}");} +fn main() { + let a: Vec = std::env::args().collect(); + let needle = a.get(2).cloned().unwrap_or_else(|| "CHALLENGE".into()); + let arc = PakArchive::open(format!("{}/dat/tables.pak", a[1])).unwrap(); + for (i, e) in arc.entries().iter().enumerate() { + let Ok(b) = arc.read(e) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { + continue; + }; + let t = o.tokens(); + if !t + .iter() + .any(|s| s.to_lowercase().contains(&needle.to_lowercase())) + { + continue; + } + let path = t + .iter() + .find(|s| s.contains(".pak+")) + .cloned() + .unwrap_or_default(); + if !path.is_empty() && !path.contains("+eng") { + continue; + } + println!( + "\n=== entry #{i} schema {:08x} [{path}] {} tokens ===", + o.schema_hash, + t.len() + ); + for (j, tok) in t.iter().enumerate() { + println!(" {j:3} {tok}"); + } } } diff --git a/crates/sylpheed-formats/examples/screen_layout.rs b/crates/sylpheed-formats/examples/screen_layout.rs index 4009fe6a..b5e2f228 100644 --- a/crates/sylpheed-formats/examples/screen_layout.rs +++ b/crates/sylpheed-formats/examples/screen_layout.rs @@ -24,7 +24,10 @@ fn main() { let h = u32::from_str_radix(h.trim_start_matches("0x"), 16).expect("hash"); arc.read_by_hash(h).expect("entry").expect("decompress") } - Some(name) => arc.read_by_name(&name).expect("entry present").expect("decompress"), + Some(name) => arc + .read_by_name(&name) + .expect("entry present") + .expect("decompress"), None => arc .entries() .iter() @@ -57,23 +60,36 @@ fn main() { // X/Y are SIGNED: off-screen animation starts are negative (e.g. -516). let mut placements: Vec> = vec![Vec::new(); count]; for _ in 0..count { - if pos + 8 > bytes.len() { break } + if pos + 8 > bytes.len() { + break; + } let idx = be32(&bytes, pos) as usize; let frames = be32(&bytes, pos + 4) as usize; - if idx >= count || frames == 0 || frames > 4096 { break } + if idx >= count || frames == 0 || frames > 4096 { + break; + } let first = pos + 28; // header + lead-in, verified on the pause bundles let mut group = Vec::with_capacity(frames); for k in 0..frames { let blk = first + k * 40; - if blk + 20 > bytes.len() { break } - group.push((be32(&bytes, blk + 12) as i32, be32(&bytes, blk + 16) as i32, be32(&bytes, blk + 20))); + if blk + 20 > bytes.len() { + break; + } + group.push(( + be32(&bytes, blk + 12) as i32, + be32(&bytes, blk + 16) as i32, + be32(&bytes, blk + 20), + )); } placements[idx] = group; pos = first + frames * 40 - 20; } println!("{} elements", count); - println!("{:<3} {:<30} {:>7} {:>8} {:>12} {:>4} placement", "#", "element", "parent", "kind", "pivot", "kf"); + println!( + "{:<3} {:<30} {:>7} {:>8} {:>12} {:>4} placement", + "#", "element", "parent", "kind", "pivot", "kf" + ); for i in 0..count { let (parent, kind, px, py) = meta[i]; let p = &placements[i]; @@ -88,20 +104,32 @@ fn main() { let mut best = (0usize, 0u32); for k in 0..p.len() - 1 { let d = p[k + 1].2.saturating_sub(p[k].2); - if d > best.1 { best = (k, d) } + if d > best.1 { + best = (k, d) + } } let r = p[best.0]; format!( "rest ({},{}) t={}..{} [{}]", - r.0, r.1, r.2, p[best.0 + 1].2, - p.iter().map(|(x, y, t)| format!("{t}:{x},{y}")).collect::>().join(" ") + r.0, + r.1, + r.2, + p[best.0 + 1].2, + p.iter() + .map(|(x, y, t)| format!("{t}:{x},{y}")) + .collect::>() + .join(" ") ) }; println!( "{:<3} {:<30} {:>7} {:>8} {:>12} {:>4} {}", i, names[i], - if parent == u32::MAX { "-".into() } else { parent.to_string() }, + if parent == u32::MAX { + "-".into() + } else { + parent.to_string() + }, format!("{kind:#x}"), format!("({px},{py})"), p.len(), diff --git a/crates/sylpheed-formats/examples/se_wave_dump.rs b/crates/sylpheed-formats/examples/se_wave_dump.rs index 00fc64e2..20ec64d8 100644 --- a/crates/sylpheed-formats/examples/se_wave_dump.rs +++ b/crates/sylpheed-formats/examples/se_wave_dump.rs @@ -9,7 +9,11 @@ fn main() { let out = std::env::args().nth(1).unwrap_or_else(|| "/tmp".into()); let disc = std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"); let src = DirectorySource::new(&disc); - for (name, off, pkts) in [("move", 0x1ec0usize, 4usize), ("back", 0x0ec0, 2), ("confirm", 0x5d6c0, 6)] { + for (name, off, pkts) in [ + ("move", 0x1ec0usize, 4usize), + ("back", 0x0ec0, 2), + ("confirm", 0x5d6c0, 6), + ] { match media::se_wave_riff(&src, "Static.slb", off, pkts, 1, 48000) { Ok(riff) => { let p = format!("{out}/{name}.riff"); diff --git a/crates/sylpheed-formats/examples/shared_bank_takes.rs b/crates/sylpheed-formats/examples/shared_bank_takes.rs index a9b4c754..1dd57551 100644 --- a/crates/sylpheed-formats/examples/shared_bank_takes.rs +++ b/crates/sylpheed-formats/examples/shared_bank_takes.rs @@ -17,13 +17,38 @@ fn main() { // bank → the movies bound to it, from the record table (see movie-subtitle-link). let banks: BTreeMap<&str, Vec<&str>> = BTreeMap::from([ - ("VOICE_D_450", vec!["hokyu_LS_s02A", "hokyu_LS_s03A", "hokyu_LS_s06A"]), + ( + "VOICE_D_450", + vec!["hokyu_LS_s02A", "hokyu_LS_s03A", "hokyu_LS_s06A"], + ), ( "VOICE_D_451", - vec!["hokyu_LS_s09A", "hokyu_LS_s11A", "hokyu_LS_s15A", "hokyu_LS_s24A", "hokyu_LS_s27A"], + vec![ + "hokyu_LS_s09A", + "hokyu_LS_s11A", + "hokyu_LS_s15A", + "hokyu_LS_s24A", + "hokyu_LS_s27A", + ], + ), + ( + "VOICE_D_452", + vec![ + "hokyu_DS_s02A", + "hokyu_DS_s07A", + "hokyu_DS_s08A", + "hokyu_DS_s13A", + ], + ), + ( + "VOICE_D_453", + vec![ + "hokyu_LS_s02H", + "hokyu_LS_s03H", + "hokyu_LS_s06H", + "hokyu_LS_s09H", + ], ), - ("VOICE_D_452", vec!["hokyu_DS_s02A", "hokyu_DS_s07A", "hokyu_DS_s08A", "hokyu_DS_s13A"]), - ("VOICE_D_453", vec!["hokyu_LS_s02H", "hokyu_LS_s03H", "hokyu_LS_s06H", "hokyu_LS_s09H"]), ("VOICE_D_454", vec!["hokyu_DS_s07H", "hokyu_DS_s14H"]), ]); diff --git a/crates/sylpheed-formats/examples/shared_vbase_check.rs b/crates/sylpheed-formats/examples/shared_vbase_check.rs index eaa4ba38..7dde9346 100644 --- a/crates/sylpheed-formats/examples/shared_vbase_check.rs +++ b/crates/sylpheed-formats/examples/shared_vbase_check.rs @@ -13,9 +13,9 @@ //! Usage: //! cargo run --release --example shared_vbase_check -- \ //! ... +use std::collections::{BTreeMap, BTreeSet}; use sylpheed_formats::mesh::Xbg7Model; use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog, CapturedDraw}; -use std::collections::{BTreeMap, BTreeSet}; /// Quantised position key — the logs print 4 decimals, so compare at that scale. fn key(p: [f32; 3]) -> (i64, i64, i64) { @@ -101,7 +101,10 @@ fn main() { } let mut top: Vec<_> = delta.iter().collect(); top.sort_by_key(|(_, n)| std::cmp::Reverse(**n)); - println!("{log}: {} distinct vbases located, {unfound} not in this container", seen.len() - unfound); + println!( + "{log}: {} distinct vbases located, {unfound} not in this container", + seen.len() - unfound + ); for (d, n) in top.iter().take(5) { println!(" vbase - offset = 0x{:X} ×{n}", d); } @@ -125,7 +128,9 @@ fn main() { for m in &models { for sub in &m.meshes { if let Some(o) = sub.vbuf_offset { - ours.entry(o).or_default().push((m.name.clone(), sub.positions.len())); + ours.entry(o) + .or_default() + .push((m.name.clone(), sub.positions.len())); } } } @@ -156,15 +161,25 @@ fn main() { } // Is a capture-proven offset even a candidate the scan considers? // Absent ⇒ the run scan misses it; present ⇒ selection picked another. - let starts: BTreeSet = - sylpheed_formats::mesh::debug_vertex_run_starts(&bytes, 24).into_iter().collect(); - eprintln!("{} stride-24 candidate starts in this container", starts.len()); - println!("{:<12} {:>7} {:>9} {:<44} our resources with that vcount", "file offset", "vcount", "candidate", "claimed by our decode"); + let starts: BTreeSet = sylpheed_formats::mesh::debug_vertex_run_starts(&bytes, 24) + .into_iter() + .collect(); + eprintln!( + "{} stride-24 candidate starts in this container", + starts.len() + ); + println!( + "{:<12} {:>7} {:>9} {:<44} our resources with that vcount", + "file offset", "vcount", "candidate", "claimed by our decode" + ); for (off, vcount) in &drawn { let who = ours .get(off) .map(|v| { - v.iter().map(|(n, c)| format!("{n}({c})")).collect::>().join(", ") + v.iter() + .map(|(n, c)| format!("{n}({c})")) + .collect::>() + .join(", ") }) .unwrap_or_else(|| "— NOBODY".into()); let same = by_count @@ -185,8 +200,12 @@ fn main() { } let mut groups: BTreeMap, Vec> = BTreeMap::new(); for m in &models { - let pos: Vec<(i64, i64, i64)> = - m.meshes.iter().flat_map(|s| s.positions.iter().copied()).map(key).collect(); + let pos: Vec<(i64, i64, i64)> = m + .meshes + .iter() + .flat_map(|s| s.positions.iter().copied()) + .map(key) + .collect(); if pos.is_empty() { continue; } @@ -205,7 +224,11 @@ fn main() { // A draw belongs to this geometry if every dumped position is one of // the decoded ones (the log dumps at most the first 64). let want: BTreeSet<(i64, i64, i64)> = pos.iter().copied().collect(); - println!("\n{} ({vcount} verts, {} resources)", names.join(" ≡ "), names.len()); + println!( + "\n{} ({vcount} verts, {} resources)", + names.join(" ≡ "), + names.len() + ); for (log, draws) in &logs { let hits: Vec<&CapturedDraw> = draws.iter().filter(|d| d.vcount == vcount).collect(); let all: BTreeSet = hits.iter().map(|d| d.vbase).collect(); @@ -222,7 +245,9 @@ fn main() { .filter(|d| !ok.contains(&d.vbase)) .filter(|d| { !d.pos.is_empty() - && d.pos.iter().all(|p| want.contains(&key([-p[0], p[1], p[2]]))) + && d.pos + .iter() + .all(|p| want.contains(&key([-p[0], p[1], p[2]]))) }) .map(|d| d.vbase) .collect(); @@ -249,12 +274,19 @@ fn main() { "other" }; let at = locate_run(&bytes, &d.pos); - let shown: Vec = - at.iter().take(4).map(|(o, s)| format!("0x{o:x}/stride{s}")).collect(); + let shown: Vec = at + .iter() + .take(4) + .map(|(o, s)| format!("0x{o:x}/stride{s}")) + .collect(); println!( " vbase=0x{:08X} [{kind:6}] in container at: {}", d.vbase, - if shown.is_empty() { "NOT FOUND".into() } else { shown.join(" ") } + if shown.is_empty() { + "NOT FOUND".into() + } else { + shown.join(" ") + } ); } } diff --git a/crates/sylpheed-formats/examples/ship_audit.rs b/crates/sylpheed-formats/examples/ship_audit.rs index f099335e..dee8463d 100644 --- a/crates/sylpheed-formats/examples/ship_audit.rs +++ b/crates/sylpheed-formats/examples/ship_audit.rs @@ -15,7 +15,9 @@ fn be32(d: &[u8], o: usize) -> u32 { /// Count joint-track records whose key count != 1 in a composite's descriptor. fn multikey_tracks(bytes: &[u8], comp: &str) -> usize { - let Some((desc, desc_end)) = xbg7_descriptor_range(bytes, comp) else { return 0 }; + let Some((desc, desc_end)) = xbg7_descriptor_range(bytes, comp) else { + return 0; + }; let d = &bytes[desc..desc_end]; let mut n = 0usize; let mut i = 0usize; @@ -61,7 +63,11 @@ fn main() { .filter_map(|e| e.ok()) .map(|e| e.path()) .filter(|p| { - let n = p.file_name().unwrap_or_default().to_string_lossy().to_string(); + let n = p + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); n.starts_with("Stage_") && n.ends_with(".xpr") }) .collect(); @@ -73,7 +79,10 @@ fn main() { let names = xbg7_resource_names(&bytes); // Multi-key animation tracks per composite (breaks the single-key read). - for n in names.iter().filter(|n| n.contains("rou_") && !n.contains("break")) { + for n in names + .iter() + .filter(|n| n.contains("rou_") && !n.contains("break")) + { let mk = multikey_tracks(&bytes, n); if mk > 0 { println!("MULTIKEY {stage} {n}: {mk} tracks"); @@ -93,7 +102,9 @@ fn main() { // Per-placement world centroid. let mut cents: Vec<(String, [f32; 3])> = Vec::new(); for p in &placed { - let Some(m) = models.iter().find(|m| m.name == p.resource) else { continue }; + let Some(m) = models.iter().find(|m| m.name == p.resource) else { + continue; + }; let (mut c, mut n) = ([0.0f32; 3], 0f32); for sub in &m.meshes { for v in &sub.positions { diff --git a/crates/sylpheed-formats/examples/ship_dump.rs b/crates/sylpheed-formats/examples/ship_dump.rs index bdf91b31..70d31acf 100644 --- a/crates/sylpheed-formats/examples/ship_dump.rs +++ b/crates/sylpheed-formats/examples/ship_dump.rs @@ -1,9 +1,9 @@ //! Dump a ship's static placement + scene-graph nodes for a stage container FILE //! (works offline from an extracted disc, no ISO needed). //! cargo run --release --example ship_dump -- +use std::collections::HashSet; use sylpheed_formats::mesh::{scene_world_nodes, xbg7_resource_names, Xbg7Model}; use sylpheed_formats::ship::{assemble_ship, is_base_part, ship_id_of}; -use std::collections::HashSet; fn main() { let a: Vec = std::env::args().collect(); @@ -12,7 +12,11 @@ fn main() { let names = xbg7_resource_names(&bytes); // Base parts + their vertex counts. - let want: HashSet = names.iter().filter(|n| is_base_part(n) && ship_id_of(n)==Some(id.as_str())).cloned().collect(); + let want: HashSet = names + .iter() + .filter(|n| is_base_part(n) && ship_id_of(n) == Some(id.as_str())) + .cloned() + .collect(); let models = Xbg7Model::models_named(&bytes, &want, &|| false); println!("== base parts ({}) ==", want.len()); for m in &models { @@ -32,19 +36,50 @@ fn main() { // assemble_ship result: centroids + extent. for ext in [false, true] { let placed = assemble_ship(&bytes, id, ext); - println!("== assemble_ship(external={ext}) -> {} parts ==", placed.len()); - let mut lo=[f32::MAX;3]; let mut hi=[f32::MIN;3]; + println!( + "== assemble_ship(external={ext}) -> {} parts ==", + placed.len() + ); + let mut lo = [f32::MAX; 3]; + let mut hi = [f32::MIN; 3]; for p in &placed { - let src = models.iter().find(|m| m.name==p.resource); - let (mut c, mut n) = ([0.0f32;3], 0f32); - if let Some(src)=src { for sub in &src.meshes { for v in &sub.positions { - let w=p.apply(*v); for k in 0..3 { c[k]+=w[k]; lo[k]=lo[k].min(w[k]); hi[k]=hi[k].max(w[k]); } n+=1.0; }}} - let n=n.max(1.0); - println!(" {:20} T=[{:8.1}{:8.1}{:8.1}] centroid=[{:8.1}{:8.1}{:8.1}]", - p.resource, p.t[0],p.t[1],p.t[2], c[0]/n,c[1]/n,c[2]/n); + let src = models.iter().find(|m| m.name == p.resource); + let (mut c, mut n) = ([0.0f32; 3], 0f32); + if let Some(src) = src { + for sub in &src.meshes { + for v in &sub.positions { + let w = p.apply(*v); + for k in 0..3 { + c[k] += w[k]; + lo[k] = lo[k].min(w[k]); + hi[k] = hi[k].max(w[k]); + } + n += 1.0; + } + } + } + let n = n.max(1.0); + println!( + " {:20} T=[{:8.1}{:8.1}{:8.1}] centroid=[{:8.1}{:8.1}{:8.1}]", + p.resource, + p.t[0], + p.t[1], + p.t[2], + c[0] / n, + c[1] / n, + c[2] / n + ); } - if placed.iter().any(|p| models.iter().any(|m| m.name==p.resource)) { - println!(" EXTENT=[{:.0} {:.0} {:.0}]", hi[0]-lo[0], hi[1]-lo[1], hi[2]-lo[2]); + if placed + .iter() + .any(|p| models.iter().any(|m| m.name == p.resource)) + { + println!( + " EXTENT=[{:.0} {:.0} {:.0}]", + hi[0] - lo[0], + hi[1] - lo[1], + hi[2] - lo[2] + ); } } } diff --git a/crates/sylpheed-formats/examples/ship_render.rs b/crates/sylpheed-formats/examples/ship_render.rs index 36218a25..b3526e37 100644 --- a/crates/sylpheed-formats/examples/ship_render.rs +++ b/crates/sylpheed-formats/examples/ship_render.rs @@ -25,7 +25,9 @@ fn main() { // World triangles + per-part bounds. let mut tris: Vec<[[f32; 3]; 3]> = Vec::new(); for p in &placed { - let Some(m) = models.iter().find(|m| m.name == p.resource) else { continue }; + let Some(m) = models.iter().find(|m| m.name == p.resource) else { + continue; + }; let mut lo = [f32::MAX; 3]; let mut hi = [f32::MIN; 3]; for sub in &m.meshes { @@ -59,12 +61,18 @@ fn main() { for s in 0..SEG { tris.push([apex, ring[(s + 1) % SEG], ring[s]]); } - println!(" exhaust frame {:16} T=[{:8.1}{:8.1}{:8.1}]", f.resource, f.t[0], f.t[1], f.t[2]); + println!( + " exhaust frame {:16} T=[{:8.1}{:8.1}{:8.1}]", + f.resource, f.t[0], f.t[1], f.t[2] + ); } println!("total {} tris from {} placements", tris.len(), placed.len()); // Orthographic z-buffered flat renders: top (X/Z, look down −Y) and side (Z/Y, look down −X). - for (name, ax, ay, az) in [("top", 0usize, 2usize, 1usize), ("side", 2usize, 1usize, 0usize)] { + for (name, ax, ay, az) in [ + ("top", 0usize, 2usize, 1usize), + ("side", 2usize, 1usize, 0usize), + ] { let size = 900usize; let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]); for t in &tris { @@ -105,18 +113,29 @@ fn main() { let lum = (n[az].abs() / nl * 200.0 + 40.0) as u8; // bbox raster let minx = p.iter().map(|v| v[0]).fold(f32::MAX, f32::min).max(0.0) as usize; - let maxx = (p.iter().map(|v| v[0]).fold(f32::MIN, f32::max).min(size as f32 - 1.0)) as usize; + let maxx = (p + .iter() + .map(|v| v[0]) + .fold(f32::MIN, f32::max) + .min(size as f32 - 1.0)) as usize; let miny = p.iter().map(|v| v[1]).fold(f32::MAX, f32::min).max(0.0) as usize; - let maxy = (p.iter().map(|v| v[1]).fold(f32::MIN, f32::max).min(size as f32 - 1.0)) as usize; - let det = (p[1][0] - p[0][0]) * (p[2][1] - p[0][1]) - (p[2][0] - p[0][0]) * (p[1][1] - p[0][1]); + let maxy = (p + .iter() + .map(|v| v[1]) + .fold(f32::MIN, f32::max) + .min(size as f32 - 1.0)) as usize; + let det = (p[1][0] - p[0][0]) * (p[2][1] - p[0][1]) + - (p[2][0] - p[0][0]) * (p[1][1] - p[0][1]); if det.abs() < 1e-6 { continue; } for y in miny..=maxy { for x in minx..=maxx { let (fx, fy) = (x as f32 + 0.5, y as f32 + 0.5); - let w0 = ((p[1][0] - fx) * (p[2][1] - fy) - (p[2][0] - fx) * (p[1][1] - fy)) / det; - let w1 = ((p[2][0] - fx) * (p[0][1] - fy) - (p[0][0] - fx) * (p[2][1] - fy)) / det; + let w0 = + ((p[1][0] - fx) * (p[2][1] - fy) - (p[2][0] - fx) * (p[1][1] - fy)) / det; + let w1 = + ((p[2][0] - fx) * (p[0][1] - fy) - (p[0][0] - fx) * (p[2][1] - fy)) / det; let w2 = 1.0 - w0 - w1; if w0 < 0.0 || w1 < 0.0 || w2 < 0.0 { continue; diff --git a/crates/sylpheed-formats/examples/sibling_rule_check.rs b/crates/sylpheed-formats/examples/sibling_rule_check.rs index 10e31802..d4d8f363 100644 --- a/crates/sylpheed-formats/examples/sibling_rule_check.rs +++ b/crates/sylpheed-formats/examples/sibling_rule_check.rs @@ -22,8 +22,12 @@ fn main() { if !IdxdObject::is_idxd(&bytes) { continue; } - let Ok(obj) = IdxdObject::parse(&bytes) else { continue }; - let Some(generic) = obj.record("Generic") else { continue }; + let Ok(obj) = IdxdObject::parse(&bytes) else { + continue; + }; + let Some(generic) = obj.record("Generic") else { + continue; + }; if generic.get("Size_X").is_none() { continue; } @@ -45,11 +49,17 @@ fn main() { } } } - println!("{:<14} {:>10} {:>10} {:>10} {:>10}", "pair", "seen+diff", "seen+eq", "MISS+diff", "MISS+eq"); + println!( + "{:<14} {:>10} {:>10} {:>10} {:>10}", + "pair", "seen+diff", "seen+eq", "MISS+diff", "MISS+eq" + ); for (i, (field, sibling)) in pairs.iter().enumerate() { println!( "{:<14} {:>10} {:>10} {:>10} {:>10}", - format!("{field}/{sibling}").chars().take(14).collect::(), + format!("{field}/{sibling}") + .chars() + .take(14) + .collect::(), tally[i][0][0], tally[i][0][1], tally[i][1][0], diff --git a/crates/sylpheed-formats/examples/slab_screen.rs b/crates/sylpheed-formats/examples/slab_screen.rs index 66a55ef2..221fe223 100644 --- a/crates/sylpheed-formats/examples/slab_screen.rs +++ b/crates/sylpheed-formats/examples/slab_screen.rs @@ -7,13 +7,16 @@ //! this does the same comparison numerically. //! //! Usage: slab_screen [factor] +use std::collections::BTreeMap; use sylpheed_formats::mesh::Xbg7Model; use sylpheed_formats::ship::{is_base_part, ship_id_of}; -use std::collections::BTreeMap; fn main() { let dir = std::env::args().nth(1).expect("resource3d dir"); - let factor: f32 = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(4.0); + let factor: f32 = std::env::args() + .nth(2) + .and_then(|s| s.parse().ok()) + .unwrap_or(4.0); let mut files: Vec<_> = std::fs::read_dir(&dir) .unwrap() .flatten() @@ -24,13 +27,17 @@ fn main() { let mut flagged = 0usize; for f in &files { - let Ok(bytes) = std::fs::read(f) else { continue }; + let Ok(bytes) = std::fs::read(f) else { + continue; + }; let mut by_ship: BTreeMap> = BTreeMap::new(); for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) { if !is_base_part(&m.name) { continue; } - let Some(id) = ship_id_of(&m.name) else { continue }; + let Some(id) = ship_id_of(&m.name) else { + continue; + }; let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]); for s in &m.meshes { for q in &s.positions { @@ -49,7 +56,10 @@ fn main() { // mis-anchored block is bulky in all three, which is what the e106 // slab looked like (600×1600×998 beside siblings of ~250). let thin = (hi[0] - lo[0]).min(hi[1] - lo[1]).min(hi[2] - lo[2]); - by_ship.entry(id.to_string()).or_default().push((m.name.clone(), thin)); + by_ship + .entry(id.to_string()) + .or_default() + .push((m.name.clone(), thin)); } for (id, parts) in &by_ship { if parts.len() < 3 { diff --git a/crates/sylpheed-formats/examples/slb_fmt_probe.rs b/crates/sylpheed-formats/examples/slb_fmt_probe.rs index 60a34ada..56813cb4 100644 --- a/crates/sylpheed-formats/examples/slb_fmt_probe.rs +++ b/crates/sylpheed-formats/examples/slb_fmt_probe.rs @@ -87,9 +87,13 @@ fn main() { ); for (movie, n) in bound { let path = format!("eng\\etc\\VOICE_D_{n}.slb"); - let Some(entry) = snd.find_by_name(&path) else { continue }; + let Some(entry) = snd.find_by_name(&path) else { + continue; + }; let bytes = snd.read(entry).expect("read"); - let Some(first_riff) = bytes.windows(4).position(|w| w == b"RIFF") else { continue }; + let Some(first_riff) = bytes.windows(4).position(|w| w == b"RIFF") else { + continue; + }; if first_riff <= slb::HEADERLESS_DATA_OFFSET { println!("{movie:<16} {:>6} (no leading region)", format!("D_{n}")); continue; @@ -102,7 +106,11 @@ fn main() { .iter() .map(|(_, t)| *t) .fold(0.0f32, f32::max); - let implied = if cue > 0.0 { samples as f32 / cue } else { f32::NAN }; + let implied = if cue > 0.0 { + samples as f32 / cue + } else { + f32::NAN + }; println!( "{movie:<16} {:>6} {stereo:>9} {mono:>9} {samples:>10} {cue:>9.2} {implied:>12.0}", format!("D_{n}") diff --git a/crates/sylpheed-formats/examples/slb_hybrid_scan.rs b/crates/sylpheed-formats/examples/slb_hybrid_scan.rs index 7c828254..5d06716e 100644 --- a/crates/sylpheed-formats/examples/slb_hybrid_scan.rs +++ b/crates/sylpheed-formats/examples/slb_hybrid_scan.rs @@ -13,7 +13,9 @@ fn main() { for e in snd.entries() { let Ok(b) = snd.read(e) else { continue }; total += 1; - let Some(ri) = b.windows(4).position(|w| w == b"RIFF") else { continue }; + let Some(ri) = b.windows(4).position(|w| w == b"RIFF") else { + continue; + }; has_riff += 1; if ri > slb::HEADERLESS_DATA_OFFSET && (ri - slb::HEADERLESS_DATA_OFFSET) % slb::XMA1_PACKET == 0 diff --git a/crates/sylpheed-formats/examples/spawn_survey.rs b/crates/sylpheed-formats/examples/spawn_survey.rs index 085dc511..cf0251d1 100644 --- a/crates/sylpheed-formats/examples/spawn_survey.rs +++ b/crates/sylpheed-formats/examples/spawn_survey.rs @@ -1,31 +1,82 @@ -use sylpheed_formats::{idxd::IdxdObject, PakArchive}; use std::collections::BTreeMap; -fn main(){ - let disc=std::env::var("SYLPHEED_DISC").unwrap(); - let arc=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); +use sylpheed_formats::{idxd::IdxdObject, PakArchive}; +fn main() { + let disc = std::env::var("SYLPHEED_DISC").unwrap(); + let arc = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); // schemas whose type0/content is squadron/formation/route/group/position - let mut hit:BTreeMap=BTreeMap::new(); - for e in arc.entries(){ - let Ok(b)=arc.read(e) else{continue}; let Ok(o)=IdxdObject::parse(&b) else{continue}; - let t=o.tokens(); let t0=t.first().cloned().unwrap_or_default(); - let joined:String=t.iter().take(20).map(|s|s.as_str()).collect::>().join(" "); - if ["Squadron","Formation","Route","UnitGroup","Position","Spawn","Wave","Frame","NullFrame"].iter().any(|k|t0.contains(k)||joined.contains(k)){ - let ent=hit.entry(o.schema_hash).or_insert((0,t0.clone(),joined.chars().take(90).collect())); - ent.0+=1; + let mut hit: BTreeMap = BTreeMap::new(); + for e in arc.entries() { + let Ok(b) = arc.read(e) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { + continue; + }; + let t = o.tokens(); + let t0 = t.first().cloned().unwrap_or_default(); + let joined: String = t + .iter() + .take(20) + .map(|s| s.as_str()) + .collect::>() + .join(" "); + if [ + "Squadron", + "Formation", + "Route", + "UnitGroup", + "Position", + "Spawn", + "Wave", + "Frame", + "NullFrame", + ] + .iter() + .any(|k| t0.contains(k) || joined.contains(k)) + { + let ent = hit.entry(o.schema_hash).or_insert(( + 0, + t0.clone(), + joined.chars().take(90).collect(), + )); + ent.0 += 1; } } - let mut v:Vec<_>=hit.into_iter().collect(); v.sort_by_key(|x|std::cmp::Reverse(x.1.0)); - for (s,(c,t0,sample)) in &v{ println!("[{s:08x}] ×{c:<4} type0={t0:<18.18} :: {sample}"); } + let mut v: Vec<_> = hit.into_iter().collect(); + v.sort_by_key(|x| std::cmp::Reverse(x.1 .0)); + for (s, (c, t0, sample)) in &v { + println!("[{s:08x}] ×{c:<4} type0={t0:<18.18} :: {sample}"); + } // dump a Squadron/UnitGroup blob fully to see if it has positions (binary floats) or refs println!("\n─── sample squadron/formation blob (first matching) ───"); - for e in arc.entries(){ - let Ok(b)=arc.read(e) else{continue}; let Ok(o)=IdxdObject::parse(&b) else{continue}; - let t=o.tokens(); let t0=t.first().cloned().unwrap_or_default(); - if t0.contains("Squadron")||t0.contains("Formation")||t0.contains("UnitGroup"){ - println!("schema {:08x} type0={t0} count={} tokens={}, blob {}B", o.schema_hash,o.count,t.len(),b.len()); - for (i,tk) in t.iter().take(30).enumerate(){ print!("{i:>2}:{tk:<20.20}"); if i%4==3{println!();} } println!(); + for e in arc.entries() { + let Ok(b) = arc.read(e) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { + continue; + }; + let t = o.tokens(); + let t0 = t.first().cloned().unwrap_or_default(); + if t0.contains("Squadron") || t0.contains("Formation") || t0.contains("UnitGroup") { + println!( + "schema {:08x} type0={t0} count={} tokens={}, blob {}B", + o.schema_hash, + o.count, + t.len(), + b.len() + ); + for (i, tk) in t.iter().take(30).enumerate() { + print!("{i:>2}:{tk:<20.20}"); + if i % 4 == 3 { + println!(); + } + } + println!(); // scan blob for float-like values (reasonable coords) in the binary region - let nfloats=(0..b.len().saturating_sub(4)).step_by(4).filter(|&i|{let f=f32::from_be_bytes([b[i],b[i+1],b[i+2],b[i+3]]); f.is_finite()&&f.abs()>1.0&&f.abs()<1e6}).count(); + let nfloats = (0..b.len().saturating_sub(4)) + .step_by(4) + .filter(|&i| { + let f = f32::from_be_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]); + f.is_finite() && f.abs() > 1.0 && f.abs() < 1e6 + }) + .count(); println!(" ~{nfloats} plausible BE-float words in blob (positions live in binary region if high)"); break; } diff --git a/crates/sylpheed-formats/examples/squadrons.rs b/crates/sylpheed-formats/examples/squadrons.rs index c11a10c0..5ecb8a7c 100644 --- a/crates/sylpheed-formats/examples/squadrons.rs +++ b/crates/sylpheed-formats/examples/squadrons.rs @@ -1,11 +1,36 @@ use sylpheed_formats::{game_data, PakArchive}; -fn main(){ - let disc=std::env::var("SYLPHEED_DISC").unwrap(); - let pak=PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); - let sq=game_data::load_squadrons(&pak); +fn main() { + let disc = std::env::var("SYLPHEED_DISC").unwrap(); + let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); + let sq = game_data::load_squadrons(&pak); println!("{} squadron definitions", sq.len()); - for s in sq.iter().filter(|s|s.side.as_deref()==Some("TCAF")).take(6){ - let mem:Vec=s.members.iter().map(|m|format!("{}/{}",m.unit.trim_start_matches("UN_").chars().take(18).collect::(),m.pilot.clone().unwrap_or("·".into()))).collect(); - println!(" {:8} {:22} {:24} x{} {:?}", s.id.clone(), s.formation_id.clone().unwrap_or_default(), s.ai_id.clone().unwrap_or_default(), s.count.unwrap_or(0), mem); + for s in sq + .iter() + .filter(|s| s.side.as_deref() == Some("TCAF")) + .take(6) + { + let mem: Vec = s + .members + .iter() + .map(|m| { + format!( + "{}/{}", + m.unit + .trim_start_matches("UN_") + .chars() + .take(18) + .collect::(), + m.pilot.clone().unwrap_or("·".into()) + ) + }) + .collect(); + println!( + " {:8} {:22} {:24} x{} {:?}", + s.id.clone(), + s.formation_id.clone().unwrap_or_default(), + s.ai_id.clone().unwrap_or_default(), + s.count.unwrap_or(0), + mem + ); } } diff --git a/crates/sylpheed-formats/examples/starts.rs b/crates/sylpheed-formats/examples/starts.rs index 69f86914..16798825 100644 --- a/crates/sylpheed-formats/examples/starts.rs +++ b/crates/sylpheed-formats/examples/starts.rs @@ -8,6 +8,9 @@ fn main() { println!("{} candidate starts at stride {stride}", starts.len()); for off in &a[3..] { let o = usize::from_str_radix(off.trim_start_matches("0x"), 16).unwrap(); - println!(" 0x{o:x} in candidate list: {}", starts.binary_search(&o).is_ok()); + println!( + " 0x{o:x} in candidate list: {}", + starts.binary_search(&o).is_ok() + ); } } diff --git a/crates/sylpheed-formats/examples/starts_probe.rs b/crates/sylpheed-formats/examples/starts_probe.rs index d516eab7..e0b16b67 100644 --- a/crates/sylpheed-formats/examples/starts_probe.rs +++ b/crates/sylpheed-formats/examples/starts_probe.rs @@ -1,7 +1,15 @@ -fn main(){let a:Vec=std::env::args().collect();let b=std::fs::read(&a[1]).unwrap(); -let stride:usize=a[2].parse().unwrap(); let want:usize=a[3].parse().unwrap(); -let s=sylpheed_formats::mesh::debug_vertex_run_starts(&b,stride); -println!("{} candidate starts at stride {}", s.len(), stride); -println!("contains {:#x}: {}", want, s.contains(&want)); -let near:Vec=s.iter().filter(|&&o| o.abs_diff(want)<0x200).map(|o|format!("{o:#x}")).collect(); -println!("nearby: {}", near.join(" "));} +fn main() { + let a: Vec = std::env::args().collect(); + let b = std::fs::read(&a[1]).unwrap(); + let stride: usize = a[2].parse().unwrap(); + let want: usize = a[3].parse().unwrap(); + let s = sylpheed_formats::mesh::debug_vertex_run_starts(&b, stride); + println!("{} candidate starts at stride {}", s.len(), stride); + println!("contains {:#x}: {}", want, s.contains(&want)); + let near: Vec = s + .iter() + .filter(|&&o| o.abs_diff(want) < 0x200) + .map(|o| format!("{o:#x}")) + .collect(); + println!("nearby: {}", near.join(" ")); +} diff --git a/crates/sylpheed-formats/examples/submesh_dump.rs b/crates/sylpheed-formats/examples/submesh_dump.rs index a904e1f8..d1843176 100644 --- a/crates/sylpheed-formats/examples/submesh_dump.rs +++ b/crates/sylpheed-formats/examples/submesh_dump.rs @@ -1,14 +1,21 @@ //! Per-sub-mesh vertex/index/coverage dump for one resource. //! Usage: submesh_dump ... -use sylpheed_formats::mesh::{debug_resource_params, Xbg7Model}; use std::collections::HashSet; +use sylpheed_formats::mesh::{debug_resource_params, Xbg7Model}; fn main() { let a: Vec = std::env::args().collect(); let bytes = std::fs::read(&a[1]).expect("container"); let want: HashSet = a[2..].iter().cloned().collect(); for m in Xbg7Model::models_named(&bytes, &want, &|| false) { - let markers = debug_resource_params(&bytes, &m.name).map(|(mk, _)| mk).unwrap_or_default(); - println!("{} — {} sub-meshes decoded, {} markers declared", m.name, m.meshes.len(), markers.len()); + let markers = debug_resource_params(&bytes, &m.name) + .map(|(mk, _)| mk) + .unwrap_or_default(); + println!( + "{} — {} sub-meshes decoded, {} markers declared", + m.name, + m.meshes.len(), + markers.len() + ); for (i, s) in m.meshes.iter().enumerate() { let max_idx = s.indices.iter().max().copied().unwrap_or(0) as usize; let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]); diff --git a/crates/sylpheed-formats/examples/try_anchor.rs b/crates/sylpheed-formats/examples/try_anchor.rs index 268f6384..f8f0703c 100644 --- a/crates/sylpheed-formats/examples/try_anchor.rs +++ b/crates/sylpheed-formats/examples/try_anchor.rs @@ -5,12 +5,17 @@ fn main() { let bytes = std::fs::read(&a[1]).unwrap(); // The production scan tries pads 0..=3; pass a bigger one to ask whether the // block would validate at all with a wider index/vertex gap. - let max_pad: usize = std::env::var("MAX_PAD").ok().and_then(|v| v.parse().ok()).unwrap_or(3); + let max_pad: usize = std::env::var("MAX_PAD") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(3); for pair in a[2..].iter() { let (name, off) = pair.split_once('@').unwrap(); let off = usize::from_str_radix(off.trim_start_matches("0x"), 16).unwrap(); match debug_try_anchor(&bytes, name, off, max_pad) { - Some((v, i, pad)) => println!("{name:22} @ 0x{off:x} ACCEPTED v={v} idx={i} pad={pad}"), + Some((v, i, pad)) => { + println!("{name:22} @ 0x{off:x} ACCEPTED v={v} idx={i} pad={pad}") + } None => println!("{name:22} @ 0x{off:x} rejected"), } } diff --git a/crates/sylpheed-formats/examples/twin_mirror_audit.rs b/crates/sylpheed-formats/examples/twin_mirror_audit.rs index 6fb56107..1115e5bc 100644 --- a/crates/sylpheed-formats/examples/twin_mirror_audit.rs +++ b/crates/sylpheed-formats/examples/twin_mirror_audit.rs @@ -8,8 +8,8 @@ //! mis-anchor no count-based metric can see). //! //! Usage: twin_mirror_audit -use sylpheed_formats::mesh::Xbg7Model; use std::collections::BTreeMap; +use sylpheed_formats::mesh::Xbg7Model; fn key(p: [f32; 3]) -> (i64, i64, i64) { ( @@ -39,13 +39,19 @@ fn main() { let (mut same, mut mirrored, mut unrelated, mut related) = (0usize, 0usize, 0usize, 0usize); let mut examples: Vec = Vec::new(); for f in &files { - let Ok(bytes) = std::fs::read(f) else { continue }; + let Ok(bytes) = std::fs::read(f) else { + continue; + }; let models = Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false); let by_name: BTreeMap<&str, &Xbg7Model> = models.iter().map(|m| (m.name.as_str(), m)).collect(); for m in &models { - let Some(stem) = m.name.strip_suffix("_01") else { continue }; - let Some(t) = by_name.get(format!("{stem}_02").as_str()) else { continue }; + let Some(stem) = m.name.strip_suffix("_01") else { + continue; + }; + let Some(t) = by_name.get(format!("{stem}_02").as_str()) else { + continue; + }; let a: Vec<[f32; 3]> = m.meshes.iter().flat_map(|s| s.positions.clone()).collect(); let b: Vec<[f32; 3]> = t.meshes.iter().flat_map(|s| s.positions.clone()).collect(); if a.len() != b.len() || a.is_empty() { @@ -108,7 +114,10 @@ fn main() { } } } - println!("twin pairs of equal vertex count: {}", same + mirrored + unrelated + related); + println!( + "twin pairs of equal vertex count: {}", + same + mirrored + unrelated + related + ); println!(" exact X-mirror (expected) : {mirrored}"); println!(" IDENTICAL, one buffer (collapse) : {same}"); println!(" related other way (Y/Z mirror, reordered): {related}"); diff --git a/crates/sylpheed-formats/examples/ui_screen.rs b/crates/sylpheed-formats/examples/ui_screen.rs index 6e9e1371..d7bbaa4e 100644 --- a/crates/sylpheed-formats/examples/ui_screen.rs +++ b/crates/sylpheed-formats/examples/ui_screen.rs @@ -9,13 +9,18 @@ use sylpheed_formats::ratc; fn main() { let mut args = std::env::args().skip(1); - let pak = args.next().expect("usage: ui_screen [--dump 0xHASH out.bin]"); + let pak = args + .next() + .expect("usage: ui_screen [--dump 0xHASH out.bin]"); let arc = PakArchive::open(&pak).expect("open pak"); let rest: Vec = args.collect(); if rest.first().map(|s| s == "--dump").unwrap_or(false) { let h = u32::from_str_radix(rest[1].trim_start_matches("0x"), 16).unwrap(); - let bytes = arc.read_by_hash(h).expect("entry present").expect("decompress"); + let bytes = arc + .read_by_hash(h) + .expect("entry present") + .expect("decompress"); std::fs::write(&rest[2], &bytes).unwrap(); println!("wrote {} bytes to {}", bytes.len(), rest[2]); return; @@ -25,11 +30,20 @@ fn main() { // --child 0xHASH : carve one RATC child out by its // listed offset/size, so a 165-byte layout record can be hexdumped alone. let h = u32::from_str_radix(rest[1].trim_start_matches("0x"), 16).unwrap(); - let bytes = arc.read_by_hash(h).expect("entry present").expect("decompress"); + let bytes = arc + .read_by_hash(h) + .expect("entry present") + .expect("decompress"); let kids = ratc::parse(&bytes).expect("ratc"); - let k = kids.iter().find(|k| k.name == rest[2]).expect("child not found"); + let k = kids + .iter() + .find(|k| k.name == rest[2]) + .expect("child not found"); std::fs::write(&rest[3], &bytes[k.offset..k.offset + k.size]).unwrap(); - println!("wrote {} B ({} @ {:#x}) to {}", k.size, k.name, k.offset, rest[3]); + println!( + "wrote {} B ({} @ {:#x}) to {}", + k.size, k.name, k.offset, rest[3] + ); return; } diff --git a/crates/sylpheed-formats/examples/undecoded.rs b/crates/sylpheed-formats/examples/undecoded.rs index c81f09a8..3e6600ab 100644 --- a/crates/sylpheed-formats/examples/undecoded.rs +++ b/crates/sylpheed-formats/examples/undecoded.rs @@ -3,8 +3,8 @@ //! Coverage has been reported as "resources decoded" without a denominator. This //! prints both, per container and in total, and names the misses so the gate //! attribution (`why_rejected`) has a work list. -use sylpheed_formats::mesh::{debug_resource_params, xbg7_resource_names, Xbg7Model}; use std::collections::HashSet; +use sylpheed_formats::mesh::{debug_resource_params, xbg7_resource_names, Xbg7Model}; fn main() { let dir = std::env::args().nth(1).expect("resource3d dir"); @@ -20,7 +20,9 @@ fn main() { let (mut total, mut decoded, mut no_decl) = (0usize, 0usize, 0usize); let mut misses: Vec = Vec::new(); for f in &files { - let Ok(bytes) = std::fs::read(f) else { continue }; + let Ok(bytes) = std::fs::read(f) else { + continue; + }; let names = xbg7_resource_names(&bytes); if names.is_empty() { continue; diff --git a/crates/sylpheed-formats/examples/unit_fields.rs b/crates/sylpheed-formats/examples/unit_fields.rs index 1011ef8d..dac8f264 100644 --- a/crates/sylpheed-formats/examples/unit_fields.rs +++ b/crates/sylpheed-formats/examples/unit_fields.rs @@ -13,8 +13,12 @@ fn main() { let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).expect("pak"); for entry in pak.entries() { let Ok(bytes) = pak.read(entry) else { continue }; - let Ok(obj) = IdxdObject::parse(&bytes) else { continue }; - let Some(id) = obj.get_raw("ID") else { continue }; + let Ok(obj) = IdxdObject::parse(&bytes) else { + continue; + }; + let Some(id) = obj.get_raw("ID") else { + continue; + }; if !want.iter().any(|w| id.contains(w.as_str())) { continue; } diff --git a/crates/sylpheed-formats/examples/unit_signatures.rs b/crates/sylpheed-formats/examples/unit_signatures.rs index 8fba3519..30f0fbea 100644 --- a/crates/sylpheed-formats/examples/unit_signatures.rs +++ b/crates/sylpheed-formats/examples/unit_signatures.rs @@ -9,7 +9,10 @@ use sylpheed_formats::PakArchive; fn main() { let disc = std::env::args().nth(1).expect("disc root"); let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).expect("pak"); - println!("{:<34} {:>9} {:>8} {:>8} {:>8} kind", "id", "hp", "size_x", "size_y", "size_z"); + println!( + "{:<34} {:>9} {:>8} {:>8} {:>8} kind", + "id", "hp", "size_x", "size_y", "size_z" + ); for u in load_units(&pak) { println!( "{:<34} {:>9} {:>8} {:>8} {:>8} unit", diff --git a/crates/sylpheed-formats/examples/unit_values.rs b/crates/sylpheed-formats/examples/unit_values.rs index 83af466e..9e1a4ba6 100644 --- a/crates/sylpheed-formats/examples/unit_values.rs +++ b/crates/sylpheed-formats/examples/unit_values.rs @@ -6,9 +6,13 @@ fn main() { let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap(); for e in pak.entries() { let Ok(b) = pak.read(e) else { continue }; - let Ok(o) = IdxdObject::parse(&b) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { + continue; + }; let Some(id) = o.get_raw("ID") else { continue }; - if !id.contains(&want) { continue; } + if !id.contains(&want) { + continue; + } println!("=== {id}"); let mut seen = std::collections::BTreeSet::new(); for k in o.tokens() { diff --git a/crates/sylpheed-formats/examples/validate_cues.rs b/crates/sylpheed-formats/examples/validate_cues.rs index eb09c415..f15648d6 100644 --- a/crates/sylpheed-formats/examples/validate_cues.rs +++ b/crates/sylpheed-formats/examples/validate_cues.rs @@ -1,30 +1,91 @@ +use std::fs; +use std::io::{Read, Seek, SeekFrom}; +use std::process::Command; use sylpheed_formats::slb; -use std::fs;use std::io::{Read,Seek,SeekFrom};use std::process::Command; -fn rg(disc:&str,g:u64,n:usize)->Vec{let mut segs=vec![];let mut cum=0u64;for i in 0..5{let p=format!("{disc}/dat/sound.p{i:02}");if let Ok(m)=fs::metadata(&p){segs.push((cum,m.len(),p));cum+=m.len();}}let mut out=vec![];let(mut need,mut pos)=(n,g);for(base,len,path)in &segs{if need==0||pos>=base+len||pos<*base{continue;}let local=pos-base;let take=need.min((len-local)as usize);let mut f=fs::File::open(path).unwrap();f.seek(SeekFrom::Start(local)).unwrap();let mut b=vec![0u8;take];f.read_exact(&mut b).unwrap();out.extend_from_slice(&b);need-=take;pos+=take as u64;}out} -fn dur(w:&str)->String{let o=Command::new("ffprobe").args(["-v","error","-show_entries","format=duration:stream=channels","-of","default=nw=1:nk=1",w]).output().unwrap();String::from_utf8_lossy(&o.stdout).replace('\n'," ")} -fn movdur(disc:&str,m:&str)->String{ dur(&format!("{disc}/dat/movie/{m}")) } -fn main(){ - let disc=std::env::var("SYLPHEED_DISC").unwrap(); +fn rg(disc: &str, g: u64, n: usize) -> Vec { + let mut segs = vec![]; + let mut cum = 0u64; + for i in 0..5 { + let p = format!("{disc}/dat/sound.p{i:02}"); + if let Ok(m) = fs::metadata(&p) { + segs.push((cum, m.len(), p)); + cum += m.len(); + } + } + let mut out = vec![]; + let (mut need, mut pos) = (n, g); + for (base, len, path) in &segs { + if need == 0 || pos >= base + len || pos < *base { + continue; + } + let local = pos - base; + let take = need.min((len - local) as usize); + let mut f = fs::File::open(path).unwrap(); + f.seek(SeekFrom::Start(local)).unwrap(); + let mut b = vec![0u8; take]; + f.read_exact(&mut b).unwrap(); + out.extend_from_slice(&b); + need -= take; + pos += take as u64; + } + out +} +fn dur(w: &str) -> String { + let o = Command::new("ffprobe") + .args([ + "-v", + "error", + "-show_entries", + "format=duration:stream=channels", + "-of", + "default=nw=1:nk=1", + w, + ]) + .output() + .unwrap(); + String::from_utf8_lossy(&o.stdout).replace('\n', " ") +} +fn movdur(disc: &str, m: &str) -> String { + dur(&format!("{disc}/dat/movie/{m}")) +} +fn main() { + let disc = std::env::var("SYLPHEED_DISC").unwrap(); // descriptor trailer global offsets (from scan) => cue N data = [desc(N-1)..desc(N)] // (id, end_off, movie) - let cues=[(1600u32,437044592u64,"ADV.wmv"),(1601,437345648,"RT01A.wmv"),(1602,437712240,"RT01B.wmv"), - (1603,438080880,"RT01C_1.wmv"),(1604,438451568,"RT01C_2.wmv"),(1605,438789488,"RT02A.wmv")]; - for k in 1..cues.len(){ - let (id,end,mov)=cues[k]; - let start=cues[k-1].1; - let region=rg(&disc,start,(end-start) as usize + 12000); // +tail to include full data before next trailer - let riffs=slb::to_xma_riffs(®ion); - let mut total=0.0f32; let mut parts=vec![]; - for (j,r) in riffs.iter().enumerate(){ - let xp=format!("/tmp/cue{id}_{j}.xma.wav"); let wp=format!("/tmp/cue{id}_{j}.wav"); - fs::write(&xp,r).unwrap(); - let _=Command::new("ffmpeg").args(["-hide_banner","-v","error","-y","-i",&xp,&wp]).status(); - let d=dur(&wp); parts.push(d.clone()); - total+=d.split_whitespace().next().unwrap_or("0").parse::().unwrap_or(0.0); + let cues = [ + (1600u32, 437044592u64, "ADV.wmv"), + (1601, 437345648, "RT01A.wmv"), + (1602, 437712240, "RT01B.wmv"), + (1603, 438080880, "RT01C_1.wmv"), + (1604, 438451568, "RT01C_2.wmv"), + (1605, 438789488, "RT02A.wmv"), + ]; + for k in 1..cues.len() { + let (id, end, mov) = cues[k]; + let start = cues[k - 1].1; + let region = rg(&disc, start, (end - start) as usize + 12000); // +tail to include full data before next trailer + let riffs = slb::to_xma_riffs(®ion); + let mut total = 0.0f32; + let mut parts = vec![]; + for (j, r) in riffs.iter().enumerate() { + let xp = format!("/tmp/cue{id}_{j}.xma.wav"); + let wp = format!("/tmp/cue{id}_{j}.wav"); + fs::write(&xp, r).unwrap(); + let _ = Command::new("ffmpeg") + .args(["-hide_banner", "-v", "error", "-y", "-i", &xp, &wp]) + .status(); + let d = dur(&wp); + parts.push(d.clone()); + total += d + .split_whitespace() + .next() + .unwrap_or("0") + .parse::() + .unwrap_or(0.0); } // spanning? - let span_adv_end=437547264u64; - let spans = start < span_adv_end && end > span_adv_end || (start/1_000 != end/1_000); + let span_adv_end = 437547264u64; + let spans = start < span_adv_end && end > span_adv_end || (start / 1_000 != end / 1_000); println!("cue {id} ({mov}): region[{start}..{end}] {} bytes, {} riff(s), Σ={:.1}s | movie={} parts={:?}", end-start, riffs.len(), total, movdur(&disc,mov), parts); } diff --git a/crates/sylpheed-formats/examples/vcount_index.rs b/crates/sylpheed-formats/examples/vcount_index.rs index 47d9cc8b..c4d1bfd5 100644 --- a/crates/sylpheed-formats/examples/vcount_index.rs +++ b/crates/sylpheed-formats/examples/vcount_index.rs @@ -11,9 +11,9 @@ //! cargo run --release --example vcount_index -- [top_n] //! cargo run --release --example vcount_index -- --vcounts 10891,6000 +use std::collections::{HashMap, HashSet}; use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model}; use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog}; -use std::collections::{HashMap, HashSet}; fn main() { let args: Vec = std::env::args().collect(); @@ -27,7 +27,10 @@ fn main() { let mut draw_count: HashMap = HashMap::new(); let mut bufs: HashMap> = HashMap::new(); if args[2] == "--vcounts" { - for v in args[3].split(',').filter_map(|s| s.trim().parse::().ok()) { + for v in args[3] + .split(',') + .filter_map(|s| s.trim().parse::().ok()) + { draw_count.insert(v, 0); } } else { @@ -42,7 +45,10 @@ fn main() { bufs.entry(d.vcount).or_default().insert(d.vbase); } } - let top_n: usize = args.get(3).and_then(|s| s.parse().ok()).unwrap_or(usize::MAX); + let top_n: usize = args + .get(3) + .and_then(|s| s.parse().ok()) + .unwrap_or(usize::MAX); // Decode every container once; keep only the vcount → names mapping. let mut by_vcount: HashMap> = HashMap::new(); @@ -54,7 +60,9 @@ fn main() { files.sort(); let mut total_res = 0usize; for f in &files { - let Ok(bytes) = std::fs::read(f) else { continue }; + let Ok(bytes) = std::fs::read(f) else { + continue; + }; let names = xbg7_resource_names(&bytes); if names.is_empty() { continue; @@ -65,7 +73,10 @@ fn main() { for m in &models { total_res += 1; let whole: usize = m.meshes.iter().map(|s| s.positions.len()).sum(); - by_vcount.entry(whole as u32).or_default().push(format!("{container}:{}", m.name)); + by_vcount + .entry(whole as u32) + .or_default() + .push(format!("{container}:{}", m.name)); if m.meshes.len() > 1 { for (i, s) in m.meshes.iter().enumerate() { by_vcount @@ -76,7 +87,11 @@ fn main() { } } } - eprintln!("indexed {} resources from {} containers", total_res, files.len()); + eprintln!( + "indexed {} resources from {} containers", + total_res, + files.len() + ); let mut vcounts: Vec = draw_count.keys().copied().collect(); vcounts.sort_unstable_by(|a, b| b.cmp(a)); @@ -91,7 +106,15 @@ fn main() { let mut h = hit.clone(); h.sort(); let shown = h.len().min(8); - format!("{}{}", h[..shown].join(", "), if h.len() > shown { format!(", … ({} total)", h.len()) } else { String::new() }) + format!( + "{}{}", + h[..shown].join(", "), + if h.len() > shown { + format!(", … ({} total)", h.len()) + } else { + String::new() + } + ) }; println!("{v:6} {n:5} {b:4} {label}"); } diff --git a/crates/sylpheed-formats/examples/verify_fieldmap.rs b/crates/sylpheed-formats/examples/verify_fieldmap.rs index 4dd55562..c419c416 100644 --- a/crates/sylpheed-formats/examples/verify_fieldmap.rs +++ b/crates/sylpheed-formats/examples/verify_fieldmap.rs @@ -10,19 +10,149 @@ use sylpheed_formats::pak::PakArchive; // `addi r4, r30, -N` (r30 = 0x82088f94, so the name is a string in the image), // and the value lands in the first store into the object after the accessor // call. 125 float-typed fields. -const MAP: &[(usize, &str)] = &[(48, "Size_X"), (52, "Size_Y"), (56, "Size_Z"), (64, "Color_R"), (68, "Color_G"), (72, "Color_B"), (80, "Size_Radius"), (84, "HP"), (88, "HQRatio"), (92, "ShieldRatio"), (96, "ThrusterRatio"), (116, "ResistanceToOptics"), (120, "ResistanceToShell"), (124, "ResistanceToExplosion"), (128, "ResistanceToPlayer"), (132, "ResistanceParalyze"), (156, "MinimumVelocity"), (160, "MaximumVelocity"), (164, "CruisingVelocity"), (168, "Acceleration"), (172, "Deceleration"), (176, "AV_PitchPlus_Max"), (180, "AV_PitchPlus_Min"), (184, "AA_PitchPlus_Max"), (188, "AA_PitchPlus_Min"), (192, "AV_PitchMinus_Max"), (196, "AV_PitchMinus_Min"), (200, "AA_PitchMinus_Max"), (204, "AA_PitchMinus_Min"), (208, "AV_Yaw_Max"), (212, "AV_Yaw_Min"), (216, "AA_Yaw_Max"), (220, "AA_Yaw_Min"), (224, "AV_Roll_Max"), (228, "AV_Roll_Min"), (232, "AA_Roll_Max"), (236, "AA_Roll_Min"), (248, "SideThrustVelocity_Max"), (252, "SideThrustAcceleration"), (256, "MaximumBank_Normal"), (260, "YawDragFactor"), (264, "PitchDragFactor"), (268, "RollDragFactor"), (272, "DragFactorThreshold"), (276, "ArterBurner_Vc"), (280, "ReverseThrust_Vc"), (284, "ArterBurner_Acc"), (288, "ReverseThrust_Acc"), (292, "AccPitchFactor"), (296, "DecPitchFactor"), (300, "AV_AxisMode_Max"), (304, "AV_AxisMode_Min"), (308, "AA_AxisMode_Max"), (312, "AA_AxisMode_Min"), (316, "PowerCutConsumeShield"), (320, "PowerCutDeceleration"), (324, "AB_ConsumeShield_Begin"), (328, "AB_ConsumeShield"), (332, "AB_AV_PitchPlus"), (336, "AB_AA_PitchPlus"), (340, "AB_AV_PitchMinus"), (344, "AB_AA_PitchMinus"), (348, "AB_AV_Yaw"), (352, "AB_AA_Yaw"), (356, "AB_AV_Roll"), (360, "AB_AA_Roll"), (368, "SideRoll_Time"), (372, "SideRoll_Length"), (380, "BarrelRoll_CountMinimum"), (384, "BarrelRoll_CountMaximum"), (388, "BarrelRoll_Time"), (392, "BarrelRoll_Radius"), (400, "TurnAttack_CutoffRatio"), (404, "TurnAttack_DoubleRatio"), (408, "CutoffTimeMin"), (412, "CutoffTimeMax"), (416, "TurnAttack_DoubleTimeMin"), (420, "TurnAttack_DoubleTimeMax"), (428, "Turn_AngularVelocity"), (432, "TurnAway_Time_Minimum"), (436, "TurnAway_Time_Maximum"), (444, "BoostAway_Time_Minimum"), (448, "BoostAway_Time_Maximum"), (456, "HoldPosition_LengthMin"), (460, "HoldPosition_LengthMax"), (464, "HoldPosition_MinimumTime"), (468, "HoldPosition_MaximumTime"), (472, "HoldPosition_SideRatio"), (476, "HoldPosition_BackRatio"), (480, "HoldPosition_CutoffRatio"), (484, "HoldPosition_CancelTime"), (492, "Slalom_CutoffRatio"), (496, "Slalom_TurnCount_Min"), (500, "Slalom_TurnCount_Max"), (508, "Through_CutoffRatio"), (512, "Through_AngleMinimum"), (516, "Through_AngleMaximum"), (520, "Through_Time1Max"), (524, "Through_Time1Min"), (528, "Through_Time2Max"), (532, "Through_Time2Min"), (536, "Through_LengthMinimum"), (540, "Through_LengthMaximum"), (548, "SolidCutoff_Ratio"), (552, "SolidCutoff_LengthMin"), (556, "SolidCutoff_LengthMax"), (560, "HomingResistAdjustment"), (564, "UsingChaffRatio"), (568, "MaxValue"), (572, "ChargeDelay"), (576, "ChargeDelay_Break"), (580, "ChargeSpeed"), (584, "Delay"), (588, "DelayAdjustment"), (624, "DestroyMotionTime"), (628, "DryMass"), (632, "GrossMass"), (664, "LowerHPThresholdRatio"), (668, "SELength"), (672, "RadarRange"), (676, "FCSRange"), (680, "FiringRange"), (692, "AttackVesselPoint"), (696, "AttackCraftPoint"), (700, "DefencePoint")]; +const MAP: &[(usize, &str)] = &[ + (48, "Size_X"), + (52, "Size_Y"), + (56, "Size_Z"), + (64, "Color_R"), + (68, "Color_G"), + (72, "Color_B"), + (80, "Size_Radius"), + (84, "HP"), + (88, "HQRatio"), + (92, "ShieldRatio"), + (96, "ThrusterRatio"), + (116, "ResistanceToOptics"), + (120, "ResistanceToShell"), + (124, "ResistanceToExplosion"), + (128, "ResistanceToPlayer"), + (132, "ResistanceParalyze"), + (156, "MinimumVelocity"), + (160, "MaximumVelocity"), + (164, "CruisingVelocity"), + (168, "Acceleration"), + (172, "Deceleration"), + (176, "AV_PitchPlus_Max"), + (180, "AV_PitchPlus_Min"), + (184, "AA_PitchPlus_Max"), + (188, "AA_PitchPlus_Min"), + (192, "AV_PitchMinus_Max"), + (196, "AV_PitchMinus_Min"), + (200, "AA_PitchMinus_Max"), + (204, "AA_PitchMinus_Min"), + (208, "AV_Yaw_Max"), + (212, "AV_Yaw_Min"), + (216, "AA_Yaw_Max"), + (220, "AA_Yaw_Min"), + (224, "AV_Roll_Max"), + (228, "AV_Roll_Min"), + (232, "AA_Roll_Max"), + (236, "AA_Roll_Min"), + (248, "SideThrustVelocity_Max"), + (252, "SideThrustAcceleration"), + (256, "MaximumBank_Normal"), + (260, "YawDragFactor"), + (264, "PitchDragFactor"), + (268, "RollDragFactor"), + (272, "DragFactorThreshold"), + (276, "ArterBurner_Vc"), + (280, "ReverseThrust_Vc"), + (284, "ArterBurner_Acc"), + (288, "ReverseThrust_Acc"), + (292, "AccPitchFactor"), + (296, "DecPitchFactor"), + (300, "AV_AxisMode_Max"), + (304, "AV_AxisMode_Min"), + (308, "AA_AxisMode_Max"), + (312, "AA_AxisMode_Min"), + (316, "PowerCutConsumeShield"), + (320, "PowerCutDeceleration"), + (324, "AB_ConsumeShield_Begin"), + (328, "AB_ConsumeShield"), + (332, "AB_AV_PitchPlus"), + (336, "AB_AA_PitchPlus"), + (340, "AB_AV_PitchMinus"), + (344, "AB_AA_PitchMinus"), + (348, "AB_AV_Yaw"), + (352, "AB_AA_Yaw"), + (356, "AB_AV_Roll"), + (360, "AB_AA_Roll"), + (368, "SideRoll_Time"), + (372, "SideRoll_Length"), + (380, "BarrelRoll_CountMinimum"), + (384, "BarrelRoll_CountMaximum"), + (388, "BarrelRoll_Time"), + (392, "BarrelRoll_Radius"), + (400, "TurnAttack_CutoffRatio"), + (404, "TurnAttack_DoubleRatio"), + (408, "CutoffTimeMin"), + (412, "CutoffTimeMax"), + (416, "TurnAttack_DoubleTimeMin"), + (420, "TurnAttack_DoubleTimeMax"), + (428, "Turn_AngularVelocity"), + (432, "TurnAway_Time_Minimum"), + (436, "TurnAway_Time_Maximum"), + (444, "BoostAway_Time_Minimum"), + (448, "BoostAway_Time_Maximum"), + (456, "HoldPosition_LengthMin"), + (460, "HoldPosition_LengthMax"), + (464, "HoldPosition_MinimumTime"), + (468, "HoldPosition_MaximumTime"), + (472, "HoldPosition_SideRatio"), + (476, "HoldPosition_BackRatio"), + (480, "HoldPosition_CutoffRatio"), + (484, "HoldPosition_CancelTime"), + (492, "Slalom_CutoffRatio"), + (496, "Slalom_TurnCount_Min"), + (500, "Slalom_TurnCount_Max"), + (508, "Through_CutoffRatio"), + (512, "Through_AngleMinimum"), + (516, "Through_AngleMaximum"), + (520, "Through_Time1Max"), + (524, "Through_Time1Min"), + (528, "Through_Time2Max"), + (532, "Through_Time2Min"), + (536, "Through_LengthMinimum"), + (540, "Through_LengthMaximum"), + (548, "SolidCutoff_Ratio"), + (552, "SolidCutoff_LengthMin"), + (556, "SolidCutoff_LengthMax"), + (560, "HomingResistAdjustment"), + (564, "UsingChaffRatio"), + (568, "MaxValue"), + (572, "ChargeDelay"), + (576, "ChargeDelay_Break"), + (580, "ChargeSpeed"), + (584, "Delay"), + (588, "DelayAdjustment"), + (624, "DestroyMotionTime"), + (628, "DryMass"), + (632, "GrossMass"), + (664, "LowerHPThresholdRatio"), + (668, "SELength"), + (672, "RadarRange"), + (676, "FCSRange"), + (680, "FiringRange"), + (692, "AttackVesselPoint"), + (696, "AttackCraftPoint"), + (700, "DefencePoint"), +]; fn main() { let mut a = std::env::args().skip(1); let disc = a.next().unwrap(); let dump = a.next().unwrap(); - let pairs: Vec<(String, String)> = - a.filter_map(|s| s.split_once('=').map(|(i, v)| (i.into(), v.into()))).collect(); + let pairs: Vec<(String, String)> = a + .filter_map(|s| s.split_once('=').map(|(i, v)| (i.into(), v.into()))) + .collect(); let mut live: BTreeMap> = BTreeMap::new(); let mut cur = String::new(); for line in std::fs::read_to_string(&dump).unwrap().lines() { - if let Some(r) = line.strip_prefix("=== ") { cur = r.trim().into(); continue; } + if let Some(r) = line.strip_prefix("=== ") { + cur = r.trim().into(); + continue; + } let f: Vec<&str> = line.split_whitespace().collect(); // dump columns: addr +off hex u32 f32 -- the float is f[4], not f[3] if f.len() >= 5 && f[1].starts_with('+') { @@ -37,9 +167,13 @@ fn main() { let mut defaults: BTreeMap> = BTreeMap::new(); for e in pak.entries() { let Ok(b) = pak.read(e) else { continue }; - let Ok(o) = IdxdObject::parse(&b) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { + continue; + }; let Some(id) = o.get_raw("ID") else { continue }; - let Some((_, va)) = pairs.iter().find(|(i, _)| i == id) else { continue }; + let Some((_, va)) = pairs.iter().find(|(i, _)| i == id) else { + continue; + }; let Some(w) = live.get(va) else { continue }; for (off, key) in MAP { let Some(got) = w.get(off) else { continue }; @@ -50,12 +184,17 @@ fn main() { let rad = want * std::f32::consts::PI / 180.0; let ok = (want - got).abs() <= want.abs() * 1e-4 || (rad - got).abs() <= rad.abs() * 1e-4; - if ok { agree += 1 } else { + if ok { + agree += 1 + } else { disagree += 1; println!(" MISMATCH {id} {key}: disc {want}, live +{off} = {got}"); } } - None => defaults.entry((*key).into()).or_default().push((id.into(), *got)), + None => defaults + .entry((*key).into()) + .or_default() + .push((id.into(), *got)), } } } @@ -64,7 +203,14 @@ fn main() { for (key, hits) in &defaults { let vals: Vec = hits.iter().map(|(_, v)| format!("{v}")).collect(); let uniq: std::collections::BTreeSet<&String> = vals.iter().collect(); - println!(" {:<22} {:<28} ({} units)", key, - uniq.iter().map(|s| s.as_str()).collect::>().join(", "), hits.len()); + println!( + " {:<22} {:<28} ({} units)", + key, + uniq.iter() + .map(|s| s.as_str()) + .collect::>() + .join(", "), + hits.len() + ); } } diff --git a/crates/sylpheed-formats/examples/voice_bank_dump.rs b/crates/sylpheed-formats/examples/voice_bank_dump.rs index 87dd11e9..ba6a3e4a 100644 --- a/crates/sylpheed-formats/examples/voice_bank_dump.rs +++ b/crates/sylpheed-formats/examples/voice_bank_dump.rs @@ -8,7 +8,9 @@ fn main() { let snd = PakArchive::open(format!("{disc}/dat/sound.pak")).expect("sound.pak"); for n in 450..=454 { let path = format!("eng\\etc\\VOICE_D_{n}.slb"); - let Some(entry) = snd.find_by_name(&path) else { continue }; + let Some(entry) = snd.find_by_name(&path) else { + continue; + }; let bytes = snd.read(entry).expect("read"); for (i, r) in slb::to_xma_riffs(&bytes).iter().enumerate() { let f = format!("{out}/VOICE_D_{n}_{i}.xma"); diff --git a/crates/sylpheed-formats/examples/voice_bank_shape.rs b/crates/sylpheed-formats/examples/voice_bank_shape.rs index 585de42c..f518ec94 100644 --- a/crates/sylpheed-formats/examples/voice_bank_shape.rs +++ b/crates/sylpheed-formats/examples/voice_bank_shape.rs @@ -17,7 +17,9 @@ fn main() { for n in 450..=454 { for dir in ["etc", "Voice", "Movie"] { let path = format!("eng\\{dir}\\VOICE_D_{n}.slb"); - let Some(entry) = snd.find_by_name(&path) else { continue }; + let Some(entry) = snd.find_by_name(&path) else { + continue; + }; let bytes = snd.read(entry).expect("read"); let riffs = slb::to_xma_riffs(&bytes); let sizes: Vec = riffs.iter().map(|r| r.len()).collect(); diff --git a/crates/sylpheed-formats/examples/voice_len_vs_subs.rs b/crates/sylpheed-formats/examples/voice_len_vs_subs.rs index 44e19ab9..235a58ca 100644 --- a/crates/sylpheed-formats/examples/voice_len_vs_subs.rs +++ b/crates/sylpheed-formats/examples/voice_len_vs_subs.rs @@ -55,7 +55,9 @@ fn main() { let cues = movie_subtitle::track_voice_cues(&lang, movie); let last = cues.iter().map(|(_, t)| *t).fold(0.0f32, f32::max); let path = format!("eng\\etc\\VOICE_D_{n}.slb"); - let Some(entry) = snd.find_by_name(&path) else { continue }; + let Some(entry) = snd.find_by_name(&path) else { + continue; + }; let bytes = snd.read(entry).expect("read"); // Sum the sub-waves' payloads as the decoder currently sees them. let riffs = slb::to_xma_riffs(&bytes); diff --git a/crates/sylpheed-formats/examples/why_missed.rs b/crates/sylpheed-formats/examples/why_missed.rs index 08cb3397..a3bf8996 100644 --- a/crates/sylpheed-formats/examples/why_missed.rs +++ b/crates/sylpheed-formats/examples/why_missed.rs @@ -1,12 +1,15 @@ //! Why does a named resource never decode? Reports the furthest gate its best //! candidate reached. Usage: why_missed [name-substring] -use sylpheed_formats::mesh::{debug_best_rejection, xbg7_resource_names, Xbg7Model}; use std::collections::HashSet; +use sylpheed_formats::mesh::{debug_best_rejection, xbg7_resource_names, Xbg7Model}; fn main() { let a: Vec = std::env::args().collect(); let bytes = std::fs::read(&a[1]).unwrap(); let filter = a.get(2).cloned().unwrap_or_default(); - let decoded: HashSet = Xbg7Model::stage_models(&bytes).into_iter().map(|m| m.name).collect(); + let decoded: HashSet = Xbg7Model::stage_models(&bytes) + .into_iter() + .map(|m| m.name) + .collect(); for n in xbg7_resource_names(&bytes) { if decoded.contains(&n) || (!filter.is_empty() && !n.contains(&filter)) { continue; diff --git a/crates/sylpheed-formats/src/audio.rs b/crates/sylpheed-formats/src/audio.rs index ed98b87e..1b85c6d6 100644 --- a/crates/sylpheed-formats/src/audio.rs +++ b/crates/sylpheed-formats/src/audio.rs @@ -176,8 +176,7 @@ fn parse_riff_wave(bytes: &[u8]) -> Option { return None; } let le16 = |o: usize| u16::from_le_bytes([bytes[o], bytes[o + 1]]); - let le32 = - |o: usize| u32::from_le_bytes([bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]]); + let le32 = |o: usize| u32::from_le_bytes([bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]]); let mut pos = 12; let (mut tag, mut channels, mut rate, mut bits) = (0u16, 0u16, 0u32, 0u16); @@ -266,8 +265,12 @@ impl GameAudio { return Err(AudioError::NeedsDecoder(info.codec)); } let channels = info.channels.ok_or(AudioError::Malformed("no channels"))?; - let rate = info.sample_rate.ok_or(AudioError::Malformed("no sample rate"))?; - let bits = info.bits_per_sample.ok_or(AudioError::Malformed("no bit depth"))?; + let rate = info + .sample_rate + .ok_or(AudioError::Malformed("no sample rate"))?; + let bits = info + .bits_per_sample + .ok_or(AudioError::Malformed("no bit depth"))?; // Locate the `data` chunk body. let (off, len) = riff_data_span(bytes).ok_or(AudioError::Malformed("no data chunk"))?; @@ -299,7 +302,11 @@ impl GameAudio { .collect(), _ => return Err(AudioError::UnsupportedPcm { tag: 0, bits }), }; - Ok(Self { samples, channels, sample_rate: rate }) + Ok(Self { + samples, + channels, + sample_rate: rate, + }) } } @@ -308,8 +315,9 @@ fn riff_data_span(bytes: &[u8]) -> Option<(usize, usize)> { if bytes.len() < 12 || &bytes[..4] != b"RIFF" || &bytes[8..12] != b"WAVE" { return None; } - let le32 = - |o: usize| u32::from_le_bytes([bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]]) as usize; + let le32 = |o: usize| { + u32::from_le_bytes([bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]]) as usize + }; let mut pos = 12; while pos + 8 <= bytes.len() { let size = le32(pos + 4); diff --git a/crates/sylpheed-formats/src/game_data.rs b/crates/sylpheed-formats/src/game_data.rs index de613537..ed5fca7f 100644 --- a/crates/sylpheed-formats/src/game_data.rs +++ b/crates/sylpheed-formats/src/game_data.rs @@ -251,7 +251,11 @@ fn load_objects( } /// Read every object whose record 0 hashes to `schema` (see [`schema`]). -fn load_table(pak: &PakArchive, schema: u32, build: impl Fn(&IdxdObject) -> Option) -> Vec { +fn load_table( + pak: &PakArchive, + schema: u32, + build: impl Fn(&IdxdObject) -> Option, +) -> Vec { load_objects(pak, |o| o.schema_hash == schema, build) } @@ -474,7 +478,10 @@ pub fn load_weapons(pak: &PakArchive) -> Vec { let mut seen = std::collections::BTreeSet::new(); load_objects(pak, is_weapon, Weapon::from_idxd) .into_iter() - .filter(|w| w.id.as_deref().is_some_and(|id| seen.insert(id.to_string()))) + .filter(|w| { + w.id.as_deref() + .is_some_and(|id| seen.insert(id.to_string())) + }) .collect() } @@ -792,7 +799,9 @@ pub struct ScoreRules { impl ScoreRules { fn from_record(f: Option<&RecordFields>) -> Self { - let Some(f) = f else { return ScoreRules::default() }; + let Some(f) = f else { + return ScoreRules::default(); + }; ScoreRules { main_mission_bonus: f.i64("MainMissionBonus"), main_mission_count: f.i64("MainMissionCount"), @@ -1121,14 +1130,20 @@ pub fn load_squadrons(pak: &PakArchive) -> Vec { let mut out = Vec::new(); for e in pak.entries() { let Ok(b) = pak.read(e) else { continue }; - let Ok(o) = IdxdObject::parse(&b) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { + continue; + }; let Some(index) = squadron_index(&o).map(str::to_string) else { continue; }; let records = RecordSet::from_idxd(&o); - let Some(list) = records.record(&index) else { continue }; + let Some(list) = records.record(&index) else { + continue; + }; for id in list.named.keys() { - let Some(f) = records.record(id) else { continue }; + let Some(f) = records.record(id) else { + continue; + }; let slots: Vec<&str> = f.positional.iter().map(|(_, v)| v.as_str()).collect(); let members = slots .as_chunks::<4>() @@ -1213,7 +1228,11 @@ pub fn load_demo_messages(pak: &PakArchive) -> Vec { duration: as_f32(f.at(3)), voice_clip: text(f.at(4)), page_count: f.i64("PageCount"), - page_keys: f.slots_from(5).filter(|s| !s.is_empty()).map(str::to_string).collect(), + page_keys: f + .slots_from(5) + .filter(|s| !s.is_empty()) + .map(str::to_string) + .collect(), }) }) .collect::>() @@ -1263,8 +1282,16 @@ pub fn load_unit_rosters(pak: &PakArchive) -> Vec { |id: &str| id.contains("Asteroid") || id.contains("cmesh") || id.contains("_Box"); let roster = UnitRoster { stage, - units: ids.iter().filter(|i| !is_prop(i)).map(|i| i.to_string()).collect(), - props: ids.iter().filter(|i| is_prop(i)).map(|i| i.to_string()).collect(), + units: ids + .iter() + .filter(|i| !is_prop(i)) + .map(|i| i.to_string()) + .collect(), + props: ids + .iter() + .filter(|i| is_prop(i)) + .map(|i| i.to_string()) + .collect(), }; (!roster.units.is_empty()).then_some(roster) }, @@ -1329,7 +1356,9 @@ pub fn load_pilot_rosters(pak: &PakArchive) -> Vec { let mut assignments = Vec::new(); let mut player_unit = None; for name in list.named.keys() { - let Some(f) = records.record(name) else { continue }; + let Some(f) = records.record(name) else { + continue; + }; let Some((callsign, pilot)) = name.split_once('-') else { continue; }; @@ -1384,7 +1413,9 @@ pub fn load_arsenal(pak: &PakArchive) -> Arsenal { let mut seen = [(); 4].map(|_| std::collections::BTreeSet::new()); for e in pak.entries() { let Ok(b) = pak.read(e) else { continue }; - let Ok(o) = IdxdObject::parse(&b) else { continue }; + let Ok(o) = IdxdObject::parse(&b) else { + continue; + }; let records = RecordSet::from_idxd(&o); for (idx, slot) in ["NOSE", "ARM1", "ARM2", "ARM3"].into_iter().enumerate() { let Some(f) = records.record(&format!("STANDARD_{slot}")) else { @@ -1429,11 +1460,7 @@ pub struct Record { impl Record { fn from_idxd(o: &IdxdObject) -> Option { let records = RecordSet::from_idxd(o); - let first = |field: &str| { - records - .names() - .find_map(|n| records.record(n)?.text(field)) - }; + let first = |field: &str| records.names().find_map(|n| records.record(n)?.text(field)); Some(Record { table: records.first_name()?.to_string(), schema: o.schema_hash, @@ -1490,10 +1517,16 @@ mod tests { // Launcher record. assert_eq!(rocket.interval, Some(1.0)); assert_eq!(rocket.loading_count, Some(1000)); - assert_eq!(rocket.target_type.as_deref(), Some("Vessel,Craft,Structure")); + assert_eq!( + rocket.target_type.as_deref(), + Some("Vessel,Craft,Structure") + ); assert_eq!(rocket.shot_type.as_deref(), Some("Single")); // Shell record — a separate ID/Name the flat reader merged with the above. - assert_eq!(rocket.shell_id.as_deref(), Some("Shell_TCAF_DeltaSaber_Rocket_P")); + assert_eq!( + rocket.shell_id.as_deref(), + Some("Shell_TCAF_DeltaSaber_Rocket_P") + ); assert_eq!(rocket.power, Some(100.0)); assert_eq!(rocket.velocity, Some(3000.0)); assert_eq!(rocket.max_range, Some(6000.0)); @@ -1565,7 +1598,11 @@ mod tests { assert_eq!(raymond.faces.len(), 13); assert!(raymond.faces.iter().all(|f| f.texture.ends_with(".t32"))); assert_eq!( - raymond.faces.iter().find(|f| f.id == "FaceRAYMOND_07").map(|f| f.texture.as_str()), + raymond + .faces + .iter() + .find(|f| f.id == "FaceRAYMOND_07") + .map(|f| f.texture.as_str()), Some("pjf003_C02.t32") ); } @@ -1574,15 +1611,24 @@ mod tests { fn loads_stages() { let Some(pak) = pak() else { return }; let stages = load_stages(&pak); - let s01 = stages.iter().find(|s| s.id == "S01").expect("stage S01 present"); + let s01 = stages + .iter() + .find(|s| s.id == "S01") + .expect("stage S01 present"); assert_eq!(s01.location.as_deref(), Some("Lebendorf")); assert_eq!(s01.phase_count(), 3); assert_eq!(s01.unit_table.as_deref(), Some("EnumUnit_S01.tbl")); assert_eq!(s01.squadron_table.as_deref(), Some("UnitGroup_S01.tbl")); - assert!(s01.stage_package.as_deref().is_some_and(|p| p.contains("Stage_S01"))); + assert!(s01 + .stage_package + .as_deref() + .is_some_and(|p| p.contains("Stage_S01"))); // 16-mission main campaign is present. for n in 1..=16 { - assert!(stages.iter().any(|s| s.id == format!("S{n:02}")), "missing S{n:02}"); + assert!( + stages.iter().any(|s| s.id == format!("S{n:02}")), + "missing S{n:02}" + ); } } @@ -1592,18 +1638,21 @@ mod tests { let sq = load_squadrons(&pak); assert_eq!(sq.len(), 1160, "squadron records across all stage tables"); // Every squadron's member list matches its declared count. - assert!(sq.iter().all(|s| s.members.len() as i64 == s.count.unwrap_or(-1))); + assert!(sq + .iter() + .all(|s| s.members.len() as i64 == s.count.unwrap_or(-1))); // The player's Rhino flight: 2 Delta Sabers, Katana leading. let rhino = sq .iter() - .find(|s| { - s.id == "TCN001" && s.formation_id.as_deref() == Some("Formation_2_Rhino1") - }) + .find(|s| s.id == "TCN001" && s.formation_id.as_deref() == Some("Formation_2_Rhino1")) .expect("TCN001 Rhino flight"); assert_eq!(rhino.side.as_deref(), Some("TCAF")); assert_eq!(rhino.count, Some(2)); assert!(rhino.members.iter().all(|m| m.unit.contains("DeltaSaber"))); - assert_eq!(rhino.members[0].message_set.as_deref(), Some("MessageSet_Katana")); + assert_eq!( + rhino.members[0].message_set.as_deref(), + Some("MessageSet_Katana") + ); } #[test] @@ -1613,8 +1662,13 @@ mod tests { assert_eq!(msgs.len(), 11775); // Every line names a speaker and a portrait; the old reader attributed // only a minority. - assert!(msgs.iter().all(|m| m.character.is_some() && m.face.is_some())); - assert_eq!(msgs.iter().filter(|m| m.voice_clip.is_some()).count(), 11758); + assert!(msgs + .iter() + .all(|m| m.character.is_some() && m.face.is_some())); + assert_eq!( + msgs.iter().filter(|m| m.voice_clip.is_some()).count(), + 11758 + ); assert!(msgs.iter().all(|m| !m.page_keys.is_empty())); } @@ -1623,7 +1677,9 @@ mod tests { let Some(pak) = pak() else { return }; let rosters = load_unit_rosters(&pak); assert_eq!(rosters.len(), 31); - assert!(rosters.iter().all(|r| r.units.iter().all(|u| !u.contains("Asteroid")))); + assert!(rosters + .iter() + .all(|r| r.units.iter().all(|u| !u.contains("Asteroid")))); // A roster tagged S01 exists and names the player's Delta Saber. let s01 = rosters.iter().find(|r| r.stage.as_deref() == Some("S01")); assert!(s01.is_some_and(|r| r.units.iter().any(|u| u.contains("DeltaSaber")))); @@ -1631,16 +1687,24 @@ mod tests { #[test] fn loads_pilot_rosters() { - let Ok(disc) = std::env::var("SYLPHEED_DISC") else { return }; - let Ok(pak) = PakArchive::open(format!("{disc}/dat/GP_HANGAR_ARSENAL.pak")) else { return }; + let Ok(disc) = std::env::var("SYLPHEED_DISC") else { + return; + }; + let Ok(pak) = PakArchive::open(format!("{disc}/dat/GP_HANGAR_ARSENAL.pak")) else { + return; + }; let rosters = load_pilot_rosters(&pak); assert_eq!(rosters.len(), 168); // Every config seats the player and names the craft flown. assert!(rosters.iter().all(|r| r.player_unit.is_some())); assert!(rosters.iter().any(|r| { - r.assignments.iter().any(|a| a.callsign.starts_with("Rhino") && a.pilot == "Katana") + r.assignments + .iter() + .any(|a| a.callsign.starts_with("Rhino") && a.pilot == "Katana") })); - assert!(rosters.iter().any(|r| r.assignments.iter().any(|a| a.callsign.starts_with("Bird")))); + assert!(rosters + .iter() + .any(|r| r.assignments.iter().any(|a| a.callsign.starts_with("Bird")))); let a = rosters .iter() .flat_map(|r| &r.assignments) @@ -1651,10 +1715,17 @@ mod tests { #[test] fn loads_arsenal() { - let Ok(disc) = std::env::var("SYLPHEED_DISC") else { return }; - let Ok(pak) = PakArchive::open(format!("{disc}/dat/GP_HANGAR_ARSENAL.pak")) else { return }; + let Ok(disc) = std::env::var("SYLPHEED_DISC") else { + return; + }; + let Ok(pak) = PakArchive::open(format!("{disc}/dat/GP_HANGAR_ARSENAL.pak")) else { + return; + }; let a = load_arsenal(&pak); - assert_eq!((a.nose.len(), a.arm1.len(), a.arm2.len(), a.arm3.len()), (8, 12, 9, 9)); + assert_eq!( + (a.nose.len(), a.arm1.len(), a.arm2.len(), a.arm3.len()), + (8, 12, 9, 9) + ); assert!(a.nose.iter().any(|w| w.starts_with("Stiletto"))); assert!(a.arm1.iter().any(|w| w.starts_with("Falcon"))); // No field keys, headers or pilot names leak into the option lists — the diff --git a/crates/sylpheed-formats/src/ixud.rs b/crates/sylpheed-formats/src/ixud.rs index fd4db12f..b30f5316 100644 --- a/crates/sylpheed-formats/src/ixud.rs +++ b/crates/sylpheed-formats/src/ixud.rs @@ -192,7 +192,13 @@ mod tests { #[test] fn reference_track() { - let b = synth(&["SUBTITLE", "MSG_DEMO_240", "00:01.00", "MSG_DEMO_241", "00:03.20"]); + let b = synth(&[ + "SUBTITLE", + "MSG_DEMO_240", + "00:01.00", + "MSG_DEMO_241", + "00:03.20", + ]); let sub = parse(&b).unwrap(); assert_eq!(sub.cues.len(), 2); assert_eq!(sub.cues[0].end, None); @@ -205,7 +211,6 @@ mod tests { } } - /// A parsed IXUD object: its records and their fields, addressable by name. /// /// The counterpart of [`crate::IdxdObject`] for wide strings. Prefer this to the diff --git a/crates/sylpheed-formats/src/lib.rs b/crates/sylpheed-formats/src/lib.rs index ea515235..10e86c87 100644 --- a/crates/sylpheed-formats/src/lib.rs +++ b/crates/sylpheed-formats/src/lib.rs @@ -90,10 +90,10 @@ pub use audio::{AudioCodec, AudioInfo, GameAudio}; pub use font::FontInfo; pub use idxd::{IdxdError, IdxdObject}; pub use ixud::{Cue, IxudField, IxudObject, IxudRecord, Subtitle}; -pub use movie_subtitle::{SubCue, SubLang}; pub use mesh::{GameMesh, Xbg7Model}; +pub use movie_subtitle::{SubCue, SubLang}; +pub use pak::{PakArchive, PakEntry, PakError}; pub use ratc::RatcChild; pub use t8ad::T8adImage; -pub use pak::{PakArchive, PakEntry, PakError}; pub use texture::{X360Texture, X360TextureFormat}; pub use vfs::{GameAssets, VfsError}; diff --git a/crates/sylpheed-formats/src/localization.rs b/crates/sylpheed-formats/src/localization.rs index 2a636b8a..46bf6b7d 100644 --- a/crates/sylpheed-formats/src/localization.rs +++ b/crates/sylpheed-formats/src/localization.rs @@ -165,8 +165,12 @@ mod tests { // Disc-backed — skipped unless SYLPHEED_DISC is set. #[test] fn resolves_real_text() { - let Ok(disc) = std::env::var("SYLPHEED_DISC") else { return }; - let Ok(pak) = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")) else { return }; + let Ok(disc) = std::env::var("SYLPHEED_DISC") else { + return; + }; + let Ok(pak) = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")) else { + return; + }; let t = TextIndex::build(&pak); assert!(t.len() > 5000, "expected a large index, got {}", t.len()); assert_eq!(t.character_name("CharacterRAYMOND"), Some("Raymond")); diff --git a/crates/sylpheed-formats/src/media.rs b/crates/sylpheed-formats/src/media.rs index 14cccb43..9fc2cffb 100644 --- a/crates/sylpheed-formats/src/media.rs +++ b/crates/sylpheed-formats/src/media.rs @@ -138,8 +138,7 @@ pub fn se_wave_riff( e.comp_size )); } - let packets = - source.read_segment_range("dat/sound", e.offset as u64 + offset as u64, len)?; + let packets = source.read_segment_range("dat/sound", e.offset as u64 + offset as u64, len)?; Ok(crate::slb::xma1_wave_riff(&packets, channels, rate)) } @@ -218,7 +217,9 @@ pub fn hokyu_voice_token( let lang_pak = source .open_pak(&format!("dat/movie/{}.pak", lang.code_pub())) .ok()?; - let want = ms::track_voice_cues(&lang_pak, movie).first().map(|&(d, _)| d)?; + let want = ms::track_voice_cues(&lang_pak, movie) + .first() + .map(|&(d, _)| d)?; crate::movie_manifest::parse(manifest) .into_iter() .find_map(|e| { @@ -338,7 +339,9 @@ impl DiscSource for DirectorySource { break; } let path = self.root.join(format!("{stem}.p{i:02}")); - let Ok(meta) = std::fs::metadata(&path) else { break }; + let Ok(meta) = std::fs::metadata(&path) else { + break; + }; let seg_len = meta.len(); if skip >= seg_len { skip -= seg_len; diff --git a/crates/sylpheed-formats/src/mesh.rs b/crates/sylpheed-formats/src/mesh.rs index a2da4384..83b94a23 100644 --- a/crates/sylpheed-formats/src/mesh.rs +++ b/crates/sylpheed-formats/src/mesh.rs @@ -232,8 +232,7 @@ impl Xbg7Model { // XBG7 layout is NOT fixed-stride — models omit UV or use fewer elements // (stride 20 = pos+normal, stride 24 = pos+normal+uv, …). Confirmed // against a Canary GPU vertex-fetch capture (see docs/re/xbg7-mesh.md). - let decl = parse_vertex_decl(&bytes[desc..desc_end]) - .ok_or(MeshError::UnsupportedLayout)?; + let decl = parse_vertex_decl(&bytes[desc..desc_end]).ok_or(MeshError::UnsupportedLayout)?; // Extract the ordered list of sub-mesh (vtx_count, idx_count) records. let records = submesh_records(&bytes[desc..desc_end]); @@ -481,7 +480,10 @@ impl Xbg7Model { return Self::anchor_models_uncached(bytes, min_consistency, should_cancel, None); }; let full = full_decode_cached(bytes, min_consistency, should_cancel); - full.iter().filter(|m| w.contains(&m.name)).cloned().collect() + full.iter() + .filter(|m| w.contains(&m.name)) + .cloned() + .collect() } fn anchor_models_uncached( @@ -554,7 +556,11 @@ impl Xbg7Model { // at a different offset when requested alone. The filter is applied // to the OUTPUT instead, so a subset is always a subset of the // container's own answer. - let mut decls = if submesh_decls() { all_vertex_decls(d) } else { Vec::new() }; + let mut decls = if submesh_decls() { + all_vertex_decls(d) + } else { + Vec::new() + }; if decls.len() != markers.len() { // Never seen on the disc, but if the two walks disagree fall back // to the single declaration rather than mis-pair them. @@ -623,8 +629,14 @@ impl Xbg7Model { } else { // Several sub-meshes sharing grouped index/vertex pools → the // deterministic grouped-pool decode (hero ships et al.). - let grouped = - anchor_grouped_meshes(bytes, data_base, starts, &r.markers, &r.decls, &empty_taken); + let grouped = anchor_grouped_meshes( + bytes, + data_base, + starts, + &r.markers, + &r.decls, + &empty_taken, + ); if !grouped.is_empty() { grouped } else { @@ -781,7 +793,9 @@ pub fn debug_try_anchor( let header = Xpr2Header::read(&mut cur).ok()?; const DIR_BASE: usize = 0x10; for _ in 0..header.num_resources { - let Ok(e) = Xpr2ResourceEntry::read(&mut cur) else { break }; + let Ok(e) = Xpr2ResourceEntry::read(&mut cur) else { + break; + }; if &e.type_tag != b"XBG7" { continue; } @@ -861,7 +875,11 @@ pub fn debug_best_rejection(bytes: &[u8], name: &str) -> Option<(usize, String)> let span = rel_ib[n - 1] + markers[n - 1].1 * 2; let kmax = (0..n).max_by_key(|&i| markers[i].1).unwrap_or(0); let (vck, ick) = markers[kmax]; - let off_v: usize = markers.iter().take(kmax).map(|&(vc, _)| vc * decl.stride).sum(); + let off_v: usize = markers + .iter() + .take(kmax) + .map(|&(vc, _)| vc * decl.stride) + .sum(); let mut best = (0usize, String::from("no pool start reached any gate")); for &vb0 in &starts { for pad in 0..=3usize { @@ -928,7 +946,9 @@ pub fn debug_best_rejection(bytes: &[u8], name: &str) -> Option<(usize, String)> /// Diagnostic: the stride of every sub-mesh declaration of a named resource. /// A grouped pool may mix them (`n201_01` → 24, 24, 24, 28). pub fn debug_decl_strides(bytes: &[u8], name: &str) -> Vec { - decls_of(bytes, name).map(|v| v.iter().map(|d| d.stride).collect()).unwrap_or_default() + decls_of(bytes, name) + .map(|v| v.iter().map(|d| d.stride).collect()) + .unwrap_or_default() } /// Every sub-mesh declaration of a named resource (see [`all_vertex_decls`]). @@ -940,7 +960,9 @@ fn decls_of(bytes: &[u8], name: &str) -> Option> { let header = Xpr2Header::read(&mut cur).ok()?; const DIR_BASE: usize = 0x10; for _ in 0..header.num_resources { - let Ok(e) = Xpr2ResourceEntry::read(&mut cur) else { break }; + let Ok(e) = Xpr2ResourceEntry::read(&mut cur) else { + break; + }; if &e.type_tag != b"XBG7" { continue; } @@ -1074,14 +1096,20 @@ pub fn debug_find_index_buffer(bytes: &[u8], name: &str, vb: usize) -> Vec<(usiz /// value that captures the whole measured gain. `XBG7_EDGE_CAP` overrides it (see /// docs/re/structures/xbg7-mesh.md). fn edge_cap() -> f32 { - std::env::var("XBG7_EDGE_CAP").ok().and_then(|v| v.parse().ok()).unwrap_or(1.0) + std::env::var("XBG7_EDGE_CAP") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(1.0) } /// Winding floor for the grouped-pool **pivot** (default `0.85`). The single-block /// path settled at 0.70 on measurement; this is the same question for the pivot, /// and `XBG7_GROUPED_CONSISTENCY` sweeps it. fn grouped_consistency() -> f32 { - std::env::var("XBG7_GROUPED_CONSISTENCY").ok().and_then(|v| v.parse().ok()).unwrap_or(0.85) + std::env::var("XBG7_GROUPED_CONSISTENCY") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(0.85) } /// Coverage requirement, as `max_index + N >= vtx_count`. **`1` since @@ -1096,14 +1124,20 @@ fn grouped_consistency() -> f32 { /// disc-wide; capture oracle unchanged at 46/46. `XBG7_COVER_SLACK` overrides /// (note `0` rejects everything — the comparison is `max_idx + N >= vtx_count`). fn cover_slack() -> usize { - std::env::var("XBG7_COVER_SLACK").ok().and_then(|v| v.parse().ok()).unwrap_or(1) + std::env::var("XBG7_COVER_SLACK") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(1) } /// Smallest bounding-box extent a block may have (default `0.5`). An absolute /// floor on a format with no unit convention is a scale assumption, so it is a /// knob: `XBG7_MIN_EXTENT`. fn min_extent() -> f32 { - std::env::var("XBG7_MIN_EXTENT").ok().and_then(|v| v.parse().ok()).unwrap_or(0.5) + std::env::var("XBG7_MIN_EXTENT") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(0.5) } /// Use a scale-free collinearity test for degeneracy instead of the absolute @@ -1136,7 +1170,10 @@ fn rel_degen() -> bool { /// perfect cross-container consistency for 442 resources — recorded rather than /// taken, since consistency is the weaker witness (see the docs). fn pad0_consistency() -> f32 { - std::env::var("XBG7_PAD0_CONSISTENCY").ok().and_then(|v| v.parse().ok()).unwrap_or(0.70) + std::env::var("XBG7_PAD0_CONSISTENCY") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(0.70) } /// Revert knob for the 2026-08-13 pad scoring: with `XBG7_PAD_FIRST_MATCH=1`, @@ -1159,22 +1196,32 @@ fn pad0_consistency() -> f32 { /// of the stage-05 mission rise **124 → 128** identical. See /// docs/re/structures/xbg7-mesh.md. fn submesh_decls() -> bool { - std::env::var("XBG7_SUBMESH_DECLS").map(|v| v != "0").unwrap_or(true) + std::env::var("XBG7_SUBMESH_DECLS") + .map(|v| v != "0") + .unwrap_or(true) } fn pad_first_match() -> bool { - std::env::var("XBG7_PAD_FIRST_MATCH").map(|v| v == "1").unwrap_or(false) + std::env::var("XBG7_PAD_FIRST_MATCH") + .map(|v| v == "1") + .unwrap_or(false) } /// Triangle count below which the looser [`small_cap`] applies. `0` (default) /// disables the split, so the flat [`edge_cap`] governs every block. fn small_tris() -> usize { - std::env::var("XBG7_SMALL_TRIS").ok().and_then(|v| v.parse().ok()).unwrap_or(0) + std::env::var("XBG7_SMALL_TRIS") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(0) } /// The connectivity cap for blocks below [`small_tris`] triangles. fn small_cap() -> f32 { - std::env::var("XBG7_EDGE_CAP_SMALL").ok().and_then(|v| v.parse().ok()).unwrap_or(0.45) + std::env::var("XBG7_EDGE_CAP_SMALL") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(0.45) } /// Internal: the descriptor parameters the diagnostics need. @@ -1186,7 +1233,9 @@ fn decl_of(bytes: &[u8], name: &str) -> Option<(VertexDecl, Vec<(usize, usize)>) let header = Xpr2Header::read(&mut cur).ok()?; const DIR_BASE: usize = 0x10; for _ in 0..header.num_resources { - let Ok(e) = Xpr2ResourceEntry::read(&mut cur) else { break }; + let Ok(e) = Xpr2ResourceEntry::read(&mut cur) else { + break; + }; if &e.type_tag != b"XBG7" { continue; } @@ -1216,7 +1265,9 @@ pub fn debug_resource_params(bytes: &[u8], name: &str) -> Option<(Vec<(usize, us let header = Xpr2Header::read(&mut cur).ok()?; const DIR_BASE: usize = 0x10; for _ in 0..header.num_resources { - let Ok(e) = Xpr2ResourceEntry::read(&mut cur) else { break }; + let Ok(e) = Xpr2ResourceEntry::read(&mut cur) else { + break; + }; if &e.type_tag != b"XBG7" { continue; } @@ -1427,7 +1478,14 @@ fn anchor_pool_mesh( // exactly ONE degenerate-free block in their container, sitting later // in file order than the lookalike we were taking. if degen == 0 || pad_first_match() { - return Some(read_pool_mesh(bytes, vb - idx_bytes - pad, vb, index_count, vtx_count, decl)); + return Some(read_pool_mesh( + bytes, + vb - idx_bytes - pad, + vb, + index_count, + vtx_count, + decl, + )); } if dirty.is_none() { dirty = Some((vb, pad)); @@ -1446,7 +1504,14 @@ fn anchor_pool_mesh( // Nothing clean anywhere: keep the first accepted block, so this can never // cost coverage relative to first-match. if let Some((vb, pad)) = dirty { - return Some(read_pool_mesh(bytes, vb - idx_bytes - pad, vb, index_count, vtx_count, decl)); + return Some(read_pool_mesh( + bytes, + vb - idx_bytes - pad, + vb, + index_count, + vtx_count, + decl, + )); } None } @@ -1631,7 +1696,11 @@ fn validate_block_report( // capture-proven false positive (`e106_eng_02_l`, ratio 0.417). Under // `XBG7_SMALL_TRIS` blocks below that triangle count get the looser // `XBG7_EDGE_CAP_SMALL` instead — a targeted relaxation, off by default. - let cap = if tris < small_tris() { small_cap() } else { edge_cap() }; + let cap = if tris < small_tris() { + small_cap() + } else { + edge_cap() + }; if mean_edge / diag > cap { return Err(format!( "connectivity: mean_edge/diag {:.3} > cap {cap:.2}", @@ -1737,7 +1806,9 @@ fn anchor_grouped_meshes( let (vc, ic) = markers[i]; let ib = ib0 + rel_ib[i]; if ib + ic * 2 > bytes.len() - || vc.checked_mul(decls[i].stride).is_none_or(|b| vb + b > bytes.len()) + || vc + .checked_mul(decls[i].stride) + .is_none_or(|b| vb + b > bytes.len()) { break; } @@ -1793,7 +1864,16 @@ fn anchor_grouped_meshes( // collapses well below 0.85, so only the true pad/pivot passes. let ib_k = ib0 + rel_ib[kmax]; let vb_k = vb0 + off_v[kmax]; - if !validate_block(bytes, ib_k, vb_k, vck, ick, &decls[kmax], grouped_consistency(), true) { + if !validate_block( + bytes, + ib_k, + vb_k, + vck, + ick, + &decls[kmax], + grouped_consistency(), + true, + ) { continue; } let (degen, wind) = index_run_quality(bytes, ib_k, vb_k, ick, &decls[kmax]); @@ -2642,16 +2722,28 @@ fn saber_measured(part: &str) -> Option<(M3, [f32; 3])> { Some(match part { // fin bdy_04 — V-tail, canted; mounted on the nacelle. "bdy_04" => ( - [[0.9563, -0.2924, 0.0], [0.2924, 0.9563, 0.0], [0.0, 0.0, 1.0]], + [ + [0.9563, -0.2924, 0.0], + [0.2924, 0.9563, 0.0], + [0.0, 0.0, 1.0], + ], [-5.2350, 1.0679, -9.7182], ), // flaps bdy_04_2 / bdy_04_3 — ride the fin (compound rotation). "bdy_04_2" => ( - [[0.9563, -0.2766, -0.0947], [0.2924, 0.9048, 0.3098], [-0.0001, -0.3240, 0.9461]], + [ + [0.9563, -0.2766, -0.0947], + [0.2924, 0.9048, 0.3098], + [-0.0001, -0.3240, 0.9461], + ], [-6.1562, 3.3121, -10.5028], ), "bdy_04_3" => ( - [[0.9563, -0.2766, -0.0947], [0.2924, 0.9048, 0.3098], [-0.0001, -0.3240, 0.9461]], + [ + [0.9563, -0.2766, -0.0947], + [0.2924, 0.9048, 0.3098], + [-0.0001, -0.3240, 0.9461], + ], [-5.7261, 3.4436, -10.5026], ), // winglet bdy_06 + tip bdy_07 — on the nacelle (no cant). @@ -2775,7 +2867,15 @@ pub fn node_transforms(bytes: &[u8], resource_name: &str) -> Vec .checked_sub(4) .filter(|&s| s > i && s + 4 <= head_end && &d[s..s + 4] == b"rou_"); let part = name.strip_prefix(&prefix).unwrap_or(name).to_string(); - recs.push(Rec { name_start: i, part, vtx, t48, local_m, local_t, sib_end }); + recs.push(Rec { + name_start: i, + part, + vtx, + t48, + local_m, + local_t, + sib_end, + }); i = j.max(i + 1); } @@ -2796,7 +2896,10 @@ pub fn node_transforms(bytes: &[u8], resource_name: &str) -> Vec while stack.last().is_some_and(|s| s.0 <= rec.name_start) { stack.pop(); } - let (pm, pt) = stack.last().map(|s| (s.1, s.2)).unwrap_or((M3_ID, [0.0; 3])); + let (pm, pt) = stack + .last() + .map(|s| (s.1, s.2)) + .unwrap_or((M3_ID, [0.0; 3])); // world = parent ∘ local let wm = m3_mul(pm, rec.local_m); let r = m3_vec(pm, rec.local_t); @@ -2950,7 +3053,14 @@ pub fn scene_world_nodes(bytes: &[u8], composite_name: &str) -> Vec { let sib_end = sib_ptr.checked_sub(4).filter(|&s| { s > i && s + 4 <= head_end && (&d[s..s + 4] == b"rou_" || &d[s..s + 3] == b"GN_") }); - recs.push(Rec { name_start: i, name, local_m, local_t, local_s, sib_end }); + recs.push(Rec { + name_start: i, + name, + local_m, + local_t, + local_s, + sib_end, + }); i = j.max(i + 1); } @@ -2961,11 +3071,19 @@ pub fn scene_world_nodes(bytes: &[u8], composite_name: &str) -> Vec { while stack.last().is_some_and(|s| s.0 <= rec.name_start) { stack.pop(); } - let (pm, pt) = stack.last().map(|s| (s.1, s.2)).unwrap_or((M3_ID, [0.0; 3])); + let (pm, pt) = stack + .last() + .map(|s| (s.1, s.2)) + .unwrap_or((M3_ID, [0.0; 3])); let wm = m3_mul(pm, rec.local_m); let r = m3_vec(pm, rec.local_t); let wt = [r[0] + pt[0], r[1] + pt[1], r[2] + pt[2]]; - out.push(ScenePart { resource: rec.name.clone(), m: wm, t: wt, s: rec.local_s }); + out.push(ScenePart { + resource: rec.name.clone(), + m: wm, + t: wt, + s: rec.local_s, + }); let end = rec .sib_end .unwrap_or_else(|| stack.last().map(|s| s.0).unwrap_or(head_end)); diff --git a/crates/sylpheed-formats/src/movie_manifest.rs b/crates/sylpheed-formats/src/movie_manifest.rs index 8fe955be..66f27486 100644 --- a/crates/sylpheed-formats/src/movie_manifest.rs +++ b/crates/sylpheed-formats/src/movie_manifest.rs @@ -135,8 +135,12 @@ fn parse_records(bytes: &[u8]) -> Option> { ids.sort_unstable(); let mut out = Vec::with_capacity(ids.len()); for (_, slot) in ids { - let Some(rec) = obj.record(slot) else { continue }; - let Some(movie) = rec.get("MOVIE") else { continue }; + let Some(rec) = obj.record(slot) else { + continue; + }; + let Some(movie) = rec.get("MOVIE") else { + continue; + }; let (kind, mission, phase) = classify_slot(slot); out.push(MovieEntry { slot: slot.to_string(), @@ -162,16 +166,18 @@ fn parse_string_pool(bytes: &[u8]) -> Vec { let first_wmv = toks.iter().position(|t| t.ends_with(".wmv")); let key_start = toks.iter().position(|t| t == "LOGO1"); let (slot_keys, value_toks): (Vec<&str>, &[String]) = match (key_start, first_wmv) { - (Some(k), Some(w)) if k < w => ( - toks[k..w].iter().map(String::as_str).collect(), - &toks[w..], - ), + (Some(k), Some(w)) if k < w => { + (toks[k..w].iter().map(String::as_str).collect(), &toks[w..]) + } _ => (Vec::new(), toks.as_slice()), }; // Build the value records (drop the schema-template field keys). let mut records: Vec = Vec::new(); - for t in value_toks.iter().filter(|t| !FIELD_KEYS.contains(&t.as_str())) { + for t in value_toks + .iter() + .filter(|t| !FIELD_KEYS.contains(&t.as_str())) + { if let Some(name) = t.strip_suffix(".wmv") { records.push(MovieEntry { slot: String::new(), @@ -198,10 +204,8 @@ fn parse_string_pool(bytes: &[u8]) -> Vec { // Attach slot keys to records, recovering valueless-slot gaps via MS anchors. if !slot_keys.is_empty() { - let ms_targets: std::collections::HashSet = slot_keys - .iter() - .filter_map(|k| ms_target(k)) - .collect(); + let ms_targets: std::collections::HashSet = + slot_keys.iter().filter_map(|k| ms_target(k)).collect(); let mut j = 0usize; for slot in &slot_keys { if j >= records.len() { @@ -419,13 +423,21 @@ mod tests { &[ &["logo1.wmv", "MOVIE"], &["S01A.wmv", "eng.pak+SUBTITLE_S01A.tbl", "VOICE_S01A"], - &["RT01A.wmv", "eng.pak+pwrt01.prt", "VOICE_RT01A", "VOICETRACK"], + &[ + "RT01A.wmv", + "eng.pak+pwrt01.prt", + "VOICE_RT01A", + "VOICETRACK", + ], &["RT01C_2.wmv", "VOICE_RT01C_2"], ], ); let m = parse(&blob); assert_eq!(m.len(), 4); - assert_eq!((m[0].slot.as_str(), m[0].kind), ("LOGO1", MovieKind::System)); + assert_eq!( + (m[0].slot.as_str(), m[0].kind), + ("LOGO1", MovieKind::System) + ); assert_eq!( (m[1].slot.as_str(), m[1].kind, m[1].mission), ("MS01A", MovieKind::Intro, Some(1)) diff --git a/crates/sylpheed-formats/src/movie_subtitle.rs b/crates/sylpheed-formats/src/movie_subtitle.rs index 7a0e112b..e9dc5233 100644 --- a/crates/sylpheed-formats/src/movie_subtitle.rs +++ b/crates/sylpheed-formats/src/movie_subtitle.rs @@ -142,7 +142,11 @@ pub fn load(movie_basename: &str, lang_pak: &PakArchive, text_pak: &PakArchive) text: resolved, }); } - cues.sort_by(|a, b| a.start.partial_cmp(&b.start).unwrap_or(std::cmp::Ordering::Equal)); + cues.sort_by(|a, b| { + a.start + .partial_cmp(&b.start) + .unwrap_or(std::cmp::Ordering::Equal) + }); cues } @@ -222,9 +226,7 @@ pub fn build_demo_text(text_pak: &PakArchive) -> BTreeMap> { // Pair only when the preceding token is real text — not another // MSG_DEMO key (bare keys are also serialized consecutively in the // record directory, which would otherwise masquerade as text). - if demo_ref(&w[0]).is_none() - && text_key(&w[0]).is_none() - && !w[0].trim().is_empty() + if demo_ref(&w[0]).is_none() && text_key(&w[0]).is_none() && !w[0].trim().is_empty() { by_demo .entry(demo) @@ -277,14 +279,19 @@ pub fn build_caption_text(text_pak: &PakArchive) -> BTreeMap // rule of the format. Same mistake the IDXD reader made. for rec in obj.records() { for f in &rec.fields { - let Some(key) = f.name.as_deref() else { continue }; + let Some(key) = f.name.as_deref() else { + continue; + }; let Some((id, page, line)) = caption_key(key) else { continue; }; if f.value.trim().is_empty() { continue; } - by_id.entry(id).or_default().insert((page, line), clean(&f.value)); + by_id + .entry(id) + .or_default() + .insert((page, line), clean(&f.value)); } } } diff --git a/crates/sylpheed-formats/src/movie_voice.rs b/crates/sylpheed-formats/src/movie_voice.rs index 3a042d34..b2faeae0 100644 --- a/crates/sylpheed-formats/src/movie_voice.rs +++ b/crates/sylpheed-formats/src/movie_voice.rs @@ -37,10 +37,7 @@ pub fn registry_voice_ids(registry: &[u8]) -> HashMap { let mut out = HashMap::new(); for w in toks.windows(2) { let (name, num) = (w[0], w[1]); - if name.starts_with(b"VOICE_") - && !num.is_empty() - && num.iter().all(u8::is_ascii_digit) - { + if name.starts_with(b"VOICE_") && !num.is_empty() && num.iter().all(u8::is_ascii_digit) { if let (Ok(n), Ok(id)) = ( std::str::from_utf8(name), std::str::from_utf8(num).unwrap().parse::(), diff --git a/crates/sylpheed-formats/src/ratc.rs b/crates/sylpheed-formats/src/ratc.rs index b1a8aced..094b30aa 100644 --- a/crates/sylpheed-formats/src/ratc.rs +++ b/crates/sylpheed-formats/src/ratc.rs @@ -123,7 +123,9 @@ fn name_before(bytes: &[u8], off: usize) -> String { let mut run_start: Option = None; let flush = |from: usize, to: usize, best: &mut String| { if to - from >= 3 { - *best = String::from_utf8_lossy(&window[from..to]).trim().to_string(); + *best = String::from_utf8_lossy(&window[from..to]) + .trim() + .to_string(); } }; for (i, &c) in window.iter().enumerate() { diff --git a/crates/sylpheed-formats/src/savegame.rs b/crates/sylpheed-formats/src/savegame.rs index 6905c0f5..8f9d43e9 100644 --- a/crates/sylpheed-formats/src/savegame.rs +++ b/crates/sylpheed-formats/src/savegame.rs @@ -223,11 +223,36 @@ impl Header { /// payload field was the wrong one. pub fn summary(&self) -> Vec { vec![ - Mirror { header_offset: 0x14, value: self.be32(0x14), ghad_offset: Some(52), name: "Stage" }, - Mirror { header_offset: 0x1c, value: self.be32(0x1c), ghad_offset: Some(24), name: "Points" }, - Mirror { header_offset: 0x20, value: self.be32(0x20), ghad_offset: Some(4), name: "FlightTime" }, - Mirror { header_offset: 0x24, value: self.be32(0x24), ghad_offset: Some(8), name: "ClearRatio" }, - Mirror { header_offset: 0x28, value: self.be32(0x28), ghad_offset: Some(12), name: "TimesCleared" }, + Mirror { + header_offset: 0x14, + value: self.be32(0x14), + ghad_offset: Some(52), + name: "Stage", + }, + Mirror { + header_offset: 0x1c, + value: self.be32(0x1c), + ghad_offset: Some(24), + name: "Points", + }, + Mirror { + header_offset: 0x20, + value: self.be32(0x20), + ghad_offset: Some(4), + name: "FlightTime", + }, + Mirror { + header_offset: 0x24, + value: self.be32(0x24), + ghad_offset: Some(8), + name: "ClearRatio", + }, + Mirror { + header_offset: 0x28, + value: self.be32(0x28), + ghad_offset: Some(12), + name: "TimesCleared", + }, ] } @@ -235,9 +260,27 @@ impl Header { /// a parse and a hand-written save the title will load. pub fn derived(&self) -> Vec { vec![ - Mirror { header_offset: 0x30, value: self.be32(0x30), ghad_offset: None, name: "deflate length + 10" }, - Mirror { header_offset: 0x8c, value: u32::from(u16::from_be_bytes([self.bytes.get(0x8c).copied().unwrap_or(0), self.bytes.get(0x8d).copied().unwrap_or(0)])), ghad_offset: None, name: "payload length" }, - Mirror { header_offset: 0x8e, value: self.be32(0x8e), ghad_offset: None, name: "adler32(payload)" }, + Mirror { + header_offset: 0x30, + value: self.be32(0x30), + ghad_offset: None, + name: "deflate length + 10", + }, + Mirror { + header_offset: 0x8c, + value: u32::from(u16::from_be_bytes([ + self.bytes.get(0x8c).copied().unwrap_or(0), + self.bytes.get(0x8d).copied().unwrap_or(0), + ])), + ghad_offset: None, + name: "payload length", + }, + Mirror { + header_offset: 0x8e, + value: self.be32(0x8e), + ghad_offset: None, + name: "adler32(payload)", + }, ] } } @@ -267,7 +310,10 @@ pub enum SaveError { NotGdaa, Truncated(&'static str), /// A chunk tag was not where the serializer puts it. - BadTag { expected: &'static str, at: usize }, + BadTag { + expected: &'static str, + at: usize, + }, /// Bytes left over after the trailer — the layout is supposed to close /// exactly, so this means the parse is wrong, not that the file has extras. TrailingBytes(usize), @@ -330,7 +376,10 @@ pub fn parse_payload(payload: &[u8]) -> Result { o += nlen; if payload.get(o..o + 4) != Some(b"GHAD") { - return Err(SaveError::BadTag { expected: "GHAD", at: o }); + return Err(SaveError::BadTag { + expected: "GHAD", + at: o, + }); } o += 4; let ghad = payload @@ -344,7 +393,10 @@ pub fn parse_payload(payload: &[u8]) -> Result { let mut records = Vec::with_capacity(count); for _ in 0..count { if payload.get(o..o + 4) != Some(b"SHAB") { - return Err(SaveError::BadTag { expected: "SHAB", at: o }); + return Err(SaveError::BadTag { + expected: "SHAB", + at: o, + }); } o += 4; let mut v = [0u32; RECORD_FIELDS]; diff --git a/crates/sylpheed-formats/src/ship.rs b/crates/sylpheed-formats/src/ship.rs index fe1cfc60..da0cbbe1 100644 --- a/crates/sylpheed-formats/src/ship.rs +++ b/crates/sylpheed-formats/src/ship.rs @@ -127,7 +127,8 @@ pub fn is_base_part(resource: &str) -> bool { // `e108_eng_01_b01`). `bdy` etc. are safe — only an exact `b`-then-digits token // counts. if resource.split('_').skip(1).any(|t| { - t == "b" || (t.starts_with('b') && t.len() >= 2 && t[1..].bytes().all(|c| c.is_ascii_digit())) + t == "b" + || (t.starts_with('b') && t.len() >= 2 && t[1..].bytes().all(|c| c.is_ascii_digit())) }) { return false; } @@ -161,7 +162,9 @@ pub fn ships_in_container(bytes: &[u8]) -> Vec { entry.parts.push(n.clone()); } ids.sort(); - ids.into_iter().map(|id| ships.remove(&id).unwrap()).collect() + ids.into_iter() + .map(|id| ships.remove(&id).unwrap()) + .collect() } /// Map a composite scene-graph node name to the geometry resource it draws. @@ -274,8 +277,16 @@ pub fn assemble_ship(bytes: &[u8], id: &str, include_external: bool) -> Vec Vec Vec Vec = frames .iter() .filter(|f| { - f.resource.contains(gncat) - && (idx.is_empty() || trailing_index(&f.resource) == idx) + f.resource.contains(gncat) && (idx.is_empty() || trailing_index(&f.resource) == idx) }) .collect(); - cands.sort_by_key(|f| trailing_index(&f.resource).parse::().unwrap_or(u32::MAX)); + cands.sort_by_key(|f| { + trailing_index(&f.resource) + .parse::() + .unwrap_or(u32::MAX) + }); if let Some(frame) = cands.first().copied() { - placed.push(ScenePart { resource: part.clone(), m: frame.m, t: frame.t, s: frame.s }); + placed.push(ScenePart { + resource: part.clone(), + m: frame.m, + t: frame.t, + s: frame.s, + }); placed_res.insert(part.clone()); } } @@ -380,8 +409,16 @@ pub fn assemble_ship(bytes: &[u8], id: &str, include_external: bool) -> Vec = Vec::new(); for i in 0..placed.len() { let a = &placed[i]; - let Some(stem) = a.resource.strip_suffix("_01") else { continue }; + let Some(stem) = a.resource.strip_suffix("_01") else { + continue; + }; let twin = format!("{stem}_02"); - let Some(j) = placed.iter().position(|p| p.resource == twin) else { continue }; + let Some(j) = placed.iter().position(|p| p.resource == twin) else { + continue; + }; let b = &placed[j]; if (a.t[0] + b.t[0]).abs() < 0.5 && a.t[0].abs() > 1.0 { pairs.push((i, j)); @@ -445,12 +486,18 @@ fn apply_twin_mirrors(bytes: &[u8], placed: &mut [ScenePart]) { continue; }; let first = |m: &Xbg7Model| -> Vec<[f32; 3]> { - m.meshes.iter().flat_map(|s| s.positions.iter().copied()).take(16).collect() + m.meshes + .iter() + .flat_map(|s| s.positions.iter().copied()) + .take(16) + .collect() }; let (fa, fb) = (first(ma), first(mb)); if fa.len() != fb.len() || !fa.iter().zip(&fb).all(|(p, q)| { - (p[0] - q[0]).abs() < 1e-3 && (p[1] - q[1]).abs() < 1e-3 && (p[2] - q[2]).abs() < 1e-3 + (p[0] - q[0]).abs() < 1e-3 + && (p[1] - q[1]).abs() < 1e-3 + && (p[2] - q[2]).abs() < 1e-3 }) { continue; // distinct (already-mirrored) geometries — nothing to do @@ -461,7 +508,11 @@ fn apply_twin_mirrors(bytes: &[u8], placed: &mut [ScenePart]) { .flat_map(|s| s.positions.iter()) .map(|p| p[0]) .sum::() - / ma.meshes.iter().map(|s| s.positions.len()).sum::().max(1) as f32; + / ma.meshes + .iter() + .map(|s| s.positions.len()) + .sum::() + .max(1) as f32; if mean_x.abs() < 1e-3 { continue; // symmetric geometry — mirroring is a no-op } @@ -469,7 +520,11 @@ fn apply_twin_mirrors(bytes: &[u8], placed: &mut [ScenePart]) { // dominant side (drawn plain it would fold onto the centreline; the // capture shows the engine reflects exactly this one — for e106 the // −264 port instance draws the file data plain, the +264 one mirrored). - let k = if (placed[i].t[0] > 0.0) == (mean_x > 0.0) { j } else { i }; + let k = if (placed[i].t[0] > 0.0) == (mean_x > 0.0) { + j + } else { + i + }; for row in &mut placed[k].m { row[0] = -row[0]; } @@ -536,10 +591,14 @@ mod tests { }; let bytes = { use crate::xiso::open_iso; - let rt = tokio::runtime::Builder::new_current_thread().build().unwrap(); + let rt = tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(); rt.block_on(async { let mut r = open_iso(std::path::Path::new(&iso)).await.unwrap(); - r.read_file("hidden/resource3d/Stage_S02.xpr").await.unwrap() + r.read_file("hidden/resource3d/Stage_S02.xpr") + .await + .unwrap() }) }; let ships = ships_in_container(&bytes); @@ -563,10 +622,14 @@ mod tests { }; let bytes = { use crate::xiso::open_iso; - let rt = tokio::runtime::Builder::new_current_thread().build().unwrap(); + let rt = tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(); rt.block_on(async { let mut r = open_iso(std::path::Path::new(&iso)).await.unwrap(); - r.read_file("hidden/resource3d/Stage_S01.xpr").await.unwrap() + r.read_file("hidden/resource3d/Stage_S01.xpr") + .await + .unwrap() }) }; let placed = assemble_ship(&bytes, "e106", true); @@ -672,7 +735,10 @@ mod tests { + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]) }; let one = |res: &str| placed.iter().find(|p| p.resource == res).unwrap(); - assert!(det(&one("e106_bdy_02").m) > 0.0, "starboard hull plain (mirror is in the data)"); + assert!( + det(&one("e106_bdy_02").m) > 0.0, + "starboard hull plain (mirror is in the data)" + ); assert!(det(&one("e106_bdy_01").m) > 0.0, "port hull plain"); } @@ -687,14 +753,21 @@ mod tests { }; let bytes = { use crate::xiso::open_iso; - let rt = tokio::runtime::Builder::new_current_thread().build().unwrap(); + let rt = tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(); rt.block_on(async { let mut r = open_iso(std::path::Path::new(&iso)).await.unwrap(); - r.read_file("hidden/resource3d/Stage_S02.xpr").await.unwrap() + r.read_file("hidden/resource3d/Stage_S02.xpr") + .await + .unwrap() }) }; let placed = assemble_ship(&bytes, "e106", true); - assert!(placed.len() >= 5, "e106 should place its hull + external parts"); + assert!( + placed.len() >= 5, + "e106 should place its hull + external parts" + ); // The forward hull body and the bridge must land at distinct fore/aft Z. let z = |res: &str| placed.iter().find(|p| p.resource == res).map(|p| p.t[2]); let bdy = z("e106_bdy_01").expect("bdy_01 placed"); diff --git a/crates/sylpheed-formats/src/ship_capture.rs b/crates/sylpheed-formats/src/ship_capture.rs index 5dac0ec8..69b8d228 100644 --- a/crates/sylpheed-formats/src/ship_capture.rs +++ b/crates/sylpheed-formats/src/ship_capture.rs @@ -145,7 +145,14 @@ pub fn parse_capture(text: &str) -> Vec { return; // no WorldView for this draw — skip it }; if let Some((r, t)) = normalize_wvp([c0, c1, c2]) { - out.push(CapturedDraw { vbase, vcount, r, t, pos, ib }); + out.push(CapturedDraw { + vbase, + vcount, + r, + t, + pos, + ib, + }); } }; @@ -155,7 +162,9 @@ pub fn parse_capture(text: &str) -> Vec { flush(vbase, vcount, &mut pos, &mut ib, &consts, &mut out); consts.clear(); let f = |k: &str| rest.split_whitespace().find_map(|t| t.strip_prefix(k)); - vbase = f("vbase=0x").and_then(|s| u32::from_str_radix(s, 16).ok()).unwrap_or(0); + vbase = f("vbase=0x") + .and_then(|s| u32::from_str_radix(s, 16).ok()) + .unwrap_or(0); vcount = f("vcount=").and_then(|s| s.parse().ok()).unwrap_or(0); } else if let Some(rest) = l.strip_prefix("ib base=0x") { // `ib base=0x… count=N fmt=u16 endian=E len=L delta_vb=D min=a max=b idx: …` @@ -190,8 +199,12 @@ pub fn parse_capture(text: &str) -> Vec { pos = parse_pos_line(l, 8); } else if l.starts_with("vsconst") { for cap in l.split('c').skip(1) { - let Some((idx, rest)) = cap.split_once('=') else { continue }; - let Ok(i) = idx.trim().parse::() else { continue }; + let Some((idx, rest)) = cap.split_once('=') else { + continue; + }; + let Ok(i) = idx.trim().parse::() else { + continue; + }; let nums: Vec = rest .trim_start_matches('(') .split(')') @@ -214,8 +227,13 @@ pub fn parse_capture(text: &str) -> Vec { fn parse_pos_line(l: &str, max: usize) -> Vec<[f32; 3]> { let mut out = Vec::new(); for group in l.split('(').skip(1) { - let Some(inner) = group.split(')').next() else { continue }; - let nums: Vec = inner.split(',').filter_map(|x| x.trim().parse().ok()).collect(); + let Some(inner) = group.split(')').next() else { + continue; + }; + let nums: Vec = inner + .split(',') + .filter_map(|x| x.trim().parse().ok()) + .collect(); if nums.len() == 3 { out.push([nums[0], nums[1], nums[2]]); if out.len() >= max { @@ -299,11 +317,20 @@ pub fn parse_drawlog(text: &str) -> Vec { return; } let get = |i: usize| consts.iter().find(|(k, _)| *k == i).map(|(_, v)| *v); - let (Some(c0), Some(c1), Some(c2)) = (get(0), get(1), get(2)) else { return }; + let (Some(c0), Some(c1), Some(c2)) = (get(0), get(1), get(2)) else { + return; + }; if let Some((r, t)) = normalize_wvp([c0, c1, c2]) { // The draw-logger format carries an index base too, but it de-dups // by vertex declaration, so it never lines up per part — left None. - out.push(CapturedDraw { vbase: base, vcount: size / stride, r, t, pos, ib: None }); + out.push(CapturedDraw { + vbase: base, + vcount: size / stride, + r, + t, + pos, + ib: None, + }); } }; @@ -323,14 +350,25 @@ pub fn parse_drawlog(text: &str) -> Vec { if let Some(b) = f("base=0x").and_then(|s| u32::from_str_radix(s, 16).ok()) { base = b; } - stride = f("stride_words=").and_then(|s| s.parse().ok()).unwrap_or(stride); - size = f("size_words=").and_then(|s| s.parse().ok()).unwrap_or(size); + stride = f("stride_words=") + .and_then(|s| s.parse().ok()) + .unwrap_or(stride); + size = f("size_words=") + .and_then(|s| s.parse().ok()) + .unwrap_or(size); } else if is_ship && l.starts_with('c') { // `c x y z w` — the vsconst rows (space-separated). let mut it = l.splitn(2, char::is_whitespace); let Some(tag) = it.next() else { continue }; - let Ok(i) = tag[1..].parse::() else { continue }; - let nums: Vec = it.next().unwrap_or("").split_whitespace().filter_map(|x| x.parse().ok()).collect(); + let Ok(i) = tag[1..].parse::() else { + continue; + }; + let nums: Vec = it + .next() + .unwrap_or("") + .split_whitespace() + .filter_map(|x| x.parse().ok()) + .collect(); if nums.len() >= 4 { consts.push((i, [nums[0], nums[1], nums[2], nums[3]])); } @@ -422,7 +460,10 @@ pub fn correlate( // among validated candidates the best hit count wins (routes twins), and // a mirror-validated match records the X-reflection. let mut best: Option<(&CapturedDraw, usize, bool)> = None; - for d in draws.iter().filter(|d| d.vcount == key.vcount && !used.contains(&d.vbase)) { + for d in draws + .iter() + .filter(|d| d.vcount == key.vcount && !used.contains(&d.vbase)) + { let Some((score, mirrored)) = pos_validate(d, &key.ref_pos) else { continue; // positions disagree — not this part }; @@ -435,7 +476,10 @@ pub fn correlate( matched.push((key.part.clone(), d.r, d.t, mirrored)); } } - let ref_idx = matched.iter().position(|(p, ..)| p.contains(ref_sub)).unwrap_or(0); + let ref_idx = matched + .iter() + .position(|(p, ..)| p.contains(ref_sub)) + .unwrap_or(0); let (ref_part, ref_r, ref_t, _) = matched.get(ref_idx)?.clone(); let rt_ref = transpose(&ref_r); @@ -460,7 +504,11 @@ pub fn correlate( } }) .collect(); - Some(ShipPlacement { id: id.to_string(), reference: ref_part, parts: parts_out }) + Some(ShipPlacement { + id: id.to_string(), + reference: ref_part, + parts: parts_out, + }) } /// Snap near-axis rotation entries (float noise from the WV products) to exact @@ -488,7 +536,12 @@ fn snap_m3(m: &M3) -> [[f32; 3]; 3] { pub fn to_scene_parts(ship: &ShipPlacement) -> Vec { ship.parts .iter() - .map(|p| ScenePart { resource: p.part.clone(), m: p.m, t: p.t, s: [1.0, 1.0, 1.0] }) + .map(|p| ScenePart { + resource: p.part.clone(), + m: p.m, + t: p.t, + s: [1.0, 1.0, 1.0], + }) .collect() } @@ -514,10 +567,18 @@ pub fn serialize_table(ships: &[ShipPlacement]) -> String { s.push_str(&format!( " {} {} {} {} {} {} {} {} {} {} {} {} {}\n", p.part, - p.m[0][0], p.m[0][1], p.m[0][2], - p.m[1][0], p.m[1][1], p.m[1][2], - p.m[2][0], p.m[2][1], p.m[2][2], - p.t[0], p.t[1], p.t[2], + p.m[0][0], + p.m[0][1], + p.m[0][2], + p.m[1][0], + p.m[1][1], + p.m[1][2], + p.m[2][0], + p.m[2][1], + p.m[2][2], + p.t[0], + p.t[1], + p.t[2], )); } } @@ -542,7 +603,11 @@ pub fn parse_table(text: &str) -> Vec { .and_then(|s| s.strip_prefix("ref=")) .unwrap_or("") .to_string(); - ships.push(ShipPlacement { id, reference, parts: Vec::new() }); + ships.push(ShipPlacement { + id, + reference, + parts: Vec::new(), + }); } else if let Some(ship) = ships.last_mut() { let mut it = l.split_whitespace(); let part = it.next().unwrap_or("").to_string(); @@ -603,7 +668,11 @@ mod tests { } fn key(part: &str, vcount: u32) -> PartKey { - PartKey { part: part.to_string(), vcount, ref_pos: Vec::new() } + PartKey { + part: part.to_string(), + vcount, + ref_pos: Vec::new(), + } } #[test] @@ -613,7 +682,10 @@ mod tests { assert_eq!(draws.len(), 1); assert_eq!(draws[0].vcount, 3); assert_eq!(draws[0].t, [10.0, 0.0, 0.0]); - assert_eq!(draws[0].r, [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]); + assert_eq!( + draws[0].r, + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] + ); } #[test] @@ -666,7 +738,7 @@ mod tests { assert_eq!(draws.len(), 2); assert_eq!(draws[0].vcount, 1633); // 9798/6 assert_eq!(draws[1].vcount, 815); // 4890/6 - // Correlate: part B sits 50 along Z from reference part A. + // Correlate: part B sits 50 along Z from reference part A. let parts = vec![key("e106_bdy_04", 1633), key("e106_bdy_03", 815)]; let ship = correlate("e106", &draws, &parts, "bdy_04").unwrap(); let b = ship.parts.iter().find(|p| p.part == "e106_bdy_03").unwrap(); @@ -692,10 +764,21 @@ mod tests { assert_eq!(draws.len(), 3); assert_eq!(draws[0].pos.len(), 2); // Both twins carry the SAME (file) positions — +X side geometry. - let file_pos = vec![[134.4215, 133.8319, -118.1757], [178.8384, 85.3847, 238.0463]]; + let file_pos = vec![ + [134.4215, 133.8319, -118.1757], + [178.8384, 85.3847, 238.0463], + ]; let parts = vec![ - PartKey { part: "e106_bdy_01".to_string(), vcount: 426, ref_pos: file_pos.clone() }, - PartKey { part: "e106_bdy_02".to_string(), vcount: 426, ref_pos: file_pos }, + PartKey { + part: "e106_bdy_01".to_string(), + vcount: 426, + ref_pos: file_pos.clone(), + }, + PartKey { + part: "e106_bdy_02".to_string(), + vcount: 426, + ref_pos: file_pos, + }, key("e106_bdy_04", 558), ]; let ship = correlate("e106", &draws, &parts, "bdy_04").unwrap(); diff --git a/crates/sylpheed-formats/src/slb.rs b/crates/sylpheed-formats/src/slb.rs index 01355daa..2c4d7364 100644 --- a/crates/sylpheed-formats/src/slb.rs +++ b/crates/sylpheed-formats/src/slb.rs @@ -442,7 +442,9 @@ pub fn to_xma_riffs(slb: &[u8]) -> Vec> { // UPPER bound, not an exact one: 5 296 of 7 586 banks declare more than // the entry holds and none declares exactly what it holds, so the clamp // below is load-bearing (docs/re/structures/slb-data-offset.md). - let Some(fi) = find(slb, b"fmt ", ri) else { break }; + let Some(fi) = find(slb, b"fmt ", ri) else { + break; + }; let Some(fsz) = le32(slb, fi + 4) else { break }; let Some(fmt_end) = fi.checked_add(8).and_then(|v| v.checked_add(fsz as usize)) else { break; @@ -450,7 +452,9 @@ pub fn to_xma_riffs(slb: &[u8]) -> Vec> { if fmt_end > slb.len() { break; } - let Some(di) = find(slb, b"data", fi) else { break }; + let Some(di) = find(slb, b"data", fi) else { + break; + }; let Some(dsz) = le32(slb, di + 4) else { break }; let Some(ds) = di.checked_add(8) else { break }; let de = ds @@ -570,7 +574,7 @@ fn synth_xma1_fmt(channels: u8, channel_mask: u16, rate: u32) -> Vec { fmt.extend_from_slice(&1u16.to_le_bytes()); // NumStreams fmt.push(0); // LoopCount fmt.push(3); // Version - // XMASTREAMFORMAT[0] + // XMASTREAMFORMAT[0] fmt.extend_from_slice(&(rate * channels as u32 * 2).to_le_bytes()); // PsuedoBytesPerSec fmt.extend_from_slice(&rate.to_le_bytes()); // SampleRate fmt.extend_from_slice(&0u32.to_le_bytes()); // LoopStart diff --git a/crates/sylpheed-formats/src/t8ad.rs b/crates/sylpheed-formats/src/t8ad.rs index 9a595fd0..7cbde2d7 100644 --- a/crates/sylpheed-formats/src/t8ad.rs +++ b/crates/sylpheed-formats/src/t8ad.rs @@ -141,7 +141,7 @@ mod tests { b[0x18..0x1c].copy_from_slice(&h.to_be_bytes()); b[0x1c..0x20].copy_from_slice(&1u32.to_be_bytes()); // 1 tile b.extend_from_slice(&0x30u32.to_be_bytes()); // offset table: tile 0 @ 0x30 - // rectangle header: dst (0,0), size w×h + // rectangle header: dst (0,0), size w×h b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&w.to_be_bytes()); @@ -177,9 +177,13 @@ mod tests { let off1 = off0 + 16 + 256 * 4; // tile-0 header + its 256 pixels b.extend_from_slice(&(off0 as u32).to_be_bytes()); b.extend_from_slice(&(off1 as u32).to_be_bytes()); - for v in [0u32, 0, 256, 1] { b.extend_from_slice(&v.to_be_bytes()) } // dst(0,0) 256×1 + for v in [0u32, 0, 256, 1] { + b.extend_from_slice(&v.to_be_bytes()) + } // dst(0,0) 256×1 b.extend_from_slice(&[0xFF, 0xFF, 0, 0].repeat(256)); // A,R,G,B red - for v in [256u32, 0, 44, 1] { b.extend_from_slice(&v.to_be_bytes()) } // dst(256,0) 44×1 + for v in [256u32, 0, 44, 1] { + b.extend_from_slice(&v.to_be_bytes()) + } // dst(256,0) 44×1 b.extend_from_slice(&[0xFF, 0, 0, 0xFF].repeat(44)); // A,R,G,B blue let img = parse(&b).expect("decodes"); assert_eq!((img.width, img.height), (300, 1)); diff --git a/crates/sylpheed-formats/src/texture.rs b/crates/sylpheed-formats/src/texture.rs index b3dcfdf3..530c997b 100644 --- a/crates/sylpheed-formats/src/texture.rs +++ b/crates/sylpheed-formats/src/texture.rs @@ -39,7 +39,7 @@ //! - Xenia: `src/xenia/gpu/xenos.h` (GPUTEXTUREFORMAT enum, GPUFC bitfields) //! - Xenia: `src/xenia/gpu/texture_util.cc` (de-tiling algorithm) -use binrw::{BinRead, binread}; +use binrw::{binread, BinRead}; use thiserror::Error; // ── Error type ─────────────────────────────────────────────────────────────── @@ -99,34 +99,41 @@ pub enum X360TextureFormat { impl X360TextureFormat { pub fn from_u8(v: u8) -> Option { match v { - 6 => Some(Self::A8R8G8B8), - 7 => Some(Self::X8R8G8B8), + 6 => Some(Self::A8R8G8B8), + 7 => Some(Self::X8R8G8B8), 18 => Some(Self::Dxt1), 19 => Some(Self::Dxt3), 20 => Some(Self::Dxt5), 49 => Some(Self::Dxn), 59 => Some(Self::Dxt5A), - _ => None, + _ => None, } } /// Bytes per compressed block (4×4 texel group) or per pixel for uncompressed. pub fn bytes_per_block(&self) -> usize { match self { - Self::Dxt1 | Self::Dxt5A => 8, - Self::Dxt3 | Self::Dxt5 | Self::Dxn => 16, - Self::A8R8G8B8 | Self::X8R8G8B8 => 4, + Self::Dxt1 | Self::Dxt5A => 8, + Self::Dxt3 | Self::Dxt5 | Self::Dxn => 16, + Self::A8R8G8B8 | Self::X8R8G8B8 => 4, } } /// Is this a BCn block-compressed format? pub fn is_block_compressed(&self) -> bool { - matches!(self, Self::Dxt1 | Self::Dxt3 | Self::Dxt5 | Self::Dxn | Self::Dxt5A) + matches!( + self, + Self::Dxt1 | Self::Dxt3 | Self::Dxt5 | Self::Dxn | Self::Dxt5A + ) } /// Texels per block side (4 for BCn, 1 for uncompressed). pub fn block_size(&self) -> usize { - if self.is_block_compressed() { 4 } else { 1 } + if self.is_block_compressed() { + 4 + } else { + 1 + } } /// Canary's canonical `k_…` GPUTEXTUREFORMAT name (e.g. `k_DXT1`). @@ -174,7 +181,14 @@ const fn d( bpp: u16, compressed: bool, ) -> GpuFormatDesc { - GpuFormatDesc { code, name, block_w, block_h, bpp, compressed } + GpuFormatDesc { + code, + name, + block_w, + block_h, + bpp, + compressed, + } } /// The complete GPUTEXTUREFORMAT table (codes 0..=63), verbatim from @@ -364,7 +378,10 @@ pub struct Cubemap { impl Cubemap { /// The D3D9 cube-face name for slice `i` (0..6). pub fn face_label(i: usize) -> &'static str { - ["+X", "-X", "+Y", "-Y", "+Z", "-Z"].get(i).copied().unwrap_or("?") + ["+X", "-X", "+Y", "-Y", "+Z", "-Z"] + .get(i) + .copied() + .unwrap_or("?") } } @@ -432,7 +449,8 @@ impl X360Texture { // Select a texture resource. TX2D = 2D texture; TXCM = cubemap // (skybox / backdrop) — same 52-byte descriptor + GPUFC layout, but the // pixel section holds 6 faces. For a preview we decode face 0. - let tex_entry = entries.iter() + let tex_entry = entries + .iter() .filter(|e| e.is_texture() || e.is_cubemap()) .nth(want) .ok_or(TextureError::NoTextureFound)?; @@ -456,10 +474,10 @@ impl X360Texture { u32::from_be_bytes(bytes[offset..offset + 4].try_into().unwrap()) }; - let gpufc0 = be_u32(gpufc_base); // +0x18 - let gpufc1 = be_u32(gpufc_base + 0x04); // +0x1C - let gpufc2 = be_u32(gpufc_base + 0x08); // +0x20 - let gpufc4 = be_u32(gpufc_base + 0x10); // +0x28 + let gpufc0 = be_u32(gpufc_base); // +0x18 + let gpufc1 = be_u32(gpufc_base + 0x04); // +0x1C + let gpufc2 = be_u32(gpufc_base + 0x08); // +0x20 + let gpufc4 = be_u32(gpufc_base + 0x10); // +0x28 // GPUFC[1] bits[5:0] = GPUTEXTUREFORMAT let fmt_code = (gpufc1 & 0x3F) as u8; @@ -475,7 +493,7 @@ impl X360Texture { let base_address = (gpufc1 & 0xFFFFF000) as usize; // GPUFC[2] / size_2d: width-1 in bits[12:0], height-1 in bits[25:13] - let width = (gpufc2 & 0x1FFF) + 1; + let width = (gpufc2 & 0x1FFF) + 1; let height = ((gpufc2 >> 13) & 0x1FFF) + 1; // GPUFC[4]: mip_max in bits[9:6]; mip_count = mip_max + 1 @@ -494,9 +512,23 @@ impl X360Texture { } // De-tile + endian-correct the single (face-0) surface. - let data = decode_surface(&bytes[data_start..], width, height, format, is_tiled, endianness)?; + let data = decode_surface( + &bytes[data_start..], + width, + height, + format, + is_tiled, + endianness, + )?; - Ok(X360Texture { width, height, format, mip_levels: mip_count, is_cubemap, data }) + Ok(X360Texture { + width, + height, + format, + mip_levels: mip_count, + is_cubemap, + data, + }) } /// Decode all 6 faces of a cubemap (`TXCM`) resource, or `Ok(None)` if the @@ -533,7 +565,10 @@ impl X360Texture { const DIR_BASE: usize = 0x10; let gpufc_base = tex_entry.data_offset as usize + DIR_BASE + 0x18; if bytes.len() < gpufc_base + 6 * 4 { - return Err(TextureError::BufferTooSmall { needed: gpufc_base + 24, have: bytes.len() }); + return Err(TextureError::BufferTooSmall { + needed: gpufc_base + 24, + have: bytes.len(), + }); } let be_u32 = |o: usize| u32::from_be_bytes(bytes[o..o + 4].try_into().unwrap()); let gpufc0 = be_u32(gpufc_base); @@ -555,12 +590,20 @@ impl X360Texture { let mut faces = Vec::with_capacity(6); for f in 0..6 { let start = data_start + f * stride; - let raw = bytes - .get(start..) - .ok_or(TextureError::BufferTooSmall { needed: start + 1, have: bytes.len() })?; - faces.push(decode_surface(raw, width, height, format, is_tiled, endianness)?); + let raw = bytes.get(start..).ok_or(TextureError::BufferTooSmall { + needed: start + 1, + have: bytes.len(), + })?; + faces.push(decode_surface( + raw, width, height, format, is_tiled, endianness, + )?); } - Ok(Some(Cubemap { width, height, format, faces })) + Ok(Some(Cubemap { + width, + height, + format, + faces, + })) } /// Parse a texture from already-known parameters + raw tiled data. @@ -574,7 +617,14 @@ impl X360Texture { format: X360TextureFormat, ) -> Result { let linear_data = detile(tiled_data, width, height, format)?; - Ok(X360Texture { width, height, format, mip_levels: 1, is_cubemap: false, data: linear_data }) + Ok(X360Texture { + width, + height, + format, + mip_levels: 1, + is_cubemap: false, + data: linear_data, + }) } } @@ -630,7 +680,10 @@ fn decode_surface( let bh = height.div_ceil(block_size).max(1); let needed = bw as usize * bh as usize * format.bytes_per_block(); if raw_data.len() < needed { - return Err(TextureError::BufferTooSmall { needed, have: raw_data.len() }); + return Err(TextureError::BufferTooSmall { + needed, + have: raw_data.len(), + }); } raw_data[..needed].to_vec() }; @@ -750,7 +803,10 @@ pub fn detile( // The tiled surface occupies pitch_aligned × height_aligned blocks. let src_needed = pitch_aligned as usize * height_aligned as usize * bpb; if src.len() < src_needed { - return Err(TextureError::BufferTooSmall { needed: src_needed, have: src.len() }); + return Err(TextureError::BufferTooSmall { + needed: src_needed, + have: src.len(), + }); } let dst_len = blocks_wide as usize * blocks_tall as usize * bpb; @@ -758,7 +814,8 @@ pub fn detile( for by in 0..blocks_tall { for bx in 0..blocks_wide { - let src_offset = tiled_2d_offset(bx as i32, by as i32, pitch_aligned, bpb_log2) as usize; + let src_offset = + tiled_2d_offset(bx as i32, by as i32, pitch_aligned, bpb_log2) as usize; let dst_offset = (by * blocks_wide + bx) as usize * bpb; if src_offset + bpb <= src.len() && dst_offset + bpb <= dst.len() { dst[dst_offset..dst_offset + bpb] @@ -784,11 +841,11 @@ pub fn morton_decode(index: u32) -> (u32, u32) { /// Used by `morton_decode` to de-interleave X and Y. #[inline] fn compact_bits(mut x: u32) -> u32 { - x &= 0x5555_5555; // keep even-position bits - x = (x ^ (x >> 1)) & 0x3333_3333; - x = (x ^ (x >> 2)) & 0x0f0f_0f0f; - x = (x ^ (x >> 4)) & 0x00ff_00ff; - x = (x ^ (x >> 8)) & 0x0000_ffff; + x &= 0x5555_5555; // keep even-position bits + x = (x ^ (x >> 1)) & 0x3333_3333; + x = (x ^ (x >> 2)) & 0x0f0f_0f0f; + x = (x ^ (x >> 4)) & 0x00ff_00ff; + x = (x ^ (x >> 8)) & 0x0000_ffff; x } @@ -813,7 +870,11 @@ mod tests { // descriptor consistent with its own is_block_compressed(). for code in [6u8, 7, 18, 19, 20, 49, 59] { let fmt = X360TextureFormat::from_u8(code).unwrap(); - assert_eq!(fmt.desc().compressed, fmt.is_block_compressed(), "code {code}"); + assert_eq!( + fmt.desc().compressed, + fmt.is_block_compressed(), + "code {code}" + ); } } @@ -882,7 +943,7 @@ mod tests { #[test] fn x360_format_bytes_per_block() { - assert_eq!(X360TextureFormat::Dxt1.bytes_per_block(), 8); + assert_eq!(X360TextureFormat::Dxt1.bytes_per_block(), 8); assert_eq!(X360TextureFormat::Dxt5A.bytes_per_block(), 8); assert_eq!(X360TextureFormat::Dxt5.bytes_per_block(), 16); assert_eq!(X360TextureFormat::A8R8G8B8.bytes_per_block(), 4); @@ -891,8 +952,14 @@ mod tests { #[test] fn format_from_u8_roundtrip() { for code in [6u8, 7, 18, 19, 20, 49, 59] { - assert!(X360TextureFormat::from_u8(code).is_some(), "missing format {code}"); + assert!( + X360TextureFormat::from_u8(code).is_some(), + "missing format {code}" + ); } - assert!(X360TextureFormat::from_u8(0x52).is_none(), "old D3DFORMAT 0x52 must not match"); + assert!( + X360TextureFormat::from_u8(0x52).is_none(), + "old D3DFORMAT 0x52 must not match" + ); } } diff --git a/crates/sylpheed-formats/src/ui_layout.rs b/crates/sylpheed-formats/src/ui_layout.rs index 2a2ac43b..21ba19af 100644 --- a/crates/sylpheed-formats/src/ui_layout.rs +++ b/crates/sylpheed-formats/src/ui_layout.rs @@ -221,8 +221,7 @@ impl Element { } let mut best = (0usize, 0u32); for k in 0..n - 1 { - let (Some(t0), Some(t1)) = - (self.keyframes[k].time, self.keyframes[k + 1].time) + let (Some(t0), Some(t1)) = (self.keyframes[k].time, self.keyframes[k + 1].time) else { continue; // the last frame carries no time }; @@ -855,26 +854,46 @@ fn measured_paint_order(build: &UiBuild) -> Option> { let names: Vec<&str> = build.elements.iter().map(|e| e.name.as_str()).collect(); // GP_TITLE.pak entry 4 (a60fcb85) — the title screen the game actually runs. 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", + "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", ]; // GP_TITLE.pak entries 11/14 — the GAME ARTS / SETA / studio anima splash. 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_eff0.prm", + "palogo_gamearts.t32", + "palogo_gamearts_eff.t32", + "palogo_seta.t32", + "palogo_seta_eff.t32", + "palogo_anima.t32", "palogo_anima_eff.t32", ]; if names == TITLE { // background, the rotating pair, the other full-screen layers, the // back2 glow group, the wordmarks, the copyright, the fade. return Some(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, + 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 { @@ -886,10 +905,22 @@ fn measured_paint_order(build: &UiBuild) -> Option> { // the third measured permutation and the first that contains TWO // primitives, which is why it matters — see `ui-prm-primitives.md`. 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", + "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 == MENU { // background, the two loops, the full-screen effect, the DIM quad, @@ -988,7 +1019,10 @@ pub fn compose( // unresolved, `screen render` still said "sprites that did not // resolve: none". A diagnostic with a hole in it is worse than none. // See docs/re/structures/ratc-child-names.md. - missing.push(format!("{} (element declares no resolvable sprite)", el.name)); + missing.push(format!( + "{} (element declares no resolvable sprite)", + el.name + )); continue; }; let Some(&(off, size)) = build.sprites.get(sprite) else { @@ -1184,7 +1218,10 @@ mod tests { /// A synthetic build bundle: RATC magic, entry count at 0x14, a declaration /// table at 0x20, then a placement region. - fn synth_build(decls: &[(&str, u32, u32, u32, u32)], groups: &[(usize, Vec)]) -> Vec { + fn synth_build( + decls: &[(&str, u32, u32, u32, u32)], + groups: &[(usize, Vec)], + ) -> Vec { let count = decls.len(); let mut b = vec![0u8; DECL_TABLE_AT + count * DECL_ENTRY]; b[0..4].copy_from_slice(b"RATC"); diff --git a/crates/sylpheed-formats/src/unit_layout.rs b/crates/sylpheed-formats/src/unit_layout.rs index 8f79ce53..0eb6c237 100644 --- a/crates/sylpheed-formats/src/unit_layout.rs +++ b/crates/sylpheed-formats/src/unit_layout.rs @@ -61,9 +61,7 @@ pub fn fields() -> Vec { }; // `name` is a &'static str because TABLE is 'static. let name = it.next()?; - let name: &'static str = TABLE.get( - TABLE.find(name).map(|s| s..s + name.len())?, - )?; + let name: &'static str = TABLE.get(TABLE.find(name).map(|s| s..s + name.len())?)?; Some(Field { offset, name, kind }) }) .collect() @@ -76,7 +74,10 @@ pub fn field_at(offset: usize) -> Option { /// The offset of `name`, if it is mapped. pub fn offset_of(name: &str) -> Option { - fields().into_iter().find(|f| f.name == name).map(|f| f.offset) + fields() + .into_iter() + .find(|f| f.name == name) + .map(|f| f.offset) } #[cfg(test)] @@ -87,7 +88,10 @@ mod tests { fn table_parses_and_is_ordered() { let f = fields(); assert!(f.len() > 150, "expected the full map, got {}", f.len()); - assert!(f.windows(2).all(|w| w[0].offset < w[1].offset), "offsets must be strictly increasing"); + assert!( + f.windows(2).all(|w| w[0].offset < w[1].offset), + "offsets must be strictly increasing" + ); } #[test] diff --git a/crates/sylpheed-formats/src/vfs.rs b/crates/sylpheed-formats/src/vfs.rs index cd91c9c6..323a615c 100644 --- a/crates/sylpheed-formats/src/vfs.rs +++ b/crates/sylpheed-formats/src/vfs.rs @@ -67,10 +67,11 @@ impl GameAssets { pub fn list(&self, subdir: &str) -> Result, VfsError> { let dir = self.resolve(subdir); let mut result = Vec::new(); - self.walk_dir(&dir, &dir, &mut result).map_err(|e| VfsError::Io { - path: subdir.to_string(), - source: e, - })?; + self.walk_dir(&dir, &dir, &mut result) + .map_err(|e| VfsError::Io { + path: subdir.to_string(), + source: e, + })?; Ok(result) } @@ -81,12 +82,7 @@ impl GameAssets { self.root.join(native) } - fn walk_dir( - &self, - dir: &Path, - root: &Path, - out: &mut Vec, - ) -> std::io::Result<()> { + fn walk_dir(&self, dir: &Path, root: &Path, out: &mut Vec) -> std::io::Result<()> { for entry in std::fs::read_dir(dir)? { let entry = entry?; let path = entry.path(); @@ -191,9 +187,18 @@ pub fn is_probably_text(bytes: &[u8]) -> bool { // roughly half the bytes (the odd positions) are NUL and the even bytes are // printable. if sample.len() >= 16 { - let nul_odd = sample.iter().skip(1).step_by(2).filter(|&&b| b == 0).count(); + let nul_odd = sample + .iter() + .skip(1) + .step_by(2) + .filter(|&&b| b == 0) + .count(); if nul_odd * 2 >= sample.len() / 2 { - let printable_even = sample.iter().step_by(2).filter(|&&b| is_text_byte(b)).count(); + let printable_even = sample + .iter() + .step_by(2) + .filter(|&&b| is_text_byte(b)) + .count(); if printable_even * 2 >= sample.len() / 2 - 2 { return true; } @@ -227,7 +232,12 @@ pub fn decode_text(bytes: &[u8]) -> (String, &'static str) { // BOM-less UTF-16LE detection (same signal as `is_probably_text`). let sample = &bytes[..bytes.len().min(4096)]; if sample.len() >= 16 { - let nul_odd = sample.iter().skip(1).step_by(2).filter(|&&b| b == 0).count(); + let nul_odd = sample + .iter() + .skip(1) + .step_by(2) + .filter(|&&b| b == 0) + .count(); if nul_odd * 2 >= sample.len() / 2 { return (decode_utf16(bytes, false), "UTF-16LE"); } diff --git a/crates/sylpheed-formats/src/xiso.rs b/crates/sylpheed-formats/src/xiso.rs index ea96b29f..ee5b1bcb 100644 --- a/crates/sylpheed-formats/src/xiso.rs +++ b/crates/sylpheed-formats/src/xiso.rs @@ -51,11 +51,13 @@ impl XisoReader { .await .context("Found XDVDFS partition but failed to parse volume descriptor")?; - info!( - "Opened XISO: root directory table at sector {}", - { volume.root_table.region.sector } - ); - Ok(Self { volume, file: wrapper }) + info!("Opened XISO: root directory table at sector {}", { + volume.root_table.region.sector + }); + Ok(Self { + volume, + file: wrapper, + }) } /// List all files in the disc image, recursively (directories excluded). @@ -126,13 +128,11 @@ impl XisoReader { let rel_path = format!("{}/{}", parent, name); let rel_path = rel_path.trim_start_matches('/'); - let disk_path = - output_dir.join(rel_path.replace('/', std::path::MAIN_SEPARATOR_STR)); + let disk_path = output_dir.join(rel_path.replace('/', std::path::MAIN_SEPARATOR_STR)); if let Some(parent_dir) = disk_path.parent() { - std::fs::create_dir_all(parent_dir).with_context(|| { - format!("Failed to create dir: {}", parent_dir.display()) - })?; + std::fs::create_dir_all(parent_dir) + .with_context(|| format!("Failed to create dir: {}", parent_dir.display()))?; } let data = entry @@ -182,8 +182,7 @@ mod tests { #[tokio::test] #[ignore = "requires a real ISO image — set SYLPHEED_ISO env var"] async fn test_list_iso() { - let iso_path = - std::env::var("SYLPHEED_ISO").expect("Set SYLPHEED_ISO to your ISO path"); + let iso_path = std::env::var("SYLPHEED_ISO").expect("Set SYLPHEED_ISO to your ISO path"); let mut reader = open_iso(Path::new(&iso_path)).await.unwrap(); let files = reader.list_all_files().await.unwrap(); for f in &files { diff --git a/crates/sylpheed-formats/tests/caption_families_disc.rs b/crates/sylpheed-formats/tests/caption_families_disc.rs index 1b779c12..f344c998 100644 --- a/crates/sylpheed-formats/tests/caption_families_disc.rs +++ b/crates/sylpheed-formats/tests/caption_families_disc.rs @@ -38,7 +38,8 @@ fn all_eight_caption_families_are_read() { let mut per: BTreeMap = BTreeMap::new(); for (id, lines) in &all { - *per.entry(id.split('_').next().unwrap().to_string()).or_default() += lines.len(); + *per.entry(id.split('_').next().unwrap().to_string()) + .or_default() += lines.len(); } let fams: Vec<&str> = per.keys().map(String::as_str).collect(); assert_eq!( @@ -54,7 +55,10 @@ fn all_eight_caption_families_are_read() { assert_eq!(all.len(), 4085, "recovered caption ids"); // `VOICE` is the only family with a letter before the id; its ids must keep it. - assert!(all.contains_key("VOICE_A_150"), "VOICE ids keep their family letter"); + assert!( + all.contains_key("VOICE_A_150"), + "VOICE ids keep their family letter" + ); } /// The control: generalising must not lose anything the DEMO-only reader had. @@ -81,5 +85,8 @@ fn demo_family_is_not_lost_by_generalising() { // …and 16x more text overall than the DEMO-only path saw. let total: usize = all.values().map(|v| v.len()).sum(); - assert!(total > old * 16, "expected a large gain, got {total} vs {old}"); + assert!( + total > old * 16, + "expected a large gain, got {total} vs {old}" + ); } diff --git a/crates/sylpheed-formats/tests/game_data_disc.rs b/crates/sylpheed-formats/tests/game_data_disc.rs index 71ad02ac..9a792176 100644 --- a/crates/sylpheed-formats/tests/game_data_disc.rs +++ b/crates/sylpheed-formats/tests/game_data_disc.rs @@ -39,7 +39,10 @@ fn craft_hardpoints_are_per_record() { .filter(|h| h.kind == HardpointKind::Turret) .collect(); assert_eq!(turrets.len(), 63, "Turret_NNN records"); - assert!(turrets.iter().all(|t| t.hp == Some(100.0)), "every mount has HP 100"); + assert!( + turrets.iter().all(|t| t.hp == Some(100.0)), + "every mount has HP 100" + ); // Each mount names its own weapon model, `rou_f001_wep_NN`. let mount = turrets.iter().find(|t| t.index == 33).expect("Turret_033"); assert_eq!(mount.model.as_deref(), Some("rou_f001_wep_33")); @@ -73,7 +76,10 @@ fn vessel_hardpoints_are_per_record() { .iter() .filter_map(|h| h.hp.map(|v| format!("{v}"))) .collect(); - assert!(distinct.len() >= 4, "distinct component HP values: {distinct:?}"); + assert!( + distinct.len() >= 4, + "distinct component HP values: {distinct:?}" + ); // A main gun names the weapon it fires and the shield generator its share. let asgun = bs .hardpoints @@ -126,7 +132,9 @@ fn main_mission_bonus_is_per_difficulty() { } } } - eprintln!("MainMissionBonus: 3-distinct {distinct3}/24, all-equal {all_equal}, doubling {doubling}"); + eprintln!( + "MainMissionBonus: 3-distinct {distinct3}/24, all-equal {all_equal}, doubling {doubling}" + ); assert_eq!(distinct3, PIN_BONUS_DISTINCT3); assert_eq!(doubling, PIN_BONUS_DOUBLING); // The first config's own numbers. @@ -145,19 +153,35 @@ fn rank_scores_repeat_across_difficulties() { let mut identical = 0; for c in &cfgs { let row = |s: &gd::ScoreRules| { - (s.rank_score_s, s.rank_score_a, s.rank_score_b, s.rank_score_c, s.rank_score_d) + ( + s.rank_score_s, + s.rank_score_a, + s.rank_score_b, + s.rank_score_c, + s.rank_score_d, + ) }; - if row(&c.score.easy) == row(&c.score.normal) && row(&c.score.normal) == row(&c.score.hard) { + if row(&c.score.easy) == row(&c.score.normal) && row(&c.score.normal) == row(&c.score.hard) + { identical += 1; } } - eprintln!("rank scores identical across difficulties: {identical}/{}", cfgs.len()); + eprintln!( + "rank scores identical across difficulties: {identical}/{}", + cfgs.len() + ); assert_eq!(identical, cfgs.len()); assert_eq!(cfgs[0].score.normal.rank_score_s, Some(10000)); assert_eq!(cfgs[0].score.normal.rank_score_d, Some(1000)); // The difficulty scaling that *is* per record. - assert_eq!(cfgs[0].difficulty_f32(Difficulty::Easy, "DamageAdjustment"), PIN_EASY_DMG); - assert_eq!(cfgs[0].difficulty_f32(Difficulty::Hard, "ShieldDamageAdjustment"), PIN_HARD_SHIELD); + assert_eq!( + cfgs[0].difficulty_f32(Difficulty::Easy, "DamageAdjustment"), + PIN_EASY_DMG + ); + assert_eq!( + cfgs[0].difficulty_f32(Difficulty::Hard, "ShieldDamageAdjustment"), + PIN_HARD_SHIELD + ); } /// `SpaceSize` lives in `Phase_1` / `Phase_2` / `Phase_3`, one value each. @@ -215,7 +239,9 @@ fn previously_unreadable_stats_are_populated() { ] { assert_eq!(count, units.len(), "{label} set on every unit"); } - assert!(vessels.iter().all(|v| v.fcs_range.is_some() && v.shield_ratio.is_some())); + assert!(vessels + .iter() + .all(|v| v.fcs_range.is_some() && v.shield_ratio.is_some())); // Hardpoint HP is readable for every vessel component. let hp_missing = vessels .iter() @@ -239,7 +265,10 @@ fn weapon_launcher_and_shell_stay_separate() { .iter() .filter(|w| w.id.is_some() && w.shell_id.is_some() && w.id != w.shell_id) .count(); - eprintln!("weapons whose shell id differs from the launcher id: {differing}/{}", ws.len()); + eprintln!( + "weapons whose shell id differs from the launcher id: {differing}/{}", + ws.len() + ); assert_eq!(differing, PIN_WEAPON_SHELL_IDS); // A player missile the corpus quotes through the flat reader — the launcher // fields agree, and `MaximumRange` (a `Shell` field) reads the same. @@ -254,13 +283,23 @@ fn weapon_launcher_and_shell_stay_separate() { assert_eq!(m26.trigger_shot_count, Some(12)); assert_eq!(m26.max_range, Some(10000.0)); - let with_wake = ws.iter().filter(|w| w.records.record("ShellWake").is_some()).count(); + let with_wake = ws + .iter() + .filter(|w| w.records.record("ShellWake").is_some()) + .count(); eprintln!("weapons with a ShellWake record: {with_wake}"); assert_eq!(with_wake, PIN_WEAPON_WAKE); // `Power` is a `Shell` field: every weapon but one has it. - let no_power: Vec<&str> = - ws.iter().filter(|w| w.power.is_none()).filter_map(|w| w.id.as_deref()).collect(); - assert_eq!(no_power, ["Weapon_NULL"], "only the placeholder weapon has no Shell.Power"); + let no_power: Vec<&str> = ws + .iter() + .filter(|w| w.power.is_none()) + .filter_map(|w| w.id.as_deref()) + .collect(); + assert_eq!( + no_power, + ["Weapon_NULL"], + "only the placeholder weapon has no Shell.Power" + ); } /// `RecordSet::everywhere` is the API that answers "which record did that come diff --git a/crates/sylpheed-formats/tests/idxd_records_disc.rs b/crates/sylpheed-formats/tests/idxd_records_disc.rs index d88b1ac4..3c84e01d 100644 --- a/crates/sylpheed-formats/tests/idxd_records_disc.rs +++ b/crates/sylpheed-formats/tests/idxd_records_disc.rs @@ -313,8 +313,12 @@ fn sibling_default_rules_are_a_dedup_artefact() { if !IdxdObject::is_idxd(&bytes) { continue; } - let Ok(obj) = IdxdObject::parse(&bytes) else { continue }; - let Some(generic) = obj.record("Generic") else { continue }; + let Ok(obj) = IdxdObject::parse(&bytes) else { + continue; + }; + let Some(generic) = obj.record("Generic") else { + continue; + }; if generic.get("Size_X").is_none() { continue; } @@ -328,7 +332,11 @@ fn sibling_default_rules_are_a_dedup_artefact() { // The field is present on disc far more often than it "differs", so the // premise "these fields are defaulted" is simply false. - assert_eq!(tally[0][0][0] + tally[0][1][0], 90, "Size_Y differs from Size_X"); + assert_eq!( + tally[0][0][0] + tally[0][1][0], + 90, + "Size_Y differs from Size_X" + ); // The mechanism: a value shared with the sibling is ALWAYS invisible to the // legacy reader. If this cell were ever non-zero the dedup story would be @@ -343,6 +351,9 @@ fn sibling_default_rules_are_a_dedup_artefact() { // …and it almost never misses a value that differs. Those few cells are // exactly where the rule predicts the wrong number. assert_eq!(tally[0][1][0], 0, "Size_Y: rule never wrong"); - assert_eq!(tally[1][1][0], 1, "FCSRange: UN_e011_ADAN_Attacker_B_HF_Wayne"); + assert_eq!( + tally[1][1][0], 1, + "FCSRange: UN_e011_ADAN_Attacker_B_HF_Wayne" + ); assert_eq!(tally[2][1][0], 1, "DefencePoint: UN_e104_ADAN_Carrier"); } diff --git a/crates/sylpheed-formats/tests/ixud_records_disc.rs b/crates/sylpheed-formats/tests/ixud_records_disc.rs index b84eb78e..c833ad57 100644 --- a/crates/sylpheed-formats/tests/ixud_records_disc.rs +++ b/crates/sylpheed-formats/tests/ixud_records_disc.rs @@ -34,7 +34,9 @@ fn all_paks(root: &Path) -> Vec { let mut out = Vec::new(); let mut stack = vec![root.to_path_buf()]; while let Some(dir) = stack.pop() { - let Ok(rd) = std::fs::read_dir(&dir) else { continue }; + let Ok(rd) = std::fs::read_dir(&dir) else { + continue; + }; for e in rd.flatten() { let p = e.path(); if p.is_dir() { @@ -53,7 +55,9 @@ fn ixud_records_roundtrip_disc() { skip_without_disc!(root); let (mut objects, mut records, mut named, mut positional, mut bad) = (0, 0, 0, 0, 0); for pak in all_paks(&root) { - let Ok(ar) = PakArchive::open(&pak) else { continue }; + let Ok(ar) = PakArchive::open(&pak) else { + continue; + }; for entry in ar.entries() { let Ok(bytes) = ar.read(entry) else { continue }; if bytes.len() < 4 || bytes[0..4] != *b"IXUD" { diff --git a/crates/sylpheed-formats/tests/mesh_consistency_disc.rs b/crates/sylpheed-formats/tests/mesh_consistency_disc.rs index 8523fe76..cc0b73a4 100644 --- a/crates/sylpheed-formats/tests/mesh_consistency_disc.rs +++ b/crates/sylpheed-formats/tests/mesh_consistency_disc.rs @@ -71,13 +71,17 @@ fn shared_resources_decode_identically_in_every_container() { // name -> (verts, tris) -> set of spans seen let mut seen: BTreeMap> = BTreeMap::new(); for f in &files { - let Ok(bytes) = std::fs::read(f) else { continue }; + let Ok(bytes) = std::fs::read(f) else { + continue; + }; let where_ = f.file_name().unwrap().to_string_lossy().to_string(); for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) { let Some(sp) = span(&m) else { continue }; let v: usize = m.meshes.iter().map(|s| s.positions.len()).sum(); let t: usize = m.meshes.iter().map(|s| s.indices.len() / 3).sum(); - seen.entry(m.name.clone()).or_default().push((sp, v, t, where_.clone())); + seen.entry(m.name.clone()) + .or_default() + .push((sp, v, t, where_.clone())); } } @@ -127,18 +131,31 @@ fn twin_pairs_do_not_share_a_buffer() { let mut collapsed: Vec = Vec::new(); for f in &files { - let Ok(bytes) = std::fs::read(f) else { continue }; + let Ok(bytes) = std::fs::read(f) else { + continue; + }; let models = Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false); let by_name: BTreeMap<&str, &Xbg7Model> = models.iter().map(|m| (m.name.as_str(), m)).collect(); for m in &models { - let Some(stem) = m.name.strip_suffix("_01") else { continue }; - let Some(t) = by_name.get(format!("{stem}_02").as_str()) else { continue }; + let Some(stem) = m.name.strip_suffix("_01") else { + continue; + }; + let Some(t) = by_name.get(format!("{stem}_02").as_str()) else { + continue; + }; let (a, b) = (m.meshes[0].vbuf_offset, t.meshes[0].vbuf_offset); if a.is_some() && a == b { - collapsed.push(format!("{}/{stem}_02 in {}", m.name, f.file_name().unwrap().to_string_lossy())); + collapsed.push(format!( + "{}/{stem}_02 in {}", + m.name, + f.file_name().unwrap().to_string_lossy() + )); } } } - assert!(collapsed.is_empty(), "twin pairs sharing one buffer: {collapsed:?}"); + assert!( + collapsed.is_empty(), + "twin pairs sharing one buffer: {collapsed:?}" + ); } diff --git a/crates/sylpheed-formats/tests/mesh_disc.rs b/crates/sylpheed-formats/tests/mesh_disc.rs index a7928dc2..ea4ec8df 100644 --- a/crates/sylpheed-formats/tests/mesh_disc.rs +++ b/crates/sylpheed-formats/tests/mesh_disc.rs @@ -16,8 +16,7 @@ fn res3d_dir() -> Option { return Some(p); } } - let default = - PathBuf::from("/home/fabi/RE - Project Sylpheed/sylph_extract/hidden/resource3d"); + let default = PathBuf::from("/home/fabi/RE - Project Sylpheed/sylph_extract/hidden/resource3d"); default.is_dir().then_some(default) } @@ -38,14 +37,29 @@ fn hero_ship_submesh_material_graph() { map.insert((v, i), name); } // (vtx, idx) → expected albedo (rou_ stripped = TX2D name). - assert_eq!(map.get(&(314, 645)).map(String::as_str), Some("f001_bdy_04_col")); - assert_eq!(map.get(&(48, 84)).map(String::as_str), Some("f001_bdy_04_col")); - assert_eq!(map.get(&(96, 180)).map(String::as_str), Some("f001_bdy_06_col")); - assert_eq!(map.get(&(60, 108)).map(String::as_str), Some("f001_bdy_07_col")); + assert_eq!( + map.get(&(314, 645)).map(String::as_str), + Some("f001_bdy_04_col") + ); + assert_eq!( + map.get(&(48, 84)).map(String::as_str), + Some("f001_bdy_04_col") + ); + assert_eq!( + map.get(&(96, 180)).map(String::as_str), + Some("f001_bdy_06_col") + ); + assert_eq!( + map.get(&(60, 108)).map(String::as_str), + Some("f001_bdy_07_col") + ); // Names strip cleanly to real TX2D resources. let tex = sylpheed_formats::texture::X360Texture::texture_names(&bytes); for name in map.values() { - assert!(tex.iter().any(|t| t == name), "albedo {name} not a TX2D resource"); + assert!( + tex.iter().any(|t| t == name), + "albedo {name} not a TX2D resource" + ); } } @@ -111,27 +125,55 @@ fn hero_ship_node_transforms_move_fins_to_tail() { // Body (sub 0) identity, drawn once. let body: Vec<_> = places.iter().filter(|p| p.sub_index == 0).collect(); assert_eq!(body.len(), 1, "body drawn once"); - assert!(body[0].t.iter().all(|c| c.abs() < 0.1), "body must not translate"); + assert!( + body[0].t.iter().all(|c| c.abs() < 0.1), + "body must not translate" + ); assert!(!body[0].reflect, "body is not mirrored"); // Fin bdy_04 (sub 1): mirrored pair near the tail, mounted OUT on the nacelle // (|X| ≈ 5.2, not the graph's inboard ≈1.1). let fins: Vec<_> = places.iter().filter(|p| p.sub_index == 1).collect(); assert_eq!(fins.len(), 2, "V-tail is a mirrored pair"); - assert!(fins.iter().all(|f| (f.t[2] + 9.72).abs() < 0.3), "fin fore/aft ≈ −9.7"); - assert!(fins.iter().all(|f| f.t[0].abs() > 4.0), "fin mounted out on the nacelle"); - assert!(fins[0].t[0] * fins[1].t[0] < 0.0, "the pair mirrors across X"); - assert!(fins.iter().any(|f| f.reflect), "one of the pair is reflected"); + assert!( + fins.iter().all(|f| (f.t[2] + 9.72).abs() < 0.3), + "fin fore/aft ≈ −9.7" + ); + assert!( + fins.iter().all(|f| f.t[0].abs() > 4.0), + "fin mounted out on the nacelle" + ); + assert!( + fins[0].t[0] * fins[1].t[0] < 0.0, + "the pair mirrors across X" + ); + assert!( + fins.iter().any(|f| f.reflect), + "one of the pair is reflected" + ); // Winglet bdy_06 (sub 4): on the nacelle (|X| ≈ 6.6, Z ≈ −15.5). let wings: Vec<_> = places.iter().filter(|p| p.sub_index == 4).collect(); assert_eq!(wings.len(), 2, "L/R winglet pair"); - assert!(wings.iter().all(|p| p.t[0].abs() > 6.0 && (p.t[2] + 15.5).abs() < 0.3), - "winglets out on the nacelle"); - assert!(wings[0].t[0] * wings[1].t[0] < 0.0, "winglets mirror across X"); + assert!( + wings + .iter() + .all(|p| p.t[0].abs() > 6.0 && (p.t[2] + 15.5).abs() < 0.3), + "winglets out on the nacelle" + ); + assert!( + wings[0].t[0] * wings[1].t[0] < 0.0, + "winglets mirror across X" + ); // Small fin bdy_10 (sub 6): inboard nacelle stub (|X| ≈ 4.45, Z ≈ −17). let sfins: Vec<_> = places.iter().filter(|p| p.sub_index == 6).collect(); assert_eq!(sfins.len(), 2, "L/R small-fin pair"); - assert!(sfins.iter().all(|p| (p.t[0].abs() - 4.45).abs() < 0.3), "small fins on the stub"); - assert!(sfins[0].t[0] * sfins[1].t[0] < 0.0, "small fins mirror across X"); + assert!( + sfins.iter().all(|p| (p.t[0].abs() - 4.45).abs() < 0.3), + "small fins on the stub" + ); + assert!( + sfins[0].t[0] * sfins[1].t[0] < 0.0, + "small fins mirror across X" + ); } #[test] @@ -160,8 +202,8 @@ fn weapon_model_decodes_to_expected_geometry() { assert!(m.indices.iter().all(|&i| (i as usize) < m.positions.len())); // positions are real geometry within the model's ~2-unit bbox let ys: Vec = m.positions.iter().map(|p| p[1]).collect(); - let span = ys.iter().cloned().fold(f32::MIN, f32::max) - - ys.iter().cloned().fold(f32::MAX, f32::min); + let span = + ys.iter().cloned().fold(f32::MIN, f32::max) - ys.iter().cloned().fold(f32::MAX, f32::min); assert!(span > 1.0 && span < 10.0, "y-span {span} out of range"); // Correct vertex alignment ⇒ normals are unit-length (the pin for the @@ -172,9 +214,14 @@ fn weapon_model_decodes_to_expected_geometry() { .map(|n| (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt()) .sum::() / m.normals.len() as f32; - assert!((mean_nlen - 1.0).abs() < 0.05, "mean |normal| {mean_nlen} ≠ 1"); assert!( - m.uvs.iter().all(|uv| uv[0] > -0.1 && uv[0] < 2.0 && uv[1] > -0.1 && uv[1] < 2.0), + (mean_nlen - 1.0).abs() < 0.05, + "mean |normal| {mean_nlen} ≠ 1" + ); + assert!( + m.uvs + .iter() + .all(|uv| uv[0] > -0.1 && uv[0] < 2.0 && uv[1] > -0.1 && uv[1] < 2.0), "UVs out of expected [0,1]-ish range" ); } @@ -225,11 +272,14 @@ fn complex_body_mesh_is_declined_not_garbage() { // be cleanly rejected, never returned as partial geometry. let bytes = std::fs::read(dir.join("DeltaSaber_A.xpr")).unwrap(); match Xbg7Model::from_xpr2(&bytes) { - Err(_) => {} // expected: declined + Err(_) => {} // expected: declined Ok(m) => { // If it ever does decode, it must at least be self-consistent. for mesh in &m.meshes { - assert!(mesh.indices.iter().all(|&i| (i as usize) < mesh.positions.len())); + assert!(mesh + .indices + .iter() + .all(|&i| (i as usize) < mesh.positions.len())); } } } @@ -252,7 +302,10 @@ fn hero_ship_grouped_pool_decodes() { let models = Xbg7Model::stage_models(&bytes); // The neutral pose `f001` (the mnv*/turn180 resources are animation poses). - let f001 = models.iter().find(|m| m.name == "f001").expect("f001 decoded"); + let f001 = models + .iter() + .find(|m| m.name == "f001") + .expect("f001 decoded"); assert_eq!(f001.meshes.len(), 8, "body + 7 detail sub-meshes"); let (v, t) = f001.totals(); assert_eq!(v, 11607, "vertex total across sub-meshes"); @@ -266,7 +319,9 @@ fn hero_ship_grouped_pool_decodes() { for (i, m) in f001.meshes.iter().enumerate() { // Every index in range. assert!( - m.indices.iter().all(|&ix| (ix as usize) < m.positions.len()), + m.indices + .iter() + .all(|&ix| (ix as usize) < m.positions.len()), "sub{i}: index out of range" ); // Correct alignment ⇒ unit normals, and the winding agrees with them @@ -279,7 +334,10 @@ fn hero_ship_grouped_pool_decodes() { .map(|nv| (nv[0] * nv[0] + nv[1] * nv[1] + nv[2] * nv[2]).sqrt()) .sum::() / n as f32; - assert!((mean_nlen - 1.0).abs() < 0.05, "sub{i}: mean |normal| {mean_nlen} ≠ 1"); + assert!( + (mean_nlen - 1.0).abs() < 0.05, + "sub{i}: mean |normal| {mean_nlen} ≠ 1" + ); let mut agree = 0usize; let mut counted = 0usize; @@ -345,19 +403,26 @@ fn stage_models_decode() { ); } // Known: e003 (main enemy, ~1400 tris) must be among the decoded models. - let e003 = models.iter().find(|m| m.name == "e003").expect("e003 decoded"); + let e003 = models + .iter() + .find(|m| m.name == "e003") + .expect("e003 decoded"); let (v, t) = e003.totals(); assert_eq!(v, 2383, "e003 vertex count"); assert!(t > 1400, "e003 triangle count {t}"); - assert!(models.len() >= 3, "at least 3 stage sub-models, got {}", models.len()); + assert!( + models.len() >= 3, + "at least 3 stage sub-models, got {}", + models.len() + ); } /// Timing/coverage sweep across all stage containers (manual; release recommended). #[test] #[ignore] fn stage_models_sweep() { - use sylpheed_formats::mesh::Xbg7Model; use std::time::Instant; + use sylpheed_formats::mesh::Xbg7Model; let dir = std::env::var("SYLPHEED_RES3D").unwrap_or_else(|_| { "/home/fabi/RE - Project Sylpheed/sylph_extract/hidden/resource3d".to_string() }); @@ -374,7 +439,12 @@ fn stage_models_sweep() { let models = Xbg7Model::stage_models(&bytes); let dt = t0.elapsed().as_millis(); tot += models.len(); - println!("{n:16} {:>4} MB {:>3} models {:>5} ms", bytes.len()/1_000_000, models.len(), dt); + println!( + "{n:16} {:>4} MB {:>3} models {:>5} ms", + bytes.len() / 1_000_000, + models.len(), + dt + ); } println!("TOTAL stage sub-models decoded: {tot}"); } @@ -394,33 +464,69 @@ fn stage_models_quality_audit() { let mut seen = std::collections::HashSet::new(); let mut worst_deg = 0.0f32; for m in &models { - if !seen.insert(m.name.clone()) { dupnames += 1; } - let mut lo = [f32::MAX; 3]; let mut hi = [f32::MIN; 3]; - let mut deg = 0usize; let mut tot = 0usize; + if !seen.insert(m.name.clone()) { + dupnames += 1; + } + let mut lo = [f32::MAX; 3]; + let mut hi = [f32::MIN; 3]; + let mut deg = 0usize; + let mut tot = 0usize; for sub in &m.meshes { - for p in &sub.positions { for a in 0..3 { lo[a]=lo[a].min(p[a]); hi[a]=hi[a].max(p[a]); } } + for p in &sub.positions { + for a in 0..3 { + lo[a] = lo[a].min(p[a]); + hi[a] = hi[a].max(p[a]); + } + } for tri in sub.indices.chunks_exact(3) { - let (a,b,c)=(tri[0] as usize,tri[1] as usize,tri[2] as usize); - if a>=sub.positions.len()||b>=sub.positions.len()||c>=sub.positions.len(){continue;} - let pa=sub.positions[a]; let pb=sub.positions[b]; let pc=sub.positions[c]; - let u=[pb[0]-pa[0],pb[1]-pa[1],pb[2]-pa[2]]; - let v=[pc[0]-pa[0],pc[1]-pa[1],pc[2]-pa[2]]; - let cx=[u[1]*v[2]-u[2]*v[1],u[2]*v[0]-u[0]*v[2],u[0]*v[1]-u[1]*v[0]]; - if 0.5*(cx[0]*cx[0]+cx[1]*cx[1]+cx[2]*cx[2]).sqrt()<1e-9 { deg+=1; } - tot+=1; + let (a, b, c) = (tri[0] as usize, tri[1] as usize, tri[2] as usize); + if a >= sub.positions.len() || b >= sub.positions.len() || c >= sub.positions.len() + { + continue; + } + let pa = sub.positions[a]; + let pb = sub.positions[b]; + let pc = sub.positions[c]; + let u = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]]; + let v = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]]; + let cx = [ + u[1] * v[2] - u[2] * v[1], + u[2] * v[0] - u[0] * v[2], + u[0] * v[1] - u[1] * v[0], + ]; + if 0.5 * (cx[0] * cx[0] + cx[1] * cx[1] + cx[2] * cx[2]).sqrt() < 1e-9 { + deg += 1; + } + tot += 1; } } - let ext = (hi[0]-lo[0]).max(hi[1]-lo[1]).max(hi[2]-lo[2]); - let df = if tot>0 { deg as f32/tot as f32 } else {1.0}; + let ext = (hi[0] - lo[0]).max(hi[1] - lo[1]).max(hi[2] - lo[2]); + let df = if tot > 0 { + deg as f32 / tot as f32 + } else { + 1.0 + }; worst_deg = worst_deg.max(df); - if ext < 200.0 { small += 1; } else if ext < 5000.0 { mid += 1; } else { huge += 1; } + if ext < 200.0 { + small += 1; + } else if ext < 5000.0 { + mid += 1; + } else { + huge += 1; + } } println!("S07: {} models | small(<200u)={} mid={} huge(>5k)={} | dup-names={} | worst full-mesh degeneracy={:.1}%", models.len(), small, mid, huge, dupnames, worst_deg*100.0); // Each XBG7 resource must anchor to a *distinct* block (no collisions), the // bulk must be bounded-scale geometry, and none may be mostly-degenerate. - assert_eq!(dupnames, 0, "no two resources should anchor to the same block"); - assert!(small + mid > models.len() * 9 / 10, "≥90% bounded-scale geometry"); + assert_eq!( + dupnames, 0, + "no two resources should anchor to the same block" + ); + assert!( + small + mid > models.len() * 9 / 10, + "≥90% bounded-scale geometry" + ); assert!(huge < models.len() / 20, "few huge (skybox-plane) models"); assert!(worst_deg < 0.35, "no model should be mostly-degenerate"); } @@ -453,7 +559,9 @@ fn decoded_index_runs_have_almost_no_degenerate_triangles() { let mut offenders: Vec<(String, String, usize)> = Vec::new(); for f in &files { - let Ok(bytes) = std::fs::read(f) else { continue }; + let Ok(bytes) = std::fs::read(f) else { + continue; + }; let where_ = f.file_name().unwrap().to_string_lossy().to_string(); for m in Xbg7Model::stage_models(&bytes) { for sm in &m.meshes { diff --git a/crates/sylpheed-formats/tests/movie_manifest_disc.rs b/crates/sylpheed-formats/tests/movie_manifest_disc.rs index bad81bac..bdea2406 100644 --- a/crates/sylpheed-formats/tests/movie_manifest_disc.rs +++ b/crates/sylpheed-formats/tests/movie_manifest_disc.rs @@ -89,8 +89,13 @@ fn binds_and_resolves_movie_voice() { .expect("hokyu_DS_s13A present in manifest"); assert_eq!(e.voice_token.as_deref(), Some("VOICE_D_452")); assert_eq!( - movie_manifest::resolve_voice_entry(&manifest, &sounds, "hokyu_DS_s13A", VoiceLang::English) - .as_deref(), + movie_manifest::resolve_voice_entry( + &manifest, + &sounds, + "hokyu_DS_s13A", + VoiceLang::English + ) + .as_deref(), Some("eng\\etc\\VOICE_D_452.slb") ); @@ -114,7 +119,10 @@ fn binds_and_resolves_movie_voice() { } } assert!(resolved > 80, "expected 80+ voiced movies, got {resolved}"); - assert!(missing.is_empty(), "resolved but absent in sound.pak: {missing:?}"); + assert!( + missing.is_empty(), + "resolved but absent in sound.pak: {missing:?}" + ); } /// The manifest's shape, pinned. These counts were wrong in the docs for a long @@ -142,7 +150,11 @@ fn manifest_slot_and_movie_counts() { .filter(|e| f(e).is_some()) .map(|e| e.movie.as_str()) .collect(); - let distinct: HashSet<&str> = entries.iter().filter_map(|e| f(e)).map(String::as_str).collect(); + let distinct: HashSet<&str> = entries + .iter() + .filter_map(|e| f(e)) + .map(String::as_str) + .collect(); (slots, movies.len(), distinct.len()) }; // (slots, movies, distinct strings) — the third is what the docs used to report. @@ -165,7 +177,9 @@ fn manifest_slot_and_movie_counts() { assert_eq!(shared, ["hokyu_DS_s07A", "hokyu_DS_s07H", "hokyu_LS_s02A"]); // Every id names a real record, so nothing dangles. - assert!(entries.iter().all(|e| !e.slot.is_empty() && !e.movie.is_empty())); + assert!(entries + .iter() + .all(|e| !e.slot.is_empty() && !e.movie.is_empty())); } /// Everything the Cutscenes browser shows, asserted against the disc. @@ -187,8 +201,7 @@ fn cutscene_catalog_binds_movies_and_transcripts() { let rows = movie_manifest::parse(&manifest); assert_eq!(rows.len(), 104, "manifest slots"); - let movies: std::collections::BTreeSet<&str> = - rows.iter().map(|r| r.movie.as_str()).collect(); + let movies: std::collections::BTreeSet<&str> = rows.iter().map(|r| r.movie.as_str()).collect(); assert_eq!(movies.len(), 101, "distinct movies"); assert_eq!(rows.iter().filter(|r| r.subtitle.is_some()).count(), 99); assert_eq!(rows.iter().filter(|r| r.voice_token.is_some()).count(), 99); @@ -213,7 +226,13 @@ fn cutscene_catalog_binds_movies_and_transcripts() { absent.sort_unstable(); assert_eq!( absent, - ["SYLPH_HD720p_8M-CBR_2ch", "logo1", "logo2", "logo3", "logo4"], + [ + "SYLPH_HD720p_8M-CBR_2ch", + "logo1", + "logo2", + "logo3", + "logo4" + ], "manifest-bound movies with no .wmv on the disc" ); diff --git a/crates/sylpheed-formats/tests/movie_subtitle_disc.rs b/crates/sylpheed-formats/tests/movie_subtitle_disc.rs index d7914789..760874f5 100644 --- a/crates/sylpheed-formats/tests/movie_subtitle_disc.rs +++ b/crates/sylpheed-formats/tests/movie_subtitle_disc.rs @@ -18,7 +18,8 @@ fn resolves_english_radio_subtitles() { return; }; let lang = SubLang::English; - let lang_pak = PakArchive::open(root.join(format!("dat/movie/{}.pak", lang.pak_code()))).unwrap(); + let lang_pak = + PakArchive::open(root.join(format!("dat/movie/{}.pak", lang.pak_code()))).unwrap(); let text_pak = PakArchive::open(root.join(format!("dat/GP_MAIN_GAME_{}.pak", lang.game_code()))).unwrap(); @@ -59,5 +60,9 @@ fn resolves_english_radio_subtitles() { multiline.text, "Look at it father\n& beautiful isn't it", "both lines of the caption must be present" ); - assert!((multiline.start - 74.8).abs() < 0.1, "start {}", multiline.start); + assert!( + (multiline.start - 74.8).abs() < 0.1, + "start {}", + multiline.start + ); } diff --git a/crates/sylpheed-formats/tests/pak_idxd_disc.rs b/crates/sylpheed-formats/tests/pak_idxd_disc.rs index 47e8a5f4..b7e14d61 100644 --- a/crates/sylpheed-formats/tests/pak_idxd_disc.rs +++ b/crates/sylpheed-formats/tests/pak_idxd_disc.rs @@ -62,7 +62,10 @@ fn deltasaber_craft_stats() { skip_without_disc!(root); let arc = PakArchive::open(root.join("dat/GP_MAIN_GAME_E.pak")).unwrap(); // rou_f001 "DeltaSaber" — the player craft. Entry hash discovered via content scan. - let bytes = arc.read_by_hash(0x7c96_296c).expect("entry present").unwrap(); + let bytes = arc + .read_by_hash(0x7c96_296c) + .expect("entry present") + .unwrap(); let obj = IdxdObject::parse(&bytes).unwrap(); // identifier-valued fields via get_raw assert_eq!(obj.get_raw("Type"), Some("Craft")); @@ -93,8 +96,14 @@ fn deltasaber_craft_stats() { // The values above are real, but each lives in a *different* record, which // the flat API cannot express: these three are Generic / Maneuver / Shield. assert_eq!(generic.get("HP"), Some("1000.0")); - assert_eq!(obj.record("Maneuver").unwrap().get("Acceleration"), Some("600.0")); - assert_eq!(obj.record("Shield").unwrap().get("ChargeSpeed"), Some("800.0")); + assert_eq!( + obj.record("Maneuver").unwrap().get("Acceleration"), + Some("600.0") + ); + assert_eq!( + obj.record("Shield").unwrap().get("ChargeSpeed"), + Some("800.0") + ); // And `HP` is not one number: 63 turret records carry their own (all 100.0). let turrets = obj @@ -114,7 +123,10 @@ fn deltasaber_craft_stats() { fn legacy_reader_flattens_per_record_fields() { skip_without_disc!(root); let arc = PakArchive::open(root.join("dat/GP_HANGAR_ARSENAL.pak")).unwrap(); - let bytes = arc.read_by_hash(0x8f72_ddde).expect("entry present").unwrap(); + let bytes = arc + .read_by_hash(0x8f72_ddde) + .expect("entry present") + .unwrap(); let obj = IdxdObject::parse(&bytes).unwrap(); // One answer for the whole object … @@ -135,7 +147,10 @@ fn dsaber_missile_weapon_stats() { skip_without_disc!(root); let arc = PakArchive::open(root.join("dat/GP_MAIN_GAME_E.pak")).unwrap(); // Weapon_DSaber_P_wep_26_Missile - let bytes = arc.read_by_hash(0x4666_5408).expect("entry present").unwrap(); + let bytes = arc + .read_by_hash(0x4666_5408) + .expect("entry present") + .unwrap(); let obj = IdxdObject::parse(&bytes).unwrap(); assert_eq!(obj.get_str("TargetType"), Some("Vessel,Craft")); assert_eq!(obj.get_i64("LoadingCount"), Some(144)); @@ -232,11 +247,14 @@ fn ratc_bundle_lists_children() { if !sylpheed_formats::ratc::is_ratc(&p) { continue; } - let Some(kids) = sylpheed_formats::ratc::parse(&p) else { continue }; + let Some(kids) = sylpheed_formats::ratc::parse(&p) else { + continue; + }; let has_named = kids.iter().any(|c| c.name.contains('.')); - let has_t8ad = kids - .iter() - .any(|c| c.kind == "T8aD" && sylpheed_formats::t8ad::parse(&p[c.offset..c.offset + c.size]).is_some()); + let has_t8ad = kids.iter().any(|c| { + c.kind == "T8aD" + && sylpheed_formats::t8ad::parse(&p[c.offset..c.offset + c.size]).is_some() + }); if !kids.is_empty() && has_named && has_t8ad { ok = true; break; @@ -251,7 +269,10 @@ fn eng_movie_subtitle_track() { skip_without_disc!(root); let arc = PakArchive::open(root.join("dat/movie/eng.pak")).unwrap(); // S04A's track — the longest inline English one (starts "Calm down!"). - let bytes = arc.read_by_hash(0x73d2_2a3b).expect("entry present").unwrap(); + let bytes = arc + .read_by_hash(0x73d2_2a3b) + .expect("entry present") + .unwrap(); let sub = sylpheed_formats::ixud::parse(&bytes).expect("IXUD parses as subtitle"); assert!(sub.cues.len() > 50, "cues={}", sub.cues.len()); assert!(!sub.is_reference_only()); @@ -277,7 +298,10 @@ fn subtitles_are_localized() { fn eng_movie_font_metadata() { skip_without_disc!(root); let arc = PakArchive::open(root.join("dat/movie/eng.pak")).unwrap(); - let bytes = arc.read_by_hash(0x5cd0_fca6).expect("entry present").unwrap(); + let bytes = arc + .read_by_hash(0x5cd0_fca6) + .expect("entry present") + .unwrap(); assert!(sylpheed_formats::font::is_font(&bytes)); let info = sylpheed_formats::font::parse_info(&bytes).expect("font parses"); assert!(info.glyphs > 0, "glyphs={}", info.glyphs); diff --git a/crates/sylpheed-formats/tests/savegame_samples.rs b/crates/sylpheed-formats/tests/savegame_samples.rs index 3f0349db..5165a004 100644 --- a/crates/sylpheed-formats/tests/savegame_samples.rs +++ b/crates/sylpheed-formats/tests/savegame_samples.rs @@ -29,7 +29,11 @@ fn every_sample_parses_and_round_trips_byte_identically() { for name in SAMPLES { let raw = sample(name); let save = savegame::parse(&raw).unwrap_or_else(|e| panic!("{name}: {e}")); - assert_eq!(save.payload.len(), 545, "{name}: the whole save is 545 bytes"); + assert_eq!( + save.payload.len(), + 545, + "{name}: the whole save is 545 bytes" + ); assert_eq!(save.ghad.len(), GHAD_SIZE, "{name}"); assert_eq!(save.records.len(), RECORD_COUNT, "{name}"); assert!( @@ -91,7 +95,11 @@ fn developing_one_weapon_moves_points_ratio_and_two_blob_entries() { // Blob: the bought item became developed, its successor became developable. let (b, a) = (before.develop_state(), after.develop_state()); let moved: Vec = (0..b.len()).filter(|&i| b[i] != a[i]).collect(); - assert_eq!(moved.len(), 2, "exactly two blob entries move, got {moved:?}"); + assert_eq!( + moved.len(), + 2, + "exactly two blob entries move, got {moved:?}" + ); assert_eq!(a[moved[0]], DevelopState::Developed); assert_eq!(a[moved[1]], DevelopState::Developable); } diff --git a/crates/sylpheed-formats/tests/slb_disc.rs b/crates/sylpheed-formats/tests/slb_disc.rs index 5920d0d1..e17b4e56 100644 --- a/crates/sylpheed-formats/tests/slb_disc.rs +++ b/crates/sylpheed-formats/tests/slb_disc.rs @@ -23,7 +23,9 @@ fn read_range(root: &PathBuf, mut off: u64, size: usize) -> Vec { break; } let seg = root.join(format!("dat/sound.p{i:02}")); - let Ok(meta) = std::fs::metadata(&seg) else { break }; + let Ok(meta) = std::fs::metadata(&seg) else { + break; + }; let len = meta.len(); if off >= len { off -= len; @@ -89,12 +91,22 @@ fn enumerates_voice_clips_from_sounds_tbl() { let pak = PakArchive::open(root.join("dat/tables.pak")).unwrap(); let tbl = pak.read_by_name("eng\\sounds.tbl").unwrap().unwrap(); let clips = slb::list_voice_clips(&tbl, VoiceLang::English); - assert!(clips.len() > 1000, "expected many voice clips, got {}", clips.len()); + assert!( + clips.len() > 1000, + "expected many voice clips, got {}", + clips.len() + ); // Every clip is an eng voice path present in sound.pak. let sound = std::fs::read(root.join("dat/sound.pak")).unwrap(); - let keys: std::collections::HashSet = - PakArchive::parse_toc(&sound).unwrap().iter().map(|e| e.name_hash).collect(); - let present = clips.iter().filter(|c| keys.contains(&name_hash(&c.name))).count(); + let keys: std::collections::HashSet = PakArchive::parse_toc(&sound) + .unwrap() + .iter() + .map(|e| e.name_hash) + .collect(); + let present = clips + .iter() + .filter(|c| keys.contains(&name_hash(&c.name))) + .count(); assert!( present as f32 / clips.len() as f32 > 0.95, "{present}/{} clips resolve in sound.pak", diff --git a/crates/sylpheed-formats/tests/slb_leading_segment_disc.rs b/crates/sylpheed-formats/tests/slb_leading_segment_disc.rs index cfb3302c..c5e69f6b 100644 --- a/crates/sylpheed-formats/tests/slb_leading_segment_disc.rs +++ b/crates/sylpheed-formats/tests/slb_leading_segment_disc.rs @@ -48,7 +48,11 @@ fn leading_segment_is_a_whole_number_of_packets() { let ri = b.windows(4).position(|w| w == b"RIFF").expect("has a RIFF"); assert!(ri > slb::HEADERLESS_DATA_OFFSET, "VOICE_D_{n}"); let lead = ri - slb::HEADERLESS_DATA_OFFSET; - assert_eq!(lead % slb::XMA1_PACKET, 0, "VOICE_D_{n} not a whole packet count"); + assert_eq!( + lead % slb::XMA1_PACKET, + 0, + "VOICE_D_{n} not a whole packet count" + ); assert_eq!(lead / slb::XMA1_PACKET, packets, "VOICE_D_{n} packet count"); } } @@ -87,7 +91,9 @@ fn all_zero_leading_region_is_skipped() { fn bank_named(root: &Path, path: &str) -> Vec { let snd = PakArchive::open(root.join("dat/sound.pak")).expect("sound.pak"); - let entry = snd.find_by_name(path).unwrap_or_else(|| panic!("{path} present")); + let entry = snd + .find_by_name(path) + .unwrap_or_else(|| panic!("{path} present")); snd.read(entry).expect("read") } @@ -130,7 +136,10 @@ fn leading_data_offset_is_derived_not_assumed() { #[test] fn derived_offset_recovers_voice_banks_without_regressing_etc() { skip_without_disc!(root); - for path in ["eng\\Voice\\VOICE_TCAF_592.slb", "jpn\\Voice\\VOICE_TCAF_592.slb"] { + for path in [ + "eng\\Voice\\VOICE_TCAF_592.slb", + "jpn\\Voice\\VOICE_TCAF_592.slb", + ] { let b = bank_named(&root, path); let ri = b.windows(4).position(|w| w == b"RIFF").expect("has a RIFF"); // Under the old constant this leading region was not a whole packet @@ -149,9 +158,10 @@ fn derived_offset_recovers_voice_banks_without_regressing_etc() { } // Control: an `etc` bank still produces what it did before. let b = bank_named(&root, "eng\\etc\\VOICE_D_452.slb"); - assert_eq!(slb::leading_data_offset( - b.windows(4).position(|w| w == b"RIFF").unwrap()), - slb::HEADERLESS_DATA_OFFSET); + assert_eq!( + slb::leading_data_offset(b.windows(4).position(|w| w == b"RIFF").unwrap()), + slb::HEADERLESS_DATA_OFFSET + ); } /// The scan agrees with the truth wherever the truth is knowable. @@ -170,9 +180,13 @@ fn scan_data_offset_agrees_with_the_riff_derived_answer() { for (dir, lo, hi) in [("Voice", 1u32, 120u32), ("etc", 1, 120)] { for n in lo..hi { let path = format!("{lang}\\{dir}\\VOICE_TCAF_{n:03}.slb"); - let Some(entry) = snd.find_by_name(&path) else { continue }; + let Some(entry) = snd.find_by_name(&path) else { + continue; + }; let Ok(b) = snd.read(entry) else { continue }; - let Some(ri) = b.windows(4).position(|w| w == b"RIFF") else { continue }; + let Some(ri) = b.windows(4).position(|w| w == b"RIFF") else { + continue; + }; if ri <= slb::HEADERLESS_DATA_OFFSET { continue; } @@ -186,7 +200,10 @@ fn scan_data_offset_agrees_with_the_riff_derived_answer() { } } } - assert!(checked >= 20, "expected a usable labelled set, got {checked}"); + assert!( + checked >= 20, + "expected a usable labelled set, got {checked}" + ); // The whole-disc rate is 99.62%; allow a little slack for a small slice. let rate = agreed as f64 / checked as f64; assert!( @@ -207,7 +224,9 @@ fn scan_only_returns_known_offsets() { format!("eng\\Voice\\VOICE_ADAN_{n:03}.slb"), format!("jpn\\Voice\\VOICE_ADAN_{n:03}.slb"), ] { - let Some(entry) = snd.find_by_name(&path) else { continue }; + let Some(entry) = snd.find_by_name(&path) else { + continue; + }; let Ok(b) = snd.read(entry) else { continue }; let got = slb::scan_data_offset(&b); assert!( @@ -240,18 +259,28 @@ fn a_waves_declared_size_is_confirmed_by_the_next_seek() { format!("eng\\Voice\\VOICE_TCAF_{n:03}.slb"), format!("jpn\\Voice\\VOICE_ADAN_{n:03}.slb"), ] { - let Some(entry) = snd.find_by_name(&path) else { continue }; + let Some(entry) = snd.find_by_name(&path) else { + continue; + }; let Ok(b) = snd.read(entry) else { continue }; - let Some(ri) = b.windows(4).position(|w| w == b"RIFF") else { continue }; - let Some(rel) = b[ri..].windows(4).position(|w| w == b"data") else { continue }; + let Some(ri) = b.windows(4).position(|w| w == b"RIFF") else { + continue; + }; + let Some(rel) = b[ri..].windows(4).position(|w| w == b"data") else { + continue; + }; let di = ri + rel; - let Some(sz) = b.get(di + 4..di + 8) else { continue }; + let Some(sz) = b.get(di + 4..di + 8) else { + continue; + }; let declared = u32::from_le_bytes(sz.try_into().unwrap()) as usize; // The boundary lies outside this entry's own TOC window whenever the // declared size overruns it, which is the common case — so read from // the archive's flat data rather than from the entry slice. let probe = entry.offset as usize + di + 8 + declared; - let Some(tag) = snd.data_at(probe, 16) else { continue }; + let Some(tag) = snd.data_at(probe, 16) else { + continue; + }; assert_eq!( &tag[0..4], b"seek", diff --git a/crates/sylpheed-formats/tests/texture_disc.rs b/crates/sylpheed-formats/tests/texture_disc.rs index d8563745..6e552ae9 100644 --- a/crates/sylpheed-formats/tests/texture_disc.rs +++ b/crates/sylpheed-formats/tests/texture_disc.rs @@ -42,7 +42,11 @@ async fn xpr_pipeline_over_disc_sample() { println!("found {} .xpr files", xprs.len()); // Sample across the set so we hit different texture sizes/formats. - let sample: Vec = xprs.iter().step_by((xprs.len() / 24).max(1)).cloned().collect(); + let sample: Vec = xprs + .iter() + .step_by((xprs.len() / 24).max(1)) + .cloned() + .collect(); let mut ok = 0usize; let mut fail = 0usize; @@ -66,12 +70,21 @@ async fn xpr_pipeline_over_disc_sample() { let magic: String = bytes .iter() .take(4) - .map(|b| if b.is_ascii_graphic() { *b as char } else { '.' }) + .map(|b| { + if b.is_ascii_graphic() { + *b as char + } else { + '.' + } + }) .collect(); if fmt != sylpheed_formats::vfs::FileFormat::Xpr2Texture { nonxpr += 1; - println!(" [{path}] NOT XPR2 (magic {magic:?}, {} bytes)", bytes.len()); + println!( + " [{path}] NOT XPR2 (magic {magic:?}, {} bytes)", + bytes.len() + ); continue; } @@ -85,10 +98,19 @@ async fn xpr_pipeline_over_disc_sample() { let bw = ((t.width + bs - 1) / bs).max(1) as usize; let bh = ((t.height + bs - 1) / bs).max(1) as usize; let need = bw * bh * t.format.bytes_per_block(); - let size_ok = if need == t.data.len() { "ok" } else { "MISMATCH" }; + let size_ok = if need == t.data.len() { + "ok" + } else { + "MISMATCH" + }; println!( " [{path}] {:?} {}x{} mips={} data={} need={} {size_ok}", - t.format, t.width, t.height, t.mip_levels, t.data.len(), need + t.format, + t.width, + t.height, + t.mip_levels, + t.data.len(), + need ); } Err(e) => { @@ -121,7 +143,13 @@ fn dump_xpr2_structure(bytes: &[u8]) { } let tag: String = bytes[base..base + 4] .iter() - .map(|b| if b.is_ascii_graphic() { *b as char } else { '.' }) + .map(|b| { + if b.is_ascii_graphic() { + *b as char + } else { + '.' + } + }) .collect(); println!( " res[{i}] tag={tag:?} data_off=0x{:X} desc_size=0x{:X} name_off=0x{:X}", @@ -131,10 +159,6 @@ fn dump_xpr2_structure(bytes: &[u8]) { ); } // First 48 bytes hex for orientation. - let hx: String = bytes - .iter() - .take(48) - .map(|b| format!("{b:02X} ")) - .collect(); + let hx: String = bytes.iter().take(48).map(|b| format!("{b:02X} ")).collect(); println!(" hex[0..48] {hx}"); } diff --git a/crates/sylpheed-formats/tests/ui_focus_kind_disc.rs b/crates/sylpheed-formats/tests/ui_focus_kind_disc.rs index aacdbaac..b2498463 100644 --- a/crates/sylpheed-formats/tests/ui_focus_kind_disc.rs +++ b/crates/sylpheed-formats/tests/ui_focus_kind_disc.rs @@ -46,7 +46,9 @@ fn for_each_build(root: &Path, mut f: impl FnMut(&str, &[u8])) { paks.sort(); for p in &paks { let name = p.file_name().unwrap().to_string_lossy().to_string(); - let Ok(arc) = PakArchive::open(p) else { continue }; + let Ok(arc) = PakArchive::open(p) else { + continue; + }; for e in arc.entries() { let Ok(bytes) = arc.read(e) else { continue }; if ratc::is_ratc(&bytes) { @@ -146,7 +148,10 @@ fn focused_and_base_declaration_entries_are_compared_word_by_word() { eprintln!(" {e}"); } - assert!(pairs > 0, "no focused/base pairs found — the sweep is broken"); + assert!( + pairs > 0, + "no focused/base pairs found — the sweep is broken" + ); // MEASURED 2026-08-24, and asserted so the answer cannot rot back into a // suspicion: all 54 pairs on the disc carry the SAME `kind`, no bit is ever @@ -156,7 +161,10 @@ fn focused_and_base_declaration_entries_are_compared_word_by_word() { // focused state at all, and the naming pairing is not a shortcut around a // field that exists: there is no field. assert_eq!(pairs, 54, "the disc has 54 name-paired focused elements"); - assert_eq!(kind_equal, pairs, "some pair's `kind` differs from its base"); + assert_eq!( + kind_equal, pairs, + "some pair's `kind` differs from its base" + ); assert!( set_on_focused.is_empty(), "a kind bit distinguishes focused from base: {set_on_focused:?}" diff --git a/crates/sylpheed-formats/tests/ui_header_time_disc.rs b/crates/sylpheed-formats/tests/ui_header_time_disc.rs index cc3d8d76..d9eb8e0b 100644 --- a/crates/sylpheed-formats/tests/ui_header_time_disc.rs +++ b/crates/sylpheed-formats/tests/ui_header_time_disc.rs @@ -42,7 +42,9 @@ fn for_each_build(root: &Path, mut f: impl FnMut(&str, &[u8])) { paks.sort(); for p in &paks { let name = p.file_name().unwrap().to_string_lossy().to_string(); - let Ok(arc) = PakArchive::open(p) else { continue }; + let Ok(arc) = PakArchive::open(p) else { + continue; + }; for e in arc.entries() { let Ok(bytes) = arc.read(e) else { continue }; if ratc::is_ratc(&bytes) { @@ -69,9 +71,9 @@ fn header_0x08_against_the_keyframe_times() { let mut ratio: HashMap = HashMap::new(); // Does the rate word co-vary with anything? let mut by_rate: HashMap = HashMap::new(); // rate -> (bundles, max seen 0x08) - // The 16.16 frame-rate reading rests on twelve bundles at 30.0. If those are - // VARIANTS of 60.0 bundles - same elements, different rate - the reading - // gains a real discriminator; if they are unrelated one-offs it does not. + // The 16.16 frame-rate reading rests on twelve bundles at 30.0. If those are + // VARIANTS of 60.0 bundles - same elements, different rate - the reading + // gains a real discriminator; if they are unrelated one-offs it does not. let mut odd_rate: Vec = Vec::new(); for_each_build(&root, |pak, bytes| { @@ -91,7 +93,12 @@ fn header_0x08_against_the_keyframe_times() { e.1 = e.1.max(dur); if rate != 0x3C_0000 && odd_rate.len() < 20 { - let names: Vec<&str> = build.elements.iter().map(|e| e.name.as_str()).take(6).collect(); + let names: Vec<&str> = build + .elements + .iter() + .map(|e| e.name.as_str()) + .take(6) + .collect(); odd_rate.push(format!( "{pak}: rate {rate:#x} dur {dur} elements {} {:?}", build.elements.len(), @@ -148,7 +155,10 @@ fn header_0x08_against_the_keyframe_times() { // large unrelated constant would bound everything too, but then the ratios // would pile up near zero instead of peaking at 1.0. assert_eq!(over, 0, "a keyframe time runs past the header's +0x08"); - assert!(exact > 400, "the bound is never attained — it may be unrelated"); + assert!( + exact > 400, + "the bound is never attained — it may be unrelated" + ); let near_one = ratio.get(&10).copied().unwrap_or(0); let near_zero = ratio.get(&0).copied().unwrap_or(0); assert!( diff --git a/crates/sylpheed-formats/tests/ui_opt_link_disc.rs b/crates/sylpheed-formats/tests/ui_opt_link_disc.rs index 19db11b7..d1c54833 100644 --- a/crates/sylpheed-formats/tests/ui_opt_link_disc.rs +++ b/crates/sylpheed-formats/tests/ui_opt_link_disc.rs @@ -43,7 +43,9 @@ fn for_each_build(root: &Path, mut f: impl FnMut(&str, &[u8])) { paks.sort(); for p in &paks { let name = p.file_name().unwrap().to_string_lossy().to_string(); - let Ok(arc) = PakArchive::open(p) else { continue }; + let Ok(arc) = PakArchive::open(p) else { + continue; + }; for e in arc.entries() { let Ok(bytes) = arc.read(e) else { continue }; if ratc::is_ratc(&bytes) { @@ -94,8 +96,14 @@ fn every_opt_link_on_the_disc_is_classified() { links += 1; let src = el.name.to_ascii_lowercase(); let tgt = target.to_ascii_lowercase(); - let se = src.rsplit_once('.').map(|(_, e)| e.to_string()).unwrap_or_default(); - let te = tgt.rsplit_once('.').map(|(_, e)| e.to_string()).unwrap_or_default(); + let se = src + .rsplit_once('.') + .map(|(_, e)| e.to_string()) + .unwrap_or_default(); + let te = tgt + .rsplit_once('.') + .map(|(_, e)| e.to_string()) + .unwrap_or_default(); *ext_pairs.entry((se, te)).or_default() += 1; if tgt == src { self_link += 1; @@ -118,7 +126,11 @@ fn every_opt_link_on_the_disc_is_classified() { "{pak}: {} -> {}{}", el.name, target, - if names.contains(&tgt) { " [declared element]" } else { "" } + if names.contains(&tgt) { + " [declared element]" + } else { + "" + } )); } } @@ -144,9 +156,14 @@ fn every_opt_link_on_the_disc_is_classified() { // link resolves to a RATC child of its own bundle, and every one is // .rat -> .rat. assert_eq!(links, 1467, "the disc has 1467 opt links"); - assert_eq!(ratc_child, links, "an opt link does not resolve to a RATC child"); assert_eq!( - ext_pairs.get(&("rat".to_string(), "rat".to_string())).copied(), + ratc_child, links, + "an opt link does not resolve to a RATC child" + ); + assert_eq!( + ext_pairs + .get(&("rat".to_string(), "rat".to_string())) + .copied(), Some(links), "an opt link points at something other than a .rat record" ); diff --git a/crates/sylpheed-formats/tests/ui_paint_order_disc.rs b/crates/sylpheed-formats/tests/ui_paint_order_disc.rs index 37ac6f97..7f280a32 100644 --- a/crates/sylpheed-formats/tests/ui_paint_order_disc.rs +++ b/crates/sylpheed-formats/tests/ui_paint_order_disc.rs @@ -86,7 +86,9 @@ fn for_each_build(root: &Path, mut f: impl FnMut(&str, &[u8])) { paks.sort(); for p in &paks { let name = p.file_name().unwrap().to_string_lossy().to_string(); - let Ok(arc) = PakArchive::open(p) else { continue }; + let Ok(arc) = PakArchive::open(p) else { + continue; + }; for e in arc.entries() { let Ok(bytes) = arc.read(e) else { continue }; if ui_layout::is_build(&bytes) { @@ -349,7 +351,10 @@ fn no_composable_build_has_an_instance_without_its_template() { } } } - assert!(builds_seen > 500, "expected the disc's builds, saw {builds_seen}"); + assert!( + builds_seen > 500, + "expected the disc's builds, saw {builds_seen}" + ); assert!( orphans.is_empty(), "{} composable elements are kind=0x4 with no template present, so the \ @@ -376,7 +381,10 @@ fn the_derived_order_matches_the_measured_ones_up_to_ties() { ( 24, "ptlogo1.t32", - &[9, 11, 12, 10, 13, 6, 20, 19, 14, 15, 18, 16, 17, 0, 2, 4, 7, 1, 3, 5, 22, 23, 21, 8], + &[ + 9, 11, 12, 10, 13, 6, 20, 19, 14, 15, 18, 16, 17, 0, 2, 4, 7, 1, 3, 5, 22, 23, 21, + 8, + ], ), (7, "palogo_eff0.prm", &[0, 2, 4, 6, 1, 3, 5]), ]; @@ -402,9 +410,7 @@ fn the_derived_order_matches_the_measured_ones_up_to_ties() { if build.elements.len() != *n || build.elements[0].name != *first { continue; } - let key = |i: usize| { - ui_layout::sprite_layer_key(&build, &bundle, &build.elements[i]) - }; + let key = |i: usize| ui_layout::sprite_layer_key(&build, &bundle, &build.elements[i]); // 1. the measured order is non-decreasing in the key let mut last: Option = None; for &i in measured.iter() { @@ -421,15 +427,9 @@ fn the_derived_order_matches_the_measured_ones_up_to_ties() { } } // 2. and the derived order produces the same key sequence - let derived = ui_layout::compose( - &build, - &bundle, - ComposeOptions::default(), - None, - ); - let seq = |order: &[usize]| -> Vec { - order.iter().filter_map(|&i| key(i)).collect() - }; + let derived = ui_layout::compose(&build, &bundle, ComposeOptions::default(), None); + let seq = + |order: &[usize]| -> Vec { order.iter().filter_map(|&i| key(i)).collect() }; let measured_keys = seq(measured); let drawn_keys = seq(&derived.drawn); let mut expected = measured_keys.clone(); @@ -562,8 +562,14 @@ fn a_focused_state_always_has_the_element_it_is_the_focused_state_of() { ); } }); - assert!(total > 5000, "only {total} elements — the sweep did not run"); - assert!(eff > 100, "only {eff} `_eff` elements, expected the disc's glows"); + assert!( + total > 5000, + "only {total} elements — the sweep did not run" + ); + assert!( + eff > 100, + "only {eff} `_eff` elements, expected the disc's glows" + ); eprintln!("focused states: {flagged} of {total} elements; {eff} `_eff` glows kept"); } @@ -615,7 +621,10 @@ fn the_developer_logo_splash_composes_with_its_glows() { ); assert!(c.missing.is_empty(), "missing sprites: {:?}", c.missing); } - assert!(seen >= 2, "found {seen} splash bundles, expected the language pair"); + assert!( + seen >= 2, + "found {seen} splash bundles, expected the language pair" + ); } /// Applying the keyframe's `fade` alpha must not gut the corpus. @@ -663,7 +672,10 @@ fn applying_the_fade_alpha_blanks_no_screen() { blank += 1; } }); - assert!(sprite_els > 5000, "only {sprite_els} elements — sweep did not run"); + assert!( + sprite_els > 5000, + "only {sprite_els} elements — sweep did not run" + ); assert_eq!(blank, 0, "{blank} of {builds} builds render nothing"); // The modulate must stay overwhelmingly a no-op. If a future change to the // resting rule pushes many elements onto a ramp frame, this catches it. @@ -696,7 +708,10 @@ fn the_derived_order_puts_every_element_in_the_right_layer_group() { ( 24, "ptlogo1.t32", - &[9, 11, 12, 10, 13, 6, 20, 19, 14, 15, 18, 16, 17, 0, 2, 4, 7, 1, 3, 5, 22, 23, 21, 8], + &[ + 9, 11, 12, 10, 13, 6, 20, 19, 14, 15, 18, 16, 17, 0, 2, 4, 7, 1, 3, 5, 22, 23, 21, + 8, + ], ), (7, "palogo_eff0.prm", &[0, 2, 4, 6, 1, 3, 5]), ( @@ -773,7 +788,10 @@ fn order_disagreements_that_change_pixels_are_pinned() { ( 24, "ptlogo1.t32", - &[9, 11, 12, 10, 13, 6, 20, 19, 14, 15, 18, 16, 17, 0, 2, 4, 7, 1, 3, 5, 22, 23, 21, 8], + &[ + 9, 11, 12, 10, 13, 6, 20, 19, 14, 15, 18, 16, 17, 0, 2, 4, 7, 1, 3, 5, 22, 23, 21, + 8, + ], ), (7, "palogo_eff0.prm", &[0, 2, 4, 6, 1, 3, 5]), ( @@ -825,10 +843,19 @@ fn order_disagreements_that_change_pixels_are_pinned() { let &(off, size) = build.sprites.get(sprite)?; let img = t8ad::parse(&bundle[off..off + size])?; let (sx, sy) = (kf.scale_x.max(1), kf.scale_y.max(1)); - let (dw, dh) = ((img.width * sx / 100) as i32, (img.height * sy / 100) as i32); + let (dw, dh) = ( + (img.width * sx / 100) as i32, + (img.height * sy / 100) as i32, + ); let ox = kf.x - (el.pivot_x as i32 * (sx as i32 - 100)) / 100; let oy = kf.y - (el.pivot_y as i32 * (sy as i32 - 100)) / 100; - Some(Drawn { img, ox, oy, dw, dh }) + Some(Drawn { + img, + ox, + oy, + dw, + dh, + }) }; let alpha_at = |d: &Drawn, x: i32, y: i32| -> u8 { let (cx, cy) = (x - d.ox, y - d.oy); @@ -844,7 +871,8 @@ fn order_disagreements_that_change_pixels_are_pinned() { let pos = |order: &[usize], i: usize| order.iter().position(|&x| x == i).unwrap(); for a in 0..*n { for b in (a + 1)..*n { - if (pos(&derived, a) < pos(&derived, b)) == (pos(measured, a) < pos(measured, b)) + if (pos(&derived, a) < pos(&derived, b)) + == (pos(measured, a) < pos(measured, b)) { continue; } @@ -853,7 +881,10 @@ fn order_disagreements_that_change_pixels_are_pinned() { }; pairs += 1; let (x0, y0) = (da.ox.max(db.ox), da.oy.max(db.oy)); - let (x1, y1) = ((da.ox + da.dw).min(db.ox + db.dw), (da.oy + da.dh).min(db.oy + db.dh)); + let (x1, y1) = ( + (da.ox + da.dw).min(db.ox + db.dw), + (da.oy + da.dh).min(db.oy + db.dh), + ); if x0 >= x1 || y0 >= y1 { continue; // rects disjoint } @@ -876,7 +907,10 @@ fn order_disagreements_that_change_pixels_are_pinned() { } } } - assert!(seen.iter().all(|&b| b), "not every measured screen was found: {seen:?}"); + assert!( + seen.iter().all(|&b| b), + "not every measured screen was found: {seen:?}" + ); matters.sort(); matters.dedup(); eprintln!( @@ -960,7 +994,10 @@ fn the_save_load_screens_match_what_the_running_game_paints() { ); } } - assert!(headers >= 3, "found {headers} slot-list headers, expected several"); + assert!( + headers >= 3, + "found {headers} slot-list headers, expected several" + ); assert!(frames >= 1, "found {frames} save/load frames"); eprintln!("GP_SAVE_LOAD: {headers} headers exact, {frames} frames agree by layer group"); } diff --git a/crates/sylpheed-formats/tests/ui_prm_primitives_disc.rs b/crates/sylpheed-formats/tests/ui_prm_primitives_disc.rs index e306e2c7..9b8e4357 100644 --- a/crates/sylpheed-formats/tests/ui_prm_primitives_disc.rs +++ b/crates/sylpheed-formats/tests/ui_prm_primitives_disc.rs @@ -35,7 +35,9 @@ fn for_each_build(root: &Path, mut f: impl FnMut(&str, &[u8])) { paks.sort(); for p in &paks { let name = p.file_name().unwrap().to_string_lossy().to_string(); - let Ok(arc) = PakArchive::open(p) else { continue }; + let Ok(arc) = PakArchive::open(p) else { + continue; + }; for e in arc.entries() { let Ok(bytes) = arc.read(e) else { continue }; if ratc::is_ratc(&bytes) { @@ -96,9 +98,15 @@ fn kind_bit_0x10_means_prm_exactly_and_prm_carries_no_payload() { } } }); - assert!(prm > 300, "only {prm} `.prm` elements — the sweep did not run"); + assert!( + prm > 300, + "only {prm} `.prm` elements — the sweep did not run" + ); assert_eq!(prm_without_bit, 0, "a `.prm` element without kind bit 0x10"); - assert_eq!(bit_without_prm, 0, "kind bit 0x10 on something that is not `.prm`"); + assert_eq!( + bit_without_prm, 0, + "kind bit 0x10 on something that is not `.prm`" + ); assert_eq!(with_child, 0, "a `.prm` element with a payload child"); // 361 of 369 are exactly the design space; the handful that are not are // small coloured quads (844x600, and two degenerate 0x720). @@ -106,7 +114,9 @@ fn kind_bit_0x10_means_prm_exactly_and_prm_carries_no_payload() { full_screen * 10 > sized * 9, "only {full_screen}/{sized} `.prm` elements are full-screen" ); - eprintln!("{prm} `.prm` elements: all kind&0x10, none with a payload, {full_screen} full-screen"); + eprintln!( + "{prm} `.prm` elements: all kind&0x10, none with a payload, {full_screen} full-screen" + ); } /// **A `.prm` fade quad rests at its transparent plateau**, which is what makes @@ -153,7 +163,10 @@ fn the_title_fade_quad_rests_transparent() { ); } } - assert!(checked >= 3, "found {checked} title fade quads, expected several"); + assert!( + checked >= 3, + "found {checked} title fade quads, expected several" + ); } /// **Drawing the primitives with the derived order swallows screens** — which diff --git a/crates/sylpheed-formats/tests/ui_screen_vs_fragment_disc.rs b/crates/sylpheed-formats/tests/ui_screen_vs_fragment_disc.rs index 75019150..9e8bcec7 100644 --- a/crates/sylpheed-formats/tests/ui_screen_vs_fragment_disc.rs +++ b/crates/sylpheed-formats/tests/ui_screen_vs_fragment_disc.rs @@ -47,7 +47,9 @@ fn for_each_build(root: &Path, mut f: impl FnMut(&str, &[u8])) { paks.sort(); for p in &paks { let name = p.file_name().unwrap().to_string_lossy().to_string(); - let Ok(arc) = PakArchive::open(p) else { continue }; + let Ok(arc) = PakArchive::open(p) else { + continue; + }; for e in arc.entries() { let Ok(bytes) = arc.read(e) else { continue }; if ratc::is_ratc(&bytes) { @@ -151,7 +153,9 @@ fn the_bundle_header_does_not_label_a_screen() { let mut bits: Vec<_> = flag_bits.iter().collect(); bits.sort(); - eprintln!(" +0x10 bits: bit -> (bundles with it set, of those full-screen, of those >=10 elements)"); + eprintln!( + " +0x10 bits: bit -> (bundles with it set, of those full-screen, of those >=10 elements)" + ); for (bit, (n, fs, big)) in bits { eprintln!(" bit {bit:2}: {n:5} full-screen {fs:5} big {big:5}"); } diff --git a/crates/sylpheed-formats/tests/ui_surfaces_disc.rs b/crates/sylpheed-formats/tests/ui_surfaces_disc.rs index c103c4d0..5acb1e5f 100644 --- a/crates/sylpheed-formats/tests/ui_surfaces_disc.rs +++ b/crates/sylpheed-formats/tests/ui_surfaces_disc.rs @@ -46,7 +46,9 @@ fn for_each_blob(root: &Path, mut f: impl FnMut(&str, &str, &[u8])) { paks.sort(); for p in &paks { let pak_name = p.file_name().unwrap().to_string_lossy().to_string(); - let Ok(arc) = PakArchive::open(p) else { continue }; + let Ok(arc) = PakArchive::open(p) else { + continue; + }; for e in arc.entries() { let Ok(bytes) = arc.read(e) else { continue }; f(&pak_name, &format!("{:08x}", e.name_hash), &bytes); @@ -82,7 +84,10 @@ fn every_t8ad_on_the_disc_decodes() { first_failure = Some(format!("{pak}:{name}")); } }); - assert!(total > 19_000, "expected the disc's ~19 216 surfaces, saw {total}"); + assert!( + total > 19_000, + "expected the disc's ~19 216 surfaces, saw {total}" + ); assert_eq!(ok, total, "first failure: {first_failure:?}"); } @@ -117,10 +122,15 @@ fn lsta_count_equals_sprites_plus_primitives() { if declared == sprites + prims { exact += 1; } else if bad.len() < 4 { - bad.push(format!("{pak}:{name} declared {declared} != {sprites}+{prims}")); + bad.push(format!( + "{pak}:{name} declared {declared} != {sprites}+{prims}" + )); } }); - assert!(lists >= 60, "expected the disc's 64 LSTA lists, saw {lists}"); + assert!( + lists >= 60, + "expected the disc's 64 LSTA lists, saw {lists}" + ); assert_eq!(exact, lists, "mismatches: {bad:?}"); } @@ -147,6 +157,9 @@ fn ratc_nesting_is_exactly_one_level() { } } }); - assert!(bundles > 2_000, "expected thousands of RATC bundles, saw {bundles}"); + assert!( + bundles > 2_000, + "expected thousands of RATC bundles, saw {bundles}" + ); assert_eq!(grandchildren, 0, "a nested RATC record listed children"); } diff --git a/crates/sylpheed-formats/tests/unit_layout_disc.rs b/crates/sylpheed-formats/tests/unit_layout_disc.rs index 26731c29..40012e83 100644 --- a/crates/sylpheed-formats/tests/unit_layout_disc.rs +++ b/crates/sylpheed-formats/tests/unit_layout_disc.rs @@ -38,7 +38,10 @@ fn disc_root() -> Option { } } let d = "/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja)"; - std::path::Path::new(d).join("dat").is_dir().then(|| d.to_string()) + std::path::Path::new(d) + .join("dat") + .is_dir() + .then(|| d.to_string()) } /// `va -> offset -> value`, from the dump's `addr +off hex u32 f32` columns. @@ -69,14 +72,23 @@ fn mapped_fields_match_the_disc_records() { }; let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).expect("main pak"); let live = live(); - let floats: Vec<_> = fields().into_iter().filter(|f| f.kind == Kind::F32).collect(); + let floats: Vec<_> = fields() + .into_iter() + .filter(|f| f.kind == Kind::F32) + .collect(); let (mut agree, mut bad) = (0usize, Vec::new()); for entry in pak.entries() { let Ok(bytes) = pak.read(entry) else { continue }; - let Ok(obj) = IdxdObject::parse(&bytes) else { continue }; - let Some(id) = obj.get_raw("ID") else { continue }; - let Some((_, va)) = IDENTIFIED.iter().find(|(i, _)| *i == id) else { continue }; + let Ok(obj) = IdxdObject::parse(&bytes) else { + continue; + }; + let Some(id) = obj.get_raw("ID") else { + continue; + }; + let Some((_, va)) = IDENTIFIED.iter().find(|(i, _)| *i == id) else { + continue; + }; let Some(words) = live.get(*va) else { continue }; for f in &floats { let (Some(want), Some(got)) = (obj.get_f32(f.name), words.get(&f.offset)) else { @@ -87,7 +99,10 @@ fn mapped_fields_match_the_disc_records() { if (want - got).abs() <= want.abs() * 1e-4 || (rad - got).abs() <= rad.abs() * 1e-4 { agree += 1; } else { - bad.push(format!("{id}.{} disc {want} vs live +{} = {got}", f.name, f.offset)); + bad.push(format!( + "{id}.{} disc {want} vs live +{} = {got}", + f.name, f.offset + )); } } } diff --git a/crates/sylpheed-viewer/src/asset_loader.rs b/crates/sylpheed-viewer/src/asset_loader.rs index 2c521da2..32eabd86 100644 --- a/crates/sylpheed-viewer/src/asset_loader.rs +++ b/crates/sylpheed-viewer/src/asset_loader.rs @@ -16,7 +16,9 @@ use bevy::asset::{AssetLoader, LoadContext}; use bevy::image::ImageSampler; use bevy::prelude::*; use bevy::render::render_asset::RenderAssetUsages; -use bevy::render::render_resource::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages}; +use bevy::render::render_resource::{ + Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, +}; use sylpheed_formats::texture::{X360Texture, X360TextureFormat}; // ── Plugin ──────────────────────────────────────────────────────────────────── @@ -153,9 +155,7 @@ fn x360_format_to_wgpu(format: &X360TextureFormat) -> Result TextureFormat::Bc4RUnorm, // Uncompressed — reordered to [R,G,B,A] by `x360_texture_to_bevy_image`. - X360TextureFormat::A8R8G8B8 | X360TextureFormat::X8R8G8B8 => { - TextureFormat::Rgba8UnormSrgb - } + X360TextureFormat::A8R8G8B8 | X360TextureFormat::X8R8G8B8 => TextureFormat::Rgba8UnormSrgb, }) } diff --git a/crates/sylpheed-viewer/src/camera.rs b/crates/sylpheed-viewer/src/camera.rs index 3aff9215..1c853cf5 100644 --- a/crates/sylpheed-viewer/src/camera.rs +++ b/crates/sylpheed-viewer/src/camera.rs @@ -6,8 +6,8 @@ //! - Scroll wheel → zoom //! - R → reset to default view -use bevy::prelude::*; use bevy::input::mouse::{MouseMotion, MouseWheel}; +use bevy::prelude::*; use bevy::render::render_asset::RenderAssetUsages; use bevy::render::render_resource::{ Extent3d, TextureDimension, TextureFormat, TextureViewDescriptor, TextureViewDimension, @@ -18,7 +18,7 @@ pub struct OrbitCameraPlugin; impl Plugin for OrbitCameraPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, spawn_camera) - .add_systems(Update, orbit_camera); + .add_systems(Update, orbit_camera); } } @@ -159,7 +159,9 @@ fn orbit_camera( mut mouse_motion: EventReader, mut scroll: EventReader, ) { - let Ok((mut cam, mut transform, mut projection)) = query.get_single_mut() else { return }; + let Ok((mut cam, mut transform, mut projection)) = query.get_single_mut() else { + return; + }; let mut delta_motion = Vec2::ZERO; for ev in mouse_motion.read() { @@ -173,7 +175,7 @@ fn orbit_camera( // Orbit (left mouse drag) if mouse_buttons.pressed(MouseButton::Left) { - cam.yaw -= delta_motion.x * cam.orbit_sensitivity; + cam.yaw -= delta_motion.x * cam.orbit_sensitivity; cam.pitch -= delta_motion.y * cam.orbit_sensitivity; // Clamp pitch to avoid gimbal lock cam.pitch = cam.pitch.clamp(-1.5, 1.5); @@ -182,12 +184,12 @@ fn orbit_camera( // Pan (right mouse drag) if mouse_buttons.pressed(MouseButton::Right) { let right = transform.rotation * Vec3::X; - let up = transform.rotation * Vec3::Y; + let up = transform.rotation * Vec3::Y; // Copy fields before mutably borrowing `cam.focus` let pan_sens = cam.pan_sensitivity; - let radius = cam.radius; + let radius = cam.radius; cam.focus -= right * delta_motion.x * pan_sens * radius; - cam.focus += up * delta_motion.y * pan_sens * radius; + cam.focus += up * delta_motion.y * pan_sens * radius; } // Zoom (scroll) @@ -212,6 +214,5 @@ fn orbit_camera( fn orbit_transform(cam: &OrbitCamera) -> Transform { let rotation = Quat::from_euler(EulerRot::YXZ, cam.yaw, cam.pitch, 0.0); let offset = rotation * Vec3::new(0.0, 0.0, cam.radius); - Transform::from_translation(cam.focus + offset) - .looking_at(cam.focus, Vec3::Y) + Transform::from_translation(cam.focus + offset).looking_at(cam.focus, Vec3::Y) } diff --git a/crates/sylpheed-viewer/src/iso_loader.rs b/crates/sylpheed-viewer/src/iso_loader.rs index 3fffdfa5..0914d1f0 100644 --- a/crates/sylpheed-viewer/src/iso_loader.rs +++ b/crates/sylpheed-viewer/src/iso_loader.rs @@ -251,7 +251,6 @@ pub struct ModelPreview { pub info: String, } - /// The currently-open `.wmv` cutscene, shown in the central panel with a /// transport bar. Registered unconditionally (the decode/audio engine below is /// native-only); on wasm `active` stays false and `.wmv` shows the info panel. @@ -1383,10 +1382,7 @@ fn handle_open_iso( let mut reader = sylpheed_formats::xiso::open_iso(&path) .await .map_err(|e| e.to_string())?; - let files = reader - .list_all_files() - .await - .map_err(|e| e.to_string())?; + let files = reader.list_all_files().await.map_err(|e| e.to_string())?; Ok::<_, String>((iso_path, label, files)) }); @@ -1465,7 +1461,11 @@ fn handle_file_selected( // decode is heavy — assemble + label off-thread, then hand the UI plain // rows. Everything else falls through to the single-file read below. if file_path.to_ascii_lowercase().ends_with(".pak") { - let name = file_path.rsplit('/').next().unwrap_or(&file_path).to_string(); + let name = file_path + .rsplit('/') + .next() + .unwrap_or(&file_path) + .to_string(); let base = file_path[..file_path.len() - 4].to_string(); // strip ".pak" match &iso_state.source_kind { SourceKind::Iso(iso_path) => { @@ -1534,7 +1534,11 @@ fn handle_file_selected( // real path), probe dimensions/duration, and pre-decode the audio track // to a temp WAV — all off-thread. The video frame pipe is opened later. if file_path.to_ascii_lowercase().ends_with(".wmv") { - let name = file_path.rsplit('/').next().unwrap_or(&file_path).to_string(); + let name = file_path + .rsplit('/') + .next() + .unwrap_or(&file_path) + .to_string(); match &iso_state.source_kind { SourceKind::Iso(iso_path) => { let iso_path = iso_path.clone(); @@ -1579,10 +1583,9 @@ fn handle_file_selected( let iso_path = iso_path.clone(); std::thread::spawn(move || { let result = futures::executor::block_on(async { - let mut reader = - sylpheed_formats::xiso::open_iso(&iso_path) - .await - .map_err(|e| e.to_string())?; + let mut reader = sylpheed_formats::xiso::open_iso(&iso_path) + .await + .map_err(|e| e.to_string())?; let bytes = reader .read_file(&file_path) .await @@ -1592,8 +1595,7 @@ fn handle_file_selected( match result { Ok((path, bytes)) => { - let _ = sender - .send(IsoLoaderMsg::FileLoaded { path, bytes }); + let _ = sender.send(IsoLoaderMsg::FileLoaded { path, bytes }); } Err(e) => { let _ = sender.send(IsoLoaderMsg::Error(e)); @@ -1602,8 +1604,7 @@ fn handle_file_selected( }); } SourceKind::Directory(root) => { - let assets = - sylpheed_formats::vfs::GameAssets::from_directory(root); + let assets = sylpheed_formats::vfs::GameAssets::from_directory(root); match assets.read(&file_path) { Ok(bytes) => { let _ = sender.send(IsoLoaderMsg::FileLoaded { @@ -1901,8 +1902,7 @@ fn prepare_video(name: &str, bytes: Vec) -> Result { let dir = std::env::temp_dir(); let stem = format!("sylph_vid_{}_{}", std::process::id(), sanitize_stem(name)); let temp_video = dir.join(format!("{stem}.wmv")); - std::fs::write(&temp_video, &bytes) - .map_err(|e| format!("writing temp video: {e}"))?; + std::fs::write(&temp_video, &bytes).map_err(|e| format!("writing temp video: {e}"))?; let (width, height, fps, duration) = ffprobe_video(&temp_video)?; @@ -1971,14 +1971,19 @@ fn ffprobe_video(path: &Path) -> Result<(u32, u32, f32, f32), String> { let text = String::from_utf8_lossy(&out.stdout); let (mut w, mut h, mut fps, mut dur) = (0u32, 0u32, 30.0f32, 0.0f32); for line in text.lines() { - let Some((k, v)) = line.split_once('=') else { continue }; + let Some((k, v)) = line.split_once('=') else { + continue; + }; match k.trim() { "width" => w = v.trim().parse().unwrap_or(0), "height" => h = v.trim().parse().unwrap_or(0), "avg_frame_rate" => { // "30/1" (or "0/0" for VFR/unknown → keep the 30 fps default). if let Some((n, d)) = v.trim().split_once('/') { - let (n, d) = (n.parse::().unwrap_or(0.0), d.parse::().unwrap_or(0.0)); + let (n, d) = ( + n.parse::().unwrap_or(0.0), + d.parse::().unwrap_or(0.0), + ); if n > 0.0 && d > 0.0 { fps = n / d; } @@ -2159,13 +2164,7 @@ fn grab_one_frame(path: &Path, t: f32, frame_len: usize) -> Option> { /// recent** (so fast dragging skips stale targets), grabs that frame, and sends /// it back. One ffmpeg at a time; exits when the request channel closes. #[cfg(not(target_arch = "wasm32"))] -fn scrub_worker( - rx: mpsc::Receiver, - tx: mpsc::Sender>, - path: PathBuf, - w: u32, - h: u32, -) { +fn scrub_worker(rx: mpsc::Receiver, tx: mpsc::Sender>, path: PathBuf, w: u32, h: u32) { let frame_len = (w as usize) * (h as usize) * 4; while let Ok(mut t) = rx.recv() { while let Ok(newer) = rx.try_recv() { @@ -2348,7 +2347,8 @@ fn poll_loader_channel( game_data.loading = false; // An empty snapshot means the decode failed (no game source, or a // bare pak) — leave `loaded=false` so re-opening retries. - let empty = snap.weapons.is_empty() && snap.craft.is_empty() && snap.missions.is_empty(); + let empty = + snap.weapons.is_empty() && snap.craft.is_empty() && snap.missions.is_empty(); if empty { game_data.loaded = false; } else { @@ -2560,7 +2560,9 @@ fn pick_albedo_index(model_name: &str, tex_names: &[String]) -> Option { return Some(*i); } // 3. Stem mentioned anywhere in a colour map's name. - cols.iter().find(|(_, n)| n.contains(&stem)).map(|(i, _)| *i) + cols.iter() + .find(|(_, n)| n.contains(&stem)) + .map(|(i, _)| *i) } /// Per-vertex smooth normals = normalized sum of incident face normals. @@ -2582,7 +2584,13 @@ fn compute_smooth_normals(positions: &[[f32; 3]], indices: &[u32]) -> Vec<[f32; } acc.into_iter() .map(|n| n.normalize_or_zero().to_array()) - .map(|n| if n == [0.0, 0.0, 0.0] { [0.0, 1.0, 0.0] } else { n }) + .map(|n| { + if n == [0.0, 0.0, 0.0] { + [0.0, 1.0, 0.0] + } else { + n + } + }) .collect() } @@ -2642,11 +2650,7 @@ fn apply_loaded_texture( *model_preview = ModelPreview::default(); // Populate FileInfo for the status / info panel. - file_info.name = path - .split('/') - .next_back() - .unwrap_or(&path) - .to_string(); + file_info.name = path.split('/').next_back().unwrap_or(&path).to_string(); file_info.size_bytes = bytes.len(); file_info.detected_format = Some(fmt); @@ -2820,7 +2824,10 @@ fn pack_metallic_roughness(spc: Option<&Image>, gls: Option<&Image>) -> Option, gls: Option<&Image>) -> Option Option { - let ti = tex_names.iter().position(|t| t.eq_ignore_ascii_case(name))?; + let ti = tex_names + .iter() + .position(|t| t.eq_ignore_ascii_case(name))?; load_idx(ti) }; let slot_for_ti = |ti: usize, @@ -2927,12 +2940,22 @@ fn prepare_models_impl( .and_then(|n| n.strip_suffix("_col").or(Some(n.as_str()))) .unwrap_or("") .to_string(); - let spc = (!base.is_empty()).then(|| load_named(&format!("{base}_spc"))).flatten(); - let gls = (!base.is_empty()).then(|| load_named(&format!("{base}_gls"))).flatten(); - let emissive = (!base.is_empty()).then(|| load_named(&format!("{base}_lum"))).flatten(); + let spc = (!base.is_empty()) + .then(|| load_named(&format!("{base}_spc"))) + .flatten(); + let gls = (!base.is_empty()) + .then(|| load_named(&format!("{base}_gls"))) + .flatten(); + let emissive = (!base.is_empty()) + .then(|| load_named(&format!("{base}_lum"))) + .flatten(); let metallic_roughness = pack_metallic_roughness(spc.as_ref(), gls.as_ref()); let slot = materials.len(); - materials.push(PreparedMat { albedo, metallic_roughness, emissive }); + materials.push(PreparedMat { + albedo, + metallic_roughness, + emissive, + }); tex_slot.insert(ti, slot); slot }; @@ -3043,12 +3066,18 @@ fn prepare_models_impl( sylpheed_formats::mesh::material_groups(bytes, &model.name, sub.indices.len()) }; let slices: Vec<(usize, usize, Option)> = if groups.is_empty() { - vec![(0, sub.indices.len(), pick_albedo_index(&model.name, &tex_names))] + vec![( + 0, + sub.indices.len(), + pick_albedo_index(&model.name, &tex_names), + )] } else { groups .iter() .map(|g| { - let ti = tex_names.iter().position(|t| t.eq_ignore_ascii_case(&g.albedo)); + let ti = tex_names + .iter() + .position(|t| t.eq_ignore_ascii_case(&g.albedo)); (g.idx_offset, g.idx_count, ti) }) .collect() @@ -3146,7 +3175,10 @@ fn prepare_models_impl( if buf.pos.is_empty() || buf.idx.is_empty() { continue; } - let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default()); + let mut mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ); mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, buf.pos); mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, buf.nrm); mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, buf.uv); @@ -3412,7 +3444,6 @@ fn apply_prepared_xpr( } } - /// Consumes a staged pack, freeing the previous texture/text previews and /// populating `PakView` for the master-detail browser. // Bevy system: every parameter is a `Res`/`ResMut`/`EventWriter` the @@ -3706,7 +3737,9 @@ fn handle_subtitle_request( &source, &format!("dat/GP_MAIN_GAME_{}.pak", lang.game_code()), )?; - Ok(sylpheed_formats::movie_subtitle::load(&movie, &lang_pak, &text_pak)) + Ok(sylpheed_formats::movie_subtitle::load( + &movie, &lang_pak, &text_pak, + )) })() .unwrap_or_default(); let _ = sender.send(IsoLoaderMsg::SubtitlesLoaded { @@ -3732,9 +3765,7 @@ fn read_source_file(source: &SourceKind, path: &str) -> Result, String> reader.read_file(&path).await.map_err(|e| e.to_string()) }) } - SourceKind::Directory(root) => { - std::fs::read(root.join(path)).map_err(|e| e.to_string()) - } + SourceKind::Directory(root) => std::fs::read(root.join(path)).map_err(|e| e.to_string()), SourceKind::None => Err("no source open".to_string()), } } @@ -3762,7 +3793,9 @@ fn read_segment_range( match source { SourceKind::Directory(root) => { let path = root.join(&seg); - let Ok(meta) = std::fs::metadata(&path) else { break }; + let Ok(meta) = std::fs::metadata(&path) else { + break; + }; let seg_len = meta.len(); if skip >= seg_len { skip -= seg_len; @@ -3779,7 +3812,9 @@ fn read_segment_range( } SourceKind::Iso(_) => { // No partial read on ISO; read the whole segment and slice. - let Ok(bytes) = read_source_file(source, &seg) else { break }; + let Ok(bytes) = read_source_file(source, &seg) else { + break; + }; let seg_len = bytes.len() as u64; if skip >= seg_len { skip -= seg_len; @@ -3871,7 +3906,9 @@ fn decode_riffs_to_wav( }; let filter = format!( "{}concat=n={}:v=0:a=1{}[a]", - (0..inputs.len()).map(|i| format!("[{i}:a]")).collect::(), + (0..inputs.len()) + .map(|i| format!("[{i}:a]")) + .collect::(), inputs.len(), if mono { ",pan=mono|c0=c0" } else { "" }, ); @@ -3888,12 +3925,17 @@ fn decode_riffs_to_wav( let _ = std::fs::remove_file(p); } match status { - Ok(o) if o.status.success() && out_path.metadata().map(|m| m.len() > 44).unwrap_or(false) => { + Ok(o) + if o.status.success() && out_path.metadata().map(|m| m.len() > 44).unwrap_or(false) => + { Ok(out_path) } Ok(o) => Err(format!( "ffmpeg voice decode failed: {}", - String::from_utf8_lossy(&o.stderr).lines().last().unwrap_or("") + String::from_utf8_lossy(&o.stderr) + .lines() + .last() + .unwrap_or("") )), Err(e) => Err(format!("spawning ffmpeg: {e}")), } @@ -3941,7 +3983,8 @@ fn handle_voice_request( let wav = sylpheed_formats::media::resolve_movie_voice_region(&source, &movie, lang) .and_then(|(s, e)| decode_voice_region(&source, s, e, duration).ok()) .or_else(|| { - let clip = sylpheed_formats::media::resolve_movie_voice_clip(&source, &movie, lang)?; + let clip = + sylpheed_formats::media::resolve_movie_voice_clip(&source, &movie, lang)?; if clip.contains("\\Movie\\") { return None; } @@ -4030,15 +4073,18 @@ fn handle_audio_request( // stream (same resolution as playback), falling back to a non-`\Movie\` // clip (hokyu `\etc\`); for a named library clip, decode it directly. let wav = match movie { - Some((m, lang)) => sylpheed_formats::media::resolve_movie_voice_region(&source, &m, lang) - .and_then(|(s, e)| decode_voice_region(&source, s, e, f32::INFINITY).ok()) - .or_else(|| { - let c = sylpheed_formats::media::resolve_movie_voice_clip(&source, &m, lang)?; - if c.contains("\\Movie\\") { - return None; - } - decode_sound_clip(&source, &c, f32::INFINITY, true).ok() - }), + Some((m, lang)) => { + sylpheed_formats::media::resolve_movie_voice_region(&source, &m, lang) + .and_then(|(s, e)| decode_voice_region(&source, s, e, f32::INFINITY).ok()) + .or_else(|| { + let c = + sylpheed_formats::media::resolve_movie_voice_clip(&source, &m, lang)?; + if c.contains("\\Movie\\") { + return None; + } + decode_sound_clip(&source, &c, f32::INFINITY, true).ok() + }) + } None => decode_sound_clip(&source, &clip, f32::INFINITY, mono).ok(), } .and_then(|p| wav_duration(&p).map(|d| (p, d))); @@ -4089,7 +4135,11 @@ fn build_game_snapshot(source: &SourceKind) -> Option { let id = c.id?; let short = id.trim_start_matches("Character").to_string(); let name = text.character_name(&short).unwrap_or(&short).to_string(); - Some(CharRow { name, faction: c.faction.unwrap_or_default(), faces: c.faces.len() }) + Some(CharRow { + name, + faction: c.faction.unwrap_or_default(), + faces: c.faces.len(), + }) }) .collect(); characters.sort_by_key(|a| (a.faction.clone(), a.name.clone())); @@ -4097,8 +4147,16 @@ fn build_game_snapshot(source: &SourceKind) -> Option { // Combat rosters, keyed by stage where the table self-identifies. let rosters = gd::load_unit_rosters(&main); let stat_hp = |id: &str| -> Option { - craft.iter().find(|u| u.id.as_deref() == Some(id)).and_then(|u| u.hp) - .or_else(|| vessels.iter().find(|v| v.id.as_deref() == Some(id)).and_then(|v| v.hp)) + craft + .iter() + .find(|u| u.id.as_deref() == Some(id)) + .and_then(|u| u.hp) + .or_else(|| { + vessels + .iter() + .find(|v| v.id.as_deref() == Some(id)) + .and_then(|v| v.hp) + }) }; let mut missions: Vec = Vec::new(); for s in gd::load_stages(&main) { @@ -4106,21 +4164,34 @@ fn build_game_snapshot(source: &SourceKind) -> Option { if s.id.len() != 3 || !s.id.starts_with('S') || s.id[1..].parse::().is_err() { continue; } - let objectives: Vec = - (1..=s.phase_count()).flat_map(|p| text.objectives(&s.id, p)).map(str::to_string).collect(); - let lose: Vec = - (1..=s.phase_count()).flat_map(|p| text.lose_conditions(&s.id, p)).map(str::to_string).collect(); + let objectives: Vec = (1..=s.phase_count()) + .flat_map(|p| text.objectives(&s.id, p)) + .map(str::to_string) + .collect(); + let lose: Vec = (1..=s.phase_count()) + .flat_map(|p| text.lose_conditions(&s.id, p)) + .map(str::to_string) + .collect(); let enemies: Vec = rosters .iter() .find(|r| r.stage.as_deref() == Some(s.id.as_str())) .map(|r| { - r.units.iter().filter(|u| u.contains("ADAN")).map(|u| { - let name = u.trim_start_matches("UN_").splitn(3, '_').nth(2).unwrap_or(u).replace('_', " "); - match stat_hp(u) { - Some(h) => format!("{name} ({h:.0} HP)"), - None => name, - } - }).collect() + r.units + .iter() + .filter(|u| u.contains("ADAN")) + .map(|u| { + let name = u + .trim_start_matches("UN_") + .splitn(3, '_') + .nth(2) + .unwrap_or(u) + .replace('_', " "); + match stat_hp(u) { + Some(h) => format!("{name} ({h:.0} HP)"), + None => name, + } + }) + .collect() }) .unwrap_or_default(); missions.push(MissionRow { @@ -4141,7 +4212,12 @@ fn build_game_snapshot(source: &SourceKind) -> Option { let mut seen = std::collections::BTreeSet::new(); let mut flights = Vec::new(); for r in gd::load_pilot_rosters(&h) { - let key: String = r.pilots().iter().map(|(c, p)| format!("{c}:{p}")).collect::>().join(","); + let key: String = r + .pilots() + .iter() + .map(|(c, p)| format!("{c}:{p}")) + .collect::>() + .join(","); if seen.insert(key) { flights.push(r.pilots()); } @@ -4150,7 +4226,15 @@ fn build_game_snapshot(source: &SourceKind) -> Option { }) .unwrap_or_default(); - Some(GameSnapshot { weapons, craft, vessels, characters, missions, arsenal, flights }) + Some(GameSnapshot { + weapons, + craft, + vessels, + characters, + missions, + arsenal, + flights, + }) } // ── UI screens ─────────────────────────────────────────────────────────────── @@ -4207,11 +4291,7 @@ fn handle_screen_catalog_request( /// Open every `GP_*.pak` and record which entries parse as a screen build. #[cfg(not(target_arch = "wasm32"))] -fn build_screen_catalog( - source: &SourceKind, - files: &[String], - fragments: bool, -) -> Vec { +fn build_screen_catalog(source: &SourceKind, files: &[String], fragments: bool) -> Vec { use sylpheed_formats::ui_layout; let mut out = Vec::new(); let mut candidates: Vec<&String> = files @@ -4571,9 +4651,9 @@ fn handle_ship_catalog_request( /// and join each to its [`sylpheed_formats::game_data::Vessel`] stats. #[cfg(not(target_arch = "wasm32"))] fn build_ship_catalog(source: &SourceKind) -> Vec { + use std::collections::BTreeMap; use sylpheed_formats::game_data::{self as gd, Vessel}; use sylpheed_formats::ship::ships_in_container; - use std::collections::BTreeMap; // Vessel stats keyed by family id (`rou_e105` → `e105`). let vessels: Vec = read_pak_archive_blocking(source, "dat/GP_MAIN_GAME_E.pak") @@ -4664,8 +4744,12 @@ fn handle_ship_render_request( let Some(req) = events.read().last() else { return; }; - let (file, id, external, label) = - (req.file.clone(), req.id.clone(), req.external, req.label.clone()); + let (file, id, external, label) = ( + req.file.clone(), + req.id.clone(), + req.external, + req.label.clone(), + ); // Supersede any in-flight XPR/stage decode, exactly like a file selection. xpr_load.generation = xpr_load.generation.wrapping_add(1); @@ -4774,7 +4858,10 @@ fn build_ship_model( // capture-verified). Draw a simple cone at each frame so the assembled ship // reads correctly. if external { - for (i, f) in sylpheed_formats::ship::exhaust_frames(&bytes, id).iter().enumerate() { + for (i, f) in sylpheed_formats::ship::exhaust_frames(&bytes, id) + .iter() + .enumerate() + { let mut cone = exhaust_cone_mesh(); for v in &mut cone.positions { *v = f.apply(*v); @@ -5107,9 +5194,17 @@ fn advance_video_playback( if let Some(wav) = voice.pending_wav.take() { if let Some(handle) = &inner.stream_handle { let vol = if voice.enabled { video.volume } else { 0.0 }; - info!("[voice] APPLY sink movie={:?} wav={wav:?} pos={}", voice.movie, video.position); - inner.voice_sink = - build_audio_sink(handle, &wav, video.position, vol, video.playing && !video.scrubbing); + info!( + "[voice] APPLY sink movie={:?} wav={wav:?} pos={}", + voice.movie, video.position + ); + inner.voice_sink = build_audio_sink( + handle, + &wav, + video.position, + vol, + video.playing && !video.scrubbing, + ); inner.voice_applied_volume = vol; inner.voice_wav = Some(wav); } @@ -5120,7 +5215,13 @@ fn advance_video_playback( // until the restarted stream produces its first frame — no black flash. ── if let Some(target) = video.seek_request.take() { let target = target.clamp(0.0, video.duration); - match spawn_video_decoder(&inner.temp_video, inner.width, inner.height, inner.fps, target) { + match spawn_video_decoder( + &inner.temp_video, + inner.width, + inner.height, + inner.fps, + target, + ) { Ok((new_child, new_rx)) => { let _ = inner.child.kill(); let _ = inner.child.wait(); @@ -5265,7 +5366,11 @@ mod albedo_match_tests { use super::*; fn cue(start: f32, end: Option, text: &str) -> sylpheed_formats::SubCue { - sylpheed_formats::SubCue { start, end, text: text.into() } + sylpheed_formats::SubCue { + start, + end, + text: text.into(), + } } #[test] @@ -5273,12 +5378,19 @@ mod albedo_match_tests { // Single start-only cue (a resupply line): shown from its start for an // estimated reading span, then hidden — NOT for the whole video. let subs = MovieSubtitles { - cues: vec![cue(0.0, None, "Resupply complete. You are cleared for take-off!")], + cues: vec![cue( + 0.0, + None, + "Resupply complete. You are cleared for take-off!", + )], ..Default::default() }; assert!(subs.active_at(0.0).is_some()); assert!(subs.active_at(3.0).is_some()); - assert!(subs.active_at(30.0).is_none(), "must not linger the whole video"); + assert!( + subs.active_at(30.0).is_none(), + "must not linger the whole video" + ); } #[test] @@ -5286,12 +5398,21 @@ mod albedo_match_tests { // Two far-apart radio lines: each visible near its own start, with a gap // in between (the first doesn't stretch to the second). let subs = MovieSubtitles { - cues: vec![cue(0.5, None, "We did it!"), cue(19.3, None, "Yeah, but Brandon...")], + cues: vec![ + cue(0.5, None, "We did it!"), + cue(19.3, None, "Yeah, but Brandon..."), + ], ..Default::default() }; - assert_eq!(subs.active_at(1.0).map(|c| c.text.as_str()), Some("We did it!")); + assert_eq!( + subs.active_at(1.0).map(|c| c.text.as_str()), + Some("We did it!") + ); assert!(subs.active_at(12.0).is_none(), "gap between the two lines"); - assert_eq!(subs.active_at(20.0).map(|c| c.text.as_str()), Some("Yeah, but Brandon...")); + assert_eq!( + subs.active_at(20.0).map(|c| c.text.as_str()), + Some("Yeah, but Brandon...") + ); } #[test] @@ -5432,6 +5553,9 @@ mod font_sample_tests { .expect("real TrueType font should rasterize a sample"); assert!(img.width > 0 && img.height > 0); assert_eq!(img.rgba.len(), (img.width * img.height * 4) as usize); - assert!(img.rgba.iter().skip(3).step_by(4).any(|&a| a > 0), "some ink"); + assert!( + img.rgba.iter().skip(3).step_by(4).any(|&a| a > 0), + "some ink" + ); } } diff --git a/crates/sylpheed-viewer/src/lib.rs b/crates/sylpheed-viewer/src/lib.rs index 48607390..36956791 100644 --- a/crates/sylpheed-viewer/src/lib.rs +++ b/crates/sylpheed-viewer/src/lib.rs @@ -46,16 +46,14 @@ impl Default for ViewerState { pub fn run() { let mut app = App::new(); - app.add_plugins( - DefaultPlugins.set(WindowPlugin { - primary_window: Some(Window { - title: "Project Sylpheed: Arc of Deception — Asset Viewer".into(), - resolution: (1280.0_f32, 720.0_f32).into(), - ..default() - }), + app.add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "Project Sylpheed: Arc of Deception — Asset Viewer".into(), + resolution: (1280.0_f32, 720.0_f32).into(), ..default() }), - ); + ..default() + })); app.add_plugins(EguiPlugin); app.add_plugins(asset_loader::SylpheedAssetPlugin); @@ -81,7 +79,7 @@ fn setup_scene(mut commands: Commands) { // camera always has some light on the visible side — the single front light // left the backside unreadable. let lights = [ - (Vec3::new(1.0, 2.0, 1.5), 10_000.0, true), // key: front-top-right + (Vec3::new(1.0, 2.0, 1.5), 10_000.0, true), // key: front-top-right (Vec3::new(-2.0, 1.0, 0.5), 4_500.0, false), // fill: left (Vec3::new(0.5, 0.8, -2.0), 6_000.0, false), // rim: behind (Vec3::new(0.0, -1.5, 0.5), 2_500.0, false), // underside fill diff --git a/crates/sylpheed-viewer/src/ui.rs b/crates/sylpheed-viewer/src/ui.rs index 17f74919..e9ec5472 100644 --- a/crates/sylpheed-viewer/src/ui.rs +++ b/crates/sylpheed-viewer/src/ui.rs @@ -13,9 +13,9 @@ use crate::iso_loader::{ AudioLibrary, AudioPreview, CutsceneBrowser, FileInfo, FileSelected, GameCategory, GameData, ImageRgba, IsoLoaderSystemSet, IsoState, ModelPreview, MovieSubtitles, MovieVoice, PakContent, PakView, RequestAudio, RequestAudioLibrary, RequestCutscenes, RequestGameData, RequestOpenDir, - RequestOpenIso, RequestSaveOpen, RequestScreenCatalog, RequestScreenCompose, RequestShipCatalog, - RequestShipRender, RequestSubtitles, SaveBrowser, ScreenBrowser, ShipBrowser, SkyboxPreview, - TextPreview, TexturePreview, VideoPreview, + RequestOpenIso, RequestSaveOpen, RequestScreenCatalog, RequestScreenCompose, + RequestShipCatalog, RequestShipRender, RequestSubtitles, SaveBrowser, ScreenBrowser, + ShipBrowser, SkyboxPreview, TextPreview, TexturePreview, VideoPreview, }; use crate::ViewerState; use sylpheed_formats::SubLang; @@ -203,10 +203,7 @@ fn draw_viewer_ui( } } #[cfg(target_arch = "wasm32")] - ui.colored_label( - egui::Color32::GRAY, - "File loading not available in browser", - ); + ui.colored_label(egui::Color32::GRAY, "File loading not available in browser"); }); ui.menu_button("View", |ui| { @@ -306,10 +303,8 @@ fn draw_viewer_ui( // headings instead of the "?" a directory split gave them. use std::collections::BTreeMap; type Group<'a> = BTreeMap<&'a str, Vec<&'a sylpheed_formats::slb::AudioEntry>>; - let mut groups: BTreeMap< - sylpheed_formats::slb::AudioCategory, - Group<'_>, - > = BTreeMap::new(); + let mut groups: BTreeMap> = + BTreeMap::new(); for e in &audio_lib.entries { if !f.is_empty() && !e.clip.name.to_lowercase().contains(&f) { continue; @@ -323,68 +318,62 @@ fn draw_viewer_ui( } let shown: usize = groups.values().flat_map(|s| s.values()).map(Vec::len).sum(); ui.label( - egui::RichText::new(format!( - "{shown} / {} banks", - audio_lib.entries.len() - )) - .weak() - .small(), + egui::RichText::new(format!("{shown} / {} banks", audio_lib.entries.len())) + .weak() + .small(), ); ui.separator(); let filtering = !f.is_empty(); egui::ScrollArea::vertical().show(ui, |ui| { for (cat, speakers) in &groups { let ctotal: usize = speakers.values().map(Vec::len).sum(); - egui::CollapsingHeader::new(format!( - "{} ({ctotal})", - cat.label() - )) - .id_salt(("acat", *cat)) - .default_open(filtering || ctotal <= 40) - .show(ui, |ui| { - for (speaker, entries) in speakers { - // A single-bank group (Static.slb) would be a - // pointless nested header. - let flat = speakers.len() == 1 || entries.len() == 1; - let mut row = |ui: &mut egui::Ui| { - for e in entries { - ui.horizontal(|ui| { - if ui - .button("▶") - .on_hover_text(&e.clip.name) - .clicked() - { - audio.generation = - audio.generation.wrapping_add(1); - audio.loading = true; - audio.active = true; - audio.error = None; - audio.name = e.clip.display.clone(); - events.audio.send(RequestAudio { - clip: e.clip.name.clone(), - display: e.clip.display.clone(), - movie: None, - mono: e.category.is_voice(), - generation: audio.generation, - }); - } - ui.label(&e.clip.display); - }); + egui::CollapsingHeader::new(format!("{} ({ctotal})", cat.label())) + .id_salt(("acat", *cat)) + .default_open(filtering || ctotal <= 40) + .show(ui, |ui| { + for (speaker, entries) in speakers { + // A single-bank group (Static.slb) would be a + // pointless nested header. + let flat = speakers.len() == 1 || entries.len() == 1; + let mut row = |ui: &mut egui::Ui| { + for e in entries { + ui.horizontal(|ui| { + if ui + .button("▶") + .on_hover_text(&e.clip.name) + .clicked() + { + audio.generation = + audio.generation.wrapping_add(1); + audio.loading = true; + audio.active = true; + audio.error = None; + audio.name = e.clip.display.clone(); + events.audio.send(RequestAudio { + clip: e.clip.name.clone(), + display: e.clip.display.clone(), + movie: None, + mono: e.category.is_voice(), + generation: audio.generation, + }); + } + ui.label(&e.clip.display); + }); + } + }; + if flat { + row(ui); + } else { + egui::CollapsingHeader::new(format!( + "{speaker} ({})", + entries.len() + )) + .id_salt(("aspk", *cat, *speaker)) + .default_open(filtering || entries.len() <= 6) + .show(ui, &mut row); } - }; - if flat { - row(ui); - } else { - egui::CollapsingHeader::new(format!( - "{speaker} ({})", - entries.len() - )) - .id_salt(("aspk", *cat, *speaker)) - .default_open(filtering || entries.len() <= 6) - .show(ui, &mut row); } - } - }); + }); } }); } @@ -413,10 +402,7 @@ fn draw_viewer_ui( ui.spinner(); ui.label("Loading…"); } else if let Some(err) = &iso_state.error { - ui.colored_label( - egui::Color32::RED, - format!("⚠ Error:\n{}", err), - ); + ui.colored_label(egui::Color32::RED, format!("⚠ Error:\n{}", err)); } else if browser.files.is_empty() { ui.colored_label( egui::Color32::YELLOW, @@ -656,39 +642,92 @@ fn draw_viewer_ui( let done = egui::Color32::LIGHT_GREEN; let partial = egui::Color32::from_rgb(150, 210, 255); let todo = egui::Color32::YELLOW; - egui::Grid::new("re_notes").striped(true).num_columns(3).show(ui, |ui| { - ui.strong("Format"); - ui.strong("Status"); - ui.strong("Notes"); - ui.end_row(); - - let row = |ui: &mut egui::Ui, fmt: &str, c, status: &str, notes: &str| { - ui.label(fmt); - ui.colored_label(c, status); - ui.label(notes); + egui::Grid::new("re_notes") + .striped(true) + .num_columns(3) + .show(ui, |ui| { + ui.strong("Format"); + ui.strong("Status"); + ui.strong("Notes"); ui.end_row(); - }; - row(ui, "IPFB .pak/.pNN", done, "✓ done", - "Archive TOC + name-hash recovered (paths resolved)"); - row(ui, "IDXD objects", partial, "◑ most", - "Reflective ship/weapon/effect defs; some fields defaulted in-code"); - row(ui, "XBG7 mesh", done, "✓ done", - "3D models — position + normal + UV from vertex decl"); - row(ui, "XPR2 texture", done, "✓ done", - "A8R8G8B8 / DXT1/3/5, de-tiled; 2D + cubemaps"); - row(ui, "T8aD / RATC / LSTA", partial, "◑ most", - "2D UI textures, bundles, sprite lists (colours unverified)"); - row(ui, "IXUD subtitles", done, "✓ done", - "Localized movie subtitle cue tables"); - row(ui, "Font (OTF/TTF/ttcf)", done, "✓ done", - "Embedded subtitle fonts — metadata + glyph sample"); - row(ui, "WMV cutscene", done, "✓ done", - "wmv3 / wmapro playback with transport + subtitles"); - row(ui, "XISO disc", done, "✓ done", - "XDVDFS filesystem — direct ISO browsing"); - row(ui, "XMA audio", todo, "⏳ wip", - "Xbox 360 XMA → PCM (scaffold)"); - }); + + let row = |ui: &mut egui::Ui, fmt: &str, c, status: &str, notes: &str| { + ui.label(fmt); + ui.colored_label(c, status); + ui.label(notes); + ui.end_row(); + }; + row( + ui, + "IPFB .pak/.pNN", + done, + "✓ done", + "Archive TOC + name-hash recovered (paths resolved)", + ); + row( + ui, + "IDXD objects", + partial, + "◑ most", + "Reflective ship/weapon/effect defs; some fields defaulted in-code", + ); + row( + ui, + "XBG7 mesh", + done, + "✓ done", + "3D models — position + normal + UV from vertex decl", + ); + row( + ui, + "XPR2 texture", + done, + "✓ done", + "A8R8G8B8 / DXT1/3/5, de-tiled; 2D + cubemaps", + ); + row( + ui, + "T8aD / RATC / LSTA", + partial, + "◑ most", + "2D UI textures, bundles, sprite lists (colours unverified)", + ); + row( + ui, + "IXUD subtitles", + done, + "✓ done", + "Localized movie subtitle cue tables", + ); + row( + ui, + "Font (OTF/TTF/ttcf)", + done, + "✓ done", + "Embedded subtitle fonts — metadata + glyph sample", + ); + row( + ui, + "WMV cutscene", + done, + "✓ done", + "wmv3 / wmapro playback with transport + subtitles", + ); + row( + ui, + "XISO disc", + done, + "✓ done", + "XDVDFS filesystem — direct ISO browsing", + ); + row( + ui, + "XMA audio", + todo, + "⏳ wip", + "Xbox 360 XMA → PCM (scaffold)", + ); + }); }); }); } @@ -751,27 +790,29 @@ fn draw_pak_browser(ui: &mut egui::Ui, pak: &mut PakView) { // Right — detail of the selected entry, dispatched on decoded content. egui::ScrollArea::vertical() .id_salt("pak_detail") - .show(&mut cols[1], |ui| match selected.and_then(|i| rows.get(i)) { - None => { - ui.label("Select an entry to inspect."); + .show(&mut cols[1], |ui| { + match selected.and_then(|i| rows.get(i)) { + None => { + ui.label("Select an entry to inspect."); + } + Some(row) => match &row.content { + PakContent::Subtitle(sub) => draw_subtitle_detail(ui, row, sub), + PakContent::Font { info, sample } => { + draw_font_detail(ui, row, info, sample.as_ref(), img_tex) + } + PakContent::Png(img) => draw_png_detail(ui, row, img, img_tex), + PakContent::T8ad(img) => draw_t8ad_detail(ui, row, img, img_tex), + PakContent::Lsta(frames) => draw_lsta_detail(ui, row, frames, img_tex), + PakContent::Ratc(children, ui_screen) => { + draw_ratc_detail(ui, row, children, ui_screen.as_ref(), img_tex) + } + PakContent::Text { text, encoding } => { + draw_text_detail(ui, row, text, encoding) + } + PakContent::Audio(info) => draw_audio_detail(ui, row, info), + PakContent::None => draw_idxd_detail(ui, row), + }, } - Some(row) => match &row.content { - PakContent::Subtitle(sub) => draw_subtitle_detail(ui, row, sub), - PakContent::Font { info, sample } => { - draw_font_detail(ui, row, info, sample.as_ref(), img_tex) - } - PakContent::Png(img) => draw_png_detail(ui, row, img, img_tex), - PakContent::T8ad(img) => draw_t8ad_detail(ui, row, img, img_tex), - PakContent::Lsta(frames) => draw_lsta_detail(ui, row, frames, img_tex), - PakContent::Ratc(children, ui_screen) => { - draw_ratc_detail(ui, row, children, ui_screen.as_ref(), img_tex) - } - PakContent::Text { text, encoding } => { - draw_text_detail(ui, row, text, encoding) - } - PakContent::Audio(info) => draw_audio_detail(ui, row, info), - PakContent::None => draw_idxd_detail(ui, row), - }, }); }); @@ -794,7 +835,11 @@ fn row_kind(row: &crate::iso_loader::PakRow) -> String { PakContent::T8ad(img) => format!("T8aD {}×{}", img.width, img.height), PakContent::Lsta(f) => format!("LSTA · {} sprite(s)", f.len()), PakContent::Ratc(c, screen) => { - let s = if screen.is_some() { " · UI screen" } else { "" }; + let s = if screen.is_some() { + " · UI screen" + } else { + "" + }; format!("RATC · {} item(s){s}", c.len()) } PakContent::Text { .. } => "text".into(), @@ -826,7 +871,10 @@ fn draw_idxd_detail(ui: &mut egui::Ui, row: &crate::iso_loader::PakRow) { if let Some(name) = &row.name { ui.colored_label(egui::Color32::LIGHT_GREEN, format!("📄 {name}")); } - ui.label(format!("schema 0x{:08x} count {}", d.schema_hash, d.count)); + ui.label(format!( + "schema 0x{:08x} count {}", + d.schema_hash, d.count + )); ui.label(format!("{} bytes hash {:08x}", row.size, row.hash)); ui.separator(); if d.fields.is_empty() { @@ -1099,7 +1147,11 @@ fn draw_ratc_detail( img_i += 1; } ui.vertical(|ui| { - let name = if c.name.is_empty() { "(unnamed)" } else { &c.name }; + let name = if c.name.is_empty() { + "(unnamed)" + } else { + &c.name + }; ui.strong(name); ui.weak(format!("{} · {} bytes", c.kind, c.size)); }); @@ -1151,30 +1203,41 @@ fn draw_audio_detail( ui.separator(); let dash = "—".to_string(); - egui::Grid::new("audio_meta").striped(true).num_columns(2).show(ui, |ui| { - ui.strong("Codec"); - ui.label(info.codec.label()); - ui.end_row(); - ui.strong("Channels"); - ui.label(info.channels.map(|c| c.to_string()).unwrap_or_else(|| dash.clone())); - ui.end_row(); - ui.strong("Sample rate"); - ui.label(info.sample_rate.map(|r| format!("{r} Hz")).unwrap_or_else(|| dash.clone())); - ui.end_row(); - if let Some(d) = info.duration_secs { - ui.strong("Duration"); - ui.label(format!("{d:.2} s")); + egui::Grid::new("audio_meta") + .striped(true) + .num_columns(2) + .show(ui, |ui| { + ui.strong("Codec"); + ui.label(info.codec.label()); ui.end_row(); - } - if let Some(p) = info.xma_packets { - ui.strong("XMA packets"); - ui.label(format!("{p} (2048 B each)")); + ui.strong("Channels"); + ui.label( + info.channels + .map(|c| c.to_string()) + .unwrap_or_else(|| dash.clone()), + ); ui.end_row(); - } - ui.strong("Size"); - ui.label(format!("{} bytes", info.size_bytes)); - ui.end_row(); - }); + ui.strong("Sample rate"); + ui.label( + info.sample_rate + .map(|r| format!("{r} Hz")) + .unwrap_or_else(|| dash.clone()), + ); + ui.end_row(); + if let Some(d) = info.duration_secs { + ui.strong("Duration"); + ui.label(format!("{d:.2} s")); + ui.end_row(); + } + if let Some(p) = info.xma_packets { + ui.strong("XMA packets"); + ui.label(format!("{p} (2048 B each)")); + ui.end_row(); + } + ui.strong("Size"); + ui.label(format!("{} bytes", info.size_bytes)); + ui.end_row(); + }); if info.codec.needs_decoder() { ui.separator(); @@ -1233,7 +1296,10 @@ fn draw_video_player( ui.horizontal(|ui| { ui.heading(&video.name); ui.separator(); - ui.label(format!("{}×{} · wmv3 / wmapro", video.width, video.height)); + ui.label(format!( + "{}×{} · wmv3 / wmapro", + video.width, video.height + )); if !video.has_audio { ui.separator(); ui.colored_label(egui::Color32::YELLOW, "no audio"); @@ -1373,12 +1439,7 @@ fn paint_caption(ui: &egui::Ui, rect: egui::Rect, text: &str) { let size = (rect.height() * 0.045).clamp(13.0, 30.0); let font = egui::FontId::proportional(size); let wrap = rect.width() * 0.9; - let galley = painter.layout( - text.to_string(), - font, - egui::Color32::WHITE, - wrap, - ); + let galley = painter.layout(text.to_string(), font, egui::Color32::WHITE, wrap); let margin = egui::vec2(10.0, 6.0); let box_size = galley.size() + margin * 2.0; let top_left = egui::pos2( @@ -1422,19 +1483,15 @@ fn draw_audio_player(ui: &mut egui::Ui, audio: &mut AudioPreview) { // Transport bar. ui.horizontal(|ui| { - if ui - .button(if audio.playing { "⏸" } else { "▶" }) - .clicked() - { + if ui.button(if audio.playing { "⏸" } else { "▶" }).clicked() { audio.playing = !audio.playing; } ui.label(fmt_time(audio.position)); let tl_width = (ui.available_width() - 170.0).max(80.0); ui.spacing_mut().slider_width = tl_width; let mut pos = audio.position; - let resp = ui.add( - egui::Slider::new(&mut pos, 0.0..=audio.duration.max(0.1)).show_value(false), - ); + let resp = + ui.add(egui::Slider::new(&mut pos, 0.0..=audio.duration.max(0.1)).show_value(false)); if resp.changed() { audio.position = pos; audio.seek_request = Some(pos); @@ -1535,7 +1592,12 @@ fn draw_game_data_ui( ui.label("Open a game source first (File ▸ Open…)."); return; } - let GameData { category, filter, snapshot, .. } = &mut *game_data; + let GameData { + category, + filter, + snapshot, + .. + } = &mut *game_data; ui.horizontal_wrapped(|ui| { for cat in GameCategory::ALL { @@ -1556,163 +1618,207 @@ fn draw_game_data_ui( let f = filter.to_lowercase(); let hit = |s: &str| f.is_empty() || s.to_lowercase().contains(&f); - egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| match *category { - GameCategory::Weapons => { - egui::Grid::new("g_weap").striped(true).num_columns(6).show(ui, |ui| { - for h in ["Weapon", "Targets", "Power", "Velocity", "Range", "Reload"] { - ui.strong(h); - } - ui.end_row(); - for w in &snapshot.weapons { - let name = w.id.as_deref().unwrap_or("?").trim_start_matches("Weapon_"); - if !hit(name) { - continue; - } - ui.label(name); - ui.label(w.target_type.as_deref().unwrap_or("·")); - ui.label(fnum(w.power)); - ui.label(fnum(w.velocity)); - ui.label(fnum(w.max_range)); - ui.label(fint(w.loading_count)); - ui.end_row(); - } - }); - } - GameCategory::Craft => { - egui::Grid::new("g_craft").striped(true).num_columns(6).show(ui, |ui| { - for h in ["Craft", "HP", "Cruise", "Accel", "Radar", "Turrets"] { - ui.strong(h); - } - ui.end_row(); - for u in &snapshot.craft { - let name = u.id.as_deref().unwrap_or("?").trim_start_matches("UN_"); - if !hit(name) { - continue; - } - ui.label(name); - ui.label(fnum(u.hp)); - ui.label(fnum(u.cruising_velocity)); - ui.label(fnum(u.acceleration)); - ui.label(fnum(u.radar_range)); - ui.label(fint(u.turret_count)); - ui.end_row(); - } - }); - } - GameCategory::Vessels => { - egui::Grid::new("g_ves").striped(true).num_columns(6).show(ui, |ui| { - for h in ["Vessel", "HP", "Length", "Turrets", "Bridges", "Shield gen"] { - ui.strong(h); - } - ui.end_row(); - for v in &snapshot.vessels { - let name = v.id.as_deref().unwrap_or("?").trim_start_matches("UN_"); - if !hit(name) { - continue; - } - ui.label(name); - ui.label(fnum(v.hp)); - ui.label(fnum(v.size_z)); - ui.label(fint(v.turret_count)); - ui.label(fint(v.bridge_count)); - ui.label(fint(v.shield_generator_count)); - ui.end_row(); - } - }); - } - GameCategory::Characters => { - egui::Grid::new("g_char").striped(true).num_columns(3).show(ui, |ui| { - for h in ["Name", "Faction", "Portraits"] { - ui.strong(h); - } - ui.end_row(); - for c in &snapshot.characters { - if !hit(&c.name) && !hit(&c.faction) { - continue; - } - ui.label(&c.name); - let col = if c.faction == "TCAF" { - egui::Color32::from_rgb(90, 160, 232) - } else if c.faction == "ADAN" { - egui::Color32::from_rgb(224, 86, 122) - } else { - egui::Color32::GRAY - }; - ui.colored_label(col, if c.faction.is_empty() { "—" } else { &c.faction }); - ui.label(c.faces.to_string()); - ui.end_row(); - } - }); - } - GameCategory::Missions => { - for m in &snapshot.missions { - if !hit(&m.id) && !hit(&m.location) && !m.objectives.iter().any(|o| hit(o)) { - continue; - } - let head = format!("{} · {} · {} phases", m.id, m.location, m.phases); - egui::CollapsingHeader::new(head).id_salt(&m.id).show(ui, |ui| { - if !m.objectives.is_empty() { - ui.strong("Objectives"); - for o in &m.objectives { - ui.label(format!("▸ {o}")); + egui::ScrollArea::vertical() + .auto_shrink([false, false]) + .show(ui, |ui| match *category { + GameCategory::Weapons => { + egui::Grid::new("g_weap") + .striped(true) + .num_columns(6) + .show(ui, |ui| { + for h in + ["Weapon", "Targets", "Power", "Velocity", "Range", "Reload"] + { + ui.strong(h); } - } - if !m.lose.is_empty() { - ui.add_space(4.0); - ui.strong("Fail conditions"); - for l in &m.lose { - ui.colored_label(egui::Color32::from_rgb(224, 86, 122), l); + ui.end_row(); + for w in &snapshot.weapons { + let name = + w.id.as_deref() + .unwrap_or("?") + .trim_start_matches("Weapon_"); + if !hit(name) { + continue; + } + ui.label(name); + ui.label(w.target_type.as_deref().unwrap_or("·")); + ui.label(fnum(w.power)); + ui.label(fnum(w.velocity)); + ui.label(fnum(w.max_range)); + ui.label(fint(w.loading_count)); + ui.end_row(); } + }); + } + GameCategory::Craft => { + egui::Grid::new("g_craft") + .striped(true) + .num_columns(6) + .show(ui, |ui| { + for h in ["Craft", "HP", "Cruise", "Accel", "Radar", "Turrets"] { + ui.strong(h); + } + ui.end_row(); + for u in &snapshot.craft { + let name = + u.id.as_deref().unwrap_or("?").trim_start_matches("UN_"); + if !hit(name) { + continue; + } + ui.label(name); + ui.label(fnum(u.hp)); + ui.label(fnum(u.cruising_velocity)); + ui.label(fnum(u.acceleration)); + ui.label(fnum(u.radar_range)); + ui.label(fint(u.turret_count)); + ui.end_row(); + } + }); + } + GameCategory::Vessels => { + egui::Grid::new("g_ves") + .striped(true) + .num_columns(6) + .show(ui, |ui| { + for h in + ["Vessel", "HP", "Length", "Turrets", "Bridges", "Shield gen"] + { + ui.strong(h); + } + ui.end_row(); + for v in &snapshot.vessels { + let name = + v.id.as_deref().unwrap_or("?").trim_start_matches("UN_"); + if !hit(name) { + continue; + } + ui.label(name); + ui.label(fnum(v.hp)); + ui.label(fnum(v.size_z)); + ui.label(fint(v.turret_count)); + ui.label(fint(v.bridge_count)); + ui.label(fint(v.shield_generator_count)); + ui.end_row(); + } + }); + } + GameCategory::Characters => { + egui::Grid::new("g_char") + .striped(true) + .num_columns(3) + .show(ui, |ui| { + for h in ["Name", "Faction", "Portraits"] { + ui.strong(h); + } + ui.end_row(); + for c in &snapshot.characters { + if !hit(&c.name) && !hit(&c.faction) { + continue; + } + ui.label(&c.name); + let col = if c.faction == "TCAF" { + egui::Color32::from_rgb(90, 160, 232) + } else if c.faction == "ADAN" { + egui::Color32::from_rgb(224, 86, 122) + } else { + egui::Color32::GRAY + }; + ui.colored_label( + col, + if c.faction.is_empty() { + "—" + } else { + &c.faction + }, + ); + ui.label(c.faces.to_string()); + ui.end_row(); + } + }); + } + GameCategory::Missions => { + for m in &snapshot.missions { + if !hit(&m.id) + && !hit(&m.location) + && !m.objectives.iter().any(|o| hit(o)) + { + continue; } - if !m.enemies.is_empty() { - ui.add_space(4.0); - ui.strong("Enemy roster"); - for e in &m.enemies { - ui.label(format!("• {e}")); + let head = + format!("{} · {} · {} phases", m.id, m.location, m.phases); + egui::CollapsingHeader::new(head) + .id_salt(&m.id) + .show(ui, |ui| { + if !m.objectives.is_empty() { + ui.strong("Objectives"); + for o in &m.objectives { + ui.label(format!("▸ {o}")); + } + } + if !m.lose.is_empty() { + ui.add_space(4.0); + ui.strong("Fail conditions"); + for l in &m.lose { + ui.colored_label( + egui::Color32::from_rgb(224, 86, 122), + l, + ); + } + } + if !m.enemies.is_empty() { + ui.add_space(4.0); + ui.strong("Enemy roster"); + for e in &m.enemies { + ui.label(format!("• {e}")); + } + } + }); + } + } + GameCategory::Arsenal => { + ui.columns(4, |cols| { + for (i, (title, list)) in [ + ("Nose", &snapshot.arsenal.nose), + ("Arm 1", &snapshot.arsenal.arm1), + ("Arm 2", &snapshot.arsenal.arm2), + ("Arm 3", &snapshot.arsenal.arm3), + ] + .into_iter() + .enumerate() + { + cols[i].strong(format!("{title} ({})", list.len())); + for w in list { + cols[i].label(w.replace('_', " ")); } } }); } - } - GameCategory::Arsenal => { - ui.columns(4, |cols| { - for (i, (title, list)) in [ - ("Nose", &snapshot.arsenal.nose), - ("Arm 1", &snapshot.arsenal.arm1), - ("Arm 2", &snapshot.arsenal.arm2), - ("Arm 3", &snapshot.arsenal.arm3), - ] - .into_iter() - .enumerate() - { - cols[i].strong(format!("{title} ({})", list.len())); - for w in list { - cols[i].label(w.replace('_', " ")); - } - } - }); - } - GameCategory::Flights => { - ui.label( - egui::RichText::new("Distinct wingman line-ups (story order)").weak().small(), - ); - ui.add_space(4.0); - for (n, lineup) in snapshot.flights.iter().enumerate() { - egui::CollapsingHeader::new(format!("Line-up {}", n + 1)) - .id_salt(n) - .default_open(n == 0) - .show(ui, |ui| { - egui::Grid::new(("g_flight", n)).striped(true).num_columns(2).show(ui, |ui| { - for (cs, pilot) in lineup { - ui.label(cs); - ui.strong(pilot); - ui.end_row(); - } + GameCategory::Flights => { + ui.label( + egui::RichText::new("Distinct wingman line-ups (story order)") + .weak() + .small(), + ); + ui.add_space(4.0); + for (n, lineup) in snapshot.flights.iter().enumerate() { + egui::CollapsingHeader::new(format!("Line-up {}", n + 1)) + .id_salt(n) + .default_open(n == 0) + .show(ui, |ui| { + egui::Grid::new(("g_flight", n)) + .striped(true) + .num_columns(2) + .show(ui, |ui| { + for (cs, pilot) in lineup { + ui.label(cs); + ui.strong(pilot); + ui.end_row(); + } + }); }); - }); + } } - } - }); + }); }); game_data.open &= open; } @@ -1759,9 +1865,12 @@ fn draw_ships_ui( // Faction filter chips. ui.horizontal(|ui| { - for (label, val) in - [("All", ""), ("ADAN", "ADAN"), ("TCAF", "TCAF"), ("Neutral", "Neutral")] - { + for (label, val) in [ + ("All", ""), + ("ADAN", "ADAN"), + ("TCAF", "TCAF"), + ("Neutral", "Neutral"), + ] { let sel = ships.faction == val; if ui.selectable_label(sel, label).clicked() { ships.faction = val.to_string(); @@ -1776,7 +1885,10 @@ fn draw_ships_ui( } }); ui.horizontal(|ui| { - ui.checkbox(&mut ships.show_external, "External parts (bridge/engines/turrets)"); + ui.checkbox( + &mut ships.show_external, + "External parts (bridge/engines/turrets)", + ); ui.label( egui::RichText::new("(bridge / shield gens / engines — approximate placement)") .weak() @@ -1785,7 +1897,14 @@ fn draw_ships_ui( }); ui.separator(); - let ShipBrowser { rows, filter, faction, selected, show_external, .. } = &mut *ships; + let ShipBrowser { + rows, + filter, + faction, + selected, + show_external, + .. + } = &mut *ships; let show_external = *show_external; let needle = filter.to_lowercase(); let mut to_render: Option<(String, String, String)> = None; @@ -1803,7 +1922,11 @@ fn draw_ships_ui( continue; } // Section header: capital ships first, then other assemblies. - let section = if row.has_vessel { "Capital ships" } else { "Other assemblies" }; + let section = if row.has_vessel { + "Capital ships" + } else { + "Other assemblies" + }; if section != last_section { ui.add_space(4.0); ui.label(egui::RichText::new(section).strong().weak()); @@ -1822,37 +1945,39 @@ fn draw_ships_ui( .id_salt(&row.id) .default_open(false) .show(ui, |ui| { - egui::Grid::new(("shipstat", &row.id)).num_columns(2).show(ui, |ui| { - ui.label("Faction"); - ui.strong(&row.faction); - ui.end_row(); - if let Some(hp) = row.hp { - ui.label("Hull HP"); - ui.strong(format!("{hp:.0}")); + egui::Grid::new(("shipstat", &row.id)) + .num_columns(2) + .show(ui, |ui| { + ui.label("Faction"); + ui.strong(&row.faction); ui.end_row(); - } - if let Some((x, y, z)) = row.size { - ui.label("Size (m)"); - ui.strong(format!("{x:.0} × {y:.0} × {z:.0}")); + if let Some(hp) = row.hp { + ui.label("Hull HP"); + ui.strong(format!("{hp:.0}")); + ui.end_row(); + } + if let Some((x, y, z)) = row.size { + ui.label("Size (m)"); + ui.strong(format!("{x:.0} × {y:.0} × {z:.0}")); + ui.end_row(); + } + if row.turrets.is_some() || row.shield_gens.is_some() { + ui.label("Hardpoints"); + ui.strong(format!( + "{} turrets · {} bridges · {} shield gens", + fint(row.turrets), + fint(row.bridges), + fint(row.shield_gens), + )); + ui.end_row(); + } + ui.label("Model parts"); + ui.strong(format!("{}", row.parts.len())); ui.end_row(); - } - if row.turrets.is_some() || row.shield_gens.is_some() { - ui.label("Hardpoints"); - ui.strong(format!( - "{} turrets · {} bridges · {} shield gens", - fint(row.turrets), - fint(row.bridges), - fint(row.shield_gens), - )); + ui.label("Appears in"); + ui.strong(row.stages.join(", ")); ui.end_row(); - } - ui.label("Model parts"); - ui.strong(format!("{}", row.parts.len())); - ui.end_row(); - ui.label("Appears in"); - ui.strong(row.stages.join(", ")); - ui.end_row(); - }); + }); let btn = egui::Button::new(if is_sel { "● Showing in 3D view" } else { @@ -1872,7 +1997,12 @@ fn draw_ships_ui( if let Some((id, file, label)) = to_render { let id2 = id.clone(); *selected = Some(id); - render.send(RequestShipRender { file, id: id2, external: show_external, label }); + render.send(RequestShipRender { + file, + id: id2, + external: show_external, + label, + }); } }); ships.open &= open; @@ -1971,8 +2101,8 @@ fn draw_screens_ui( ); } for (bi, (entry, size)) in pak.builds.iter().enumerate() { - let selected = screens.selected == Some(pi) - && screens.build == bi; + let selected = + screens.selected == Some(pi) && screens.build == bi; let label = format!( "build {bi} · entry {entry} · {} KB", size / 1024 @@ -1988,7 +2118,10 @@ fn draw_screens_ui( egui::CentralPanel::default().show_inside(ui, |ui| { ui.horizontal(|ui| { - if ui.checkbox(&mut screens.show_focus, "Focused states").changed() { + if ui + .checkbox(&mut screens.show_focus, "Focused states") + .changed() + { recompose = true; } if ui @@ -2110,8 +2243,7 @@ fn draw_screens_ui( } ui.end_row(); for el in &result.elements { - let mut vis = - !hidden.get(el.index).copied().unwrap_or(false); + let mut vis = !hidden.get(el.index).copied().unwrap_or(false); if ui.checkbox(&mut vis, "").changed() { if hidden.len() <= el.index { hidden.resize(el.index + 1, false); @@ -2126,10 +2258,7 @@ fn draw_screens_ui( egui::RichText::new(&el.name).weak() }; if ui - .selectable_label( - *selected_element == Some(el.index), - name, - ) + .selectable_label(*selected_element == Some(el.index), name) .clicked() { *selected_element = Some(el.index); @@ -2184,11 +2313,7 @@ fn draw_screens_ui( build: screens.build, // The catalog stored (pak entry index, size) per build; the // entry index is what lets the worker read one entry. - entry: pak - .builds - .get(screens.build) - .map(|(e, _)| *e) - .unwrap_or(0), + entry: pak.builds.get(screens.build).map(|(e, _)| *e).unwrap_or(0), focus: screens.show_focus, animated: screens.show_animated, black_backdrop: screens.black_backdrop, @@ -2293,109 +2418,136 @@ fn draw_save_ui( } ui.separator(); - egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| { - ui.strong("GHAD progress block"); - egui::Grid::new("g_save_ghad").striped(true).num_columns(4).show(ui, |ui| { - for h in ["", "Offset", "Field", "Value"] { - ui.strong(h); - } - ui.end_row(); - for f in &r.fields { - use sylpheed_formats::savegame::Confidence; - if !saves.show_unknown && f.confidence == Confidence::Unknown { - continue; - } - let col = confidence_color(f.confidence); - ui.colored_label(col, confidence_mark(f.confidence)); - ui.label(format!("+{}", f.offset)); - let label = ui.colored_label(col, &f.name); - if !f.note.is_empty() { - label.on_hover_text(&f.note); - } - ui.label(&f.value); - ui.end_row(); - } - }); + egui::ScrollArea::vertical() + .auto_shrink([false, false]) + .show(ui, |ui| { + ui.strong("GHAD progress block"); + egui::Grid::new("g_save_ghad") + .striped(true) + .num_columns(4) + .show(ui, |ui| { + for h in ["", "Offset", "Field", "Value"] { + ui.strong(h); + } + ui.end_row(); + for f in &r.fields { + use sylpheed_formats::savegame::Confidence; + if !saves.show_unknown && f.confidence == Confidence::Unknown { + continue; + } + let col = confidence_color(f.confidence); + ui.colored_label(col, confidence_mark(f.confidence)); + ui.label(format!("+{}", f.offset)); + let label = ui.colored_label(col, &f.name); + if !f.note.is_empty() { + label.on_hover_text(&f.note); + } + ui.label(&f.value); + ui.end_row(); + } + }); - ui.add_space(8.0); - ui.strong("Arsenal development"); - { - use sylpheed_formats::savegame::DevelopState; - let owned = r.develop.iter().filter(|(_, d)| *d == DevelopState::Developed).count(); - let ready = r.develop.iter().filter(|(_, d)| *d == DevelopState::Developable).count(); - ui.label(format!( - "{owned} developed · {ready} developable · {} locked (index space = \ + ui.add_space(8.0); + ui.strong("Arsenal development"); + { + use sylpheed_formats::savegame::DevelopState; + let owned = r + .develop + .iter() + .filter(|(_, d)| *d == DevelopState::Developed) + .count(); + let ready = r + .develop + .iter() + .filter(|(_, d)| *d == DevelopState::Developable) + .count(); + ui.label(format!( + "{owned} developed · {ready} developable · {} locked (index space = \ strings.tbl item order, cut items included)", - r.develop.len() - owned - ready - )); - ui.horizontal_wrapped(|ui| { - for (i, d) in &r.develop { - let (txt, col) = match d { - DevelopState::Developed => ("4", egui::Color32::from_rgb(120, 200, 140)), - DevelopState::Developable => ("2", egui::Color32::from_rgb(224, 196, 110)), - DevelopState::Locked => ("0", egui::Color32::DARK_GRAY), - DevelopState::Other(_) => ("?", egui::Color32::from_rgb(224, 110, 110)), - }; - ui.colored_label(col, txt).on_hover_text(format!("item {i}")); - } - }); - } - - ui.add_space(8.0); - ui.strong("Per-stage records"); - ui.label( - egui::RichText::new( - "SHAB — these are the per-stage results, NOT the UI's save slots.", - ) - .weak(), - ); - egui::Grid::new("g_save_shab").striped(true).num_columns(4).show(ui, |ui| { - for h in ["Stage", "difficulty ~", "points ?", "best time ✔"] { - ui.strong(h); + r.develop.len() - owned - ready + )); + ui.horizontal_wrapped(|ui| { + for (i, d) in &r.develop { + let (txt, col) = match d { + DevelopState::Developed => { + ("4", egui::Color32::from_rgb(120, 200, 140)) + } + DevelopState::Developable => { + ("2", egui::Color32::from_rgb(224, 196, 110)) + } + DevelopState::Locked => ("0", egui::Color32::DARK_GRAY), + DevelopState::Other(_) => { + ("?", egui::Color32::from_rgb(224, 110, 110)) + } + }; + ui.colored_label(col, txt) + .on_hover_text(format!("item {i}")); + } + }); } - ui.end_row(); - for (stage, a, b, ms) in &r.records { - ui.label(format!("{stage:02}")); - ui.label(a.to_string()); - ui.label(b.to_string()); - ui.label(sylpheed_formats::savegame::fmt_millis(*ms)); - ui.end_row(); - } - }); - ui.add_space(8.0); - ui.strong("Header summary"); - ui.label( - egui::RichText::new( - "What the in-game Details panel actually reads. A payload edit that \ + ui.add_space(8.0); + ui.strong("Per-stage records"); + ui.label( + egui::RichText::new( + "SHAB — these are the per-stage results, NOT the UI's save slots.", + ) + .weak(), + ); + egui::Grid::new("g_save_shab") + .striped(true) + .num_columns(4) + .show(ui, |ui| { + for h in ["Stage", "difficulty ~", "points ?", "best time ✔"] { + ui.strong(h); + } + ui.end_row(); + for (stage, a, b, ms) in &r.records { + ui.label(format!("{stage:02}")); + ui.label(a.to_string()); + ui.label(b.to_string()); + ui.label(sylpheed_formats::savegame::fmt_millis(*ms)); + ui.end_row(); + } + }); + + ui.add_space(8.0); + ui.strong("Header summary"); + ui.label( + egui::RichText::new( + "What the in-game Details panel actually reads. A payload edit that \ leaves these stale shows no change on the panel — which is not \ evidence that the payload field was the wrong one.", - ) - .weak(), - ); - egui::Grid::new("g_save_summary").striped(true).num_columns(4).show(ui, |ui| { - for h in ["Header", "Field", "Value", "Payload"] { - ui.strong(h); - } - ui.end_row(); - for (off, name, value, payload) in &r.summary { - ui.label(format!("{off:#04x}")); - ui.label(name); - ui.label(value.to_string()); - match payload { - Some(p) if u64::from(*value) == *p => { - ui.colored_label(egui::Color32::from_rgb(120, 200, 140), "agrees") + ) + .weak(), + ); + egui::Grid::new("g_save_summary") + .striped(true) + .num_columns(4) + .show(ui, |ui| { + for h in ["Header", "Field", "Value", "Payload"] { + ui.strong(h); } - Some(p) => ui.colored_label( - egui::Color32::from_rgb(224, 110, 110), - format!("STALE — payload has {p}"), - ), - None => ui.label("·"), - }; - ui.end_row(); - } + ui.end_row(); + for (off, name, value, payload) in &r.summary { + ui.label(format!("{off:#04x}")); + ui.label(name); + ui.label(value.to_string()); + match payload { + Some(p) if u64::from(*value) == *p => ui.colored_label( + egui::Color32::from_rgb(120, 200, 140), + "agrees", + ), + Some(p) => ui.colored_label( + egui::Color32::from_rgb(224, 110, 110), + format!("STALE — payload has {p}"), + ), + None => ui.label("·"), + }; + ui.end_row(); + } + }); }); - }); }); // A flag, not a self-send: holding both an EventReader and an EventWriter @@ -2507,8 +2659,8 @@ fn draw_cutscenes_ui( .num_columns(2) .show(ui, |ui| { for (i, r) in cut.rows.iter().enumerate() { - let hay = format!("{} {} {}", r.slot, r.movie, r.kind) - .to_lowercase(); + let hay = + format!("{} {} {}", r.slot, r.movie, r.kind).to_lowercase(); if !filter.is_empty() && !hay.contains(&filter) { continue; } @@ -2528,9 +2680,7 @@ fn draw_cutscenes_ui( { pick = Some(i); } - ui.label( - egui::RichText::new(r.kind).weak().small(), - ); + ui.label(egui::RichText::new(r.kind).weak().small()); ui.end_row(); } }); @@ -2672,18 +2822,17 @@ fn draw_cutscenes_ui( ctx.request_repaint(); } else if cut.cues.is_empty() { ui.label( - egui::RichText::new( - "no captions resolved for this movie in this language", - ) - .weak(), + egui::RichText::new("no captions resolved for this movie in this language") + .weak(), ); } else { egui::ScrollArea::vertical() .id_salt("cue_scroll") .show(ui, |ui| { - egui::Grid::new("cue_grid").num_columns(2).striped(true).show( - ui, - |ui| { + egui::Grid::new("cue_grid") + .num_columns(2) + .striped(true) + .show(ui, |ui| { for c in &cut.cues { ui.label( egui::RichText::new(fmt_time(c.start)) @@ -2694,8 +2843,7 @@ fn draw_cutscenes_ui( ui.label(&c.text); ui.end_row(); } - }, - ); + }); }); } }); @@ -2729,7 +2877,10 @@ fn draw_cutscenes_ui( // Route through the normal file-open path, so the existing video player // handles it exactly as it would from the tree. browser.loading = true; - browser.selected = browser.files.iter().position(|f| f.eq_ignore_ascii_case(&path)); + browser.selected = browser + .files + .iter() + .position(|f| f.eq_ignore_ascii_case(&path)); file_selected.send(FileSelected(path)); } cut.open = open;