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>
This commit is contained in:
43
crates/sylpheed-formats/examples/capture_match.rs
Normal file
43
crates/sylpheed-formats/examples/capture_match.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
//! 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 -- <capture.log> <resource3d dir>
|
||||
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<String> = 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<u32> = 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();
|
||||
entries.sort();
|
||||
for path in entries {
|
||||
let bytes = std::fs::read(&path).unwrap();
|
||||
let names = xbg7_resource_names(&bytes);
|
||||
let want: HashSet<String> = names.iter().cloned().collect();
|
||||
let models = Xbg7Model::models_named(&bytes, &want, &|| false);
|
||||
// resource -> vcount; group hits by ship-id prefix
|
||||
let mut hits: BTreeMap<String, Vec<(String, u32)>> = BTreeMap::new();
|
||||
for m in &models {
|
||||
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));
|
||||
}
|
||||
}
|
||||
let total: usize = hits.values().map(|v| v.len()).sum();
|
||||
if total >= 3 {
|
||||
println!("== {} : {} matching resources ==", path.file_name().unwrap().to_string_lossy(), total);
|
||||
for (id, v) in &hits {
|
||||
let s: Vec<String> = v.iter().map(|(n, c)| format!("{n}({c})")).collect();
|
||||
println!(" {id}: {}", s.join(" "));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,31 @@
|
||||
//! Recover exact capital-ship part placement from a Canary F10 ship-capture log
|
||||
//! 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 (the match key), correlates, and prints the result.
|
||||
//! With `--emit` it prints the `ship …` block to paste into
|
||||
//! 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. `... /tmp/xenia_ship_capture.log Stage_S01 e106 bdy_04 --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};
|
||||
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;
|
||||
@@ -33,8 +44,8 @@ fn main() {
|
||||
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
|
||||
// (mission_draws.log); auto-detect by trying the F10 parser first.
|
||||
// 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() {
|
||||
@@ -44,7 +55,8 @@ fn main() {
|
||||
eprintln!("parsed {} draws (F10 capture format)", draws.len());
|
||||
}
|
||||
|
||||
// Decode the ship's base parts to get each part's vertex count (the match key).
|
||||
// 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 {
|
||||
@@ -53,33 +65,69 @@ fn main() {
|
||||
})
|
||||
};
|
||||
let names = xbg7_resource_names(&bytes);
|
||||
let want: HashSet<String> = names
|
||||
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 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 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())
|
||||
};
|
||||
|
||||
let Some(ship) = correlate(id, &draws, &parts, ref_sub) else {
|
||||
// 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=[{:8.1}{:8.1}{:8.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]);
|
||||
}
|
||||
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(", "));
|
||||
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 {
|
||||
|
||||
20
crates/sylpheed-formats/examples/part_pos.rs
Normal file
20
crates/sylpheed-formats/examples/part_pos.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
//! Print a resource's first N decoded positions (buffer order), for matching a
|
||||
//! capture draw's `pos:` dump to a part:
|
||||
//! cargo run --release --example part_pos -- <xpr path> <resource> [n]
|
||||
use std::collections::HashSet;
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let bytes = std::fs::read(&a[1]).unwrap();
|
||||
let n: usize = a.get(3).and_then(|s| s.parse().ok()).unwrap_or(4);
|
||||
let want: HashSet<String> = [a[2].clone()].into();
|
||||
for m in Xbg7Model::models_named(&bytes, &want, &|| false) {
|
||||
let vc: usize = m.meshes.iter().map(|s| s.positions.len()).sum();
|
||||
print!("{} vtx={}:", m.name, vc);
|
||||
for p in m.meshes.iter().flat_map(|s| &s.positions).take(n) {
|
||||
print!(" ({:.4},{:.4},{:.4})", p[0], p[1], p[2]);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user