A new Visual Studio Code extension for Hearts of Iron IV mod development, focused on ease of use, orchestration, and assisting the mod community and mod teams with a modern HOI4 modding toolset with multiple functions and uses.
A comprehensive Visual Studio Code extension for Hearts of Iron IV mod development. This extension provides a complete suite of visual editors, analyzers, and productivity tools for creating professional HOI4 mods.

New in 2.32.0 — HOI4 Git visual editor
The diff viewer and file list in the HOI4 Git panel, rebuilt.
Word-level diff. Changing manpower = 1000 to manpower = 2500 now highlights
1000 against 2500 instead of repainting the whole line. Tokenisation is tuned
for Clausewitz script, so add_core_of={GER} splits on the braces and the operator,
not just on spaces. Lines too dissimilar to pair fall back to plain add/remove —
below that floor, per-word marks are confetti.
Line-level staging. Click a line to select it, shift-click for a range, then
stage, unstage or discard just those lines. Previously the finest grain was a whole
hunk. New stage_lines operation (84 git operations, exposed as git_stage_lines
over MCP).
Side-by-side view, toggled per session, with removed and added lines aligned so
word-diff has something to compare against.
Folded context. Long runs of unchanged lines collapse behind a "⋯ 24 unchanged
lines" row. Runs short enough that folding would not save anything are left alone.
File list. A filter box (matches path, semantic summary, label and object id;
space-separated terms are ANDed) and an optional folder tree with rolled-up
counts — single-child directory chains collapse, so history/states is one row,
not two.
Keyboard. j/k move between files, s stage, u unstage, o open the diff
editor, t tree, v split view, / focus filter, Esc steps back out.
The presentation logic lives in out/git/gitDiffView.js as a dual-load module
(the guiEmit.js pattern): required on the host for tests, inlined into the
webview so both run identical code.
New in 2.36.1 — Two silent-corruption fixes
Both are the shape of the railway bug fixed in 2.36.0: a writer that stayed
syntactically valid while quietly doing the wrong thing, so nothing flagged it.
Erased provinces no longer leave dangling references. map_scale,
map_resize_canvas, map_reproject and map_stretch can erase a province that
shrinks below a pixel, and with allow_erase they removed its row from
definition.csv — and nowhere else. Every state and strategic region went on
listing an id the game could no longer resolve, along with its victory_points
and its per-province buildings block. None of the recommended follow-up tools
(map_fix_contiguity, map_sync_default_map, map_generate_positions) touch
state files, so the mod simply failed to load. All four tools now prune the
province out of every file that names it, and map_delete_provinces picks up the
victory-point and buildings cleanup it was missing too.
Localisation writes are idempotent. A HOI4 loc entry may be written without a
version number (KEY: "text") and with an empty value (KEY:0 ""); the
extension's own reader accepts both. loc_set and loc_bulk_set matched
neither, so they reported success, appended a second definition of the key and
left the line the modder was looking at untouched. They now update in place
whatever shape the entry has, collapse duplicates a previous run created
(duplicates_removed in the result), and report action and changed.
Separately, a value containing $&, $1 or $` was handed to
String.replace as a replacement pattern and spliced the matched text back
into itself, destroying the line; values are now written literally.
loc_generate_missing no longer appends a blank line on every no-op run, and
both writers refuse a file path that escapes the mod root.
New in 2.36.0 — Map reprojection
map_scale takes a single factor and map_resize_canvas takes an offset, so
between them the toolset could change how big the map is and where it sits, but
never the relationship between a pixel and a place. Three tools close that.
map_reproject warps every bitmap layer and every pixel coordinate from one
cylindrical projection to another — Miller, equirectangular, Mercator, Gall
stereographic, central cylindrical, and four cylindrical equal-area variants.
Nearest-neighbour on provinces and indexed layers so colours and palettes
survive, bilinear on the heightmap, each layer at its own resolution. The
projection set is cylindrical on purpose: x stays a function of longitude
alone, so HOI4 keeps wrapping east-west, and the warp stays separable — one
transcendental per row and column rather than one per pixel, which is all seven
layers of a 5632x2048 map in about 345 ms.
map_stretch scales x and y independently, which map_scale cannot express.
preserve_area derives the second factor as the reciprocal of the first, so the
aspect ratio changes at constant pixel count — a uniform 1.25 costs 1.5625x the
area and blows a 1.07 cap, while the same shape change area-preserving costs
nothing.
map_fit_projection works out which projection and extent the map actually
uses, by least-squares fitting control points given as lat/lon plus a pixel or a
province id, and emits the arguments for map_reproject. It is explicit about
its own limits: longitude is linear in every cylindrical projection so it fixes
the extent but never discriminates, and projections within one family differ only
by a constant the fit absorbs, so it reports the family rather than tie-breaking.
Also in 2.36.0: map_generate_railways is now idempotent. It appended a fresh
spanning tree on every run with no deduplication, so repeated runs grew
railways.txt without bound while it stayed syntactically valid — nothing
flagged it. Lines are now deduplicated by province path, in either direction, and
running the tool against an already-duplicated file collapses it and reports how
many copies it removed.
New in 2.35.0 — Scripted GUI editing (element-level, not window-level)
The Scripted GUI creator's toolkit went over MCP in 2.29, but every write was a
whole window: gui_upsert_window replaces the window, gui_project_write takes
the entire model. Moving one button on a forty-element window cost the model
twice over, and the four files a GUI lives in drifted apart by hand. Twelve new
gui_* tools (233 on the Map MCP server in all), all on the same
read → mutate → lint → merge → backup path gui_project_write uses, in
out/guiEditTools.js.
Element-level edits. gui_set_element patches one element — or several
through edits[] — by position, size, sprite, text, parent, any binding, and any
raw .gui key through extra: {frame: 2, shortcut: "ESCAPE", orientation: null}
(null removes a key). set.type on a name that does not exist creates the
element. gui_move_elements moves a selection (names, a glob, or a container's
children) by dx/dy, aligns it to the first element / the window / a named
element, distributes it with an even or fixed gap, or lays it out on a grid.
gui_clone_element duplicates an element with its children count times,
stepping by dx/dy, substituting {i} in the names and in the cloned scripts,
so a row template becomes rows 1..N in one call. gui_delete_element takes the
subtree and the scripted_gui blocks with it. Children follow a moved container.
Wiring without the naming convention. The scripted side is keyed by element
name and suffix — <btn>_click, <el>_visible, <btn>_click_enabled,
properties/<el> — and a misspelt suffix fails silently in game.
gui_set_effect { element, kind: click | right_click | enabled | visible | property } derives the block name, refuses bindings the game never reads (a
click effect on an iconType), refuses unbalanced braces before they break every
GUI in the file, and clears a binding when the script is empty. Without an
element, kind: visible | ai_enabled | dirty sets the window-level blocks.
Consistency across the four files. gui_rename renames an element with its
parent references, entry_container, opens_menu, owned loc keys and every
bound block; or a whole GUI with its window, its scripted_gui entry and
window_name, its <name>_title key (the localisation file moves), the decision
categories that name it, and the .gui file itself — the renamed window keeps
its place in the file rather than being appended. gui_delete_gui removes a GUI
across its files: windows, entries (main and modals), scripted_gui = lines in
decision categories, the generated localisation and the generated .gfx — the
last two only when nothing else uses them, entry templates only when no other
file clones them. It defaults to dry_run: true.
The variables a GUI lives on. gui_variables audits every variable, array
and flag the GUI reads or writes, where each is used, which other mod files write
them, and what that adds up to: a read with no writer anywhere (a button that can
never do anything), a dynamic-list array nobody fills, a temp variable read
outside the block that set it, a flag checked but never set, and whether dirty
names something the rendering side actually reads — with a suggestion when it is
missing.
Vanilla overrides. A mod can only change a vanilla window by shipping the
whole file. gui_override_vanilla copies it in (and can add elements in the same
call); gui_diff_vanilla then reports every override with its added, removed and
modified windows, whether the game's copy is newer than yours after a patch, and
per element every changed field with vanilla → mod values.
Sprites end to end. DDS decoding was pure JS but encoding shelled out to
nvcompress or ImageMagick from a panel. gui_make_sprite writes an uncompressed
A8R8G8B8 DDS — a 128-byte header and raw pixels, which HOI4 loads without any
compressor — from a PNG (pure-JS decoder, all colour types and bit depths), plus
the .gfx entry; several images concatenate into a frame strip with
noOfFrames. gui_sprite_usage reports where one sprite is defined and used, or
audits the mod: sprites nothing references, sprites whose texture file is
missing, and GFX_ names referenced but defined nowhere.
The lint gate is relative. These tools refuse only the errors the edit
introduces, so a hand-written window that already trips warnings can still be
touched, and a refused edit leaves the files untouched. Editing a plain .gui
window no longer mints a scripted_gui entry or a placeholder localisation file
for it.
New suite test_gui_edit.js (106 checks) drives all twelve against a throwaway
mod and a throwaway install: the DDS we write decodes back to the same pixels,
frame = 9 on a four-frame strip is refused, a renamed GUI's category is
repointed, a deleted GUI leaves its untouched neighbours alone, and every file
touched is brace-balanced at the end. Totals: 1,631 checks across 21 suites.
HOI4_MCP_GUI_EDIT_TOOLS=0 hides the family.
"Resize" means three different things on a map and the toolset covered fragments
of each. 12 new tools — 221 total — plus one bug fix that matters on its own.
Fixed: map_paint on the provinces layer had no erase protection; a generous
expand could silently delete a small neighbour, and a definition.csv row with no
pixels crashes HOI4. It now refuses unless allow_erase, like map_create_provinces.
| Tool |
What it does |
map_grow_province |
±N px into named neighbours; refuses to erase, to drop a neighbour below a minimum, or to split one |
map_move_border |
Shift only the shared border between two provinces — the most common reshape |
map_reshape_province |
replace / add / subtract a shape; released pixels go to the nearest neighbour, never orphaned |
map_fill_unknown |
Every pixel whose colour has no row → nearest province |
map_split_state |
N contiguous parts; VPs and province buildings travel with their province; manpower by area |
map_merge_states |
Provinces, VPs, cores united; manpower summed; infrastructure max, other buildings summed |
map_grow_state |
Absorb bordering provinces, longest border first, both sides kept contiguous; toward: "sea" |
map_rebalance_states |
Move border provinces larger→smaller until within tolerance |
map_move_location |
VP, hub (railways re-routed), position anchor, building row, unitstack row, airport, rocket site |
map_relocate_after_edit |
Regenerate anchors for touched provinces; report VPs/hubs/airports that no longer make sense |
map_resize_canvas |
Extend or crop the map; every layer at its own ratio, every coordinate in four text files shifted |
map_scale |
Nearest-neighbour on provinces and indexed layers, bilinear on the heightmap; refuses if a province would vanish |
Every pixel tool works on a copy of the province-id-per-pixel array, audits it
(erase / minimum / contiguity) and commits one paint per province — so the
checks are integer counting before anything touches the raster.
Oceans were the least-covered part of the map: nothing read or wrote
default.map's sea_starts/lakes, nothing placed ports, nothing knew a lake
from an inland sea, and every sea region got the same flat naval_terrain = ocean.
The naval AI plans per strategic region and lands only where a port exists, so
those gaps are the difference between a navy that fights and one that idles.
9 new tools — 209 total.
| Tool |
What it does |
map_ocean_check |
One pass over every sea-specific fault: sea above sea level, default.map out of step (crash), sea in no region, regions mixing land and sea, missing naval_terrain, enclosed seas, ports with no adjacent sea (crash), coastal landmasses with no port (AI never lands), straits through land, land terrain under water |
map_sync_default_map |
sea_starts and lakes regenerated from definition.csv; nothing else touched |
map_place_ports |
A naval_base per coastal state on the province with the longest sea border facing the largest sea region; every island guaranteed one |
map_auto_naval_terrain |
shallow_sea / ocean / deep_ocean from depth under the heightmap, archipelago when most of a region touches land |
map_shape_seabed |
Continental shelf under sea and lakes — shallow at the coast, ramping to the floor over shelf_width px; land never touched |
map_tile_ocean |
A from-image map arrives with one province for the whole ocean; cut it on a hex or square grid, slivers merged, tiles joining the original's region |
map_make_lakes |
Enclosed seas → lakes, with definition.csv, default.map, regions and any adjacency touching them kept in step |
map_add_canal |
The adjacencies row plus the adjacency_rules.txt block (contested/enemy/friend/neutral) nothing wrote before |
map_naval_reach |
Sea connectivity including straits and canals: components, ports per sea, landlocked seas, and from→to reachability with hop count |
map_ocean_check knows the difference between a strait (sea row, no rule,
crosses sea) and a canal (sea row with a rule, crosses land) and checks each
against its own rule.
The raster tools could mint a province. What they left behind was a province with
guessed definition.csv columns, no strait, no positions.txt entry, no railway and no
victory point — each a hand-edit or a script. 12 new MCP tools close that loop
and add the structural operations that make a map editable rather than merely
paintable. 200 tools total.
| Tool |
What it does |
map_autoflag_coastal |
Recompute coastal from the raster — land touching sea, sea touching land |
map_auto_continent |
Continents from land connectivity; islands inherit across the sea; sea → 0; appends continent.txt |
map_generate_adjacencies |
Detect straits (land–sea–land within N px, line crosses only sea) → sea rows with through |
map_generate_positions |
positions.txt from centroids + heightmap — the last layer with no generator |
map_generate_world_normal |
Sobel normal map from heightmap.bmp at half resolution; no more Photoshop step |
map_split_province |
k-means on pixels or explicit shapes; original keeps the largest part, new ids join its state and region |
map_merge_provinces |
N-into-1: repaint, drop rows, fix states/regions, move victory points, rewrite railways/supply/adjacencies |
map_fix_contiguity |
Find multi-blob provinces (units can't path) and give strays to the neighbour they touch most |
map_auto_states / map_auto_regions |
Cluster unassigned provinces into contiguous states/regions by size target |
map_generate_railways |
Capital per state → shortest land path between bordering capitals → minimum spanning tree |
map_suggest_vps |
Most central province per state; apply writes VPs where none exist |
Everything runs off one raster pass (out/mapAnalysis.js: province graph with
border lengths, connected components, blobs, k-means, Dijkstra, Sobel, strait
detection, region growing) — no MapDataLoader, so a tool that needs adjacency
does not pay for river decoding. Every mutating tool backs up first (bitmaps
included where pixels change), supports dry_run, and answers with counts and
capped samples rather than dumps. map_fix_contiguity defaults to dry run.
New in 2.31.0 — Structured script editing (no more throwaway scripts)
common/ and history/ had surgical writers for focuses, events, GUI windows and
localisation — and regex for everything else. Ideas, decisions, characters, units,
technologies, scripted effects and country history had no structured writer at all,
so changing them in bulk meant a throwaway Python script.
8 new MCP tools — script_get_block, script_set_value, script_upsert_block,
script_set_block, script_delete_block, script_edit_list, script_bulk_edit,
script_create_file — bringing the total to 187.
Blocks are addressed by structure, not by line number:
ideas/country/my_idea nest by key
focus_tree/focus[id=POL_army] the block whose child id is POL_army
country_event/option[1] the second option (0-based)
ideas/country/* every sibling — drives the bulk operations
script_bulk_edit is the point. One call carries a glob and a list of
operations, and answers with a summary instead of file contents:
script_bulk_edit {
"files": "common/ideas/**/*.txt",
"ops": [{ "op": "set_value_all", "selector": "ideas/country/*",
"key": "removal_cost", "value": "-1" }]
}
That replaces a read-edit-verify loop whose cost scales with the size of the mod.
It defaults to dry_run: true, like script_replace.
Formatting is preserved, because a writer that quietly reformats is worse than
the regex it replaces. Every edit is a substring splice against the original
text: bytes outside the target are never rewritten, so comments, alignment,
tabs-vs-spaces, CRLF and compact-vs-multiline brace style all survive. Inserted
lines are indented to match the siblings they join. Deletions take the whole line
only when the node owns it.
Safety. Every write is backed up first and brace-checked before it reaches
disk — unbalanced braces are the one error that takes a whole file with it, since
HOI4 swallows every following block into the unclosed one and reports the failure
somewhere unrelated. A batch is atomic per file: if any operation fails, that file
is left untouched. Ambiguous selectors are rejected rather than silently resolved
to the first match.
Under the hood: parseClausewitz now carries absolute character spans
(start, end, endLine, bodyStart, bodyEnd) on every node. Purely additive —
nothing that consumed the tree before sees a difference — but it is what makes an
edit a splice instead of a re-serialisation.
The map MCP could always do the bookkeeping — definition.csv rows, province groups,
victory points — but nothing could put a pixel on provinces.bmp. Structural map
edits therefore needed an out-of-band Python script. They no longer do.
11 new MCP tools (map_raster_info, map_describe_shape, map_paint,
map_create_provinces, map_create_province_chain, map_delete_provinces,
map_generate_unitstacks, map_generate_buildings, map_edit_adjacencies,
map_backup_full, map_restore_full) bring the pixel layer under the same tool
surface as everything else — 179 tools total.
Geometry is described, not enumerated. A 40-province border wall is one path
and two numbers:
map_create_province_chain {
path: [[1200,430],[1240,505],[1310,560]],
width: 3, segment_length: 40,
terrain: "urban", state_id: 412, region_id: 31
}
Shapes compose (union / intersect / subtract) and take expand, shrink and
outline modifiers, and they can name existing map features — so "a two-pixel wall
around state 7" is {kind:"state", id:7, outline:2} with no coordinates at all.
{kind:"flood"}, {kind:"color"} and {kind:"province"} cover the rest.
All seven bitmap layers are writable: provinces and world_normal (24-bit),
terrain, cities, rivers, trees and heightmap (8-bit indexed, palettes
preserved byte-for-byte). Masks rescale automatically between layers stored at
different resolutions, so "paint trees over province 42" hits the right pixels.
Safety. Every mutating tool snapshots the bitmaps and the text files first
(map_backup_full / map_restore_full); the old map_create_backup covered text
only, which cannot undo a paint. Painting refuses by default to reduce an existing
province to zero pixels — a definition.csv row with no pixels crashes HOI4 — and
reports exactly which provinces lost area. map_describe_shape and every tool's
dry_run give a free preview before anything is written.
Also fixed: the image-to-map generator wrote map/adjacency.csv; HOI4 and the
rest of this extension read map/adjacencies.csv, so every generated strait and
impassable crossing was being silently ignored.
- Semantic cache: per-file semantic diffs and id harvests are cached by HEAD sha + mtime/size, so a refresh after saving one file re-parses only that file (1,800 changed files: cold 226 ms → warm 82 ms, single edit 56 ms).
- One git process for all "before" blobs (
git cat-file --batch) instead of one git show per changed file.
- Persistent mod index (
out/git/hoi4Index.js): incremental (mtime/size) index of every id each script file defines and references, stored under VS Code's global storage — never inside the mod. Powers the new Context files toggle on the File Graph (unchanged files that a change references / is referenced by, drawn hollow), harvest reuse, and the git_index_search / git_index_refresh / git_perf_stats MCP tools.
- Frontend: spatial-grid repulsion (O(N) per frame instead of O(N²)) with early stop, node/edge lookup maps, view culling, label thinning when zoomed out, debounced search over precomputed keys, table capped at 400 rows with "show more", no re-layout when the graph key is unchanged, positions / pan / zoom / active tab persisted across panel reloads, a loading overlay and a timings/cache readout in the graph header.
New in 2.29.1
- Fixed
command 'workbench.view.extension.hoi4GitRevert' not found when opening the Git Revert panel: the hoi4GitRevert view container and its views (mainView, quickActions, recentCommits) are now declared in package.json.
New in 2.29.0 — HOI4 Git (GitHub Desktop replacement)
A full source-control workbench for mod teams, opened with HOI4 Git: Open (Ctrl+Alt+G, the $(source-control) HOI4 Git status bar item, or HOI4 Tools → Git & Release). Design of record: docs/GIT_PLAN.md.
- Tier 1 — Desktop parity: Changes (stage / unstage / discard per file and per hunk, semantic summaries per file), commit box with amend / sign-off / co-authors and a Suggest button that writes the message from the HOI4 semantic diff, History with a lane graph, commit details and per-file diffs, Branches (create / rename / delete / checkout-with-stash / merge / rebase / compare), stashes, tags, remotes, fetch / pull / push, clone,
.gitignore template.
- Tier 2 — beyond Desktop: GitHub sign-in through VS Code (PR list / create / checkout / merge / comment, issues, check runs, Actions runs), interactive rebase editor (drag to reorder, pick / reword / squash / fixup / drop), reflog Undo, bisect stepper, worktrees, submodules, Git LFS tracking for
.dds/.tga/.ogg…, publish repository.
- Tier 3 — HOI4-specific: semantic diffs for states, focus trees, events, decisions, ideas, country history, localisation, sprites and images (
owner GER → POL, +focus POL_c, moved (1,1)→(3,1), ~key: "A" → "B", TGA origin warnings); File Graph tab in the style of the Dependency Explorer (files as nodes, HOI4 references as edges, feature clusters, stage a cluster / neighbourhood); validation gate before commit (braces, BOM, conflict markers, missing loc keys, unknown focus prerequisites, state sanity, upside-down flags); structural 3-way merge of Clausewitz files by block (installable as a git merge driver); "Blame mod object" for a state / focus / event / loc key; one-click release (descriptor version bump → commit → tag → zip → optional GitHub release + push).
- MCP parity: every panel operation is also an MCP tool (
git_status, git_commit, git_semantic_diff, git_file_graph, git_structural_merge, git_pr_create, git_release, … 80 tools) generated from the same operation table, so AI agents get exactly what the UI gets. Disable with HOI4_MCP_GIT_TOOLS=0.
- Tests:
test_git_tools.js (110 checks, temporary repositories only) and test_git_panel.js (28 checks, webview render pass).
New in 2.28.1
Two bug fixes reported by users:
- State History Editor (Shift+Click mass edit) changes now show up in game. "Set owner" / "Add core" / claims were always written as a dated
1936.1.1 = { ... } sub-block. The game only applies a dated block once a bookmark reaches that date, so mods with an earlier start date saw the change in the Map Editor but not in game. The editor now reads your mod's start date from common/bookmarks (the Target Date defaults to it), and any date on or before it is written straight into the history root (owner = TAG, add_core_of = TAG, ...). Later dates still become dated blocks, and the status message tells you which happened. Existing cores/claims are merged, removed tags are dropped, dated sub-blocks are left alone, and id =/history inside comments no longer confuse the file scan.
- Country Wizard flags are no longer upside down. Generated
.tga flags used a top-left-origin header; HOI4 ignores that bit and reads flags bottom-up like the vanilla files, so every generated flag rendered flipped. Flags (and solid-colour placeholders) are now written bottom-up with the same descriptor vanilla uses (0x08).
New in 2.28.0
The Scripted GUI creator's toolkit is now available over MCP, so an AI client
builds scripted GUIs with the same tools a human uses in the editor. Thirteen
new gui_* tools and four upgraded ones on the Map MCP server (85 tools in
all), all running the editor's own code: out/guiProject.js is the creator's
host side (parse → model → emit → merge → plan/backup, sprite/loc/source
indexes over the mod and the install, decision wiring, the decisions-panel and
parent-window renders) made vscode-free, and the panel now delegates to it;
out/guiLint.js is the editor's Check button as a dual-load module, so the
editor and gui_lint report identical problems in identical words.
gui_reference - start here: the model schema field by field, context
types with the scope each runs in, parent window tokens, fonts (mod
bitmapfonts included), the per-type property catalogue for extra, common
sprites, built-in templates, the decision-category layout numbers and the
recommended workflow.
gui_project_read - a window the way Import reads it: the .gui and the
scripted_gui entry merged into one model, click_effect /
enabled_trigger / visible_trigger / property on the elements that own
them, dynamic lists on their gridbox, every unmodelled key verbatim in
extra. Reads vanilla files as a read-only fallback. Runs lint on the way out.
gui_project_write - the four files through the editor's emitter and
surgical merges, decision category wired, .bak of every overwritten file,
dry_run for the plan and diffs, and a lint gate: it refuses to write while
there are errors unless force: true.
gui_lint - duplicate and invalid names, missing parents, boxes outside the
window, sprites unknown to mod and vanilla, gridbox slot and entry-container
problems, nested entry templates, frame past the strip, brace balance and
unknown effect names, effects in the wrong scope for the context, loc keys
that exist nowhere, orphan scripted_gui entries, window_name already
claimed, and the decision-category rules (502px row, no parent token,
category wiring).
gui_render_preview now takes a model (preview exactly what you are about
to write), window_name selects a modal or entry template of the model,
highlight outlines named elements, text resolves through localisation and
vanilla files render with a percentage base.
gui_render_decision_preview, gui_render_parent_window, gui_sprite_info
(where a sprite is defined, texture, size, frames, rendered on request),
gui_list_sources, gui_list_decision_categories,
gui_wire_decision_category, and gui_project_save / _list / _load -
.guiproj files the editor's Load Project opens, so the AI and the human can
hand a design back and forth.
The model accepts the friendlier shape an AI writes (elements, click_effect,
is_modal, context_type, decision_category: "MOD_cat", extra: {frame: 2})
and fills the defaults the editor would. entry_name carries a scripted_gui
entry named differently from its window - hand-written GUIs do this all the
time, and without it a round trip renamed the entry and repointed the category.
Doing this properly turned up editor bugs, all fixed: Import of a multi-window
file treated every other window as an element of the first, so the next export
nested them inside it - windows now come back as the modals and entry
templates they are (found through the scripted_gui entries), and
write(read(x)) is clean; the previewer's Import this file failed on a vanilla
file with "not found" because it looked in the mod only; every export left an
empty .gfx behind when there was nothing new to define; a translated
<gui>_title was overwritten by the generated placeholder on re-export; the
Check button flagged always = yes as an unknown trigger; corneredTileSpriteType
and textSpriteType entries were never indexed because their bodies hold nested
braces - which is why no window background ever resolved in a preview; and Save
Project dropped the scripted_gui side, so a reloaded project came back as a
plain player_context window with its effects gone.
The server finds the game on its own: HOI4_DIR, then the workspace's
hoi4.gamePath, then the usual Steam locations (HOI4_DIR=none switches vanilla
off), and the extension passes its hoi4.gamePath to the server it starts. On
the Half-Life mod and a stock install: 569 .gui files listed (116 mod, 453
vanilla, 3.4s once, then cached), 743 decision categories, a decision preview in
under 100ms, gui_reference { topic: "all" } is 28KB.
New suite test_gui_project.js (122 checks) drives every tool against a
throwaway mod plus a throwaway install and asserts the editor's Import and the
server's gui_project_read build the same model, and that the editor's export
plan and gui_project_write agree file for file. Totals: 710 checks across 11
suites.
New in 2.27.0
Scripted GUIs can be built into a decision category instead of as a window of
their own. A HOI4 decision category can host one: the category gets
scripted_gui = <name>, the scripted GUI entry gets
context_type = decision_category, and the game draws the window inside that
category's row in the decisions panel, between the category description and its
decision buttons. Seventeen vanilla categories do this.
The Scripted GUI section of the properties pane now has a Decision Category
picker, listing the categories in your mod first and the vanilla ones after,
each annotated with the GUI it already hosts if it has one. Set up switches
the context to decision_category, drops the parent window token (it is not
used for this context), turns off moveable and sizes the canvas to 502x210 -
502 being the real category content width, which comes out of
countrydecisionview.gui: the decision grid container sits at (5,45) with the
grid itself at x=9 inside it.
Export then writes the other half of the wiring. If the chosen category lives in
your mod, the export plan gains its file and the diff shows the scripted_gui
line being added, or repointed if the category already named a different GUI.
Wiring the same category twice changes nothing, a commented-out scripted_gui
is correctly not treated as one, and a category that only exists in the vanilla
install is listed as skipped with a note telling you to copy it into your mod -
export does not silently write into the game directory.
Decision GUI preview. The In panel button draws the decisions panel with
your window sitting exactly where the game will put it. Every band is a real
vanilla window rendered with real sprites - the panel frame, the category header
row, the category description, then your GUI outlined in magenta, then two of the
category's decision buttons underneath - so the widths, the chrome and the
vertical rhythm are the game's rather than an approximation. If your window is
wider than the 502px category row, the preview says how much the game will clip.
Any .gui file can be previewed. The new Preview .gui button opens a
browser over every interface file in the mod and in the vanilla install - 906
files on a stock install - listing each file's windows and rendering the one you
pick. Sprites resolve through the mod's .gfx first and vanilla's second, the
way the game resolves them, and any sprite that cannot be found is named rather
than silently drawn blank. From there, Import this file takes it into the
editor.
Two things had to be fixed for those renders to be worth looking at. Positions
written with a file-scope @CONSTANT - which 32 of the 130 vanilla interface
files use - parsed as nothing at all, so every element in those windows landed at
0,0 on top of each other; constants are now resolved before anything reads a
number out of a window. And size = { width = 100% }, which every
decision-category GUI uses, parsed as 100 pixels; a percentage now resolves
against whatever contains the window, which the previewer lets you set.
Text is resolved through localisation as well - localisation/english in the mod
first, then the vanilla install, matching on HOI4's _l_english.yml naming so a
mod's other languages cannot win - with colour codes and icon tokens stripped.
Runtime tokens like [GetLoyaltyStatus] stay as they are, since that is a string
the game assembles when it draws.
test_gui_fidelity.js grew from 151 to 181 checks, covering the category wiring
in all five of its cases, the previewer's file and window listing, percentage and
@constant resolution, localisation lookup, and that a vanilla category never
reaches the export payload.
New in 2.26.0
The Scripted GUI creator picks up the editor behaviour it was missing.
Six things were quietly broken. Picking any placement tool ran
querySelectorAll('.toolbar .tool-btn') and cleared active on every button
in the toolbar, so Grid, Snap, Links and Preview all went dark while still being
on; the tool buttons carry a data-tool attribute now and setTool only touches
those. Resizing never took an undo snapshot, so it could not be undone. Every
property edit rebuilt the properties pane, which destroyed the field you had
just tabbed into - a plain value edit now repaints everything except the panel,
and only a change that alters which fields exist (type, sprite, parent, the
sub-menu wiring) rebuilds it. Pasting twice produced two elements called
x_copy, and the Check panel then reported a duplicate name you did not make.
Ctrl+A selected across every window tab including modals you cannot see, so the
next Align silently moved them. Undo cleared the selection.
Two more came out of the same pass: dragging at anything other than 100% zoom
moved elements by the wrong distance, because the pointer delta was never
divided by the zoom factor; and duplicating a panel together with its children
left the copies parented to the original panel. A duplicated group now
rewrites parent, opensMenu and entryContainer among its own copies, and
renaming a container follows through to everything that names it.
Selection. Drag on empty canvas for a marquee, Alt+click to walk down
through stacked elements instead of always getting the topmost one, and a lock
and a hide toggle on every layer row. Both are editor-only - a hidden element
still exports - and locked elements stay out of canvas clicks, drags and nudges
while remaining selectable from the layers list.
Layers were one flat row per element with a (parent) suffix, which on a
300-element import is a wall of text. It is a real tree now: children nested
under their parent, collapsible, with a filter box that keeps matches plus the
ancestors needed to reach them, drag to reparent or reorder (drop on the middle
of a container to go inside it, on the top or bottom edge to sit before or after
it), double-click to rename in place, and the selected row scrolled into view.
Self-parented and cyclic containers - both of which exist in vanilla - get a row
rather than hanging the walk.
Alignment guides. Grid snap was the only thing holding a layout together:
lining a button up with the one above it meant selecting both and pressing an
Align button. A dragged or resized box now snaps its own edges and centres to the
edges and centres of everything else in the window, and to the window itself,
with magenta lines showing what it locked on to and a live x, y w x h readout
following the box. When there is no edge in reach it looks for the position that
leaves the same gap to the nearest neighbour on either side. The toolbar's new
Guides button turns the whole thing off.
Multi-select used to say "3 elements selected" and stop. The fields the
selection has in common are editable now - position, size, parent, sprite, font -
with a field whose values differ showing blank and a "mixed" placeholder, writing
only when you actually type something, plus Lock / Unlock / Hide / Show for the
whole selection.
Dragging got cheap. Moving a box went through render(), which cleared the
canvas and rebuilt every element, the layers list and the properties pane on
every mouse move. Only the boxes that actually moved are touched now, plus the
wiring that follows them; one full rebuild happens on mouse up. The undo snapshot
is taken on the first real movement, so a click that never moved no longer eats a
slot in a 50-deep stack.
Switching the Map Editor's View or Colors is about twice as fast, and no
longer redraws things the dropdowns cannot affect. Both handlers used to call
renderMap(), renderOverlay() and renderLabels(); the overlay is railways,
supply nodes, bases and rivers drawn from their own data and their own toggles,
so neither dropdown can change it.
The colour pass itself was doing per-province work that belongs to the render.
manpower recomputed Math.max(...mapData.states.map(...)) - a spread over every
state - once for each of 13,041 provinces. warnings rescanned the entire warning
list per province. statecategory built its colour table per province. Those are
computed once per render now. The province fill writes each cover-zone run as one
memset through a 32-bit view of the ImageData instead of four byte writes per
pixel, and the hashed state/region palette is memoised per id rather than running
hslToRgb per province. On a synthetic vanilla-sized map (5632x2048, 13,041
provinces, 900 states) the mean Colors switch goes from 126ms to 74ms of render
work - manpower 244ms to 85ms, state ID 168ms to 116ms, province ID 47ms to 26ms -
on top of dropping the overlay repaint.
One thing to know: viewMode, the variable behind the View dropdown, is written
by its change handler and read nowhere in the extension. Changing View has never
done anything except trigger three full repaints. It is now instant instead of
slow, but it is still inert - worth deciding what it should select on.
test_gui_fidelity.js grew from 116 to 151 checks and test_map_editor_load.js
from 29 to 35, covering the marquee, the guide maths, the layers tree and its
drops, multi-select editing, the rename cascade, undo bookkeeping, and that a
Colors change leaves the overlay alone.
New in 2.25.0
The Scripted GUI creator draws the wiring between elements. The canvas is
flat - every element is absolutely positioned, so a button and the sub-menu it
opens looked like two unrelated boxes and the only way to tell they were
connected was to read the generated script. Relationships are now drawn on the
canvas as dashed and hatched connectors: amber long-dash for "opens this
sub-menu / modal" with the arrowhead landing on the target's border, red
short-dash for the close button that clears the menu flag, pink hatched rungs
for the entry template a gridbox stamps, and blue dotted for plain containment.
A legend in the sidebar names every kind that is currently on screen.
Modals and entry templates live in their own window tabs, so their connectors
cannot be drawn in place. Those edges finish in a small badge in the edge's own
colour and dash pattern - click it to jump to that tab. A target that no longer
exists gets a red ? badge instead, which is how a dangling opensMenu now
shows up on the canvas and not only in the validation list.
Containment is dense enough to bury the wiring underneath it, so it follows the
selection by default; the toolbar's Links button cycles off / wiring only /
Links+ (every parent-child edge). Selecting a single button or checkbox puts
an amber dot on its right edge: drag it onto a container to make that container
the button's sub-menu, or onto empty canvas to create the panel and its close
button right where you dropped it. Dropping on anything else cancels rather than
dropping a new panel on top of it. The properties pane and the layers list now
show the other direction too - a container that is the target of an opener says
so, with a Go button.
The Map Editor opens in about half the time. On a vanilla-sized map
(5632x2048, 13,041 provinces, 900 states) the panel used to hand the webview
56.6MB of JSON. Most of it was geometry as individual objects: 461,611
cover-zone runs as {x,y,w,h} and 1,412,218 sampled border pixels as {x,y},
each of which had to be stringified in the extension host, copied over the
webview channel, parsed, and allocated as a heap object in the renderer. The
message handler then ran JSON.stringify(msg.data) on every payload so it could
log the first 100 characters, which cost more than parsing all of it did.
Geometry now travels as base64'd typed arrays - cover zones as packed 16-bit
x,y,w,h, border pixels as one packed y * width + x each - and the webview
takes zero-copy subarray views of one buffer per chunk instead of building
1.9M objects. Rivers went the same way (175,004 pixels, 9MB of {x,y,type,r,g,b}
down to 1.2MB), and so did the heightmap. End to end - load, serialise,
transfer, parse, first paint - that is 2.0s down to 0.9s, 56.6MB down to 19.7MB,
and peak memory 549MB down to 405MB.
The loader itself got faster too. provinces.bmp was scanned twice, once for
cover zones and once for the adjacency graph, each decoding all 11.5M pixels;
the two now share one pass and one three-row window, and interior pixels (the
large majority) drop out of the adjacency work in a single comparison. A
vanilla history/states holds ~900 files that were read one await at a time,
which on Windows is mostly per-file latency - they are read in batches now.
Together the loader is ~630ms down to ~450ms.
Three smaller things on the same path: the five overlay layers arrive as five
separate messages during a load and each one used to force a full overlay
repaint before loadComplete painted it again; the render ImageData (46MB on a
full-size map) is allocated once instead of on every view-mode change, and
cleared through a 32-bit view; and the panel only re-sends its data when the
webview was actually torn down, not every time the tab gains or loses focus.
Drawing rivers no longer builds a string key per pixel and eight more per
neighbour lookup - it uses an occupancy grid.
The GUI editor's state-silhouette renderer loads the same map data, and only
needs province shapes, so it now skips the adjacency graph, rivers and the
heightmap entirely.
test_map_editor_load.js covers the new wire format: cover zones and border
pixels have to survive the round trip byte for byte, and renderMap has to
produce a pixel-identical image to one built from the loader's own objects.
New in 2.24.0
The Scripted GUI creator no longer loses your file. Importing a .gui and
exporting it back used to be lossy, and often impossible. Run 2.23.0 over the
441 vanilla interface files and 255 of them - 58% - crash the exporter outright
with a stack overflow: vanilla reuses container names, and a few files parent a
container to itself, which sent the recursive generator into an infinite loop.
Of the 186 files that did survive, 16 came back unchanged; 27,533 of their
140,623 properties were dropped (19.6%). Over a 115-file mod: 3 crashed, none
round-tripped clean, 29,110 of 143,519 properties lost (20.3%).
The losses were not cosmetic. quadTextureSprite was rewritten as spriteType
on 3,171 vanilla and 3,698 mod elements, throwing away their hover, pressed and
disabled frames. orientation was stripped from 4,251 elements, moving them to
a different corner of their parent. alwaystransparent went from 10,400
elements, making click-through overlays start swallowing clicks. frame,
bordersize, clipping, scale and pdx_tooltip all disappeared, and ten
element types the canvas cannot draw (scrollbarType, dropDownBoxType,
lineChartType and the rest) vanished with everything inside them.
The parser and the emitter were rebuilt around a "known keys plus verbatim tail"
model: anything the editor does not model is carried on the element and written
back untouched, block types it cannot represent are captured whole, and every
element is emitted exactly once so duplicate and self-parented names cannot
recurse. Across the same 441 vanilla files nothing crashes, 439 round-trip with
nothing changed at all, and 4 properties out of 204,077 differ. Across the
115-file mod: nothing crashes, every file round-trips, zero properties lost.
Also preserved now: file-scope @CONSTANTS (dropping them broke every @NAME
reference below), min = { width = 100% } and preserve_aspect_ratio inside a
size block, whichever of x/y or width/height the file used, background
blocks that only name a sprite, an explicitly empty pdx_tooltip = "", and
file-scope properties like core.gfx's default_clicksound. Duplicate and
self-parented container names - both of which exist in vanilla - used to send
the exporter into infinite recursion; each element is now emitted exactly once.
One emitter, two runtimes. The code generator lives in out/guiEmit.js and
is both required by the extension host and inlined into the webview, so the
preview pane is byte-for-byte what export writes. A test asserts the two agree.
Export shows you the diff first. Export now opens a preview listing every
file it would touch, with a line-by-line diff and what the merge decided
("updated army_window", "added GFX_panel_bg"). Nothing is written until you
confirm, and every file that already existed is copied to
.vscode/gui_backups/ before being overwritten (ten generations kept).
Localisation and .gfx are merged, not overwritten. Both used to be
rewritten wholesale. Localisation is now merged key by key: hand-written
translations survive, unrelated keys and comments survive, and a real edit in
the editor still propagates - but the placeholder the generator writes when it
has nothing better to say can no longer clobber a string you typed. .gfx is
merged per sprite definition, so hand-written entries and types the tool never
generates (frameAnimatedSpriteType, corneredTileSpriteType) stay put.
Sprites already defined in another .gfx are referenced rather than redefined,
which stops the duplicate-spriteType warnings in error.log.
Every property is now editable. Selecting an element shows a More
Properties panel: the keys imported from the file, editable, plus an
Add property picker offering the properties vanilla actually uses for that
element type, each with a one-line explanation of what it does. Buttons, icons
and checkboxes gained an Export explicit size toggle - previously the canvas
box was purely decorative for those types, because the exporter never wrote a
size at all. Block types the canvas cannot draw appear on the canvas and in
the layer list, marked read-only, showing the source that will be written back.
Validation goes deeper. Check now catches: a frame = N past the end of
the sprite's noOfFrames; a dynamic list whose entry container is nested inside
another container instead of sitting at guiTypes level; localisation keys that
exist nowhere in the mod or the configured game install; effects, triggers
and properties entries left over from an import that match no element; a
window_name already claimed by a different scripted_gui entry elsewhere in
the mod; and effects whose scope contradicts the window's context_type -
add_political_power in a selected_state_context does nothing at all in game,
and nothing else tells you.
New in 2.23.0
Animation viewer. Models now carry an Animation dropdown listing every
.anim that belongs to them - HOI4 names them <model>_<state>.anim, and each
one is annotated with the name declared for it in the folder's .asset files
(useful, because the two often differ: cavalry_horse_walk.anim is declared as
cavalry_horse_moving_animation). Pick one and the model is re-rendered with
that clip applied.
Playback at the animation's real speed. The renderer reads the clip's own
fps and frame count out of the .anim, samples frames evenly across the true
range, and the viewer plays them back over the clip's actual duration - a
48-frame clip at 30fps plays over 1.6 seconds, not at some arbitrary rate.
A scrubber steps through frames by hand, showing the real game frame number
and timestamp, with 0.25x-2x speed control.
Opening a .anim directly works. .anim files are registered with the
preview editor too: opening one finds its companion model automatically
(walking the filename back a segment at a time, so
cavalry_horse_idle_forward.anim still resolves to cavalry_horse.mesh) and
opens with that clip selected. If no model can be found, the panel explains
the naming convention rather than failing silently.
Applying an animation to an unrigged model now reports "this mesh has no
skeleton" instead of quietly rendering a static frame.
Sidebar entries. Both 3D Model Preview (.mesh) and 3D Animation
Viewer (.anim) are listed in the HOI4 Tools sidebar under Analysis, and both
have Explorer right-click entries scoped to their file type.
model_list_animations MCP tool returns a model's clips with their
declared names and paths, and model_render_mesh_preview now reports the
clip's fps, frame range and which frames it sampled.
New in 2.22.0
3D model previews inside VS Code. Clicking any .mesh file in the
explorer now opens a preview: the extension drives Blender (via the
io_pdx_mesh addon) to import the model, frames it, and renders a turntable
you can drag to spin, scroll to step, or let auto-rotate. Because the model is
imported once and only the camera orbits, extra frames are nearly free -
16 frames of a real unit model render in about 2 seconds.
Beside the model it reports what the file actually contains: objects,
triangle counts, materials, bones, and every .dds texture it references -
with textures that are missing from disk flagged in red, and the ones present
clickable to reveal them.
Proxy meshes are hidden automatically. HOI4 unit models often ship a
small untextured box (collision/shadow proxy) alongside the real geometry,
which otherwise sits in front of the model and hides it. Untextured
box-shaped objects are detected and hidden by default, with a toolbar toggle
and a "proxy - hidden" badge so nothing is silently dropped.
Animation previews. Point the preview at a .anim file and it imports
the skeleton, applies the animation and renders frames through it - so a rig
can be checked without launching the game.
Two ways to reach Blender. By default a background Blender is spawned per
render (no Blender window needed). If your Blender is already open with the
pdx_hoi4_mcp_bridge addon running, the preview uses that session instead -
no startup cost. Controlled by hoi4.meshPreview.mode (auto / headless /
bridge).
Blender 4.x compatibility. io_pdx_mesh's material-join path calls
bpy.ops.object.join(ctx) with a positional context dict, which Blender 4.x
rejects - so any multi-material mesh failed to import. The preview skips the
join (every object is rendered regardless), which fixed every multi-material
model tested. Sampled results: 50/50 vanilla meshes and 39/40 mod meshes
render; the remaining one is a malformed file the addon's own parser rejects.
Everything is cached by the mesh's own mtime and size plus the render
settings, so reopening a preview is instant (about 1ms against ~2s cold);
the cache evicts oldest-first and can be cleared from the command palette.
model_render_mesh_preview MCP tool. The same renderer over MCP, so the
AI can see a model - and the model MCP server now returns real image
content instead of a wall of base64, matching the map server.
New in 2.21.0
Focus trees over MCP. Six new tools give the AI the same reach into
national focus trees the editor has: focus_tree_list, focus_tree_read
(structured focuses with computed ABSOLUTE grid positions, following
relative_position_id chains), focus_tree_validate (duplicate ids, cyclic
or unknown relative positions, dangling prerequisite/mutex refs, overlapping
positions, prerequisites that sit below their dependents, missing loc),
focus_tree_upsert_focus / focus_tree_delete_focus (surgical single-block
writes with automatic backups; delete reports what still references the
focus), and focus_tree_render - the tree drawn as a PNG with real focus
icons, prerequisite connectors, dashed OR-groups and red mutually-exclusive
links. Reading and rendering fall back to the vanilla install, so you can
inspect Paradox's trees too.
The error.log bridge. game_log_analyze closes the loop between editing
and the running game: it parses the HOI4 error log, dedupes repeats with
counts, classifies each entry (script / localisation / gfx / model / event /
focus / map), extracts file:line references and flags the ones inside your
mod. On the test mod's real log: 2505 lines to 1791 unique problems, 463 of
them pointing at exact lines in mod files.
Events you can see. event_list, event_read, event_upsert (surgical,
auto-declares add_namespace) and event_render - which composes the event
window the way HOI4 shows it: title bar, event picture from the real sprite,
localized description and option buttons.
Map shapes over MCP. map_render_shape exposes the whole 2.9-2.20
importer: state, continent, country (with include_cores for formables) or
a custom union of states, rendered whole or divided into per-state
clickable pieces with a composite preview and manifest.
Safety and refactoring. map_list_backups / map_restore_backup make
every automatic backup restorable over MCP (and the restore itself is backed
up first), script_replace does regex refactors across the mod with a
dry-run preview by default and backups on apply, and loc_generate_missing
turns validation findings into written stub keys.
Border fix. Divided maps had a sub-pixel flaw: a piece whose width was
not a multiple of the shared scale factor clamped its final sample back
inside its own bounding box, so it and its neighbour both painted one shared
column. Pieces now leave out-of-box samples empty and tile exactly (verified
by decode-and-recompose on 134- and 251-piece country maps: identical pixel
counts, zero overlap).
New in 2.20.0
Formable nations. Countries mode gained "Include cored states": tags
that own nothing at game start but hold cores (releasables, formables) now
list with a [formable] marker, and rendering/division uses owned + cored
territory (on the test mod: 54 owned countries grow to 258 with 204
formables).
Custom state unions. States mode gained "Combine multiple states": tick
any set of states, name the region, and render their union as one shape -
saved under gfx/interface/regions with the state list embedded in the click
scaffold.
Custom sizes + crisp scaling. Max Size is now a free input (16-2048),
and the whole rendering backend was rebuilt to be resolution-independent:
shape geometry is point-sampled at the target resolution and outlines are
traced AFTER scaling, so borders stay exactly one crisp pixel at any size -
up or down - instead of being averaged into blur (verified: zero blended
pixels on a 5x upscale). Continent/country division inherits this and now
tiles with mathematically zero overlap between adjacent state pieces.
New in 2.19.0
Country images. The map-image modal gained a Countries mode: every tag
with owned states lists with its political color swatch (parsed from
common/countries/colors.txt), picking a country adopts its color as the
fill, and rendering produces the country's full territory silhouette
(saved under gfx/interface/countries). Clickable countries scaffold
selected_country_id via TAG.id with an exists check. "Divide into clickable
states" works here too - a country becomes per-state clickable pieces
composing its exact territory (shared-scale machinery, piece cap raised to
500 for large-mod mega-nations).
New in 2.18.0
Divide continents into clickable states. The continent importer gained a
"Divide into clickable states" option: every state on the continent renders
as its own piece at one shared scale (border states clip cleanly to the
continent), lands as a clickable button (or icon) positioned inside a
container so the pieces compose the exact continent shape, each with the
state-scoped click/enabled scaffolds and its PNG saved under
gfx/interface/states. Verified by decode-and-recompose: 211 North American
states reassemble pixel-perfectly. One click, one clickable map.
New in 2.17.0 — GUI Creator: advanced batch
- Continent images: the map-image modal gained a States/Continents
switch - render any continent's silhouette (named, province-counted) and
add it as an icon or clickable button, saved under gfx/interface/continents.
- Vanilla parent backdrop: with a parent_window_token set, "Show Parent
Backdrop" renders the actual vanilla window (topbar, state view...) from
your HOI4 install - real decoded textures - behind the canvas, so you
position attachments against the real UI (needs hoi4.gamePath).
- Structured bindings: one-click binds on icons/progress bars/text -
frame = "[?variable]" for variable-driven frames, image/[Getter] and
text/[Getter] flows that also write the common/scripted_localisation stub.
- Window tabs: the main window, each modal, and each entry template edit
on their own canvas tab; new elements land in the active window and the
hierarchy jumps tabs when you click across windows. Preview still shows the
composite with live open/close.
- Preview fidelity: true 9-slice rendering for corneredTileSpriteType
(borderSize-aware), hover/click frame states on multi-frame button sprites,
and per-font approximate text sizing.
New in 2.16.0
Province Creator: edit existing provinces. The creator modal gained an
"Edit existing province" row: enter an ID and Load, or use Pick from Map (an
eyedropper - click any province to adopt its ID, color, terrain, type,
coastal flag and continent). In edit mode the brush color is locked to the
province's real RGB (painting another color would silently split it) and
finishing paints pixels onto the BMP without creating a duplicate
definition.csv row.
Province ID renumbering. The Province Editor panel can change a
province's ID, updating every reference: definition.csv, state files
(provinces lists, victory points, province-keyed building blocks - while
leaving state ids and manpower values that happen to equal the number
untouched), strategic regions, railways and supply nodes (level/count fields
protected), adjacencies.csv (exact fields only), airports and rocket sites
(state keys protected). Renumbering onto an existing ID is refused. Also
available over MCP as map_renumber_province, with an automatic backup.
Covered by an 18-check test suite including the pathological
renumber-province-1 case.
New in 2.15.0
HOI4 MCP status-bar toggle. The PDX HOI4 MCP Python server (gfx/asset/
entity/texture tools + Blender bridge from the model pipeline) now has its
own bottom-right status item - "HOI4 MCP: Off/Running" - next to MCP Map and
MCP Model, with the same click-to-toggle behavior, an info command that
copies the claude mcp add line, and a hoi4.pdxMcp.command setting for the
executable path (default pdx-hoi4-mcp; install via pip install -e).
New in 2.14.0
Sub-menus from the sidebar. The Add Elements pane gained a Sub-menus
section: "+ Sub-menu (in-window)" and "+ Modal Window" adapt to the current
selection - a selected button becomes the opener, a selected icon (state
images included) is converted to a button and wired, a selected container
hosts a newly created opener inside it, and with nothing selected a complete
opener + menu + close-button kit is placed on the canvas. Clicking the option
while the opener already has a menu jumps the selection to that menu.
Four new Map MCP tools let an AI client author and visualize scripted GUIs:
- gui_render_preview — renders any window to a PNG returned over MCP:
sprites resolved from your .gfx files and decoded (DDS/TGA, vanilla
fallback via HOI4_DIR), placeholders with name labels for the rest, text
rendered with a built-in microfont. Works from a file or an inline spec,
so the AI can preview a layout before writing it.
- gui_read_window — a window as structured JSON (elements with absolute
coordinates, gridbox slots) plus the scripted_gui entry that drives it
(effects, triggers, properties, dynamic lists).
- gui_upsert_window — write a window from that same JSON shape, merged
surgically into existing files; supports nesting, modals and entry
templates.
- gui_render_state_image — state silhouettes from provinces.bmp over MCP,
with optional save into the mod.
gui_create_scripted_gui now merges into existing files (replacing the
same-named entry, keeping siblings and comments) instead of overwriting.
Tool count: 55.
New in 2.12.0
Submenus and modals. Any button (including clickable states) can now open
a sub-panel or a modal with one click: "+ Sub-panel" creates a flag-gated
nested container in the window; "+ Modal" creates a separate centered
containerWindowType with its OWN scripted_gui entry, visible only while its
flag is set. Both come pre-wired: the opener gets a proper if/else toggle
(set/clr_country_flag), the menu gets a close button, and effects, triggers,
properties and dynamic lists belonging to a modal are emitted into the
modal's entry rather than the main window's. You can also link a button to
any existing container, or unlink it (removing exactly the toggle it added).
Preview mode is now interactive for this wiring: click the opener and the
menu actually opens; click its close button and it closes. Validation checks
for dangling submenu links and menus with no close control.
New in 2.11.0
Clickable state images. The State Image tool can now add states as real
buttons: "Clickable" makes the silhouette a buttonType wired into the
scripted_gui, and "Scaffold state-scoped checks" pre-fills working starters -
a click effect that scopes into the state (sets a state flag and
selected_state_id), an is_owned_by = ROOT enabled check, and a per-state
visible trigger. Any existing icon can be converted to a clickable button
(and back) from the property inspector; state sprites get the same wiring on
conversion. Preview mode shows pointer cursor + hover highlight on buttons.
New in 2.10.0 — Scripted GUI Creator: full round-trip editing
Import now round-trips everything. Importing a .gui also finds and parses
its matching common/scripted_guis block (by window_name): effects, triggers,
properties, dynamic_lists, context type, parent_window_token, visible, dirty
and ai_enabled all land back on the right elements - and entries that match
no element are preserved and re-emitted, never dropped. Export writes back
surgically: same-named windows are replaced in place inside existing .gui
files (other windows, comments, and headers untouched), and the scripted_gui
entry is replaced by name in its original file - including across renames.
Dynamic-list entry designer. Gridboxes now have a one-click "Create Entry
Template": design the per-row layout on canvas (it exports as its own
top-level containerWindowType and auto-fills entry_container), and preview
mode tiles sample rows into the gridbox so lists look real before the game
ever loads them.
Intelligent script boxes. Every effect/trigger textarea now validates as
you type (unbalanced braces, unknown effect/trigger names against the
240-entry database - country tags, flags, and variables excluded from
warnings) and autocompletes effect names with syntax hints (Tab/Enter to
accept). An "fx math" link compiles an infix formula into set_variable
script via the 1.19.x math engine, straight into the effect.
One-click validation. The new Check button audits the whole GUI: duplicate
or invalid names, elements outside the window, sprites missing from every
scanned .gfx, dynamic lists without entry containers, slot-size mistakes, and
script problems - with click-to-select on each finding.
New in 2.9.0
State images in the Scripted GUI Creator. A new "+ State Image" tool
renders any state's silhouette straight from provinces.bmp (the mod's map,
or the vanilla install via hoi4.gamePath for GUI-only mods): searchable state
picker with localized names, fill/outline colors, live preview, then one
click saves the PNG to gfx/interface/states/, drops it on the canvas as an
icon element, and points the generated .gfx spriteType at the saved file.
Rendering uses the map loader's run-length geometry - about a second to load
the map, instant per state, no native dependencies, and the map data is
released when the panel closes.
New in 2.8.0 — Focus Tree Editor overhaul
Your files are safe now. The editor used to fully regenerate the file for
its most common actions (linking prerequisites, adding a focus, bulk edits) —
silently deleting header comments, shared_focus/joint_focus blocks, any
second focus_tree in the file, and every key it didn't recognize. Every
write is now a surgical splice that leaves the rest of the file byte-for-byte
identical, unknown/modded focus keys are captured and re-emitted when a block
does get regenerated, and if a minimal edit can't be applied the editor
refuses to write rather than falling back to a destructive rewrite. Verified
against 125 real files / 10,561 focuses (vanilla + a large mod) with zero
parse errors.
Fixed along the way: dragging a relative_position_id focus corrupted its
in-game position (now written as a delta, relation preserved); renaming a
focus never reached disk (now saves and cascades into prerequisites, mutex,
relative_position_id, and localisation keys); bulk move / auto-layout /
"Load Dependency Mod" called methods that didn't exist; blur-autosave silently
discarded edits to 17 of 21 fields; quoted and non-GFX_ icons were rewritten
to GFX_goal_unknown; completion_reward log lines were injected into every
focus that had none; multiple shared_focus refs collapsed to one;
initial_show_position was dropped; relative_position_id cycles crashed the
extension host; bulk deletes left ghost nodes on the canvas; loc values with
quotes or $VAR$ tokens broke the .yml.
Also: joint-focus fields (joint_trigger,
completion_reward_joint_originator/_member) parse and save,
common/national_focus subfolders are scanned, focus icons use the shared
pure-JS DDS decoder (DX10 headers now supported; the 15s-per-icon ImageMagick
fallback and a 260-line duplicate decoder are gone), undo history stores raw
file text (byte-faithful restore, ~3x smaller), and History restore repaints
the canvas.
New in 2.7.0
Memory overhaul. The map editor's undo system no longer snapshots the whole
map per brush stroke (46 MB each, up to ~4.6 GB across undo+redo) — it stores
per-stroke pixel deltas (~150 KB for a 10k-pixel stroke). Hiding the map editor
tab now genuinely frees its renderer (it re-sends data on re-show instead of
coming back blank), Refresh no longer duplicates loader data, orphaned WebGL
buffers are freed, and the Map MCP child releases its ~250 MB of decoded map
after 10 idle minutes. A workspace-wide V8 string-slice retention bug — one
20-char captured name could pin an entire file's text — was fixed across the
index, localization, dependency graph, and the event/decision editors
(measured: 30 MB corpus now retains 4 MB instead of all 30). See MEMORY.md for
the full engineering log. New command: HOI4: Memory Doctor — live usage
report, one-click remedies, and honest guidance for CW Tools users (its .NET
language server is a separate process this extension cannot shrink).
Scripted GUI Creator: real previews. The GFX browser now decodes .dds
textures (DXT1/3/5 and uncompressed, pure JS — no native deps) and lazy-loads
thumbnails as you scroll. A new Preview toolbar toggle renders your layout
HOI4-style: window background sprite, real sprite textures on elements,
multi-frame button sprites cropped to their first frame, and game-ish styling
fallbacks for elements without sprites.
New in 2.6.0
1.19.1 / 1.19.2 math expression support (MCP). The Map MCP server gained four
pure-computation tools for the math expression system Paradox shipped in
1.19.1 (pow, sqrt, cos, sin, tan, exp, log) and 1.19.2 (lerp, atan, atan2,
logical and/or/xor/not):
script_math_reference — every function with syntax, argument shape, and the game version it needs
script_math_validate — catches unknown functions (with suggestions), malformed arguments, and version mismatches; warns when atan/atan2/log/pow/root lack a trailing round = yes
script_math_eval — evaluates an expression with variable bindings, returning a step-by-step accumulator trace with HOI4 fixed-point (0.001) simulation
script_math_build — compiles a normal infix formula like lerp(lo, hi, clamp(p / total, 0, 1)) into ready-to-paste set_variable script, hoisting non-linear subexpressions into temp variables
The effect database also gained the modulo_variable / *_temp_variable
family and per-function math entries (240 total), and the script decorator
highlights the new functions.
Model MCP server (3D pipeline). A second MCP server
(hoi4.startModelMcp / status-bar toggle) exposes a Meshy-based 3D asset
pipeline: submit text→3D or image→3D generation, poll, download GLB/FBX +
textures, auto-rig, drive a live Blender session through the pdx_hoi4_mcp
bridge addon (validate/prepare/decimate/export .mesh and .anim), and generate
the Clausewitz wiring (.gfx + .asset + entity) for a finished model.
Generation tools are submit-only — they return a task id immediately and never
block on the minutes-long, credit-costing Meshy jobs. Set MESHY_API_KEY in
the environment or a .env in the pipeline root
(hoi4.modelMcp.pipelineRoot); the key never appears in tool output.
Scripted GUI Creator, fleshed out. The visual editor now covers the real
scripted GUI surface:
gridBoxType elements with slot size/format and dynamic_lists bindings, editBoxType, OverlappingElementsboxType
- Per-button click / right-click effect editors,
_click_enabled and per-element _visible triggers
- Context type,
parent_window_token, editable visible trigger, dirty variable, and ai_enabled on the scripted_gui block
- Element nesting: parent a control to a container and the generated
.gui nests it with relative coordinates
- Import uses a real recursive parser (window settings, nesting, gridbox slots all survive a roundtrip)
- Export now writes the
.gfx file (progress bars emit progressbartype entries) and localisation gets the UTF-8 BOM HOI4 requires
- The MCP
gui_create_scripted_gui tool gained the same fields (triggers, dynamic_lists, ai_enabled, parent_window_token, dirty) and its effects are now correctly wrapped in an effects = { } block
Working on a very large mod?
Indexing was rewritten in 2.5.4 to remove several quadratic hot spots that made
startup on a large mod take minutes to hours and pushed the extension host into
multi-gigabyte memory use. See MEMORY.md for the measurements, the
new hoi4.index.* / hoi4.memory.* settings, and the costs that remain.
Table of Contents
Installation
From VSIX File (Recommended)
- Download the latest
.vsix file from Releases
- Open VS Code
- Press
Ctrl+Shift+P and type Install from VSIX
- Select the downloaded file
- Reload VS Code when prompted
Quick Start
- Open your mod folder in VS Code (File > Open Folder)
- Look for the Shield icon in the Activity Bar (left sidebar)
- Click it to reveal all tools organized by category
- Start with Welcome / Help for an interactive guide
Visual Editors
Focus Tree Editor
Command: HOI4: Focus Tree Editor
A full visual editor for creating and modifying national focus trees with drag-and-drop functionality.
Opening the Editor
- Click Focus Tree Editor in the sidebar, or
- Press
Ctrl+Shift+P and type "Focus Tree Editor"
Main Interface
- Left Sidebar: List of all focus trees in your mod
- Center Canvas: Visual focus tree with draggable nodes
- Right Panel: Property editor for selected focus
Creating a New Focus Tree
- Click the + New Tree button
- Enter country tag (e.g.,
GER, ENG)
- Enter tree name
- Click Create
Adding Focuses
- Click Add Focus in toolbar, or
- Right-click on canvas and select "Add Focus Here", or
- Press A key to add at center
Editing Focus Properties
- Click a focus node to select it
- Edit properties in right panel:
- Focus ID: Internal identifier
- Name/Description: Click to edit localization
- Icon: GFX sprite name (with preview)
- X/Y Position: Grid coordinates
- Cost: Days to complete
- Prerequisites: Required focuses (AND groups, OR within groups)
- Mutually Exclusive: Blocking focuses
- Available/Bypass: Trigger conditions
- Completion Reward: Effects on completion
- AI Will Do: AI weighting
Moving Focuses
- Drag and drop focus nodes to reposition
- Position snaps to grid automatically
- Connections update in real-time
Linking Prerequisites
- Click Link Prerequisites button or press P
- Click the parent focus first
- Click the child focus
- Repeat for more connections
- Press Escape to exit link mode
For OR prerequisites (any one required), add multiple focuses to the same prerequisite group using the dropdown in the editor.
Linking Mutual Exclusives
- Click Link Exclusives button or press M
- Click first focus
- Click second focus
- Both focuses will block each other
Focus Tree Keyboard Shortcuts
| Key |
Action |
| A |
Add focus at center |
| P |
Toggle prerequisite linking mode |
| M |
Toggle exclusive linking mode |
| Delete |
Delete selected focus |
| Escape |
Cancel current operation |
Tips
- Focus positions use HOI4's grid system (each unit = 1 column/row)
- Focuses with
relative_position_id show calculated absolute positions
- Double-click a focus to quickly edit its name
- Use the zoom controls for large trees
Decision Editor
Command: HOI4: Decision Editor
Visual editor for creating and managing decisions with full support for all decision types.
Decision Types Supported (Work in Progress)
- Standard Decisions: Instant effect on activation
- Timed Decisions: Effect after countdown
- Timed Missions: AI-triggered with timeout
- Selectable Missions: Player-chosen missions
Main Interface
- Left Sidebar: File browser and decision list
- Center Panel: Decision list with categories
- Right Panel: Full property editor
Creating Decisions from Template
- Click Templates button
- Choose a template:
- War Goal Decision
- Political Action
- Timed Mission
- Economic Decision
- State Action
- Faction Interaction
- Select target category
- Click "Create from Template"
Manual Creation
- Click + Add button
- Select decision type
- Select category
- Edit properties in right panel
Editing Decisions
Select a decision to edit:
- Basic Info: ID, name, description, icon, category
- Cost and Timing: PP cost, days, re-enable delay
- Conditions: Allowed, Available, Visible triggers
- Targets: Fixed targets, target array, state targeting
- Effects: Complete, Remove, Timeout, Cancel effects
- AI: AI weighting and conditions
Icon Browser
- Click Browse... next to Icon field
- Search or scroll through available GFX sprites
- Click to preview, double-click to select
State Picker
For highlight_states:
- Click Pick States... button
- Search by ID or name
- Click states to select (multi-select)
- Click "Apply" to insert state list
In-Game Preview
Click Preview to see how the decision appears in-game UI, including icon display, name and description, cost and duration, and category placement.
Validation
- Click Validate to check for errors
- Automatic validation on save
- Shows missing localizations, invalid references
Search Across Files
- Click Search All button
- Enter search term
- Filter by type (flags, events, ideas, states)
- Click results to jump to that decision
Tech Tree Editor
Command: HOI4: Tech Tree Editor
Visual editor for technology trees with research time calculations.
Features
- Visual tech tree layout matching in-game view
- Drag-and-drop positioning
- Research time and cost editing
- Prerequisite linking
- Category management
- Icon preview
Usage
- Select a technology file
- Click technologies to edit properties
- Drag to reposition
- Link prerequisites by clicking connection mode
Technology Properties
- Research time and cost
- Start year
- Categories and folders
- Prerequisites
- Research bonuses
- Unlocked equipment/units
Map Editor
Command: HOI4: Open Map Editor
Visual map editor for provinces, states, and strategic regions.
Map Layers
- Provinces: Individual province view
- States: State boundaries and ownership
- Strategic Regions: Military regions
- Supply Areas: Logistics visualization
Province Picker
- Open Map Editor
- Click on map to select province
- Province ID shown in status bar
- Copy ID with button or
Ctrl+C
State Editing
Click state to select, then edit properties in side panel:
- Owner and controller
- Victory points
- Buildings (infrastructure, factories, etc.)
- Resources (steel, oil, etc.)
- Core states
Building Placement
- Click building icons to place
- Adjust levels with +/- buttons
- Visual indicators on map
OOB Creator
Command: HOI4: OOB Creator
Create Orders of Battle (starting military units) for countries.
Creating an OOB
- Click New OOB or select existing
- Set country tag
- Build military hierarchy
Unit Hierarchy
- Theater > Army Group > Army > Corps > Division
- Drag units to reorganize
- Right-click for context menu
Division Designer
- Click New Division
- Select division template or create custom
- Add battalions: Infantry, Artillery, Armor, Support companies
- Set equipment levels
- Assign to army/corps
Air Wings
- Create air wings
- Assign aircraft types and counts
- Set home air base
Naval Fleets
- Create task forces
- Assign ships
- Set home port
Export
- Generates valid HOI4 OOB file
- Automatically creates unit history file
- Proper formatting and structure
Equipment Analyzer
Command: HOI4: Equipment Analyzer
Analyze, compare, and simulate production of equipment.
Equipment Browser
- Browse all equipment by category
- Filter by year, type, archetype
- View detailed stats
Equipment Editor
- Select equipment to edit
- Modify any stat
- See combat preview update
- Save to original file
Production Simulator
- Select equipment
- Set factory count
- Apply modifiers (industrial capacity, production efficiency, resource availability)
- View production rate and time estimates
Comparison Mode
- Select up to 3 equipment items
- Side-by-side stat comparison
- Highlights best/worst values
- Cost efficiency analysis
Battle Simulator
Command: HOI4: Battle Simulator
Simulate combat between divisions to test effectiveness.
Setting Up a Battle
- Attacker: Build or select division
- Defender: Build or select division
- Terrain: Select combat terrain
- Modifiers: Add entrenchment, air support, etc.
Division Builder
- Add any unit types
- Set equipment variants
- Apply doctrines and modifiers
Combat Stats
- Soft/Hard Attack values
- Defense and Breakthrough
- Organization and HP
- Armor and Piercing
Simulation Results
- Predicted winner
- Estimated battle duration
- Casualty estimates
- Organization damage over time
AI Behavior Viewer
Command: HOI4: AI Behavior
Analyze and understand AI decision-making in your mod.
Features
- View AI strategy plans
- Analyze focus tree AI weights
- See decision AI priorities
- Understand AI division templates
Usage
- Select country or AI file
- Browse AI behaviors by type
- View conditions and weights
- Identify potential issues
Event Chain Visualizer
Command: HOI4: Event Chain Visualizer
Visualize event relationships and chains.
Features
- Interactive graph view
- Automatic chain detection
- Option tracking
- Namespace filtering
Usage
- Open Event Chain Visualizer
- Select event namespace or file
- View connected events as graph
- Click nodes to see event details
- Follow option arrows to see triggers
Graph Controls
- Zoom with scroll wheel
- Pan by dragging background
- Click nodes to select
- Double-click to open event file
Dependency Graph
Command: HOI4: Show Dependency Graph
Shortcut: Ctrl+Alt+D
Visualize relationships between files in your mod.
Features
- File dependency visualization
- Circular dependency detection
- Impact analysis
- Filter by file type
Usage
- Open from sidebar or command
- Select root file or view all
- Explore connections
- Click nodes to see details
Impact Analysis
Right-click any file and select "Analyze Impact" to see:
- What files depend on this file
- What this file depends on
- Potential cascade effects of changes
Content Browsers
Idea Browser
Command: HOI4: Idea Browser
Browse and search all ideas, national spirits, and advisors.
Categories
- National Spirits
- Political Advisors
- Military Advisors (Army, Navy, Air)
- Theorists
- Laws (Economy, Trade, Conscription)
- Hidden Ideas
Features
- Search by name or effect
- Filter by category
- View all modifiers
- See availability conditions
- Cost information
Usage
- Open Idea Browser
- Select category or search
- Click idea to see details
- Double-click to open source file
Flag and Variable Tracker
Command: HOI4: Flag Tracker
Track global flags, country flags, and variables across your mod.
Features
- Find all flag/variable definitions
- Track where flags are set
- Track where flags are checked
- Identify unused flags
Flag Types
- Global flags
- Country flags
- State flags
- Variables
- Dynamic modifiers
Usage
- Open Flag Tracker
- Search for flag name
- View all references
- Click to jump to location
GFX Auditor
Command: HOI4: GFX Auditor
Validate and manage graphical assets.
Audit Types
- Missing Assets: GFX defined but file missing
- Unused Assets: Files with no GFX reference
- Invalid Dimensions: Wrong image sizes
- Format Issues: Incorrect file formats
Usage
- Open GFX Auditor
- Run audit (automatic on open)
- Review issues by category
- Click to navigate to problem
- Use Quick Fix for common issues
Quick Fixes
- Generate missing GFX entries
- Create placeholder images
- Fix path references
Command: HOI4: Image Toolkit
Image processing and conversion tools.
Features
- View DDS files directly
- Convert between formats (DDS, PNG, TGA)
- Resize images
- Generate mipmaps
- Batch processing
- DDS (DXT1, DXT5, BC7)
- PNG
- TGA
- BMP
Event Picture Creator
Command: HOI4: Event Picture Creator
Create and manage event pictures.
Features
- Browse existing event pictures
- Create new event GFX entries
- Preview at correct dimensions
- Auto-generate sprite definitions
Usage
- Open Event Picture Creator
- Browse or create new
- Select source image
- Set GFX name
- Generate entry and copy files
Animated DDS Viewer
Command: HOI4: Animated DDS
View animated DDS files used in HOI4.
Features
- Play animated textures
- Frame-by-frame view
- Speed controls
- Export frames
Localization Dashboard
Command: HOI4: Localization Dashboard
Shortcut: Ctrl+Alt+L
Overview of localization coverage and management.
Features
- Coverage statistics by category
- Missing key detection
- Language comparison
- Batch operations
Dashboard Views
- Overview: Total coverage percentage
- By File: Coverage per localization file
- Missing: List of unlocalized keys
- Comparison: Compare languages
Batch Operations
- Add multiple keys at once
- Copy from one language to another
- Export missing keys list
Event Localizer
Command: HOI4: Event Localizer
Quickly localize events with assisted suggestions.
Features
- Event-focused localization
- Title and description editing
- Option text management
- Preview formatted text
Usage
- Open Event Localizer
- Select event file
- Edit titles, descriptions, options
- Save to localization file
Quick Localize
Shortcut: Ctrl+Shift+L
Instantly add localization for selected text.
Usage
- Select a localization key in your code (e.g.,
my_event.1.t)
- Press
Ctrl+Shift+L
- Enter the localized text
- Automatically added to your localization file
Configuration
Set your default localization file in settings:
{
"hoi4.localizationFile": "localisation/mymod_l_english.yml"
}
Dev Notes and Kanban
Command: HOI4: Dev Notes
Project management integrated into your mod workspace.
Kanban Board
- Drag-and-drop task management
- Customizable columns
- Card labels and due dates
- Rich text descriptions
File Notes
- Attach notes to specific files
- Quick access from explorer
- Color-coded by priority
TODO Scanner
Automatically finds comments:
// TODO: ...
// FIXME: ...
// NOTE: ...
# ISSUE: ...
Data Storage
- Saved to
.hoi4-dev-notes.json
- Add to
.gitignore if desired
- Share via version control
Command: HOI4: Performance Profiler
Shortcut: Ctrl+Alt+P
Analyze mod performance and find bottlenecks.
Metrics Analyzed
- File size analysis
- Trigger complexity scoring
- Effect chain depth
- Event fire frequency estimates
Reports
- Large Files: Files that may cause loading issues
- Complex Triggers: Expensive condition checks
- Heavy Effects: Resource-intensive effects
- Recommendations: Optimization suggestions
Usage
- Open Performance Profiler
- Run analysis
- Review issues by severity
- Click items to navigate
- Follow recommendations
Debug Log Viewer
Command: HOI4: Show Debug Log
Shortcut: Ctrl+Alt+O
View and analyze HOI4's debug output.
Features
- Live log tailing
- Error highlighting (red)
- Warning highlighting (yellow)
- Filter by type
- Search functionality
- Click errors to open source
Log Watcher
Start continuous monitoring:
HOI4: Start Debug Log Watcher
- Run the game
- Errors appear in real-time
HOI4: Stop Debug Log Watcher when done
Configuration
Set your log path:
{
"hoi4.debugLogPath": "C:/Users/You/Documents/Paradox Interactive/Hearts of Iron IV/logs"
}
Changelog Generator
Command: HOI4: Changelog Generator
Generate changelogs from git history.
Features
- Reads git commits automatically
- Auto-categorizes changes
- Version tagging
- Markdown export
- Custom templates
Categories
- Features
- Bug Fixes
- Balance Changes
- Content Additions
- Performance
Usage
- Open Changelog Generator
- Select date range or version tags
- Review and edit entries
- Export as Markdown
- Ready for Steam/GitHub
Search and Navigation
Global Search
Command: HOI4: Global Search
Shortcut: Ctrl+Alt+G
Search across all mod content.
Specialized Searches
| Command |
Shortcut |
Searches |
| Search Flags |
Ctrl+Shift+F |
Country flags, global flags |
| Search Events |
- |
Event IDs and content |
| Search Focus |
- |
Focus tree IDs and effects |
| Search Decisions |
- |
Decision IDs and triggers |
Go to Definition
Shortcut: F12
Jump to the definition of event IDs, focus IDs, decision IDs, idea IDs, and technology IDs.
Find All References
Shortcut: Shift+F12
Find everywhere something is used.
Keyboard Shortcuts
| Shortcut |
Action |
|
Ctrl+Shift+L |
Quick Localize selected text |
(Or select text and right click and click HOI4: Add Dev Note Here) |
Ctrl+Alt+G |
Global Search |
|
Ctrl+Alt+D |
Dependency Graph |
|
Ctrl+Alt+P |
Performance Profiler |
|
Ctrl+Alt+L |
Localization Dashboard |
|
Ctrl+Alt+O |
Debug Log Viewer |
Or click Problems by Output on the bottom menu pane |
Ctrl+Shift+I |
Analyze Impact |
|
F12 |
Go to Definition |
|
Shift+F12 |
Find All References |
|
Ctrl+Space |
IntelliSense suggestions |
|
Ctrl+. |
Quick Fix menu |
|
Configuration
Extension Settings
Access via File > Preferences > Settings and search for "HOI4" or edit the Extension Settings Workspace.
{
"hoi4.gamePath": "C:/Program Files/Steam/steamapps/common/Hearts of Iron IV",
"hoi4.defaultLanguage": "english",
"hoi4.localizationFile": "localisation/mymod_l_english.yml",
"hoi4.enableDiagnostics": true,
"hoi4.debugLogPath": "Documents/Paradox Interactive/Hearts of Iron IV/logs"
}
Workspace Settings
Create .vscode/settings.json in your mod folder for project-specific settings:
{
"hoi4.modName": "My Awesome Mod",
"hoi4.modVersion": "1.0.0",
"hoi4.supportedGameVersion": "1.14.*"
}
Troubleshooting
Extension Not Loading
- Ensure you opened a folder, not individual files
- Check for
descriptor.mod in root (identifies HOI4 mod)
- Reload VS Code:
Ctrl+Shift+P > "Reload Window"
- Check Output panel for errors: View > Output > "HOI4 Modding Tools"
Map Editor Issues
- Verify
map/definition.csv exists
- Ensure
map/provinces.bmp is valid
- Check file permissions
Localization Not Working
- Set localization file in settings
- Ensure file uses UTF-8-BOM encoding
- Verify file path is correct relative to workspace
Focus Tree Not Displaying Correctly
- Check for circular
relative_position_id references
- Verify all referenced focuses exist
- Save and reload the tree
- Large mods take time to index on first open
- Close unused editor panels
- Disable unused features in settings
- Rebuild index:
HOI4: Rebuild File Index
Expected Mod Structure
your-mod/
├── common/
│ ├── countries/
│ ├── country_tags/
│ ├── decisions/
│ │ └── categories/
│ ├── ideas/
│ ├── national_focus/
│ ├── technologies/
│ └── units/
│ └── equipment/
├── events/
├── gfx/
│ ├── event_pictures/
│ ├── interface/
│ │ └── goals/
│ └── leaders/
├── history/
│ ├── countries/
│ ├── states/
│ └── units/
├── interface/
├── localisation/
├── map/
│ ├── definition.csv
│ └── provinces.bmp
└── descriptor.mod
Reporting Issues
- Message awesome___. aka Olander on Discord
Acknowledgments
- Hearts of Iron IV by Paradox Interactive
- VS Code Extension API
- The HOI4 modding community
- Chaofan
Changelog
v2.5.0
Chinese Localization & i18n System
- Added Simplified Chinese (simp_chinese) support across all language lists — localization manager, dashboard, and settings
- Built full i18n translation framework with
t() function, 232 translated strings (English + Chinese), and locale switching via hoi4.extensionLanguage setting
- Migrated 143 strings across 5 panels: Map Editor, Particle Editor, Focus Tree Editor, Building Generator, and Localization Editor
- Localization Dashboard "Generate Missing Keys" now respects the active language setting
Translator Workbench (New Panel)
- Tab 1 — MOD Localisation: scan source/target coverage, inline editing with auto-save, export templates with English comments, import completed translations
- Tab 2 — Extension UI: scan and edit extension locale strings with coverage stats
Map Editor — Full State Editor
- Quick Edit State panel expanded: owner, category, manpower, display name, victory points (add/edit/remove), cores (add/remove chip UI), resources (all 6 types), buildings, and province list
- Province management: "Move Selection Here", "New State from Selection", manual province ID input to add provinces to a state
- Province move backend with automatic source/target state file updates
- Data loader expanded:
updateStateFile now handles owner, category, resources, cores; new moveProvincesToState method
Map Editor — Railway Drawing Fix
- Fixed railway and supply network rendering: in-progress railway lines, placed supply hubs, and deletions now render immediately (7 missing
renderOverlay() calls fixed)
Localization Editor Fixes
- Fixed file list empty on load (race condition: embedded initial data + message retries)
- Now scans all 9 language subfolders (english, german, french, spanish, russian, polish, braz_por, japanese, simp_chinese)
- File creation respects selected language and adds UTF-8 BOM
- Language-colored file badges (EN, DE, FR, ES, RU, PL, PT, JA, ZH)
- CSV export scans all
.yml files instead of hardcoded English
Focus Tree Editor Fixes
- Fixed tree list not rendering (onclick handler crash from escaped quotes in template literal)
- Fixed 10 i18n
t() calls using wrong string concatenation syntax inside template literals
Province Editor Crash Fixes
- Fixed 2 webview crash bugs from
getElementById('...') with single quotes inside inline onclick/oninput handlers
Keybinding Fix
- Color insert keybindings changed from
Shift+Number to Ctrl+Shift+Number (stops hijacking !, #, (, ) etc.)
Critical Webview Fix
- Fixed i18n injection in all 5 panels — was using string concatenation inside template literals instead of
${...} interpolation, which caused SyntaxError: Unexpected token 'const' and killed entire panel scripts
Translation Guide
- TRANSLATION_GUIDE_CHINESE.md bundled: HOI4 mod localization format, extension UI translation approach, and full English↔中文 terminology table
Support