Save migration
X4 has been actively developed since 2018. Player saves from 2020 still load in 2026 builds — even though scripts have evolved across DLCs and patches. This is possible because X4 has a declarative save migration mechanism built into MD and aiscript: the version= attribute and <patch sinceversion> blocks.
This overview explains the mechanism, when it kicks in, and how modders should version their own scripts.
Vanilla MD + aiscript has 1992 sinceversion= references across the codebase — every long-lived feature has migration logic.
The version= attribute
Section titled “The version= attribute”Cues, aiscripts, libraries, and individual parameters can carry a version= attribute:
<cue name="MyCue" version="3"> ...</cue><aiscript name="order.wait" version="8"> ...</aiscript><param name="fidget" type="internal" default="false" version="7"> <patch value="false" sinceversion="7"/></param>version= declares “this is the current schema version”. The engine remembers per-save what version was active when the save was written.
How migration runs
Section titled “How migration runs”When a save is loaded, for every active cue / variable / order, the engine compares:
Engine reads from save: cue's last-active version = 2
Engine reads from current script file: cue's declared version = 4
Engine sees mismatch → look for <patch> blocks<patch sinceversion="N"> blocks declare migration logic:
<patch sinceversion="3"> <!-- runs when migrating from version < 3 to current --> <set_value name="$newField" exact="0"/></patch>
<patch sinceversion="4"> <!-- runs when migrating from version < 4 --> <set_value name="$anotherField" exact="true"/></patch>The engine runs all <patch> blocks whose sinceversion exceeds the save’s last-active version. Multiple patches run in order.
Patch types
Section titled “Patch types”Variable / parameter patches
Section titled “Variable / parameter patches”<param name="newOption" type="bool" default="false"> <patch value="false" sinceversion="3"/></param>When a save from version < 3 loads, the newOption variable is initialised to false — even though the parameter is new.
State patches (advanced)
Section titled “State patches (advanced)”Vanilla finalisestations.xml:
<patch sinceversion="2" state="waiting"> <debug_text text="'Construction sequence generation was interrupted. Triggering again.'"/> <do_if value="$Conn_PlannedConnectors.count"> ... </do_if></patch>state="waiting" restricts the patch to cues that were in waiting state when the save was written. Used for fixing interrupted multi-stage operations.
Default value patches
Section titled “Default value patches”<patch value="true" sinceversion="5"/>When the parameter default changes, this patch updates existing saves to the new default. Without it, old saves keep the previous default forever.
When migration runs
Section titled “When migration runs”| Trigger | Behaviour |
|---|---|
| Save loaded with script version > save version | All applicable <patch> blocks run |
| Save loaded with script version == save version | No patches run |
| Save loaded with script version < save version | Script was downgraded — engine warns, behaviour undefined |
| New cue added to script | New cue starts at its declared version |
Mods that decrease version numbers (downgrade) generally break saves.
Vanilla patterns
Section titled “Vanilla patterns”Pattern: new parameter with safe default
Section titled “Pattern: new parameter with safe default”<param name="newSetting" type="bool" default="false"> <patch value="false" sinceversion="7"/></param>Old saves get newSetting=false. New saves use whatever the user sets.
Pattern: changed param semantics
Section titled “Pattern: changed param semantics”<param name="oldMeaningChanged" type="time" default="60s"> <patch value="60s" sinceversion="5" comment="Old behaviour was instantaneous; new default is 60s"/></param>Document the semantic change in the comment. Patch ensures old saves don’t behave oddly.
Pattern: state recovery
Section titled “Pattern: state recovery”<cue name="MyLongRunningCue" version="3"> <conditions>...</conditions> <actions>...</actions>
<patch sinceversion="3" state="active"> <!-- if a v2 save was mid-execution, fix up state --> <do_if value="not @$expectedField"> <set_value name="$expectedField" exact="default"/> </do_if> </patch></cue>Helps when a long-running cue was in mid-execution when the save was taken.
Modder implications
Section titled “Modder implications”Always start at version 1
Section titled “Always start at version 1”Your first version of a custom script:
<mdscript name="mlog_my_mod" version="1"> ...</mdscript>When you make breaking changes, bump to version 2, 3, etc.
Add <patch> for breaking changes
Section titled “Add <patch> for breaking changes”Don’t just bump the version — also add <patch> blocks:
<param name="threshold" type="integer" default="100"> <patch value="100" sinceversion="2"/></param>Without the patch, old saves crash or behave incorrectly.
Test save compatibility
Section titled “Test save compatibility”After every schema change:
- Load an old save with the OLD version of your mod — confirm works
- Update to NEW version
- Load the same save — confirm migrates cleanly
Without this test, save compatibility silently degrades.
Cross-mod compatibility
Section titled “Cross-mod compatibility”Different mods have independent version numbers. Your mod’s version 5 + someone else’s mod’s version 12 don’t interact — each tracks its own save state.
Don’t decrease versions
Section titled “Don’t decrease versions”A version=2 script later changed to version=1 confuses the engine. Save compatibility breaks. If you need to roll back, fork and ship as a different mod name.
What can’t be migrated
Section titled “What can’t be migrated”Some changes can’t be cleanly migrated:
- Renaming a cue — old saves reference the old name; renaming breaks them
- Removing required parameters — saves still have old values that don’t fit
- Changing variable types (e.g. integer to string) — runtime errors when accessed
For these changes, either:
- Don’t make them (preserve compatibility)
- Accept that old saves can’t load this mod version
Cross-references
Section titled “Cross-references”- Cue —
version=attribute - Order definition —
version=on aiscript root and params - Variable — save persistence
- Setup runs once memory — related lifecycle gotcha
Related architectural overviews
Section titled “Related architectural overviews”- (none yet — Save migration is its own concern)