Task 191: .ESF File Format

Task 191: .ESF File Format

File Format Specifications for .ESF

The .ESF file format refers to the Empire Total War Object Serialization Format, a binary container format used in Total War games (starting from Empire: Total War) for storing game data such as savegames, campaign settings, maps, and other serialized objects. It is a tree-like structure of nodes, similar to XML, with a header, body (nodes), and footer. The format has variants identified by magic numbers (0xABCD, 0xABCE, 0xABCF, 0xABCA). All data is little-endian. The specifications are based on detailed reverse-engineering notes from sources like taw's blog and Total War Center forums.

Key structure:

  • Header: Magic (4 bytes), optional zeros (4 bytes), optional timestamp (4 bytes), footer offset (4 bytes).
  • Body: Root node containing nested nodes (records, arrays, primitive values).
  • Footer: String tables (node names, types).
  • Nodes have types (byte identifier), optional name index (uint16), optional version (uint8), and content based on type.
  • Strings: Prefixed by uint16 length; ASCII (char8) or Unicode (char16).
  • Variable integers: uintvar for compact unsigned ints.

List of all the properties of this file format intrinsic to its file system:

  • Magic number (uint32): Identifies format variant (e.g., 0xABCE).
  • Zeros field (uint32, optional): Always 0x00000000 in some variants.
  • Timestamp (uint32, optional): Unix-like timestamp in some variants.
  • Footer offset (uint32): Pointer to start of footer.
  • Node type (uint8): Defines data type (e.g., 0x01 = bool8, 0x04 = int32, 0x0A = float32, 0x0C = XY coords, 0x80 = single record, 0x81 = multi-record, 0x40-0x4D = arrays).
  • Name index (uint16, optional): Index into footer string table for node name.
  • Version (uint8, optional): Node version for compatibility.
  • Content size/length (varies): For arrays/records (e.g., uint32 count, offsets).
  • Primitive values: bool8 (0x01), int8 (0x02), int16 (0x03), int32 (0x04), int64 (0x05), uint8 (0x06), uint16 (0x07), uint32 (0x08), uint64 (0x09), float32 (0x0A), float64 (0x0B), XY (float32 x2), XYZ (float32 x3), angle (uint16).
  • String types: ca_ascii (ASCII string), ca_unicode (UTF-16 string).
  • Exotic encodings: int24be/uint24be (big-endian 24-bit), uintvar (variable-length unsigned int).
  • Record nodes: Contain child nodes; single (0x80) or multiple instances (0x81).
  • Array nodes: Typed arrays (e.g., 0x40 = bool[], 0x44 = int32[], 0x4A = float32[]).
  • Footer: Offset lists for string tables (node names, types); each table has uint16 count, then repeated {uint16 length, string data}.

Two direct download links for files of format .ESF:

Ghost blog embedded HTML JavaScript for drag and drop .ESF file dump:

ESF File Dumper
Drag and drop .ESF file here
  1. Python class for .ESF open, decode, read, write, print properties:
import struct
import os

class ESFHandler:
    def __init__(self, filepath):
        self.filepath = filepath
        self.data = None
        self.header = None
        self.nodes = []  # Simplified storage

    def read(self):
        with open(self.filepath, 'rb') as f:
            self.data = f.read()
        self._decode()

    def _decode(self):
        offset = 0
        magic = struct.unpack_from('<I', self.data, offset)[0]
        offset += 4
        zeros = 0
        timestamp = 0
        if hex(magic) != '0xabcd':
            zeros = struct.unpack_from('<I', self.data, offset)[0]
            offset += 4
            timestamp = struct.unpack_from('<I', self.data, offset)[0]
            offset += 4
        footer_offset = struct.unpack_from('<I', self.data, offset)[0]
        offset += 4
        self.header = {'magic': hex(magic), 'zeros': zeros, 'timestamp': timestamp, 'footer_offset': footer_offset}

        # Simplified node parsing (example for a few nodes)
        while offset < footer_offset:
            node_type = struct.unpack_from('<B', self.data, offset)[0]
            offset += 1
            name_index = struct.unpack_from('<H', self.data, offset)[0]
            offset += 2
            version = struct.unpack_from('<B', self.data, offset)[0]
            offset += 1
            data = None
            if node_type == 0x01:  # bool8
                data = struct.unpack_from('<B', self.data, offset)[0] != 0
                offset += 1
            elif node_type == 0x04:  # int32
                data = struct.unpack_from('<i', self.data, offset)[0]
                offset += 4
            elif node_type == 0x0A:  # float32
                data = struct.unpack_from('<f', self.data, offset)[0]
                offset += 4
            elif node_type == 0x0F:  # ascii string
                len_ = struct.unpack_from('<H', self.data, offset)[0]
                offset += 2
                data = self.data[offset:offset + len_].decode('ascii')
                offset += len_
            # Add more as needed; full parser would recurse for records/arrays
            self.nodes.append({'type': hex(node_type), 'name_index': name_index, 'version': version, 'data': data})

    def print_properties(self):
        print("Header:")
        for k, v in self.header.items():
            print(f"{k}: {v}")
        print("\nNodes (sample):")
        for node in self.nodes[:10]:  # Limit for console
            print(node)

    def write(self, new_filepath=None):
        if not new_filepath:
            new_filepath = self.filepath + '.new'
        with open(new_filepath, 'wb') as f:
            # Simplified write: rewrite original data (full impl would rebuild from structures)
            f.write(self.data)
        print(f"Written to {new_filepath}")

