
A robust, whitespace-aware formatter for Paradox Interactive game scripts (Stellaris, HOI4, EU4, CK3).
This extension provides smart indentation, block expansion, and syntax protection, ensuring your code looks clean without breaking game logic or deleting comments. It also supports the Stellaris v4.4+ Safe Navigation syntax (scope? = { ... }): an optional pass that folds exists = xyz + xyz = { ... } (and has_owner = yes + owner/space_owner = { ... }) into xyz? = { ... } for 4.4+ targets, including exists = xyz + NOT = { xyz = { ... } } and the disjunctive OR = { NOT = { exists = xyz } xyz? = { ... } } forms, and reverts it for older versions.
✨ Features
1. Smart Block Expansion
Automatically expands one-line blocks into readable, multi-line structures, which is crucial for complex scope logic.
Before:
AND = { NOT = { has_overlord = event_target:FirstSleeper } NOT = { has_overlord = event_target:SecondSleeper } }
After:
NOR = {
has_overlord = event_target:FirstSleeper
has_overlord = event_target:SecondSleeper
}
2. Code Protection Logic
Unlike other formatters that delete code context, this formatter treats your code as text, preserving crucial elements:
- Preserves Comments:
# Comments are protected and restored exactly where they were.
- Preserves Strings: Strings like
name = "don't split { here }" are safe from accidental formatting.
- Re-indents raw blocks: the contents of
switch/inverted_switch blocks are script, so they are re-indented line by line - a body line left at column 0 (or indented with spaces) no longer stays collapsed through every run. Template-style blocks (resource_terms, in_breach_of, discrete_terms) are still kept verbatim, because their leading whitespace can be meaningful.
Allows formatting of just a specific block of code without touching the rest of the file.
- Shortcut:
Ctrl + K, Ctrl + F (or Cmd + K, Cmd + F on Mac)
- A selection keeps its own base indentation (taken from the first selected line) and its blank lines, so formatting an event's
trigger = { ... } block in place leaves the rest of the file untouched.
- Selections run the same logic conversions as whole documents (
scope? folding, if → OR, NOR repair): the block is wrapped, formatted by the Python tool, then re-based onto the selection's indentation so only the selected lines change.
- Selections that cut through a block (unbalanced braces) are only re-indented, and any failure falls back to that built-in re-indenter.
- Regression test:
node test/extension_range_formatter.test.js
4. Advanced Logic Optimization (NAND)
The extension can now recognize and simplify complex logical expressions, such as nested NAND blocks, into a more readable and efficient format. This is particularly useful for complex AI logic or event scripting.
Optional - off by default. The conditional rewrites in this section - a trigger if into the OR implication, and complementary OR branches back into if/else - only run when the paradox-formatter.conditionalConversions setting is enabled, and the exactly one of {A, B} fold when paradox-formatter.calcTrueIfConversions is on (see section 5). The NAND/NOR merging and the scope handling described below are always on.
Inside allow, potential, destroy_trigger and trigger blocks a trigger-side conditional is rewritten as the equivalent implication: if = { limit = L body } means "if L then body", which is exactly OR = { NOT = { L } body }. A limit that is already a negation is negated through its positive form instead of being wrapped again - limit = { NOR = { ... } } becomes the OR that the NOR denies, so the rewrite never produces NOT = { NOT = ... }, NOT = { NOR = ... } or a NAND stacked on a NAND (00_agreement_term_values.txt line 879, where the limit is a NOR of two ANDs and the body a NOT, and the result is an OR of the two ANDs plus that NOT). The shared conditions of those OR branches are factored into one AND - OR = { AND = {A B C} AND = {A B D} } is AND = {A B OR = {C D}}, plain distributivity, so it holds wherever the OR sits, inside an OR/NOR/NOT as much as in a conjunctive list. Pushing those leaves the other way, out of the OR into the surrounding list, is the half that is only sound when that list is conjunctive, so the hoist keeps its guard (OR/NOR/NOT/counted lists). In a container the formatter does not know (my_scripted_trigger = { ... } and the like) a conditional is rewritten only when it can only be trigger script - its body consisting of trigger leaves (is_owned_by = ..., is_same_value = ..., comparisons, any_*/count_*, logic) - because an effect body can never look like that; scope blocks in the body are recursed into, so a body like federation = { has_federation_perk = ... } counts as trigger too, while an effect verb (set_/add_/kill_/..., every_*/random_*/ordered_*, a *_event, or immediate/hidden_effect/random_list/switch) proves effect script and stops the scan, so the conditional is left as written. An if with an empty body and a following else collapses by where it sits: in trigger script the pair is L OR B (an empty body is true), so it becomes one OR; in an effect it runs the else body when the limit is false, so it becomes one if with the limit negated - if = { limit = { NOT = { L } } B }, through the positive form when the limit already was a negation - with the comments of the empty body moved above it (machine_age_crisis_events.txt line 3571). Inside an effect iterator (every_*/ordered_*/random_*, but not the random_list/random_weight/random_names/random_events look-alikes) a lone if = { limit = L body } dissolves into the iterator's own limit: every_country = { limit = L body } - the two limits mean the same thing (41 such blocks in the 4.4 tree, 3 of them with an else and therefore left alone). An if with an else always stays, because the else has to run for the members the limit rejects. Conditional chains (else_if) keep their form, and on the effect side only the empty-bodied pair above is rewritten - an effect if that has a body is never touched - and the whole rewrite is independent of the safe navigation setting. A conditional that holds a tooltip (custom_tooltip or its text key, anywhere in the limit or body) is always left as written, because tooltips exist in trigger and effect script alike and moving one into an OR branch - or into the negated limit - would change when it is shown.
Two spellings of one leaf are one leaf: has_trait = "trait_mechanical" and has_trait = trait_mechanical are the same trigger (quotes are only token delimiters), so a branch pair that spells it differently still factors. Vanilla's every_owned_pop_group limit in crisis_events_3.txt (line 599) quotes it on one side and writes it bare on the other; while that leaf went unrecognised the OR came out as an if/else that repeated the trait in both bodies. With the leaf factored out, an OR holding a negation and that negation's own positive form reduces by itself - (T AND NOT S) OR (T AND S AND NOT C) is T with NAND = { S C } - and the NAND wins over an if/else, because a trigger list has no use for an effect conditional. An arm the factoring empties is spliced out of its AND wrapper before the if/else rule looks at the arms, and an else that ends up empty is dropped instead of written.
Alongside that, the formatter repairs a specific dead leftover that older builds of this extension used to write: NOR = { exists = x x = { C... } }. While x exists the guard makes the NOR false, and while it is missing the block cannot be true, so the script behind it was really the negated, guarded block. Older builds wrote the NOR where they meant NOT, so the repair puts the NOT back and folds the guard into NOT = { x? = { C... } } - the very form this tool writes for that input itself, so running it again changes nothing:
NOR = {
exists = event_target:MSI_country
event_target:MSI_country = { allows_slavery = yes }
}
becomes
NOT = {
event_target:MSI_country? = { allows_slavery = yes }
}
Only that exact shape is repaired - exists = x plus a block on the same scope holding a single condition - so valid idioms like NOR = { exists = archaeological_site has_planet_flag = y } and blocks with several conditions stay untouched. Note it is a repair rather than a rewrite: the NOR form is dead while the scope exists, and the negated, guarded block is what the script was meant to say.
Before:
allow = {
if = {
limit = { exists = orbital_defence }
has_starbase_size >= starbase_starport
}
}
After:
allow = {
OR = {
NOT = { exists = orbital_defence }
has_starbase_size >= starbase_starport
}
}
Before:
limit = {
OR = {
AND = {
NOT = { exists = owner }
OR = {
is_active_resolution = "resolution_rulesofwar_reverence_for_life"
is_active_resolution = "resolution_rulesofwar_independent_tribunals"
is_active_resolution = "resolution_rulesofwar_last_resort_doctrine"
is_active_resolution = "resolution_rulesofwar_demobilization_initiative"
}
}
AND = {
exists = owner
owner = {
is_crisis_faction = no
NOT = { has_been_declared_crisis = yes }
}
OR = {
is_active_resolution = "resolution_rulesofwar_reverence_for_life"
is_active_resolution = "resolution_rulesofwar_independent_tribunals"
is_active_resolution = "resolution_rulesofwar_last_resort_doctrine"
is_active_resolution = "resolution_rulesofwar_demobilization_initiative"
}
}
}
}
After:
limit = {
OR = {
is_active_resolution = "resolution_rulesofwar_reverence_for_life"
is_active_resolution = "resolution_rulesofwar_independent_tribunals"
is_active_resolution = "resolution_rulesofwar_last_resort_doctrine"
is_active_resolution = "resolution_rulesofwar_demobilization_initiative"
}
NAND = {
exists = owner
owner = {
OR = {
is_crisis_faction = yes
has_been_declared_crisis = yes
}
}
}
}
5. Conditional Conversions (optional, beta, off by default)
The conditional rewrites described in section 4 are behind settings of their own, because they change how a script reads, not just how it is formatted. The rewrites themselves come with paradox-formatter.conditionalConversions; the exactly one fold has a second switch, paradox-formatter.calcTrueIfConversions, because it is the one rewrite that creates a counted trigger instead of re-spelling conditionals that are already there:
Beta. The group is still being tuned: it can reorder conditionals and rewrite them between forms, so review the result before keeping it.
// .vscode/settings.json
{
"paradox-formatter.conditionalConversions": true, // default: false
"paradox-formatter.calcTrueIfConversions": true // default: false
}
| Direction |
Conversion |
if → OR |
if = { limit = L body } becomes OR = { NOT = { L } body } (L implies body). |
if/else → OR |
an if with an empty body followed by else = { B } is L OR B (an empty body is true), so the pair collapses into a single OR. |
OR → if/else |
OR = { AND = { A ...B } AND = { NOT = { A } ...C } } becomes if = { limit = { A } ...B } else = { ...C }, which evaluates A only once. |
if/if → if/else_if |
when the second if's limit is the full negation of one condition of the first (limit = { A B } then limit = { NOT = { A } }), the second moves in front and the first becomes an else_if without A - so the trigger leaf A is evaluated once, by the if. |
break = yes → the else side |
if = { limit = A body break = yes } <rest> runs body and stops the block while A holds, so <rest> is really the else branch (early Stellaris had no else_if, so the idiom is everywhere). A run of break conditionals becomes an if/else_if chain - and when script follows that run, the chain gets an else holding it, since that script is exactly what runs when no branch matched (precursor_events.txt artifact sellers; an else the chain already ends in is used for it, and script behind that goes inside too). A break whose conditional is followed by else_if/else is already in a chain, so that chain is never wrapped up: with nothing behind the chain the break is redundant and simply goes (crisis_events_2.txt line 2092), and with script behind the chain the chain moves under an else with its leading else_if rebased to an if, which keeps the chain valid and its guards intact. A lone break whose conditional has nothing else in its body is a guard, not a chain link: it is dropped and its condition negated onto the script that follows, because the following conditional cannot become an else_if - the script behind that would then run even when the guard held. An else behind such a guard is that script, so the pair collapses into one inverted if holding the else's body (horizonsignal_events.txt akx.9470), and anything behind the else comes along, since the break skipped that too. A trailing break in the else of a chain goes the same way when the chain is the last thing in its block - the closing brace already stops it (leviathans_events_2.txt line 4232) - while script after the chain is genuinely skipped by that break and keeps it. |
else wrapping a chain |
an else whose whole body is comments plus one complete if/else_if/else chain is that chain one level deeper - it runs exactly when the condition above it failed. else = { # c if = { limit = { B } ... } } becomes else_if = { limit = { B } ... }, so hand written files flatten into the same chain the break = yes rule above produces. A raw block behind the chain (switch, which the parser keeps as raw text rather than a node) counts as script, so an else holding one is left alone - hoisting it would drop the switch out of the else and run it in every case (crisis_events_2.txt line 2649). |
exactly one → calc_true_if |
OR = { AND = { not A B } AND = { A not B } } says exactly one of the two holds, which calc_true_if = { amount = 1 A B } says in one node instead of three - and it is the only spelling for three or more members, where the written-out form needs one AND per member. Both spellings of that expression are recognised, so either input lands in the same place: the OR above, and the trigger if/else pair an earlier build wrote for it (if = { limit = { A } B } else = { C } means "if A then B, if not A then C", which counts once C negates B - amount = 1 when exactly one of A/B is negated, amount != 1 when both or neither is, the "both or neither" reading). The negated half (NOR over the same arms, "not exactly one") becomes amount != 1, and a following else_if whose count is the exact opposite of the if's drops its limit to become an else. amount = 1 is an exact comparison from Stellaris 2.1 on - before that the built-in tooltip read "If at least $AMOUNT$ of the following are true" and = behaved as >=, which is where the wiki's "at least" phrasing comes from and why vanilla only ever wrote amount >= N. paradox-formatter.xorForm (or --xor-form) picks the spelling: calc_true_if (the default) writes the count, or_nand writes the version-safe pair, and auto mirrors the file - the compact form only when the document already writes an amount >= ... comparison, the OR/NAND pair otherwise. Set or_nand (or auto) when formatting files for a pre-2.1 Stellaris. Effect-side if/else is never counted: there the pair runs one of two bodies and has no count reading. This row has a switch of its own - paradox-formatter.calcTrueIfConversions (or --calc-true-if-conversions/--no-calc-true-if-conversions) - because it is the only rewrite here that creates a counted trigger: with it off, the OR above is left to the row below (an if/else when conditionalConversions is on, the OR as written when it is off too). |
With the setting off - the default - none of them runs: a trigger if keeps its if, complementary OR branches keep their OR, and an if/if pair keeps its form. The exactly one fold is switched independently: it can run while conditionalConversions is off (leaving everything else as written), and with it off the OR it would claim falls back to the OR → if/else rule, so an exactly one written as OR arms still folds with conditionalConversions alone. Everything else is unaffected, in particular the NAND/NOR merging, the scope? folding, the NOR repair and the effect-side if/else_if → else fold. The settings are independent of paradox-formatter.safeNavigation and apply to whole-document formatting and to selections alike. The standalone bin/logic_optimizer.py script (used by tools like mod-updater4.4.py) defaults both groups on - a bare python bin/logic_optimizer.py run keeps them - and --no-conditional-conversions / --no-calc-true-if-conversions turn them off; only the extension defaults them off, by passing those flags when its settings are off.
The if/if → if/else_if reorder only runs on the effect side (a trigger if is read as the implication shorthand), only for the next if in the same block, and only when the two bodies are mutually exclusive - which is exactly the negated-condition case. The break = yes rewrites are effect-side too, and only where the stop is local: a break inside a while ends the loop and one inside an inline_script stops the caller's block, so both are left as written - and because a break otherwise unwinds through the scope blocks around it, a rewrite that moves the following script into an else only runs while every container between that list and the next fresh effect body (EFFECT_ROOTS - immediate, option, after, effect, success, fail, abort, on_success, on_fail, on_start, on_complete, on_cancel, on_monthly) ends with that conditional. So if = { limit = L body break = yes } <rest> folds when rest really is all the break skips, and the overlord = { if = { … break = yes } … } inside an if that is followed by more script is left alone (marauder_events.txt line 9675, where folding it would fire the retry event twice). When a break is followed by an else that itself has script after it, that script is moved into the else and a warning is printed, because the original order was already inaccurate. An else is only flattened when nothing but comments and the chain sits in it - an else with any other script in it keeps its form, since its first branch would otherwise run unconditionally.
Comment lines are part of the script, so all of these rewrites read past them instead of giving up on the conditional, and leave them where they are - distant_stars_events_2.txt has a break dispatch chain and on_action_events_1.txt a cascade of pop_amount branches whose every branch is introduced by its own comment, and both come out as a single chain with all of those comments still in place. A comment written on the break line itself (break = yes # never comes back) moves onto the script in front of it, so nothing is lost.
As with the rest of the aggressive scope handling, these rewrites assume every scope exists, which is why they are opt-in rather than always on.
- Regression test:
node test/extension_conditional_conversions.test.js (asserts the setting reaches the Python tool as --conditional-conversions).
You can trigger a bulk formatting operation across the workspace using the PDX Formatter: Format all files command - it walks **/*.txt (skipping node_modules, .git, out and bin) and reports its progress per file.
7. Safe Navigation Support (Stellaris v4.4+)
Enable or disable the four handling modes for the Stellaris v4.4+ safe navigation syntax (xyz? = { ... }) with the paradox-formatter.safeNavigation setting:
| Mode |
Behaviour |
auto (default) |
Mirror the file's own style: files that already use safe navigation get their exists = xyz + xyz = { ... } pairs folded into xyz? = { ... }; files without it are left untouched. |
fold |
Always create safe navigation: exists = xyz + xyz = { ... } becomes xyz? = { ... } (comments may sit in between, and so may at most three plain positive leaves - none of them using xyz, a scope block, or a negation); the scope-less has_owner = yes guard is folded the same way into an owner/space_owner block. exists = xyz + NOT = { xyz = { ... } } and OR = { NOT = { exists = xyz } xyz? = { NOT = { C... } } } leftovers are folded too (see the notes). Targets Stellaris v4.4+. |
revert |
Always expand safe navigation back to exists = xyz + xyz = { ... }; inside known effect scopes the pair is wrapped in if = { limit = { exists = xyz } ... }. Targets pre-4.4 Stellaris. |
ignore |
Never touch safe navigation - neither fold nor revert. |
// .vscode/settings.json
{
"paradox-formatter.safeNavigation": "fold" // "auto" | "fold" | "revert" | "ignore"
}
Notes:
- Folding only happens in conjunctive lists (never inside
OR/NOR/NOT/calc_true_if), and only when dropping the guard cannot lose anything: the scope block either follows directly (comments aside) or at most three plain trigger conditions sit in between - a leaf or a whole block, as long as none of them touches that scope (is_owned_by = from, a nested from = { ... }) and none is a negated leaf (= no). So exists = from + is_owned_by = from + from = { ... } keeps its guard, exists = from + has_star_flag = x + from = { ... } folds, exists = from + species = { has_trait = ... } + from = { ... } folds too (the block is one condition, the guard gates no sibling, and from? = { ... } re-asserts the existence itself), and exists = from + four conditions + from = { ... } keeps its guard. A redundant exists = xyz in front of an existing xyz? = { ... } is removed.
exists = xyz + NOT = { xyz = { C... } } becomes xyz? = { not C... } (NAND instead of NOT when the block holds several conditions), because xyz = { ... } can only be true when the scope exists, so exists AND NOT(C...) is exactly exists AND NOT(xyz = { C... }), and the inner negation is collapsed onto its positive form.
- The same holds for the disjunctive form:
OR = { NOT = { exists = xyz } xyz? = { NOT = { C... } } } (the long way other tools and hand conversions write NOT = { xyz = { C... } }) collapses into NOT = { xyz? = { C... } }, since not(exists xyz) OR (xyz exists AND not C...) is not(xyz exists AND C...). The inner negation may be a NOT, a NOR/NAND or a = no/!= leaf; anything else (un-negated content, a different scope, extra children) is left alone. A redundant exists = xyz in front of NOT = { xyz? = { C... } } is merged into xyz? = { NOT = { C... } }.
- Every scope is assumed to exist, so a negation is pushed into a scope block like any other wrapper:
NOT = { xyz = { C... } } becomes xyz = { not C... } (NOR when the content is an OR, e.g. NOT = { xyz = { OR = { a b } } } becomes xyz = { NOR = { a b } }). A scope block therefore also counts as the negation of its positive form, so X = { NOT = { k } } merges into a NAND/NOR as if it were not X = { k }. This is the historic (pre-0.5.9) behaviour and is only exact while the scope exists. A block trigger that compares a number negates on that comparison and keeps its selector: NOT = { has_resource = { type = volatile_motes amount > 0 } } is has_resource = { type = volatile_motes amount <= 0 } (02_scripted_triggers_federations.txt).
- Two negations that wrap a whole scope block cancel:
NOT = { xyz = { NOT = { C... } } } is xyz = { C... }, because while xyz exists not(not C...) is just C... - whatever C... is (a leaf, a scope block, or an any_*/count_* trigger block, which is moved as it stands since a NOT is never pushed into one). A comment on either vanishing NOT node stops the fold (dropping the node would lose it), while a comment on the scope block itself is kept as written.
- Comments always travel with the block they end up in: a comment that sat on a negation (
NOR = { # c ... }) follows it when the negation is unwrapped, merged into a sibling NOR/NAND or hoisted out of a nested block - it is never dropped and never duplicated, in either direction.
- A
xyz? = { ... } node is never negated (exists AND ... cannot be negated by flipping only its inner value), and trigger-side conditionals (if = { limit = L body }, i.e. L implies body) are never folded into scope? = ... - inside allow, potential, destroy_trigger and trigger blocks they are rewritten as OR = { NOT = { L } body } instead (see section 4). For the same reason a guard inside a NAND/NOT is folded into a ? block rather than kept separate: those containers AND their children and negate the whole, and a ? block keeps the existence inside that same conjunction, so NAND = { exists = xyz xyz = { C } } and NAND = { xyz? = { C } } both read not(E and C). The two-child shape comes out as NOT = { xyz? = { C } } (a one-child NAND is a NOT), and when safe navigation is not wanted it expands into OR = { NOT = { exists = xyz } xyz = { not C } } (or stays as written when the block cannot be negated). What must never happen is collapsing the NAND itself onto the block: that turns not(E and C) into E and C. It never becomes xyz? = { not C }, which reads E and not C.
- When reverting, the
if = { limit = { exists = xyz } xyz = { ... } } wrapper is written inside known effect scopes and in an unknown container whose block is effect script; everywhere else (trigger scope, and unknown containers with trigger-only content) the flat exists = xyz + xyz = { ... } pair is used.
- The reverse holds too:
if = { limit = { exists = xyz } xyz = { ... } } folds back into xyz? = { ... } inside an unknown container - a scripted effect such as create_voidworm_system_poi carries no name that marks it as effect, so the conditional's own body decides, and only a body that proves effect script (set_/add_/..., an effect container, a nested conditional whose body proves it) is folded: trigger evidence (owner = { is_ai = yes }, a negation, has_*/is_*/uses_*) leaves the if to the OR rule above, and a body that proves nothing stays exactly as written - the absence of trigger evidence is not proof of effect (00_rules.txt). custom_tooltip = { ... } is a trigger container (1835 of them hold trigger content in the 4.4 script tree, none an effect verb), so its children are trigger script and are never folded this way; tooltip = { ... } is the effect one (356 blocks, no trigger-only ones). $MACRO$ placeholders are ignored when a scope name is matched, and dotted paths are matched link by link (root.owner), so exists = event_target:sys_$SIZE$ + event_target:sys_$SIZE$ = { ... } folds as well.
- Migration: the old boolean
paradox-formatter.useSafeNavigation option was replaced by this setting - useSafeNavigation: true corresponds to safeNavigation: "fold".
8. Diagnostics (CLI)
--check-indent reports every line whose indentation does not match its block depth (the tool's rule is one tab per level), for the input and for the result:
py bin/logic_optimizer.py --safe-navigation fold --check-indent < yourfile.txt
- stderr lists the input's offenders as
input line N: <tabs> tab(s), expected <depth> | <text> plus a summary N line(s) in the input, M left after formatting;
- the JSON output carries them as
indent_issues (input) and indent_issues_after (what the formatter left), so a script can act on them.
Anything still listed after formatting is a construct the renderer does not model (or a line that needs another pass) - useful to audit a whole mod with a single command.
--with-vfix tags every repair of broken syntax with an inline # Vfix comment, so a hand-checked file shows where the tool had to fix something. It is a dev switch for running the CLI by hand - the extension never passes it, so normal output is unchanged:
| repair |
input |
tagged output |
dead NOR leftover (a NOT renamed by an old bug) |
NOR = { exists = X
X = { C } } |
NOT = { # Vfix |
any_ trigger holding a count (invalid script) |
any_owned_planet = { count = 2 ... } |
count_owned_planet = { # Vfix |
| closing brace missing at end of file |
truncated file |
} # Vfix per invented brace |
The tag is appended after the comment the node already carries (NOR = { # old bug → NOT = { # old bug # Vfix), it is written once (a second run does not double it) and it stays until you delete it. A stray } is only reported on stderr - nothing survives that could carry a note.
py bin/logic_optimizer.py --safe-navigation fold --with-vfix < yourfile.txt
9. Hints (CLI and extension)
Some shapes are recognised and deliberately left alone. Instead of rewriting them, the tool reports them - each one names the file it came from and quotes what it found, so it can be located without searching:
common/scripted_effects/my_effect.txt: nested 'if' inside an 'if' with an 'else' - limits not merged: limit = { has_ethic = x }
The one shape reported today is a nested conditional that cannot be flattened - an if whose whole body is another if, while the outer one has an else:
if = {
limit = { A }
if = { limit = { B } body } # only node child of the outer body
}
else = { other } # ... but the outer one has an else
The two limits cannot be merged here: the else covers "A is false", and a merged limit = { A B } would make it cover "A holds and B does not" as well, so other would start running in a case where it does not run today. Flatten the inner conditional by hand, or move the else somewhere it belongs. An else_if chain is not reported - each branch carries its own limit, which is a dispatch pattern rather than a nesting mistake.
🚀 Installation
Manual Installation (.vsix)
You can install the packaged extension directly using the .vsix file.
- Download the
paradox-script-formatter-0.6.8.vsix file.
- Open VS Code.
- Go to the Extensions View (
Ctrl+Shift+X).
- Click the three dots icon (...) at the top-right of the Extensions menu.
- Select "Install from VSIX...".
- Locate and select the
paradox-script-formatter-0.6.8.vsix file.
Alternatively, you can install it via the command line:
code --install-extension paradox-script-formatter-0.6.8.vsix
Supported File Types
Automatically activates for:
.txt (Paradox Script) and .mod (mod descriptor files)
- Language IDs:
paradox, stellaris, hoi4, ck2, eu4, imperator, vic2, ck3, vic3, eu5
Syntax highlighting ships for paradox, stellaris, hoi4, ck2 and eu4; the other IDs activate the formatter only, since their highlighting comes from whichever extension defines them. .gui interface files are formatted as well once another extension maps them onto one of those language IDs - this extension itself claims .txt and .mod.
⚙️ Configuration
To ensure this formatter is used automatically when you save a file, you need to set it as the default formatter for the Paradox language.
- Open your Settings (
Ctrl + ,).
- Search for
default formatter.
- You can set the formatter globally or specifically for Paradox files in your
settings.json:
{
// Sets the PDX Formatter as the default for all languages where it applies
"editor.defaultFormatter": "f1r3pr1nc3.paradox-script-formatter",
// Optional: rewrite conditionals between forms, e.g. `if`/`else` <-> `OR` (BETA, see section 5)
"paradox-formatter.conditionalConversions": false,
// Optional: fold a fully written out `exactly one of {A, B}` into `calc_true_if` (BETA, see section 5)
"paradox-formatter.calcTrueIfConversions": false,
// Optional: how a fully written out `exactly one of {A, B}` is spelled (BETA, see section 5)
"paradox-formatter.xorForm": "calc_true_if",
// Recommended: Set preferred indentation style (if not using .editorconfig)
"editor.insertSpaces": false, // Use tabs
"editor.tabSize": 4
}
To automatically format code when pasting, add these settings to your settings.json:
"[paradox]": {
"editor.defaultFormatter": "f1r3pr1nc3.paradox-script-formatter",
"editor.formatOnPaste": false
},
"[stellaris]": {
"editor.defaultFormatter": "f1r3pr1nc3.paradox-script-formatter",
"editor.formatOnPaste": false
}
Troubleshooting Tips:
If it doesn't format automatically, check the global setting: Search settings for Format On Paste and ensure it is checked.
🤝 Contributing
Contributions are welcome! If you find a case where the formatter breaks a specific script structure, please open an issue with a code snippet.
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature)
- Commit your Changes (
git commit -m 'Add some AmazingFeature')
- Push to the Branch (
git push origin feature/AmazingFeature)
- Open a Pull Request
📜 License
Distributed under the MIT License. See LICENSE for more information.