ufftools 0.1.0

reader/writer and inspect CLI for the UFF character package format. includes the uff decoder lib for use in other projects.
Documentation
use std::io::Write;
use byteorder::{BigEndian, WriteBytesExt};
use crate::error::UffError;
use crate::types::*;

const MAGIC: [u8; 4] = [b'U', b'F', b'F', 0x00];

fn write_str<W: Write>(w: &mut W, s: &str) -> Result<(), UffError> {
    let bytes = s.as_bytes();
    w.write_u16::<BigEndian>(bytes.len() as u16)?;
    w.write_all(bytes)?;
    Ok(())
}

fn write_hitbox<W: Write>(w: &mut W, h: &Hitbox) -> Result<(), UffError> {
    write_str(w, &h.name)?;
    w.write_u8(h.box_type as u8)?;
    let flags = (h.enabled as u8) | ((h.knockback as u8) << 1);
    w.write_u8(flags)?;
    w.write_f32::<BigEndian>(h.x)?;
    w.write_f32::<BigEndian>(h.y)?;
    w.write_f32::<BigEndian>(h.width)?;
    w.write_f32::<BigEndian>(h.height)?;
    w.write_u16::<BigEndian>(h.damage)?;
    w.write_u16::<BigEndian>(h.hitstun)?;
    w.write_u16::<BigEndian>(h.blockstun)?;
    w.write_f32::<BigEndian>(h.knockback_angle)?;
    w.write_f32::<BigEndian>(h.knockback_strength)?;
    w.write_f32::<BigEndian>(h.rotation)?;
    Ok(())
}

fn write_audio_cue<W: Write>(w: &mut W, c: &AudioCue) -> Result<(), UffError> {
    write_str(w, &c.clip_id)?;
    w.write_f32::<BigEndian>(c.volume)?;
    w.write_f32::<BigEndian>(c.pitch)?;
    Ok(())
}

fn write_frame_entry<W: Write>(w: &mut W, f: &HitboxFrame) -> Result<(), UffError> {
    w.write_u16::<BigEndian>(f.id)?;
    write_str(w, &f.label)?;
    write_str(w, &f.program)?;
    write_str(w, &f.tags)?;
    w.write_u16::<BigEndian>(f.hitboxes.len() as u16)?;
    for h in &f.hitboxes {
        write_hitbox(w, h)?;
    }
    w.write_u16::<BigEndian>(f.audio_cues.len() as u16)?;
    for c in &f.audio_cues {
        write_audio_cue(w, c)?;
    }
    Ok(())
}

fn write_sprite_entry<W: Write>(w: &mut W, e: &SpriteEntry) -> Result<(), UffError> {
    w.write_u16::<BigEndian>(e.src_x)?;
    w.write_u16::<BigEndian>(e.src_y)?;
    w.write_u16::<BigEndian>(e.src_w)?;
    w.write_u16::<BigEndian>(e.src_h)?;
    w.write_i16::<BigEndian>(e.pivot_x)?;
    w.write_i16::<BigEndian>(e.pivot_y)?;
    w.write_u8(e.entry_flags)?;
    w.write_u8(e.tag)?;
    w.write_u16::<BigEndian>(e.duration_ms)?;
    Ok(())
}

fn serialise_anim(anim: &UffAnimation) -> Result<Vec<u8>, UffError> {
    let mut buf: Vec<u8> = Vec::new();

    // Pre-compute HitboxBlock bytes so we know hitbox_size before writing the header.
    let hitbox_buf: Vec<u8> = if !anim.frames.is_empty() {
        let mut hb: Vec<u8> = Vec::new();
        for f in &anim.frames {
            write_frame_entry(&mut hb, f)?;
        }
        hb
    } else {
        Vec::new()
    };

    let pixel_size = anim.pixel_data.as_ref().map_or(0u32, |d| d.len() as u32);

    // AnimHeader — 26 bytes
    let name_bytes = anim.name.as_bytes();
    buf.write_u16::<BigEndian>(name_bytes.len() as u16)?;
    buf.write_u16::<BigEndian>(anim.sprite_table.len() as u16)?;
    buf.write_u16::<BigEndian>(anim.default_fps)?;
    buf.write_u8(anim.loop_mode as u8)?;
    buf.write_u8(anim.pixel_flags)?;
    buf.write_u16::<BigEndian>(anim.sheet_w)?;
    buf.write_u16::<BigEndian>(anim.sheet_h)?;
    buf.write_u16::<BigEndian>(anim.frame_w)?;
    buf.write_u16::<BigEndian>(anim.frame_h)?;
    buf.write_u32::<BigEndian>(hitbox_buf.len() as u32)?;
    buf.write_u32::<BigEndian>(pixel_size)?;
    buf.write_u16::<BigEndian>(anim.floor_y_override)?;

    // AnimName
    buf.write_all(name_bytes)?;

    // SpriteTable
    for e in &anim.sprite_table {
        write_sprite_entry(&mut buf, e)?;
    }

    // HitboxBlock
    if !hitbox_buf.is_empty() {
        buf.write_all(&hitbox_buf)?;
    }

    // PixelBlock
    if let Some(px) = &anim.pixel_data {
        buf.write_all(px)?;
    }

    Ok(buf)
}

/// Serialise a [`UffPackage`] to a byte vector.
pub fn write_uff(pkg: &UffPackage) -> Result<Vec<u8>, UffError> {
    // Serialise all AnimBlocks first so we can compute offsets.
    let anim_bufs: Vec<Vec<u8>> = pkg.animations.iter()
        .map(serialise_anim)
        .collect::<Result<_, _>>()?;

    let char_name_bytes = pkg.char_name.as_bytes();
    let anim_count = pkg.animations.len() as u16;

    // Fixed-size header section:
    //   PackageHeader  24
    //   CharName       2 + name_len
    //   AnimOffsetTable anim_count * 4
    let dir_offset: u32 = 24 + 2 + char_name_bytes.len() as u32;
    let first_anim_offset: u32 = dir_offset + anim_count as u32 * 4;

    let mut offsets: Vec<u32> = Vec::with_capacity(anim_count as usize);
    let mut cursor = first_anim_offset;
    for buf in &anim_bufs {
        offsets.push(cursor);
        cursor += buf.len() as u32;
    }

    let mut out: Vec<u8> = Vec::new();

    // PackageHeader
    out.write_all(&MAGIC)?;
    out.write_u8(1)?;               // version
    out.write_u8(0)?;               // pkg_flags
    out.write_u16::<BigEndian>(anim_count)?;
    out.write_u32::<BigEndian>(dir_offset)?;
    out.write_u32::<BigEndian>(0)?; // reserved
    out.write_u16::<BigEndian>(pkg.floor_y)?;
    out.write_all(&[0u8; 6])?;      // reserved

    // CharName
    out.write_u16::<BigEndian>(char_name_bytes.len() as u16)?;
    out.write_all(char_name_bytes)?;

    // AnimOffsetTable
    for off in &offsets {
        out.write_u32::<BigEndian>(*off)?;
    }

    // AnimBlocks
    for buf in &anim_bufs {
        out.write_all(buf)?;
    }

    Ok(out)
}