# Example usage:
# handler = ESFHandler('path/to/file.esf')
# handler.read()
# handler.print_properties()
# handler.write()
  1. Java class for .ESF open, decode, read, write, print properties:
import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;

public class ESFHandler {
    private String filepath;
    private ByteBuffer buffer;
    private byte[] data;
    private String magic;
    private int zeros;
    private int timestamp;
    private int footerOffset;

    public ESFHandler(String filepath) {
        this.filepath = filepath;
    }

    public void read() throws IOException {
        File file = new File(filepath);
        data = new byte[(int) file.length()];
        try (FileInputStream fis = new FileInputStream(file)) {
            fis.read(data);
        }
        buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
        decode();
    }

    private void decode() {
        int magicInt = buffer.getInt();
        magic = Integer.toHexString(magicInt).toUpperCase();
        if (!"ABCD".equals(magic)) {
            zeros = buffer.getInt();
            timestamp = buffer.getInt();
        }
        footerOffset = buffer.getInt();

        // Simplified node parsing (example)
        System.out.println("Sample nodes:");
        for (int i = 0; i < 5 && buffer.position() < footerOffset; i++) {
            byte type = buffer.get();
            short nameIndex = buffer.getShort();
            byte version = buffer.get();
            Object dataVal = null;
            if (type == 0x01) {
                dataVal = buffer.get() != 0;
            } else if (type == 0x04) {
                dataVal = buffer.getInt();
            } else if (type == 0x0A) {
                dataVal = buffer.getFloat();
            } else if (type == 0x0F) {
                short len = buffer.getShort();
                byte[] strBytes = new byte[len];
                buffer.get(strBytes);
                dataVal = new String(strBytes, "ASCII");
            }
            System.out.println("Type: 0x" + Integer.toHexString(type) + ", NameIndex: " + nameIndex + ", Version: " + version + ", Data: " + dataVal);
        }
    }

    public void printProperties() {
        System.out.println("Magic: 0x" + magic);
        System.out.println("Zeros: " + zeros);
        System.out.println("Timestamp: " + timestamp);
        System.out.println("Footer Offset: " + footerOffset);
    }

    public void write(String newFilepath) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(newFilepath)) {
            fos.write(data);  // Simplified: write original
        }
    }

    // Example usage:
    // ESFHandler handler = new ESFHandler("path/to/file.esf");
    // handler.read();
    // handler.printProperties();
    // handler.write("new.esf");
}
  1. JavaScript class for .ESF open, decode, read, write, print properties (Node.js example):
const fs = require('fs');

class ESFHandler {
    constructor(filepath) {
        this.filepath = filepath;
        this.buffer = null;
        this.offset = 0;
        this.header = {};
    }

    read() {
        this.buffer = fs.readFileSync(this.filepath);
        this.decode();
    }

    readUint32() {
        const val = this.buffer.readUInt32LE(this.offset);
        this.offset += 4;
        return val;
    }

    readUint16() {
        const val = this.buffer.readUInt16LE(this.offset);
        this.offset += 2;
        return val;
    }

    readUint8() {
        const val = this.buffer.readUInt8(this.offset);
        this.offset += 1;
        return val;
    }

    readFloat32() {
        const val = this.buffer.readFloatLE(this.offset);
        this.offset += 4;
        return val;
    }

    readString(len, encoding = 'ascii') {
        const str = this.buffer.slice(this.offset, this.offset + len).toString(encoding);
        this.offset += len;
        return str;
    }

