Skip to content

Instantly share code, notes, and snippets.

@Lanilor
Last active August 14, 2021 12:43
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Lanilor/e36af29ce5ce725b9b29768c63b00ef2 to your computer and use it in GitHub Desktop.
Save Lanilor/e36af29ce5ce725b9b29768c63b00ef2 to your computer and use it in GitHub Desktop.
Random stuff about XML patching

Notes

  • This document is still a draft. Don't rely on any information until it is done and tested.
  • This is a more advanced guide with some special cases, improvements and optimizations. If you need to know the basics, read this guide.

Compatibility patches (with FindMod)

You can use cores PatchOperationFindMod to create conditional patches when certain mods are loaded without the need of an external library.

<Operation Class="PatchOperationFindMod">
    <mods>
        <!-- This is an OR-list -->
        <li><!-- Mod name from the About.xml --></li>
        <li>Alpha Animals</li>
    </mods>
    <match Class="PatchOperationSequence">
        <success>Always</success>
        <operations>
            <!-- All patches here, but with tag "li" instead of "Operation" -->
            <li Class="PatchOperationAdd">
                <xpath><!-- ... --></xpath>
                <value><!-- ... --></value>
            </li>
            <li Class="PatchOperationAdd"> <!-- More patches like a normal list here -->
                <xpath><!-- ... --></xpath>
                <value><!-- ... --></value>
            </li>
        </operations>
    </match>
    <nomatch Class="PatchOperationAdd"> <!-- Could also be a sequence like above if you want to do multiple patches -->
        <xpath><!-- ... --></xpath>
        <value><!-- ... --></value>
    </nomatch>
</Operation>

Adding content (eg. comps) defensively (to avoid problems)

If you add new fields to a def, it can happen that another mod already did that, so your PatchOperationAdd fails (depending on the situation) and throws an error, if you add a field that already exists. This also applies if you add a comp that already exists. For example you want to add a refuelable comp to a vanilla def, but another mod already did that.

If you do this on fields that are likely that another mod could change it, you can use a patch sequence to detect if a field exists and changs the value in that case instead of adding it again.

<!-- Test for the existence of a comps-node and add it if not there. Done defensively to prevent adding it twice if some other mod got there first. -->
<Operation Class="PatchOperationSequence">
    <success>Always</success>
    <operations>
        <li Class="PatchOperationTest">
            <xpath>Defs/WorldObjectDef[defName="Caravan"]/comps</xpath>
            <success>Invert</success>
        </li>
        <li Class="PatchOperationAdd">
            <xpath>Defs/WorldObjectDef[defName="Caravan"]</xpath>
            <value>
                <comps />
            </value>
        </li>
    </operations>
</Operation>

<!-- Now add a new comp to the comps-node. (Either the previously added one or one added from another mod.) -->
<Operation Class="PatchOperationAdd">
    <xpath>/Defs/WorldObjectDef[defName="Caravan"]/comps</xpath>
    <value>
        <li>
            <compClass>MoreFactionInteraction.World_Incidents.WorldObjectComp_CaravanComp</compClass>
        </li>
    </value>
</Operation>

<!-- (Example made by Mehni) -->

Technically it would be the safest to do this on every add, but in many cases the chances are low to get a conflict and a sequence is a lot of code, so the value of doing that is questionable.

(If you need to do many of such patches and your mod also uses C#, you can add my custom PatchOperationAddOrReplace class to your mod and shorten these patches to a simple 5-liner. See here.)

Removing content

In some cases you want to remove existing content (defs) from core or a mod. This is generally a risky thing, since everything can be referenced and cause errors that way. A better approach is to just change it so it doesn't appear in the game anymore. For example instead of removing the "Wood fired generator" for whatever reason, you simple patch the designationCategory so player can't build it anymore.

A short overview on selector differences

You may already know that some patches start with */, others with // or /Defs/.

/Defs/ThingDef...

The / at the start indicates that the selector starts at the root element and begins from there to search for the Defs node. This is an expicit and direct way to define the selector. (I personally recomment this as it is clear that it starts at root and less prone to errors and relatively fast.)

Defs/ThingDef...

This is a relative path from the current node. In Rimworld, this is the root so it if functionally the same as the above, but technically it works a bit different. There are not really any noticable differences in performance to the above one.

//ThingDef...

The // means /[any one node]/, so it scips the derect selection for Def that use both methods above. In Rimworld there is only a Def node after the parent, so there is not much a difference on what it does. You could also use this on other positions in the code, but you should only use it when needed and not to save a few characters, since otherwise it unnecessarily slows down the selector.

*/ThingDef...

The * is a placeholder for any amount of nodes. This means */ThingDef[defName="x"] select /Defs/ThignDef[defName="x"] but it also searches for /Defs/BiomeDef/node/another/whatever/ThingDef[defName="x"], so it is powerful but slow. It is only recommended to use when you really can benefit from it. Like the above, you can also use it at later positions in the xpath.

Some more selectors

There are many more selectors. Here are some that may be useful and used relatively often.

ThingDef[defName="WoodLog"]

This is a nested statement that search for a child node of ThingDef named defName and tests the node content for "WoodLog". This is commonly used in almost all patches to only patch a defined def. But you can also use any other node tag instead of defName. Like you could select for thingClass. This is functionally the same as ThingDef/defName[text()="WoodLog"]/.. but shorter (and the technical execution is also different). You can also create nested statements. See further below in the document for more details on both.

li[text()="WoodLog"]

This selects all node inside the list that has a text which is exactly like defined. So "WoodLog" gets selected while "HardWoodLog" does not.

li[text()[contains(.,"WoodLog")]]

This selects all node inside the list that contain the defined text. This tests if a text node contains text, so the text doesn't have to be exactly the same. This would also hit on "WoodLogLarge" forexample.

Example:

This example selects the fuelFilter node from various different node structures that otherwise would use multiple patches:

/Defs//comps/li[@Class="CompProperties_Refuelable"]/*/li[text()[contains(.,"WoodLog")]]/ancestor:fuelFilter
<!-- This would apply to: -->
/Defs/ThingDef/comps/li[@Class="CompProperties_Refuelable"]/fuelFilter/thingDefs/li[text()="WoodLog"]/../parent::fuelFilter
<!-- but also: -->
/Defs/ResearchDef/comps/li[@Class="CompProperties_Refuelable"]/fuelFilter/something/example/moreThingDefs/li[text()="HardWoodLog"]/../../../..
<!-- and more where the filter logic applies. -->

Note

The example is not practical (as some of those nodes don't exist in Rimworld), but it sould give a decent idea of what is possible. Be careful with using it as such abstract filter are slow and can also select nodes you don't want to select. But when used correctly, you can safe a lot of repeating work and often automatically include compatibility to other mods.

Patching many nodes by logic

If you need to change many nodes by a certain logic, you can use that logic to patch all relevant nodes at once, without needing to select all defNames manually. This also patches content of other mods if the logic applies.

Traversing nodes:

/Defs/ThingDef/comps/li[@Class="CompProperties_Refuelable"]/../..

Nested:

/Defs/ThingDef[comps/li[@Class="CompProperties_Refuelable"]]

Both work, but the execution is different. Which one is faster still needs to be tested for Rimworld.

Example:

This patch adds a new fuel "Coal" to all refuelable buildings that already use wood as a fuel.

<Operation Class="PatchOperationAdd">
    <xpath>/Defs/ThingDef/comps/li[@Class="CompProperties_Refuelable"]/fuelFilter/thingDefs/li[text()="WoodLog"]]/..</xpath>
    <value>
        <li>Coal</li>
    </value>
</Operation>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment