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:
- https://github.com/danielmm8888/TF2Classic/raw/main/tf2c/maps/cp_badlands.bsp
- https://github.com/danielmm8888/TF2Classic/raw/main/tf2c/maps/cp_dustbowl.bsp
Ghost blog embedded HTML JavaScript for drag-and-drop .BSP file dumping:
- 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.
- 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.
- 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.
- 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.