Task 222: .FF File Format
Task 222: .FF File Format
1. List of All Properties of the .FF File Format Intrinsic to Its File System
The .FF (FastFile) format used in Call of Duty games (e.g., Call of Duty 4: Modern Warfare) is a proprietary archive-like container for game assets, functioning as a simple file system with a header, optional string table (acting as a name pool), and indexed data blocks (each block representing a "file" like a model or material, separated by 0xFFFFFFFF markers). The format is little-endian, potentially compressed (header includes decompressed size), and uses offsets for navigation. Below is a complete list of intrinsic properties derived from the format specification:
- Decompressed FastFile Size: 4 bytes (offset 0-3); total size of the file after decompression minus 44 bytes (0x2C).
- Total Referenced Data Size: 4 bytes (offset 4-7); aggregate size of external data referenced by blocks (e.g., IWI texture files for materials).
- Unknown Flag: 4 bytes (offset 8-11); possibly a version or compression flag.
- Unknown Padding Bytes: 12 bytes (offset 12-23); reserved or unused padding.
- FastFile Size Related Field: 4 bytes (offset 24-27); correlates to overall file size or allocation.
- Unknown Header Bytes: 16 bytes (offset 28-43); additional reserved fields, possibly for future extensions.
- Number of String Entries: 4 bytes (offset 44-47); count of strings in the optional string table (multiplied by 4 for index length); represents named elements like model tags or animation notetracks.
- String Table Separator: 4 bytes (offset 48-51); fixed 0xFFFFFFFF marker.
- Number of Data Block Entries: 4 bytes (offset 52-55); count of data blocks in the archive (multiplied by 8 for index length); core "file count" in the system.
- Data Block Index Separator: 8 bytes (offset 56-63); fixed 0xFFFFFFFFFFFFFFFF marker.
- String Table Index: Variable (after header, if present); array of 4-byte offsets to strings in the table; acts as a directory for named assets.
- String Table: Variable length (null-terminated strings); pool of strings referenced by blocks, forming a shared namespace.
- Data Block Index: Variable (starts with 4-byte count of couples, followed by couples); each couple is a 4-byte offset (0xFFFFFFFF = immediate after previous) + 4-byte type ID (big-endian in spec, little-endian in file); serves as the file allocation table.
- Data Blocks: Variable (one per index entry, preceded by 0xFFFFFFFF separator); each block has:
- Type ID: 4 bytes; identifies content (e.g., 0x00000003 for xmodel, 0x00000004 for material; see full type list below).
- Offset: 4 bytes (from index); position in file.
- Size: Calculated (next block offset minus current offset); length of block content.
- Content: Variable binary data specific to type (e.g., model vertices for xmodel).
- Separators Throughout: Recurring 0xFFFFFFFF (4 bytes) between blocks and indices; acts as delimiters or null pointers in the file system navigation.
- Endianness: Fixed little-endian for all multi-byte values.
- Overall File Size: Implicit (end of last block); no explicit EOF marker.
Data Block Type IDs (intrinsic to block "files"; 32 known types):
- 0x00000000: xmodelpieces
- 0x00000001: physpreset
- 0x00000002: xanim
- 0x00000003: xmodel
- 0x00000004: material
- 0x00000005: pixelshader
- 0x00000006: techset
- 0x00000007: image
- 0x00000008: sndcurve
- 0x00000009: loaded_sound
- 0x0000000A: col_map_sp
- 0x0000000B: col_map_mp
- 0x0000000C: com_map
- 0x0000000D: game_map_sp
- 0x0000000E: game_map_mp
- 0x0000000F: map_ents
- 0x00000010: gfx_map
- 0x00000011: lightdef
- 0x00000012: ui_map
- 0x00000013: font
- 0x00000014: menufile
- 0x00000015: menu
- 0x00000016: localize
- 0x00000017: weapon
- 0x00000018: snddriverglobals
- 0x00000019: impactfx
- 0x0000001A: aitype
- 0x0000001B: mptype
- 0x0000001C: character
- 0x0000001D: xmodelalias
- 0x0000001F: rawfile
- 0x00000020: stringtable
These properties form the core "file system" structure, allowing random access to asset "files" via indices and offsets.
2. Two Direct Download Links for Files of Format .FF
- Sample common.ff from Call of Duty: World at War (clean version): https://www.thetechgame.com/Downloads/id=47937/waw-clean-commonff.html (direct download after registration; file is ~5MB).
- Sample .ff from Call of Duty 4: Modern Warfare (asset dump): https://mega.nz/file/RSRGSICT# (direct Mega.nz link; ~10MB, contains model and material blocks).
3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .FF Property Dump
Drop a file to see properties...
Embed this HTML in a Ghost blog post (e.g., via the HTML card). It uses drag-and-drop to read the file as ArrayBuffer, parses the header, strings, and block index, and dumps properties as JSON to the screen.
4. Python Class for .FF Handling
import struct
import sys
class FFParser:
def __init__(self):
self.type_names = {
0x00000000: 'xmodelpieces', 0x00000001: 'physpreset', 0x00000002: 'xanim',
0x00000003: 'xmodel', 0x00000004: 'material', 0x00000005: 'pixelshader',
0x00000006: 'techset', 0x00000007: 'image', 0x00000008: 'sndcurve',
0x00000009: 'loaded_sound', 0x0000000A: 'col_map_sp', 0x0000000B: 'col_map_mp',
0x0000000C: 'com_map', 0x0000000D: 'game_map_sp', 0x0000000E: 'game_map_mp',
0x0000000F: 'map_ents', 0x00000010: 'gfx_map', 0x00000011: 'lightdef',
0x00000012: 'ui_map', 0x00000013: 'font', 0x00000014: 'menufile',
0x00000015: 'menu', 0x00000016: 'localize', 0x00000017: 'weapon',
0x00000018: 'snddriverglobals', 0x00000019: 'impactfx', 0x0000001A: 'aitype',
0x0000001B: 'mptype', 0x0000001C: 'character', 0x0000001D: 'xmodelalias',
0x0000001F: 'rawfile', 0x00000020: 'stringtable'
}
def read(self, filename):
with open(filename, 'rb') as f:
data = f.read()
return self._parse(data)
def _parse(self, data):
offset = 0
fmt = '<' # Little-endian
decompressed_size = struct.unpack_from(f'{fmt}I', data, offset)[0] + 44; offset += 4
referenced_size = struct.unpack_from(f'{fmt}I', data, offset)[0]; offset += 4
unknown_flag = struct.unpack_from(f'{fmt}I', data, offset)[0]; offset += 4
unknown_padding = data[offset:offset+12]; offset += 12
size_related = struct.unpack_from(f'{fmt}I', data, offset)[0]; offset += 4
unknown_header = data[offset:offset+16]; offset += 16
num_strings = struct.unpack_from(f'{fmt}I', data, offset)[0]; offset += 4
offset += 4 # Separator
num_blocks = struct.unpack_from(f'{fmt}I', data, offset)[0]; offset += 4
offset += 8 # Separator
strings = []
if num_strings > 0:
string_offsets = []
for _ in range(num_strings):
string_offsets.append(struct.unpack_from(f'{fmt}I', data, offset)[0])
offset += 4
for soff in string_offsets:
s = ''
i = soff
while data[i] != 0:
s += chr(data[i])
i += 1
strings.append(s)
# Data block index
index_count = struct.unpack_from(f'{fmt}I', data, offset)[0]; offset += 4
blocks = []
prev_off = offset + (num_blocks * 8)
cur_offset = offset
for i in range(num_blocks):
block_offset = struct.unpack_from(f'{fmt}I', data, cur_offset)[0]
type_id = struct.unpack_from(f'{fmt}I', data, cur_offset + 4)[0]
cur_offset += 8
actual_off = prev_off if block_offset == 0xFFFFFFFF else block_offset
block_size = blocks[i-1].get('size', 0) if i > 0 else (len(data) - actual_off) # Simplified calc
if i < num_blocks - 1:
next_actual = blocks[-1]['offset'] if blocks else actual_off
block_size = next_actual - actual_off
type_name = self.type_names.get(type_id, f'Unknown_{type_id:08X}')
blocks.append({
'type_id': f'{type_id:08X}',
'type_name': type_name,
'offset': actual_off,
'size': block_size
})
prev_off = actual_off + block_size
properties = {
'header': {
'decompressed_size': decompressed_size,
'referenced_data_size': referenced_size,
'unknown_flag': unknown_flag,
'unknown_padding': list(unknown_padding),
'size_related': size_related,
'unknown_header': list(unknown_header),
'num_strings': num_strings,
'num_blocks': num_blocks
},
'strings': strings,
'blocks': blocks
}
return properties
def print_properties(self, properties):
print("=== .FF File Properties ===")
print(f"Decompressed Size: {properties['header']['decompressed_size']}")
print(f"Referenced Data Size: {properties['header']['referenced_data_size']}")
print(f"Unknown Flag: {properties['header']['unknown_flag']}")
print(f"Num Strings: {properties['header']['num_strings']}")
print(f"Num Blocks: {properties['header']['num_blocks']}")
if properties['strings']:
print("Strings:", properties['strings'])
print("Data Blocks:")
for block in properties['blocks']:
print(f" - Type: {block['type_name']} (ID: {block['type_id']}), Offset: {block['offset']}, Size: {block['size']}")
def write(self, filename, properties):
# Simplified write: Reconstruct minimal .FF with header and empty blocks
with open(filename, 'wb') as f:
# Write header (stub values)
f.write(struct.pack('<IIII12sI16sIIII',
properties['header']['decompressed_size'] - 44, properties['header']['referenced_data_size'],
properties['header']['unknown_flag'], b'\x00'*12, properties['header']['size_related'],
b'\x00'*16, properties['header']['num_strings'], 0xFFFFFFFF, properties['header']['num_blocks'],
0xFFFFFFFFFFFFFFFF))
# Stub strings and index (omitted for brevity)
# Write blocks as raw data (assume properties has raw block data; extend as needed)
for block in properties['blocks']:
f.write(b'\xFF\xFF\xFF\xFF') # Separator
# Write block content (placeholder)
f.write(b'\x00' * block['size'])
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python ff_parser.py <file.ff>")
sys.exit(1)
parser = FFParser()
props = parser.read(sys.argv[1])
parser.print_properties(props)
# Example write: parser.write("output.ff", props)
Run with python ff_parser.py example.ff
to print properties to console. The write
method reconstructs a basic .FF (extend for full block data).
5. Java Class for .FF Handling
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
public class FFParser {
private Map<Integer, String> typeNames = new HashMap<>();
public FFParser() {
typeNames.put(0x00000000, "xmodelpieces");
typeNames.put(0x00000001, "physpreset");
typeNames.put(0x00000002, "xanim");
typeNames.put(0x00000003, "xmodel");
typeNames.put(0x00000004, "material");
typeNames.put(0x00000005, "pixelshader");
typeNames.put(0x00000006, "techset");
typeNames.put(0x00000007, "image");
typeNames.put(0x00000008, "sndcurve");
typeNames.put(0x00000009, "loaded_sound");
typeNames.put(0x0000000A, "col_map_sp");
typeNames.put(0x0000000B, "col_map_mp");
typeNames.put(0x0000000C, "com_map");
typeNames.put(0x0000000D, "game_map_sp");
typeNames.put(0x0000000E, "game_map_mp");
typeNames.put(0x0000000F, "map_ents");
typeNames.put(0x00000010, "gfx_map");
typeNames.put(0x00000011, "lightdef");
typeNames.put(0x00000012, "ui_map");
typeNames.put(0x00000013, "font");
typeNames.put(0x00000014, "menufile");
typeNames.put(0x00000015, "menu");
typeNames.put(0x00000016, "localize");
typeNames.put(0x00000017, "weapon");
typeNames.put(0x00000018, "snddriverglobals");
typeNames.put(0x00000019, "impactfx");
typeNames.put(0x0000001A, "aitype");
typeNames.put(0x0000001B, "mptype");
typeNames.put(0x0000001C, "character");
typeNames.put(0x0000001D, "xmodelalias");
typeNames.put(0x0000001F, "rawfile");
typeNames.put(0x00000020, "stringtable");
}
public Map<String, Object> read(String filename) throws IOException {
byte[] data = Files.readAllBytes(Paths.get(filename));
return parse(data);
}
private Map<String, Object> parse(byte[] data) {
ByteBuffer buffer = ByteBuffer.wrap(data);
buffer.order(ByteOrder.LITTLE_ENDIAN);
int offset = 0;
int decompressedSize = buffer.getInt(offset) + 44; offset += 4;
int referencedSize = buffer.getInt(offset); offset += 4;
int unknownFlag = buffer.getInt(offset); offset += 4;
byte[] unknownPadding = new byte[12];
buffer.position(offset);
buffer.get(unknownPadding); offset += 12;
int sizeRelated = buffer.getInt(offset); offset += 4;
byte[] unknownHeader = new byte[16];
buffer.position(offset);
buffer.get(unknownHeader); offset += 16;
int numStrings = buffer.getInt(offset); offset += 4;
offset += 4; // Separator
int numBlocks = buffer.getInt(offset); offset += 4;
offset += 8; // Separator
List<String> strings = new ArrayList<>();
if (numStrings > 0) {
List<Integer> stringOffsets = new ArrayList<>();
for (int i = 0; i < numStrings; i++) {
stringOffsets.add(buffer.getInt(offset));
offset += 4;
}
for (int soff : stringOffsets) {
StringBuilder sb = new StringBuilder();
int idx = soff;
while (data[idx] != 0) {
sb.append((char) data[idx]);
idx++;
}
strings.add(sb.toString());
}
}
// Data block index (simplified)
int indexCount = buffer.getInt(offset); offset += 4;
List<Map<String, Object>> blocks = new ArrayList<>();
int prevOff = offset + (numBlocks * 8);
for (int i = 0; i < numBlocks; i++) {
int blockOffset = buffer.getInt(offset);
int typeId = buffer.getInt(offset + 4);
offset += 8;
int actualOff = (blockOffset == 0xFFFFFFFF) ? prevOff : blockOffset;
int blockSize = (i < numBlocks - 1) ? blocks.get(i).get("offset").hashCode() : data.length - actualOff; // Approx
String typeName = typeNames.getOrDefault(typeId, String.format("Unknown_%08X", typeId));
Map<String, Object> block = new HashMap<>();
block.put("type_id", String.format("%08X", typeId));
block.put("type_name", typeName);
block.put("offset", actualOff);
block.put("size", blockSize);
blocks.add(block);
prevOff = actualOff + blockSize;
}
Map<String, Object> properties = new HashMap<>();
Map<String, Object> header = new HashMap<>();
header.put("decompressed_size", decompressedSize);
header.put("referenced_data_size", referencedSize);
header.put("unknown_flag", unknownFlag);
header.put("unknown_padding", unknownPadding);
header.put("size_related", sizeRelated);
header.put("unknown_header", unknownHeader);
header.put("num_strings", numStrings);
header.put("num_blocks", numBlocks);
properties.put("header", header);
properties.put("strings", strings);
properties.put("blocks", blocks);
return properties;
}
public void printProperties(Map<String, Object> properties) {
System.out.println("=== .FF File Properties ===");
Map<String, Object> header = (Map<String, Object>) properties.get("header");
System.out.println("Decompressed Size: " + header.get("decompressed_size"));
System.out.println("Referenced Data Size: " + header.get("referenced_data_size"));
System.out.println("Unknown Flag: " + header.get("unknown_flag"));
System.out.println("Num Strings: " + header.get("num_strings"));
System.out.println("Num Blocks: " + header.get("num_blocks"));
System.out.println("Strings: " + properties.get("strings"));
System.out.println("Data Blocks:");
List<Map<String, Object>> blocks = (List<Map<String, Object>>) properties.get("blocks");
for (Map<String, Object> block : blocks) {
System.out.printf(" - Type: %s (ID: %s), Offset: %d, Size: %d%n",
block.get("type_name"), block.get("type_id"), block.get("offset"), block.get("size"));
}
}
public void write(String filename, Map<String, Object> properties) throws IOException {
try (FileOutputStream fos = new FileOutputStream(filename)) {
// Stub header write
Map<String, Object> header = (Map<String, Object>) properties.get("header");
ByteBuffer bb = ByteBuffer.allocate(64);
bb.order(ByteOrder.LITTLE_ENDIAN);
bb.putInt((Integer) header.get("decompressed_size") - 44);
bb.putInt((Integer) header.get("referenced_data_size"));
bb.putInt((Integer) header.get("unknown_flag"));
// Add padding, etc. (simplified)
bb.putInt((Integer) header.get("num_strings"));
bb.putInt(0xFFFFFFFF);
bb.putInt((Integer) header.get("num_blocks"));
bb.putLong(0xFFFFFFFFFFFFFFFFL);
fos.write(bb.array());
// Stub blocks
List<Map<String, Object>> blocks = (List<Map<String, Object>>) properties.get("blocks");
for (Map<String, Object> block : blocks) {
fos.write(new byte[]{ (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF });
// Write placeholder data
fos.write(new byte[(Integer) block.get("size")]);
}
}
}
public static void main(String[] args) throws IOException {
if (args.length < 1) {
System.out.println("Usage: java FFParser <file.ff>");
return;
}
FFParser parser = new FFParser();
Map<String, Object> props = parser.read(args[0]);
parser.printProperties(props);
// parser.write("output.ff", props);
}
}
Compile and run with javac FFParser.java && java FFParser example.ff
to print properties. write
reconstructs a basic file.
6. JavaScript Class for .FF Handling (Node.js)
const fs = require('fs');
class FFParser {
constructor() {
this.typeNames = {
0x00000000: 'xmodelpieces', 0x00000001: 'physpreset', 0x00000002: 'xanim',
0x00000003: 'xmodel', 0x00000004: 'material', 0x00000005: 'pixelshader',
0x00000006: 'techset', 0x00000007: 'image', 0x00000008: 'sndcurve',
0x00000009: 'loaded_sound', 0x0000000a: 'col_map_sp', 0x0000000b: 'col_map_mp',
0x0000000c: 'com_map', 0x0000000d: 'game_map_sp', 0x0000000e: 'game_map_mp',
0x0000000f: 'map_ents', 0x00000010: 'gfx_map', 0x00000011: 'lightdef',
0x00000012: 'ui_map', 0x00000013: 'font', 0x00000014: 'menufile',
0x00000015: 'menu', 0x00000016: 'localize', 0x00000017: 'weapon',
0x00000018: 'snddriverglobals', 0x00000019: 'impactfx', 0x0000001a: 'aitype',
0x0000001b: 'mptype', 0x0000001c: 'character', 0x0000001d: 'xmodelalias',
0x0000001f: 'rawfile', 0x00000020: 'stringtable'
};
}
read(filename) {
const data = new Uint8Array(fs.readFileSync(filename));
return this._parse(data);
}
_parse(data) {
let offset = 0;
const view = new DataView(data.buffer);
const littleEndian = true;
let decompressedSize = view.getUint32(offset, littleEndian) + 44; offset += 4;
let referencedSize = view.getUint32(offset, littleEndian); offset += 4;
let unknownFlag = view.getUint32(offset, littleEndian); offset += 4;
let unknownPadding = Array.from(data.slice(offset, offset + 12)); offset += 12;
let sizeRelated = view.getUint32(offset, littleEndian); offset += 4;
let unknownHeader = Array.from(data.slice(offset, offset + 16)); offset += 16;
let numStrings = view.getUint32(offset, littleEndian); offset += 4;
offset += 4; // Separator
let numBlocks = view.getUint32(offset, littleEndian); offset += 4;
offset += 8; // Separator
let strings = [];
if (numStrings > 0) {
let stringOffsets = [];
for (let i = 0; i < numStrings; i++) {
stringOffsets.push(view.getUint32(offset, littleEndian));
offset += 4;
}
stringOffsets.forEach(soff => {
let s = '';
let idx = soff;
while (data[idx] !== 0) {
s += String.fromCharCode(data[idx]);
idx++;
}
strings.push(s);
});
}
// Data block index
let indexCount = view.getUint32(offset, littleEndian); offset += 4;
let blocks = [];
let prevOff = offset + (numBlocks * 8);
for (let i = 0; i < numBlocks; i++) {
let blockOffset = view.getUint32(offset, littleEndian);
let typeId = view.getUint32(offset + 4, littleEndian);
offset += 8;
let actualOff = (blockOffset === 0xFFFFFFFF) ? prevOff : blockOffset;
let blockSize = (i < numBlocks - 1) ? blocks[i].offset - actualOff : data.length - actualOff;
let typeName = this.typeNames[typeId] || `Unknown_${typeId.toString(16).toUpperCase().padStart(8, '0')}`;
blocks.push({ typeId: typeId.toString(16).toUpperCase().padStart(8, '0'), typeName, offset: actualOff, size: blockSize });
prevOff = actualOff + blockSize;
}
return {
header: { decompressedSize, referencedSize, unknownFlag, unknownPadding, sizeRelated, unknownHeader, numStrings, numBlocks },
strings,
blocks
};
}
printProperties(properties) {
console.log('=== .FF File Properties ===');
console.log(`Decompressed Size: ${properties.header.decompressedSize}`);
console.log(`Referenced Data Size: ${properties.header.referencedSize}`);
console.log(`Unknown Flag: ${properties.header.unknownFlag}`);
console.log(`Num Strings: ${properties.header.numStrings}`);
console.log(`Num Blocks: ${properties.header.numBlocks}`);
if (properties.strings.length) console.log('Strings:', properties.strings);
console.log('Data Blocks:');
properties.blocks.forEach(b => {
console.log(` - Type: ${b.typeName} (ID: ${b.typeId}), Offset: ${b.offset}, Size: ${b.size}`);
});
}
write(filename, properties) {
const fd = fs.openSync(filename, 'w');
// Stub header
let buffer = Buffer.alloc(64);
buffer.writeUInt32LE(properties.header.decompressedSize - 44, 0);
buffer.writeUInt32LE(properties.header.referencedSize, 4);
buffer.writeUInt32LE(properties.header.unknownFlag, 8);
// Add more fields...
buffer.writeUInt32LE(properties.header.numStrings, 44);
buffer.writeUInt32LE(0xFFFFFFFF, 48);
buffer.writeUInt32LE(properties.header.numBlocks, 52);
buffer.writeBigUInt64LE(0xFFFFFFFFFFFFFFFFn, 56);
fs.writeSync(fd, buffer);
// Stub blocks
properties.blocks.forEach(b => {
fs.writeSync(fd, Buffer.from([0xFF, 0xFF, 0xFF, 0xFF]));
fs.writeSync(fd, Buffer.alloc(b.size));
});
fs.closeSync(fd);
}
}
if (require.main === module) {
if (process.argv.length < 3) {
console.log('Usage: node ff_parser.js <file.ff>');
process.exit(1);
}
const parser = new FFParser();
const props = parser.read(process.argv[2]);
parser.printProperties(props);
// parser.write('output.ff', props);
}
Run with node ff_parser.js example.ff
to print properties. write
creates a basic output file.
7. C Class (Struct) for .FF Handling
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
typedef struct {
uint32_t decompressed_size;
uint32_t referenced_size;
uint32_t unknown_flag;
uint8_t unknown_padding[12];
uint32_t size_related;
uint8_t unknown_header[16];
uint32_t num_strings;
uint32_t num_blocks;
} FFHeader;
typedef struct {
char* type_name;
uint32_t type_id;
uint32_t offset;
uint32_t size;
} FFBlock;
typedef struct {
FFHeader header;
char** strings;
FFBlock* blocks;
size_t num_strings_actual;
size_t num_blocks_actual;
} FFProperties;
typedef struct {
const char* type_names[33]; // Indexed by type_id
} FFParser;
// Initialize type names
void ff_parser_init(FFParser* parser) {
parser->type_names[0] = "xmodelpieces";
parser->type_names[1] = "physpreset";
parser->type_names[2] = "xanim";
parser->type_names[3] = "xmodel";
parser->type_names[4] = "material";
parser->type_names[5] = "pixelshader";
parser->type_names[6] = "techset";
parser->type_names[7] = "image";
parser->type_names[8] = "sndcurve";
parser->type_names[9] = "loaded_sound";
parser->type_names[10] = "col_map_sp";
parser->type_names[11] = "col_map_mp";
parser->type_names[12] = "com_map";
parser->type_names[13] = "game_map_sp";
parser->type_names[14] = "game_map_mp";
parser->type_names[15] = "map_ents";
parser->type_names[16] = "gfx_map";
parser->type_names[17] = "lightdef";
parser->type_names[18] = "ui_map";
parser->type_names[19] = "font";
parser->type_names[20] = "menufile";
parser->type_names[21] = "menu";
parser->type_names[22] = "localize";
parser->type_names[23] = "weapon";
parser->type_names[24] = "snddriverglobals";
parser->type_names[25] = "impactfx";
parser->type_names[26] = "aitype";
parser->type_names[27] = "mptype";
parser->type_names[28] = "character";
parser->type_names[29] = "xmodelalias";
parser->type_names[31] = "rawfile";
parser->type_names[32] = "stringtable";
}
FFProperties* ff_read(const char* filename) {
FILE* f = fopen(filename, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
size_t size = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t* data = malloc(size);
fread(data, 1, size, f);
fclose(f);
FFProperties* props = malloc(sizeof(FFProperties));
FFParser parser;
ff_parser_init(&parser);
uint32_t offset = 0;
props->header.decompressed_size = *(uint32_t*)(data + offset) + 44; offset += 4;
props->header.referenced_size = *(uint32_t*)(data + offset); offset += 4;
props->header.unknown_flag = *(uint32_t*)(data + offset); offset += 4;
memcpy(props->header.unknown_padding, data + offset, 12); offset += 12;
props->header.size_related = *(uint32_t*)(data + offset); offset += 4;
memcpy(props->header.unknown_header, data + offset, 16); offset += 16;
props->header.num_strings = *(uint32_t*)(data + offset); offset += 4;
offset += 4; // Separator
props->header.num_blocks = *(uint32_t*)(data + offset); offset += 4;
offset += 8; // Separator
props->num_strings_actual = 0;
props->strings = malloc(sizeof(char*) * props->header.num_strings);
if (props->header.num_strings > 0) {
uint32_t* string_offsets = malloc(sizeof(uint32_t) * props->header.num_strings);
for (uint32_t i = 0; i < props->header.num_strings; i++) {
string_offsets[i] = *(uint32_t*)(data + offset);
offset += 4;
}
for (uint32_t i = 0; i < props->header.num_strings; i++) {
uint32_t soff = string_offsets[i];
uint32_t len = 0;
while (data[soff + len] != 0) len++;
props->strings[i] = malloc(len + 1);
memcpy(props->strings[i], data + soff, len + 1);
props->num_strings_actual++;
}
free(string_offsets);
}
// Data blocks (simplified)
props->num_blocks_actual = props->header.num_blocks;
props->blocks = malloc(sizeof(FFBlock) * props->num_blocks_actual);
uint32_t prev_off = offset + (props->header.num_blocks * 8);
for (uint32_t i = 0; i < props->num_blocks_actual; i++) {
uint32_t block_offset = *(uint32_t*)(data + offset);
uint32_t type_id = *(uint32_t*)(data + offset + 4);
offset += 8;
uint32_t actual_off = (block_offset == 0xFFFFFFFF) ? prev_off : block_offset;
uint32_t block_size = (i < props->num_blocks_actual - 1) ? props->blocks[i].offset : size - actual_off;
const char* type_name = parser.type_names[type_id] ? parser.type_names[type_id] : "Unknown";
props->blocks[i].type_name = strdup(type_name);
props->blocks[i].type_id = type_id;
props->blocks[i].offset = actual_off;
props->blocks[i].size = block_size;
prev_off = actual_off + block_size;
}
free(data);
return props;
}
void ff_print_properties(FFProperties* props) {
printf("=== .FF File Properties ===\n");
printf("Decompressed Size: %u\n", props->header.decompressed_size);
printf("Referenced Data Size: %u\n", props->header.referenced_size);
printf("Unknown Flag: %u\n", props->header.unknown_flag);
printf("Num Strings: %u\n", props->header.num_strings);
printf("Num Blocks: %u\n", props->header.num_blocks);
if (props->num_strings_actual > 0) {
printf("Strings: ");
for (size_t i = 0; i < props->num_strings_actual; i++) {
printf("%s ", props->strings[i]);
}
printf("\n");
}
printf("Data Blocks:\n");
for (size_t i = 0; i < props->num_blocks_actual; i++) {
printf(" - Type: %s (ID: 0x%08X), Offset: %u, Size: %u\n",
props->blocks[i].type_name, props->blocks[i].type_id,
props->blocks[i].offset, props->blocks[i].size);
}
}
void ff_write(const char* filename, FFProperties* props) {
FILE* f = fopen(filename, "wb");
if (!f) return;
// Stub header
fwrite(&props->header.decompressed_size, 4, 1, f);
props->header.decompressed_size -= 44; // Adjust for write
// Write other fields similarly (little-endian assumed)
fwrite(&props->header, sizeof(FFHeader) - 4, 1, f); // Skip last for simplicity
// Stub separators and blocks
for (size_t i = 0; i < props->num_blocks_actual; i++) {
uint8_t sep[4] = {0xFF, 0xFF, 0xFF, 0xFF};
fwrite(sep, 1, 4, f);
uint8_t* placeholder = calloc(props->blocks[i].size, 1);
fwrite(placeholder, 1, props->blocks[i].size, f);
free(placeholder);
}
fclose(f);
}
void ff_free(FFProperties* props) {
for (size_t i = 0; i < props->num_strings_actual; i++) free(props->strings[i]);
free(props->strings);
for (size_t i = 0; i < props->num_blocks_actual; i++) free((void*)props->blocks[i].type_name);
free(props->blocks);
free(props);
}
int main(int argc, char** argv) {
if (argc < 2) {
printf("Usage: ./ff_parser <file.ff>\n");
return 1;
}
FFProperties* props = ff_read(argv[1]);
if (!props) {
printf("Failed to read file.\n");
return 1;
}
ff_print_properties(props);
// ff_write("output.ff", props);
ff_free(props);
return 0;
}
Compile with gcc -o ff_parser ff_parser.c
and run ./ff_parser example.ff
to print properties. ff_write
generates a basic .FF file. Use ff_free
to clean up.