A Ruby gem for parsing, editing, and re-serializing the proprietary script
files used by Paradox Interactive games — EU4, Stellaris, Imperator: Rome,
and EU5. The parser is Rust (a pest grammar exposed as
a native extension via magnus); the
rest is Ruby, including a small DSL for writing mods that compile down to
Paradox's script format.
This is a personal long-term project, not a published library. The gem isn't on RubyGems — install it locally (instructions below). Current state:
- Runs on Ruby 4.0.3 + Rust 1.95.0 (pinned in
.tool-versions). - Round-trip preserves the original bytes — whitespace, comments, BOMs, CRLF line endings — so editing is non-destructive.
- The parser regression suite walks every script file in EU4 / EU5 / Stellaris / Imperator / HOI4 and their engine-default sibling dirs (22,646 files total) — all parse cleanly.
- See
MODERNIZATION.mdfor the phased plan and decision log.
The "supported versions" column lists every patch level we know
about. New entries get added as Paradox ships patches; older entries
stay so users on a back version can still tell if they're covered.
Completed minor lines are collapsed to <minor>.x; an in-progress
line lists its individual builds instead, and footnotes carve out any
builds within a range that weren't run or can no longer be restored.
| game | supported versions | coverage |
|---|---|---|
| Europa Universalis IV | 1.37.5 | 100% |
| Stellaris | 4.3.x 1 4.4.x 2 |
100% |
| Imperator: Rome | 2.0.5 | 100% |
| Europa Universalis V | 1.0.x 3 1.1.x 4 1.2.x 1.3.0, 1.3.2, 1.3.4, 1.3.6, 1.3.8, 1.3.10 5 |
100% |
| Hearts of Iron IV | 1.18.x.x 6 1.19.0.0 7, 1.19.0.1, 1.19.1.0, 1.19.2.0 |
100% |
| Crusader Kings II | 3.3.5.1 | ~90% 8 |
| Crusader Kings III | — | unknown 9 |
| Victoria 3 | — | unknown 9 |
Every listed version is smoke-validated except where a note says otherwise.
- Within 4.3.x, smoked coverage spans 4.3.5–4.3.7; 4.3.6 itself was not run — a same-day emergency hotfix superseded it with 4.3.7 first.
- Within 4.4.x, smoked coverage is 4.4.1, 4.4.3, 4.4.4, and 4.4.5; 4.4.0 was skipped by Paradox and the 4.4.2 hotfix wasn't run — 4.4.3 superseded it shortly after and the same files parse clean, so those builds are claimed against 4.4.3/4.4.4/4.4.5.
- Within 1.0.x the floor is 1.0.4 (earlier builds can no longer be restored via Steam); 1.0.5–1.0.8 predate the smoke suite and weren't run. Unrun builds are still registered in
BUILD_VERSION_MAPfrom patchnotes — EU5's file-shape defects are stable across the lifecycle, so the same corrections apply. - Within 1.1.x only 1.1.9–1.1.10 are covered; 1.1.0–1.1.8 were an open beta that can no longer be restored.
- 1.3.x is an in-progress beta line, so its builds are listed individually rather than as a completed range: 1.3.0 was the open beta; 1.3.1, 1.3.3, 1.3.5, 1.3.7, and 1.3.9 were skipped by Paradox; and 1.3.2, 1.3.4, 1.3.6, 1.3.8, and 1.3.10 are the smoked beta patches. Every 1.2.0+
BUILD_VERSION_MAPentry keys on the disk-checksum suffix read from the install, since Paradox obfuscates the publicly-displayed checksum from 1.2.0 on (1.3.6 had no official checksum at all — in-game value872e; 1.3.8's is98b8, 1.3.10's isc764). - Smoked 1.18 builds are 1.18.1.0, 1.18.2.0, and 1.18.3.0.
- 1.19.0.0 shipped for ~2 days before the 1.19.0.1 hotfix replaced it and is no longer independently restorable; its corrections are validated against 1.19.0.1 and 1.19.1.0.
- EOL since Sep 2021. Parser-only; the ~10% of files that fail use older pre-Jomini script conventions, not yet triaged. CK2's legacy launcher format means mod selection is also unsupported; only direct parse / round-trip works.
- Placeholder — game module exists, no install validation yet.
The gem isn't published, so build and install it locally:
git clone https://github.com/zgavin/paradoxical.git
cd paradoxical
# Pin to the project's Ruby and Rust. The .tool-versions file is read
# by mise, asdf, rtx, etc.
mise install # or: asdf install
bundle install
bundle exec rake compile # builds the Rust extension via rb_sys
bundle exec rake install # installs the gem into your local gemsetThen in a consuming mod's Gemfile:
gem 'paradoxical'A mod-script that overrides one entry in EU5's auto-modifiers file:
require "paradoxical"
paradoxical! game: "eu5", playset: "Standard", mod: "My Mod"
modifiers = parse_files "in_game/common/auto_modifiers/country.txt"
write "in_game/common/auto_modifiers/~my_overrides.txt" do
lack = modifiers["lack_of_rivals"].dup.reset_whitespace!.single_line!
lack.clear
lack.key = "REPLACE:#{lack.key}"
push! lack
endWhat this does:
paradoxical!resolves the game slug to the matchingParadoxical::EU5module (which carries the steam id, executable, and jomini-version constants), builds aParadoxical::Game, selects the active playset and mod, and pulls the helper methods into scope so the rest of the script can use them directly.parse_filesreadscountry.txtfrom the base game (or whichever earlier mod in the playset overrides it).writeemits a new file under your mod with the modified entry. The~prefix matters — PDS reads files in lexical order, so a~filename takes effect last.
Supported game slugs: eu4, eu5, stellaris, imperator, hoi4,
ck2, ck3, v3. Pass root: and/or user_directory: to
paradoxical! to override the default install / user paths. CK2's
legacy launcher format isn't supported, so passing mod: / playset:
silently no-ops on that game.
If you just want the parser without the mod scaffolding:
require "paradoxical"
doc = Paradoxical::Parser.parse(File.read("foo.txt"))
doc.each do |element|
case element
when Paradoxical::Elements::Property
puts "property: #{element.key} = #{element.value}"
when Paradoxical::Elements::List
puts "list: #{element.key} (#{element.size} children)"
end
end
# Edit and re-serialize. Round-trip is byte-identical for well-formed input.
puts doc.to_pdxbundle exec rspec # unit tests
bundle exec rake compile # rebuild the Rust extension after grammar changes
bin/console # interactive REPL with paradoxical loadedThe parser regression smoke is env-var-gated. Point it at a real game install to walk every parseable file:
PARADOXICAL_PARSE_SMOKE="$HOME/.steam/steam/steamapps/common/Europa Universalis IV" \
bundle exec rspec --tag parse_smokeMIT — see LICENSE.txt.