Modding Tutorials/PatchOperations

From RimWorld Wiki
Revision as of 19:10, 28 October 2022 by WILDCARD (talk | contribs) (→‎PatchOperationSequence: Correction for default Insert and Add behavior)
Jump to navigation Jump to search

Modding Tutorials

RimWorld Alpha 17 added a new modding feature: XML PatchOperations. Using these allows mods to edit specific elements of XML defs, rather than simply overwriting the old def with a new one, which was the required approach until A17. This greatly reduces mod conflicts, because mods that make separate changes to the same def won't necessarily overwrite one another's changes. Mods using PatchOperations can avoid requiring special compatibility mods, and can even add special features conditional on other mods being loaded.

Here's a simple example of replacing the texture path for concrete:

<?xml version="1.0" encoding="utf-8" ?>
<Patch>
  <Operation Class="PatchOperationReplace">
    <xpath>Defs/TerrainDef[defName="Concrete"]/graphicData/texPath</xpath>
    <value>
      <texPath>Your/Texture/Path/Here</texPath>
    </value>
  </Operation>
</Patch>

This xml file simply goes inside YourMod/Patches/ and you're done.

Overview of available PatchOperations

These are the available PatchOperation types:

XPath syntax

Each PatchOperation must specify an XML node to modify. It uses XPaths to specify them. An XPath is a "path" for the patch to follow when trying to find XML nodes matching the description. It will follow each part of the path from left to right. Note that these "paths" have zero relation to the file path or folder structure: They follow the structure of the XML nodes.

XPaths start at the root node. Since the root node of all Defs is enforced to be <Defs>, our xpath always starts with Defs/. Starting with /Defs/ or */ works, but it is not recommended because it incurs additional overhead due to the way the XPath resolver works.

Once the root is selected, the XPath lets the system walk down specific branches of the tree. If we use the example from the lead, we get a specific path:

Defs/TerrainDef[defName="WaterDeep"]/texturePath

  1. The root node is selected
  2. All child nodes of the root node called "TerrainDef" are selected
  3. We narrow down from "all TerrainDefs" to only those with the child "defName" node whose value is "WaterDeep", which should give us exactly one node because defNames must be unique
  4. We select the child node "texturePath" from this node
    1. Note that we cannot put .../texturePath/ to specify the content of the texturePath node. If we want to replace texturePath, we have to replace it with <texturePath>/new/path/to/texture</texturePath>

For more info, see this Thread. XPath is a well-documented industry standard (good resource, too); there are plenty of resources available on the subject ranging from exhaustive tutorials to simple StackOverflow questions.

"Success" option

The <success>...</success> node determines how errors are handled, usually in a PatchOperationSequence. Note that use of this tag should be considered obsolete; its use was common before PatchOperationConditional was implemented, but it should no longer be required and can cause confusion as it can suppress legitimate errors from showing up.

Options available are:

  • Always - This patch operation always succeeds, i.e. it suppresses all errors that might have occurred. This used to be used in PatchOperationSequence along with PatchOperationTest in order to conditionally run patches, but is now obsolete.
  • Normal - Errors are handled normally
  • Invert - Errors are considered a success and success is considered a failure. This used to be used in PatchOperationSequences to test the negative of a PatchOperationTest; you should now use <nomatch> on PatchOperationConditional
  • Never - This patch operation always fails. Generally only used in testing to see if a Sequence is working correctly, should never be used in published mods.

Overview of individual PatchOperations

PatchOperationAdd

PatchOperationAdd adds a child node at the end of the selected node. The child can be added at the beginning using <order>Prepend</order> in the patch operation (order is Append by default).

Def before patch Def after patch Patch operation
<ExampleDef>
  <defName>NameX</defName>
  <aaa>Some text</aaa>
  
</ExampleDef>
<ExampleDef>
  <defName>NameX</defName>
  <aaa>Some text</aaa>
  <bbb>New text</bbb>
</ExampleDef>
<Operation Class="PatchOperationAdd">
  <xpath>Defs/ExampleDef[defName="NameX"]</xpath>
  <value>
    <bbb>New text</bbb>
  </value>
