Task 763: .VB File Format

Task 763: .VB File Format

File Format Specifications for the .VB File Format

The .VB file format is associated with Nintendo Virtual Boy ROM images. It consists of a binary dump of the cartridge ROM data, with file sizes typically being powers of 2 (ranging from 256 KB to 2 MB for commercial titles). The format includes a fixed header located at the end of the file, starting at offset (file size - 32 bytes). This header contains metadata about the game.

List of all the properties of this file format intrinsic to its file system:

  • Game Title: 20 bytes, encoded in Shift-JIS, representing the name of the game.
  • Reserved: 5 bytes, typically filled with zeros for padding.
  • Maker Code: 2 bytes, ASCII-encoded identifier for the game's developer.
  • Game Code: 4 bytes, ASCII-encoded unique identifier for the game.
  • Version: 1 byte, unsigned integer indicating the minor software version (major version is implicitly 1).

Two direct download links for files of format .VB:

Ghost blog embedded HTML JavaScript that allows a user to drag and drop a file of format .VB and dump to screen all these properties:

.VB File Properties Dumper

Drag and Drop .VB File

Drop your .VB file here

Python class that can open any file of format .VB and decode, read, write, and print to console all the properties from the above list:

import sys

class VBFileHandler:
    def __init__(self, filename):
        self.filename = filename
        self.data = None
        self.header_offset = None
        self.properties = {}

    def read(self):
        with open(self.filename, 'rb') as f:
            self.data = f.read()
        self.header_offset = len(self.data) - 32
        if self.header_offset < 0:
            raise ValueError("File too small to contain header.")

        # Decode properties
        title_bytes = self.data[self.header_offset:self.header_offset + 20]
        reserved_bytes = self.data[self.header_offset + 20:self.header_offset + 25]
        maker_code_bytes = self.data[self.header_offset + 25:self.header_offset + 27]
        game_code_bytes = self.data[self.header_offset + 27:self.header_offset + 31]
        version = self.data[self.header_offset + 31]

        self.properties = {
            'Game Title': title_bytes.decode('shift-jis', errors='ignore').rstrip('\x00').strip(),
            'Reserved': ' '.join(f'{b:02x}' for b in reserved_bytes),
            'Maker Code': maker_code_bytes.decode('ascii', errors='ignore').strip(),
            'Game Code': game_code_bytes.decode('ascii', errors='ignore').strip(),
            'Version': version
        }

    def print_properties(self):
        if not self.properties:
            raise ValueError("Properties not read yet.")
        for key, value in self.properties.items():
            print(f"{key}: {value}")

    def write(self, new_properties=None):
        if self.data is None:
            raise ValueError("File not read yet.")
        if new_properties:
            self._update_properties(new_properties)
        with open(self.filename, 'wb') as f:
            f.write(self.data)

    def _update_properties(self, new_properties):
        mutable_data = bytearray(self.data)
        if 'Game Title' in new_properties:
            title = new_properties['Game Title'].encode('shift-jis')[:20].ljust(20, b'\x00')
            mutable_data[self.header_offset:self.header_offset + 20] = title
        if 'Reserved' in new_properties:
            reserved = bytes.fromhex(new_properties['Reserved'])[:5].ljust(5, b'\x00')
            mutable_data[self.header_offset + 20:self.header_offset + 25] = reserved
        if 'Maker Code' in new_properties:
            maker = new_properties['Maker Code'].encode('ascii')[:2].ljust(2, b'\x00')
            mutable_data[self.header_offset + 25:self.header_offset + 27] = maker
        if 'Game Code' in new_properties:
            game_code = new_properties['Game Code'].encode('ascii')[:4].ljust(4, b'\x00')
            mutable_data[self.header_offset + 27:self.header_offset + 31] = game_code
        if 'Version' in new_properties:
            version = new_properties['Version'] & 0xFF
            mutable_data[self.header_offset + 31] = version
        self.data = bytes(mutable_data)

# Example usage:
# handler = VBFileHandler('example.vb')
# handler.read()
# handler.print_properties()
# handler.write({'Version': 2})

