re: the Stage-02 capture drew no capital ship at all — invert the match, then control range
Inverting the capture↔part question (invert_capture over one container, vcount_index over all 166) identifies every large draw in the 2026-07-31 capture: the player's own DeltaSaber (10891 verts), its weapon packs, the backdrop and particles. Of f101/e105/e106 only 1-3 of 15-37 resources have a drawn vcount, each a 44-225-vertex far-LOD/effect piece whose count collides with dozens of unrelated resources. So the zero-correlation was not an LOD-list gap, not over-strict position validation and not a different draw path: the ships were too far away to be drawn. approach_capture.py flies at a locked capital ship and presses F10 per range band, stamping each capture with its distance. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
156
crates/sylpheed-formats/examples/invert_capture.rs
Normal file
156
crates/sylpheed-formats/examples/invert_capture.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
//! Invert the capture↔part match: instead of asking, per ship part, "is there a
|
||||
//! draw with this vertex count?", ask of the **capture's** biggest draws "which
|
||||
//! decoded resource in this stage container has that vertex count?".
|
||||
//!
|
||||
//! This is the diagnostic for the 2026-07-31 negative result (Stage_S02 capture,
|
||||
//! zero parts correlated). It separates three hypotheses:
|
||||
//! 1. LOD/variant vcount not covered by the correlator's variant list
|
||||
//! → the big draws DO map to named resources, just not to the `_m`/`_l`/`_d`
|
||||
//! set the correlator tries;
|
||||
//! 2. position validation over-rejects
|
||||
//! → the vcounts match the very parts we asked for (so the vcount key was
|
||||
//! fine and the rejection happened later);
|
||||
//! 3. a different draw path (instanced/batched/merged buffers)
|
||||
//! → the big draws match NO resource in the container at all.
|
||||
//!
|
||||
//! Usage:
|
||||
//! SYLPHEED_ISO=... cargo run --release --example invert_capture -- \
|
||||
//! <capture.log> <Stage_SNN> [top_n] [--all]
|
||||
//! `--all` lists every capture vcount, not just the `top_n` (default 40) largest.
|
||||
|
||||
use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model};
|
||||
use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog};
|
||||
use sylpheed_formats::xiso::open_iso;
|
||||
use std::collections::{HashMap, 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 all = args.iter().any(|a| a == "--all");
|
||||
if positional.len() < 2 {
|
||||
eprintln!("usage: invert_capture <capture.log> <Stage_SNN> [top_n] [--all]");
|
||||
std::process::exit(2);
|
||||
}
|
||||
let (log, stage) = (positional[0], positional[1]);
|
||||
let top_n: usize = positional.get(2).and_then(|s| s.parse().ok()).unwrap_or(40);
|
||||
let iso = std::env::var("SYLPHEED_ISO").expect("SYLPHEED_ISO");
|
||||
|
||||
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);
|
||||
println!("parsed {} draws (draw-logger format)", draws.len());
|
||||
} else {
|
||||
println!("parsed {} draws (F10 capture format)", draws.len());
|
||||
}
|
||||
|
||||
// Decode EVERY geometry resource in the stage container, not just one ship's.
|
||||
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);
|
||||
println!("{stage}.xpr: {} XBG7 resources", names.len());
|
||||
let want: HashSet<String> = names.iter().cloned().collect();
|
||||
let models = Xbg7Model::models_named(&bytes, &want, &|| false);
|
||||
println!("decoded {} models", models.len());
|
||||
|
||||
// vcount -> resource names with that many vertices.
|
||||
let mut by_vcount: HashMap<u32, Vec<String>> = HashMap::new();
|
||||
for m in &models {
|
||||
let v: usize = m.meshes.iter().map(|s| s.positions.len()).sum();
|
||||
by_vcount.entry(v as u32).or_default().push(m.name.clone());
|
||||
}
|
||||
// Per-submesh counts too: a draw may be one sub-mesh of a multi-mesh resource.
|
||||
let mut by_sub_vcount: HashMap<u32, Vec<String>> = HashMap::new();
|
||||
for m in &models {
|
||||
for (i, s) in m.meshes.iter().enumerate() {
|
||||
if m.meshes.len() > 1 {
|
||||
by_sub_vcount
|
||||
.entry(s.positions.len() as u32)
|
||||
.or_default()
|
||||
.push(format!("{}#{i}", m.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture vcounts, de-duped by (vbase, vcount) so a re-drawn part counts once
|
||||
// per distinct buffer.
|
||||
let mut draw_count: HashMap<u32, usize> = HashMap::new();
|
||||
let mut bufs: HashMap<u32, HashSet<u32>> = HashMap::new();
|
||||
for d in &draws {
|
||||
*draw_count.entry(d.vcount).or_default() += 1;
|
||||
bufs.entry(d.vcount).or_default().insert(d.vbase);
|
||||
}
|
||||
let mut vcounts: Vec<u32> = draw_count.keys().copied().collect();
|
||||
vcounts.sort_unstable_by(|a, b| b.cmp(a));
|
||||
|
||||
let matched_draws: usize = draws
|
||||
.iter()
|
||||
.filter(|d| by_vcount.contains_key(&d.vcount) || by_sub_vcount.contains_key(&d.vcount))
|
||||
.count();
|
||||
println!(
|
||||
"\n{} distinct vcounts; {}/{} draws have a vcount present in {stage}.xpr ({:.1}%)",
|
||||
vcounts.len(),
|
||||
matched_draws,
|
||||
draws.len(),
|
||||
100.0 * matched_draws as f64 / draws.len().max(1) as f64
|
||||
);
|
||||
|
||||
let shown = if all { vcounts.len() } else { top_n.min(vcounts.len()) };
|
||||
println!("\nlargest capture vcounts (draws / distinct vbufs) → matching resources:");
|
||||
for &v in vcounts.iter().take(shown) {
|
||||
let n = draw_count[&v];
|
||||
let b = bufs[&v].len();
|
||||
let mut hit: Vec<String> = by_vcount.get(&v).cloned().unwrap_or_default();
|
||||
let sub: Vec<String> = by_sub_vcount.get(&v).cloned().unwrap_or_default();
|
||||
hit.extend(sub.into_iter().map(|s| format!("{s} (sub)")));
|
||||
let label = if hit.is_empty() {
|
||||
"— no resource".to_string()
|
||||
} else {
|
||||
let mut h = hit.clone();
|
||||
h.sort();
|
||||
h.truncate(6);
|
||||
format!("{}{}", h.join(", "), if hit.len() > 6 { ", …" } else { "" })
|
||||
};
|
||||
println!(" vcount {v:6} draws {n:4} bufs {b:3} {label}");
|
||||
}
|
||||
|
||||
// `--ship <id>`: every resource of one ship family, with its vertex count and
|
||||
// whether the capture drew it — this is what shows an all-`_l` (far-LOD) frame.
|
||||
if let Some(i) = args.iter().position(|a| a == "--ship") {
|
||||
if let Some(id) = args.get(i + 1) {
|
||||
let mut rows: Vec<(String, u32, usize)> = models
|
||||
.iter()
|
||||
.filter(|m| m.name.contains(id.as_str()))
|
||||
.map(|m| {
|
||||
let v = m.meshes.iter().map(|s| s.positions.len()).sum::<usize>() as u32;
|
||||
(m.name.clone(), v, draw_count.get(&v).copied().unwrap_or(0))
|
||||
})
|
||||
.collect();
|
||||
rows.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
let drawn = rows.iter().filter(|r| r.2 > 0).count();
|
||||
println!("\n{id} resources in {stage}.xpr ({drawn}/{} with a drawn vcount):", rows.len());
|
||||
for (name, v, n) in rows {
|
||||
println!(" {name:28} vcount {v:6} {}", if n > 0 { format!("DRAWN ×{n}") } else { "—".into() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The other direction, for orientation: the container's biggest resources and
|
||||
// whether the capture ever drew that many vertices.
|
||||
let mut sizes: Vec<(u32, String)> = models
|
||||
.iter()
|
||||
.map(|m| (m.meshes.iter().map(|s| s.positions.len()).sum::<usize>() as u32, m.name.clone()))
|
||||
.collect();
|
||||
sizes.sort_unstable_by(|a, b| b.0.cmp(&a.0));
|
||||
println!("\nlargest resources in {stage}.xpr → drawn in the capture?");
|
||||
for (v, name) in sizes.iter().take(top_n.min(sizes.len())) {
|
||||
let n = draw_count.get(v).copied().unwrap_or(0);
|
||||
println!(" {name:28} vcount {v:6} {}", if n > 0 { format!("DRAWN ×{n}") } else { "not drawn".into() });
|
||||
}
|
||||
}
|
||||
98
crates/sylpheed-formats/examples/vcount_index.rs
Normal file
98
crates/sylpheed-formats/examples/vcount_index.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
//! Global "which resource has N vertices?" index over every `.xpr` container in
|
||||
//! an extracted `resource3d` directory, answered for the vcounts a capture log
|
||||
//! actually drew.
|
||||
//!
|
||||
//! Companion to `invert_capture`: that one asks the question inside a single
|
||||
//! stage container, this one asks it across ALL containers — so a draw whose
|
||||
//! geometry lives in `Common.xpr`, a `rou_*` weapon pack or a `BG_*` backdrop is
|
||||
//! still identified instead of coming back "no resource".
|
||||
//!
|
||||
//! Usage:
|
||||
//! cargo run --release --example vcount_index -- <resource3d_dir> <capture.log> [top_n]
|
||||
//! cargo run --release --example vcount_index -- <resource3d_dir> --vcounts 10891,6000
|
||||
|
||||
use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model};
|
||||
use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() < 3 {
|
||||
eprintln!("usage: vcount_index <resource3d_dir> <capture.log|--vcounts a,b,c> [top_n]");
|
||||
std::process::exit(2);
|
||||
}
|
||||
let dir = &args[1];
|
||||
|
||||
// Which vertex counts are we asking about, and how often was each drawn?
|
||||
let mut draw_count: HashMap<u32, usize> = HashMap::new();
|
||||
let mut bufs: HashMap<u32, HashSet<u32>> = HashMap::new();
|
||||
if args[2] == "--vcounts" {
|
||||
for v in args[3].split(',').filter_map(|s| s.trim().parse::<u32>().ok()) {
|
||||
draw_count.insert(v, 0);
|
||||
}
|
||||
} else {
|
||||
let text = std::fs::read_to_string(&args[2]).expect("read log");
|
||||
let mut draws = parse_capture(&text);
|
||||
if draws.is_empty() {
|
||||
draws = parse_drawlog(&text);
|
||||
}
|
||||
eprintln!("parsed {} draws", draws.len());
|
||||
for d in &draws {
|
||||
*draw_count.entry(d.vcount).or_default() += 1;
|
||||
bufs.entry(d.vcount).or_default().insert(d.vbase);
|
||||
}
|
||||
}
|
||||
let top_n: usize = args.get(3).and_then(|s| s.parse().ok()).unwrap_or(usize::MAX);
|
||||
|
||||
// Decode every container once; keep only the vcount → names mapping.
|
||||
let mut by_vcount: HashMap<u32, Vec<String>> = HashMap::new();
|
||||
let mut files: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
|
||||
.expect("read dir")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|e| e == "xpr"))
|
||||
.collect();
|
||||
files.sort();
|
||||
let mut total_res = 0usize;
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||
let names = xbg7_resource_names(&bytes);
|
||||
if names.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let want: HashSet<String> = names.iter().cloned().collect();
|
||||
let models = Xbg7Model::models_named(&bytes, &want, &|| false);
|
||||
let container = f.file_stem().unwrap().to_string_lossy().to_string();
|
||||
for m in &models {
|
||||
total_res += 1;
|
||||
let whole: usize = m.meshes.iter().map(|s| s.positions.len()).sum();
|
||||
by_vcount.entry(whole as u32).or_default().push(format!("{container}:{}", m.name));
|
||||
if m.meshes.len() > 1 {
|
||||
for (i, s) in m.meshes.iter().enumerate() {
|
||||
by_vcount
|
||||
.entry(s.positions.len() as u32)
|
||||
.or_default()
|
||||
.push(format!("{container}:{}#{i}", m.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!("indexed {} resources from {} containers", total_res, files.len());
|
||||
|
||||
let mut vcounts: Vec<u32> = draw_count.keys().copied().collect();
|
||||
vcounts.sort_unstable_by(|a, b| b.cmp(a));
|
||||
println!("\nvcount draws bufs resources anywhere in resource3d/");
|
||||
for v in vcounts.into_iter().take(top_n) {
|
||||
let n = draw_count[&v];
|
||||
let b = bufs.get(&v).map(|s| s.len()).unwrap_or(0);
|
||||
let hit = by_vcount.get(&v).cloned().unwrap_or_default();
|
||||
let label = if hit.is_empty() {
|
||||
"— NONE".to_string()
|
||||
} else {
|
||||
let mut h = hit.clone();
|
||||
h.sort();
|
||||
let shown = h.len().min(8);
|
||||
format!("{}{}", h[..shown].join(", "), if h.len() > shown { format!(", … ({} total)", h.len()) } else { String::new() })
|
||||
};
|
||||
println!("{v:6} {n:5} {b:4} {label}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user