</Operation>
<ExampleDef>
  <defName>NameX</defName>
  <exampleList>
    
    <li>Bar</li>
  </exampleList>
</ExampleDef>
<ExampleDef>
  <defName>NameX</defName>
  <exampleList>
    <li>Foo</li>
    <li>Bar</li>
  </exampleList>
</ExampleDef>
<Operation Class="PatchOperationAdd">
  <xpath>Defs/ExampleDef[defName="NameX"]/exampleList</xpath>
  <order>Prepend</order>
  <value>
    <li>Foo</li>
  </value>
</Operation>

PatchOperationInsert

PatchOperationInsert inserts a sibling above the selected node. The sibling can be inserted below using <order>Append</order> in the patch operation (order is Prepend by default). Note that <xpath>Defs/ExampleDef[defName="NameX"]/exampleList/li[text()="target"]</xpath> would also work as a xpath in this example.

Def before patch Def after patch Patch operation
<ExampleDef>
  <defName>NameX</defName>
  <exampleList>
    <li>some text</li>
    
    <li>target</li>
    <li>more text</li>
  </exampleList>
</ExampleDef>
<ExampleDef>
  <defName>NameX</defName>
  <exampleList>
    <li>some text</li>
    <li>new sibling</li>
    <li>target</li>
    <li>more text</li>
  </exampleList>
</ExampleDef>
<Operation Class="PatchOperationInsert">
  <xpath>Defs/ExampleDef[defName="NameX"]/exampleList/li[1]</xpath>
  <value>
    <li>new sibling</li>
  </value>
</Operation>
<ExampleDef>
  <defName>NameX</defName>
  <exampleList>
    <li>some text</li>
    <li>target</li>
    
    <li>more text</li>
  </exampleList>
</ExampleDef>
<ExampleDef>
  <defName>NameX</defName>
  <exampleList>
    <li>some text</li>
    <li>target</li>
    <li>new sibling</li>
    <li>more text</li>
  </exampleList>
</ExampleDef>
<Operation Class="PatchOperationInsert">
  <xpath>Defs/ExampleDef[defName="NameX"]/exampleList/li[1]</xpath>
  <order>Append</order>
  <value>
    <li>new sibling</li>
  </value>
</Operation>

PatchOperationRemove

PatchOperationRemove removes the selected node.

Def before patch Def after patch Patch operation
<ExampleDef>
  <defName>NameX</defName>
  <aaa>Some text</aaa>
  <bbb>More text</bbb>
</ExampleDef>
<ExampleDef>
  <defName>NameX</defName>

  <bbb>More text</bbb>
</ExampleDef>
<Operation Class="PatchOperationRemove">
  <xpath>Defs/ExampleDef[defName="NameX"]/aaa</xpath>
</Operation>

PatchOperationReplace

PatchOperationReplace replaces the selected node.

Def before patch Def after patch Patch operation
<ExampleDef>
  <defName>NameX</defName>
  <aaa>Some text</aaa>
</ExampleDef>
<ExampleDef>
  <defName>NameX</defName>
  <bbb>Alternate node</bbb>
</ExampleDef>
<Operation Class="PatchOperationReplace">
  <xpath>Defs/ExampleDef[defName="NameX"]/aaa</xpath>
  <value>
    <bbb>Alternate node</bbb>
  </value>
</Operation>

PatchOperationAttributeAdd

PatchOperationAttributeAdd will add an attribute and set its value, but only if that attribute is not yet present.

Def before patch Def after patch Patch operation
<ExampleDef>
  <defName>Xyz</defName>
  <aaa>Text</aaa>
</ExampleDef>
<ExampleDef>
  <defName>Xyz</defName>
  <aaa ExampleAtt="Foo">Text</aaa>
</ExampleDef>
<Operation Class="PatchOperationAttributeAdd">
  <xpath>Defs/ExampleDef[defName="Xyz"]/aaa</xpath>
  <attribute>ExampleAtt</attribute>
  <value>Foo</value>
</Operation>

PatchOperationAttributeSet

PatchOperationAttributeSet will add an attribute and set its value. Unlike PatchAttributeAdd, PatchOperationAttributeSet will overwrite any existing value.

