XML Tweaker

The XML Tweaker allows you to modify any base-game XML file. As many of the files you're likely interested in are loaded before the Lua environment has been set up, the XML Tweaker isn't written in Lua. Instead, it's written in Wren. However, unless you're doing unusually complicated stuff, you can just specify your changes in XML.

Note that the API documentation for using Wren does not currently exist. If you're sure you need it, nag me (ZNix/ZNixian) and I'll hurry up and write it for you.

Before you begin, it's important to keep in mind that such changes are not implemented the same way as a simple find-and-replace operation in a text editor, despite how the end result makes it seem to be the case. This may be obvious to some among you, but for those whom are surprised by this, it's better to be acclimatized from the very start than to have to backtrack and readjust your mindset while in the midst of reading.

Recent Changes

If you have previously already read this document in its entirety and merely wish to familarize yourself with the most recent developments as quickly as possible, you may find this page's commit log here useful (unfortunately, unlike code, atomicity of changes tends to prove difficult in practice).

SuperBLT Definition

To utilize the XML Tweaker, you'll first need to write a SuperBLT definiton. This is an XML file that specifies what tweaks and/or Wren files should be loaded. For the purposes of explanation, let's write a simple mod that changes the title of the PAYDAY 2 window.

We can do this by modifying context.xml. There isn't really much point in doing so using a tweak, as it would be much easier to just modify this file by hand. Nevertheless, it's a good example of how to use XML tweaks.

First, create a new mod folder (creation of an accompanying mod.txt file is optional). Inside that, create a new supermod.xml file, and insert the following:

<?xml version="1.0"?>
<mod>
    <!--
        Apply the tweak defined in game_name_tweak.xml - if you have a lot of these files,
        you may want to place them in their own subdirectory. Just remember to fix up the
        definitions below to reflect the change
    -->
    <tweak definition="game_name_tweak.xml" />
</mod>

Do not omit the XML declaration - it should be present in all XML files you write for the tweaker.

This file tells the tweaker to load a tweak contained inside game_name_tweak.xml. Create that file now:

<?xml version="1.0"?>
<tweak version="2" name="#8db63936938575bf" extension="#8db63936938575bf">
    <search> <!-- search for a game_name tag that is a child node of a context tag -->
        <context />
        <game_name short_name="PAYDAY 2" /> <!-- Ensure that the game_name tag contains
                                                 the short_name attribute, with its value
                                                 set to "PAYDAY 2" -->
    </search>

    <!--
        If mode is set to "replace", it deletes the old tag and substitutes this one.
        If mode is omitted (defaults to "attach"), it adds the target tag(s) as new child
        node(s) of the searched tag instead.
    -->
    <target mode="attributes">
        <!-- <game_name short_name="PAYDAY 2" long_name="Hello, World!" full_name="PAYDAY 2" /> -->
        <attr name="long_name" value="Hello, Attributes!" />
    </target>
</tweak>

Let's break this down. In this file, we're only defining a single tweak, but you can also have multiple tweaks within a single XML file by using a <tweaks> root node, and placing multiple <tweak> nodes within it.

The following shows how a tweak file would appear when configured as such:

Click to show/hide
<?xml version="1.0"?>
<tweaks>
    <tweak version="2" name="[FILENAME]" extension="[FILEEXTENSION]">
        <search>
            <!-- Search nodes... -->
        </search>
        <target mode="[insertmodehere]">
            <!-- Target node(s)... -->
        </target>
    </tweak>

    <tweak version="2" name="[FILENAME]" extension="[FILEEXTENSION]">
        <search>
            <!-- Search nodes... -->
        </search>
        <target mode="[insertmodehere]">
            <!-- Target node(s)... -->
        </target>
    </tweak>

    <!-- More <tweak> nodes... -->
</tweaks>

Tweak Tag

First is the <tweak> tag.

