Task 786: .VPP File Format
Task 786: .VPP File Format
1. List of Properties of the .VPP File Format Intrinsic to Its File System
The .VPP file format, as specified for Volition's Version 1 (used in games like Red Faction and Summoner), is an uncompressed archive format that functions as a container for game assets, resembling a simple file system without hierarchical directories. The properties intrinsic to its structure and organization are as follows:
- Magic Signature: A 4-byte unsigned integer (uint32, little-endian) with the fixed value 0x51890ACE, serving as the file identifier.
- Version Number: A 4-byte unsigned integer (uint32) with the fixed value 0x00000001, indicating the format version.
- Number of Files: A 4-byte unsigned integer (uint32) specifying the count of archived files.
- Total File Size: A 4-byte unsigned integer (uint32) representing the overall size of the .VPP file in bytes.
- Header Padding: 2032 bytes of zero-filled padding immediately following the header fields, ensuring the header occupies exactly 2048 bytes.
- Directory Offset: Fixed at 0x0800 (2048 bytes from the file start), where file entries begin.
- Directory Entry Size: Each entry is 64 bytes fixed.
- Filename in Directory Entry: 60 bytes as a null-padded or null-terminated character array (ASCII string), with a maximum length of 60 characters.
- File Size in Directory Entry: A 4-byte unsigned integer (uint32) indicating the uncompressed size of the corresponding file data.
- File Data Alignment: All file data starts at offsets aligned to 2048-byte boundaries; each file's data is followed by zero-padding to reach the next 2048-byte boundary.
- File Data Placement: File data follows the directory, with the first file starting at the next 2048-byte aligned offset after the directory ends; no explicit offsets are stored in entries and must be calculated cumulatively.
- Compression: None; all file data is stored uncompressed.
- Directory Structure: Flat (no subdirectories or hierarchical organization).
- Timestamps: Absent; no creation, modification, or access times are recorded.
- Endianness: Little-endian, as determined by the magic signature.
- Padding in File Data: Zero bytes (0x00) used to fill unused space between files for alignment.
- Maximum Wasted Space per File: Up to 2047 bytes due to alignment requirements.
These properties define the format's layout and behavior as a container file system.
2. Two Direct Download Links for .VPP Files
The following are direct download links to sample .VPP files from Red Faction community mods, which conform to the Volition .VPP format:
- https://www.factionfiles.com/download.php?id=6623 (DM-Outpost_III.vpp – a multiplayer map archive)
- https://www.factionfiles.com/download.php?id=6654 (DM-ApocalypseVoidQ3.vpp – a multiplayer map archive)
3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .VPP File Dump
The following is a self-contained HTML snippet with embedded JavaScript that can be embedded into a Ghost blog (or any web page). It allows users to drag and drop a .VPP file, parses it according to the Version 1 specification, and dumps all properties listed in section 1 to the screen.
4. Python Class for .VPP File Handling
The following Python class can open a .VPP file, decode and read its properties, print them to the console, and write a new .VPP file based on provided data.
import struct
import os
class VPPHandler:
ALIGNMENT = 2048
def __init__(self, filepath=None):
self.filepath = filepath
self.properties = {}
if filepath:
self.read()
def read(self):
with open(self.filepath, 'rb') as f:
data = f.read()
self._decode(data)
def _decode(self, data):
offset = 0
self.properties['Magic Signature'] = hex(struct.unpack_from('<I', data, offset)[0])
offset += 4
self.properties['Version Number'] = hex(struct.unpack_from('<I', data, offset)[0])
offset += 4
num_files = struct.unpack_from('<I', data, offset)[0]
self.properties['Number of Files'] = num_files
offset += 4
self.properties['Total File Size'] = struct.unpack_from('<I', data, offset)[0]
offset += 4
self.properties['Header Padding'] = '2032 bytes of zeros'
offset += 2032 # Skip padding
self.properties['Directory Offset'] = '0x0800'
self.properties['Directory Entry Size'] = '64 bytes'
self.properties['Compression'] = 'None'
self.properties['Directory Structure'] = 'Flat'
self.properties['Timestamps'] = 'Absent'
self.properties['Endianness'] = 'Little-endian'
self.properties['File Data Alignment'] = '2048 bytes'
self.properties['Padding in File Data'] = 'Zero bytes'
files = []
for _ in range(num_files):
filename = data[offset:offset+60].decode('ascii', errors='ignore').rstrip('\x00')
offset += 60
file_size = struct.unpack_from('<I', data, offset)[0]
offset += 4
files.append({'filename': filename, 'size': file_size})
self.properties['Files'] = files
# Derived property example
directory_size = num_files * 64
self.properties['First File Data Offset'] = ((2048 + directory_size + self.ALIGNMENT - 1) // self.ALIGNMENT) * self.ALIGNMENT
def print_properties(self):
for key, value in self.properties.items():
if key == 'Files':
print(f"{key}:")
for file in value:
print(f" - Filename: {file['filename']}, Size: {file['size']} bytes")
else:
print(f"{key}: {value}")
def write(self, output_path, files_data):
# files_data: list of {'filename': str, 'data': bytes}
header = struct.pack('<IIII', 0x51890ACE, 1, len(files_data), 0) # Temp total size 0
padding = b'\x00' * 2032
directory = b''
file_data_section = b''
current_offset = self.ALIGNMENT + len(files_data) * 64
current_offset = ((current_offset + self.ALIGNMENT - 1) // self.ALIGNMENT) * self.ALIGNMENT
for file in files_data:
filename = file['filename'].encode('ascii')[:60].ljust(60, b'\x00')
size = len(file['data'])
directory += filename + struct.pack('<I', size)
file_data = file['data']
padding_size = ((len(file_data) + self.ALIGNMENT - 1) // self.ALIGNMENT) * self.ALIGNMENT - len(file_data)
file_data += b'\x00' * padding_size
file_data_section += file_data
total_size = len(header) + len(padding) + len(directory) + len(file_data_section)
header = struct.pack('<IIII', 0x51890ACE, 1, len(files_data), total_size)
with open(output_path, 'wb') as f:
f.write(header + padding + directory + file_data_section)
5. Java Class for .VPP File Handling
The following Java class can open a .VPP file, decode and read its properties, print them to the console, and write a new .VPP file based on provided data.
import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.util.*;
public class VPPHandler {
private static final int ALIGNMENT = 2048;
private Map<String, Object> properties = new HashMap<>();
private String filepath;
public VPPHandler(String filepath) {
this.filepath = filepath;
if (filepath != null) {
read();
}
}
public void read() {
try (RandomAccessFile raf = new RandomAccessFile(filepath, "r")) {
FileChannel channel = raf.getChannel();
ByteBuffer buffer = ByteBuffer.allocate((int) new File(filepath).length());
channel.read(buffer);
buffer.flip();
buffer.order(ByteOrder.LITTLE_ENDIAN);
decode(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
private void decode(ByteBuffer buffer) {
properties.put("Magic Signature", "0x" + Integer.toHexString(buffer.getInt()).toUpperCase());
properties.put("Version Number", "0x" + Integer.toHexString(buffer.getInt()).toUpperCase());
int numFiles = buffer.getInt();
properties.put("Number of Files", numFiles);
properties.put("Total File Size", buffer.getInt());
properties.put("Header Padding", "2032 bytes of zeros");
buffer.position(2048); // Skip to directory
properties.put("Directory Offset", "0x0800");
properties.put("Directory Entry Size", "64 bytes");
properties.put("Compression", "None");
properties.put("Directory Structure", "Flat");
properties.put("Timestamps", "Absent");
properties.put("Endianness", "Little-endian");
properties.put("File Data Alignment", "2048 bytes");
properties.put("Padding in File Data", "Zero bytes");
List<Map<String, Object>> files = new ArrayList<>();
for (int i = 0; i < numFiles; i++) {
byte[] filenameBytes = new byte[60];
buffer.get(filenameBytes);
String filename = new String(filenameBytes, "ASCII").trim();
int size = buffer.getInt();
Map<String, Object> file = new HashMap<>();
file.put("filename", filename);
file.put("size", size);
files.add(file);
}
properties.put("Files", files);
int directorySize = numFiles * 64;
int firstDataOffset = ((2048 + directorySize + ALIGNMENT - 1) / ALIGNMENT) * ALIGNMENT;
properties.put("First File Data Offset", firstDataOffset);
}
public void printProperties() {
for (Map.Entry<String, Object> entry : properties.entrySet()) {
if (entry.getKey().equals("Files")) {
System.out.println(entry.getKey() + ":");
@SuppressWarnings("unchecked")
List<Map<String, Object>> files = (List<Map<String, Object>>) entry.getValue();
for (Map<String, Object> file : files) {
System.out.println(" - Filename: " + file.get("filename") + ", Size: " + file.get("size") + " bytes");
}
} else {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
public void write(String outputPath, List<Map<String, Object>> filesData) throws IOException {
// filesData: list of maps with "filename": String, "data": byte[]
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// Temp header
ByteBuffer header = ByteBuffer.allocate(16).order(ByteOrder.LITTLE_ENDIAN);
header.putInt(0x51890ACE);
header.putInt(1);
header.putInt(filesData.size());
header.putInt(0); // Temp total size
baos.write(header.array());
// Padding
baos.write(new byte[2032]);
// Directory
ByteBuffer directory = ByteBuffer.allocate(filesData.size() * 64).order(ByteOrder.LITTLE_ENDIAN);
int currentOffset = ALIGNMENT + filesData.size() * 64;
currentOffset = ((currentOffset + ALIGNMENT - 1) / ALIGNMENT) * ALIGNMENT;
ByteArrayOutputStream fileDataSection = new ByteArrayOutputStream();
for (Map<String, Object> file : filesData) {
String filename = (String) file.get("filename");
byte[] data = (byte[]) file.get("data");
byte[] filenameBytes = filename.getBytes("ASCII");
byte[] paddedName = new byte[60];
System.arraycopy(filenameBytes, 0, paddedName, 0, Math.min(60, filenameBytes.length));
directory.put(paddedName);
directory.putInt(data.length);
// File data with padding
fileDataSection.write(data);
int paddingSize = ((data.length + ALIGNMENT - 1) / ALIGNMENT) * ALIGNMENT - data.length;
fileDataSection.write(new byte[paddingSize]);
}
baos.write(directory.array());
baos.write(fileDataSection.toByteArray());
// Update total size
int totalSize = baos.size();
ByteBuffer updatedHeader = ByteBuffer.wrap(baos.toByteArray(), 0, 16).order(ByteOrder.LITTLE_ENDIAN);
updatedHeader.position(12);
updatedHeader.putInt(totalSize);
try (FileOutputStream fos = new FileOutputStream(outputPath)) {
fos.write(baos.toByteArray());
}
}
}
6. JavaScript Class for .VPP File Handling
The following JavaScript class (for Node.js) can open a .VPP file, decode and read its properties, print them to the console, and write a new .VPP file based on provided data. Requires Node.js with 'fs' module.
const fs = require('fs');
class VPPHandler {
static ALIGNMENT = 2048;
constructor(filepath = null) {
this.properties = {};
this.filepath = filepath;
if (filepath) {
this.read();
}
}
read() {
const data = fs.readFileSync(this.filepath);
this.decode(data);
}
decode(data) {
const view = new DataView(data.buffer);
let offset = 0;
this.properties['Magic Signature'] = '0x' + view.getUint32(offset, true).toString(16).toUpperCase();
offset += 4;
this.properties['Version Number'] = '0x' + view.getUint32(offset, true).toString(16).toUpperCase();
offset += 4;
const numFiles = view.getUint32(offset, true);
this.properties['Number of Files'] = numFiles;
offset += 4;
this.properties['Total File Size'] = view.getUint32(offset, true);
offset += 4;
this.properties['Header Padding'] = '2032 bytes of zeros';
offset += 2032;
this.properties['Directory Offset'] = '0x0800';
this.properties['Directory Entry Size'] = '64 bytes';
this.properties['Compression'] = 'None';
this.properties['Directory Structure'] = 'Flat';
this.properties['Timestamps'] = 'Absent';
this.properties['Endianness'] = 'Little-endian';
this.properties['File Data Alignment'] = '2048 bytes';
this.properties['Padding in File Data'] = 'Zero bytes';
const files = [];
for (let i = 0; i < numFiles; i++) {
let filename = '';
for (let j = 0; j < 60; j++) {
const char = view.getUint8(offset + j);
if (char === 0) break;
filename += String.fromCharCode(char);
}
offset += 60;
const size = view.getUint32(offset, true);
offset += 4;
files.push({ filename, size });
}
this.properties['Files'] = files;
const directorySize = numFiles * 64;
this.properties['First File Data Offset'] = Math.ceil((2048 + directorySize) / this.ALIGNMENT) * this.ALIGNMENT;
}
printProperties() {
for (const [key, value] of Object.entries(this.properties)) {
if (key === 'Files') {
console.log(`${key}:`);
value.forEach(file => {
console.log(` - Filename: ${file.filename}, Size: ${file.size} bytes`);
});
} else {
console.log(`${key}: ${value}`);
}
}
}
write(outputPath, filesData) {
// filesData: array of {filename: string, data: Buffer}
let buffer = Buffer.alloc(16);
buffer.writeUint32LE(0x51890ACE, 0);
buffer.writeUint32LE(1, 4);
buffer.writeUint32LE(filesData.length, 8);
buffer.writeUint32LE(0, 12); // Temp total
const padding = Buffer.alloc(2032, 0);
let directory = Buffer.alloc(filesData.length * 64);
let dirOffset = 0;
let fileDataSection = Buffer.alloc(0);
let currentOffset = VPPHandler.ALIGNMENT + filesData.length * 64;
currentOffset = Math.ceil(currentOffset / VPPHandler.ALIGNMENT) * VPPHandler.ALIGNMENT;
for (const file of filesData) {
const filenameBuf = Buffer.alloc(60, 0);
Buffer.from(file.filename).copy(filenameBuf, 0, 0, Math.min(60, file.filename.length));
filenameBuf.copy(directory, dirOffset);
dirOffset += 60;
directory.writeUint32LE(file.data.length, dirOffset);
dirOffset += 4;
const paddingSize = Math.ceil(file.data.length / VPPHandler.ALIGNMENT) * VPPHandler.ALIGNMENT - file.data.length;
const paddedData = Buffer.concat([file.data, Buffer.alloc(paddingSize, 0)]);
fileDataSection = Buffer.concat([fileDataSection, paddedData]);
}
const fullBuffer = Buffer.concat([buffer, padding, directory, fileDataSection]);
fullBuffer.writeUint32LE(fullBuffer.length, 12); // Update total size
fs.writeFileSync(outputPath, fullBuffer);
}
}
7. C Struct and Functions for .VPP File Handling
Since C does not have classes in the same way as object-oriented languages, the following implementation uses a struct with associated functions to open a .VPP file, decode and read its properties, print them to the console, and write a new .VPP file based on provided data.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#define ALIGNMENT 2048
typedef struct {
char *filename;
uint32_t size;
} VPPFileEntry;
typedef struct {
uint32_t magic;
uint32_t version;
uint32_t num_files;
uint32_t total_size;
char *header_padding_desc;
char *dir_offset_desc;
char *dir_entry_size_desc;
char *compression_desc;
char *dir_structure_desc;
char *timestamps_desc;
char *endianness_desc;
char *file_alignment_desc;
char *padding_desc;
VPPFileEntry *files;
uint32_t first_data_offset;
} VPPProperties;
void read_vpp(const char *filepath, VPPProperties *props) {
FILE *f = fopen(filepath, "rb");
if (!f) {
perror("Failed to open file");
return;
}
fseek(f, 0, SEEK_END);
long file_size = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t *data = malloc(file_size);
fread(data, 1, file_size, f);
fclose(f);
uint32_t offset = 0;
memcpy(&props->magic, data + offset, 4);
offset += 4;
memcpy(&props->version, data + offset, 4);
offset += 4;
memcpy(&props->num_files, data + offset, 4);
offset += 4;
memcpy(&props->total_size, data + offset, 4);
offset += 4;
props->header_padding_desc = "2032 bytes of zeros";
offset += 2032;
props->dir_offset_desc = "0x0800";
props->dir_entry_size_desc = "64 bytes";
props->compression_desc = "None";
props->dir_structure_desc = "Flat";
props->timestamps_desc = "Absent";
props->endianness_desc = "Little-endian";
props->file_alignment_desc = "2048 bytes";
props->padding_desc = "Zero bytes";
props->files = malloc(props->num_files * sizeof(VPPFileEntry));
for (uint32_t i = 0; i < props->num_files; i++) {
props->files[i].filename = malloc(61);
strncpy(props->files[i].filename, (char *)(data + offset), 60);
props->files[i].filename[60] = '\0';
offset += 60;
memcpy(&props->files[i].size, data + offset, 4);
offset += 4;
}
uint32_t directory_size = props->num_files * 64;
props->first_data_offset = ((2048 + directory_size + ALIGNMENT - 1) / ALIGNMENT) * ALIGNMENT;
free(data);
}
void print_vpp_properties(const VPPProperties *props) {
printf("Magic Signature: 0x%X\n", props->magic);
printf("Version Number: 0x%X\n", props->version);
printf("Number of Files: %u\n", props->num_files);
printf("Total File Size: %u\n", props->total_size);
printf("Header Padding: %s\n", props->header_padding_desc);
printf("Directory Offset: %s\n", props->dir_offset_desc);
printf("Directory Entry Size: %s\n", props->dir_entry_size_desc);
printf("Compression: %s\n", props->compression_desc);
printf("Directory Structure: %s\n", props->dir_structure_desc);
printf("Timestamps: %s\n", props->timestamps_desc);
printf("Endianness: %s\n", props->endianness_desc);
printf("File Data Alignment: %s\n", props->file_alignment_desc);
printf("Padding in File Data: %s\n", props->padding_desc);
printf("First File Data Offset: %u\n", props->first_data_offset);
printf("Files:\n");
for (uint32_t i = 0; i < props->num_files; i++) {
printf(" - Filename: %s, Size: %u bytes\n", props->files[i].filename, props->files[i].size);
}
}
void free_vpp_properties(VPPProperties *props) {
for (uint32_t i = 0; i < props->num_files; i++) {
free(props->files[i].filename);
}
free(props->files);
}
void write_vpp(const char *output_path, VPPFileEntry *files, uint32_t num_files, uint8_t **file_datas) {
FILE *f = fopen(output_path, "wb");
if (!f) {
perror("Failed to open output file");
return;
}
// Temp header
uint32_t magic = 0x51890ACE;
uint32_t version = 1;
uint32_t total_size = 0; // Temp
fwrite(&magic, 4, 1, f);
fwrite(&version, 4, 1, f);
fwrite(&num_files, 4, 1, f);
fwrite(&total_size, 4, 1, f);
// Padding
uint8_t padding[2032] = {0};
fwrite(padding, 2032, 1, f);
// Directory
for (uint32_t i = 0; i < num_files; i++) {
uint8_t filename_padded[60] = {0};
strncpy((char *)filename_padded, files[i].filename, 59);
fwrite(filename_padded, 60, 1, f);
fwrite(&files[i].size, 4, 1, f);
}
// File data
long current_pos = ftell(f);
long aligned_pos = ((current_pos + ALIGNMENT - 1) / ALIGNMENT) * ALIGNMENT;
uint8_t align_padding[ALIGNMENT] = {0};
fwrite(align_padding, aligned_pos - current_pos, 1, f);
for (uint32_t i = 0; i < num_files; i++) {
fwrite(file_datas[i], files[i].size, 1, f);
long data_end = ftell(f);
long padded_end = ((data_end + ALIGNMENT - 1) / ALIGNMENT) * ALIGNMENT;
fwrite(align_padding, padded_end - data_end, 1, f);
}
// Update total size
total_size = ftell(f);
fseek(f, 12, SEEK_SET);
fwrite(&total_size, 4, 1, f);
fclose(f);
}