rimpatch
Find RimWorld PatchOperations that no longer resolve, without launching the game.
RimWorld applies your Patches/**/*.xml to the combined Def tree at load time. When a
def you target gets renamed or moved, your <Operation> silently matches nothing and
the only signal is a line buried in the player's log:
Failed to find a node with the given xpath: Defs/ThingDef[defName="Gun_Autopistol"]/statBases
You find out when a player opens an issue. rimpatch assembles the same Def tree the game would assemble, runs every operation against it in load order, and tells you which ones match zero nodes, with file, line, mod, class and xpath. No game launch, no C#, no bundled game data.
Install
pip install rimpatch
Python 3.11+. Pulls in lxml and click.
Use it
Stand in your mod folder and run it. rimpatch treats the current folder as the mod and finds your RimWorld install on its own, including Steam libraries on other drives:
$ cd MyMod
$ rimpatch check
rimpatch: note: checking the current folder as a mod: D:\Mods\MyMod
rimpatch: note: using the RimWorld install at D:\SteamLibrary\steamapps\common\RimWorld
0 findings (2830 operations checked)
Vanilla defs are read from that install. rimpatch never ships game data. If detection guesses wrong or finds nothing, say where things are:
rimpatch check --game "C:/Program Files (x86)/Steam/steamapps/common/RimWorld" --mods ./MyMod
rimpatch where prints what it found, and when it finds nothing, every path it tried.
In CI, where the game is not installed, check the checkout against sibling checkouts of
the mods it declares in About.xml:
rimpatch check --repo .
Exit code is 0 when clean, 1 when there are findings, 2 when rimpatch itself could not run (bad path, unreadable ModsConfig).
Commands and options
rimpatch check |
Run the operations and report what misses |
rimpatch mods |
Print the resolved load order and each mod's content folders |
rimpatch where |
Show the install and ModsConfig.xml it can find |
--game PATH |
RimWorld install root. Found automatically if omitted |
--no-game |
Do not look for an install at all |
--mods PATH |
A mod folder, or a folder of mod folders. Repeatable, order is load order |
--repo PATH |
Check this checkout and resolve its modDependencies from sibling folders |
--mods-config PATH |
Take the active list and order from a ModsConfig.xml, or auto to find it |
--no-auto-order |
Use the order given instead of the mods' declared rules |
--game-version 1.6 |
Version used for LoadFolders and version subfolders |
--format text|json|github |
Output format |
--strict |
Also report operations whose <success> mode hides the failure |
--no-warnings |
Hide warnings |
--exit-zero |
Always exit 0 |
--baseline FILE |
Accept the findings recorded in FILE, report only new ones |
--write-baseline FILE |
Record the current findings and exit 0 |
Environment: RIMWORLD_DIR and RIMWORLD_CONFIG_DIR point detection straight at an
install or a Config folder. RIMPATCH_NO_AUTODETECT=1 turns detection off entirely, so
a run cannot depend on what happens to be installed. Set it in CI for reproducibility.
The worked example
Combat Extended's Patches/Core/Stats/Stats.xml opens with an operation that expects
Core's Flammability StatDef. Run Combat Extended on its own, with no Core, and that
first operation has nothing to land on:
$ rimpatch check --mods ./CombatExtended
Patches/Core/Stats/Stats.xml:6 PatchOperationAdd matched 0 nodes
xpath: Defs/StatDef[defName="Flammability"]
deepest match: Defs (1 node) - no StatDef with defName="Flammability" among active mods
mod: CETeam.CombatExtended
... one of these for every Core def Combat Extended expects ...
1401 findings in 96 files (1459 operations checked, 4.3s), 52 operations from mod assemblies not evaluated
deepest match is the point where your assumption stopped being true. Here nothing
below Defs matched, so the StatDef itself is missing. Add the game and the same run is
clean:
$ rimpatch check --game "D:/SteamLibrary/steamapps/common/RimWorld" --mods ./CombatExtended
0 findings (2830 operations checked), 68 operations from mod assemblies not evaluated
$ echo $?
0
Those 2,830 operations are Combat Extended's real Patches tree against a real RimWorld 1.6 install with all five expansions, and every one of them resolves.
What a real break looks like
From a 242-mod load order on a live install. Alpha Animals renamed a base def, so a
patch in Rim of Madness - Bones lands nowhere and takes 75 later operations with it,
because PatchOperationSequence stops at the first child that fails:
Patches/BonelessAlphaAnimals.xml:23 PatchOperationAdd matched 0 nodes
xpath: /Defs/ThingDef[@Name="AA_PseudoBaseMechanoid"]/statBases
deepest match: Defs (1 node) - no ThingDef with @Name="AA_PseudoBaseMechanoid" among active mods
in: PatchOperationFindMod > PatchOperationSequence > PatchOperationAdd
skipped as a result: PatchOperationAdd (line 30), PatchOperationAdd (line 36), PatchOperationAdd (line 42), PatchOperationAdd (line 48), PatchOperationAdd (line 54), and 71 more
mod: sihv.rombones
When a def looks renamed rather than gone, you get candidates:
Patches/ButcherRotten.xml:3 PatchOperationReplace matched 0 nodes
xpath: Defs/RecipeDef[defName="ButcherCorpseRotten"]/recipeUsers
deepest match: Defs (1 node) - no RecipeDef with defName="ButcherCorpseRotten" among active mods
did you mean: ButcherCorpseFlesh, ButcherCorpseMechanoid
mod: sihv.rombones
GitHub Action
action.yml at the repo root installs the package and runs rimpatch check --format github, so findings land as inline annotations on the changed lines.
name: Patches
on: [push, pull_request]
jobs:
rimpatch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Check out whatever your About.xml declares as a modDependency, next to your mod.
- uses: actions/checkout@v4
with:
repository: CombatExtended-Continued/CombatExtended
path: ../CombatExtended
- uses: Booyaka101/rimpatch@v1
with:
repo: .
Inputs: repo, mods (one path per line), game, mods-config, game-version,
baseline, strict, warnings, fail-on-findings, version, python-version.
Outputs: findings (count) and report (path to the JSON report).
Without a RimWorld install on the runner, vanilla defs are absent, so operations targeting Core will legitimately report as unresolved. Either check out the mods you patch as siblings, or scope the action to a mod that only patches other mods.
Adopting it on a mod that is already broken
A mod with a dozen stale patches would go red the day you add this, and a permanently red check gets deleted rather than fixed. Record what is already broken and CI will only fail on what you break next:
$ rimpatch check --write-baseline rimpatch-baseline.json
wrote 12 finding(s) to rimpatch-baseline.json. Commit it, then pass --baseline to report only what is new.
$ rimpatch check --baseline rimpatch-baseline.json
0 findings (412 operations checked), 12 accepted by the baseline
Commit the file. From then on a new break comes through on its own:
$ rimpatch check --baseline rimpatch-baseline.json
Patches/Guns.xml:14 PatchOperationAdd matched 0 nodes
xpath: Defs/ThingDef[defName="Gun_Autopistol"]/statBases
...
1 finding in 1 file (413 operations checked, 0.4s), 12 accepted by the baseline
Fix one and rimpatch tells you the entry is dead: 2 baseline entries no longer needed.
Regenerate with --write-baseline to clear them. Entries are matched on mod, file,
class and xpath but deliberately not on line number, so adding a comment above an
operation does not silently un-baseline everything below it.
The action takes the same file via the baseline input.
Getting the load order right
Which patches can resolve depends entirely on what loaded before them, so rimpatch takes load order seriously.
--mods-config autois the exact answer: the active list and order the game itself will use, found for you. Pass a path instead if you keep it somewhere unusual.rimpatch whereshows which one it picked.- Otherwise official content loads first, then your mods are ordered by what they
declare:
modDependencies,loadAfter,loadBeforeand theforce*variants. Anything the mods do not constrain keeps the order you gave.--no-auto-orderturns this off and uses your order verbatim. rimpatch mods ...prints the resolved order and each mod's content folders, which is the fastest way to see why something did or did not load.
What it models
Every one of these is a place a naive checker would invent failures that the game never reports:
| Behaviour | What rimpatch does |
|---|---|
PatchOperationSequence |
Runs children in order, stops at the first failure, reports that child and lists the ones skipped as a result |
PatchOperationTest |
Reports success or failure, mutates nothing |
PatchOperationConditional |
Its own xpath matching nothing is normal, not a finding; runs <match> or <nomatch> |
PatchOperationFindMod |
Taking the <nomatch> branch because the named mods are inactive is normal, not a finding |
<success>Always</success> |
A failure the author asked to ignore, suppressed unless --strict |
<success>Invert</success> |
Failure is the intended result, handled as the game does |
MayRequire / MayRequireAnyOf |
Gated nodes are dropped at load time, before any xpath runs, in both defs and patch files |
LoadFolders.xml |
IfModActive / IfModNotActive are an OR, IfModActiveAll / IfModNotActiveAll an AND |
| Def inheritance | Not resolved, because the game applies patches before resolving ParentName. A patch cannot see an inherited field, and rimpatch cannot either |
| Relative xpaths | Defs/StatDef[...] is evaluated from the document node, as XmlDocument.SelectNodes does. Roughly 40% of real-world patch xpaths are written this way |
All twelve operation classes are implemented: Add, Insert, Remove, Replace,
AttributeAdd, AttributeSet, AttributeRemove, AddModExtension, SetName,
Sequence, Test, Conditional, plus FindMod.
Findings and warnings
Findings set the exit code. Kinds: no-match, bad-xpath (the xpath does not parse),
bad-operation (a class is missing something it needs), unknown-operation (a
misspelled built-in such as PatchOperationsequence), parse-error (malformed XML,
reported with file and line while every other file still loads).
Warnings never affect the exit code and are hidden with --no-warnings: duplicate
defNames across active mods, a patch file whose root is not <Patch>, a
PatchOperationSequence with an empty <operations> list, unreadable About.xml.
Output formats
--format text (default), --format json for tooling, --format github for
annotations:
::error file=Patches/Core/Stats/Stats.xml,line=6,title=PatchOperationAdd::matched 0 nodes%0Axpath: Defs/StatDef[defName="Flammability"]%0A...
The JSON report carries the full skipped list, the diagnosis, and a summary with
operationsChecked, defsLoaded, nodesGatedByMayRequire and the mods in load order.
Limitations
- Operation classes provided by mod assemblies, such as
CombatExtended.PatchOperationMakeGunCECompatible, cannot be evaluated without running C#. They are counted in the summary and never reported as findings. - rimpatch checks whether an xpath resolves. It does not check that the value you patch
in is a field the game's classes actually have; that needs
Assembly-CSharp.dll. PatchOperationFindModmatches on mod name, as the game does, and falls back to packageId.- Vanilla defs come from your own install via
--game. No game data is bundled. - Roughly a second per 50 operations against a 46,000-def tree. A single mod against Core plus expansions is a few seconds; a 242-mod load order is about a minute.
Tests
python -m pip install -e ".[dev]"
python -m pytest # everything
python -m pytest -m "not integration" # skip the test that fetches Combat Extended
The integration test pulls Combat Extended's live Patches tree from GitHub and asserts every operation in it parses and evaluates without raising. It skips itself if GitHub is unreachable.
First distribution step
Post it in the RimWorld modding Discord's #mod-development
channel and on /r/RimWorldMods, replying to one of the open "Failed to find a node with
the given xpath" issues with the exact finding rimpatch produces for it. The people
filing those issues are the users; showing the tool naming the broken line beats
describing it.
License
MIT.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rimpatch-1.0.0.tar.gz.
File metadata
- Download URL: rimpatch-1.0.0.tar.gz
- Upload date:
- Size: 46.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
19472f473472ae4a4a9f672561e5f07cdcf5227961ddf62666e79035ef75c0f2
|
|
| MD5 |
a90a93d838187b459c5ce343fb6f6984
|
|
| BLAKE2b-256 |
d87a6e31ec93664b637b23590aced57e2b6276fcfe240294f8798423fd0a5e6b
|
File details
Details for the file rimpatch-1.0.0-py3-none-any.whl.
File metadata
- Download URL: rimpatch-1.0.0-py3-none-any.whl
- Upload date:
- Size: 36.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
829e681d398027efd5cc48edad3df1e0b02c3f6e7d6bb78f4dade0a38fd5ab31
|
|
| MD5 |
f512963d9b231b032a8707c8037e0ac0
|
|
| BLAKE2b-256 |
15ab3e49a8e34e09d4ac121b7eaf6a89eb97d0efc98d789561dc4f1b5c8488a3
|