The version attribute is an integer that determines how the tweak will be interpreted and implemented. At this time, the current version is 2, but it may be incremented in future if changes that break backward compatibility (usually referred to as "breaking changes") are necessary. To minimize disruption and tweak author overhead, such changes are avoided whenever possible and only used as a last resort.


Specifying the version attribute correctly is important because omitting it or using the wrong version can cause the tweaker's behavior to deviate greatly from your expectations, which can lead to significant amounts of wasted time attempting to debug issues that, in reality, stem from the differences between what you are expecting the XML Tweaker to do, and what it actually does.

(The following segment concerns returning readers who have existing tweak files; if you are learning about the XML Tweaker for the first time, feel free to skip it and proceed on)

If you are a returning reader and are confused by the addition of the version attribute and what it means for your existing tweak files, there is no cause for alarm - if your tweaks were already working prior to this change, they will continue functioning as before without requiring any further modifications.

With that said, you should still ensure that you add the version attribute (and set it to the newest version available) on all tweaks that you create to take advantage of the new/corrected behavior, as well as upgrade existing tweaks to newer versions (but please ensure that you perform complete tests on them when doing so to avoid inadvertently breaking them).

If you have large quantities of existing tweaks distributed across a comparatively small number of tweak files, you can still progressively upgrade these tweaks in batches since the per-tweak (as opposed to per-file) versioning design lends itself well to such progressive upgrades.

In general, unless you have a very good reason not to, you should always add the version attribute and set it to the newest version documented here. In addition, you should always attempt to upgrade your existing tweaks to newer versions, but please ensure that you perform complete tests on them when doing so to avoid inadvertently breaking them.

The following table summarizes breaking changes made across tweak versions:

Tweak Version Notes
1 (or unspecified) Initial release
2 replace and append modes now insert nodes in the expected order (see !19)

Tweaks are not forward compatible; specifying numbers greater than the current version will cause them to be clamped to the latter with no further effects, but doing so remains unwise due to potential breakage that can occur in future if your tweak requires changes for compatibility with subsequent versions.


Ordinarily, you'd just specify the name and extension of the file you're trying to tweak. For example, to target the settings/network.network_settings file, specify:

<tweak version="2" name="settings/network" extension="network_settings">

