Skip to content
| Marketplace
Sign in
Visual Studio Code>Programming Languages>HOI4 Modding ToolsNew to Visual Studio Code? Get it now.
HOI4 Modding Tools

HOI4 Modding Tools

UC-Modding Utilities

|
2,552 installs
| (1) | Free
| Sponsor
Comprehensive Hearts of Iron 4 modding extension with file navigation, cross-reference search, localization tools, dependency analysis, debug log integration, performance profiling, and country creation wizard
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

HOI4 Modding Tools Extension

Logo Logo Logo Logo Logo Logo

HOI4 Modding Tools

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.

HOI4 Modding Tools for VS Code

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.

Version HOI4 VS Code License


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.

New in 2.34.0 — Resize & reshape tools (provinces, states, locations, canvas)

"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.

New in 2.33.0 — Sea & naval tools

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.

New in 2.32.0 — Map-making tools (a painted province becomes a playable one)

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.

New in 2.30.0 — Map raster tools (edit the bitmaps over MCP)

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.

New in 2.29.2 — File Graph performance

  • 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.

New in 2.13.0 — Scripted GUI MCP tools (AI can see your GUI)

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
  • Quick Start
  • Visual Editors
  • Analysis Tools
  • Content Browsers
  • Graphics Tools
  • Localization Tools
  • Development Tools
  • Search and Navigation
  • Keyboard Shortcuts
  • Configuration
  • Troubleshooting
  • Contributing

Installation

From VSIX File (Recommended)

  1. Download the latest .vsix file from Releases
  2. Open VS Code
  3. Press Ctrl+Shift+P and type Install from VSIX
  4. Select the downloaded file
  5. Reload VS Code when prompted

Quick Start

  1. Open your mod folder in VS Code (File > Open Folder)
  2. Look for the Shield icon in the Activity Bar (left sidebar)
  3. Click it to reveal all tools organized by category
  4. 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

  1. Click Focus Tree Editor in the sidebar, or
  2. 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

  1. Click the + New Tree button
  2. Enter country tag (e.g., GER, ENG)
  3. Enter tree name
  4. 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

  1. Click a focus node to select it
  2. 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

  1. Click Link Prerequisites button or press P
  2. Click the parent focus first
  3. Click the child focus
  4. Repeat for more connections
  5. 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

  1. Click Link Exclusives button or press M
  2. Click first focus
  3. Click second focus
  4. 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

  1. Click Templates button
  2. Choose a template:
    • War Goal Decision
    • Political Action
    • Timed Mission
    • Economic Decision
    • State Action
    • Faction Interaction
  3. Select target category
  4. Click "Create from Template"

Manual Creation

  1. Click + Add button
  2. Select decision type
  3. Select category
  4. 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

  1. Click Browse... next to Icon field
  2. Search or scroll through available GFX sprites
  3. Click to preview, double-click to select

State Picker

For highlight_states:

  1. Click Pick States... button
  2. Search by ID or name
  3. Click states to select (multi-select)
  4. 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

  1. Click Search All button
  2. Enter search term
  3. Filter by type (flags, events, ideas, states)
  4. 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

  1. Select a technology file
  2. Click technologies to edit properties
  3. Drag to reposition
  4. 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

  1. Open Map Editor
  2. Click on map to select province
  3. Province ID shown in status bar
  4. 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

  1. Click New OOB or select existing
  2. Set country tag
  3. Build military hierarchy

Unit Hierarchy

  • Theater > Army Group > Army > Corps > Division
  • Drag units to reorganize
  • Right-click for context menu

Division Designer

  1. Click New Division
  2. Select division template or create custom
  3. Add battalions: Infantry, Artillery, Armor, Support companies
  4. Set equipment levels
  5. 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

Analysis Tools

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

  1. Select equipment to edit
  2. Modify any stat
  3. See combat preview update
  4. Save to original file

Production Simulator

  1. Select equipment
  2. Set factory count
  3. Apply modifiers (industrial capacity, production efficiency, resource availability)
  4. 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

  1. Attacker: Build or select division
  2. Defender: Build or select division
  3. Terrain: Select combat terrain
  4. 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

  1. Select country or AI file
  2. Browse AI behaviors by type
  3. View conditions and weights
  4. 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

  1. Open Event Chain Visualizer
  2. Select event namespace or file
  3. View connected events as graph
  4. Click nodes to see event details
  5. 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

  1. Open from sidebar or command
  2. Select root file or view all
  3. Explore connections
  4. 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

  1. Open Idea Browser
  2. Select category or search
  3. Click idea to see details
  4. 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

  1. Open Flag Tracker
  2. Search for flag name
  3. View all references
  4. Click to jump to location

Graphics Tools

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

  1. Open GFX Auditor
  2. Run audit (automatic on open)
  3. Review issues by category
  4. Click to navigate to problem
  5. Use Quick Fix for common issues

Quick Fixes

  • Generate missing GFX entries
  • Create placeholder images
  • Fix path references

Image Toolkit

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

Supported Formats

  • 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

  1. Open Event Picture Creator
  2. Browse or create new
  3. Select source image
  4. Set GFX name
  5. 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 Tools

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

  1. Open Event Localizer
  2. Select event file
  3. Edit titles, descriptions, options
  4. Save to localization file

Quick Localize

Shortcut: Ctrl+Shift+L

Instantly add localization for selected text.

Usage

  1. Select a localization key in your code (e.g., my_event.1.t)
  2. Press Ctrl+Shift+L
  3. Enter the localized text
  4. Automatically added to your localization file

Configuration

Set your default localization file in settings:

{
  "hoi4.localizationFile": "localisation/mymod_l_english.yml"
}

Development Tools

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

Performance Profiler

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

  1. Open Performance Profiler
  2. Run analysis
  3. Review issues by severity
  4. Click items to navigate
  5. 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:

  1. HOI4: Start Debug Log Watcher
  2. Run the game
  3. Errors appear in real-time
  4. 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

  1. Open Changelog Generator
  2. Select date range or version tags
  3. Review and edit entries
  4. Export as Markdown
  5. 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

  1. Ensure you opened a folder, not individual files
  2. Check for descriptor.mod in root (identifies HOI4 mod)
  3. Reload VS Code: Ctrl+Shift+P > "Reload Window"
  4. 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

  1. Set localization file in settings
  2. Ensure file uses UTF-8-BOM encoding
  3. 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

Performance Issues

  1. Large mods take time to index on first open
  2. Close unused editor panels
  3. Disable unused features in settings
  4. 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

  1. 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

  • Issues: GitHub Issues
  • Discussions: GitHub Discussions

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
  • Your Privacy Choices
  • Consumer Health Privacy
© 2026 Microsoft