Promote the F10 ship-capture correlator from an example into a reusable, unit- tested library module (ship_capture): parse_capture (log -> per-draw rigid WorldView from the c0..c2 WVP constants), correlate (match parts to draws by vertex count, express each in the reference part's frame), and a checked-in placement-table format (serialize_table/parse_table, embedded via embedded_placement from data/ship_placements.txt). build_ship_model now prefers a ship's captured placement over the static assemble_ship when a table entry exists (empty table -> unchanged fallback). correlate_capture example refactored onto the module + gains --emit to print a table block. Doc updated: bake plumbing done; remaining is running real F10 captures to populate the table (needs the emulator). 5 new ship_capture tests (parse/normalize/correlate/table round-trip); 76 formats-lib tests + viewer build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
81 lines
3.3 KiB
Rust
81 lines
3.3 KiB
Rust
//! Recover exact capital-ship part placement from a Canary F10 ship-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 (the match key), correlates, and prints the result.
|
|
//! With `--emit` it prints the `ship …` block to paste into
|
|
//! `crates/sylpheed-formats/data/ship_placements.txt`.
|
|
//!
|
|
//! Usage:
|
|
//! SYLPHEED_ISO=... cargo run --release --example correlate_capture -- \
|
|
//! <capture.log> <Stage_SNN> <ship_id> [ref_part_substr] [--emit]
|
|
//! e.g. `... /tmp/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, serialize_table};
|
|
use sylpheed_formats::xiso::open_iso;
|
|
use std::collections::HashSet;
|
|
use std::path::Path;
|
|
|
|
fn main() {
|
|
let args: Vec<String> = 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 <capture.log> <Stage_SNN> <ship_id> [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");
|
|
|
|
let draws = parse_capture(&std::fs::read_to_string(log).expect("read log"));
|
|
eprintln!("parsed {} draws with WorldView", draws.len());
|
|
|
|
// Decode the ship's base parts to get each part's vertex count (the match key).
|
|
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 want: HashSet<String> = 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);
|
|
let parts: Vec<(String, u32)> = models
|
|
.iter()
|
|
.map(|m| {
|
|
let vc = m.meshes.iter().map(|s| s.positions.len()).sum::<usize>() as u32;
|
|
(m.name.clone(), vc)
|
|
})
|
|
.collect();
|
|
|
|
let Some(ship) = correlate(id, &draws, &parts, 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=[{:8.1}{:8.1}{:8.1}]", p.part, p.t[0], p.t[1], p.t[2]);
|
|
}
|
|
let unmatched: Vec<&str> =
|
|
parts.iter().map(|(n, _)| n.as_str()).filter(|n| !ship.parts.iter().any(|p| p.part == *n)).collect();
|
|
if !unmatched.is_empty() {
|
|
eprintln!(" (no captured draw — occluded/culled: {})", unmatched.join(", "));
|
|
}
|
|
|
|
if emit {
|
|
print!("{}", serialize_table(std::slice::from_ref(&ship)));
|
|
}
|
|
}
|