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
  • Coverage
  • 25.64%
    20 out of 78 items documented0 out of 0 items with examples
  • Size
  • Source code size: 62.61 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 816.82 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 25s Average build duration of successful builds.
  • all releases: 25s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • dyxribo

ufftools

Reader, writer, and CLI inspector for the UFF (Universal Fighter Frame) character package format (.uff).


Library

Add to your Cargo.toml:

cargo install uff-tools

or manually:

[dependencies]
ufftools = { path = "path/to/ufftools" }
[dependencies]
ufftools = "0.1.0"

Reading a file

use ufftools::read_uff;

let data = std::fs::read("character.uff")?;
let pkg = read_uff(&data)?;

println!("{}{} animations", pkg.char_name, pkg.animations.len());

for anim in &pkg.animations {
    println!("  {} ({} frames)", anim.name, anim.sprite_table.len());
}

Writing a file

use ufftools::{write_uff, UffPackage, UffAnimation, LoopMode};

let pkg = UffPackage {
    char_name: "MyChar".into(),
    floor_y: 12,
    animations: vec![
        UffAnimation {
            name: "idle".into(),
            default_fps: 12,
            loop_mode: LoopMode::Loop,
            // ...
        }
    ],
};

let bytes = write_uff(&pkg)?;
std::fs::write("character.uff", &bytes)?;

Types

Type Description
UffPackage Top-level container: char_name, floor_y, animations
UffAnimation One animation: sprite table, hitbox frames, optional pixel data
SpriteEntry Per-frame sprite sheet coordinates, pivot, flags, duration
HitboxFrame Per-frame combat data: label, STAX program, tags, hitboxes, audio cues
Hitbox One combat box: type, position, size, damage, stun, knockback
AudioCue Audio event on a frame: clip ID, volume, pitch
BoxType Enum of 12 box types (Attack, Hurt, Grab, Invincible, …)
LoopMode Once, Loop, PingPong
UffError Parse/write error type

CLI — uff-tools

Install

cargo install --path .

Subcommands

inspect — print the contents of a .uff file

# summary (package header + one line per animation)
uff-tools inspect character.uff

# full frame, hitbox, and audio cue data
uff-tools inspect --verbose character.uff

# focus on one animation
uff-tools inspect --anim punch character.uff
uff-tools inspect -v --anim punch character.uff

Example output:

Character : Ryu
floor_y   : 12 px
Animations: 2

  [  0] "idle"  12 fps  loop  sheet 256×128  frames 2  floor_y inherit
        hitboxes: 1 across 1 frames  audio_cues: 0  pixel_data: 64 bytes
  [  1] "punch"  24 fps  once  sheet 256×128  frames 3  floor_y 10 px (override)
        hitboxes: 4 across 3 frames  audio_cues: 2  pixel_data: none

generate — write a sample fixture .uff to disk

Useful for testing and verifying a downstream parser:

uff-tools generate sample.uff

Format

.uff is the engine-consumable export of a .uffp project. All integers are big-endian. See the next section for the full binary spec.

UMIB Fighter Factory Export Format Specification: Universal Fighter Frame (.uff)

Universal Fighter Frame — the universal 2D game character binary character format. Originally created for fighting games, it can be used for any 2D characters that needs comprehensive hitbox and animation authoring. Derived from a .uffp project at export time. All hitbox and frame data is re-serialized into a universal, language-agnostic binary layout — 0 runtime dependencies.

For the editor-native project format, see uffp_format.md.


Relationship to .uffp

The container structure is identical. Only three things change on export:

Field / Block .uffp .uff
magic UFFP (0x55464650) UFF\x00 (0x55464600)
version 24 1
floor_y v4 only (0x10 in PackageHeader) always present (0x10 in PackageHeader)
floor_y_override v4 only (0x18 in AnimHeader) always present (0x18 in AnimHeader)
AnimHeader size 24 bytes (v2/v3) / 26 (v4) 26 bytes (always)
HitboxBlock encoding AMF3 writeObject() blob flat binary (see below)
Everything else unchanged identical

An exporter walks the live UFFPackage in memory and writes the same bytes for everything except the HitboxBlock, where it emits the flat layout instead of calling writeObject().


File Layout

