Task 291: .HOI4 File Format

Task 291: .HOI4 File Format

.HOI4 File Format Specifications

The .HOI4 file format is used for save files in the game Hearts of Iron IV (HOI4), developed by Paradox Interactive. It is a ZIP archive containing compressed data files that store the game state. The contained files are typically 'meta' (metadata like player info and game settings) and 'gamestate' (the main game data like countries, armies, etc.). Optionally, it may include 'rnsshot.png' for a screenshot. The 'meta' and 'gamestate' files can be in plaintext or binary format. Plaintext files use a custom key-value pair syntax similar to a mix of INI and JSON, with nesting using curly braces {}. Binary files are a tokenized version for efficiency, with keys as u16 integers and typed values. Plaintext files start with the header "HOI4txt", while binary files start with "HOI4bin". The encoding for plaintext is UTF-8. The syntax allows keys followed by = and values, which can be strings, numbers, dates, or nested objects.

  1. List of all the properties of this file format intrinsic to its file system:
  • File extension: .hoi4
  • Container type: ZIP archive
  • ZIP magic number: PK\x03\x04 (hex: 50 4B 03 04)
  • Contained files: meta, gamestate (required); rnsshot.png (optional)
  • Format of contained files: plaintext or binary
  • Plaintext header: HOI4txt
  • Binary header: HOI4bin
  • Syntax for plaintext: key = value or key = { nested key = value }
  • Value types: strings (quoted if needed), numbers, dates (e.g., 1936.1.1.1), arrays, nested objects
  • Encoding: UTF-8 for plaintext
  • Typical top-level keys in meta: player, date, ideology, is_ironman, mods, dlc_enabled, campaign_id
  • Typical top-level keys in gamestate: date, speed, player, mods, countries, states, diplomacy, wars, peace_conferences, buildings, production, research, ideas, decisions, events, ai, flags, variables
  1. Two direct download links for files of format .HOI4:
  1. Ghost blog embedded HTML JavaScript for drag and drop to dump properties:
HOI4 File Dumper
Drag and drop .HOI4 file here

    

  1. Python class:
import zipfile
import os

class HOI4FileHandler:
    def __init__(self, filepath):
        self.filepath = filepath
        self.properties = [
            'File extension: .hoi4',
            'Container type: ZIP archive',
            'ZIP magic number: PK\\x03\\x04',
            'Contained files: meta, gamestate (required); rnsshot.png (optional)',
            'Format of contained files: plaintext or binary',
            'Plaintext header: HOI4txt',
            'Binary header: HOI4bin',
            'Syntax for plaintext: key = value or key = { nested key = value }',
            'Value types: strings, numbers, dates, arrays, nested objects',
            'Encoding: UTF-8 for plaintext'
        ]
        self.meta_content = None
        self.gamestate_content = None

    def read_decode(self):
        with zipfile.ZipFile(self.filepath, 'r') as zf:
            if 'meta' in zf.namelist():
                with zf.open('meta') as f:
                    data = f.read()
                    if data.startswith(b'HOI4bin'):
                        print("Binary format detected for meta. Decoding not supported without token resolver.")
                    else:
                        self.meta_content = data.decode('utf-8')
            if 'gamestate' in zf.namelist():
                with zf.open('gamestate') as f:
                    data = f.read()
                    if data.startswith(b'HOI4bin'):
                        print("Binary format detected for gamestate. Decoding not supported without token resolver.")
                    else:
                        self.gamestate_content = data.decode('utf-8')

    def print_properties(self):
        for prop in self.properties:
            print(prop)
        if self.meta_content:
            print("\nMeta content:")
            print(self.meta_content)
        if self.gamestate_content:
            print("\nGamestate content:")
            print(self.gamestate_content)

    def write(self, new_filepath, new_meta=None, new_gamestate=None):
        with zipfile.ZipFile(new_filepath, 'w') as zf:
            if new_meta or self.meta_content:
                zf.writestr('meta', new_meta or self.meta_content)
            if new_gamestate or self.gamestate_content:
                zf.writestr('gamestate', new_gamestate or self.gamestate_content)

# Example usage:
# handler = HOI4FileHandler('example.hoi4')
# handler.read_decode()
# handler.print_properties()
# handler.write('new.hoi4', new_meta='HOI4txt\nplayer="NEW"')
  1. Java class:
import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

public class HOI4FileHandler {
    private String filepath;
    private String[] properties = {
        "File extension: .hoi4",
        "Container type: ZIP archive",
        "ZIP magic number: PK\\x03\\x04",
        "Contained files: meta, gamestate (required); rnsshot.png (optional)",
        "Format of contained files: plaintext or binary",
        "Plaintext header: HOI4txt",
        "Binary header: HOI4bin",
        "Syntax for plaintext: key = value or key = { nested key = value }",
        "Value types: strings, numbers, dates, arrays, nested objects",
        "Encoding: UTF-8 for plaintext"
    };
    private String metaContent;
    private String gamestateContent;

