//! Which container should the next runtime capture aim at? //! //! The 2026-08-13 stage-05 capture showed that a mission keeps several containers //! resident and that its OWN `Stage_SNN.xpr` is among them (the f101 ACROPOLIS was //! drawn from `Stage_S05.xpr`), so a capture can be aimed by choosing the mission. //! This ranks the targets: for every XBG7 resource on the disc it records the //! containers where it decodes and the ones where it does not, then reports //! //! * resources that decode **nowhere** — the real gaps, where a capture is the //! only ground truth left; //! * per container, how many of those it holds (fly that mission); //! * resources that miss in one container but decode in another — a defect worth //! fixing, but the geometry is already recoverable, so a capture is not needed. //! //! Usage: miss_targets use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model}; use std::collections::{BTreeMap, BTreeSet}; fn main() { let dir = std::env::args().nth(1).expect("resource3d dir"); let mut files: Vec<_> = std::fs::read_dir(&dir) .unwrap() .flatten() .map(|e| e.path()) .filter(|p| p.extension().and_then(|s| s.to_str()) == Some("xpr")) .collect(); files.sort(); // resource -> (containers where it decodes, containers where it misses) let mut ok: BTreeMap> = BTreeMap::new(); let mut miss: BTreeMap> = BTreeMap::new(); for f in &files { let Ok(bytes) = std::fs::read(f) else { continue }; let where_ = f.file_name().unwrap().to_string_lossy().to_string(); let declared: BTreeSet = xbg7_resource_names(&bytes).into_iter().collect(); if declared.is_empty() { continue; } let decoded: BTreeSet = Xbg7Model::stage_models(&bytes).into_iter().map(|m| m.name).collect(); for n in &declared { if decoded.contains(n) { ok.entry(n.clone()).or_default().insert(where_.clone()); } else { miss.entry(n.clone()).or_default().insert(where_.clone()); } } } let never: BTreeMap<&String, &BTreeSet> = miss.iter().filter(|(n, _)| !ok.contains_key(*n)).collect(); let recoverable: Vec<&String> = miss.keys().filter(|n| ok.contains_key(*n)).collect(); println!("resources that decode NOWHERE: {}", never.len()); println!("resources that miss somewhere but decode elsewhere: {}\n", recoverable.len()); // Rank containers by how many never-decoding resources they hold. let mut per_container: BTreeMap<&String, Vec<&String>> = BTreeMap::new(); for (n, wheres) in &never { for w in wheres.iter() { per_container.entry(w).or_default().push(n); } } let mut ranked: Vec<_> = per_container.iter().collect(); ranked.sort_by_key(|(_, v)| std::cmp::Reverse(v.len())); println!("container never-decoding resources it holds"); for (w, v) in ranked.iter().take(14) { let sample: Vec<&str> = v.iter().take(6).map(|s| s.as_str()).collect(); println!("{:<26} {:>3} {}", w, v.len(), sample.join(", ")); } // Exclusives: a never-decoding resource that only ONE container holds is only // reachable through whatever loads that container. let excl: Vec<(&&String, &&BTreeSet)> = never.iter().filter(|(_, w)| w.len() == 1).collect(); println!("\nof the never-decoding, {} live in exactly one container", excl.len()); let mut by_one: BTreeMap<&String, Vec<&String>> = BTreeMap::new(); for (n, w) in &excl { by_one.entry(w.iter().next().unwrap()).or_default().push(n); } let mut b: Vec<_> = by_one.iter().collect(); b.sort_by_key(|(_, v)| std::cmp::Reverse(v.len())); for (w, v) in b.iter().take(10) { println!(" {:<24} {:>3} {}", w, v.len(), v.iter().take(5).map(|s| s.as_str()).collect::>().join(", ")); } }