Task 193: .EU4 File Format

Task 193: .EU4 File Format

File Format Specifications for .EU4

The .EU4 file format is utilized for save games in Europa Universalis IV, a strategy game developed by Paradox Interactive. Based on comprehensive research from reliable sources such as the official game wiki and community discussions, the format varies depending on the save type. Regular (non-ironman) saves may be stored as plain text or compressed ZIP archives containing text files (primarily "gamestate" and "meta"). Ironman saves are always compressed ZIP archives with a binary "gamestate" file. The text format employs a hierarchical key-value pair structure, encoded in Windows-1252, resembling a scripted configuration language with nested braces for sections. File signatures include "EU4txt" for text-based content and "EU4bin" for binary content (after decompression if applicable). The binary format lacks publicly available detailed specifications, limiting full decoding without proprietary tools; thus, the subsequent analysis and code examples focus on the text format for practicality. Compression, when present, uses standard ZIP methods, allowing extraction via common utilities.

List of All Properties Intrinsic to the File Format

The properties refer to the top-level keys typically found in the decompressed text-based .EU4 save file structure. These are intrinsic to the format's organization within the game's file system, representing core game state elements. The list is derived from standard save file layouts as documented in the Europa Universalis IV wiki:

  • date
  • save_game
  • player
  • displayed_country_name
  • savegame_version
  • dlc_enabled
  • mod_enabled
  • multi_player
  • not_observer
  • campaign_id
  • campaign_length
  • speed
  • current_age
  • map_area_data
  • trade
  • production_leader_tag
  • tradegoods_total_produced
  • change_price
  • id
  • dynasty
  • rebel_faction
  • great_powers
  • empire
  • active_trade_league
  • catholic (and other religions, e.g., protestant, muslim)
  • fired_events
  • pending_events
  • provinces
  • countries
  • active_advisors
  • diplomacy
  • active_war
  • previous_war
  • income_statistics
  • nation_size_statistics
  • score_statistics
  • inflation_statistics
  • expanded_dip_action_groups
  • achievement_ok
  • unit
  • trade_company_manager
  • tech_level_dates
  • idea_dates
  • ai

These properties may include simple values (e.g., strings, numbers) or nested structures (e.g., provinces contains sub-keys for each province ID).

Two Direct Download Links for .EU4 Files

Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .EU4 File Dump

The following is a self-contained HTML page with embedded JavaScript suitable for embedding in a Ghost blog post (or any HTML-compatible platform). It provides a drag-and-drop area for uploading a .EU4 file. The script checks if the file is compressed (ZIP), extracts the gamestate if applicable, verifies the "EU4txt" signature, and dumps the specified properties to the screen by parsing the text content. Binary files are not supported and will trigger an error message. For ZIP handling, it uses the JSZip library (loaded via CDN for simplicity).

EU4 File Property Dumper

Drag and Drop .EU4 File to Dump Properties

Drop .EU4 file here

    

Python Class for .EU4 File Handling

The following Python class handles opening, decoding (decompressing if ZIP), reading, writing, and printing the specified properties. It assumes text format; binary is detected and noted as unsupported. Writing modifies an existing file by updating a property (example: update 'date') and saves back, potentially as ZIP if original was compressed.

import zipfile
import io
import os

