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.
Offset | Size | Description |
---|---|---|
0x00 | 4 | Magic signature (0x51890ACE or 0xCE0A8951 ) |
0x04 | 4 | Format 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:
Offset | Size | Description |
---|---|---|
0x08 | 4 | Number of entries present in this VPP file |
0x0C | 4 | Size 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:
Offset | Size | Description |
---|---|---|
0x00 | 60 | File name (ASCII encoded) |
0x3C | 4 | Size 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
- Summoner (2000)
- Red Faction (2001)
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:
Offset | Size | Description |
---|---|---|
0x00 | 24 | File name (ASCII encoded) |
0x18 | 4 | Size of the file in bytes* |
0x1C | 4 | Size 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
- Red Faction II (2002) (PlayStation 2 version only)
Version 3
Following the generic file header is a new set of additional fields:
Offset | Size | Description |
---|---|---|
0x0008 | 64 | VPP name (unused?) |
0x0048 | 256 | VPP path (unused?) |
0x0148 | 4 | Unknown |
0x014C | 4 | VPP flags |
0x0150 | 4 | Unknown |
0x0154 | 4 | Number of entries present in this VPP file |
0x0158 | 4 | Size of the VPP file in bytes |
0x015C | 4 | Size of file entry table in bytes |
0x0160 | 4 | Size of file names table in bytes |
0x0164 | 4 | Size of uncompressed file data in bytes |
0x0168 | 4 | Size of compressed file data in bytes (or 0xFFFFFFFF if uncompressed) |
the VPP flags field has two known values:
- If bit 0 is set, the VPP file uses compression.
- If bit 1 is set, the VPP file is condensed.*
*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:
Offset | Size | Description |
---|---|---|
0x0000 | 4 | File name offset (relative to file names start) |
0x0004 | 4 | Unknown |
0x0008 | 4 | File data offset (relative to file data start) |
0x000C | 4 | Unknown |
0x0010 | 4 | File uncompressed size in bytes |
0x0014 | 4 | File compressed size in bytes (or 0xFFFFFFFF if uncompressed) |
0x0018 | 4 | Unknown |
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
- The Punisher (2005)
- Saints Row (2006)
- Red Faction: Guerrilla (2009)
Version 4
Following the generic file header is a modified version of the Version 3 header:
Offset | Size | Description |
---|---|---|
0x0008 | 64 | VPP name (unused?) |
0x0048 | 256 | VPP path (unused?) |
0x0148 | 4 | Unknown |
0x014C | 4 | VPP flags |
0x0150 | 4 | Unknown |
0x0154 | 4 | Number of entries present in this VPP file |
0x0158 | 4 | Size of the VPP file in bytes |
0x015C | 4 | Size of file entry table in bytes |
0x0160 | 4 | Size of file names table in bytes |
0x0164 | 4 | Size of file extensions table in bytes |
0x0168 | 4 | Size of uncompressed file data in bytes |
0x016C | 4 | Size 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:
Offset | Size | Description |
---|---|---|
0x0000 | 4 | File name offset (relative to file names start) |
0x0004 | 4 | File extension offset (relative to file extensions start) |
0x0008 | 4 | Unknown |
0x000C | 4 | File data offset (relative to file data start) |
0x0010 | 4 | File uncompressed size in bytes |
0x0014 | 4 | File compressed size in bytes (or 0xFFFFFFFF if uncompressed) |
0x0018 | 4 | Unknown |
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
- Saints Row 2 (2008)
Version 5
TODO: Come back later.
Version 6
TODO: Come back later.