x4-update-mod
Port an existing X4 mod to a newer game version (e.g. 7.x → 9.0). Runs the mechanical checks (x4validate sel/refs/completeness + XSD schema validation + the runtime migration-map heuristic) and produc
SKILL.md.claude/skills/x4-update-mod/SKILL.md
GitHub<!-- GENERATED by tools/x4validate/scripts/gen-agent-trees.py from agent/. Do not edit: regenerate. -->
Port ONE mod to the current game version. Mechanical work is automated + validated; design/gameplay decisions are surfaced to the USER — never auto-decided (the two-layer rule).
Tool: cd $CLAUDE_PROJECT_DIR/tools/x4validate && uv run --python 3.13 x4validate <dev\mod> --update
Establish the current situation
Before proposing changes, read ## My preferences in KNOWLEDGEBASE.local.md
and reconcile it with the current request and session context: is this a kept save,
a new game or a disposable test, and are updates, additions, removals or ordering
changes authorized? Assume neither an ongoing playthrough nor a fresh game.
Ask only about material facts that remain unknown or contradictory; a missing
local file alone does not justify repeating an answered question. Continue
independent read-only work while waiting. Apply standing user rulings before
choosing work, and record new lasting rulings, dated, in that user-owned section.
Keep it concise; if session context is truncated, read the full section directly.
These choices do not waive verified backups, write guards or scoped authorization.
Phases
- Research (API-FIRST) — dispatch the
mod-researchagent: game changelog (Egosoft patch notes), the mod's Nexus changelog/version/9.0-status, whether an updated upstream exists, known issues. Cross-checkKNOWLEDGEBASE.md"Version Migration Map". - Mechanical checks — run
x4validate <mod> --updateand read the report:- sel= / refs / completeness / connection — diff patches matching nothing, dangling refs, broken loadout connections, missing companion files.
- XSD (
--update, the migration backbone, ~100s warmup):[error] xsd= "attribute is required but missing" → a REAL breakage (e.g. thespace=family) — fix these.[info] xsd-strict= md.xsd is stricter than the engine (lowercase script/cue names, unknown-but-tolerated attributes) → ADVISORY, usually safe to ignore (the mod runs); only investigate if it actually misbehaves. (See KB: "md.xsd is STRICTER than the engine".) - migration (
[warn] migration) — runtime-only dead APIs the XSD can't see (dead loader entry points, deprecated list accessors, retired UI hooks) — each reported with a fix note; the current catalogue lives in the tool, not here. Heuristic too (x4validate calls it a migration-map heuristic): a note is a SUSPECTED break. Bring each note to the user before changing anything, the same way as exprlint below, and fix only the ones they approve. - exprlint (
[warn]/[info] exprlint) — the script-expression grammar inside attribute values, which XSD treats as opaque strings and cannot see: the deadrandom(min,max)call form, a'…'[…]interpolation missing its dot, a{a,b}list-literal that now parses as a{page,line}textref (→'null' is not a listat runtime),.keys.list.count. Heuristic: a flag is a SUSPECTED break, not a proven one. Bring each flag to the user before changing anything: show the line, the pattern it matched, and what the fix would change, and fix only the flags they approve. It only knows patterns, so it is NOT exhaustive either way.
- Apply mechanical fixes, then CONFIRM against a real load — fix the real errors (required attrs) and the migration notes and exprlint flags the user approved; re-run
--updateuntil theerrorclass is clean and every migration note and exprlint flag is either fixed or left by the user's decision. Then the load-check is MANDATORY before declaring a final fix-count: an XSD/exprlint pass is necessary, not sufficient — it cannot see novel expression breaks or runtime errors. Deploy, load a save, and runx4validate <mod> --debug <profile>\debug.txt(folds the engine's OWN[=ERROR=]lines for this mod into the report and gates — including runtime errors like'null' is not a list). Only a clean--debug(against a log captured after the fixes) closes the port. Validate each fix (and honor the diff-patch / vanilla-as-frame rules). - Design brief (for the USER — research-assisted, NOT automated): per notable feature / custom edit, classify the feature-fate —
still-needed|obsolete(game now does it natively) |moot/superseded(a different game change made it irrelevant) |same-goal-different-impl. Then list the decisions only the user can make (keep/drop/adapt, balance/taste). The schema/API can't read gameplay intent — surface, don't decide. - Custom edits (Scenario B) — if the mod is
markedcustom_edited(registry) or has adev\twin, surface the user's local changes for manual re-apply onto the updated upstream. (Automated 3-way merge is a later capability.)
Honest framing
Mechanical = auto + validated. Gameplay/feature-fate = advisory brief, the user decides. In-game testing + debug.txt (/x4-debug) are still required — a clean --update is necessary, not sufficient. Two validation layers, know their limits: XSD sees XML structure; exprlint is a heuristic over expression values (known patterns only); the --debug correlation is the authoritative layer (the engine's own verdict, incl. runtime errors) — that's why the load-check in Phase 3 is mandatory, not optional. The cautionary tale is on record in the KB Session Logs: a static-only pass on one port reported "2 fixes" while the real in-game load surfaced 5. Every mod is different — read the worked ports in KNOWLEDGEBASE Session Logs as examples, never as a template.