    public HOI4FileHandler(String filepath) {
        this.filepath = filepath;
    }

    public void readDecode() throws IOException {
        try (ZipFile zf = new ZipFile(filepath)) {
            ZipEntry metaEntry = zf.getEntry("meta");
            if (metaEntry != null) {
                try (InputStream is = zf.getInputStream(metaEntry);
                     ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                    byte[] buffer = new byte[1024];
                    int len;
                    while ((len = is.read(buffer)) > 0) {
                        baos.write(buffer, 0, len);
                    }
                    byte[] data = baos.toByteArray();
                    if (new String(data, 0, 7).equals("HOI4bin")) {
                        System.out.println("Binary format detected for meta. Decoding not supported without token resolver.");
                    } else {
                        metaContent = new String(data, "UTF-8");
                    }
                }
            }
            ZipEntry gamestateEntry = zf.getEntry("gamestate");
            if (gamestateEntry != null) {
                try (InputStream is = zf.getInputStream(gamestateEntry);
                     ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                    byte[] buffer = new byte[1024];
                    int len;
                    while ((len = is.read(buffer)) > 0) {
                        baos.write(buffer, 0, len);
                    }
                    byte[] data = baos.toByteArray();
                    if (new String(data, 0, 7).equals("HOI4bin")) {
                        System.out.println("Binary format detected for gamestate. Decoding not supported without token resolver.");
                    } else {
                        gamestateContent = new String(data, "UTF-8");
                    }
                }
            }
        }
    }

    public void printProperties() {
        for (String prop : properties) {
            System.out.println(prop);
        }
        if (metaContent != null) {
            System.out.println("\nMeta content:");
            System.out.println(metaContent);
        }
        if (gamestateContent != null) {
            System.out.println("\nGamestate content:");
            System.out.println(gamestateContent);
        }
    }

    public void write(String newFilepath, String newMeta, String newGamestate) throws IOException {
        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(newFilepath))) {
            if (newMeta != null || metaContent != null) {
                zos.putNextEntry(new ZipEntry("meta"));
                zos.write((newMeta != null ? newMeta : metaContent).getBytes("UTF-8"));
                zos.closeEntry();
            }
            if (newGamestate != null || gamestateContent != null) {
                zos.putNextEntry(new ZipEntry("gamestate"));
                zos.write((newGamestate != null ? newGamestate : gamestateContent).getBytes("UTF-8"));
                zos.closeEntry();
            }
        }
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     HOI4FileHandler handler = new HOI4FileHandler("example.hoi4");
    //     handler.readDecode();
    //     handler.printProperties();
    //     handler.write("new.hoi4", "HOI4txt\nplayer=\"NEW\"", null);
    // }
}
  1. JavaScript class:
const JSZip = require('jszip'); // For Node.js, install jszip
const fs = require('fs');

class HOI4FileHandler {
    constructor(filepath) {
        this.filepath = filepath;
        this.properties = [
            'File extension: .hoi4',
            'Container type: ZIP archive',
            'ZIP magic number: PK\\x03\\x04',
            'Contained files: meta, gamestate (required); rnsshot.png (optional)',
            'Format of contained files: plaintext or binary',
            'Plaintext header: HOI4txt',
            'Binary header: HOI4bin',
            'Syntax for plaintext: key = value or key = { nested key = value }',
            'Value types: strings, numbers, dates, arrays, nested objects',
            'Encoding: UTF-8 for plaintext'
        ];
        this.metaContent = null;
        this.gamestateContent = null;
    }

    async readDecode() {
        const data = fs.readFileSync(this.filepath);
        const zip = await JSZip.loadAsync(data);
        const meta = await zip.file('meta')?.async('nodebuffer');
        if (meta) {
            const header = meta.toString('utf8', 0, 7);
            if (header === 'HOI4bin') {
                console.log('Binary format detected for meta. Decoding not supported without token resolver.');
            } else {
                this.metaContent = meta.toString('utf8');
            }
        }
        const gamestate = await zip.file('gamestate')?.async('nodebuffer');
        if (gamestate) {
            const header = gamestate.toString('utf8', 0, 7);
            if (header === 'HOI4bin') {
                console.log('Binary format detected for gamestate. Decoding not supported without token resolver.');
            } else {
                this.gamestateContent = gamestate.toString('utf8');
            }
        }
    }

    printProperties() {
        this.properties.forEach(prop => console.log(prop));
        if (this.metaContent) {
            console.log('\nMeta content:');
            console.log(this.metaContent);
        }
        if (this.gamestateContent) {
            console.log('\nGamestate content:');
            console.log(this.gamestateContent);
        }
    }

