XPATH surgery - Need help with xpath? post here.

Started by skullywag, June 02, 2017, 04:26:23 AM

Previous topic - Next topic

o-o-o

I think there is a list in the source code.  Still kind of new at this, but anywhere I see <li>stuff</li> i read that as a list, even if it has only a single entry.  The Apparel_VestPlate/apparel/tags section has two entries:
Quote<tags>
        <li>Outlander</li>
        <li>Military</li>
      </tags>

I want to replace "Outlander" with "RaidMonkey".   I don't know how to correctly select the list item "Outlander" in order to get xpath to replace it.  I am fine with leaving "Military" as-is.  I am also fine with Outlanders spawning without the benefit of an armored vest.  Why would city-folk need body armor anyway?  City guards could always use the Military apparel tag.

Nightinggale

Quote from: o-o-o on December 11, 2017, 11:51:16 PMI think there is a list in the source code.
Gahhh, you are right.... there is one. I really hate the vanilla PatchOperations. They are written so confusing, at least the way they appear in ILSpy. The complete lack of comments doesn't help.

The issue is that it doesn't have a list in the code. Instead it has a single node. However that node is value and then later on it use the xml child nodes and not value itself. It looks like this is the approach in some PatchOperation methods while others have a list in C#.

  <Operation Class="PatchOperationReplace">
    <xpath>Defs/ThingDef[defName = "Apparel_VestPlate"]/apparel/tags/Outlander</xpath>
    <value>
      <li>RaidMonkey</li>
    </value>
  </Operation>

It goes to the Outlander node, locates the parent, inserts all the child nodes from value before Outlander and then it deletes Outlander. The fact that Military is also there doesn't matter.
ModCheck - boost your patch loading times and include patchmods in your main mod.

CannibarRechter

#92
Method 1 is to replace all tags with a complete new set. Method 2 is to replace the first tag with another tag. I don't believe there is any other alternative, unless the li has attributes (which yours don't).

Method 1 (finds and replaces all the tags and all their list items):


  <Operation Class="PatchOperationReplace">
    <xpath>Defs/ThingDef[defName = "Apparel_VestPlate"]/apparel/tags</xpath>
    <value>
      <tags>
          <li>RaidMonkey</li>
          <li>Military</li>
      <tags>
    </value>
  </Operation>


Method 2 (finds the first list item; note xpath indexes begin at '1'):


  <Operation Class="PatchOperationReplace">
    <xpath>Defs/ThingDef[defName = "Apparel_VestPlate"]/apparel/tags/li[1]</xpath>
    <value>
      <tags>
          <li>RaidMonkey</li>
      <tags>
    </value>
  </Operation>


There is probably an advanced xpath query mechanism to find an li with specific text body, but I don't know it.

EDIT:

Online documentation for xpath would suggest to me that the advanced query method would be this:


<xpath>Defs/ThingDef[defName = "Apparel_VestPlate"]/apparel/tags/li[text()[contains(.,"Outlander")]]</xpath>


It didn't test it, but if that actually works, let us know. It could be really useful. ;-P
CR All Mods and Tools Download Link
CR Total Texture Overhaul : Gives RimWorld a Natural Feel
CR Moddable: make RimWorld more moddable.
CR CompFX: display dynamic effects over RimWorld objects

TeflonJim

You can use . to refer to the current node and therefore filter to a single instance in the list. For example:


<xpath>Defs/ThingDef[defName = "Apparel_VestPlate"]/apparel/tags/li[.="Outlander"]</xpath>


This will let you target it for patch operations.

o-o-o

A bunch of stuff worked.

Method 1 worked with slight modification (forgot slash at end of tags)
Quote<Operation Class="PatchOperationReplace">
    <xpath>Defs/ThingDef[defName = "Apparel_VestPlate"]/apparel/tags</xpath>
    <value>
      <tags>
          <li>RaidMonkey</li>
          <li>Military</li>
      </tags>
    </value>
  </Operation>