Def before patch Def after patch Patch operation
<ExampleDef>
  <defName>Xyz</defName>
  <aaa ExampleAtt="Foo">Text</aaa>
</ExampleDef>
<ExampleDef>
  <defName>Xyz</defName>
  <aaa ExampleAtt="Bar">Text</aaa>
</ExampleDef>
<Operation Class="PatchOperationAttributeSet">
  <xpath>Defs/ExampleDef[defName="Xyz"]/aaa</xpath>
  <attribute>ExampleAtt</attribute>
  <value>Bar</value>
</Operation>

PatchOperationAttributeRemove

PatchOperationAttributeRemove removes the specified attribute.

Def before patch Def after patch Patch operation
<ExampleDef>
  <defName>Xyz</defName>
  <aaa ExampleAtt="Foo">Text</aaa>
</ExampleDef>
<ExampleDef>
  <defName>Xyz</defName>
  <aaa>Text</aaa>
</ExampleDef>
<Operation Class="PatchOperationAttributeRemove">
  <xpath>Defs/ExampleDef[defName="Xyz"]/aaa</xpath>
  <attribute>ExampleAtt</attribute>
</Operation>

PatchOperationAddModExtension

PatchOperationAddModExtension adds a Mod Extension.

Def before patch Def after patch Patch operation
<ExampleDef>
  <defName>Xyz</defName>
  <aaa>Some text</aaa>
  
  
  


</ExampleDef>
<ExampleDef>
  <defName>Xyz</defName>
  <aaa>Some text</aaa>
  <modExtensions>
    <li Class="SomeNameSpace.SomeExtension">
      <exampleValue>true</exampleValue>
    </li>
  </modExtensions>
</ExampleDef>
<Operation Class="PatchOperationAddModExtension">
  <xpath>Defs/ExampleDef[defName="Xyz"]</xpath>
  <value>
    <li Class="SomeNameSpace.SomeExtension">
      <exampleValue>true</exampleValue>
    </li>
  </value>
</Operation>

PatchOperationSetName

PatchOperationSetName changes the name of a node. It is primarily useful for changing the names of the nodes in "Dictionary" types, such as for recipe products.

Def before patch Def after patch Patch operation
<RecipeDef>
  <defName>A</defName>
    <!--...-->
  <products>
    <WoodLog>30</WoodLog>
  </products>
</RecipeDef>
<RecipeDef>
  <defName>B</defName>
  <!--...-->
  <products>
    <WoodLog>300</WoodLog>
  </products>
</RecipeDef>
<RecipeDef>
  <defName>A</defName>
  <!--...-->
  <products>
    <Steel>30</Steel>
  </products>
</RecipeDef>
<RecipeDef>
  <defName>B</defName>
  <!--...-->
  <products>
    <Steel>300</Steel>
  </products>
</RecipeDef>
<Operation Class="PatchOperationSetName">
  <xpath>Defs/RecipeDef[defName="A" or defName="B"]/products/WoodLog</xpath>
  <name>Steel</name>
</Operation>

PatchOperationSequence

PatchOperationSequence simply completes all operations in the order of their listing, but stops when any of them fails.

Patch Operation

<Operation Class="PatchOperationSequence">
  <operations>
    <li Class="PatchOperationInsert">
      <xpath>Defs/ExampleDef[defName="Example"]/exampleNode</xpath>
      <value>
        <anotherExample>A new sibling</anotherExample>
      </value>
    </li>
    <li Class="PatchOperationAdd">
      <xpath>Defs/ExampleDef[defName="Example"]</xpath>
      <value>
        <label>I'm a node</label>
      </value>
    </li>
    <!-- etc -->
  </operations>
</Operation>

Effect

Def before patch Def after patch
<ExampleDef>
  <defName>Example</defName>
  
  <exampleNode>Some text</exampleNode>

  <exampleList />
</ExampleDef>
<ExampleDef>
  <defName>Example</defName>
  <anotherExample>A new sibling</anotherExample>
  <exampleNode>Some text</exampleNode>
  <label>I'm a node</label>
  <exampleList />
</ExampleDef>

