Adventures in World of Tanks modding

Modding World of Tanks is not a process for the faint of heart…

Mostly because there are no resources out there really. There’s Korean Random but you better be able to read Russian when you go there, and there’s the vbAddict developer wiki (contributed to by yours truly). Other than that, really nothing out there.

Given the replays without battle results thing and the mod I wanted to write for that, I figured I might as well use that as a little look at how it all works.

Before you start modding, there’s a few things you will need:

  • Knowledge of Python
  • A Python decompiler (uncompyle works) for Python 2.7
  • Patience
  • More patience

You’ll have to start off by decompiling all the .pyc files under res/scripts/ – these pretty much make up 80% of the game client’s logic, and that’s actually what you’ll be working with most of the time if you’re not doing any UI related mods. Since I don’t do UI stuff, I can’t really give you the details on that, but… someone probably can.

Once you have your decompiled scripts, the key is to figure out what you want to do. In my case, I want to upload a battle result when it’s viewed. I happen to know that battle results get cached on disk, and a quick little grep through the scripts soon finds a file in scripts/client/account_helpers/battleresultscache.pyc_dis (the _dis being the bit that gets added by the decompiler usually). Well, that’s exactly what we need! The interesting bit is this little piece here:

def save(accountName, battleResults):
    fileHandler = None
        arenaUniqueID = battleResults[0]
        folderName = getFolderName(accountName, arenaUniqueID)
        if not os.path.isdir(folderName):
        fileName = os.path.join(folderName, '%s.dat' % arenaUniqueID)
        fileHandler = open(fileName, 'wb')
        cPickle.dump((BATTLE_RESULTS_VERSION, battleResults), fileHandler, -1)
    if fileHandler is not None:

It appears that’s what actually saves the thing to disk. Which is just what we need, because whatever gets written to disk is also what we want to be uploading in the first place. Now, knowing what I know about replay files, the arenaUniqueID is something I can obtain from the raw replay stream, it’s part of the “arena” initialisation packet. The arena being the map you’re playing on.

Some more digging finds that this save method is never called as the result of an event; events are things that you can rather easily hook into by adding another handler, so that leaves us with two choices…

  1. Replace the entire battleresultscache.pyc with a custom version (installed in res_mods/0.8.11/scripts/client/account_helpers)
  2. Do some evil shenanigans to the existing battleresultscache.

I’m an evil bastard so I chose option #2. One advantage that Python has is that the method save in the package battleresultscache can in fact be altered, replaced, or otherwise messed with. For example:

from account_helpers import BattleResultsCache
# and yes, it is BattleResultsCache, Python maps it to the proper file

old_save =
# old_save now actually contains a pointer to the save method (well, not really but...)

I’m sure you can see where this is going, right? So, what I did is this:

from account_helpers import BattleResultsCache

std_save =

def __new_save(accountName, battleResults):
    print "Replacement calledn"
    std_save(accountName, battleResults)

        s_obj = (battleResults, BattleResultsCache.convertToFullForm(battleResults))
        j_obj = {
            "raw" : base64.b64encode(cPickle.dumps(battleResults, 2)),
            "full": base64.b64encode(cPickle.dumps(BattleResultsCache.convertToFullForm(battleResults), 2))
        jsonString = json.dumps(j_obj)
        submitter = BattleResultsSubmitter(jsonString)
        thread = Thread(target=submitter.submit);
    except Exception as e:
        LOG_CURRENT_EXCEPTION() = __new_save

What this does is that the original save method is replaced with my customized one; but my customized one calls the existing save method before it does anything itself, just so it doesn’t break the client – since after all, you do want to see the battle result on the screen ;)

The Base 64 encoding is due to a pickle not being ASCII text, and it needs to be dumped as a pickle because flat out dumping it causes issues with the JSON encoder for some reason. It’s ugly, but it works. And regardless of the security implications of using pickles, happens to be in the merry old position of having an unpickler that’s written in Perl, so it’s 100% safe. (Just figured I’d mention that…)

The BattleResultsSubmitter is a simple class that does some prep work to submit something in a separate thread:

class BattleResultsSubmitter(object):
    def __init__(self, jsonString):
        self.jsonString = jsonString

    def submit(self):
        u = urlparse('')
        print('[BattleResultsSubmitter.submit] to ' + repr(u))
            conn = httplib.HTTPConnection(u.netloc, timeout=5)
            conn.request('POST', u.path, self.jsonString, { "Content-Type": "application/json" })
            response = conn.getresponse()
            print(response.status, response.reason)
        except Exception as e:

And that’s what submits the whole thing to in the background. Once it arrives there, I decode the Base 64 string, unpickle the data, and store it in the database for later usage.

There’s still one thing left to do and that is retrieving a battle result regardless of whether you’ve clicked the ‘view details’ button in the game; right now you still need to view the result in order for the game to actually go fetch the full result. However, if you happen to use vbAddict’s ADU, all you have to do is tell it to install the “BRR” mod; this mod should in theory work together with mine, but that’s theory, and untested.

Now that you have your .py file, the next step is to recompile it, you can do this by using pycompile -V 2.7 which will get you a yourfile.pyc file. You will then have to get this file loaded somehow as a module. Most mods supply a scripts/client/CameraNode.pyc file that contains a primitive mod loader, so copy one from an existing mod and zip it up with your own; you will want to place your own module in the scripts/client/mods folder, and it will automatically be picked up by the CameraNode mod loader.

There’s more to it than that, though, but maybe this will give you a nice start.

Ben van Staveren

A somewhat odd traveler, wanderer, wonderer and all-around sarcastic pain in peoples' asses, most of the time. Keeps busy with IT security, random acts of geekery, and other things that have nothing whatsoever to do with IT, computers, or electronics. Can currently be found residing in Jakarta, Indonesia.