For beginner and small-to-medium mods, one lower-case mod_*.py file is
a practical organization recipe. It is not a World of Tanks runtime law; use it when it makes
ownership, imports, and teardown easier to verify.
Source file versus package path
In this pattern, keep the authored Python implementation in one mod_*.py file under
scripts/client/gui/mods. A distributable archive normally mirrors that location
below res/. See First Mod & Safe Patching for a complete package tree and
compilation checklist.
The complete source layout
scripts/
└── client/
└── gui/
└── mods/
└── mod_example.py
The filename is the loader entry point and, for this recipe, the implementation. A descriptive, collision-resistant filename is enough when the feature does not need a mod-owned package for external assets or unusually large internal structure.
Recommended order inside the file
| Section | What belongs there | Why |
|---|---|---|
| Imports and metadata | Standard modules, stable client imports, logger, version, and immutable constants | Makes compatibility assumptions visible immediately |
| Owned runtime state | Started flag, callback handles, current context token, view references, caches | Keeps all mutable ownership in one obvious place |
| Pure helpers | Validation, formatting, identifier conversion, calculations | Easy to test and safe to call from several handlers |
| Runtime helpers | Service lookup, scheduling, view creation, persistence, network handoff | Separates fragile client access from pure logic |
| Hooks and handlers | Wrappers, event callbacks, UI callbacks, async result handlers | Keeps externally invoked code together |
| Context lifecycle | Account, arena, vehicle, or view start/stop functions | Shows exactly when scoped state is created and destroyed |
| Loader lifecycle | init() and fini() |
Leaves the public entry points at the bottom after every dependency is defined |
Single-file skeleton
Use section comments and small functions so the file remains readable without requiring extra helper modules.
import logging
LOG = logging.getLogger(__name__)
VERSION = '1.0.0'
# -----------------------------------------------------------------------------
# Owned runtime state
# -----------------------------------------------------------------------------
_started = False
_context_token = 0
_callback_ids = set()
_original_method = None
_installed_wrapper = None
# -----------------------------------------------------------------------------
# Pure helpers
# -----------------------------------------------------------------------------
def clamp(value, minimum, maximum):
return max(minimum, min(maximum, value))
# -----------------------------------------------------------------------------
# Runtime helpers
# -----------------------------------------------------------------------------
def resolve_optional_service():
try:
from optional_client_area import get_service
except ImportError:
return None
try:
return get_service()
except Exception:
LOG.exception('Optional service lookup failed')
return None
def cancel_callbacks():
while _callback_ids:
callback_id = _callback_ids.pop()
try:
cancel_engine_callback(callback_id)
except Exception:
LOG.exception('Callback cancellation failed')
# -----------------------------------------------------------------------------
# Hooks and event handlers
# -----------------------------------------------------------------------------
def on_runtime_event(*args, **kwargs):
if not _started:
return
# Read current context and perform the smallest necessary action.
def install_hooks():
global _original_method, _installed_wrapper
if _installed_wrapper is not None:
return
current = TargetClass.target_method
_original_method = current
def wrapper(instance, *args, **kwargs):
result = current(instance, *args, **kwargs)
try:
on_runtime_event(instance)
except Exception:
LOG.exception('Hook handler failed')
return result
wrapper._single_file_owner = __name__
_installed_wrapper = wrapper
TargetClass.target_method = wrapper
def restore_hooks():
global _original_method, _installed_wrapper
if _installed_wrapper is None:
return
if TargetClass.target_method is _installed_wrapper:
TargetClass.target_method = _original_method
_original_method = None
_installed_wrapper = None
# -----------------------------------------------------------------------------
# Context lifecycle
# -----------------------------------------------------------------------------
def start_context():
global _context_token
_context_token += 1
token = _context_token
# Create only state owned by this context.
return token
def stop_context():
global _context_token
_context_token += 1 # Invalidates delayed results from the old context.
cancel_callbacks()
# Remove owned views, listeners, and context state here.
# -----------------------------------------------------------------------------
# Loader lifecycle
# -----------------------------------------------------------------------------
def init():
global _started
if _started:
return
_started = True
try:
install_hooks()
subscribe_lifecycle_events()
except Exception:
LOG.exception('Initialization failed')
fini()
def fini():
global _started
if not _started:
return
stop_context()
unsubscribe_lifecycle_events()
restore_hooks()
_started = False
Keep the file readable as it grows
Prefer small functions
A function should usually perform one client lookup, one state transition, or one rendering action—not all three.
Use section dividers
Stable headings make a long file easier to scan without introducing folders.
Name by ownership
Use names such as start_arena_state, destroy_view, and cancel_callbacks
so teardown is discoverable.
Keep data schemas compact
Settings defaults and migration functions can stay near persistence helpers instead of living in a separate settings module.
Import safely in one file
- Keep stable, inexpensive imports at the top.
- Import optional or mode-specific client areas inside the narrow helper that uses them.
- Never let one unavailable optional system prevent the rest of the file from loading.
- Do not depend on another mod's helper module or on alphabetical loader order.
- Resolve runtime services when their owning lifecycle is active, not merely because this file was imported.
def perform_optional_action():
service = resolve_optional_service()
if service is None:
return False
try:
service.perform_action()
return True
except Exception:
LOG.exception('Optional action failed')
return False
Persistent data is still separate from source
Keeping the mod implementation in one file does not mean writing user state back into that file. Store settings and runtime data in the established preferences location, validate every loaded value, and write atomically.
- Define defaults and schema version constants near the top of the source file.
- Keep load, validate, migrate, and save functions together in the persistence section.
- Use a unique preferences filename or section.
- Never store credentials, access tokens, or private endpoints in logs.
Common packaging mistakes
my_mod/res/scripts/...is stored in the archive instead of starting directly atres/.- The source file was edited but stale bytecode was packaged.
- The filename does not begin with the loader-supported
mod_prefix. - A helper import points to a local development file that was not included.
- The archive was copied to the wrong game-version directory.
Single-file review checklist
- The chosen pattern is intentional: a single lower-case
mod_*.pyfile keeps this mod easier to inspect and package. - The file is ordered into imports, state, helpers, hooks, context lifecycle, and loader lifecycle.
- No shared loose helper module or load-order-dependent helper import is required.
- Optional game imports are local and fail independently.
init()andfini()are idempotent and reverse every owned integration.- Every callback, listener, view, worker, and hook has a teardown path in the same file.
- Persistent user data lives outside replaceable source files.