use mercs2_engine::wad;
use mercs2_formats::anim::AnimType;
use std::collections::{BTreeMap, BTreeSet};
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let mut wads: Vec<String> = Vec::new();
let mut list: Option<String> = None;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--wad" => {
wads.push(args.get(i + 1).cloned().unwrap_or_else(|| usage("--wad needs a path")));
i += 2;
}
"--list" => {
list = Some(args.get(i + 1).cloned().unwrap_or_else(|| usage("--list needs a codec")));
i += 2;
}
a => usage(&format!("unknown argument {a}")),
}
}
if wads.is_empty() {
let p = wad::resolve_vz_wad(None).unwrap_or_else(|| usage("no vz.wad found — pass --wad or set VZ_WAD"));
wads.push(p);
}
let mut by_codec: BTreeMap<&'static str, BTreeSet<u32>> = BTreeMap::new();
let mut bones_of: BTreeMap<u32, Vec<u32>> = BTreeMap::new();
let mut failures = 0usize;
for path in &wads {
let mut w = wad::open(path).unwrap_or_else(|e| usage(&format!("{path}: {e}")));
let mut listed: BTreeSet<u32> = BTreeSet::new();
for blk in wad::animgroup_blocks(&w) {
let data = match wad::decompress_block_index(&mut w, blk) {
Ok(d) => d,
Err(e) => {
eprintln!("{path}: block {blk}: {e}");
failures += 1;
continue;
}
};
let ag = match mercs2_formats::animgroup::parse_animgroup(&data) {
Ok(a) => a,
Err(e) => {
eprintln!("{path}: block {blk}: animgroup: {e}");
failures += 1;
continue;
}
};
for c in &ag.clips {
listed.insert(c.name_hash);
bones_of.entry(c.name_hash).or_insert_with(|| c.binding.track_to_bone_hash.clone());
if c.havok_len == 0 {
by_codec.entry("manm (material animation)").or_default().insert(c.name_hash);
continue;
}
let codec = match c.packfile(&data).and_then(|pk| mercs2_formats::anim::parse_anim(pk)) {
Ok(clip) => match (clip.anim_type, clip.decoded) {
(AnimType::Interleaved, true) => "interleaved",
(AnimType::Wavelet, true) => "wavelet",
(AnimType::Wavelet, false) => "wavelet (frames not decoded)",
(AnimType::Delta, _) => "delta",
(AnimType::Spline, _) => "spline",
(AnimType::Interleaved, false) => "interleaved (frames not decoded)",
},
Err(e) => {
eprintln!("{path}: block {blk}: clip 0x{:08X}: {e}", c.name_hash);
"unreadable"
}
};
by_codec.entry(codec).or_default().insert(c.name_hash);
}
}
let rows: BTreeSet<u32> = wad::all_asets(&w)
.into_iter()
.filter(|(_, ty, _)| *ty == mercs2_formats::types::TYPE_ID_ANIMATION)
.map(|(h, _, _)| h)
.collect();
for h in rows.difference(&listed) {
by_codec.entry("unlisted").or_default().insert(*h);
}
}
println!("=== ANIMATION CODEC CENSUS ({} wad(s)) ===", wads.len());
for (codec, set) in &by_codec {
println!(" {codec:<34} {:>6}", set.len());
}
let mut tracks: BTreeMap<&'static str, usize> = BTreeMap::new();
for b in bones_of.values() {
let bucket = match b.len() {
0 => "0 tracks",
1 => "1 track",
2..=19 => "2-19 tracks",
20..=59 => "20-59 tracks",
60..=105 => "60-105 tracks",
_ => "over 105 tracks",
};
*tracks.entry(bucket).or_default() += 1;
}
println!("=== BY TRACK COUNT ({} listed clips, longest {}) ===", bones_of.len(), bones_of.values().map(Vec::len).max().unwrap_or(0));
for (b, n) in &tracks {
println!(" {b:<34} {n:>6}");
}
let mut w0 = wad::open(&wads[0]).unwrap_or_else(|e| usage(&format!("{}: {e}", wads[0])));
let all_bones: BTreeSet<u32> = bones_of.values().flatten().copied().collect();
let part_bones: BTreeSet<u32> = bones_of.values().filter(|b| b.len() < 20).flatten().copied().collect();
let (mut rigged, mut driven, mut part_driven) = (0usize, 0usize, 0usize);
wad::for_each_model_hier(&mut w0, |_, _| {}, |_, read| {
let Ok((_, hier)) = read else { return };
if hier.len() < 2 {
return;
}
rigged += 1;
if hier.iter().any(|n| all_bones.contains(&n.hash)) {
driven += 1;
}
if hier.iter().any(|n| part_bones.contains(&n.hash)) {
part_driven += 1;
}
});
println!("=== RIGS ({}) ===", wads[0]);
println!(" models with 2+ HIER nodes {rigged:>6}");
println!(" ... a clip's binding names a node {driven:>6}");
println!(" ... a <20-track clip names a node {part_driven:>6}");
if let Some(want) = list {
let set = by_codec.iter().find(|(k, _)| k.starts_with(want.as_str())).map(|(_, v)| v);
match set {
Some(s) => {
for h in s {
println!("0x{h:08X}");
}
}
None => println!("(no {want} clips)"),
}
}
let unreadable = by_codec.get("unreadable").map_or(0, |s| s.len());
if failures + unreadable > 0 {
eprintln!("{failures} block(s) and {unreadable} clip(s) did not parse");
std::process::exit(1);
}
}
fn usage(msg: &str) -> ! {
eprintln!("anim_codec_census: {msg}\nusage: anim_codec_census [--wad <path>]... [--list <codec>]");
std::process::exit(2)
}