use std::{fs, process};
use ufftools::{read_uff, BoxType, LoopMode, SpriteEntry, UffAnimation, UffPackage};
fn loop_label(m: LoopMode) -> &'static str {
match m {
LoopMode::Once => "once",
LoopMode::Loop => "loop",
LoopMode::PingPong => "ping-pong",
}
}
fn box_label(t: BoxType) -> &'static str {
match t {
BoxType::Attack => "ATTACK",
BoxType::AttackInactive => "ATTACK_INACTIVE",
BoxType::Hurt => "HURT",
BoxType::Stun => "STUN",
BoxType::PhysicalExtent => "PHYSICAL_EXTENT",
BoxType::Grab => "GRAB",
BoxType::GuardAir => "GUARD_AIR",
BoxType::GuardHigh => "GUARD_HIGH",
BoxType::GuardMed => "GUARD_MED",
BoxType::GuardLow => "GUARD_LOW",
BoxType::ThrowInvincible => "THROW_INVINCIBLE",
BoxType::Invincible => "INVINCIBLE",
}
}
fn flag_str(e: &SpriteEntry) -> String {
let mut f = String::new();
if e.entry_flags & 0x01 != 0 { f.push_str("FLIP_H "); }
if e.entry_flags & 0x02 != 0 { f.push_str("FLIP_V "); }
if f.is_empty() { f.push('-'); }
f.trim_end().to_string()
}
fn print_package_header(pkg: &UffPackage) {
println!("Character : {}", pkg.char_name);
println!("floor_y : {} px", pkg.floor_y);
println!("Animations: {}", pkg.animations.len());
}
fn print_anim_summary(idx: usize, a: &UffAnimation) {
let floor = if a.floor_y_override > 0 {
format!("{} px (override)", a.floor_y_override)
} else {
"inherit".to_string()
};
println!(
" [{idx:>3}] \"{name}\" {fps} fps {lm} sheet {sw}×{sh} frames {fc} floor_y {floor}",
idx = idx,
name = a.name,
fps = a.default_fps,
lm = loop_label(a.loop_mode),
sw = a.sheet_w,
sh = a.sheet_h,
fc = a.sprite_table.len(),
floor = floor,
);
let hb_frames = a.frames.iter().filter(|f| !f.hitboxes.is_empty()).count();
let total_hb: usize = a.frames.iter().map(|f| f.hitboxes.len()).sum();
let total_cues: usize = a.frames.iter().map(|f| f.audio_cues.len()).sum();
let px = a.pixel_data.as_ref().map_or("none".into(), |d| format!("{} bytes", d.len()));
println!(
" hitboxes: {total_hb} across {hb_frames} frames audio_cues: {total_cues} pixel_data: {px}"
);
}
fn print_anim_verbose(a: &UffAnimation) {
println!(" Sprite table ({} entries):", a.sprite_table.len());
for (i, e) in a.sprite_table.iter().enumerate() {
println!(
" [{i:>3}] src ({sx},{sy}) {sw}×{sh} pivot ({px},{py}) tag {tag} dur {dur}ms flags {flags}",
i = i,
sx = e.src_x, sy = e.src_y,
sw = e.src_w, sh = e.src_h,
px = e.pivot_x, py = e.pivot_y,
tag = e.tag,
dur = e.duration_ms,
flags = flag_str(e),
);
}
if a.frames.is_empty() {
println!(" Hitbox data: absent");
return;
}
println!(" Frames ({}):", a.frames.len());
for f in &a.frames {
let label = if f.label.is_empty() { "-".into() } else { format!("\"{}\"", f.label) };
let tags = if f.tags.is_empty() { "-".into() } else { f.tags.clone() };
let program = if f.program.is_empty() { "-".into() } else { format!("{} line(s)", f.program.lines().count()) };
println!(
" frame {id} label {label} tags [{tags}] program {program}",
id = f.id, label = label, tags = tags, program = program,
);
if !f.program.is_empty() {
for line in f.program.lines() {
println!(" | {line}");
}
}
for hb in &f.hitboxes {
let flags = format!(
"{}{}",
if hb.enabled { "enabled " } else { "disabled " },
if hb.knockback { "knockback" } else { "" },
);
println!(
" hb \"{name}\" {typ} {flags}",
name = hb.name,
typ = box_label(hb.box_type),
flags = flags.trim(),
);
println!(
" pos ({x:.2},{y:.2}) size {w:.2}×{h:.2} rot {rot:.1}°",
x = hb.x, y = hb.y, w = hb.width, h = hb.height, rot = hb.rotation,
);
if hb.damage > 0 || hb.hitstun > 0 || hb.blockstun > 0 {
println!(
" dmg {d} hitstun {hs} blockstun {bs} kb {ks:.2}@{ka:.1}°",
d = hb.damage, hs = hb.hitstun, bs = hb.blockstun,
ks = hb.knockback_strength, ka = hb.knockback_angle,
);
}
}
for cue in &f.audio_cues {
println!(
" cue \"{}\" vol {:.2} pitch {:.2}",
cue.clip_id, cue.volume, cue.pitch,
);
}
}
}
pub fn inspect_package(file: &str, verbose: bool, anim: Option<&str>) {
let data = fs::read(file).unwrap_or_else(|e| {
eprintln!("error: cannot read \"{file}\": {e}");
process::exit(1);
});
let pkg = read_uff(&data).unwrap_or_else(|e| {
eprintln!("error: parse failed: {e}");
process::exit(1);
});
print_package_header(&pkg);
let anims: Vec<(usize, &UffAnimation)> = if let Some(name) = anim {
let matches: Vec<_> = pkg.animations.iter()
.enumerate()
.filter(|(_, a)| a.name == name)
.collect();
if matches.is_empty() {
eprintln!("error: no animation named \"{name}\"");
eprintln!("available:");
for a in &pkg.animations { eprintln!(" {}", a.name); }
process::exit(1);
}
matches
} else {
pkg.animations.iter().enumerate().collect()
};
println!();
for (idx, a) in &anims {
print_anim_summary(*idx, a);
if verbose {
println!();
print_anim_verbose(a);
println!();
}
}
}