RSS ☼

erysdren's WWW site

software, gameware, strangeware


Volition VPP Format
2025-02-26

The VPP file format stores uncompressed files in a table. It usually ends with the .vpp file extension, though it can have extended suffixes defining the target platform such as .vpp_pc and .vpp_xbox2. It was introduced by Volition with Red Faction (2001).

The format supports both little-endian and big-endian byte order. All values are written accordingly, and the only way to tell them apart is through the 4-byte magic signature at the start of the file. For a little-endian VPP file, the magic signature will be 0x51890ACE. For a big-endian VPP file, the magic signature will be 0xCE0A8951.

The VPP format is notable for the fact that almost every data structure in it is sector-aligned (in this case, by 2048 bytes) presumably to help with I/O buffering on consoles like the original Xbox and the Xbox 360. I don't know exactly why that is better, but I do know that other games used this same method for their console ports. Valve's Orange Box on the Xbox 360 and PlayStation 3 being a good example.

To easily align a structure size to this boundary, you could use a helper function like this:

size_t alignSize(size_t size, size_t alignment)
{
	if (size % alignment == 0)
		return size;

	size -= size % alignment;
	size += alignment;

	return size;
}

File Header

This header is the same across all versions of the format.

OffsetSizeDescription
0x004Magic signature (0x51890ACE or 0xCE0A8951)
0x044Format version

NOTE: The magic signature should be used to determine the file endianness. The following format version field will use whatever endianness was determined.


Version 1

Following the generic file header is two additional fields:

OffsetSizeDescription
0x084Number of entries present in this VPP file
0x0C4Size of the VPP file in bytes

Following the file header and padded up to the 2048 byte boundary is a tightly packaged array of file table entry structures:

OffsetSizeDescription
0x0060File name (ASCII encoded)
0x3C4Size of the file in bytes

Calculating the offsets to each file takes some work, since the offsets aren't stored in the file entry table. Immediately following the file entry table is padding up to the 2048 byte boundary, and then the first file's data. To calculate the file offsets, you could use code like this:

// offset of the first file's data
size_t entryOffset = alignSize(vppHeaderSize, 2048) + alignSize(vppNumFiles * vppFileEntrySize, 2048);

// seek to file entry table position
fileSeek(vppFileHandle, alignSize(vppHeaderSize, 2048));

// read each file entry and calculate offset
for (size_t i = 0; i < vppNumFiles; i++)
{
	// read name and file size
	vppFiles[i].name = readBytes(vppFileHandle, 60);
	vppFiles[i].length = readU32(vppFileHandle);

	// calculate offset
	vppFiles[i].offset = entryOffset;
	entryOffset += alignSize(vppFiles[i].length, 2048);
}

NOTE: As far as I know, VPP Version 1 does not support any form of compression.

Games using VPP Version 1


Version 2

Following the generic file header is the same additional fields as Version 1.

In version 2, the file table entry structure is different:

OffsetSizeDescription
0x0024File name (ASCII encoded)
0x184Size of the file in bytes*
0x1C4Size of the file in bytes*

*The size of the file is repeated twice here. I believe that one of them probably represents the compressed size while the other represents the uncompressed size, but I haven't found any examples where the sizes differ, so I can't confirm that either way.

The file offsets should be calculated the same way as VPP Version 1.

NOTE: If anyone finds any examples of a Version 2 VPP file that uses compression, please send it my way!

Games using VPP Version 2


Version 3

Following the generic file header is a new set of additional fields:

OffsetSizeDescription
0x000864VPP name (unused?)
0x0048256VPP path (unused?)
0x01484Unknown
0x014C4VPP flags
0x01504Unknown
0x01544Number of entries present in this VPP file
0x01584Size of the VPP file in bytes
0x015C4Size of file entry table in bytes
0x01604Size of file names table in bytes
0x01644Size of uncompressed file data in bytes
0x01684Size of compressed file data in bytes (or 0xFFFFFFFF if uncompressed)

the VPP flags field has two known values:

*TODO: Explain what this means.

Following the file header and padded up to the 2048 byte boundary is a tightly packaged array of file table entry structures:

OffsetSizeDescription
0x00004File name offset (relative to file names start)
0x00044Unknown
0x00084File data offset (relative to file data start)
0x000C4Unknown
0x00104File uncompressed size in bytes
0x00144File compressed size in bytes (or 0xFFFFFFFF if uncompressed)
0x00184Unknown

Following the file entry table and padded up to the 2048 byte boundary is a buffer of null-terminated ASCII strings representing file paths and names. The "File name offset" field from the file table entry structure is a direct offset into this buffer.

Following the file names buffer and padded up to the 2048 byte boundary is where the actual file data starts. The "File data offset" field from the file table entry structure is a direct offset into this buffer. Each file is padded to the 2048 byte boundary, even small ones.

Version 3 file compression

Compressed files use zlib deflate compression, so decompressing them is fairly simple. Reading a file's data could be done like this (using miniz as an example):

if (vppFiles[i].compressedLength == 0xFFFFFFFF)
{
	// uncompressed
	// make absolute file data offset
	offset = vppFiles[i].offset;
	offset += alignSize(vppHeaderSize, 2048);
	offset += alignSize(vppDirectorySize, 2048);
	offset += alignSize(vppNamesSize, 2048);

	// seek to file data position
	fileSeek(vppFileHandle, offset);

	// read and return uncompressed file data
	return readBytes(vppFileHandle, vppFiles[i].length);
}
else
{
	// compressed
	// make absolute file data offset
	offset = vppFiles[i].offset;
	offset += alignSize(vppHeaderSize, 2048);
	offset += alignSize(vppDirectorySize, 2048);
	offset += alignSize(vppNamesSize, 2048);

	// seek to file data position
	fileSeek(vppFileHandle, offset);

	// read compressed data
	compressedData = readBytes(vppFileHandle, vppFiles[i].compressedLength);

	// allocate buffer for uncompressed data
	uncompressedData = memAlloc(vppFiles[i].length);

	// run miniz to inflate the file from compressedData into uncompressedData
	mz_uncompress(uncompressedData, vppFiles[i].length, compressedData, vppFiles[i].compressedLength);

	// return uncompressed file data
	return uncompressedData;
}

Games using VPP Version 3


Version 4

Following the generic file header is a modified version of the Version 3 header:

OffsetSizeDescription
0x000864VPP name (unused?)
0x0048256VPP path (unused?)
0x01484Unknown
0x014C4VPP flags
0x01504Unknown
0x01544Number of entries present in this VPP file
0x01584Size of the VPP file in bytes
0x015C4Size of file entry table in bytes
0x01604Size of file names table in bytes
0x01644Size of file extensions table in bytes
0x01684Size of uncompressed file data in bytes
0x016C4Size of compressed file data in bytes (or 0xFFFFFFFF if uncompressed)

Following the file header and padded up to the 2048 byte boundary is a tightly packaged array of file table entry structures:

OffsetSizeDescription
0x00004File name offset (relative to file names start)
0x00044File extension offset (relative to file extensions start)
0x00084Unknown
0x000C4File data offset (relative to file data start)
0x00104File uncompressed size in bytes
0x00144File compressed size in bytes (or 0xFFFFFFFF if uncompressed)
0x00184Unknown

Following the file entry table and padded up to the 2048 byte boundary is the same buffer of null-terminated strings representing the file names as Version 3, though now they are missing the extension. The extensions are in the next 2048-byte-aligned buffer.

After the extensions is the start of the file data table, same as Version 3.

Games using VPP Version 4


Version 5

TODO: Come back later.


Version 6

TODO: Come back later.