Another Example: You can use this operation as a simple and quick way to patch only if a mod is loaded by using the MayRequire attribute:

<Operation Class="PatchOperationSequence">
  <operations>
    <li Class="PatchOperationRemove" MayRequire="ludeon.rimworld.royalty"> <!-- Only runs if Royalty is active -->
      <xpath>Defs/ExampleDef[defName="Example"]<xpath>
    </li>
    <li Class="PatchOperationAdd" MayRequire="ludeon.rimworld.ideology"><!-- Only runs if Ideology is active -->
      <xpath>Defs</xpath>
      <value>
        <ExampleDef>
          <defName>MyBetterExample</defName>
        </ExampleDef>
      </value>
    </li>
  </operations>
</Operation>

PatchOperationTest

PatchOperationTest tests for the existence/validity of an xpath. Useful to stop a PatchOperationSequence.

NOTE: Conditionally applying patches in this way is considered obsolete. Using PatchOperationConditional is a much better idea as the use of <success>Always</success> here will also suppress legitimate errors, making Sequences difficult to debug.

The following example patch is from Apparello, by Shinzy.

<Operation Class="PatchOperationSequence">
  <!-- Must use <success>Always</success> because of the PatchOperationTest -->
  <success>Always</success>
  <!-- check for worn graphics and if none found, add one -->
  <operations>
    <li Class="PatchOperationTest">
      <xpath>Defs/ThingDef[defName = "Apparel_Pants"]/apparel/wornGraphicPath</xpath>
      <success>Invert</success>
    </li>
    <li Class="PatchOperationAdd">
      <xpath>Defs/ThingDef[defName = "Apparel_Pants"]/apparel</xpath>
      <value>
        <wornGraphicPath>Accessorello/Pants/Pants</wornGraphicPath>
      </value>
    </li>
  </operations>
</Operation>

PatchOperationConditional

PatchOperationConditional can branch logic based on a match/nomatch. The following example patch adds a <comps> node if it does not yet exist, and adds itself

This patch is courtesy Mehni. See original.

<!-- Add /comps/li/compClass if there are no comps yet. -->
<!-- Add /li/compClass to /comps if exists (i.e. other mod already added the comps field first) -->
<Operation Class="PatchOperationConditional">
  <xpath>Defs/WorldObjectDef[defName="Caravan"]/comps</xpath>
  <nomatch Class="PatchOperationAdd">
    <xpath>Defs/WorldObjectDef[defName="Caravan"]</xpath>
    <value>
      <comps>
        <li>
          <compClass>MoreFactionInteraction.WorldObjectComp_CaravanComp</compClass>
        </li>
      </comps>
    </value>
  </nomatch>
  <match Class="PatchOperationAdd">
    <xpath>Defs/WorldObjectDef[defName="Caravan"]/comps</xpath>
    <value>
      <li>
        <compClass>MoreFactionInteraction.WorldObjectComp_CaravanComp</compClass>
      </li>
    </value>
  </match>
</Operation>

PatchOperationFindMod

PatchOperationFindMod searches the list of active mods for the mods listed in <li></li> tags.

For a successful match, <li>ModName</li> has to match the <name>ModName</name> in the About.xml of the mod you base your patch behavior on. It only needs to find one of the mods in the list for a successful match. This (shortened) patch is courtesy Mehni. See original.

The following Patch adds the RimQuest mod extension to the listed Defs when RimQuest is found.

<Operation Class="PatchOperationFindMod">
  <mods>
    <li>RimQuest</li>
  </mods>
  <match Class="PatchOperationAddModExtension">
    <xpath>Defs/IncidentDef[defName="MFI_DiplomaticMarriage" or defName="MFI_HuntersLodge" or defName="MFI_Quest_PeaceTalks"]</xpath>
    <value>
      <li Class = "RimQuest.RimQuest_ModExtension">
        <canBeARimQuest>false</canBeARimQuest>
      </li>
    </value>
  </match>
</Operation>

The following Patch replace the tabWindowClass of the xpath when Relations Tab is not found.

