Files
benilla-octo/crates/benilla-formats/examples/dump_model_anim.rs
T
SamandClaude Fable 5 6b980b9efb the player build: one binary that carries its content
- a player build ships beside the dev one — dev chords and probe surfaces go dark, and a gate enforces what a player can still reach, by medium not by module
- the binary carries its own shaders and its shipped UI, finds the WoW folder by itself, and keeps its local state in one benilla-config folder
- the glue screens get a capture shutter, and the engine stops depending on the protocol crate

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:18:02 +07:00

103 lines
3.8 KiB
Rust

//! What can this model move, at all? Print the [`M2AnimSummary`](benilla_formats::M2AnimSummary)
//! — every animation channel family — plus the texture-transform (UV-scroll) tracks, for one M2 or
//! for every M2 whose path matches a substring:
//!
//! ```text
//! cargo run -p benilla-formats --example dump_model_anim -- 'World\...\RubyCrystalLarge01.m2'
//! cargo run -p benilla-formats --example dump_model_anim -- maraudon # every match
//! ```
//!
//! This is the **falsifier for a flicker report** (decision 0653): if the summary says a model is
//! fully static and the screen shows it changing every frame, the change is not coming from the
//! model, and the hunt moves to what is drawn *over* it (particles, a second doodad) or to the
//! renderer. Answering that for the Maraudon crystal took one run of this and one of
//! `WOW_NO_PARTICLES=1`.
//!
//! Output is Blizzard data — pipe it to the scratchpad, never into the repo.
use std::io::Cursor;
fn main() -> anyhow::Result<()> {
let arg = std::env::args()
.nth(1)
.ok_or_else(|| anyhow::anyhow!("usage: dump_model_anim <m2 path | substring>"))?;
let data = benilla_formats::wow_data().expect("no WoW install found (set $WOW_DATA)");
let mut chain = benilla_formats::open_chain(&data)?;
// An exact path dumps in full; anything else is a substring sweep that prints only the models
// with something to say (a listing of "all static" is noise when you asked about a zone).
let exact = chain.contains(&arg);
let names: Vec<String> = if exact {
vec![arg]
} else {
let pat = arg.to_lowercase();
chain
.list()?
.into_iter()
.map(|e| e.name)
.filter(|n| {
let l = n.to_lowercase();
l.ends_with(".m2") && l.contains(&pat)
})
.collect()
};
for name in names {
let Ok(bytes) = chain.read_file(&name) else {
continue;
};
let Ok(sum) = benilla_formats::parse_m2_animation_summary(&bytes) else {
continue;
};
if !exact && sum.is_fully_static() {
continue;
}
println!("{name}");
if sum.is_fully_static() {
println!(" FULLY STATIC — no channel family animates anything");
}
println!(
" bones {} (seq0 motion {}) gseq {} alpha {:?} rgb {:?} transp {:?} \
texanim {} particles {} ribbons {}",
sum.seq0_animated_bone_count,
sum.seq0_has_bone_motion,
sum.global_seq_channels.len(),
sum.color_alpha_tracks,
sum.color_rgb_tracks,
sum.transparency_tracks,
sum.texture_transform_count,
sum.particle_emitter_count,
sum.ribbon_emitter_count,
);
if sum.texture_transform_count == 0 {
continue;
}
let Ok(fmt) = parse_m2(&bytes) else { continue };
let m = fmt.model();
println!(
" global_sequences {:?} texAnimLookup {:?}",
m.global_sequences, m.texture_transform_lookup
);
for (i, t) in m.texture_transforms.iter().enumerate() {
let tr = &t.translation;
println!(
" tt[{i}] translation: interp {} gseq {:#x} keys {}",
tr.interp,
tr.gseq,
tr.keys.len()
);
for (ts, v) in tr.keys.iter().take(8) {
println!(" t={ts:>7} ms {v:?}");
}
if tr.keys.len() > 8 {
println!(" … {} more", tr.keys.len() - 8);
}
}
}
Ok(())
}
fn parse_m2(bytes: &[u8]) -> anyhow::Result<benilla_m2::M2Format> {
benilla_m2::parse_m2(&mut Cursor::new(bytes)).map_err(|e| anyhow::anyhow!("parsing M2: {e}"))
}