TypeScript library for reading and writing UTF (Universal Tree Format) files — a bespoke binary container format used by the Freelancer PC game (2003).
UTF stores a hierarchical tree of named directories and files inside a single binary blob. The on-disk layout consists of:
- Version + Header — magic signature (
UTF, version0x101), offsets and sizes for the three data regions. - Tree block — fixed-size (44-byte) entries describing the directory/file hierarchy.
- Dictionary block — NUL-terminated ASCII entry names, deduplicated by CRC32.
- Data block — raw file payloads.
npm install @treewyrm/utfRequires Node.js >= 18.
import { Directory } from '@treewyrm/utf'
import { readFileSync } from 'node:fs'
import BufferView from '@treewyrm/utf/dist/bufferview.js' // internal; use your own DataView wrapper if preferred
const bytes = readFileSync('ship.3db')
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
const root = Directory.read(view as any)
// Navigate the tree
const vmeshDir = root.getDirectory('MultiLevel', 'Level0', 'VMeshPart')
const meshFile = root.getFile('MultiLevel', 'Level0', 'VMeshPart', 'VMeshData')import { Directory, File } from '@treewyrm/utf'
const root = new Directory()
// Create nested directory and file
const file = root.setFile('Cmpnd', 'Root', 'Transform')
file.writeFloats(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
// Serialize to binary
const view = root.write()
const output = new Uint8Array(view.buffer, view.byteOffset, view.byteLength)File implements ArrayBufferView and provides typed read/write helpers:
const f = new File('data')
// Write
f.writeIntegers(1, 2, 3)
f.writeFloats(1.0, 2.0, 3.0)
f.writeStrings('alpha', 'beta') // NUL-separated
// Read (iterators)
for (const n of f.readIntegers()) console.log(n)
for (const x of f.readFloats()) console.log(x)
for (const s of f.readStrings()) console.log(s)
// Chain writes
f.writeFloats(0.5, 1.5).writeIntegers(42)
// Append raw data
f.append(someUint8Array)// Get (returns undefined if missing)
const dir = root.getDirectory('A', 'B', 'C')
const file = root.getFile('A', 'B', 'C', 'data')
// Get or create
const dir2 = root.setDirectory('A', 'B', 'C')
const file2 = root.setFile('A', 'B', 'C', 'data')
// Delete
root.delete('A', 'B', 'C')
// Replace/append children (replaces existing by name)
root.append(new Directory('NewDir'), new File('newfile'))
// Filtered children
const subdirs = root.directories // Directory[]
const files = root.files // File[]| Member | Description |
|---|---|
static read(view) |
Parses a UTF binary from a BufferView; returns root Directory |
write() |
Serializes the tree to a BufferView |
getDirectory(...path) |
Finds a nested directory by path segments |
setDirectory(...path) |
Finds or creates a nested directory |
getFile(...path) |
Finds a file by path (last segment is filename) |
setFile(...path) |
Finds or creates a file |
delete(...path) |
Removes all entries matching the path |
append(...entries) |
Inserts or replaces children by name |
directories |
Filtered list of child Directory instances |
files |
Filtered list of child File instances |
| Member | Description |
|---|---|
readIntegers() |
Iterator of signed integers (32/16/8-bit depending on remaining bytes) |
writeIntegers(...values) |
Appends values as 32-bit signed integers |
readFloats() |
Iterator of 32-bit floats |
writeFloats(...values) |
Appends values as 32-bit floats |
readStrings() |
Iterator of NUL-terminated strings |
writeStrings(...values) |
Appends NUL-separated strings |
append(...views) |
Appends raw ArrayBufferView data |
import {
toDOSTimestamp, fromDOSTimestamp,
toFileTime, fromFileTime,
toHex, isHex, parseHex,
getResourceId, getObjectId,
getResource, getObject,
type Hash,
} from '@treewyrm/utf/utils'| Export | Description |
|---|---|
toDOSTimestamp(date) |
Date → 32-bit DOS timestamp |
fromDOSTimestamp(value) |
32-bit DOS timestamp → Date |
toFileTime(date) |
Date → Windows 64-bit FILETIME (bigint) |
fromFileTime(value) |
Windows FILETIME → Date |
toHex(value, byteLength?, prefix?) |
Number to hex string |
isHex(value) |
Tests for 0x… hex string |
parseHex(value) |
Parses 0x… hex string |
getResourceId(value, caseSensitive?) |
CRC32 hash (materials, mesh names, UTF resources) |
getObjectId(value, caseSensitive?) |
id32 hash (object nicknames, INI references) |
getResource(items, predicate, value) |
Finds array entry by CRC32 key |
getObject(items, predicate, value) |
Finds array entry by id32 key |
Two hash algorithms match Freelancer's internal conventions:
getResourceId— Freelancer CRC32 (table extracted fromdacom.dll). Used for material names, mesh library names, and most UTF resource references.getObjectId— A byte-swapped CRC32 variant (id32). Used for object/archetype nicknames typically found in INI files.
Both accept number | string | ArrayBufferView | ArrayBufferLike and default to case-insensitive matching.
npm run build # compile TypeScript → dist/
npm test # build + run Node test runnerMIT