class EU4FileHandler:
    def __init__(self, filepath):
        self.filepath = filepath
        self.content = None
        self.is_zip = False
        self.properties = [
            'date', 'save_game', 'player', 'displayed_country_name', 'savegame_version',
            'dlc_enabled', 'mod_enabled', 'multi_player', 'not_observer', 'campaign_id',
            'campaign_length', 'speed', 'current_age', 'map_area_data', 'trade',
            'production_leader_tag', 'tradegoods_total_produced', 'change_price', 'id',
            'dynasty', 'rebel_faction', 'great_powers', 'empire', 'active_trade_league',
            'catholic', 'fired_events', 'pending_events', 'provinces', 'countries',
            'active_advisors', 'diplomacy', 'active_war', 'previous_war', 'income_statistics',
            'nation_size_statistics', 'score_statistics', 'inflation_statistics',
            'expanded_dip_action_groups', 'achievement_ok', 'unit', 'trade_company_manager',
            'tech_level_dates', 'idea_dates', 'ai'
        ]

    def read(self):
        with open(self.filepath, 'rb') as f:
            data = f.read()
        if data[:2] == b'\x50\x4b':  # ZIP magic
            self.is_zip = True
            with zipfile.ZipFile(io.BytesIO(data)) as z:
                if 'gamestate' in z.namelist():
                    self.content = z.read('gamestate').decode('windows-1252', errors='ignore')
        else:
            self.content = data.decode('windows-1252', errors='ignore')
        if self.content and not self.content.startswith('EU4txt'):
            print('Unsupported binary format.')
            self.content = None

    def print_properties(self):
        if not self.content:
            print('No content loaded.')
            return
        for prop in self.properties:
            start = self.content.find(f'{prop}=')
            if start != -1:
                end = self.content.find('\n', start)
                while end != -1 and self.content[end+1].isspace():  # Simple nested handling
                    end = self.content.find('\n', end+1)
                value = self.content[start:end].strip()
                print(f'{value}\n')
            else:
                print(f'{prop}: Not found')

    def write(self, new_date='1444.11.11'):  # Example: update 'date'
        if not self.content:
            print('No content to write.')
            return
        self.content = self.content.replace('date=1444.11.11', f'date={new_date}', 1)  # Simple replace
        output_path = self.filepath + '.modified.eu4'
        if self.is_zip:
            with io.BytesIO() as mem:
                with zipfile.ZipFile(mem, 'w') as z:
                    z.writestr('gamestate', self.content.encode('windows-1252'))
                mem.seek(0)
                with open(output_path, 'wb') as f:
                    f.write(mem.read())
        else:
            with open(output_path, 'w', encoding='windows-1252') as f:
                f.write(self.content)
        print(f'File written to {output_path}')

# Example usage:
# handler = EU4FileHandler('example.eu4')
# handler.read()
# handler.print_properties()
# handler.write('1500.1.1')

Java Class for .EU4 File Handling

The following Java class performs similar operations: opening, decoding, reading, writing, and printing properties. It uses java.util.zip for decompression and assumes text format.

import java.io.*;
import java.nio.charset.Charset;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

public class EU4FileHandler {
    private String filepath;
    private String content;
    private boolean isZip;
    private String[] properties = {
            "date", "save_game", "player", "displayed_country_name", "savegame_version",
            "dlc_enabled", "mod_enabled", "multi_player", "not_observer", "campaign_id",
            "campaign_length", "speed", "current_age", "map_area_data", "trade",
            "production_leader_tag", "tradegoods_total_produced", "change_price", "id",
            "dynasty", "rebel_faction", "great_powers", "empire", "active_trade_league",
            "catholic", "fired_events", "pending_events", "provinces", "countries",
            "active_advisors", "diplomacy", "active_war", "previous_war", "income_statistics",
            "nation_size_statistics", "score_statistics", "inflation_statistics",
            "expanded_dip_action_groups", "achievement_ok", "unit", "trade_company_manager",
            "tech_level_dates", "idea_dates", "ai"
    };

    public EU4FileHandler(String filepath) {
        this.filepath = filepath;
        this.content = null;
        this.isZip = false;
    }

    public void read() throws IOException {
        File file = new File(filepath);
        byte[] data = new byte[(int) file.length()];
        try (FileInputStream fis = new FileInputStream(file)) {
            fis.read(data);
        }
        if (data[0] == 0x50 && data[1] == 0x4B) {  // ZIP
            isZip = true;
            try (ZipFile zip = new ZipFile(file)) {
                ZipEntry entry = zip.getEntry("gamestate");
                if (entry != null) {
                    try (InputStream is = zip.getInputStream(entry);
                         ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                        byte[] buffer = new byte[1024];
                        int len;
                        while ((len = is.read(buffer)) > 0) {
                            baos.write(buffer, 0, len);
                        }
                        content = baos.toString("windows-1252");
                    }
                }
            }
        } else {
            content = new String(data, Charset.forName("windows-1252"));
        }
        if (content != null && !content.startsWith("EU4txt")) {
            System.out.println("Unsupported binary format.");
            content = null;
        }
    }

