Files
Sylpheed/crates/sylpheed-formats/examples/correlate_capture.rs
MechaCat02 d9442a2106 ship_capture: bake e106 COMPLETE from the 2026-07-26 F10 capture — all 8 parts
The second F10 capture (ship at _m LOD distance) closes e106 entirely: all 8
parts placed including the bridge both earlier captures missed, cross-validating
the doc's 7 known translations to 0.1 units and adding brg_01 at the exact
centreline (0, 153.3, -164.0).

Matching hardened by what the real capture taught us:
- LOD-aware: each part tries every variant vcount (base/_m/_l/_d) — a distant
  ship draws its LOD copies, same local frame.
- Position-validated, SET-based, against the UNION of a part's variants: buffer
  order != decode order, and one draw's vcount equalled the _m count while its
  buffer held the FULL 1633-vert geometry. A coincidental vcount (foreign
  51-vert mesh vs the bridge) is rejected by geometry.
- Runtime mirror handled: twins share one file geometry; the engine uploads the
  starboard copy X-reflected. Mirror-validated draws bake diag(-1,1,1) and the
  viewer reverses triangle winding for det<0 so front faces stay outward.
- Near-axis rotation entries snapped to exact 0/+-1 for a clean table; eng_01
  keeps its genuine 30-degree nacelle rotation.

data/ship_placements.txt now ships e106 (viewer renders via the captured table);
embedded_e106_is_complete locks the baked data. part_pos + capture_match helper
examples added. 10 ship_capture tests; 80 formats-lib tests green; viewer builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 18:26:23 +02:00

137 lines
6.0 KiB
Rust

//! 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 -- \
//! <capture.log> <Stage_SNN> <ship_id> [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<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");
// 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<String> = 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<String> = 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<Vec<[f32; 3]>> {
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<PartKey> = 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)));
}
}