However, because context.xml isn't loaded from the bundle DB like the other XML files are, we're instead using the name and extension of the file that was loaded directly before it (which isn't an XML file).

(Note: as of version v3.3.0 of SuperBLT loader the internals of file tweaking have been changed to improve stability. This breaks this hack, but everything else in this document is unaffected.)

If you don't know the textual name of the file, and it's not contained in whatever hashlist you're using, you can directly use hashes by prefixing them with a # symbol. Please note that the XML Tweaker represents hashes in hexadecimal, padded out to 16 characters long using 0s (prefixed; e.g. d34d8eef -> #00000000d34d8eef). This is important if you're using a hash you retrieved from Bundle Modder, as it doesn't pad its hashes.

Search Tag

Next, there's the <search> tag.

This tells the tweaker which tag in the target file we're trying to modify, and where to find it. Each entry in the search tag is an empty element with the same name as the target element in the XML file, with no child nodes, and any attributes that the target element has. Note that if you do not specify any attributes in the search element, it will instead generically target all such elements whether or not they have any attributes defined.

That's a lot of jargon, so here's a quick illustration to get you oriented:

<----- Tag (sometimes Node) ------>
           <--- Attribute ----->
<game_name short_name="PAYDAY 2" />
    ^          ^          ^      ^
    |          |          |      |
 Element      Name      Value    |
                                 |
            The / indicates that this is an empty element
                    (i.e. it has no child nodes)

--------------------------------------------------------------------------------

   Parent node (If this node has no parent nodes, it is called the root node)
   (start-tag)
        |
        V
    <search>
        <context />
        <game_name short_name="PAYDAY 2" />
    </search>     ^
        ^         |
        |     Child nodes
        | (on the same level)
        |
   Parent node
    (end-tag)

Each element in the search tag represents an element in the target file which is a child node of the previous element in the search tag, starting with the root node. That may be a little hard to understand, so here's an example:

The file we want to tweak contains the following:

<?xml version="1.0" encoding="utf-8"?>
<a>
    <b abc="def">
        <c id="a">
            <f/>
        </c>
        <c id="b">
            <f/>
            <g/>
        </c>
    </b>
</a>

Let's say we want to insert a new <helloworld/> node as a child of the second <f/> node. The necessary <search> tag contents to achieve this are as follows:

<search>
    <?xml version="1.0" encoding="utf-8"?> <!-- Note: If the game file begins with an XML
                                                declaration like this one, then it *must*
                                                be copied here. Otherwise, do not specify
                                                it. -->
    <a/>
    <b/>
    <c id="b"/>
    <f/>
</search>

To reiterate, all elements specified within the search tag must be on the same level (i.e. none of them are, nor have, child nodes).

Note that some game files omit the XML declaration (i.e. <?xml...?>); in such cases you should also omit it from the search tag.

Target Tag

Last comes the <target> tag.

This contains the block of XML that you want to inject into the XML file. This can be set to one of several modes by adjusting the mode attribute.

For example, to insert a <helloworld> node into the document:

<target mode="[insertmodehere]">
    <helloworld greeting="Hello" subject="XML Tweaker" />
</target>

Please note that all modes must be specified in lower case.

attach mode (default)

In attach mode (which you'll get if you omit the mode attribute), the contents of the target node are attached to the node found by search as new child nodes. Continuing from the example above, the result in attach mode would be:

<?xml version="1.0" encoding="utf-8"?>
<a>
    <b abc="def">
        <c id="a">
            <f/>
        </c>
        <c id="b">
            <f>
                <helloworld greeting="Hello" subject="XML Tweaker" />
            </f>
            <g/>
        </c>
    </b>
</a>

append mode

append mode inserts the target node after (but on the same level as) the node found by search. Revisiting the example from above, the result in append mode would be:

<?xml version="1.0" encoding="utf-8"?>
<a>
    <b abc="def">
        <c id="a">
            <f/>
        </c>
        <c id="b">
            <f/>
            <helloworld greeting="Hello" subject="XML Tweaker" />
            <g/>
        </c>
    </b>
</a>

It is important to pay attention to the difference between attach and append, as misusing them can cost you plenty of wasted time tracking down issues that do not appear to make much sense.

If your target nodes are being inserted in reverse order, ensure that you have set your tweak's version attribute to at least 2.

replace mode

replace mode works similarly to append mode, but also removes the targeted node:

<?xml version="1.0" encoding="utf-8"?>
<a>
    <b abc="def">
        <c id="a">
            <f/>
        </c>
        <c id="b">
            <helloworld greeting="Hello" subject="XML Tweaker" />
            <g/>
        </c>
    </b>
</a>

Note that replacing the root node of the game XML file causes an error in the tweaker. This means that replacing all contents of a file (e.g. for quick proof-of-concept testing) may not currently be feasible, depending upon the length and structure of the file in question.

If your target nodes are being inserted in reverse order, ensure that you have set your tweak's version attribute to at least 2.

Despite the lack of a 'remove' mode, it is still possible to remove nodes by specifying replace mode and simply leaving the <target> tag empty:
<target mode="replace" />

attributes mode

attributes mode works quite differently from the other modes. Whereas in the other modes you're inserting an arbitary block of XML somewhere in the target document, in attributes mode you're instead modifying the same node you targeted. Specifying the same <helloworld/> target as before in combination with attributes mode would have resulted in a crash.

Attributes mode is especially useful when changing some attributes on a tag that has a large number of child nodes, and you don't wish to deal with the hassle of duplicating them (which would then require you to update your mod whenever a PAYDAY 2 update is subsequently released that modifies any of those child nodes).

In this mode, the <target> tag contents become:

<target mode="attributes">
    <attr greeting="Hello" subject="XML Tweaker" />
</target>

Which results in the following:

<?xml version="1.0" encoding="utf-8"?>
<a>
    <b abc="def">
        <c id="a">
            <f/>
        </c>
        <c id="b">
            <f greeting="Hello" subject="XML Tweaker" />
            <g/>
        </c>
    </b>
</a>

Complications

If there are multiple elements in the game XML file with the exact same attributes, specifying a child node that is unique among them should be sufficient to provide a match. As an example:

Example: Duplicate intermediates

<?xml version="1.0" encoding="utf-8"?>
<a>
    <b id="Value1 Value2">
        <c name="foo1"/>
        <d/>
        <c name="bar1"/>
    </b>
    <b id="Value1 Value2">
        <c name="foo2"/>
        <d/>
        <c name="bar2"/>
    </b>
</a>

Suppose we want to insert a new <helloworld/> node as a child of the second <b/> node, but on the same level as the other existing nodes (for now, let's assume that the order is unimportant). Unfortunately, there is some ambiguity as both <b/> nodes are on the same level, and they both have identical attributes. In this case, the following <tweak> contents will achieve the desired result:

<search>
    <?xml version="1.0" encoding="utf-8"?>
    <a/>
    <b id="Value1 Value2"/>
    <c name="foo2"/>
</search>

<target mode="append">
    <helloworld greeting="Hello" subject="XML Tweaker" />
</target>

Which results in the following:

<?xml version="1.0" encoding="utf-8"?>
<a>
    <b id="Value1 Value2">
        <c name="foo1"/>
        <d/>
        <c name="bar1"/>
    </b>
    <b id="Value1 Value2">
        <c name="foo2"/>
        <helloworld greeting="Hello" subject="XML Tweaker" />
        <d/>
        <c name="bar2"/>
    </b>
</a>

On the other hand, if the <helloworld/> node is to be the very first child node of the second <b/> node:

<search>
    <?xml version="1.0" encoding="utf-8"?>
    <a/>
    <b id="Value1 Value2"/>
    <c name="foo2"/>
</search>

<target mode="replace">
    <helloworld greeting="Hello" subject="XML Tweaker" />
    <c name="foo2"/>
</target>

Which in turn results in the following:

<?xml version="1.0" encoding="utf-8"?>
<a>
    <b id="Value1 Value2">
        <c name="foo1"/>
        <d/>
        <c name="bar1"/>
    </b>
    <b id="Value1 Value2">
        <helloworld greeting="Hello" subject="XML Tweaker" />
        <c name="foo2"/>
        <d/>
        <c name="bar2"/>
    </b>
</a>

This example also raises several interesting points, namely that if multiple elements that are identical exist on the same level:

  1. A specific instance can be (pseudo-)targeted by targeting a child node unique to itself, since the XML Tweaker searches exhaustively for matching nodes even in the presence of multiple identical intermediate elements
  2. Provided that point #1 is fulfilled, it is also possible to insert child nodes at specific positions within such elements
  3. However, it is still not possible to target the element itself, so please be aware of this limitation

Debugging

Sometimes, despite all your best efforts, you just can't quite seem to get things working as they should. This is usually where colorful words start being spouted in various quantities, but the following techniques should be of some use to you:

"Have you tried turning it off and on again?"

As you would expect, XML tweaks occur at the point where the game's XML file in question gets loaded, with most of these files being loaded during game startup (the remainder are loaded afterward at various points throughout the game's operation). However, unlike Lua scripts, not all of these files are reloaded at a later point.

For such files, once the game has been started, a full shutdown-restart cycle of the game is necessary - no amount of level reloads will suffice. If the latency of the cycle proves to be a strong irritant and the file you are tweaking is loaded at game startup, create a Lua script that triggers a crash on game startup (naturally, make sure you keep a backup copy of your save file before you do this).

Check your most recent BLT log file

If you've made a syntax error in your tweak file, the XML Tweaker logs details about the issue to the BLT logs and also dumps the entire tweak file's contents there, after which (Windows-only) a message box pops up. Such issues are typically trivial to resolve.

Verify your search nodes

You may inadvertently have specified an intermediate node that does not have any child nodes, such as <else /> (which you probably shouldn't be specifying as an intermediate node anyway, because it never has any child nodes). Alternatively, you may have missed out an intermediate node that subsequent search nodes are child nodes of. Verify your list of search nodes against an up-to-date, pristine copy of the game's XML file to rule out both possibilities.

If the game's XML file begins with an XML declaration (i.e. <?xml...?>), you must also add that line to the top of your list of search nodes.

Manually dump the changed file to log output

  1. Quit the game, if it is currently running.
  2. Edit mods/base/wren/base.wren and search for text = xml.string
    (for future reference, it's this line: https://gitlab.com/znixian/payday2-superblt-lua/blob/0a4826e48a236b3211df4c8df535d18ee3884b2e/wren/base.wren#L24)
  3. Change that line to become:

    Logger.log("Before tweaking:\n%(text)")
    text = xml.string
    Logger.log("After tweaking:\n%(text)")
    
  4. Isolate the problematic XML tweak by removing all other mods that make XML tweaks (and other XML tweaks within the mod in question).

  5. (Optional, but can make things a bit easier if your text editor chokes on large files)
    Rename/remove the BLT log file corresponding to the current day.
  6. Start the game up, then quit it again (you may be prompted to update the BLT basemod, cancel it for now). This may take a bit of time if the target XML file in question is large, or if the game is being started for the first time this session.
  7. The file should then be present in the newest BLT log file, both before and after tweaking (search for [WREN] Before tweaking:).
  8. If necessary, use a pretty printing tool* of your choice to make the output human-readable and diff tool-friendly.
  9. Diff the two versions with a diff tool* of your choice.
  10. Undo the change made in step #3, or simply re-update the BLT basemod.

*: Note that this page's authors have no affiliation with these sites. Proceed at your own risk, or use offline utilities if you prefer.

Check crash.txt and your most recent BLT log file

If the game spontaneously disappears instead of starting up, or never appears to start at all, it is possible that you have ran into an XML Tweaker crash. This usually manifests as the following callstack in crash.txt:

Application has crashed: access violation

-------------------------------

Callstack:

                      IPHLPAPI  (???)     ConvertGuidToStringA
                      IPHLPAPI  (???)     ConvertGuidToStringA
                      IPHLPAPI  (???)     ConvertGuidToStringA
                      IPHLPAPI  (???)     ConvertGuidToStringA
                      IPHLPAPI  (???)     ConvertGuidToStringA
                      IPHLPAPI  (???)     ConvertGuidToStringA
                      IPHLPAPI  (???)     ConvertGuidToStringA
                      IPHLPAPI  (???)     ConvertGuidToStringA
                      IPHLPAPI  (???)     ConvertGuidToStringA
         payday2_win32_release  (???)     ???
         payday2_win32_release  (???)     ???
         payday2_win32_release  (???)     ???
         payday2_win32_release  (???)     ???
         payday2_win32_release  (???)     ???
         payday2_win32_release  (???)     ???
         payday2_win32_release  (???)     ???
         payday2_win32_release  (???)     zip_get_name
                           ???  (???)     ???
                           ???  (???)     ???
                           ???  (???)     ???
                           ???  (???)     ???
                           ???  (???)     ???
                           ???  (???)     ???


-------------------------------

Current thread: Main

-------------------------------

Naturally, if you use the WSOCK32.dll version of the SuperBLT hook, you should expect to see WSOCK32 and socket instead of IPHLPAPI and ConvertGuidToStringA, respectively.

Your most recent BLT log file may also contain clues about the issue, so be sure to check it as well.