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.

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .FF Property Dump

.FF File Analyzer
Drag and drop a .FF file here to analyze properties.
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.