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 crate::*;

fn minimal_package() -> UffPackage {
    UffPackage {
        char_name: "TestChar".into(),
        floor_y: 8,
        animations: vec![
            UffAnimation {
                name: "idle".into(),
                default_fps: 12,
                loop_mode: LoopMode::Loop,
                pixel_flags: 0,
                sheet_w: 128,
                sheet_h: 64,
                frame_w: 64,
                frame_h: 64,
                floor_y_override: 0,
                sprite_table: vec![
                    SpriteEntry {
                        src_x: 0, src_y: 0, src_w: 0, src_h: 0,
                        pivot_x: -32, pivot_y: -32,
                        entry_flags: 0, tag: 0, duration_ms: 0,
                    },
                    SpriteEntry {
                        src_x: 64, src_y: 0, src_w: 0, src_h: 0,
                        pivot_x: -32, pivot_y: -32,
                        entry_flags: 1, tag: 0, duration_ms: 83,
                    },
                ],
                frames: vec![
                    HitboxFrame {
                        id: 0,
                        label: "start".into(),
                        program: "".into(),
                        tags: "vulnerable=true".into(),
                        hitboxes: vec![
                            Hitbox {
                                name: "body".into(),
                                box_type: BoxType::Hurt,
                                enabled: true,
                                knockback: false,
                                x: 10.0, y: 5.0, width: 40.0, height: 55.0,
                                damage: 0, hitstun: 0, blockstun: 0,
                                knockback_angle: 0.0, knockback_strength: 0.0,
                                rotation: 0.0,
                            },
                        ],
                        audio_cues: vec![],
                    },
                    HitboxFrame {
                        id: 1,
                        label: "".into(),
                        program: "LOO 0\nLOP".into(),
                        tags: "".into(),
                        hitboxes: vec![],
                        audio_cues: vec![
                            AudioCue {
                                clip_id: "step".into(),
                                volume: 0.8,
                                pitch: 1.0,
                            },
                        ],
                    },
                ],
                pixel_data: Some(vec![0xDE, 0xAD, 0xBE, 0xEF]),
            },
        ],
    }
}

#[test]
fn round_trip_minimal() {
    let original = minimal_package();
    let bytes = write_uff(&original).expect("write failed");
    let parsed = read_uff(&bytes).expect("read failed");

    assert_eq!(parsed.char_name, original.char_name);
    assert_eq!(parsed.floor_y,   original.floor_y);
    assert_eq!(parsed.animations.len(), 1);

    let anim = &parsed.animations[0];
    assert_eq!(anim.name, "idle");
    assert_eq!(anim.default_fps, 12);
    assert!(matches!(anim.loop_mode, LoopMode::Loop));
    assert_eq!(anim.sheet_w, 128);
    assert_eq!(anim.frame_w, 64);
    assert_eq!(anim.sprite_table.len(), 2);
    assert_eq!(anim.sprite_table[1].src_x, 64);
    assert_eq!(anim.sprite_table[1].entry_flags, 1);
    assert_eq!(anim.sprite_table[1].duration_ms, 83);

    assert_eq!(anim.frames.len(), 2);
    let f0 = &anim.frames[0];
    assert_eq!(f0.id, 0);
    assert_eq!(f0.label, "start");
    assert_eq!(f0.tags, "vulnerable=true");
    assert_eq!(f0.hitboxes.len(), 1);
    let hb = &f0.hitboxes[0];
    assert_eq!(hb.name, "body");
    assert!(matches!(hb.box_type, BoxType::Hurt));
    assert!(hb.enabled);
    assert!(!hb.knockback);
    assert_eq!(hb.width, 40.0);
    assert_eq!(f0.audio_cues.len(), 0);

    let f1 = &anim.frames[1];
    assert_eq!(f1.program, "LOO 0\nLOP");
    assert_eq!(f1.audio_cues.len(), 1);
    assert_eq!(f1.audio_cues[0].clip_id, "step");
    assert_eq!(f1.audio_cues[0].volume, 0.8);

    assert_eq!(anim.pixel_data, Some(vec![0xDE, 0xAD, 0xBE, 0xEF]));
}

#[test]
fn bad_magic_rejected() {
    let mut bytes = write_uff(&minimal_package()).unwrap();
    bytes[0] = b'X';
    assert!(read_uff(&bytes).is_err());
}

#[test]
fn bad_version_rejected() {
    let mut bytes = write_uff(&minimal_package()).unwrap();
    bytes[4] = 99;
    assert!(read_uff(&bytes).is_err());
}

#[test]
fn no_hitboxes_no_pixel_data() {
    let pkg = UffPackage {
        char_name: "Empty".into(),
        floor_y: 0,
        animations: vec![UffAnimation {
            name: "stand".into(),
            default_fps: 1,
            loop_mode: LoopMode::Once,
            pixel_flags: 0,
            sheet_w: 0, sheet_h: 0, frame_w: 0, frame_h: 0,
            floor_y_override: 0,
            sprite_table: vec![],
            frames: vec![],
            pixel_data: None,
        }],
    };
    let bytes = write_uff(&pkg).unwrap();
    let parsed = read_uff(&bytes).unwrap();
    assert!(parsed.animations[0].frames.is_empty());
    assert!(parsed.animations[0].pixel_data.is_none());
}