Player-Friendly Atomic Game Modding

Player-Friendly Atomic Game Modding

Like many game devs, I got my start making mods for other games. And if your experience was anything like mine, you'll recall brilliant creative highs punctuated by long tedious slogs sorting through obscure and poorly documented data formats, unless you were really unlucky and reduced to directly hacking binary files themselves. Modding is often difficult and error-prone both for the user and the consumer, but it gets even worse if you try combining mods.

I have to admit to my shame that the legacy version of our game Defender's Quest (originally released in 2012) had pretty shoddy mod support. To our credit, our mod system was pretty well documented, but the format was intimidating to non-programmers.

Last year, we released the free Defender's Quest DX update to the original game, completely overhauling the game engine with full HD support, a by-product of our ongoing work on Defender's Quest II.

Today we are releasing expanded mod support, Steam Workshop integration, and an official mod editor.

This makes it really easy to add new Bonus Battles, for instance:


Tutorials for the editor are available on Github. For instance, here's a full tutorial on how to make the level featured above.

The kind of Mod system I like best, and aspire to create myself, is:

  1. Data driven
  2. Modder/Player-Friendly
  3. Atomic

Let's go over these principles one by one, using specific examples from Defender's Quest.

Data Driven

A data driven game is one where as much behavior as possible is exposed as data files (preferably loose data files that are easily edited). Rather than hard-coding every single bit of enemy logic, for instance, with "RedGoblin" and "GreenGoblin" being their own separate classes, you might have only one "Goblin" class, with a set of properties that are loaded from data files. Or you can go one step further and have a single "Enemy" class, with data-driven properties doing all the differentiation. This way, all you have to do to create a new enemy type (or anything else that uses this paradigm) is mix and match existing properties via data.

Friendliness

This is where the level editor comes in. We're kind of stuck with our existing game data format, which is a bit of a rat's nest of files that all cross-reference each other. However, it's pretty easy to make a computer keep track of those references and update them all at once.

Our writer, James, suggested to me that instead of thinking of the data first, I should think of the game objects. The average Modder doesn't care about the four or five different files that reference a single battle, they care about the battles. There should be an easy way to get new battles into the game that doesn't feel like doing homework. We knew we didn't have time to create a super full-fledged editor that could do everything right out of the box, but if we kept it to a simple scope, we could make it a lot easier to do common tasks, and speed up our own development, to boot.

James insisted we focus on just two things:

  • Make new levels
  • Make new items

The easiest way to add a new battle to the game is to make it a bonus battle. This makes it easy to drop in, with no need to worry about where it appears on the map or how it ties into the story. And the easiest way to add a new item is to make it a reward in a bonus battle.

The current version of the official editor focuses on just these two things. There's a lot more features that the game supports, and that an advanced modder can accomplish by just diving directly into the data. But this makes it a lot easier to do the sorts of thing we expect the average modder wants to do.

In the future we'll expose more features such as enemy editing, sprite/animation editing, and more as we improve those tools for our own use for DQII.

But what I really want to talk about today is how we made our mod system atomic.

Up and Atom!

An "atomic" mod system is one that allows each mod to be treated as an individual "atom" that can be combined together according to certain rules. You might have noticed that the kind of content the editor is designed for is also the kind of content that easily mixes together.

In the original game, you could only have one mod active at a time. If you wanted to mix mods together, you'd have to compose them manually -- a tedious, and error-prone process.

Now, you can mix several mods together.

From the in-game mod browser, one can select individual mods from the left, then arrange the order in which they load on the right. Pressing "play" restarts the game with all of the selected mods applied.

Let's talk a bit about how it works.

First, let's open the mod directory for "legendsofawesome", which is the same mod described in the Level Editor tutorial.

"legendsofawesome" does exactly two things:

  • Adds one new bonus battle
  • Adds one new special item

But as we can see from the screenshot, there's a lot more going on. The "settings.xml" file contains the game's basic metadata, such as the title and description that show up on steam, whether the mod is publicly visible, etc. The icon.png file is what will be used as the mod's preview image both in the in-game mod browser and on Steam. The rest are data files to be used in the game.

There are three special directories, /_append, /_merge, and the root directory (everything that's not in /_append or /_merge, and isn't icon.png, or _settings.xml).

