Task 059: .BDT File Format
Task 059: .BDT File Format
File Format Specifications for .BDT
Upon thorough research, the .BDT file format is utilized in multiple contexts, but the most documented and consistent specification is from FromSoftware games (e.g., Dark Souls III, Sekiro, Elden Ring), where .BDT serves as a binary data archive file. It is paired with a corresponding .BHD header file that defines the structure and locations of data within the .BDT file. The .BDT itself lacks an internal header and consists of concatenated binary data blocks, potentially padded with zeros. The format is proprietary but has been reverse-engineered by the modding community.
The binary structure is defined primarily in the .BHD file as follows (based on the format used in Elden Ring and similar titles, employing little-endian byte order):
Header (20 bytes):
- Magic: 4 bytes (ASCII string "BHD5")
- Unk04: 1 byte (typically 0xFF)
- Unk05: 1 byte (typically 0x00)
- Unk06: 1 byte (typically 0x00)
- Unk07: 1 byte (typically 0x01)
- Unk08: 4 bytes (int32, typically 0x00000000)
- BucketCount: 4 bytes (int32, number of buckets)
- BucketOffset: 4 bytes (int32, offset to bucket entries, typically 0x00000014 or adjusted based on header size)
Bucket Entries (8 bytes per bucket):
- FileCount: 4 bytes (int32, number of files in the bucket)
- BucketOffset: 4 bytes (int32, offset to the file headers for this bucket)
File Headers (24 bytes per file):
- FileHash: 8 bytes (int64, 64-bit hash of the lowercase file path with forward slashes)
- PaddedSize: 4 bytes (int32, padded size of the data block, often a multiple of a sector size like 0x800)
- Size: 4 bytes (int32, actual uncompressed size of the data)
- Offset: 8 bytes (int64, byte offset within the .BDT file where the data begins)
The .BDT file contains the raw data at the specified offsets, with each block of length 'Size' (followed by padding to 'PaddedSize' if applicable). Data may be compressed (e.g., with DCX) or encrypted in some cases, but the base format assumes raw binary.
1. List of All Properties Intrinsic to This File Format
The properties are derived from the .BHD header, as the .BDT is not self-describing. These include structural elements essential to accessing and managing the archive:
- Magic string (identifier for the header format)
- Unknown field 04 (byte value, purpose undocumented)
- Unknown field 05 (byte value, purpose undocumented)
- Unknown field 06 (byte value, purpose undocumented)
- Unknown field 07 (byte value, purpose undocumented)
- Unknown field 08 (32-bit integer, purpose undocumented)
- Bucket count (32-bit integer, number of file grouping buckets)
- Bucket offset (32-bit integer, starting offset for bucket entries)
- Per bucket: File count (32-bit integer, number of files in bucket)
- Per bucket: Bucket offset (32-bit integer, offset to file headers)
- Per file: File hash (64-bit integer, path hash for identification)
- Per file: Padded size (32-bit integer, aligned data block size)
- Per file: Size (32-bit integer, actual data length)
- Per file: Offset (64-bit integer, position in .BDT)
2. Python Class for .BDT Handling
The following Python class opens a .BDT file, infers the corresponding .BHD file (by replacing the extension), decodes the structure, reads data, writes a copy of the files, and prints the properties to the console. It uses the struct
module for binary parsing.
import struct
import os
import shutil
class BDThandler:
def __init__(self, bdt_path):
self.bdt_path = bdt_path
self.bhd_path = os.path.splitext(bdt_path)[0] + '.bhd'
self.properties = self.decode()
def decode(self):
with open(self.bhd_path, 'rb') as f:
data = f.read()
offset = 0
magic = struct.unpack_from('4s', data, offset)[0].decode('ascii')
offset += 4
unk04, unk05, unk06, unk07 = struct.unpack_from('BBBB', data, offset)
offset += 4
unk08 = struct.unpack_from('<i', data, offset)[0]
offset += 4
bucket_count = struct.unpack_from('<i', data, offset)[0]
offset += 4
bucket_offset = struct.unpack_from('<i', data, offset)[0]
properties = {
'magic': magic,
'unk04': unk04,
'unk05': unk05,
'unk06': unk06,
'unk07': unk07,
'unk08': unk08,
'bucket_count': bucket_count,
'bucket_offset': bucket_offset,
'buckets': []
}
offset = bucket_offset
for _ in range(bucket_count):
file_count = struct.unpack_from('<i', data, offset)[0]
file_header_offset = struct.unpack_from('<i', data, offset + 4)[0]
offset += 8
bucket = {'file_count': file_count, 'file_header_offset': file_header_offset, 'files': []}
f_offset = file_header_offset
for __ in range(file_count):
file_hash = struct.unpack_from('<q', data, f_offset)[0]
padded_size = struct.unpack_from('<i', data, f_offset + 8)[0]
size = struct.unpack_from('<i', data, f_offset + 12)[0]
file_offset = struct.unpack_from('<q', data, f_offset + 16)[0]
bucket['files'].append({'hash': file_hash, 'padded_size': padded_size, 'size': size, 'offset': file_offset})
f_offset += 24
properties['buckets'].append(bucket)
return properties
def print_properties(self):
p = self.properties
print(f"Magic: {p['magic']}")
print(f"Unknown 04: {p['unk04']}")
print(f"Unknown 05: {p['unk05']}")
print(f"Unknown 06: {p['unk06']}")
print(f"Unknown 07: {p['unk07']}")
print(f"Unknown 08: {p['unk08']}")
print(f"Bucket Count: {p['bucket_count']}")
print(f"Bucket Offset: {p['bucket_offset']}")
for i, bucket in enumerate(p['buckets']):
print(f"Bucket {i}: File Count = {bucket['file_count']}, Header Offset = {bucket['file_header_offset']}")
for j, file in enumerate(bucket['files']):
print(f" File {j}: Hash = {file['hash']}, Padded Size = {file['padded_size']}, Size = {file['size']}, Offset = {file['offset']}")
def read_file_data(self, bucket_index, file_index):
file_info = self.properties['buckets'][bucket_index]['files'][file_index]
with open(self.bdt_path, 'rb') as f:
f.seek(file_info['offset'])
return f.read(file_info['size'])
def write(self, output_bdt_path, output_bhd_path):
# Write BHD from properties
with open(output_bhd_path, 'wb') as f:
f.write(struct.pack('4s', self.properties['magic'].encode('ascii')))
f.write(struct.pack('BBBB', self.properties['unk04'], self.properties['unk05'], self.properties['unk06'], self.properties['unk07']))
f.write(struct.pack('<i', self.properties['unk08']))
f.write(struct.pack('<i', self.properties['bucket_count']))
f.write(struct.pack('<i', self.properties['bucket_offset']))
# Write bucket entries
current_offset = self.properties['bucket_offset']
file_header_offsets = []
current_file_header_offset = self.properties['bucket_offset'] + (self.properties['bucket_count'] * 8)
for bucket in self.properties['buckets']:
f.write(struct.pack('<i', bucket['file_count']))
f.write(struct.pack('<i', current_file_header_offset))
file_header_offsets.append(current_file_header_offset)
current_file_header_offset += bucket['file_count'] * 24
current_offset += 8
# Write file headers
for i, bucket in enumerate(self.properties['buckets']):
for file in bucket['files']:
f.write(struct.pack('<q', file['hash']))
f.write(struct.pack('<i', file['padded_size']))
f.write(struct.pack('<i', file['size']))
f.write(struct.pack('<q', file['offset']))
# Copy BDT (for simple write; modifications would require data updates)
shutil.copy(self.bdt_path, output_bdt_path)
3. Java Class for .BDT Handling
The following Java class performs similar operations, using ByteBuffer
for binary parsing.
import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
public class BDThandler {
private String bdtPath;
private String bhdPath;
private Properties properties;
private static class Properties {
String magic;
byte unk04, unk05, unk06, unk07;
int unk08;
int bucketCount;
int bucketOffset;
Bucket[] buckets;
static class Bucket {
int fileCount;
int fileHeaderOffset;
FileEntry[] files;
static class FileEntry {
long hash;
int paddedSize;
int size;
long offset;
}
}
}
public BDThandler(String bdtPath) throws IOException {
this.bdtPath = bdtPath;
this.bhdPath = bdtPath.replace(".bdt", ".bhd");
this.properties = decode();
}
private Properties decode() throws IOException {
byte[] data = Files.readAllBytes(Paths.get(bhdPath));
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
Properties props = new Properties();
byte[] magicBytes = new byte[4];
bb.get(magicBytes);
props.magic = new String(magicBytes);
props.unk04 = bb.get();
props.unk05 = bb.get();
props.unk06 = bb.get();
props.unk07 = bb.get();
props.unk08 = bb.getInt();
props.bucketCount = bb.getInt();
props.bucketOffset = bb.getInt();
props.buckets = new Properties.Bucket[props.bucketCount];
bb.position(props.bucketOffset);
for (int i = 0; i < props.bucketCount; i++) {
Properties.Bucket bucket = new Properties.Bucket();
bucket.fileCount = bb.getInt();
bucket.fileHeaderOffset = bb.getInt();
props.buckets[i] = bucket;
}
for (int i = 0; i < props.bucketCount; i++) {
bb.position(props.buckets[i].fileHeaderOffset);
Properties.Bucket bucket = props.buckets[i];
bucket.files = new Properties.Bucket.FileEntry[bucket.fileCount];
for (int j = 0; j < bucket.fileCount; j++) {
Properties.Bucket.FileEntry file = new Properties.Bucket.FileEntry();
file.hash = bb.getLong();
file.paddedSize = bb.getInt();
file.size = bb.getInt();
file.offset = bb.getLong();
bucket.files[j] = file;
}
}
return props;
}
public void printProperties() {
Properties p = properties;
System.out.println("Magic: " + p.magic);
System.out.println("Unknown 04: " + p.unk04);
System.out.println("Unknown 05: " + p.unk05);
System.out.println("Unknown 06: " + p.unk06);
System.out.println("Unknown 07: " + p.unk07);
System.out.println("Unknown 08: " + p.unk08);
System.out.println("Bucket Count: " + p.bucketCount);
System.out.println("Bucket Offset: " + p.bucketOffset);
for (int i = 0; i < p.bucketCount; i++) {
Properties.Bucket bucket = p.buckets[i];
System.out.println("Bucket " + i + ": File Count = " + bucket.fileCount + ", Header Offset = " + bucket.fileHeaderOffset);
for (int j = 0; j < bucket.fileCount; j++) {
Properties.Bucket.FileEntry file = bucket.files[j];
System.out.println(" File " + j + ": Hash = " + file.hash + ", Padded Size = " + file.paddedSize + ", Size = " + file.size + ", Offset = " + file.offset);
}
}
}
public byte[] readFileData(int bucketIndex, int fileIndex) throws IOException {
Properties.Bucket.FileEntry file = properties.buckets[bucketIndex].files[fileIndex];
try (RandomAccessFile raf = new RandomAccessFile(bdtPath, "r")) {
raf.seek(file.offset);
byte[] buffer = new byte[file.size];
raf.readFully(buffer);
return buffer;
}
}
public void write(String outputBdtPath, String outputBhdPath) throws IOException {
// Write BHD
try (FileOutputStream fos = new FileOutputStream(outputBhdPath);
FileChannel channel = fos.getChannel()) {
Properties p = properties;
ByteBuffer bb = ByteBuffer.allocate((int) new File(bhdPath).length()).order(ByteOrder.LITTLE_ENDIAN);
bb.put(p.magic.getBytes());
bb.put(p.unk04);
bb.put(p.unk05);
bb.put(p.unk06);
bb.put(p.unk07);
bb.putInt(p.unk08);
bb.putInt(p.bucketCount);
bb.putInt(p.bucketOffset);
for (Properties.Bucket bucket : p.buckets) {
bb.putInt(bucket.fileCount);
bb.putInt(bucket.fileHeaderOffset);
}
for (Properties.Bucket bucket : p.buckets) {
for (Properties.Bucket.FileEntry file : bucket.files) {
bb.putLong(file.hash);
bb.putInt(file.paddedSize);
bb.putInt(file.size);
bb.putLong(file.offset);
}
}
bb.flip();
channel.write(bb);
}
// Copy BDT
Files.copy(Paths.get(bdtPath), Paths.get(outputBdtPath), StandardCopyOption.REPLACE_EXISTING);
}
}
4. JavaScript Class for .BDT Handling
The following JavaScript class is designed for Node.js, using fs
and Buffer
for binary operations.
const fs = require('fs');
class BDThandler {
constructor(bdtPath) {
this.bdtPath = bdtPath;
this.bhdPath = bdtPath.replace('.bdt', '.bhd');
this.properties = this.decode();
}
decode() {
const data = fs.readFileSync(this.bhdPath);
let offset = 0;
const magic = data.toString('ascii', offset, offset + 4);
offset += 4;
const unk04 = data.readUInt8(offset++);
const unk05 = data.readUInt8(offset++);
const unk06 = data.readUInt8(offset++);
const unk07 = data.readUInt8(offset++);
const unk08 = data.readInt32LE(offset);
offset += 4;
const bucketCount = data.readInt32LE(offset);
offset += 4;
const bucketOffset = data.readInt32LE(offset);
offset += 4;
const properties = {
magic,
unk04,
unk05,
unk06,
unk07,
unk08,
bucketCount,
bucketOffset,
buckets: []
};
offset = bucketOffset;
for (let i = 0; i < bucketCount; i++) {
const fileCount = data.readInt32LE(offset);
offset += 4;
const fileHeaderOffset = data.readInt32LE(offset);
offset += 4;
const bucket = { fileCount, fileHeaderOffset, files: [] };
let fOffset = fileHeaderOffset;
for (let j = 0; j < fileCount; j++) {
const hash = data.readBigInt64LE(fOffset);
fOffset += 8;
const paddedSize = data.readInt32LE(fOffset);
fOffset += 4;
const size = data.readInt32LE(fOffset);
fOffset += 4;
const fileOffset = data.readBigInt64LE(fOffset);
fOffset += 8;
bucket.files.push({ hash: Number(hash), paddedSize, size, offset: Number(fileOffset) });
}
properties.buckets.push(bucket);
}
return properties;
}
printProperties() {
const p = this.properties;
console.log(`Magic: ${p.magic}`);
console.log(`Unknown 04: ${p.unk04}`);
console.log(`Unknown 05: ${p.unk05}`);
console.log(`Unknown 06: ${p.unk06}`);
console.log(`Unknown 07: ${p.unk07}`);
console.log(`Unknown 08: ${p.unk08}`);
console.log(`Bucket Count: ${p.bucketCount}`);
console.log(`Bucket Offset: ${p.bucketOffset}`);
p.buckets.forEach((bucket, i) => {
console.log(`Bucket ${i}: File Count = ${bucket.fileCount}, Header Offset = ${bucket.fileHeaderOffset}`);
bucket.files.forEach((file, j) => {
console.log(` File ${j}: Hash = ${file.hash}, Padded Size = ${file.paddedSize}, Size = ${file.size}, Offset = ${file.offset}`);
});
});
}
readFileData(bucketIndex, fileIndex) {
const file = this.properties.buckets[bucketIndex].files[fileIndex];
const fd = fs.openSync(this.bdtPath, 'r');
const buffer = Buffer.alloc(file.size);
fs.readSync(fd, buffer, 0, file.size, file.offset);
fs.closeSync(fd);
return buffer;
}
write(outputBdtPath, outputBhdPath) {
const p = this.properties;
const bufferSize = fs.statSync(this.bhdPath).size;
const bb = Buffer.alloc(bufferSize);
let offset = 0;
bb.write(p.magic, offset, 4, 'ascii');
offset += 4;
bb.writeUInt8(p.unk04, offset++);
bb.writeUInt8(p.unk05, offset++);
bb.writeUInt8(p.unk06, offset++);
bb.writeUInt8(p.unk07, offset++);
bb.writeInt32LE(p.unk08, offset);
offset += 4;
bb.writeInt32LE(p.bucketCount, offset);
offset += 4;
bb.writeInt32LE(p.bucketOffset, offset);
offset += 4;
offset = p.bucketOffset;
p.buckets.forEach(bucket => {
bb.writeInt32LE(bucket.fileCount, offset);
offset += 4;
bb.writeInt32LE(bucket.fileHeaderOffset, offset);
offset += 4;
});
p.buckets.forEach(bucket => {
offset = bucket.fileHeaderOffset;
bucket.files.forEach(file => {
bb.writeBigInt64LE(BigInt(file.hash), offset);
offset += 8;
bb.writeInt32LE(file.paddedSize, offset);
offset += 4;
bb.writeInt32LE(file.size, offset);
offset += 4;
bb.writeBigInt64LE(BigInt(file.offset), offset);
offset += 8;
});
});
fs.writeSync(fs.openSync(outputBhdPath, 'w'), bb);
fs.copyFileSync(this.bdtPath, outputBdtPath);
}
}
5. C Implementation for .BDT Handling
Since C does not have built-in classes, the following implementation uses a struct for properties and functions for operations. It includes <stdio.h>, <stdlib.h>, and <stdint.h> for types.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
typedef struct {
int64_t hash;
int32_t padded_size;
int32_t size;
int64_t offset;
} FileEntry;
typedef struct {
int32_t file_count;
int32_t file_header_offset;
FileEntry* files;
} Bucket;
typedef struct {
char magic[5];
uint8_t unk04;
uint8_t unk05;
uint8_t unk06;
uint8_t unk07;
int32_t unk08;
int32_t bucket_count;
int32_t bucket_offset;
Bucket* buckets;
} Properties;
Properties* decode(const char* bhd_path) {
FILE* f = fopen(bhd_path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t* data = malloc(size);
fread(data, 1, size, f);
fclose(f);
Properties* props = malloc(sizeof(Properties));
memcpy(props->magic, data, 4);
props->magic[4] = '\0';
int offset = 4;
props->unk04 = data[offset++];
props->unk05 = data[offset++];
props->unk06 = data[offset++];
props->unk07 = data[offset++];
memcpy(&props->unk08, data + offset, 4);
offset += 4;
memcpy(&props->bucket_count, data + offset, 4);
offset += 4;
memcpy(&props->bucket_offset, data + offset, 4);
offset += 4;
props->buckets = malloc(props->bucket_count * sizeof(Bucket));
offset = props->bucket_offset;
for (int i = 0; i < props->bucket_count; i++) {
memcpy(&props->buckets[i].file_count, data + offset, 4);
offset += 4;
memcpy(&props->buckets[i].file_header_offset, data + offset, 4);
offset += 4;
}
for (int i = 0; i < props->bucket_count; i++) {
offset = props->buckets[i].file_header_offset;
props->buckets[i].files = malloc(props->buckets[i].file_count * sizeof(FileEntry));
for (int j = 0; j < props->buckets[i].file_count; j++) {
memcpy(&props->buckets[i].files[j].hash, data + offset, 8);
offset += 8;
memcpy(&props->buckets[i].files[j].padded_size, data + offset, 4);
offset += 4;
memcpy(&props->buckets[i].files[j].size, data + offset, 4);
offset += 4;
memcpy(&props->buckets[i].files[j].offset, data + offset, 8);
offset += 8;
}
}
free(data);
return props;
}
void print_properties(Properties* p) {
printf("Magic: %s\n", p->magic);
printf("Unknown 04: %u\n", p->unk04);
printf("Unknown 05: %u\n", p->unk05);
printf("Unknown 06: %u\n", p->unk06);
printf("Unknown 07: %u\n", p->unk07);
printf("Unknown 08: %d\n", p->unk08);
printf("Bucket Count: %d\n", p->bucket_count);
printf("Bucket Offset: %d\n", p->bucket_offset);
for (int i = 0; i < p->bucket_count; i++) {
printf("Bucket %d: File Count = %d, Header Offset = %d\n", i, p->buckets[i].file_count, p->buckets[i].file_header_offset);
for (int j = 0; j < p->buckets[i].file_count; j++) {
printf(" File %d: Hash = %ld, Padded Size = %d, Size = %d, Offset = %ld\n",
j, p->buckets[i].files[j].hash, p->buckets[i].files[j].padded_size,
p->buckets[i].files[j].size, p->buckets[i].files[j].offset);
}
}
}
uint8_t* read_file_data(const char* bdt_path, Properties* p, int bucket_index, int file_index, int* out_size) {
FileEntry file = p->buckets[bucket_index].files[file_index];
*out_size = file.size;
uint8_t* buffer = malloc(file.size);
FILE* f = fopen(bdt_path, "rb");
fseek(f, file.offset, SEEK_SET);
fread(buffer, 1, file.size, f);
fclose(f);
return buffer;
}
void write(Properties* p, const char* output_bdt_path, const char* output_bhd_path, const char* original_bdt_path) {
FILE* f = fopen(output_bhd_path, "wb");
fwrite(p->magic, 1, 4, f);
fwrite(&p->unk04, 1, 1, f);
fwrite(&p->unk05, 1, 1, f);
fwrite(&p->unk06, 1, 1, f);
fwrite(&p->unk07, 1, 1, f);
fwrite(&p->unk08, 4, 1, f);
fwrite(&p->bucket_count, 4, 1, f);
fwrite(&p->bucket_offset, 4, 1, f);
for (int i = 0; i < p->bucket_count; i++) {
fwrite(&p->buckets[i].file_count, 4, 1, f);
fwrite(&p->buckets[i].file_header_offset, 4, 1, f);
}
for (int i = 0; i < p->bucket_count; i++) {
for (int j = 0; j < p->buckets[i].file_count; j++) {
fwrite(&p->buckets[i].files[j].hash, 8, 1, f);
fwrite(&p->buckets[i].files[j].padded_size, 4, 1, f);
fwrite(&p->buckets[i].files[j].size, 4, 1, f);
fwrite(&p->buckets[i].files[j].offset, 8, 1, f);
}
}
fclose(f);
// Copy original BDT
FILE* src = fopen(original_bdt_path, "rb");
FILE* dst = fopen(output_bdt_path, "wb");
uint8_t buf[4096];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), src)) > 0) {
fwrite(buf, 1, n, dst);
}
fclose(src);
fclose(dst);
}
// Example usage:
// Properties* props = decode("example.bhd");
// print_properties(props);
// int size;
// uint8_t* data = read_file_data("example.bdt", props, 0, 0, &size);
// write(props, "output.bdt", "output.bhd", "example.bdt");