Task 070: .BSP File Format

Task 070: .BSP File Format

File Format Specifications for .BSP

The .BSP file format, as used in the Source engine developed by Valve, is a binary format for storing map data in games such as Half-Life 2, Team Fortress 2, and Counter-Strike: Source. It organizes map geometry, textures, entities, visibility data, and other elements into a header followed by subsections known as lumps. The format begins with a fixed-size header that includes an identification string, version number, lump directory, and map revision. The lump directory consists of 64 entries, each describing the offset, length, version, and identifier for a specific lump. Lumps contain structured data specific to their type, with variations across engine versions (typically ranging from 17 to 47). Compression may be applied to certain lumps using LZMA on specific platforms.

List of all properties of this file format intrinsic to its structure:

  • Identification string: A 4-byte string, typically "VBSP" in little-endian order, confirming the file as a Source BSP.
  • Version: A 4-byte integer specifying the BSP format version (e.g., 20 for many Source games).
  • Lump directory: An array of 64 lump entries, each 16 bytes, containing:
  • File offset: 4-byte integer, position of the lump data from the start of the file.
  • File length: 4-byte integer, size of the lump data in bytes.
  • Lump version: 4-byte integer, version of the lump's internal format (often 0).
  • FourCC identifier: 4-byte character array, typically null or used for compression details.
  • Map revision: A 4-byte integer indicating the revision number of the map based on its source VMF file.

These properties define the core structure, enabling access to the 64 lumps (indexed 0-63), which include entities, planes, textures, vertices, visibility, nodes, and more. Each lump's data follows its own sub-structure, but the above are the header-level properties intrinsic to the format.

Two direct download links for .BSP files:

Ghost blog embedded HTML JavaScript for drag-and-drop .BSP file dumping:

BSP File Dumper
Drag and drop a .BSP file here
  1. Python class for .BSP handling:
import struct
import sys

class BSPFile:
    def __init__(self, filename=None):
        self.ident = 'VBSP'
        self.version = 0
        self.lumps = [(0, 0, 0, '\x00\x00\x00\x00') for _ in range(64)]  # (offset, length, version, fourCC)
        self.map_revision = 0
        self.filename = filename
        if filename:
            self.read(filename)

    def read(self, filename):
        with open(filename, 'rb') as f:
            data = f.read()
        # Unpack ident
        self.ident = struct.unpack_from('<4s', data, 0)[0].decode('ascii')
        # Unpack version
        self.version = struct.unpack_from('<i', data, 4)[0]
        # Unpack lumps
        for i in range(64):
            offset = 8 + i * 16
            fileofs, filelen, lump_version, fourCC = struct.unpack_from('<ii i 4s', data, offset)
            self.lumps[i] = (fileofs, filelen, lump_version, fourCC)
        # Unpack map revision
        self.map_revision = struct.unpack_from('<i', data, 1032)[0]

    def write(self, filename):
        with open(filename, 'wb') as f:
            # Write ident
            f.write(struct.pack('<4s', self.ident.encode('ascii')))
            # Write version
            f.write(struct.pack('<i', self.version))
            # Write lumps
            for lump in self.lumps:
                fileofs, filelen, lump_version, fourCC = lump
                f.write(struct.pack('<ii i 4s', fileofs, filelen, lump_version, fourCC))
            # Write map revision
            f.write(struct.pack('<i', self.map_revision))
            # Note: This writes only the header; actual lump data would need to be appended based on offsets.

    def print_properties(self):
        print(f"Identification: {self.ident}")
        print(f"Version: {self.version}")
        print("Lumps:")
        for i, lump in enumerate(self.lumps):
            fileofs, filelen, lump_version, fourCC = lump
            fourCC_str = ''.join([chr(b) if 32 <= b <= 126 else '\\x{:02x}'.format(b) for b in fourCC])
            print(f"Lump {i}: Offset={fileofs}, Length={filelen}, Version={lump_version}, FourCC='{fourCC_str}'")
        print(f"Map Revision: {self.map_revision}")

# Example usage
if __name__ == "__main__":
    if len(sys.argv) > 1:
        bsp = BSPFile(sys.argv[1])
        bsp.print_properties()

Note: The write method currently handles only the header; full implementation would require managing lump data positions and contents.

  1. Java class for .BSP handling:
import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.nio.file.*;

public class BSPFile {
    private String ident;
    private int version;
    private Lump[] lumps;
    private int mapRevision;
    private String filename;

    static class Lump {
        int fileofs;
        int filelen;
        int lumpVersion;
        byte[] fourCC = new byte[4];
    }

    public BSPFile(String filename) throws IOException {
        this.filename = filename;
        this.lumps = new Lump[64];
        for (int i = 0; i < 64; i++) {
            lumps[i] = new Lump();
        }
        if (filename != null) {
            read(filename);
        }
    }