    public void printProperties() {
        if (content == null) {
            System.out.println("No content loaded.");
            return;
        }
        for (String prop : properties) {
            int start = content.indexOf(prop + "=");
            if (start != -1) {
                int end = content.indexOf("\n", start);
                while (end != -1 && Character.isWhitespace(content.charAt(end + 1))) {
                    end = content.indexOf("\n", end + 1);
                }
                String value = content.substring(start, end != -1 ? end : content.length()).trim();
                System.out.println(value + "\n");
            } else {
                System.out.println(prop + ": Not found");
            }
        }
    }

    public void write(String newDate) throws IOException {  // Example: update 'date'
        if (content == null) {
            System.out.println("No content to write.");
            return;
        }
        content = content.replaceFirst("date=\\d{4}\\.\\d{1,2}\\.\\d{1,2}", "date=" + newDate);
        String outputPath = filepath + ".modified.eu4";
        if (isZip) {
            try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
                 ZipOutputStream zos = new ZipOutputStream(baos)) {
                zos.putNextEntry(new ZipEntry("gamestate"));
                zos.write(content.getBytes("windows-1252"));
                zos.closeEntry();
            } catch (IOException e) {
                throw e;
            }
            // Write baos to file (omitted for brevity)
        } else {
            try (FileWriter fw = new FileWriter(outputPath, Charset.forName("windows-1252"))) {
                fw.write(content);
            }
        }
        System.out.println("File written to " + outputPath);
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     EU4FileHandler handler = new EU4FileHandler("example.eu4");
    //     handler.read();
    //     handler.printProperties();
    //     handler.write("1500.1.1");
    // }
}

JavaScript Class for .EU4 File Handling

The following JavaScript class is designed for Node.js (requires 'fs' and 'adm-zip' modules; install via npm if needed). It handles opening, decoding, reading, writing, and printing properties.

const fs = require('fs');
const AdmZip = require('adm-zip');

class EU4FileHandler {
    constructor(filepath) {
        this.filepath = filepath;
        this.content = null;
        this.isZip = false;
        this.properties = [
            'date', 'save_game', 'player', 'displayed_country_name', 'savegame_version',
            'dlc_enabled', 'mod_enabled', 'multi_player', 'not_observer', 'campaign_id',
            'campaign_length', 'speed', 'current_age', 'map_area_data', 'trade',
            'production_leader_tag', 'tradegoods_total_produced', 'change_price', 'id',
            'dynasty', 'rebel_faction', 'great_powers', 'empire', 'active_trade_league',
            'catholic', 'fired_events', 'pending_events', 'provinces', 'countries',
            'active_advisors', 'diplomacy', 'active_war', 'previous_war', 'income_statistics',
            'nation_size_statistics', 'score_statistics', 'inflation_statistics',
            'expanded_dip_action_groups', 'achievement_ok', 'unit', 'trade_company_manager',
            'tech_level_dates', 'idea_dates', 'ai'
        ];
    }

    read() {
        const data = fs.readFileSync(this.filepath);
        if (data[0] === 0x50 && data[1] === 0x4B) {  // ZIP
            this.isZip = true;
            const zip = new AdmZip(this.filepath);
            const entry = zip.getEntry('gamestate');
            if (entry) {
                this.content = zip.readAsText(entry, 'win1252');
            }
        } else {
            this.content = data.toString('win1252');
        }
        if (this.content && !this.content.startsWith('EU4txt')) {
            console.log('Unsupported binary format.');
            this.content = null;
        }
    }

    printProperties() {
        if (!this.content) {
            console.log('No content loaded.');
            return;
        }
        this.properties.forEach(prop => {
            const regex = new RegExp(`^${prop}=(.*?(?=\\n\\w+=|$))`, 'gms');
            const match = regex.exec(this.content);
            if (match) {
                console.log(`${prop}=${match[1].trim()}\n`);
            } else {
                console.log(`${prop}: Not found`);
            }
        });
    }

    write(newDate = '1444.11.11') {  // Example: update 'date'
        if (!this.content) {
            console.log('No content to write.');
            return;
        }
        this.content = this.content.replace(/date=\d{4}\.\d{1,2}\.\d{1,2}/, `date=${newDate}`);
        const outputPath = this.filepath + '.modified.eu4';
        if (this.isZip) {
            const zip = new AdmZip();
            zip.addFile('gamestate', Buffer.from(this.content, 'win1252'));
            zip.writeZip(outputPath);
        } else {
            fs.writeFileSync(outputPath, this.content, { encoding: 'win1252' });
        }
        console.log(`File written to ${outputPath}`);
    }
}

