XPath-style modding API (Patching)

Tahvohck K shared this feedback 5 years ago
Submitted

Currently, modding the data files requires completely re-defining a given atomic element (Definition, PhysicalItem, etc). This results in poor forwards-compatibility, poor inter-mod compatibility, and bloated overall mod sizes. I suggest a XPath-based patching system be implemented, similar to the one in use by Rimworld. I've written several example patches.

This patch modifies the light armor small block to require four girders and one steel plate to build:


<Patch> <!-- Freestanding Patch: Simplest method of patching. -->
	<xpath>Definitions/CubeBlocks/Definition[Id/SubtypeId='SmallBlockArmorBlock']/Components</xpath>
	<value> <!-- Here we are replacing the contents of the Light Armor Small Block <Components> tag. -->
		<Component Subtype="Girder" Count="4" />
		<Component Subtype="SteelPlate" Count="1" />
	</value>
</Patch>
Patches can come in multiple forms. This one is an explicit edit patch (the same as before):
<Patch type='edit'> <!-- Default operation (done if type not specified). Replace the node contents with <value>. -->
	<xpath>Definitions/CubeBlocks/Definition[Id/SubtypeId='LargeBlockArmorBlock']/Components</xpath>
	<value>
		<Component Subtype="Girder" Count="29" />
		<Component Subtype="SteelPlate" Count="10" />
		<Component Subtype="SteelPlate" Count="15" />
	</value>
</Patch>
The next example shows several functions. PatchSet is used to group multiple patches together, most useful when multiple edits will be done to a single block, or multiple patches share the same requirements. Requirements will cause the patch to not be applied if they are not met. At the moment, only <exists> and <unaltered> tags are specified. Modders with larger scope can suggest further requirement types. Patch types "insert" and "delete" are demonstrated as well.

    <PatchSet id="PatchSetExample>
    <!-- PatchSet is used to group multiple Patches together. This is useful when you're making multiple adjustments to
    one node. Additionally any requirements set will apply to all Patches in the PatchSet. PatchSet may have an @id
    attribute that should be unique (not enforced), this will be used in error logging if present. -->
        <xpath>Definitions/CubeBlocks/Definition[Id/SubtypeId='LargeBlockArmorSlope']</xpath>
        <requires>
        <!-- The requires tag or any sub-tags may have the attribute @inverse with any value, this will cause the
        requirement(s) to only be met if they are NOT true. -->
            <exists />  <!-- This tag may be self-closed, or contain the XPath that should exist.
                        The presence of a PatchSet XPath implies a self-closed <exists> tag. -->
            <exists>Definitions/CubeBlocks/Definition[Id/SubtypeId='LargeBlockArmorBlock']</exists>
            <unaltered />   <!-- This tag may be self-closed, or contain the XPath that should be unaltered. -->
        </requires>
        <Patch type='edit'>
            <xpath>Components</xpath>   <!-- Combined with PatchSet xpath results. In this case we are editing the Components node of the Light Armor Large Block (Sloped)-->
            <value>
                <Component Subtype="Girder" Count="15" />
                <Component Subtype="SteelPlate" Count="5" />
                <Component Subtype="SteelPlate" Count="7" />
            </value>
        </Patch>
        <Patch type='insert' where="after">             <!-- Add <value> as peer to xpath location, based on @where (default AFTER) -->
            <xpath>Components/Component[last()]</xpath> <!-- Note the last() XPath expression. FULL XPath should be supported-->
            <value>
                <Component Subtype="SteelPlate" Count="70" />
            </value>
        </Patch>
        <Patch type='delete'>   <!-- Remove specified node. Notice that <value> is not needed for this Patch type. -->
            <xpath>Components/Component[last()]</xpath>
        </Patch>
    </PatchSet>
Because that last example was a jump in complexity, I'll break it down. In processing order:

  1. A PatchSet with the id "PatchSetExample" is begun. If it is not applied for any reason, it will be logged with this id included.
  2. This PatchSet will work with the Large Block Armor Slope (LBAS).
  3. This PatchSet explicitly requires that the LBAS exists. This would have been implicitly declared, since the PatchSet includes an xpath.
  4. This PatchSet requires that the Large Block Armor Block (LBAB) also exists.
  5. This PatchSet requires that the LBAS is not yet altered (whether this is Patch-only or applies to any mod is not within the scope of this initial suggestion)
  6. This PatchSet performs an Edit action to the Components sub-node of the LBAS. It completely re-writes the build costs.
  7. This PatchSet performs an Insert action to the last Component node in the Components sub-node. The insert is explicitly after that node.
  8. This PatchSet performs a Delete operation on the last Component node in the Components sub-node. Because this is a deletion, <value> is not needed.
  9. The PatchSet ends.

There are a few restritions that I can think of for this:

  1. The Id/SubtypeId node should deny all operations for data integrity reasons. Potentially, the entire Id node should do this.
  2. Because these would probably be stored in the Definitions tree along with everything else, the master node for Patches and PatchSets should be un-editable. Further patches are applied in sequence, NOT by editing another patch.
  3. PatchSet and Patch should have equal priority and be executed in document order.

Shortfalls of this suggestion:

  1. The suggested operations are unable to edit attributes without redefining the parent node. A clean way of doing this should be suggested.
  2. Patch ordering is not specified, and so patches from multiple files will be applied in filename order.

Replies (5)

photo
1

After reviewing the Rimworld system, I'm going to make a slight alteration to the suggested framework in my initial post. Edit actions should edit the node as a whole, instead of just the contents. This will provide moderate ability to edit attributes. In addition, this means the content of a <value> node should be simpler; it will ALWAYS be a single node, named the same as the node that's being edited. It may have changed attributes and it may have changed contents. Any other form will result in a patch error. The changed edit syntax is as follows:

<Patch>
	<xpath>Definitions/CubeBlocks/Definition[Id/SubtypeId='SmallBlockArmorBlock']/Components</xpath>
	<value>
		<Components uselessAttr="yay">
			<Component Subtype="Girder" Count="4" />
			<Component Subtype="SteelPlate" Count="1" />
		</Components>
	</value>
</Patch>

photo
1

This would be very useful. I'm currently working on including modded components in a variety of tech/weapon mods and the only way to do this currently is fully duplicating every mod. This forces the mod to be private to ensure that I'm not publishing the work of other modders. It also adds additional work to keep each of the constituent mods up to date.

photo
1

I would really love to see this implemented, it would be great rather than trying to replace the entire xml of a definition and break other mods that change other parts of said definition. Another game, RimWorld, has this already implemented in their modding API, and it works wonders and there is very little conflict with other mods, to the point where you can have mods include patches without needing to worry about load order or such. Although I think the lack of worry about load order is because of how they go about loading mods. If keen could someone implement the same and make worrying about load order, as not much of an issue that would be great!

photo
1

It absolutely astounds me that this is NOT in the SE engine.

photo
2

That would open up so much more flexibility and compatibility across mods.

Even if you don't know what it's for, even if you only use mods rather than make mods, you should vote for this.

You may not know that you'd want this, but you'll want this.

Leave a Comment
 
Attach a file