前言
相关内容来自RimWorld Wiki ,本文仅做翻译和有限的补充,除非另有说明,所属内容均在 CC BY-SA 3.0 下提供。
RimWorld Wiki:https://www.rimworldwiki.com/wiki/Main_PageRimWorld
RimWorld Wiki - PatchOperations:https://www.rimworldwiki.com/wiki/Modding_Tutorials/PatchOperations
水平有限,如有错漏,敬请见谅
如果您喜欢这篇文章,麻烦大家帮我点个赞,谢谢大家的支持!
基础
补丁操作(PatchOperations,以下称为Patch操作)是一个允许你修改XML Def内容的功能。在RimWorld alpha17版本之前,模组只能通过覆盖原来的Defs
来修改Defs
。这样做通常会导致兼容问题,如果有多个模组试图覆盖相同的Def
,只有最后一个加载的模组会成功,因为前面的模组都将被覆盖。
补丁操作是以XML节点的形式编写的,位于模组根文件夹的Patches文件夹中。
MyModFolder
├ About
├ Defs
└ Patches
└ MyPatchFile.xml
和XML Defs
一样,Patches
文件夹中的子文件夹名称和文件名并不重要,可以按照个人习惯进行命名。每个单个patches
文件是一个以<Patch>
为根标签的标准XML文件:
<?xml version="1.0" encoding="utf-8"?>
<Patch>
<!-- PatchOperations -->
</Patch>
XPath
大多数Patch操作必须针对主XML
文档中的一个或多个XML节点。通过XML路径语言(xpath)实现Path操作与XML节点的对应。
注意,xpath的目标是经过RimWorld解析器解析的XML文档的结构,因此它与实际的文件或文件夹路径无关。例如,如果想给原版的墙添加一个统计值,可以使用这样的xpath:
`Defs/ThingDef[defName="Wall"]/statBases`
-
任何针对XML Def的xpath的开始部分都是
Defs/
,因为所有XML Def都使用<Defs>
作为根标签; -
方括号表示谓词匹配。在上例中,我们正在寻找具有子标签
<defName>
且<defName>
值等于“Wall
”的<thingdef>
。
以属性为目标
对于没有defName
的Def
(如abstract bases),可以它们的标识属性来进行定位。例如,如果想为所有Stuff的abstract bases Def添加另一个Stuff类别,可以使用下面的xpath:
`Defs/ThingDef[@Name="ShelfBase"]/stuffCategories`
可以使用相同的技巧来定位所有继承了某个common base的Defs
。例如,你可以使用下面的xpath来定位所有继承了ApparelBase
的<Thingdef>
:
`Defs/ThingDef[@ParentName="ApparelBase"]`
PatchOperation类型
PatchOperation类型总览
基本的XML节点操作 | XML属性操作 |
---|---|
PatchOperationAdd :将提供的节点作为子节点添加到所选节点中;PatchOperationInsert :将提供的节点作为同级节点插入在所选节点之上;PatchOperationRemove :删除选中的节点;PatchOperationReplace :将选中的节点替换为提供的节点。 | PatchOperationAttributeAdd :当且仅当所提供的属性不存在时,将提供的属性添加到所选节点;PatchOperationAttributeSet :为所选节点设置属性,如果属性已经存在,则覆盖属性值;PatchOperationAttributeRemove :删除所选节点的属性。 |
特殊操作 | 条件操作 |
PatchOperationSequence :包含一组其他的Path操作,并且在任何操作失败时终止;PatchOperationAddModExtension :添加一个<ModExtension> ;PatchOperationSetName :用于修改节点名称。 | PatchOperationFindMod :测试是否存在另一个模组,并且可以根据结果执行不同的操作;PatchOperationConditional :测试节点,并且可以根据结果执行不同的操作;PatchOperationTest :测试节点,在PatchOperationSequence 中很有用。 |
基本的XML节点操作
PatchOperationAdd
将指定的值作为操作的xpath所针对的XML节点的子节点插入。默认情况下,新节点将插入到任何现有子节点之后(Append)。可以在PatchOperationAdd
中使用<order>Prepend</order>
中将它们插入到现有子节点之前。
注意:PatchOperationAdd
不会覆盖任何现有的标签。如果插入某个值与现有节点的值重叠,并且目标不是列表节点,那么将导致游戏加载错误。
PatchOperationInsert
将指定的值作为同级节点插入选定节点上方。可以使用<order>Append</order>
将其插入到目标节点之后(默认为Prepend)。
PatchOperationRemove
删除目标节点。
PatchOperationReplace
用提供的值替换所选节点的值。
XML属性操作
PatchOperationAttributeAdd
当且仅当目标节点不存在所提供的属性时,将提供的属性添加到目标节点。
PatchOperationAttributeSet
为所选节点设置属性,如果属性已经存在,则覆盖属性值。
PatchOperationAttributeRemove
删除所选节点的属性。
特殊操作
PatchOperationSequence
包含一个或多个按顺序执行的子Patch操作。如果其中任何一个失败,则Sequence停止并且不会继续执行后续的子Patch操作。
警告:
- 即使不使用
PatchOperationSequence
, XML文件中的Patch操作也会按顺序运行。 - 使用PatchOperationSequence会导致错误混淆或隐藏,如果
PatchOperationSequence
中的子Path操作有错误,将会难以调试。 - 除非需要用单个
PatchOperationConditional
或PatchOperationFindMod
对多个Patch操作进行排序,或者需要在子Patch操作上使用MayRequire,否则不要使用PatchOperationSequence
。 - 即使这样,也强烈建议将Patch操作编写为独立执行的,以确保它们按预期工作。
<Operation Class="PatchOperationSequence">
<operations>
<li Class="PatchOperationAdd">
<xpath>Defs/ExampleDef[defName="Sample"]/statBases</xpath>
<value>
<Mass>10</Mass>
</value>
</li>
<li Class="PatchOperationSetName">
<xpath>Defs/ExampleDef[defName="Sample"]/statBases/Flammability</xpath>
<name>ToxicEnvironmentResistance</name>
</li>
<!-- etc -->
</operations>
</Operation>
如前文所述,你可以在PatchOperationSequence
的子Patch操作上使用MayRequire
属性,但仍应该在将它们添加到序列之前进行单独测试:
<Operation Class="PatchOperationSequence">
<operations>
<li Class="PatchOperationAdd" MayRequire="Ludeon.Rimworld.Biotech"> <!-- Only runs if Biotech is active -->
<xpath>Defs/ThingDef[defName="MechGestator"]/recipes<xpath>
<value>
<li>MyCustomMech</li>
</value>
</li>
<li Class="PatchOperationAdd" MayRequire="MyProject.OtherModPackageId"><!-- Only runs if the specific mod is active -->
<xpath>Defs/ThingDef[defName="OtherModWorkbench"]/recipes</xpath>
<value>
<li>MyCustomResource</li>
</value>
</li>
</operations>
</Operation>
PatchOperationAddModExtension
将指定的DefModExtension
添加到目标Def
。如果目标Def
不存在<ModExtensions>
节点,将会自动创建。
PatchOperationSetName
修改节点名称。最适用于更改字典节点中的键节点名称而不更改其内容,例如stat和配方。
条件操作
PatchOperationFindMod
检查指定的模组或DLC是否被加载,并允许使用子Patch操作来处理匹配和不匹配结果。
警告:与RimWorld中的所有其他模组兼容性功能不同,PatchOperationFindMod
使用模组name
而不是其packageId
。
注意:
PatchOperationFindMod
只应该在为目标模组实现可选兼容性时使用,即本模组可以使用,也可以不使用相关的目标模组。- 如果本模组是解决目标模组兼容问题的模组,没有目标模组的话本模组就没有意义,那么最好在
About.xml
中简单地指定目标模组作为依赖模组,并放弃使用PatchOperationFindMod
,避免引入的潜在问题。
示例1:若RimQuest模组被加载则添加一个<ModExtension>
到目标Defs
(原文连接)。
<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>
示例2:若未加载Relations Tab模组,则替换派系按钮的tabWindowClass。
<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>
PatchOperationConditional
测试目标节点的存在性/有效性,并允许根据结果运行match或nomatch的Patch操作。
示例: 如果Caravan Def
中不存在<comps>
节点,将<comps>
节点添加到Caravan Def
中,然后将自定义的comp项添加到<comps>
中:
<?xml version="1.0" encoding="utf-8" ?>
<Patch>
<!-- add comps field to Caravan WorldObjectDef if it doesn't exist -->
<Operation Class="PatchOperationConditional">
<xpath>Defs/WorldObjectDef[defName="Caravan"]/comps</xpath>
<nomatch Class="PatchOperationAdd">
<xpath>Defs/WorldObjectDef[defName="Caravan"]</xpath>
<value>
<comps />
</value>
</nomatch>
</Operation>
<!-- add pyromaniac caravan handler comp to Caravan WorldObjectDef -->
<Operation Class="PatchOperationAdd">
<xpath>Defs/WorldObjectDef[defName="Caravan"]/comps</xpath>
<value>
<li Class="BetterPyromania.WorldObjectCompProperties_Pyromania">
<fuelCount>20</fuelCount>
<cooldown>30000</cooldown>
<needThreshold>0.5</needThreshold>
</li>
</value>
</Operation>
</Patch>
PatchOperationTest
(可能过时)
测试xpath的存在性/有效性。作为主动停止PatchOperationSequence
的一种方法很有用。
注意: 以这种方式条件性地应用patches被认为是过时的,因为这里使用<success>Always</success>
也会抑制合法错误,使序列难以调试。使用PatchOperationConditional
是更好的方法,
示例: 来自Shinzy的Apparello。
<Operation Class="PatchOperationSequence">
<!-- 因为PatchOperationSequence,必须使用 <success>Always</success> -->
<success>Always</success>
<!-- 检查wornGraphicPath,如果没有发现,添加一个 -->
<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>
杂项
自定义Path操作
自定义Patch操作可以通过在C#子类化Verse. PatchOperation来创建。这对于基于ModSettings值或其他自定义行为执行Patch操作非常有用。
Wiki上目前还没有此类教程,但你可以查看一下文章作为自定义Patch操作的参考:
- xml Extensions:一个完整的框架模组,包含许多有用的自定义PatchOperations;
- PatchOperationMakeGunCECompatible:由Combat Extended使用,可自动对单个枪支应用多个更改以实现兼容性。
- PatchOperationAddOrReplace:PatchOperationAdd的一个自定义变体示例,用于替换现有值。
Success
选项(可能过时)
<success>...</success>
节点决定如何处理错误,通常在PatchOperationSequence
中使用。- 请注意,此标签的使用应被视为过时。在
PatchOperationConditional
引入之前,该标签被经常使用,但如今停止继续使用该标签。<success>
标签可能会引起混乱,因为它会抑制合法错误的出现。
可用Success
选项包括:
- Always:此Patch操作始终被视为成功,它会抑制所有可能发生的错误。曾经与
PatchOperationTest
一起用于PatchOperationSequence
,以便条件性地运行Patch操作,但现在已过时; - Normal:正常错误处理;
- Invert:反转处理,错误被视为成功,成功被视为失败。这曾在
PatchOperationSequences
中用于测试PatchOperationTest
的否定式。如今可以在PatchOperationConditional
上使用<nomatch>
作为替代; - Never:此Patch操作始终被视为错误。通常只用于测试
Sequence
是否正常工作,不应该用于已发布的模组。
提示和技巧
- Path在将所有XML Def加载到内存中后开始按模组列表顺序运行。如果遇到与另一个模组的Patch操作的兼容性问题,可以在
About.xml
中使用<loadBefore>
和<loadAfter>
来帮助玩家解决; - Path发生在Def继承之前,这意味着不能以一个从父类标签继承的标签作为目标;不过如果直接更改父类标签,其所有子类标签都将继承Patch操作后的值;
- 可以使用
Inherit=“False”
属性覆盖从父类Def继承的值,示例如下:
<Operation Class="PatchOperationAdd">
<xpath>/Defs/ThingDef[defName = "Apparel_KidPants"]</xpath>
<value>
<thingCategories Inherit="False">
<li>NewValue</li>
</thingCategories>
</value>
</Operation>
- 可以在谓词中使用
or
来同时定位多个节点,如:Defs/ThingDef[dfName=“Cassowary” or defName=“Emu” or defName=“Ostrich” or defName:“Turkey”]
。但需要保证对每个目标进行相同的更改,这通常比使用多个Patch操作要好; - 对于
PatchOperationReplace
等操作,可以使用/text()
来定位标签的文本内容,而不是整个标签。如果不想意外删除整个属性,这一点尤其有用。
常见问题及处理
- xpath和XML节点通常区分大小写,并且必须正确拼写。强烈建议通过复制取值,如
defName
字段,以避免出现错误; - 格式不正确的XML(如未封闭或不匹配的标签,不完整的Def)可能会导致XML解析器完全崩溃,这可能导致启动时出现空白屏幕或“从致命错误中恢复”屏幕,并清除所有启用的模组。如果发生这种情况,请通过XML验证器(xml validator)运行所有XML,以确保其结构正确。检查
Player.log
文件也有助于诊断确切的原因; - Insert和Add和操作很容易混淆;Insert将把值“添加”为目标节点的同级节点,而Add将把值作为目标节点的子节点“插入”;
- 请记住,xpath的目标是XML数据结构,而不是文件路径;
- 除
<li>
列表项之外的XML中的节点在每个级别上都必须是唯一的。如果添加或插入重复的节点,将在加载时生成红色错误,并导致Def无法加载; - 如果仍然感到困惑,随时可以加入RimWorld Discord服务器,并在
#mod-development
频道中提问。
参考资料及链接(可能过时)
Zhentar创建的PatchOperations原始教程:Introduction to PatchOperation