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::{Cursor, Read, Seek, SeekFrom};
use byteorder::{BigEndian, ReadBytesExt};
use crate::error::UffError;
use crate::types::*;

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

/// Parse a `uint16`-prefixed UTF-8 string (no null terminator).
fn read_str<R: Read>(r: &mut R) -> Result<String, UffError> {
    let len = r.read_u16::<BigEndian>()? as usize;
    let mut buf = vec![0u8; len];
    r.read_exact(&mut buf)?;
    Ok(String::from_utf8(buf)?)
}

fn read_hitbox<R: Read>(r: &mut R) -> Result<Hitbox, UffError> {
    let name  = read_str(r)?;
    let raw_type  = r.read_u8()?;
    let flags = r.read_u8()?;
    let x     = r.read_f32::<BigEndian>()?;
    let y     = r.read_f32::<BigEndian>()?;
    let width = r.read_f32::<BigEndian>()?;
    let height= r.read_f32::<BigEndian>()?;
    let damage     = r.read_u16::<BigEndian>()?;
    let hitstun    = r.read_u16::<BigEndian>()?;
    let blockstun  = r.read_u16::<BigEndian>()?;
    let knockback_angle    = r.read_f32::<BigEndian>()?;
    let knockback_strength = r.read_f32::<BigEndian>()?;
    let rotation   = r.read_f32::<BigEndian>()?;

    Ok(Hitbox {
        name,
        box_type: BoxType::from_u8(raw_type).unwrap_or(BoxType::Hurt),
        enabled:   (flags & 0x01) != 0,
        knockback: (flags & 0x02) != 0,
        x, y, width, height,
        damage, hitstun, blockstun,
        knockback_angle, knockback_strength, rotation,
    })
}

fn read_audio_cue<R: Read>(r: &mut R) -> Result<AudioCue, UffError> {
    let clip_id = read_str(r)?;
    let volume  = r.read_f32::<BigEndian>()?;
    let pitch   = r.read_f32::<BigEndian>()?;
    Ok(AudioCue { clip_id, volume, pitch })
}

fn read_frame_entry<R: Read>(r: &mut R) -> Result<HitboxFrame, UffError> {
    let id      = r.read_u16::<BigEndian>()?;
    let label   = read_str(r)?;
    let program = read_str(r)?;
    let tags    = read_str(r)?;

    let hitbox_count = r.read_u16::<BigEndian>()? as usize;
    let mut hitboxes = Vec::with_capacity(hitbox_count);
    for _ in 0..hitbox_count {
        hitboxes.push(read_hitbox(r)?);
    }

    let cue_count = r.read_u16::<BigEndian>()? as usize;
    let mut audio_cues = Vec::with_capacity(cue_count);
    for _ in 0..cue_count {
        audio_cues.push(read_audio_cue(r)?);
    }

    Ok(HitboxFrame { id, label, program, tags, hitboxes, audio_cues })
}

fn read_sprite_entry<R: Read>(r: &mut R) -> Result<SpriteEntry, UffError> {
    let src_x       = r.read_u16::<BigEndian>()?;
    let src_y       = r.read_u16::<BigEndian>()?;
    let src_w       = r.read_u16::<BigEndian>()?;
    let src_h       = r.read_u16::<BigEndian>()?;
    let pivot_x     = r.read_i16::<BigEndian>()?;
    let pivot_y     = r.read_i16::<BigEndian>()?;
    let entry_flags = r.read_u8()?;
    let tag         = r.read_u8()?;
    let duration_ms = r.read_u16::<BigEndian>()?;
    Ok(SpriteEntry { src_x, src_y, src_w, src_h, pivot_x, pivot_y, entry_flags, tag, duration_ms })
}

fn read_anim(data: &[u8], offset: u32) -> Result<UffAnimation, UffError> {
    let mut r = Cursor::new(data);
    r.seek(SeekFrom::Start(offset as u64))?;

    // AnimHeader — 26 bytes
    let name_len      = r.read_u16::<BigEndian>()? as usize;
    let frame_count   = r.read_u16::<BigEndian>()? as usize;
    let default_fps   = r.read_u16::<BigEndian>()?;
    let loop_mode_raw = r.read_u8()?;
    let pixel_flags   = r.read_u8()?;
    let sheet_w       = r.read_u16::<BigEndian>()?;
    let sheet_h       = r.read_u16::<BigEndian>()?;
    let frame_w       = r.read_u16::<BigEndian>()?;
    let frame_h       = r.read_u16::<BigEndian>()?;
    let hitbox_size   = r.read_u32::<BigEndian>()?;
    let pixel_size    = r.read_u32::<BigEndian>()?;
    let floor_y_override = r.read_u16::<BigEndian>()?;

    // AnimName
    let mut name_bytes = vec![0u8; name_len];
    r.read_exact(&mut name_bytes)?;
    let name = String::from_utf8(name_bytes)?;

    // SpriteTable
    let mut sprite_table = Vec::with_capacity(frame_count);
    for _ in 0..frame_count {
        sprite_table.push(read_sprite_entry(&mut r)?);
    }

    // HitboxBlock
    let frames = if hitbox_size > 0 {
        let mut frames = Vec::with_capacity(frame_count);
        for _ in 0..frame_count {
            frames.push(read_frame_entry(&mut r)?);
        }
        frames
    } else {
        Vec::new()
    };

    // PixelBlock
    let pixel_data = if pixel_size > 0 {
        let mut buf = vec![0u8; pixel_size as usize];
        r.read_exact(&mut buf)?;
        Some(buf)
    } else {
        None
    };

    Ok(UffAnimation {
        name,
        default_fps,
        loop_mode: LoopMode::from_u8(loop_mode_raw),
        pixel_flags,
        sheet_w, sheet_h, frame_w, frame_h,
        floor_y_override,
        sprite_table,
        frames,
        pixel_data,
    })
}

/// Parse a `.uff` file from a byte slice.
pub fn read_uff(data: &[u8]) -> Result<UffPackage, UffError> {
    let mut r = Cursor::new(data);

    // PackageHeader — 24 bytes
    let mut magic = [0u8; 4];
    r.read_exact(&mut magic)?;
    if magic != MAGIC {
        return Err(UffError::BadMagic(magic));
    }
    let version = r.read_u8()?;
    if version != 1 {
        return Err(UffError::UnsupportedVersion(version));
    }
    let _pkg_flags  = r.read_u8()?;
    let anim_count  = r.read_u16::<BigEndian>()? as usize;
    let dir_offset  = r.read_u32::<BigEndian>()?;
    r.seek(SeekFrom::Current(4))?; // reserved
    let floor_y     = r.read_u16::<BigEndian>()?;
    r.seek(SeekFrom::Current(6))?; // reserved

    // CharName (uint16-prefixed)
    let char_name = read_str(&mut r)?;

    // AnimOffsetTable
    r.seek(SeekFrom::Start(dir_offset as u64))?;
    let mut offsets = Vec::with_capacity(anim_count);
    for _ in 0..anim_count {
        offsets.push(r.read_u32::<BigEndian>()?);
    }

    // AnimBlocks
    let mut animations = Vec::with_capacity(anim_count);
    for off in offsets {
        animations.push(read_anim(data, off)?);
    }

    Ok(UffPackage { char_name, floor_y, animations })
}