root

This one's the simplest. Anything that's in the root directory will be treated as a replacement for the file of the same name in the base game. If multiple mods are loaded in succession, this behavior cascades.

Example:
Say have a file called foo.txt, which in the base game contains the text "Hello, World!". We create a mod, "A" which provides it's own foo.txt in the root directory, but with the content "Hi, World!", and a mod, "B" which does the same except here it's "Aloha, World!".

The result of loading the two mods in the following configurations is:

  • A foo.txt = "Hi, World!"
  • B foo.txt = "Aloha, World!"
  • A+B foo.txt = "Aloha, World!"
  • B+A foo.txt = "Hi, World!"

So as you can see, load order matters, and the Defender's Quest mod loader lets you control this.

It's very easy to just open a base file, make some changes, and save a copy of that file to a mod folder. The only downside to this method is that copying an entire file to make one change can be overkill, as it requires frequent updates if the game later releases an update that changes other parts of that file.

The "legendsofawesome" mod uses the root folder to add some new files to the game, in this case, a new bonus battle:

The map looks like this (increased in scale 6x):

And the XML has this content:

<?xml version="1.0" encoding="utf-8" ?>
<data>
	<title value="" id=""/>
	<music value="battle"/>
	<victory>
		<condition value="kill_all"/>
	</victory>
	<failure/>
	<tiles>
		<start x="0" y="7" id="a"/>
		<start x="14" y="7" id="b"/>
		<end x="7" y="7" id="a"/>
		<tile tile_sheet="single" layer="0" value="grey_dirt" rgb="0x000000"/>
		<tile layer="1" value="blue_sand" rgb="0x9BB3CD"/>
		<tile layer="2" value="dark_cliff" rgb="0x435054"/>
		<tile layer="3" value="water" rgb="0x132F54"/>
	</tiles>
	<waves first_wait="10" diff="easy">
		<wave loc="a,b" rate="2" level="15" wait="1" type="attacker" count="30"/>
		<wave loc="a" rate="1" level="15" wait="1" type="speedy" count="20"/>
		<wave loc="a,b" rate="1" level="15" wait="1" type="normal_w" count="10"/>
		<wave loc="a" rate="1" level="1" wait="1" type="right_middle_finger" count="1"/>
	</waves>
	<waves first_wait="1" diff="normal">
		<wave loc="a" rate="1" level="1" wait="1" type="normal" count="1"/>
	</waves>
	<waves first_wait="1" diff="hard">
		<wave loc="a" rate="1" level="1" wait="1" type="normal" count="1"/>
	</waves>
</data>

This provides all the data necessary for a battle, but does nothing to actually register it anywhere the player can access it.

_append

Files in the /_append folder are partial changes that are simply added to the end of the base file with the same name. Similarly to files in the root folder, this behavior cascades with multiple mods (and order likewise matters).

Currently, the /_append folder will only recognize text files, and has special rules for handling .tsv files (tables, mostly used for localization and human-readable text) and .xml files (generic game data).

The "legendsofawesome" mod uses this method to add references to the aforementioned bonus battle, so that it actually shows up in game. This requires modifying the bonus battle registry and the localization tables.

Example 1 (TSV):
The mod includes localization text so that the game can display the title and description of the new bonus battle. The American English ("en-US") data is stored in /_append/locales/en-US/maps.tsv, for instance, and looks like this:

$LEGENDSOFAWESOME~AWESOMEINTRO_TITLE	The Awesomeness Begins...
$LEGENDSOFAWESOME~AWESOMEINTRO_TEXT	This time it's personal.

These two lines will now simply be added to the end of each of the base game's maps.tsv files on a per-locale basis. If multiple mods are loaded together, each of which adds new TSV lines to the same file, the load order shouldn't affect the game's behavior. The exception is if several mods have lines with the exact same entry flag names (ie, "$LEGENDSOFAWESOME~AWESOMEINTRO_TITLE"). In this case, I believe the last entry will overwrite the previous one when it gets loaded in game.

Example 2 (XML):

Displaying the correct text is nice, but the new battle needs to actually be accessible from somewhere. The bonus registry file controls this, which the mod edits with the /_append/xml/bonus.xml file:

