Finishes #16 in the three places its earlier remedies missed. `tests/`: the last four local `disc_root()` copies now use `tests/common`, and with them goes the one real hardcoded fallback — `ui_keyframe_record_disc.rs` fell back to an absolute path on one machine, which made `unset SYLPHEED_DISC` a no-op there. Control: with the corpus absent that suite now finishes in 0.00s instead of 57.55s, so it skips rather than finding a disc of its own. `examples/`: seventeen examples defaulted to `/disc`, the mount point inside the CI container. Redundant there — `docker/ci/run` sets `SYLPHEED_DISC=/disc` — and wrong everywhere else, where a missing corpus turned into a file-not-found against a path that has never existed on the host. They now name the variable to set, like the other hundred examples already did. `docker/ci/run`: mount `$SYLPHEED_RES3D` and `$SYLPHEED_ISO` alongside the disc. Only the disc was mounted, so an in-container run sat out the res3d and iso suites while looking like a full one — the defect this issue is about, in the runner itself. Measured in the container on this desktop with all three corpora present: 45 suites / 377 passed / 0 failed / 14 ignored, and `sylpheed-corpus-report.txt` now reports PRESENT for all three rather than for the disc alone. Refs #16. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
167 lines
6.5 KiB
Rust
167 lines
6.5 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="/path/to/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 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;
|
|
|
|
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)));
|
|
}
|
|
}
|