// Example usage:
// const handler = new EU4FileHandler('example.eu4');
// handler.read();
// handler.printProperties();
// handler.write('1500.1.1');

C++ Class for .EU4 File Handling

The following C++ class handles similar functionality. It uses <zip.h> for ZIP (assuming libzip installed) and assumes text format. Compilation requires linking to libzip.

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <zip.h>
#include <regex>

class EU4FileHandler {
private:
    std::string filepath;
    std::string content;
    bool isZip;
    std::vector<std::string> properties = {
        "date", "save_game", "player", "displayed_country_name", "savegame_version",
        "dlc_enabled", "mod_enabled", "multi_player", "not_observer", "campaign_id",
        "campaign_length", "speed", "current_age", "map_area_data", "trade",
        "production_leader_tag", "tradegoods_total_produced", "change_price", "id",
        "dynasty", "rebel_faction", "great_powers", "empire", "active_trade_league",
        "catholic", "fired_events", "pending_events", "provinces", "countries",
        "active_advisors", "diplomacy", "active_war", "previous_war", "income_statistics",
        "nation_size_statistics", "score_statistics", "inflation_statistics",
        "expanded_dip_action_groups", "achievement_ok", "unit", "trade_company_manager",
        "tech_level_dates", "idea_dates", "ai"
    };

public:
    EU4FileHandler(const std::string& fp) : filepath(fp), content(""), isZip(false) {}

    void read() {
        std::ifstream file(filepath, std::ios::binary);
        std::vector<char> data((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
        if (data.size() > 1 && data[0] == 'P' && data[1] == 'K') {  // ZIP
            isZip = true;
            zip *z = zip_open(filepath.c_str(), 0, nullptr);
            if (z) {
                zip_file *zf = zip_fopen(z, "gamestate", 0);
                if (zf) {
                    zip_stat_t stat;
                    zip_stat(z, "gamestate", 0, &stat);
                    content.resize(stat.size);
                    zip_fread(zf, &content[0], stat.size);
                    zip_fclose(zf);
                }
                zip_close(z);
            }
        } else {
            content = std::string(data.begin(), data.end());
        }
        if (!content.empty() && content.substr(0, 6) != "EU4txt") {
            std::cout << "Unsupported binary format." << std::endl;
            content = "";
        }
    }

    void printProperties() {
        if (content.empty()) {
            std::cout << "No content loaded." << std::endl;
            return;
        }
        for (const auto& prop : properties) {
            std::regex regex("^" + prop + "=(.*?(?=\\n\\w+=|$))", std::regex::multiline | std::regex::dotall);
            std::smatch match;
            if (std::regex_search(content, match, regex)) {
                std::cout << prop << "=" << match[1].str() << std::endl << std::endl;
            } else {
                std::cout << prop << ": Not found" << std::endl;
            }
        }
    }

    void write(const std::string& newDate = "1444.11.11") {  // Example: update 'date'
        if (content.empty()) {
            std::cout << "No content to write." << std::endl;
            return;
        }
        std::regex dateRegex("date=\\d{4}\\.\\d{1,2}\\.\\d{1,2}");
        content = std::regex_replace(content, dateRegex, "date=" + newDate, std::regex_constants::format_first_only);
        std::string outputPath = filepath + ".modified.eu4";
        if (isZip) {
            zip *z = zip_open(outputPath.c_str(), ZIP_CREATE, nullptr);
            if (z) {
                zip_source *src = zip_source_buffer(z, content.c_str(), content.size(), 0);
                zip_file_add(z, "gamestate", src, ZIP_FL_OVERWRITE);
                zip_close(z);
            }
        } else {
            std::ofstream out(outputPath);
            out << content;
        }
        std::cout << "File written to " << outputPath << std::endl;
    }
};

// Example usage:
// int main() {
//     EU4FileHandler handler("example.eu4");
//     handler.read();
//     handler.printProperties();
//     handler.write("1500.1.1");
//     return 0;
// }