<Operation Class="PatchOperationFindMod">
  <mods>
    <li>Relations Tab</li>
  </mods>
  <nomatch Class="PatchOperationReplace">
    <xpath>/Defs/MainButtonDef[defName="Factions"]/tabWindowClass</xpath>
    <value>
      <tabWindowClass>MyNameSpace.MyTabWindowClass</tabWindowClass>
    </value>
  </nomatch>
</Operation>

Common issues

Spelling matters, and XPath is case sensitive. It's very case sensitive. If you've written something that doesn't work but you're convinced it should, put it through some XPath validator. Case sensitivity includes but is not limited to the <Operation Class="PatchOperationWhatever">, the XPath, the value, every tag -- spell it right.

Another common issue is swapping Insert and Add, which leads to a different position in the node. A common misconception is trying to account for file paths. The file path is completely irrelevant to the expression you're writing. XPath is used to search XML content, not files themselves.

Still beating your head against a wall? The main RimWorld discord has helpful people and a bot where you can test XPath, and the forums have a help section as well.

Some advanced examples

This (shortened) patch is courtesy XeoNovaDan. See original. Viewer discretion is advised. Parents please take precautions if young children are present.

<Operation Class="PatchOperationFindMod">
  <mods>
    <li>Expanded Woodworking for Vegetable Garden Project</li>
  </mods>
  <match Class="PatchOperationSequence">
    <operations>
      <!-- Nested PatchOpFindMod. Used because Expanded Woodworking adds more Wood when VGP Xtra is installed. -->
      <li Class="PatchOperationFindMod">
        <mods>
          <li>VGP Xtra Trees and Flowers</li>
        </mods>
        <match Class="PatchOperationSequence">
          <success>Always</success>
          <operations>
            <!-- Processed camellia lumber. Mostly useful for building long-lasting, simple structures and furniture.-->
            <li Class="PatchOperationAddModExtension">
              <xpath>Defs/ThingDef[defName="LumberCamellia"]</xpath>
              <value>
                <li Class="SurvivalTools.StuffPropsTool">
                  <toolStatFactors>
                    <ConstructionSpeed>0.55</ConstructionSpeed>
                  </toolStatFactors>
                </li>
              </value>
            </li>
          </operations>
        </match>
      </li>
    </operations>
  </match>
</Operation>

Random stuff

  • @ select by attribute. Useful for patching (Abstract) bases.
    Defs/ThingDef[@Name="BuildingBase"]
  • or is inclusive.
    Defs/ThingDef[defName="Cassowary" or defName = "Emu" or defName = "Ostrich" or defName = "Turkey"]
    selects all of those in a single (faster) operation.
  • PatchOperationAdd and PatchOperationInsert have an order tag which allows you to either Prepend or Append your add. Default behaviour of Add is to Append, insert Prepends.
  • In most cases, you just want to replace the value of some text, not replace the whole tag itself. You can use a PatchOperationReplace, and simplify your <value> by selecting the text() node, like this:
    ExampleTag/text()
  • There is a success tag whose values can be Normal, Invert, Always and Never. This is used for the sake of error reporting: by default an error is logged when a patch fails to apply. For PatchOperationSequences used with PatchOperationTests, you can set success to Always to avoid error logging.
  • Inheritance is handled after patches, since you can change attributes with patches. Therefore, you cannot patch nodes that do not exist before inheritance.
  • More useful things:
    • Select nodes by text
      ExampleTag/li[text()="Submit"]
    • Select nodes by text with a type of wildcard
      ExampleTag[contains(text(), "MFI_")]
    • Exclude nodes with not()
      [not(xxxxx)]
    • Go to a specific li in the list. Useful for patching hediff givers. "count" here represents the entry number you want to edit.
      li[count]
    • Go back to parentnode with
      ..
    • Select a list by the value of an item
      ExampleTag[li/text()="content"]

Write your own

Outside the scope of this wiki, but it's possible to write your own PatchOperation.
- Example PatchOperationMakeGunCECompatible
- Example PatchOperationAddOrReplace

Resources

The best tutorial on PatchOperations created by Zhentar: Introduction to PatchOperation

You can also learn about XPaths here: XPath tutorial

There is also an Overview of PatchOperations on the RimWorldModGuide.