I didn't want to try Method 2 - as I understand it, it looks for a specific location (first item in list) and replaces whatever it finds, as opposed to searching for the specific target (Outlander) and just replacing that.  Seems like a potential nightmare when things change in the vanilla code in chasing down those misapplied operations.

Both of the advanced querry methods worked:

Quote<Operation Class="PatchOperationReplace">
    <xpath>*/ThingDef[defName = "Apparel_VestPlate"]/apparel/tags/li[.="Outlander"]</xpath>
    <value>
      <li>RaidMonkey</li>
      </value>
    </Operation>

and

Quote<Operation Class="PatchOperationReplace">
    <xpath>*/ThingDef[defName = "Apparel_VestPlate"]/apparel/tags/li[text()[contains(.,"Outlander")]]</xpath>
    <value>
      <li>RaidMonkey</li>
      </value>
    </Operation>

My rich pawns with the Outlander apparel tag never spawned armored vests, and my rich pawns with both Outlander and RaidMonkey tags spawn standard apparel plus the armored vests.

Can you comment on the differences between the two advanced querry operations?  Is one more efficient than the other? Are there more complex cases where one would be preferable to the other?




TeflonJim

This expression is a very simple one and I would recommend it over nested function calls. I doubt you'll notice a speed difference, but there's certainly a complexity difference.


[.="Outlander"]


In theory all it needs you to know is that "." is the current XML node (be that an element as it is here, or an attribute), and that we can test a simple value using "=".

It's perhaps worth mentioning you can drop the "text()" function entirely, making the other expression into this:


*/ThingDef[defName = "Apparel_VestPlate"]/apparel/tags/li[contains(.,"Outlander")]


About text()

text() gets text elements (as opposed to node elements) from a given path. In the example below, "item" is a text element, "child" is a node element.


<document>
  <item>ItemValue</item>
  <child>
    <otherItem>ItemValue</otherItem>
  </child>
</document>


The expression below will nodes of text type only, therefore you would only find "item", and not "child".


document/*[text()]


About contains(this, containsthat)

The xpath expression below uses contains:


document/stuff/li[contains(., "Wood")]


When used to search the following document it will find both Wood_Green and Wood_Orange. Both contain the specified value.


<document>
  <stuff>
    <li>Wood_Green</li>
    <li>Wood_Orange</li>
    <li>Metal</li>
    <li>Plastic</li>
  </stuff>
</document>


Contains is most useful where you might have wanted a wildcard, a partial match. In the case where you want an exact value equality (using "=") is much more appropriate.

In all cases, XML (and xpath expressions) are case-sensitive. The changes in B18 should make it easier to develop patches as documents will be more predictable in the future.

CannibarRechter

> I didn't want to try Method 2 - as I understand it, it looks for a specific location (first item in list) and replaces whatever it finds, as opposed to searching for the specific target (Outlander) and just replacing that.

I agree with your decision there. I see it used in a lot of mods. It's probably because one of the well-known online xpath query generators prefers that method, and a lot of people copy others. But yeah, order-dependency is just a recipe for future headache.
CR All Mods and Tools Download Link
CR Total Texture Overhaul : Gives RimWorld a Natural Feel
CR Moddable: make RimWorld more moddable.
CR CompFX: display dynamic effects over RimWorld objects

kaptain_kavern

Regarding this precisely,

I'm currently updating my mods patches, trying to reflect all the things I've learned here. And I would need help for one, please.


                        <li Class="PatchOperationAdd"> <!-- Gazelle drop horn  -->
<xpath>Defs/PawnKindDef[defName = "Gazelle"]/lifeStages/li[3]</xpath>
<value>
<butcherBodyPart>
<bodyPartGroup>HornAttackTool</bodyPartGroup>
<thing>KKAO_GazelleHorn</thing>
<allowFemale>true</allowFemale>
</butcherBodyPart>
</value>
</li>


Here the patch I currently using. Noticed the "li[3]" in the xpath.

This is what I wanted to change for something more precise, but vanilla corresponding code is looking like:

    <lifeStages>
      <li>
            ......
      </li>
      <li>
            ......
      </li>
      <li>
        <bodyGraphicData>
          <texPath>Things/Pawn/Animal/Gazelle/Gazelle</texPath>
          <drawSize>1.65</drawSize>
          <shadowData>
            <volume>(0.4, 0.4, 0.4)</volume>
            <offset>(0,0,-0.2)</offset>
          </shadowData>
        </bodyGraphicData>
        <dessicatedBodyGraphicData>
          <texPath>Things/Pawn/Animal/Dessicated/CritterDessicatedMedium</texPath>
          <drawSize>1.65</drawSize>
        </dessicatedBodyGraphicData>
      </li>
    </lifeStages>


Thank you in advance for your help

TeflonJim

Quote from: kaptain_kavern on December 13, 2017, 03:31:11 AM
Here the patch I currently using. Noticed the "li[3]" in the xpath.

You ideally want something that uniquely identifies the last item in the list. Is dessicatedBodyGraphicData absent from the remaining items in the list?

If so, you might use this, which find li based on the presence of the dessicatedBodyGraphicData element:


Defs/PawnKindDef[defName = "Gazelle"]/lifeStages/li[dessicatedBodyGraphicData]


Otherwise I'll pull the def out from core tonight and have a better look.

Boartato

<?xml version="1.0" encoding="utf-8" ?>
<Patch>
<Operation Class="PatchOperationReplace">
<xpath>*/RangedIndustrial[defName="Gun_Revolver"]/description</xpath>
<value>
<description>Why doesn't anything work?</description>
</value>
</Operation>
</Patch>


