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.

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 at res/.
  • 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_*.py file 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() and fini() 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.