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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
# ufftools

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


- [library installation & usage]#library
- [defined types]#types
- [cli usage]#cli--uff-tools
- [uff specification]#umib-fighter-factory-export-format-specification-universal-fighter-frame-uff

---
### Library

Add to your `Cargo.toml`:

```shell
cargo install uff-tools
```

or manually:

```toml
[dependencies]
ufftools = { path = "path/to/ufftools" }
```

```toml
[dependencies]
ufftools = "0.1.0"
```
### Reading a file

```rust
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

```rust
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

```sh
cargo install --path .
```

### Subcommands

#### `inspect` — print the contents of a `.uff` file

```sh
# 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:

```sh
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]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](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

```rust
#[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

```js
// 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
```