    decode() {
        const magic = this.readUint32().toString(16).toUpperCase();
        let zeros = 0, timestamp = 0;
        if (magic !== 'ABCD') {
            zeros = this.readUint32();
            timestamp = this.readUint32();
        }
        const footerOffset = this.readUint32();
        this.header = { magic: `0x${magic}`, zeros, timestamp, footerOffset };

        // Simplified node parsing
        const nodes = [];
        while (this.offset < footerOffset) {
            const type = this.readUint8();
            const nameIndex = this.readUint16();
            const version = this.readUint8();
            let data = null;
            if (type === 0x01) {
                data = this.readUint8() !== 0;
            } else if (type === 0x04) {
                data = this.readUint32();
            } else if (type === 0x0A) {
                data = this.readFloat32();
            } else if (type === 0x0F) {
                const len = this.readUint16();
                data = this.readString(len);
            }
            nodes.push({ type: `0x${type.toString(16)}`, nameIndex, version, data });
            if (nodes.length >= 10) break; // Limit
        }
        console.log('Nodes (sample):', nodes);
    }

    printProperties() {
        console.log('Header:', this.header);
    }

    write(newFilepath) {
        fs.writeFileSync(newFilepath || this.filepath + '.new', this.buffer);
    }
}

// Example usage:
// const handler = new ESFHandler('path/to/file.esf');
// handler.read();
// handler.printProperties();
// handler.write();
  1. C class (using C++ for class support) for .ESF open, decode, read, write, print properties:
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <cstdint>
#include <iomanip>

class ESFHandler {
private:
    std::string filepath;
    std::vector<uint8_t> data;
    uint32_t magic;
    uint32_t zeros;
    uint32_t timestamp;
    uint32_t footerOffset;

public:
    ESFHandler(const std::string& fp) : filepath(fp), zeros(0), timestamp(0) {}

    void read() {
        std::ifstream file(filepath, std::ios::binary | std::ios::ate);
        std::streamsize size = file.tellg();
        file.seekg(0, std::ios::beg);
        data.resize(size);
        file.read(reinterpret_cast<char*>(data.data()), size);
        decode();
    }

private:
    void decode() {
        size_t offset = 0;
        magic = *reinterpret_cast<uint32_t*>(&data[offset]);
        offset += 4;
        if (magic != 0xABCD) {
            zeros = *reinterpret_cast<uint32_t*>(&data[offset]);
            offset += 4;
            timestamp = *reinterpret_cast<uint32_t*>(&data[offset]);
            offset += 4;
        }
        footerOffset = *reinterpret_cast<uint32_t*>(&data[offset]);
        offset += 4;

        // Simplified node parsing
        std::cout << "Sample nodes:" << std::endl;
        for (int i = 0; i < 5 && offset < footerOffset; ++i) {
            uint8_t type = data[offset++];
            uint16_t nameIndex = *reinterpret_cast<uint16_t*>(&data[offset]);
            offset += 2;
            uint8_t version = data[offset++];
            std::string dataStr = "";
            if (type == 0x01) {
                dataStr = (data[offset++] != 0) ? "true" : "false";
            } else if (type == 0x04) {
                int32_t val = *reinterpret_cast<int32_t*>(&data[offset]);
                offset += 4;
                dataStr = std::to_string(val);
            } else if (type == 0x0A) {
                float val = *reinterpret_cast<float*>(&data[offset]);
                offset += 4;
                dataStr = std::to_string(val);
            } else if (type == 0x0F) {
                uint16_t len = *reinterpret_cast<uint16_t*>(&data[offset]);
                offset += 2;
                dataStr = std::string(&data[offset], &data[offset] + len);
                offset += len;
            }
            std::cout << "Type: 0x" << std::hex << static_cast<int>(type) << ", NameIndex: " << nameIndex << ", Version: " << static_cast<int>(version) << ", Data: " << dataStr << std::endl;
        }
    }

public:
    void printProperties() {
        std::cout << "Magic: 0x" << std::hex << magic << std::endl;
        std::cout << "Zeros: " << std::dec << zeros << std::endl;
        std::cout << "Timestamp: " << timestamp << std::endl;
        std::cout << "Footer Offset: " << footerOffset << std::endl;
    }

    void write(const std::string& newFilepath) {
        std::ofstream file(newFilepath, std::ios::binary);
        file.write(reinterpret_cast<const char*>(data.data()), data.size());
    }
};

// Example usage:
// int main() {
//     ESFHandler handler("path/to/file.esf");
//     handler.read();
//     handler.printProperties();
//     handler.write("new.esf");
//     return 0;
// }