use std::collections::BTreeMap;
use std::fs::File;
use mercs2_engine::asset::AssetSource;
use mercs2_engine::facecast;
use mercs2_formats::facefx::{self, Actor, AnimSet, NodeKind};
use mercs2_formats::types::{TYPE_HASH_FACEFX_ACTOR, TYPE_HASH_FACEFX_ANIMSET, TYPE_HASH_SCRIPT, TYPE_HASH_STRINGDB, TYPE_HASH_WAVEBANK};
use mercs2_formats::worldentity::{WorldEntity, WORLDENTITY_TYPE_HASH};
pub fn facefx(wadpath: &str, name: u32) -> Result<(), String> {
let mut assets = AssetSource::discover(wadpath, &[])?;
let actor = assets.resolve(TYPE_HASH_FACEFX_ACTOR, name);
let set = assets.resolve(TYPE_HASH_FACEFX_ANIMSET, name);
if actor.is_none() && set.is_none() {
return Err(format!("0x{name:08X}: no facefxactor or facefxanimationset in {:?}", assets.paths()));
}
if let Some(c) = actor {
let a = facefx::parse_actor_container(&c).map_err(|e| format!("facefxactor 0x{name:08X}: {e}"))?;
println!("facefxactor 0x{name:08X}: version {} ({} / {})", a.header.version, a.header.licensee, a.header.project);
print_actor(&a.root);
if let Some(m) = assets.resolve(mercs2_formats::types::TYPE_HASH_MODEL, name) {
let hier = mercs2_formats::orchestrator::parse_hier(&m);
let rig = mercs2_engine::anim::facefx::FaceRig::new(&a.root)?;
println!("model 0x{name:08X}: {} HIER nodes; face bone reference pose vs HIER local", hier.len());
for p in rig.reference_pose() {
let h = mercs2_formats::hash::pandemic_hash_m2(&p.name);
match hier.iter().find(|n| n.hash == h) {
Some(n) => {
let m: [[f32; 4]; 4] = std::array::from_fn(|r| std::array::from_fn(|c| n.local[r * 4 + c]));
let q = mercs2_engine::pose::mat_to_qs(&m);
println!(" {:<26} face t {:?} r {:?}", p.name, p.local.translation, p.local.rotation);
println!(" {:<26} HIER t {:?} r {:?}", "", q.translation, q.rotation);
}
None => println!(" {:<26} not in the model's HIER", p.name),
}
}
}
}
if let Some(c) = set {
let s = facefx::parse_animset_container(&c).map_err(|e| format!("facefxanimationset 0x{name:08X}: {e}"))?;
println!("facefxanimationset 0x{name:08X}: version {}", s.header.version);
print_set(&s.root);
}
Ok(())
}
fn print_actor(a: &Actor) {
println!("actor {:?}: {} master bones, {} nodes", a.name, a.master_bones.len(), a.nodes.len());
for b in &a.master_bones {
println!(
" bone {:<26} pos {:?} rot(wxyz) {:?} scale {:?} index {} weight {:?}",
b.bone.name, b.bone.position, b.bone.rotation, b.bone.scale, b.index, b.weight
);
}
for n in &a.nodes {
println!(
" node {:<24} {} min {} max {} op {} links {} properties {}",
n.name,
n.class,
n.min,
n.max,
n.input_operation,
n.links.len(),
n.properties.len()
);
for l in &n.links {
println!(" link from {:?} via {} {:?}", l.input, l.function, l.params);
}
if let NodeKind::BonePose(bones) = &n.kind {
for b in bones {
println!(" sets {:<26} pos {:?} rot(wxyz) {:?} scale {:?}", b.name, b.position, b.rotation, b.scale);
}
}
}
for g in &a.groups {
println!(" anim group {:?}: {} anims", g.name, g.anims.len());
}
if !a.names.is_empty() {
println!(" names {:?}", a.names);
}
if let Some(m) = &a.phoneme_map {
println!(" phoneme map: {} mappings, {} targets", m.mappings.len(), m.targets.len());
}
}
fn print_set(s: &AnimSet) {
println!("anim set {:?} for actor {:?}: group {:?}, {} anims", s.name, s.actor, s.group.name, s.group.anims.len());
for a in &s.group.anims {
println!(
" anim {:<40} {} curves, ends {:.3}s, f20 {} f24 {}, bone weights {}",
a.name,
a.curves.len(),
a.bounds().1,
a.f20,
a.f24,
a.bone_weights.len()
);
for c in &a.curves {
let (lo, hi) = c.keys.iter().map(|k| k.value()).fold((f32::MAX, f32::MIN), |(l, h), v| (l.min(v), h.max(v)));
let peak = c.keys.iter().max_by(|a, b| a.value().total_cmp(&b.value())).map_or(0.0, |k| k.time());
println!(
" curve {:<26} interpolation {} keys {:>3} values {:.3}..{:.3} (highest at {peak:.3}s)",
c.name,
c.interpolation,
c.keys.len(),
if c.keys.is_empty() { 0.0 } else { lo },
if c.keys.is_empty() { 0.0 } else { hi }
);
}
}
}
pub fn facefx_all(wadpath: &str) -> Result<(), String> {
let mut assets = AssetSource::discover(wadpath, &[])?;
let mut failed = Vec::new();
let (mut actors, mut sets) = (0usize, 0usize);
let mut classes: BTreeMap<String, usize> = BTreeMap::new();
let mut functions: BTreeMap<String, usize> = BTreeMap::new();
let mut curves: BTreeMap<String, usize> = BTreeMap::new();
let mut interpolations: BTreeMap<i32, usize> = BTreeMap::new();
let (mut keys, mut sloped) = (0usize, 0usize);
let mut clips: std::collections::HashMap<u32, String> = std::collections::HashMap::new();
let mut anim_names: Vec<String> = Vec::new();
let mut set_actors: Vec<(String, u32, String)> = Vec::new();
let mut actor_hashes: std::collections::HashSet<u32> = std::collections::HashSet::new();
let mut worldentity: Option<Vec<u8>> = None;
let mut english: std::collections::HashMap<u32, String> = std::collections::HashMap::new();
for path in assets.paths() {
let mut f = File::open(path).map_err(|e| format!("{path}: {e}"))?;
let size = f.metadata().map_err(|e| format!("{path}: {e}"))?.len();
let arch = mercs2_formats::ffcs::load_ffcs_archive(&mut f, size).map_err(|e| format!("{path}: {e}"))?;
for bi in 0..arch.indx.len() {
let block = mercs2_formats::sges::decompress_block(&mut f, &arch.indx, bi as u16)
.map_err(|e| format!("{path} block {bi}: {e}"))?;
for (e, span) in mercs2_formats::ucfx::block_chunk_spans(&block).map_err(|e| format!("{path} block {bi}: {e}"))? {
let what = format!("{path} block {bi} ({}) 0x{:08X}", arch.paths[bi], e.name_hash);
let c = &block[span];
if e.type_hash == TYPE_HASH_FACEFX_ACTOR {
actors += 1;
actor_hashes.insert(e.name_hash);
match facefx::parse_actor_container(c) {
Ok(a) => {
for n in &a.root.nodes {
*classes.entry(n.class.clone()).or_default() += 1;
for l in &n.links {
*functions.entry(l.function.clone()).or_default() += 1;
}
}
}
Err(err) => failed.push(format!("facefxactor {what}: {err}")),
}
} else if e.type_hash == WORLDENTITY_TYPE_HASH {
worldentity = Some(c.to_vec());
} else if e.type_hash == TYPE_HASH_STRINGDB && e.name_hash == mercs2_formats::hash::pandemic_hash_m2("english") {
let bodies = (mercs2_formats::ucfx::extract_chunk_body(c, b"KEYS"), mercs2_formats::ucfx::extract_chunk_body(c, b"STRS"));
match bodies {
(Some(k), Some(s)) => match mercs2_formats::stringdb::parse(&k, &s) {
Ok(db) => english.extend(db.entries.into_iter().map(|e| (e.key_hash, e.text))),
Err(err) => failed.push(format!("stringdb {what}: {err}")),
},
_ => failed.push(format!("stringdb {what}: no KEYS or STRS chunk")),
}
} else if e.type_hash == TYPE_HASH_WAVEBANK {
let body = mercs2_formats::ucfx::extract_chunk_body(c, b"data");
match body.map(|b| mercs2_engine::audio::wave::WavebankFile::parse(&b)) {
Some(Ok(w)) => {
let bank = w.stream_name.clone().unwrap_or_else(|| format!("embedded 0x{:08X}", e.name_hash));
clips.extend(w.records.iter().map(|r| (r.clip_hash, bank.clone())));
}
Some(Err(err)) => failed.push(format!("wavebank {what}: {err:?}")),
None => failed.push(format!("wavebank {what}: no data chunk")),
}
} else if e.type_hash == TYPE_HASH_FACEFX_ANIMSET {
sets += 1;
match facefx::parse_animset_container(c) {
Ok(s) => {
set_actors.push((s.root.name.clone(), e.name_hash, s.root.actor.clone()));
for a in &s.root.group.anims {
anim_names.push(a.name.clone());
for c in &a.curves {
*curves.entry(c.name.clone()).or_default() += 1;
*interpolations.entry(c.interpolation).or_default() += 1;
keys += c.keys.len();
sloped += c.keys.iter().filter(|k| k.0[2] != 0.0 || k.0[3] != 0.0).count();
}
}
}
Err(err) => failed.push(format!("facefxanimationset {what}: {err}")),
}
}
}
}
}
println!("{actors} facefxactor and {sets} facefxanimationset containers in {:?}", assets.paths());
println!("node classes: {classes:?}");
println!("link functions: {functions:?}");
println!("curve interpolations: {interpolations:?}");
println!("curve keys: {keys}, with a non-zero third or fourth float: {sloped}");
let mut banks: BTreeMap<&str, usize> = BTreeMap::new();
let mut unmatched: Vec<&String> = Vec::new();
for n in &anim_names {
match clips.get(&mercs2_formats::hash::pandemic_hash_m2(n)) {
Some(b) => *banks.entry(b.as_str()).or_default() += 1,
None => unmatched.push(n),
}
}
println!(
"anims whose name hashes to a clip of a wavebank ({} clips): {} of {}, by bank {banks:?}; the others:",
clips.len(),
anim_names.len() - unmatched.len(),
anim_names.len()
);
for n in &unmatched {
println!(" {n}");
}
let texted = |n: &&String| {
english.get(&mercs2_formats::stringdb::key_hash(n)).is_some_and(|t| !t.trim().is_empty())
};
let untexted: Vec<&String> = anim_names.iter().filter(|n| !texted(n)).collect();
println!(
"anims whose name is a key of the english string table ({} keys): {} of {}; the others:",
english.len(),
anim_names.len() - untexted.len(),
anim_names.len()
);
for n in &untexted {
println!(" {n}");
}
println!("curve names ({}):", curves.len());
for (n, k) in &curves {
println!(" {n:<30} {k}");
}
set_actors.sort();
let we = worldentity.ok_or("no worldentity in any mounted WAD")?;
let we = WorldEntity::parse(&we).map_err(|e| format!("worldentity: {e}"))?;
let templates = mercs2_formats::mixr::kit_templates(&we)?;
let mut scripts = std::collections::HashMap::new();
for m in facecast::MODULES {
let name = m.to_ascii_lowercase();
let hash = mercs2_formats::hash::pandemic_hash_m2(&name);
let c = assets.resolve(TYPE_HASH_SCRIPT, hash).ok_or_else(|| format!("script {name}: not in any mounted WAD"))?;
scripts.insert(name, facecast::script_luaq(&c)?);
}
let fetch = move |name: &str| scripts.get(name).cloned().ok_or_else(|| format!("{name} is not one of facecast::MODULES"));
let mut wads = assets.paths().iter().map(|p| mercs2_engine::wad::open(p)).collect::<Result<Vec<_>, _>>()?;
let cast = facecast::Cast::read(fetch, |layer| facecast::layer_block(&mut wads, layer), &templates)?;
let mut faces = 0;
for (name, hash, _) in &set_actors {
match cast.sets.get(hash) {
Some(Ok(s)) => {
let asset = actor_hashes.contains(&s.actor);
faces += usize::from(asset);
println!(
" {name:<36} {:<24} model 0x{:08X} actor 0x{:08X}{} {}",
s.template,
s.model,
s.actor,
if asset { "" } else { " (NO SUCH ACTOR)" },
s.via
);
}
Some(Err(e)) => println!(" {name:<36} NONE: {e}"),
None => println!(" {name:<36} NONE: no script binds it"),
}
}
println!("sets with a speaker whose actor exists: {faces} of {}", set_actors.len());
for f in &failed {
eprintln!("FAILED {f}");
}
if failed.is_empty() {
Ok(())
} else {
Err(format!("{} of {} containers failed", failed.len(), actors + sets))
}
}