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.

The following are direct download links to sample .VPP files from Red Faction community mods, which conform to the Volition .VPP format:

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.

Drag and drop a .VPP file here

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);
}