From c4c914ff5902a39ab86719738eb3eebec7104792 Mon Sep 17 00:00:00 2001 From: Fabian Hamm Date: Tue, 8 Sep 2026 20:07:01 +0200 Subject: [PATCH] style: rustfmt sweep -- 774 hunks across 154 files -> 0 `cargo fmt --all -- --check` has failed on every run in this repository's history, identically on `main` and on every branch. This is #12. Mechanical: `cargo fmt --all`, nothing else. 154 files, all `.rs`, no other extension touched. `cargo check --workspace` exits 0 afterwards, so nothing changed semantically. ON THE ORDERING, WHICH WAS THE REAL QUESTION. HANDOFF-2026-09-06 section 7 warns this is the expensive fix: a whole-tree reformat before #7 and #8 return "would put a conflict in every file of 861 commits and make the reviews those items exist to enable unreadable". That is measurably too pessimistic, and it had been reasoned rather than tested. Measured here by three-way merging a rustfmt'd `main` against both unmerged branches, file by file: file/branch pairs tested 32 merges CLEAN 28 merges CONFLICTING 4 (8 conflict hunks total) sylpheed-cli/src/main.rs 1 hunk sylpheed-export/src/check.rs 1 sylpheed-export/src/screen.rs 4 sylpheed-export/src/video.rs 2 All four are against `auto/frame-blend-draw-path` only; `auto/port-p6-audio` does not conflict anywhere. The earlier framing -- 154 dirty files, 133 that cannot collide, 21 that can, the collision set carrying 147 of 774 hunks (19%) -- reproduces exactly. What it did not say is that most of the 21 still merge cleanly, because rustfmt's edits and the branches' edits rarely land on the same lines. So the cost of sweeping now is 4 files and 8 hunks for one branch, against a check that is otherwise red forever. Deliberately NOT folded into the WASM PR: 154 reformatted files would make that one unreviewable. Closes #12 --- crates/sylpheed-cli/src/main.rs | 171 ++- .../sylpheed-export/examples/bank_chunks.rs | 36 +- .../examples/bgm_size_census.rs | 17 +- .../sylpheed-export/examples/dialog_pairs.rs | 73 +- .../sylpheed-export/examples/dialog_rows.rs | 84 +- crates/sylpheed-export/examples/rat_leaf.rs | 31 +- .../examples/record_loop_control.rs | 128 +- .../examples/record_population.rs | 56 +- .../examples/static_with_cycle.rs | 41 +- .../sylpheed-export/examples/voice_chunks.rs | 15 +- crates/sylpheed-export/src/audio.rs | 104 +- crates/sylpheed-export/src/check.rs | 75 +- crates/sylpheed-export/src/main.rs | 38 +- crates/sylpheed-export/src/screen.rs | 60 +- crates/sylpheed-export/src/video.rs | 41 +- .../examples/achievements_map.rs | 4 +- crates/sylpheed-formats/examples/arsenal.rs | 21 +- crates/sylpheed-formats/examples/battle.rs | 44 +- .../sylpheed-formats/examples/bench_ships.rs | 19 +- .../sylpheed-formats/examples/better_home.rs | 38 +- .../examples/bounds_in_descriptor.rs | 19 +- crates/sylpheed-formats/examples/campaign.rs | 47 +- .../examples/capture_ib_truth.rs | 61 +- .../examples/capture_index_bytes.rs | 33 +- .../examples/capture_match.rs | 40 +- .../examples/capture_truth_scan.rs | 18 +- .../examples/capture_verify.rs | 30 +- .../examples/challenge_map.rs | 34 +- .../examples/challenge_screen.rs | 4 +- .../examples/composite_geometry.rs | 4 +- .../examples/consensus_check.rs | 14 +- .../examples/correlate_capture.rs | 57 +- .../examples/correlate_frames.rs | 99 +- .../examples/coverage_audit.rs | 17 +- .../examples/cue_unit_check.rs | 16 +- .../examples/decl_word_probe.rs | 64 +- crates/sylpheed-formats/examples/deep_map.rs | 127 +- .../examples/default_owners.rs | 18 +- .../examples/defaulted_fields.rs | 15 +- crates/sylpheed-formats/examples/desc_dump.rs | 8 +- crates/sylpheed-formats/examples/dialogue.rs | 35 +- .../sylpheed-formats/examples/dm_extract.rs | 67 +- crates/sylpheed-formats/examples/dm_roster.rs | 36 +- crates/sylpheed-formats/examples/dm_rows.rs | 93 +- crates/sylpheed-formats/examples/dossier.rs | 26 +- .../examples/edge_cap_sweep.rs | 6 +- .../examples/envelope_screen.rs | 20 +- .../examples/filter_consistency.rs | 13 +- .../sylpheed-formats/examples/find_mirror.rs | 9 +- crates/sylpheed-formats/examples/flights.rs | 26 +- .../examples/gate_histogram.rs | 11 +- .../examples/grouped_report.rs | 9 +- .../examples/hokyu_demo_resolve.rs | 169 ++- .../sylpheed-formats/examples/hokyu_final.rs | 176 ++- .../sylpheed-formats/examples/idxd_tokens.rs | 17 +- .../examples/index_hash_dump.rs | 4 +- .../examples/index_pad_check.rs | 21 +- .../examples/invert_capture.rs | 49 +- crates/sylpheed-formats/examples/iso_map.rs | 155 ++- .../sylpheed-formats/examples/live_offsets.rs | 26 +- .../sylpheed-formats/examples/miss_targets.rs | 33 +- .../sylpheed-formats/examples/mission_map.rs | 108 +- .../examples/name_resolution.rs | 37 +- crates/sylpheed-formats/examples/node_dump.rs | 18 +- .../sylpheed-formats/examples/node_scale.rs | 12 +- .../examples/pad_shift_audit.rs | 34 +- .../examples/paint_order_audit.rs | 140 ++- crates/sylpheed-formats/examples/params.rs | 21 +- .../examples/plateauless_suppression.rs | 41 +- .../sylpheed-formats/examples/pool_window.rs | 14 +- .../sylpheed-formats/examples/probe_anchor.rs | 5 +- .../examples/rat_leaf_placement.rs | 4 +- .../examples/ratc_child_names.rs | 12 +- .../examples/ratc_optless_children.rs | 12 +- .../sylpheed-formats/examples/resolve_all.rs | 164 ++- .../examples/roster_target.rs | 21 +- .../examples/screen_configs.rs | 45 +- .../examples/screen_layout.rs | 48 +- .../sylpheed-formats/examples/se_wave_dump.rs | 6 +- .../examples/shared_bank_takes.rs | 33 +- .../examples/shared_vbase_check.rs | 62 +- .../sylpheed-formats/examples/ship_audit.rs | 19 +- crates/sylpheed-formats/examples/ship_dump.rs | 61 +- .../sylpheed-formats/examples/ship_render.rs | 35 +- .../examples/sibling_rule_check.rs | 18 +- .../sylpheed-formats/examples/slab_screen.rs | 20 +- .../examples/slb_fmt_probe.rs | 14 +- .../examples/slb_hybrid_scan.rs | 4 +- .../sylpheed-formats/examples/spawn_survey.rs | 93 +- crates/sylpheed-formats/examples/squadrons.rs | 39 +- crates/sylpheed-formats/examples/starts.rs | 5 +- .../sylpheed-formats/examples/starts_probe.rs | 22 +- .../sylpheed-formats/examples/submesh_dump.rs | 13 +- .../sylpheed-formats/examples/try_anchor.rs | 9 +- .../examples/twin_mirror_audit.rs | 19 +- crates/sylpheed-formats/examples/ui_screen.rs | 24 +- crates/sylpheed-formats/examples/undecoded.rs | 6 +- .../sylpheed-formats/examples/unit_fields.rs | 8 +- .../examples/unit_signatures.rs | 5 +- .../sylpheed-formats/examples/unit_values.rs | 8 +- .../examples/validate_cues.rs | 105 +- .../sylpheed-formats/examples/vcount_index.rs | 37 +- .../examples/verify_fieldmap.rs | 166 ++- .../examples/voice_bank_dump.rs | 4 +- .../examples/voice_bank_shape.rs | 4 +- .../examples/voice_len_vs_subs.rs | 4 +- .../sylpheed-formats/examples/why_missed.rs | 7 +- crates/sylpheed-formats/src/audio.rs | 22 +- crates/sylpheed-formats/src/game_data.rs | 145 ++- crates/sylpheed-formats/src/ixud.rs | 9 +- crates/sylpheed-formats/src/lib.rs | 4 +- crates/sylpheed-formats/src/localization.rs | 8 +- crates/sylpheed-formats/src/media.rs | 11 +- crates/sylpheed-formats/src/mesh.rs | 186 ++- crates/sylpheed-formats/src/movie_manifest.rs | 38 +- crates/sylpheed-formats/src/movie_subtitle.rs | 19 +- crates/sylpheed-formats/src/movie_voice.rs | 5 +- crates/sylpheed-formats/src/ratc.rs | 4 +- crates/sylpheed-formats/src/savegame.rs | 74 +- crates/sylpheed-formats/src/ship.rs | 125 +- crates/sylpheed-formats/src/ship_capture.rs | 137 ++- crates/sylpheed-formats/src/slb.rs | 10 +- crates/sylpheed-formats/src/t8ad.rs | 10 +- crates/sylpheed-formats/src/texture.rs | 143 ++- crates/sylpheed-formats/src/ui_layout.rs | 73 +- crates/sylpheed-formats/src/unit_layout.rs | 14 +- crates/sylpheed-formats/src/vfs.rs | 36 +- crates/sylpheed-formats/src/xiso.rs | 23 +- .../tests/caption_families_disc.rs | 13 +- .../sylpheed-formats/tests/game_data_disc.rs | 67 +- .../tests/idxd_records_disc.rs | 19 +- .../tests/ixud_records_disc.rs | 8 +- .../tests/mesh_consistency_disc.rs | 31 +- crates/sylpheed-formats/tests/mesh_disc.rs | 204 +++- .../tests/movie_manifest_disc.rs | 35 +- .../tests/movie_subtitle_disc.rs | 9 +- .../sylpheed-formats/tests/pak_idxd_disc.rs | 46 +- .../tests/savegame_samples.rs | 12 +- crates/sylpheed-formats/tests/slb_disc.rs | 22 +- .../tests/slb_leading_segment_disc.rs | 59 +- crates/sylpheed-formats/tests/texture_disc.rs | 46 +- .../tests/ui_focus_kind_disc.rs | 14 +- .../tests/ui_header_time_disc.rs | 22 +- .../tests/ui_opt_link_disc.rs | 29 +- .../tests/ui_paint_order_disc.rs | 91 +- .../tests/ui_prm_primitives_disc.rs | 23 +- .../tests/ui_screen_vs_fragment_disc.rs | 8 +- .../tests/ui_surfaces_disc.rs | 23 +- .../tests/unit_layout_disc.rs | 27 +- crates/sylpheed-viewer/src/asset_loader.rs | 8 +- crates/sylpheed-viewer/src/camera.rs | 19 +- crates/sylpheed-viewer/src/iso_loader.rs | 324 +++-- crates/sylpheed-viewer/src/lib.rs | 16 +- crates/sylpheed-viewer/src/ui.rs | 1087 ++++++++++------- 154 files changed, 5660 insertions(+), 2106 deletions(-) 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;