A maintainable client mod changes the smallest useful surface. Prefer an event or a wrapper around one callable over shipping a replacement copy of a game module.

First milestone

By the end of this chapter, you should understand where a mod is placed, when its code runs, how a safe wrapper behaves, and how to confirm the package contains the correct path.

Five-minute loader check

Before adding hooks or UI code, confirm that the game can discover and execute one minimal mod file.

  1. Create a file named mod_hello.py.
  2. Place it inside the archive at scripts/client/gui/mods/mod_hello.py.
  3. Launch the client and wait until the hangar loads.
  4. Open game.log and search for [Hello Mod] loaded.
def init():
    print '[Hello Mod] loaded'


def fini():
    print '[Hello Mod] unloaded'
Expected result: the load message appears once. If it does not, check the archive path and file name before debugging anything more complicated.

Most common reasons the loader check fails

  • The archive contains an extra top-level folder before res/scripts/....
  • The file is named mod_hello.py.txt because file extensions are hidden.
  • The compiled file or source file is placed in the wrong client-version directory.
  • The package was rebuilt but the old archive is still installed.
  • A Python exception appears earlier in game.log and prevents the file from finishing.
Debug in this order: package path, filename, log exception, then code behavior. Do not start investigating hooks until the basic load message works.

What you need

  • A Python runtime compatible with the client build you target. World of Tanks client scripts use Python 2.7-era semantics, but do not assume that one historical patch release is universally correct.
  • Readable client source or decompiled scripts for the exact game version you are investigating.
  • A clean test installation, access to the client logs, and a repeatable packaging process.

Where a mod is loaded

Authored client mods normally live below scripts/client/gui/mods and use a filename beginning with mod_.

scripts/
└── client/
    └── gui/
        └── mods/
            └── mod_example.py

Inside a packaged .wotmod, the archive includes the resource root:

res/
└── scripts/
    └── client/
        └── gui/
            └── mods/
                └── mod_example.pyc

Do not replace an original client script merely to change one method. A copied module freezes an entire old implementation and is much more likely to fail after an update.

Import, init, and fini

Top-level statements run when the module is imported. Keep import-time work limited to definitions and safe constants. Use loader lifecycle functions for owned setup and teardown.

_started = False

def init():
    global _started
    if _started:
        return
    _started = True
    install_hooks()
    subscribe_events()

def fini():
    global _started
    if not _started:
        return
    cancel_callbacks()
    unsubscribe_events()
    restore_owned_hooks()
    _started = False

init() should be idempotent. fini() should undo only resources owned by this mod and should remain safe after partial startup.

Wrap a function instead of replacing a module

Capture the callable currently installed, preserve its contract, and restore it only if your wrapper still owns the slot.

_original_method = None
_wrapper = None

def install_hook():
    global _original_method, _wrapper
    if _wrapper is not None:
        return

    _original_method = TargetClass.targetMethod

    def wrapped(self, *args, **kwargs):
        result = _original_method(self, *args, **kwargs)
        after_original_call(self, result)
        return result

    wrapped.__mod_owner__ = 'example_mod'
    _wrapper = wrapped
    TargetClass.targetMethod = wrapped

def restore_hook():
    global _original_method, _wrapper
    if _wrapper is not None and TargetClass.targetMethod is _wrapper:
        TargetClass.targetMethod = _original_method
    _original_method = None
    _wrapper = None

Call before

Use this when your state must exist before the client method runs.

Call after

Use this when you need the original result or fully initialized base state.

Replace deliberately

Skip the original only when suppressing or redefining its behavior is the explicit feature.

Private-looking methods

A leading underscore is a convention, not access control. Names beginning with two underscores are name-mangled by Python. A method written as __method inside Avatar is normally stored as _Avatar__method.

Use flexible arguments carefully

*args and **kwargs reduce breakage when optional parameters are added, and forwarding them preserves the original call contract.

def wrapped(self, *args, **kwargs):
    result = original(self, *args, **kwargs)
    return result

They do not make a hook immune to updates. A renamed method, changed return type, reordered side effect, new ownership model, or removed call site can still invalidate the mod. Log failures and re-verify after every client update.

Schedule work through BigWorld

BigWorld.callback(delay, callable) schedules a callable on the game thread. Use functools.partial when arguments must be bound, but keep the returned callback ID so it can be cancelled.

import BigWorld
from functools import partial

_callback_id = None
_generation = 0

def schedule_refresh(vehicle_id):
    global _callback_id
    cancel_refresh()
    token = _generation
    _callback_id = BigWorld.callback(
        0.25,
        partial(_run_refresh, token, vehicle_id)
    )

def _run_refresh(token, vehicle_id):
    global _callback_id
    _callback_id = None
    if token != _generation:
        return
    refresh_vehicle(vehicle_id)

def cancel_refresh():
    global _callback_id
    if _callback_id is not None:
        BigWorld.cancelCallback(_callback_id)
        _callback_id = None

Prefer events when an event already owns the transition

Before patching a method, look for an existing event on the responsible controller, event bus, or lifecycle object. Some legacy lifecycle events are exposed through modules such as gui.shared.personality, but the correct source depends on the current client and feature.

def start():
    lifecycle_events.onAvatarReady += _on_avatar_ready

def stop():
    lifecycle_events.onAvatarReady -= _on_avatar_ready

def _on_avatar_ready(*args, **kwargs):
    begin_battle_feature()

Do not rely on a function merely sharing an event name unless you have verified that the loader performs that discovery. Most event systems require an explicit subscription and the exact same handler identity for unsubscription.

Compile and package

Normal client packages commonly contain compiled Python bytecode. Compile with the interpreter expected by the target client, then preserve the directory structure inside the archive.

python -m compileall path/to/mod_example.py
  • Remove stale bytecode before rebuilding.
  • Confirm the generated filename and package path.
  • Open the final archive and inspect its root rather than trusting the build script.
  • Test startup, account switching, battle entry, battle exit, and client shutdown.

Beginner checkpoint

  • The client launches without a new Python exception.
  • [Hello Mod] loaded appears once in game.log.
  • You know which line runs at import, which runs in init(), and which runs in fini().
  • You can remove the archive and return the client to its original state.
  • You inspected the final archive and confirmed the exact internal path.

Minimal release checklist

  • The mod does not replace a complete original client module.
  • Initialization is safe when called twice.
  • Every event, callback, view, hook, and worker has a teardown path.
  • The original method receives all arguments and its return value is preserved unless replacement is intentional.
  • Delayed work rejects stale account, arena, vehicle, or view contexts.
  • The package contains the expected res/scripts/client/gui/mods path.
  • The current game build has been tested rather than inferred from an old source mirror.

Next: build a complete production mod

Continue with Level 1: Configurable Garage Greeting. The production tutorial series turns this loader check into complete, working mods and grows from configuration and garage events into battle lifecycle, Gameface, persistence, local integration, analytics, and replay-aware telemetry.

Each level includes requirements, source-investigation pointers, fully annotated code, packaging, a test matrix, failure diagnosis, and maintenance guidance. Follow the levels in order if you are new; experienced readers can jump to the system they need.