I've been struggling with Xpath for a solid 8 hours. Finally decided to stop trying to do anything meaningful and really see if I could get a basic Xpath down. This code above fails. Every tutorial I've read, if I'm understanding things correctly, says this should work. This tutorial in particular would be the one closest to what I'm trying to do: https://gist.github.com/Zhentar/4a1b71cea45b9337f70b30a21d868782
There are many mods like it, but this mod is mine.

TeflonJim

Quote from: Boartato on July 20, 2018, 10:30:45 AM
<?xml version="1.0" encoding="utf-8" ?>
<Patch>
<Operation Class="PatchOperationReplace">
<xpath>*/RangedIndustrial[defName="Gun_Revolver"]/description</xpath>
<value>
<description>Why doesn't anything work?</description>
</value>
</Operation>
</Patch>


I've been struggling with Xpath for a solid 8 hours. Finally decided to stop trying to do anything meaningful and really see if I could get a basic Xpath down. This code above fails. Every tutorial I've read, if I'm understanding things correctly, says this should work. This tutorial in particular would be the one closest to what I'm trying to do: https://gist.github.com/Zhentar/4a1b71cea45b9337f70b30a21d868782
I assume you're trying to edit Gun_Revolver in Core\Defs\ThingDefs_Misc\Weapons\RangedIndustrial.xml?

If so, RangedIndustrial is the file name, not the name of the second element. I recommend you try:

<?xml version="1.0" encoding="utf-8" ?>
<Patch>
<Operation Class="PatchOperationReplace">
<xpath>Defs/ThingDef[defName="Gun_Revolver"]/description</xpath>
<value>
<description>Why doesn't anything work?</description>
</value>
</Operation>
</Patch>

For anyone else visiting, I pulled the def from core, here it is for reference:

<ThingDef ParentName="BaseHumanMakeableGun">
  <defName>Gun_Revolver</defName>
  <label>revolver</label>
  <description>An ancient pattern double-action revolver. It's not very powerful, but has a decent range for a pistol and is quick on the draw.</description>
  <graphicData>
    <texPath>Things/Item/Equipment/WeaponRanged/Revolver</texPath>
    <graphicClass>Graphic_Single</graphicClass>
  </graphicData>
  <soundInteract>Interact_Revolver</soundInteract>
  <statBases>
    <WorkToMake>4000</WorkToMake>
    <Mass>1.4</Mass>
    <AccuracyTouch>0.80</AccuracyTouch>
    <AccuracyShort>0.75</AccuracyShort>
    <AccuracyMedium>0.45</AccuracyMedium>
    <AccuracyLong>0.35</AccuracyLong>
    <RangedWeapon_Cooldown>1.6</RangedWeapon_Cooldown>
  </statBases>
  <weaponTags>
    <li>SimpleGun</li>
  </weaponTags>
  <costList>
    <Steel>30</Steel>
    <ComponentIndustrial>2</ComponentIndustrial>
  </costList>
  <recipeMaker>
    <skillRequirements>
      <Crafting>3</Crafting>
    </skillRequirements>
  </recipeMaker>
  <verbs>
    <li>
      <verbClass>Verb_Shoot</verbClass>
      <hasStandardCommand>true</hasStandardCommand>
      <defaultProjectile>Bullet_Revolver</defaultProjectile>
      <warmupTime>0.3</warmupTime>
      <minRange>1.5</minRange>
      <range>25.9</range>
      <soundCast>Shot_Revolver</soundCast>
      <soundCastTail>GunTail_Light</soundCastTail>
      <muzzleFlashScale>9</muzzleFlashScale>
    </li>
  </verbs>
  <tools>
    <li>
      <label>grip</label>
      <capacities>
        <li>Blunt</li>
      </capacities>
      <power>9</power>
      <cooldownTime>2</cooldownTime>
    </li>
    <li>
      <label>barrel</label>
      <capacities>
        <li>Blunt</li>
        <li>Poke</li>
      </capacities>
      <power>9</power>
      <cooldownTime>2</cooldownTime>
    </li>
  </tools>
</ThingDef>

Boartato

#101
Thank you very much! I was definitely including filenames in the path, I didn't realize they don't count as nodes. That said: Holy crap it works! Taking what you told me, I also got my original experiment to work as well!!!

<?xml version="1.0" encoding="utf-8" ?>
<Patch>
<Operation Class="PatchOperationAdd">
<xpath>Defs/ThingDef[defName="Human"]/recipes</xpath>
<value>
<li>Boart_InstallPogLeg</li>
</value>
</Operation>
</Patch>


You're the best :)

One thing that confuses me: Why is it /Defs/ThingDef when it's stored in Defs/ThingDefs_Misc/Weapons ? Every tutorial I've read says "where the file is stored" and yet there isn't even a folder called ThingDef. Would recipes like my little surgery mock up be stored in Defs/RecipeDef then?

There are many mods like it, but this mod is mine.

TeflonJim

The file path is completely irrelevant to the expression you're writing. XPath is used to search XML content, not files themselves. Defs is the root node (inside the XML document), and ThingDef is often the node you're most interested in editing in this case.

There was a twist with the previous versions, the game would patch a file at a time. At that point you might attempt to optimize a little, pushing more complex expressions behind simpler ones which targetted the right file first. In version 1.0 this changes. Everything is loaded into memory first, then patches are applied to the composite in memory.

In short, completely ignore the file name, and the directories it resides in. Concentrate only on the content within the files.

Boartato

There are many mods like it, but this mod is mine.

tcjaime

Help!  I'm new to patching and I'm stuck. Can someone tell me why this is changing all the power consumptions (ex wind gen) to 5 instead of just the lamp? Thank you!

<Patch>
   <Operation Class="PatchOperationReplace">
   <xpath>/Defs/ThingDef["StandingLampBase"]/comps/li["CompProperties_Power"]/basePowerConsumption</xpath>
      <value>
         <basePowerConsumption>5</basePowerConsumption>
      </value>
   </Operation>
</Patch>[/size][/size]