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:
[]
= { = "path/to/ufftools" }
[]
= "0.1.0"
Reading a file
use read_uff;
let data = read?;
let pkg = read_uff?;
println!;
for anim in &pkg.animations
Writing a file
use ;
let pkg = UffPackage ;
let bytes = write_uff?;
write?;
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
Subcommands
inspect — print the contents of a .uff file
# summary (package header + one line per animation)
# full frame, hitbox, and audio cue data
# focus on one animation
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:
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 |
2–4 |
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
JS Struct Sketch
// reading with DataView (big-endian)
// 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