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 0
s (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:
- 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
- Provided that point #1 is fulfilled, it is also possible to insert child nodes at specific positions within such elements
- 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
- Quit the game, if it is currently running.
- Edit
mods/base/wren/base.wren
and search fortext = xml.string
(for future reference, it's this line: https://gitlab.com/znixian/payday2-superblt-lua/blob/0a4826e48a236b3211df4c8df535d18ee3884b2e/wren/base.wren#L24) -
Change that line to become:
Logger.log("Before tweaking:\n%(text)") text = xml.string Logger.log("After tweaking:\n%(text)")
-
Isolate the problematic XML tweak by removing all other mods that make XML tweaks (and other XML tweaks within the mod in question).
- (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. - 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.
- The file should then be present in the newest BLT log file, both before and after tweaking
(search for
[WREN] Before tweaking:
). - If necessary, use a pretty printing tool* of your choice to make the output human-readable and diff tool-friendly.
- Diff the two versions with a diff tool* of your choice.
- 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.