┌─────────────────────────────────────────────┐
│  PackageHeader       24 bytes (fixed)        │
├─────────────────────────────────────────────┤
│  CharName            2 + nameLen bytes       │
├─────────────────────────────────────────────┤
│  AnimOffsetTable     animCount × 4 bytes     │
├─────────────────────────────────────────────┤
│  AnimBlock[0]        variable                │
│  AnimBlock[1]        variable                │
│  …                                           │
└─────────────────────────────────────────────┘

All multi-byte integers are big-endian.


PackageHeader — 24 bytes

Offset Size Field Value
0x00 4 magic UFF\x00 (0x55464600)
0x04 1 version 1
0x05 1 pkg_flags reserved, 0
0x06 2 anim_count uint16
0x08 4 dir_offset absolute offset to AnimOffsetTable
0x0C 4 reserved zeroed
0x10 2 floor_y uint16 — pixel distance from frame bottom to ground contact point (0 = bottom edge)
0x12 6 reserved zeroed

AnimOffsetTable

uint32[anim_count] — absolute byte offsets to each AnimBlock within the file.


AnimBlock

Four sequential sections per animation. AnimHeader, AnimName, SpriteTable, and PixelBlock are identical to .uffp. Only the HitboxBlock encoding changes.

AnimHeader — 26 bytes

Offset Size Field Notes
0x00 2 name_len byte length of AnimName
0x02 2 frame_count number of frames
0x04 2 default_fps playback rate hint
0x06 1 loop_mode 0=once, 1=loop, 2=ping-pong
0x07 1 pixel_flags see Pixel Flags in uffp_format.md
0x08 2 sheet_w pixel width of the spritesheet
0x0A 2 sheet_h pixel height of the spritesheet
0x0C 2 frame_w uniform frame width (0 = variable, use entry srcW)
0x0E 2 frame_h uniform frame height (0 = variable, use entry srcH)
0x10 4 hitbox_size byte size of HitboxBlock (0 = absent)
0x14 4 pixel_size byte size of PixelBlock (0 = hitbox-only animation)
0x18 2 floor_y_override per-animation ground anchor override (px from bottom); 0 = inherit from pkg floor_y

AnimName

name_len raw UTF-8 bytes — no null terminator.

SpriteTable — frame_count × 16 bytes

One entry per frame:

Offset Size Field Notes
0x00 2 src_x x origin in spritesheet
0x02 2 src_y y origin in spritesheet
0x04 2 src_w frame width (0 → use frame_w)
0x06 2 src_h frame height (0 → use frame_h)
0x08 2 pivot_x int16 hotspot x offset
0x0A 2 pivot_y int16 hotspot y offset
0x0C 1 entry_flags bit 0 = FLIP_H, bit 1 = FLIP_V
0x0D 1 tag animation group/tag ID
0x0E 2 duration_ms per-frame hold time override (0 = use fps)

HitboxBlock — hitbox_size bytes

frame_count sequential FrameEntry records in frame-index order. No count prefix — the count comes from AnimHeader. Absent when hitbox_size = 0.

FrameEntry

Field Type Notes
id uint16 frame index (0-based)
label uint16 + UTF-8 bytes empty frame = 0x0000 (zero length)
program uint16 + UTF-8 bytes STAX source; empty = 0x0000
tags uint16 + UTF-8 bytes key=val key=val ...; empty = 0x0000
hitbox_count uint16
hitbox[0..N] HitboxRecord × N
cue_count uint16 audio cues on this frame (0 = none)
cue[0..N] AudioCue × N absent when cue_count = 0

HitboxRecord

Field Type Size Notes
name uint16 + UTF-8 bytes variable
type uint8 1 0–11; see box type constants below
flags uint8 1 bit 0 = enabled, bit 1 = knockback
x float32 4 big-endian
y float32 4
width float32 4
height float32 4
damage uint16 2
hitstun uint16 2
blockstun uint16 2
knockback_angle float32 4
knockback_strength float32 4
rotation float32 4 normalized −180..180

Fixed portion (after name string): 36 bytes.

AudioCue

Field Type Notes
clip_id uint16 + UTF-8 bytes references a clip by name in the audio pool
volume float32 0.0–1.0
pitch float32 1.0 = normal

cue_count and cue[] are always present in the FrameEntry (a frame with no cues writes 0x0000 for cue_count). Readers on format version 1 should always read cue_count; a value of 0 costs two bytes and leaves the door open for audio without a version bump.