    public void read(String filename) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate((int) Files.size(Paths.get(filename))).order(ByteOrder.LITTLE_ENDIAN);
        try (FileChannel channel = FileChannel.open(Paths.get(filename), StandardOpenOption.READ)) {
            channel.read(buffer);
        }
        buffer.flip();

        // Read ident
        byte[] identBytes = new byte[4];
        buffer.get(identBytes);
        ident = new String(identBytes);

        // Read version
        version = buffer.getInt();

        // Read lumps
        for (int i = 0; i < 64; i++) {
            lumps[i].fileofs = buffer.getInt();
            lumps[i].filelen = buffer.getInt();
            lumps[i].lumpVersion = buffer.getInt();
            buffer.get(lumps[i].fourCC);
        }

        // Read map revision
        mapRevision = buffer.getInt();
    }

    public void write(String filename) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(1036).order(ByteOrder.LITTLE_ENDIAN);

        // Write ident
        buffer.put(ident.getBytes());

        // Write version
        buffer.putInt(version);

        // Write lumps
        for (Lump lump : lumps) {
            buffer.putInt(lump.fileofs);
            buffer.putInt(lump.filelen);
            buffer.putInt(lump.lumpVersion);
            buffer.put(lump.fourCC);
        }

        // Write map revision
        buffer.putInt(mapRevision);

        buffer.flip();
        try (FileChannel channel = FileChannel.open(Paths.get(filename), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
            channel.write(buffer);
        }
        // Note: This writes only the header; lump data would need separate handling.
    }

    public void printProperties() {
        System.out.println("Identification: " + ident);
        System.out.println("Version: " + version);
        System.out.println("Lumps:");
        for (int i = 0; i < 64; i++) {
            Lump lump = lumps[i];
            String fourCCStr = new String(lump.fourCC).replaceAll("[^\\x20-\\x7E]", "?");
            System.out.printf("Lump %d: Offset=%d, Length=%d, Version=%d, FourCC='%s'%n", i, lump.fileofs, lump.filelen, lump.lumpVersion, fourCCStr);
        }
        System.out.println("Map Revision: " + mapRevision);
    }

    public static void main(String[] args) throws IOException {
        if (args.length > 0) {
            BSPFile bsp = new BSPFile(args[0]);
            bsp.printProperties();
        }
    }
}

Note: The write method handles only the header; full lump data management is beyond this scope but can be extended.

  1. JavaScript class for .BSP handling (Node.js compatible):
const fs = require('fs');

class BSPFile {
    constructor(filename = null) {
        this.ident = 'VBSP';
        this.version = 0;
        this.lumps = Array.from({length: 64}, () => ({fileofs: 0, filelen: 0, lumpVersion: 0, fourCC: '\x00\x00\x00\x00'}));
        this.mapRevision = 0;
        if (filename) {
            this.read(filename);
        }
    }

    read(filename) {
        const data = fs.readFileSync(filename);
        const view = new DataView(data.buffer);

        // Read ident
        this.ident = String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3));

        // Read version
        this.version = view.getInt32(4, true);

        // Read lumps
        for (let i = 0; i < 64; i++) {
            const offset = 8 + i * 16;
            this.lumps[i].fileofs = view.getInt32(offset, true);
            this.lumps[i].filelen = view.getInt32(offset + 4, true);
            this.lumps[i].lumpVersion = view.getInt32(offset + 8, true);
            this.lumps[i].fourCC = String.fromCharCode(view.getUint8(offset + 12), view.getUint8(offset + 13),
                                                       view.getUint8(offset + 14), view.getUint8(offset + 15));
        }

        // Read map revision
        this.mapRevision = view.getInt32(1032, true);
    }

    write(filename) {
        const buffer = Buffer.alloc(1036);
        const view = new DataView(buffer.buffer);

        // Write ident
        for (let i = 0; i < 4; i++) {
            view.setUint8(i, this.ident.charCodeAt(i));
        }

        // Write version
        view.setInt32(4, this.version, true);

        // Write lumps
        for (let i = 0; i < 64; i++) {
            const offset = 8 + i * 16;
            const lump = this.lumps[i];
            view.setInt32(offset, lump.fileofs, true);
            view.setInt32(offset + 4, lump.filelen, true);
            view.setInt32(offset + 8, lump.lumpVersion, true);
            for (let j = 0; j < 4; j++) {
                view.setUint8(offset + 12 + j, lump.fourCC.charCodeAt(j));
            }
        }

        // Write map revision
        view.setInt32(1032, this.mapRevision, true);

        fs.writeFileSync(filename, buffer);
        // Note: Header only; lump data not included.
    }

    printProperties() {
        console.log(`Identification: ${this.ident}`);
        console.log(`Version: ${this.version}`);
        console.log('Lumps:');
        this.lumps.forEach((lump, i) => {
            const fourCCStr = lump.fourCC.replace(/[\x00-\x1F\x7F-\xFF]/g, m => `\\x${m.charCodeAt(0).toString(16).padStart(2, '0')}`);
            console.log(`Lump ${i}: Offset=${lump.fileofs}, Length=${lump.filelen}, Version=${lump.lumpVersion}, FourCC='${fourCCStr}'`);
        });
        console.log(`Map Revision: ${this.mapRevision}`);
    }
}