<?xml version="1.0" encoding="utf-8" ?>
<data>
	<bonus description="$LEGENDSOFAWESOME~AWESOMEINTRO_TEXT" title="$LEGENDSOFAWESOME~AWESOMEINTRO_TITLE" stars_plus="20" stars="20" color_plus="gold" id="legendsofawesome~awesomeintro" color="blue">
		<rewards>
			<reward value_plus="1500" value="armor_heavy_legendsofawesome~guardherald" feat="pass" type="item" type_plus="gold" goal="true"/>
			<reward value_plus="2000" value="200" feat="perfect" type="xp" type_plus="xp" goal="true"/>
		</rewards>
	</bonus>
</data>

When XML data like this is appended, the game strips off the <?xml> header at the top as well as the <data> envelope tag that every Defender's Quest XML file uses, and then inserts the rest of the contents at the very bottom of the base file with the same filename, right before the closing </data> tag.

This is an easy way to add new entries to simply-structured data files, such as the bonus battle registry.

Not all modifications are so easily expressed, however, which is where the _merge folder comes in handy.

/_merge

The /_merge folder is mainly meant for .xml files, and as the name implies, is means for merging new data into the middle of existing base files.

In this case, what the mod is doing is disabling the game's "original graphics" mode. (Defender's Quest DX includes new HD graphics as well as the original version's old sprite art, but for the sake of reducing complexity, we wanted to disable this secondary data set by default for mods created with the official game editor. Advanced modders can of course use whatever settings they want).

To do this we need to change an existing tag in the game's graphics.xml file, which controls various graphical settings. Here's a snippet of the base file:

<?xml version="1.0" encoding="utf-8" ?>
<data>
	<mode id="cutscenes" values="original,hd"/>
	<mode id="sprites" values="original,hd"/>
	
	<fontdef id="spell_icon" font="verdana" size="10" style="bold" color="white" outline="0xFF0000" border_quality="1" align="center"/>
	
	<scaling>
		<object id="overworld" height="400"/>		
		<object id="overworld_hd" height="540"/>	

We want to change this tag:

<mode id="sprites" values="original,hd"/>

To this:

<mode id="sprites" values="hd"/>

It'd be easy enough just to copy the file to the root directory, make that change, and call it a day, but it'd be nice to just do the one change directly.

Here's how it's done:

<?xml version="1.0" encoding="utf-8" ?>
<data>
	<mode id="sprites" values="hd">
		<merge key="id" value="sprites"/>
	</mode>
</data>

This file contains both data and merge instructions. The <merge> child tag tells the mod loader what to do, and will not be included in the final data. The actual payload is just this:

<mode id="sprites" values="hd"/>

The <merge> tag instructs the mod loader thus:

  • Look for any tags with the same name as my parent (in this case, <mode>)
  • Look within said tags for a "key" attribute (in this case, one named id)
  • Check if the key's value matches what I'm looking for (in this case, "sprites")

As soon as it finds the first match, it stops and merges the payload with the specified tag. Any attributes will be added to the base tag (overwriting any existing attributes with the same name, which in this case changes values from "original,hd" to just "hd", which is what we want). Furthermore, if the payload has child nodes, all of its children will be merged with the target tag as well.

.tsv files can be merged as well, but no logic needs to be supplied. In this case, the mod loader will look for any lines in the base file with the same flag names as those in the merge file, and replace them with the lines from the merge file.

Collisions

Another important part in an atomic mod system is avoiding collisions -- ideally we want everybody's mods to play nicely together. Nothing stops modders from creating incompatible mods in theory, especially if they mess with the data directly, but we can put some "guard rails" on the editor to keep this from happening trivially.

The easiest way to do this is to guard the internal string id references with prefixes. For instance, if the player has a mod named "myproject" and creates a battle with the id "mylevel", the actual reference id for the battle used internally is "myproject~mylevel". If the player changes the battle's id, or the project's id, all of these references are likewise updated and cleaned up properly. When creating a mod, the level editor will check Steam for registered mod IDs and warn you if you try to use one that's already taken. Little safeguards like this go a long way to preventing thorny issues before they happen and make sure that mods have a good chance of playing nicely together.