    async write(newFilepath, newMeta = null, newGamestate = null) {
        const zip = new JSZip();
        if (newMeta || this.metaContent) {
            zip.file('meta', newMeta || this.metaContent);
        }
        if (newGamestate || this.gamestateContent) {
            zip.file('gamestate', newGamestate || this.gamestateContent);
        }
        const content = await zip.generateAsync({type: 'nodebuffer'});
        fs.writeFileSync(newFilepath, content);
    }
}

// Example usage:
// (async () => {
//     const handler = new HOI4FileHandler('example.hoi4');
//     await handler.readDecode();
//     handler.printProperties();
//     await handler.write('new.hoi4', 'HOI4txt\nplayer="NEW"');
// })();
  1. C class (using C++ for class support and libzip for ZIP handling; assume libzip is available):
#include <iostream>
#include <string>
#include <vector>
#include <fstream>
#include <zip.h>

class HOI4FileHandler {
private:
    std::string filepath;
    std::vector<std::string> properties = {
        "File extension: .hoi4",
        "Container type: ZIP archive",
        "ZIP magic number: PK\\x03\\x04",
        "Contained files: meta, gamestate (required); rnsshot.png (optional)",
        "Format of contained files: plaintext or binary",
        "Plaintext header: HOI4txt",
        "Binary header: HOI4bin",
        "Syntax for plaintext: key = value or key = { nested key = value }",
        "Value types: strings, numbers, dates, arrays, nested objects",
        "Encoding: UTF-8 for plaintext"
    };
    std::string metaContent;
    std::string gamestateContent;

    std::string readZipFile(zip_t* zip, const std::string& filename) {
        zip_file_t* f = zip_fopen(zip, filename.c_str(), 0);
        if (!f) return "";
        std::string content;
        char buffer[1024];
        zip_int64_t len;
        while ((len = zip_fread(f, buffer, sizeof(buffer))) > 0) {
            content.append(buffer, len);
        }
        zip_fclose(f);
        return content;
    }

public:
    HOI4FileHandler(const std::string& fp) : filepath(fp) {}

    void readDecode() {
        int err = 0;
        zip_t* zip = zip_open(filepath.c_str(), ZIP_RDONLY, &err);
        if (!zip) {
            std::cout << "Error opening ZIP: " << err << std::endl;
            return;
        }
        std::string meta = readZipFile(zip, "meta");
        if (!meta.empty()) {
            if (meta.substr(0, 7) == "HOI4bin") {
                std::cout << "Binary format detected for meta. Decoding not supported without token resolver." << std::endl;
            } else {
                metaContent = meta;
            }
        }
        std::string gamestate = readZipFile(zip, "gamestate");
        if (!gamestate.empty()) {
            if (gamestate.substr(0, 7) == "HOI4bin") {
                std::cout << "Binary format detected for gamestate. Decoding not supported without token resolver." << std::endl;
            } else {
                gamestateContent = gamestate;
            }
        }
        zip_close(zip);
    }

    void printProperties() {
        for (const auto& prop : properties) {
            std::cout << prop << std::endl;
        }
        if (!metaContent.empty()) {
            std::cout << "\nMeta content:" << std::endl << metaContent << std::endl;
        }
        if (!gamestateContent.empty()) {
            std::cout << "\nGamestate content:" << std::endl << gamestateContent << std::endl;
        }
    }

    void write(const std::string& newFilepath, const std::string& newMeta = "", const std::string& newGamestate = "") {
        int err = 0;
        zip_t* zip = zip_open(newFilepath.c_str(), ZIP_CREATE | ZIP_TRUNCATE, &err);
        if (!zip) {
            std::cout << "Error creating ZIP: " << err << std::endl;
            return;
        }
        std::string useMeta = newMeta.empty() ? metaContent : newMeta;
        if (!useMeta.empty()) {
            zip_source_t* src = zip_source_buffer(zip, useMeta.c_str(), useMeta.size(), 0);
            zip_file_add(zip, "meta", src, ZIP_FL_OVERWRITE);
        }
        std::string useGamestate = newGamestate.empty() ? gamestateContent : newGamestate;
        if (!useGamestate.empty()) {
            zip_source_t* src = zip_source_buffer(zip, useGamestate.c_str(), useGamestate.size(), 0);
            zip_file_add(zip, "gamestate", src, ZIP_FL_OVERWRITE);
        }
        zip_close(zip);
    }
};

// Example usage:
// int main() {
//     HOI4FileHandler handler("example.hoi4");
//     handler.readDecode();
//     handler.printProperties();
//     handler.write("new.hoi4", "HOI4txt\nplayer=\"NEW\"");
//     return 0;
// }