Good runtime architecture is only half of a reliable mod. The surrounding workflow must make it easy to locate the owning system, inspect assets, catch syntax errors, package the correct paths, and compare client builds after an update.

Use a reproducible workspace

  • Keep editable source outside the live game directory and deploy generated files into a dedicated test installation.
  • Use an interpreter compatible with the client build and run a syntax-only compile before launching the game.
  • Give every mod a unique log tag so its messages can be filtered inside game.log.
  • Keep extracted game resources read-only. Copy only the files you intentionally modify into your project.
Windows build check
"C:\Python27\python.exe" -m py_compile mod_example.py

if errorlevel 1 exit /b 1
copy /Y mod_example.pyc build\res\scripts\client\gui\mods\

Locate the layer that owns a feature

Search the current client source before editing resources. The Python controller usually reveals whether the feature belongs to classic Scaleform, Wulf/Gameface, an account service, or a battle-only object.

What you find Likely owner First action
gui/Scaleform/daapi/view/... Classic Scaleform/Flash Inspect the Python-to-DAAPI update path before replacing a SWF.
gui/impl/... Wulf or Gameface Inspect view lifecycle and ViewModel ownership.
Avatar.py, arena objects, entity classes Battle runtime Wait for the correct avatar/arena readiness event.
Generated resource accessors Packaged UI/resource contract Verify the generated identifier and current package contents.

Inspect packaged resources safely

Client .pkg files are ZIP-compatible archives. Extract only the resource you need and never edit the original package in place.

Extract one packaged file
import os
import zipfile

PACKAGE = 'res/packages/gui-part1.pkg'
RESOURCE = 'gui/flash/battleCrosshairsApp.swf'
OUTPUT = 'workspace/originals'

if not os.path.isdir(OUTPUT):
    os.makedirs(OUTPUT)

with zipfile.ZipFile(PACKAGE, 'r') as archive:
    archive.extract(RESOURCE, OUTPUT)

print('[extract] wrote ' + os.path.join(OUTPUT, RESOURCE))

For icons and textures, preserve the original virtual path under res_mods/<version>/ or the res/ root of a .wotmod. Resource overrides only work when the path matches exactly.

Choose the least invasive asset strategy

  1. Prefer an existing Python or ViewModel hook when the change is data-driven.
  2. Prefer adding your own resource over replacing a complete original resource.
  3. Replace a SWF or other packaged asset only when the visual implementation itself must change.
  4. Record the original package and resource path so an update comparison is straightforward.

For sound, distinguish between playing an existing registered event and shipping a new sound bank. Existing event names are much simpler and less fragile; custom banks add tooling, packaging, and registration requirements.

Build the .wotmod from a staging directory

A .wotmod is a ZIP archive with a strict internal root. Build from a clean staging directory so stale files cannot leak into a release.

example.wotmodZIP-compatible archive
res/scripts/client/gui/mods/Compiled Python entry points
res/gui/...UI and resource overrides
meta.xmlOnly when required by the chosen package format
Deterministic package builder
import os
import zipfile

BUILD_ROOT = 'build'
OUTPUT = 'dist/example.wotmod'

if not os.path.isdir('dist'):
    os.makedirs('dist')

with zipfile.ZipFile(OUTPUT, 'w', zipfile.ZIP_STORED) as package:
    for base, dirs, files in os.walk(BUILD_ROOT):
        dirs.sort()
        files.sort()
        for filename in files:
            path = os.path.join(base, filename)
            archive_name = os.path.relpath(path, BUILD_ROOT).replace('\\', '/')
            package.write(path, archive_name)

print('[build] wrote ' + OUTPUT)

Package boundaries

A package is only the delivery container. Keep the mod implementation easy to inspect: one Python source file, plus any required UI assets, native resource declarations or legacy maps, images, sounds, or localization files.

Native extensions multiply compatibility boundaries

A native component that captures the Direct3D swap chain, hooks the game audio output, uses Media Foundation, or exposes C++ through a Python binding is not just another packaged asset. It adds renderer, sound engine, Windows API, compiler ABI, hook framework, GPU, and client-build contracts that Python-only mods do not own.

  • Adopt only the smallest required capability—capture, screenshot, playback, or editing—rather than importing an entire historical framework.
  • Define explicit teardown for presentation hooks, audio hooks, workers, encoders, decoders, media resources, and Python wrappers.
  • Keep initialization, encoding, decoding, and finalization off the game thread; return game-owned actions through a verified client callback.
  • Test resolution and display-mode changes, output-device changes, absent or surround audio, mixed GPUs, battle and replay exit, interrupted finalization, and client shutdown.
  • An allow_newer manifest policy permits loading; it does not prove ABI or hook compatibility with a newer client.

Use a repeatable update check

  • Compare the new client source against the last verified build for every imported private module and patched callable.
  • Recheck method signatures, return behavior, lifecycle timing, and the code that consumes the result.
  • Compare any replaced resource with the newly shipped original.
  • Clear stale bytecode and test from a clean deployment.
  • Test startup, hangar entry, account changes, battle entry, battle exit, replay mode where relevant, and shutdown.
Diff sourceImports, methods, consumers
→
Rebuild cleanlyNo stale bytecode or assets
→
Run transitionsLobby, battle, teardown
→
Inspect logsErrors, duplicates, leaks

Replace development packages after process exit

A development bridge, log stream, or localhost listener can close before the operating system releases the loaded package. Treat the tracked client process as the ownership boundary: wait for that exact process to exit before replacing its .wotmod.

  • Track the process you launched instead of inferring shutdown from a disconnected helper.
  • Stage the replacement, validate the archive, and promote it only after the client process has exited.
  • Compare normalized member paths and payload bytes when deciding whether a rebuild changed; ZIP timestamps alone can differ while the effective package is identical.
  • Do not delete or overwrite a package still owned by a running client.

This is a local development safety rule, not permission to install into an unverified client or publish a package.

Debug in layers

  1. Loader: confirm the file name, package path, and a single startup log line.
  2. Import: resolve the first traceback from the bottom upward.
  3. Lifecycle: log account, avatar, arena, and view transitions.
  4. Ownership: prove which object currently owns the event, model, or UI surface.
  5. Rendering: only then inspect CSS, SWF layout, resource paths, or hit-test geometry.

See Runtime Debugging for transition tracing and Source Verification for evidence standards.

Archive and filesystem safety

Packaging and local tools should handle paths as untrusted data. Normalize paths, reject traversal, distinguish authored source paths from packaged VFS paths, and never infer ownership of an extracted file from a similar name.

  • Package resources at the exact VFS path the client will resolve.
  • Keep authoring/copy directories separate from runtime paths shown in code or resource maps.
  • Validate archive members before extraction and reject absolute paths or .. traversal.
  • Write mod-owned JSON, logs, and caches under an owned directory beside the preferences root, not into the client preference XML.
  • Use atomic replace or backup-on-write for persistent files that may be edited while the client is running.