// Example usage
if (process.argv.length > 2) {
    const bsp = new BSPFile(process.argv[2]);
    bsp.printProperties();
}

Note: Requires Node.js for file I/O; the write method outputs only the header.

  1. C++ class for .BSP handling:
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <iomanip>

struct Lump {
    int32_t fileofs;
    int32_t filelen;
    int32_t lumpVersion;
    char fourCC[4];
};

class BSPFile {
private:
    std::string ident;
    int32_t version;
    std::vector<Lump> lumps;
    int32_t mapRevision;
    std::string filename;

public:
    BSPFile(const std::string& fn = "") : version(0), mapRevision(0), filename(fn) {
        lumps.resize(64);
        ident = "VBSP";
        if (!fn.empty()) {
            read(fn);
        }
    }

    void read(const std::string& fn) {
        std::ifstream file(fn, std::ios::binary);
        if (!file) {
            std::cerr << "Failed to open file." << std::endl;
            return;
        }
        char buffer[1036];
        file.read(buffer, 1036);

        // Read ident
        ident.assign(buffer, 4);

        // Read version
        std::memcpy(&version, buffer + 4, sizeof(int32_t));

        // Read lumps
        for (size_t i = 0; i < 64; ++i) {
            size_t offset = 8 + i * 16;
            std::memcpy(&lumps[i].fileofs, buffer + offset, sizeof(int32_t));
            std::memcpy(&lumps[i].filelen, buffer + offset + 4, sizeof(int32_t));
            std::memcpy(&lumps[i].lumpVersion, buffer + offset + 8, sizeof(int32_t));
            std::memcpy(lumps[i].fourCC, buffer + offset + 12, 4);
        }

        // Read map revision
        std::memcpy(&mapRevision, buffer + 1032, sizeof(int32_t));
    }

    void write(const std::string& fn) {
        std::ofstream file(fn, std::ios::binary);
        if (!file) {
            std::cerr << "Failed to write file." << std::endl;
            return;
        }
        char buffer[1036] = {0};

        // Write ident
        std::memcpy(buffer, ident.c_str(), 4);

        // Write version
        std::memcpy(buffer + 4, &version, sizeof(int32_t));

        // Write lumps
        for (size_t i = 0; i < 64; ++i) {
            size_t offset = 8 + i * 16;
            std::memcpy(buffer + offset, &lumps[i].fileofs, sizeof(int32_t));
            std::memcpy(buffer + offset + 4, &lumps[i].filelen, sizeof(int32_t));
            std::memcpy(buffer + offset + 8, &lumps[i].lumpVersion, sizeof(int32_t));
            std::memcpy(buffer + offset + 12, lumps[i].fourCC, 4);
        }

        // Write map revision
        std::memcpy(buffer + 1032, &mapRevision, sizeof(int32_t));

        file.write(buffer, 1036);
        // Note: Header only; lump data not written.
    }

    void printProperties() const {
        std::cout << "Identification: " << ident << std::endl;
        std::cout << "Version: " << version << std::endl;
        std::cout << "Lumps:" << std::endl;
        for (size_t i = 0; i < 64; ++i) {
            std::cout << "Lump " << i << ": Offset=" << lumps[i].fileofs
                      << ", Length=" << lumps[i].filelen
                      << ", Version=" << lumps[i].lumpVersion
                      << ", FourCC='";
            for (int j = 0; j < 4; ++j) {
                char c = lumps[i].fourCC[j];
                if (c >= 32 && c <= 126) {
                    std::cout << c;
                } else {
                    std::cout << "\\x" << std::hex << std::setw(2) << std::setfill('0') << (int)(unsigned char)c;
                }
            }
            std::cout << "'" << std::endl;
        }
        std::cout << "Map Revision: " << mapRevision << std::endl;
    }
};

int main(int argc, char* argv[]) {
    if (argc > 1) {
        BSPFile bsp(argv[1]);
        bsp.printProperties();
    }
    return 0;
}

Note: The write method produces only the header; extending for full file support would involve handling lump data offsets and contents.