PixelBlock — pixel_size bytes

Identical to .uffp. See uffp_format.md for pixel flag meanings and decompressed layouts.


Box Type Constants

Value Name
0 ATTACK
1 ATTACK_INACTIVE
2 HURT
3 STUN
4 PHYSICAL_EXTENT
5 GRAB
6 GUARD_AIR
7 GUARD_HIGH
8 GUARD_MED
9 GUARD_LOW
10 THROW_INVINCIBLE
11 INVINCIBLE

Versioning

The version field in PackageHeader gates HitboxBlock layout expectations.

Version HitboxBlock encoding Audio cues
1 flat binary yes (cue_count always present)

Future additions to FrameEntry or HitboxRecord append new fields at the tail. Readers should skip to hitbox_size bytes past the start of the HitboxBlock if they encounter an unknown version, rather than failing.


Parsing Pseudocode

Minimal reference for engine implementors:

read magic[4]          assert == "UFF\x00"
read version[1]        assert == 1
read pkg_flags[1]
read anim_count[2]
read dir_offset[4]
skip 4                 // reserved
read floor_y[2]        // uint16, px from bottom (0 = bottom edge)
skip 6                 // reserved

seek dir_offset
read offsets[anim_count * 4]

for each anim_offset:
  seek anim_offset
  read AnimHeader (26 bytes)
  // AnimHeader: name_len, frame_count, default_fps, loop_mode, pixel_flags,
  //             sheet_w, sheet_h, frame_w, frame_h, hitbox_size, pixel_size,
  //             floor_y_override (0 = inherit pkg floor_y)
  read AnimName (name_len bytes)
  read SpriteTable (frame_count * 16 bytes)

  if hitbox_size > 0:
    for i in 0..frame_count:
      read id[2]
      read label_len[2], label[label_len]
      read program_len[2], program[program_len]
      read tags_len[2], tags[tags_len]
      read hitbox_count[2]
      for j in 0..hitbox_count:
        read name_len[2], name[name_len]
        read type[1], flags[1]
        read x[4], y[4], w[4], h[4]        // float32 big-endian
        read damage[2], hitstun[2], blockstun[2]
        read knockback_angle[4], knockback_strength[4], rotation[4]
      read cue_count[2]
      for k in 0..cue_count:
        read clip_id_len[2], clip_id[clip_id_len]
        read volume[4], pitch[4]

  if pixel_size > 0:
    read PixelBlock (pixel_size bytes)  // decompress per uffp_format.md

Rust Struct Sketch

#[derive(Debug)]
struct UffPackage {
    char_name: String,
    animations: Vec<UffAnimation>,
}

#[derive(Debug)]
struct UffAnimation {
    name: String,
    fps: u16,
    loop_mode: u8,
    frame_w: u16,
    frame_h: u16,
    sprite_table: Vec<SpriteEntry>,
    frames: Vec<HitboxFrame>,
    pixel_data: Option<Vec<u8>>,   // zlib-compressed, decompress per pixel_flags
}

#[derive(Debug)]
struct HitboxFrame {
    id: u16,
    label: String,
    program: String,
    tags: String,
    hitboxes: Vec<Hitbox>,
    audio_cues: Vec<AudioCue>,
}

#[derive(Debug)]
struct Hitbox {
    name: String,
    box_type: u8,
    enabled: bool,
    knockback: bool,
    x: f32, y: f32, width: f32, height: f32,
    damage: u16, hitstun: u16, blockstun: u16,
    knockback_angle: f32,
    knockback_strength: f32,
    rotation: f32,
}

#[derive(Debug)]
struct AudioCue {
    clip_id: String,
    volume: f32,
    pitch: f32,
}

JS Struct Sketch

// reading with DataView (big-endian)
function readUtf16Str(view, offset) {
  const len = view.getUint16(offset);
  offset += 2;
  const bytes = new Uint8Array(view.buffer, offset, len);
  return [new TextDecoder().decode(bytes), offset + len];
}

// HitboxRecord fixed fields start after the name string:
//   type(1) flags(1) x(4) y(4) w(4) h(4) dmg(2) hs(2) bs(2) ka(4) ks(4) rot(4) = 36 bytes