//! Recover exact capital-ship part placement from a Canary capture log //! and (optionally) emit a checked-in placement-table block. //! //! The correlation math lives in [`sylpheed_formats::ship_capture`]; this example //! is the CLI wrapper: it reads the log, decodes the ship's parts from the disc to //! get their vertex counts + leading positions (the match keys), correlates, and //! prints the result. With `--emit` it prints the `ship …` block to paste into //! `crates/sylpheed-formats/data/ship_placements.txt`. //! //! Matching is **LOD-aware**: a ship on screen at distance is drawn with its //! `_m`/`_l` LOD copies, which live in the same local frame as the base part — so //! each base part tries its own vcount first, then its LOD variants', and the //! recovered transform is recorded under the base name. Same-vcount twins //! (mirrored port/starboard hulls) are routed by the draw's position dump. //! //! Usage: //! SYLPHEED_ISO=... cargo run --release --example correlate_capture -- \ //! [ref_part_substr] [--emit] //! e.g. SYLPHEED_ISO="/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of //! Deception (USA, Europe) (En,Ja).iso" \ //! cargo run --release --example correlate_capture -- \ //! xenia_ship_capture.log Stage_S01 e106 bdy_04 --emit 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(); let positional: Vec<&String> = args[1..].iter().filter(|a| !a.starts_with("--")).collect(); let emit = args.iter().any(|a| a == "--emit"); if positional.len() < 3 { eprintln!( "usage: correlate_capture [ref_part_substr] [--emit]" ); std::process::exit(2); } let (log, stage, id) = (positional[0], positional[1], positional[2]); let ref_sub = positional.get(3).map(|s| s.as_str()).unwrap_or("bdy_04"); let iso = std::env::var("SYLPHEED_ISO").expect("SYLPHEED_ISO"); // Accept either the F10 ship-capture format or the draw-logger format; // auto-detect by trying the F10 parser first. let text = std::fs::read_to_string(log).expect("read log"); let mut draws = parse_capture(&text); if draws.is_empty() { draws = parse_drawlog(&text); eprintln!("parsed {} draws (draw-logger format)", draws.len()); } else { eprintln!("parsed {} draws (F10 capture format)", draws.len()); } // 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(); rt.block_on(async { let mut r = open_iso(Path::new(&iso)).await.unwrap(); r.read_file(&format!("hidden/resource3d/{stage}.xpr")).await.unwrap() }) }; let names = xbg7_resource_names(&bytes); let base_parts: Vec = names .iter() .filter(|n| is_base_part(n) && ship_id_of(n) == Some(id.as_str())) .cloned() .collect(); // Candidate resources per base part: itself + `_m`/`_l`/`_d` LOD copies. let mut want: HashSet = base_parts.iter().cloned().collect(); for p in &base_parts { for suf in ["_m", "_l", "_d"] { let cand = format!("{p}{suf}"); if names.contains(&cand) { want.insert(cand); } } } 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()) }; // One PartKey per (part, variant-vcount present in the capture) — correlate // takes the first that validates. Validation uses the UNION of the part's // variant position sets: a draw's `vcount` can match one LOD while its // buffer holds another variant's geometry (seen on the real e106: the // 558-vcount draw carried the FULL 1633-vert bdy_04 buffer), and every // 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 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("?") }; eprintln!(" {part:20} try vcount={vcount:6} [{lod}]"); keys.push(PartKey { part: part.clone(), vcount, ref_pos: union.clone() }); any = true; } } } if !any { eprintln!(" {part:20} — no draw matches any LOD (culled/off-screen?)"); } } let Some(ship) = correlate(id, &draws, &keys, ref_sub) else { eprintln!("no parts matched a captured draw"); return; }; 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]); } for part in &base_parts { if !ship.parts.iter().any(|p| &p.part == part) { eprintln!(" {part:18} — NOT placed (culled, or its only vcount hit failed position validation)"); } } if emit { print!("{}", serialize_table(std::slice::from_ref(&ship))); } }