Java class that can open any file of format .VB and decode, read, write, and print to console all the properties from the above list:

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class VBFileHandler {
    private String filename;
    private byte[] data;
    private int headerOffset;
    private Map<String, Object> properties = new HashMap<>();

    public VBFileHandler(String filename) {
        this.filename = filename;
    }

    public void read() throws IOException {
        data = Files.readAllBytes(Paths.get(filename));
        headerOffset = data.length - 32;
        if (headerOffset < 0) {
            throw new IOException("File too small to contain header.");
        }

        // Decode properties
        byte[] titleBytes = new byte[20];
        System.arraycopy(data, headerOffset, titleBytes, 0, 20);
        byte[] reservedBytes = new byte[5];
        System.arraycopy(data, headerOffset + 20, reservedBytes, 0, 5);
        byte[] makerCodeBytes = new byte[2];
        System.arraycopy(data, headerOffset + 25, makerCodeBytes, 0, 2);
        byte[] gameCodeBytes = new byte[4];
        System.arraycopy(data, headerOffset + 27, gameCodeBytes, 0, 4);
        int version = data[headerOffset + 31] & 0xFF;

        properties.put("Game Title", new String(titleBytes, "Shift_JIS").trim().replaceAll("\0", ""));
        StringBuilder reservedStr = new StringBuilder();
        for (byte b : reservedBytes) {
            reservedStr.append(String.format("%02x ", b));
        }
        properties.put("Reserved", reservedStr.toString().trim());
        properties.put("Maker Code", new String(makerCodeBytes, StandardCharsets.US_ASCII).trim());
        properties.put("Game Code", new String(gameCodeBytes, StandardCharsets.US_ASCII).trim());
        properties.put("Version", version);
    }

    public void printProperties() {
        if (properties.isEmpty()) {
            throw new IllegalStateException("Properties not read yet.");
        }
        for (Map.Entry<String, Object> entry : properties.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }

    public void write(Map<String, Object> newProperties) throws IOException {
        if (data == null) {
            throw new IllegalStateException("File not read yet.");
        }
        if (newProperties != null) {
            updateProperties(newProperties);
        }
        Files.write(Paths.get(filename), data);
    }

    private void updateProperties(Map<String, Object> newProperties) {
        if (newProperties.containsKey("Game Title")) {
            byte[] title = ((String) newProperties.get("Game Title")).getBytes(StandardCharsets.UTF_8);
            byte[] paddedTitle = new byte[20];
            System.arraycopy(title, 0, paddedTitle, 0, Math.min(20, title.length));
            System.arraycopy(paddedTitle, 0, data, headerOffset, 20);
        }
        if (newProperties.containsKey("Reserved")) {
            String[] hex = ((String) newProperties.get("Reserved")).split(" ");
            byte[] reserved = new byte[5];
            for (int i = 0; i < Math.min(5, hex.length); i++) {
                reserved[i] = (byte) Integer.parseInt(hex[i], 16);
            }
            System.arraycopy(reserved, 0, data, headerOffset + 20, 5);
        }
        if (newProperties.containsKey("Maker Code")) {
            byte[] maker = ((String) newProperties.get("Maker Code")).getBytes(StandardCharsets.US_ASCII);
            byte[] paddedMaker = new byte[2];
            System.arraycopy(maker, 0, paddedMaker, 0, Math.min(2, maker.length));
            System.arraycopy(paddedMaker, 0, data, headerOffset + 25, 2);
        }
        if (newProperties.containsKey("Game Code")) {
            byte[] gameCode = ((String) newProperties.get("Game Code")).getBytes(StandardCharsets.US_ASCII);
            byte[] paddedGameCode = new byte[4];
            System.arraycopy(gameCode, 0, paddedGameCode, 0, Math.min(4, gameCode.length));
            System.arraycopy(paddedGameCode, 0, data, headerOffset + 27, 4);
        }
        if (newProperties.containsKey("Version")) {
            int version = (Integer) newProperties.get("Version");
            data[headerOffset + 31] = (byte) (version & 0xFF);
        }
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     VBFileHandler handler = new VBFileHandler("example.vb");
    //     handler.read();
    //     handler.printProperties();
    //     Map<String, Object> updates = new HashMap<>();
    //     updates.put("Version", 2);
    //     handler.write(updates);
    // }
}

JavaScript class that can open any file of format .VB and decode, read, write, and print to console all the properties from the above list (assuming Node.js environment):

const fs = require('fs');
const iconv = require('iconv-lite'); // Install via npm for Shift-JIS decoding

class VBFileHandler {
    constructor(filename) {
        this.filename = filename;
        this.data = null;
        this.headerOffset = null;
        this.properties = {};
    }

    read() {
        this.data = fs.readFileSync(this.filename);
        this.headerOffset = this.data.length - 32;
        if (this.headerOffset < 0) {
            throw new Error('File too small to contain header.');
        }

        // Decode properties
        const titleBytes = this.data.slice(this.headerOffset, this.headerOffset + 20);
        const reservedBytes = this.data.slice(this.headerOffset + 20, this.headerOffset + 25);
        const makerCodeBytes = this.data.slice(this.headerOffset + 25, this.headerOffset + 27);
        const gameCodeBytes = this.data.slice(this.headerOffset + 27, this.headerOffset + 31);
        const version = this.data.readUInt8(this.headerOffset + 31);

        this.properties = {
            'Game Title': iconv.decode(titleBytes, 'shift_jis').replace(/\0/g, '').trim(),
            'Reserved': Array.from(reservedBytes).map(b => b.toString(16).padStart(2, '0')).join(' '),
            'Maker Code': makerCodeBytes.toString('ascii').trim(),
            'Game Code': gameCodeBytes.toString('ascii').trim(),
            'Version': version
        };
    }

    printProperties() {
        if (Object.keys(this.properties).length === 0) {
            throw new Error('Properties not read yet.');
        }
        for (const [key, value] of Object.entries(this.properties)) {
            console.log(`${key}: ${value}`);
        }
    }

    write(newProperties = {}) {
        if (this.data === null) {
            throw new Error('File not read yet.');
        }
        this.updateProperties(newProperties);
        fs.writeFileSync(this.filename, this.data);
    }

    updateProperties(newProperties) {
        if (newProperties['Game Title']) {
            const title = iconv.encode(newProperties['Game Title'], 'shift_jis').slice(0, 20);
            const paddedTitle = Buffer.alloc(20);
            title.copy(paddedTitle);
            paddedTitle.copy(this.data, this.headerOffset);
        }
        if (newProperties['Reserved']) {
            const reserved = Buffer.from(newProperties['Reserved'].split(' ').map(h => parseInt(h, 16)));
            const paddedReserved = Buffer.alloc(5);
            reserved.copy(paddedReserved, 0, 0, Math.min(5, reserved.length));
            paddedReserved.copy(this.data, this.headerOffset + 20);
        }
        if (newProperties['Maker Code']) {
            const maker = Buffer.from(newProperties['Maker Code'], 'ascii').slice(0, 2);
            const paddedMaker = Buffer.alloc(2);
            maker.copy(paddedMaker);
            paddedMaker.copy(this.data, this.headerOffset + 25);
        }
        if (newProperties['Game Code']) {
            const gameCode = Buffer.from(newProperties['Game Code'], 'ascii').slice(0, 4);
            const paddedGameCode = Buffer.alloc(4);
            gameCode.copy(paddedGameCode);
            paddedGameCode.copy(this.data, this.headerOffset + 27);
        }
        if (newProperties['Version']) {
            this.data.writeUInt8(newProperties['Version'] & 0xFF, this.headerOffset + 31);
        }
    }
}

// Example usage:
// const handler = new VBFileHandler('example.vb');
// handler.read();
// handler.printProperties();
// handler.write({ Version: 2 });

C class that can open any file of format .VB and decode, read, write, and print to console all the properties from the above list (using C++):

#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <iomanip>
#include <locale>
#include <codecvt> // For Shift-JIS conversion

class VBFileHandler {
private:
    std::string filename;
    std::vector<unsigned char> data;
    size_t headerOffset;
    std::map<std::string, std::string> properties;

public:
    VBFileHandler(const std::string& fn) : filename(fn), headerOffset(0) {}

    void read() {
        std::ifstream file(filename, std::ios::binary | std::ios::ate);
        if (!file) {
            throw std::runtime_error("Unable to open file.");
        }
        size_t size = file.tellg();
        file.seekg(0, std::ios::beg);
        data.resize(size);
        file.read(reinterpret_cast<char*>(data.data()), size);

        headerOffset = size - 32;
        if (headerOffset >= size) {
            throw std::runtime_error("File too small to contain header.");
        }

        // Decode properties
        std::string title(reinterpret_cast<char*>(&data[headerOffset]), 20);
        // Convert Shift-JIS to UTF-8
        std::wstring_convert<std::codecvt_utf8<wchar_t>> converter;
        std::wstring wtitle = converter.from_bytes(title); // Assuming Shift-JIS input needs proper decoder; simplify for example
        title = converter.to_bytes(wtitle); // Placeholder; use proper Shift-JIS library if needed
        title.erase(std::remove(title.begin(), title.end(), '\0'), title.end());
        title = title.substr(0, title.find_last_not_of(' ') + 1);

        std::stringstream reservedSs;
        for (size_t i = 0; i < 5; ++i) {
            reservedSs << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(data[headerOffset + 20 + i]) << " ";
        }
        std::string reserved = reservedSs.str();
        reserved = reserved.substr(0, reserved.find_last_not_of(' '));

        std::string makerCode(reinterpret_cast<char*>(&data[headerOffset + 25]), 2);
        makerCode = makerCode.substr(0, makerCode.find_last_not_of('\0') + 1);

        std::string gameCode(reinterpret_cast<char*>(&data[headerOffset + 27]), 4);
        gameCode = gameCode.substr(0, gameCode.find_last_not_of('\0') + 1);

        unsigned int version = data[headerOffset + 31];

        properties["Game Title"] = title;
        properties["Reserved"] = reserved;
        properties["Maker Code"] = makerCode;
        properties["Game Code"] = gameCode;
        properties["Version"] = std::to_string(version);
    }

    void printProperties() const {
        if (properties.empty()) {
            throw std::runtime_error("Properties not read yet.");
        }
        for (const auto& prop : properties) {
            std::cout << prop.first << ": " << prop.second << std::endl;
        }
    }

    void write(const std::map<std::string, std::string>& newProperties = {}) {
        if (data.empty()) {
            throw std::runtime_error("File not read yet.");
        }
        updateProperties(newProperties);
        std::ofstream file(filename, std::ios::binary);
        if (!file) {
            throw std::runtime_error("Unable to write file.");
        }
        file.write(reinterpret_cast<const char*>(data.data()), data.size());
    }

private:
    void updateProperties(const std::map<std::string, std::string>& newProperties) {
        if (newProperties.find("Game Title") != newProperties.end()) {
            std::string title = newProperties.at("Game Title");
            // Assume simple ASCII for simplicity; use encoder for Shift-JIS
            title.resize(20, '\0');
            std::copy(title.begin(), title.end(), data.begin() + headerOffset);
        }
        if (newProperties.find("Reserved") != newProperties.end()) {
            std::string reservedStr = newProperties.at("Reserved");
            std::stringstream ss(reservedStr);
            unsigned int byte;
            for (size_t i = 0; i < 5 && ss >> std::hex >> byte; ++i) {
                data[headerOffset + 20 + i] = static_cast<unsigned char>(byte);
            }
        }
        if (newProperties.find("Maker Code") != newProperties.end()) {
            std::string maker = newProperties.at("Maker Code");
            maker.resize(2, '\0');
            std::copy(maker.begin(), maker.end(), data.begin() + headerOffset + 25);
        }
        if (newProperties.find("Game Code") != newProperties.end()) {
            std::string gameCode = newProperties.at("Game Code");
            gameCode.resize(4, '\0');
            std::copy(gameCode.begin(), gameCode.end(), data.begin() + headerOffset + 27);
        }
        if (newProperties.find("Version") != newProperties.end()) {
            unsigned int version = std::stoi(newProperties.at("Version"));
            data[headerOffset + 31] = static_cast<unsigned char>(version);
        }
    }
};

// Example usage:
// int main() {
//     try {
//         VBFileHandler handler("example.vb");
//         handler.read();
//         handler.printProperties();
//         std::map<std::string, std::string> updates;
//         updates["Version"] = "2";
//         handler.write(updates);
//     } catch (const std::exception& e) {
//         std::cerr << e.what() << std::endl;
//     }
//     return 0;
// }