The finished overlay traces only the local player vehicle. It normalizes X/Z against the current arena bounds, flips the vertical axis exactly once for SVG, keeps a bounded trail, and stops cleanly on page disposal, avatar loss, or disconnect.

What we’re building

A passive 260×260 Gameface map card. Python samples the current player vehicle every 0.25 seconds and publishes normalized primitives; JavaScript draws the trail in an SVG viewBox. No enemy positions, roster data, terrain height, or native minimap entries are read or altered.

Final result

  • A route polyline and current-position marker.
  • Normalized coordinates independent of map size.
  • At most 96 points and four payloads per second.
  • Respawn-safe entity re-resolution.
  • Complete Python, native resource, HTML, CSS, JavaScript, and manifest examples.

What you’ll learn

  • Why X/Z is the horizontal world plane and Y is elevation.
  • How current minimap grid code proves that UI rows invert world Z.
  • Why normalized tactical state belongs on the Python side while pixels belong to the renderer.
  • How callback generation tokens close cancellation races.
  • Why IDs and copied floats survive lifecycle changes better than entity references.

Difficulty and prerequisites

Level 6 — Spatial systems. Complete the Level 5 entity tutorial and understand Gameface readiness, page ownership, engine callbacks, and entity re-resolution.

Requirements and constraints

Functional

  1. Activate only with a populated battle page and current PlayerAvatar.
  2. Read current arena bounds once per battle.
  3. Re-resolve playerVehicleID for every sample.
  4. Append only valid in-bounds normalized samples.
  5. Bound memory and publish only changed trails.

Non-goals

  • No enemy or ally tracking.
  • No claim that the line is a replay, server path, or authoritative movement record.
  • No modification of the native minimap.

Acceptance criteria

  • Bottom-left maps to (0,100) and top-right to (100,0).
  • Zero-width or zero-height bounds disable sampling.
  • Missing, stopped, or wrong-space player entities do not append points.
  • Movement below the threshold does not grow the trail.
  • The 97th accepted point discards the oldest.
  • One owner has at most one sample callback and one parent-readiness callback.
  • Every stale callback fails its generation check.
  • Battle exit retains no avatar, vehicle, window, model, or callback.

Architecture

arenaType.boundingBox ──→ validate bounds ──┐
playerVehicleID ──→ BigWorld.entity ──→ X/Z ├─→ normalize + flip Z ─→ bounded float pairs
BigWorld.callback(0.25) ────────────────────┘                         ↓ JSON
                                                        ViewModel → Gameface SVG

SharedPage._dispose ─→ generation++ ─→ cancel callbacks ─→ destroy view

Researching the implementation

Current source exposes bounds through sessionProvider.arenaVisitor.type.getBoundingBox(); the visitor returns arenaType.boundingBox. Native minimap_utils.getCellIdFromPosition uses X and Z, clamps to those bounds, and calculates the row as dimensions - (... + 1). That inversion is the evidence for screenY = 1 - normalizedZ. The tutorial deliberately does not reuse minimap pixel constants because its SVG owns a different rectangle.

Source lookup

Use these pages to investigate both the spatial math and the recurring sampler:

Useful exact searches are getBoundingBox, arenaType.boundingBox, getCellIdFromPosition, playerVehicleID, BigWorld.entity, and BigWorld.callback. Treat them as investigation entry points and verify current producers, callers, callback signatures, and lifecycle behavior.

Lifecycle and ownership

  1. Install global context listeners and the guarded page wrapper once.
  2. On page population, validate the current avatar and bounds.
  3. Start one self-rescheduling callback carrying the current generation.
  4. Resolve the current player vehicle just for that callback body.
  5. Copy floats, publish, clear the callback handle, then schedule the successor.
  6. Dispose before native page teardown; avatar exit and disconnect are idempotent fallbacks.

State and data-authority model

StateOwnerMeaningInvalidation
Arena boundsBattle generationCurrent arena X/Z extentBattle exit
Player vehicle IDAvatarCurrent lookup keyRe-read each tick
TrailModAccepted local observationsBattle exit / oldest point
SVG pixelsFrontendPresentation onlyView teardown

Implementation walkthrough

Normalize once

nx=(x-left)/(right-left); ny=1-(z-bottom)/(top-bottom). Reject instead of silently projecting wildly invalid values; clamp only the tiny floating-point edge around a verified in-bounds sample.

Re-resolve, never retain

Respawn and leave-world transitions invalidate entity objects. The callback reads the current ID, resolves it, checks readiness and space, copies X/Z, then releases it.

Generation guard

Cancellation can race execution. Cleanup increments the generation; every callback checks it before reading state or scheduling its successor.

Complete source code

# -*- coding: utf-8 -*-
"""Render a bounded trace of the local player vehicle."""
from __future__ import absolute_import, unicode_literals

# Reader map:
#   1. The Wulf shell transports normalized primitive points to Gameface.
#   2. _RouteTrace owns sampling, bounds, callbacks, and the rolling trail.
#   3. SharedPage wrappers define when the visualization may exist.

import json
import logging
import math

import BigWorld
from Avatar import PlayerAvatar
from PlayerEvents import g_playerEvents
from frameworks.wulf import (ViewFlags, ViewModel, ViewSettings, WindowFlags,
                             WindowLayer, WindowStatus)
from gui.Scaleform.daapi.view.battle.shared.page import SharedPage
from gui.impl.pub import ViewImpl, WindowImpl
from helpers import dependency
from skeletons.gui.impl import IGuiLoader

from gui.impl.gen import R

__version__ = '1.0.1'
_LOGGER = logging.getLogger(__name__)
_MOD_NAME = 'personal_route_trace'
_LAYOUT = R.mods.tutorial.PersonalRouteTrace.views.PersonalRouteTrace
_WIDTH = _HEIGHT = 260
_SAMPLE_SECONDS = 0.25
_MAX_POINTS = 96
_MIN_DISTANCE_SQ = 0.000004
_MAX_PARENT_RETRIES = 80


class _TraceModel(ViewModel):
    """Expose one serialized route payload and a frontend-ready command."""
    def __init__(self, payload):
        self._payload = payload
        super(_TraceModel, self).__init__(properties=1, commands=1)

    def _initialize(self):
        super(_TraceModel, self)._initialize()
        self._addStringProperty('payload', self._payload)
        self.onReady = self._addCommand('onReady')

    def setPayload(self, value):
        self._setString(0, value)


class _TraceView(ViewImpl):
    """Connect the model command to the route owner."""
    def __init__(self, owner):
        self._owner = owner
        model = _TraceModel(owner.payload)
        owner.attach_model(model)
        super(_TraceView, self).__init__(ViewSettings(
            layoutID=_LAYOUT(), flags=ViewFlags.VIEW, model=model))

    def _getEvents(self):
        return ((self.getViewModel().onReady, self._owner.on_content_ready),)

    def _finalize(self):
        self._owner.attach_model(None)
        super(_TraceView, self)._finalize()


class _TraceWindow(WindowImpl):
    """Report when the native Wulf window is ready to be shown."""
    def __init__(self, content, parent, owner):
        self._owner = owner
        super(_TraceWindow, self).__init__(
            WindowFlags.WINDOW, content=content, layer=WindowLayer.WINDOW,
            name='PersonalRouteTrace', parent=parent)

    def _onReady(self):
        self._owner.on_window_ready()


class _RouteTrace(object):
    """Own one battle generation, sampler, rolling route, and UI surface."""
    def __init__(self):
        self._installed = False
        self._active = False
        self._avatar = None
        self._bounds = None
        self._space_id = None
        self._points = []
        self._generation = 0
        self._sample_callback_id = None
        self._parent_callback_id = None
        self._window = self._model = None
        self._window_ready = self._content_ready = False
        self._parent_size = None
        self._last_payload = None
        self._enter_handler = self._on_avatar_enter
        self._exit_handler = self._on_avatar_exit
        self._disconnect_handler = self._on_disconnect

    @property
    def payload(self):
        return json.dumps({'points': self._points}, separators=(',', ':'))

    def start(self):
        if self._installed:
            return
        try:
            layout_id = _LAYOUT()
        except Exception:
            _LOGGER.exception('%s: native layout lookup failed', _MOD_NAME)
            return
        if layout_id < 0:
            _LOGGER.error('%s: native layout declaration is missing', _MOD_NAME)
            return
        g_playerEvents.onAvatarBecomePlayer += self._enter_handler
        g_playerEvents.onAvatarBecomeNonPlayer += self._exit_handler
        g_playerEvents.onDisconnected += self._disconnect_handler
        self._installed = True
        _LOGGER.info('%s %s: initialized', _MOD_NAME, __version__)

    def stop(self):
        if self._installed:
            g_playerEvents.onAvatarBecomePlayer -= self._enter_handler
            g_playerEvents.onAvatarBecomeNonPlayer -= self._exit_handler
            g_playerEvents.onDisconnected -= self._disconnect_handler
            self._installed = False
        self.deactivate()

    def _on_avatar_enter(self):
        self.deactivate()

    def _on_avatar_exit(self):
        self.deactivate()

    def _on_disconnect(self):
        self.deactivate()

    def on_page_ready(self):
        """Validate the current battle and start exactly one sampler."""
        avatar = BigWorld.player()
        if not isinstance(avatar, PlayerAvatar):
            return
        arena = getattr(avatar, 'arena', None)
        arena_type = getattr(arena, 'arenaType', None)
        bounds = getattr(arena_type, 'boundingBox', None)
        if not self._valid_bounds(bounds):
            _LOGGER.error('%s: invalid arena bounds %r', _MOD_NAME, bounds)
            return
        self.deactivate()
        self._active = True
        self._avatar = avatar
        self._bounds = bounds
        self._space_id = getattr(avatar, 'spaceID', None)
        generation = self._generation
        self._sample(generation)
        self._ensure_window(generation)

    @staticmethod
    def _valid_bounds(bounds):
        try:
            left, bottom = float(bounds[0][0]), float(bounds[0][1])
            right, top = float(bounds[1][0]), float(bounds[1][1])
            if not _finite_values(left, bottom, right, top):
                return False
            width, height = right - left, top - bottom
            return (_finite_values(width, height) and width > 0 and height > 0)
        except (IndexError, TypeError, ValueError, OverflowError):
            return False

    def _sample(self, generation):
        """Resolve the current vehicle briefly and copy only X/Z floats."""
        self._sample_callback_id = None
        if generation != self._generation or not self._active:
            return
        try:
            vehicle_id = getattr(self._avatar, 'playerVehicleID', None)
            vehicle = BigWorld.entity(vehicle_id) if vehicle_id else None
            if (vehicle is not None and getattr(vehicle, 'isStarted', False) and
                    getattr(vehicle, 'spaceID', None) == self._space_id):
                position = vehicle.position
                point = self._normalize(float(position.x), float(position.z))
                if point is not None and self._accept(point):
                    self.publish()
        except Exception:
            _LOGGER.exception('%s: sample failed', _MOD_NAME)
        if generation == self._generation and self._active:
            self._sample_callback_id = BigWorld.callback(
                _SAMPLE_SECONDS, lambda: self._sample(generation))

    def _normalize(self, x, z):
        """Map arena X/Z into renderer-owned [0,1] coordinates."""
        bottom_left, top_right = self._bounds
        left, bottom = float(bottom_left[0]), float(bottom_left[1])
        right, top = float(top_right[0]), float(top_right[1])
        if not _finite_values(left, bottom, right, top, x, z):
            return None
        width, height = right - left, top - bottom
        if not _finite_values(width, height) or width <= 0 or height <= 0:
            return None
        nx = (x - left) / width
        nz = (z - bottom) / height
        if not _finite_values(nx, nz):
            return None
        if nx < -0.001 or nx > 1.001 or nz < -0.001 or nz > 1.001:
            return None
        return [round(min(1.0, max(0.0, nx)), 4),
                round(1.0 - min(1.0, max(0.0, nz)), 4)]

    def _accept(self, point):
        """Ignore tiny movement and retain only the newest bounded trail."""
        if self._points:
            dx = point[0] - self._points[-1][0]
            dy = point[1] - self._points[-1][1]
            if dx * dx + dy * dy < _MIN_DISTANCE_SQ:
                return False
        self._points.append(point)
        if len(self._points) > _MAX_POINTS:
            # Old points fall off so payload and SVG rendering stay finite.
            del self._points[0]
        return True

    def _ensure_window(self, generation):
        self._load_when_parent_ready(generation, 0)

    def _load_when_parent_ready(self, generation, retry):
        """Retry parent readiness without reviving an old battle generation."""
        self._parent_callback_id = None
        if generation != self._generation or not self._active or self._window:
            return
        parent = dependency.instance(IGuiLoader).windowsManager.getMainWindow()
        if (parent is None or parent.proxy is None or
                parent.windowStatus != WindowStatus.LOADED):
            if retry < _MAX_PARENT_RETRIES:
                self._parent_callback_id = BigWorld.callback(
                    0.1, lambda: self._load_when_parent_ready(generation, retry + 1))
            else:
                _LOGGER.error('%s: main-window readiness timed out', _MOD_NAME)
            return
        self._parent_size = tuple(parent.size)
        self._window = _TraceWindow(_TraceView(self), parent, self)
        self._window.load()

    def attach_model(self, model):
        self._model = model
        if model is not None:
            self.publish()

    def on_window_ready(self):
        self._window_ready = True
        self._show_if_ready()

    def on_content_ready(self):
        self._content_ready = True
        self._show_if_ready()

    def _show_if_ready(self):
        if not (self._window and self._window_ready and self._content_ready and self._active):
            return
        parent_width, _ = self._parent_size
        # Leave the top-right slot free for compact status/census overlays.
        self._window.move(max(0, parent_width - self._window.size[0] - 24), 240)
        if self._window.isHidden():
            self._window.show(False)

    def publish(self):
        payload = self.payload
        if payload == self._last_payload:
            return
        self._last_payload = payload
        if self._model is not None:
            with self._model.transaction() as model:
                model.setPayload(payload)

    def deactivate(self):
        """Invalidate callbacks first, then release model, window, and state."""
        self._active = False
        self._generation += 1
        for name in ('_sample_callback_id', '_parent_callback_id'):
            callback_id = getattr(self, name)
            if callback_id is not None:
                BigWorld.cancelCallback(callback_id)
                setattr(self, name, None)
        if self._window is not None:
            self._window.destroy()
        self._window = self._model = None
        self._window_ready = self._content_ready = False
        self._avatar = self._bounds = self._space_id = self._parent_size = None
        self._points = []
        self._last_payload = None


_TRACE = _RouteTrace()
_INSTALLED = False
_ORIGINAL_POPULATE = _ORIGINAL_DISPOSE = None


def _finite_values(*values):
    return all(not math.isnan(value) and not math.isinf(value)
               for value in values)


def _page_populate(self, *args, **kwargs):
    """Activate only after native page population succeeds."""
    result = _ORIGINAL_POPULATE(self, *args, **kwargs)
    try:
        _TRACE.on_page_ready()
    except Exception:
        _LOGGER.exception('%s: page-populate handling failed', _MOD_NAME)
    return result


def _page_dispose(self, *args, **kwargs):
    """Destroy route ownership before native page disposal continues."""
    try:
        _TRACE.deactivate()
    except Exception:
        _LOGGER.exception('%s: page-dispose handling failed', _MOD_NAME)
    return _ORIGINAL_DISPOSE(self, *args, **kwargs)


def init():
    global _INSTALLED, _ORIGINAL_POPULATE, _ORIGINAL_DISPOSE
    if _INSTALLED:
        return
    try:
        _ORIGINAL_POPULATE = SharedPage._populate
        _ORIGINAL_DISPOSE = SharedPage._dispose
        SharedPage._populate = _page_populate
        SharedPage._dispose = _page_dispose
        _TRACE.start()
        _INSTALLED = True
    except Exception:
        _LOGGER.exception('%s: initialization failed', _MOD_NAME)
        fini()


def fini():
    global _INSTALLED
    try:
        _TRACE.stop()
    except Exception:
        _LOGGER.exception('%s: finalization failed', _MOD_NAME)
    if SharedPage._populate is _page_populate:
        SharedPage._populate = _ORIGINAL_POPULATE
    if SharedPage._dispose is _page_dispose:
        SharedPage._dispose = _ORIGINAL_DISPOSE
    _INSTALLED = False
[{"id":"R.mods.tutorial.PersonalRouteTrace.views.PersonalRouteTrace","type":"Layout","path":"coui://gui/gameface/mods/tutorial/PersonalRouteTrace/PersonalRouteTrace.html","parameters":{"entrance":"PersonalRouteTrace","impl":"gameface"}}]

Native declaration field guide: id must exactly match R.mods.tutorial.PersonalRouteTrace.views.PersonalRouteTrace in Python. The path resolves the packaged HTML, and parameters selects its entrance and implementation. Mount this strict JSON list at res/gui/gen_resources/tutorial_personal_route_trace.json; do not add legacy itemID or extension fields.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="PersonalRouteTrace.css">
</head>
<body>
  <section id="panel">
    <header>PERSONAL ROUTE · LOCAL SAMPLES</header>
    <!-- The SVG owns normalized presentation coordinates from 0 to 100. -->
    <svg viewBox="0 0 100 100" aria-label="Observed personal route">
      <rect x="1" y="1" width="98" height="98"/>
      <polyline id="trail" points=""/>
      <circle id="current" cx="50" cy="50" r="2"/>
    </svg>
    <footer id="count">0 accepted points</footer>
  </section>
  <!-- Behavior loads after every DOM node referenced by the script. -->
  <script src="PersonalRouteTrace.js"></script>
</body>
</html>
/* Keep these dimensions aligned with Python and JavaScript. */
html, body { width: 260px; height: 260px; margin: 0; overflow: hidden; background: transparent; }
* { box-sizing: border-box; }

/* The panel paints the visible card; the native surface remains transparent. */
#panel { width: 260px; height: 260px; padding: 10px; color: #dce7ef; background: rgba(9,14,20,.84); border: 1px solid rgba(150,202,230,.55); border-radius: 4px; font: 11px/16px Arial, sans-serif; }
header { height: 20px; color: #9bc8e8; letter-spacing: 1px; }
svg { display: block; width: 238px; height: 205px; background: rgba(10,25,31,.72); }
rect { fill: none; stroke: #54707d; stroke-width: .6; }
polyline { fill: none; stroke: #77d4ef; stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; }
circle { fill: #fff; stroke: #77d4ef; stroke-width: .8; }
footer { height: 15px; color: #8e9ba5; text-align: right; }
(function () {
    'use strict';
    var W = 260,
        H = 260,
        panel = document.getElementById('panel'),
        trail = document.getElementById('trail'),
        current = document.getElementById('current'),
        count = document.getElementById('count'),
        domHandler = null,
        dataChangedCallbackId = null,
        initialized = false;

    /* Wulf scalar properties can arrive wrapped as { value: ... }. */
    function unwrap(value) {
        return value && typeof value === 'object' && value.value !== undefined
            ? value.value : value;
    }

    /* Validate every primitive before converting [0,1] into SVG [0,100]. */
    function render() {
        var data = {points: []};
        try { data = JSON.parse(String(unwrap(window.model.payload) || '{}')); }
        catch (error) {}

        var points = Array.isArray(data.points) ? data.points : [], out = [];
        for (var i = 0; i < points.length; i += 1) {
            var p = points[i];
            if (Array.isArray(p) && p.length === 2 &&
                    isFinite(p[0]) && isFinite(p[1]))
                out.push((p[0] * 100).toFixed(2) + ',' +
                         (p[1] * 100).toFixed(2));
        }
        trail.setAttribute('points', out.join(' '));
        if (out.length) {
            var last = points[points.length - 1];
            current.setAttribute('cx', String(last[0] * 100));
            current.setAttribute('cy', String(last[1] * 100));
            current.style.display = 'block';
        } else current.style.display = 'none';
        count.textContent = out.length + ' accepted points';
    }

    /* Scale the DOM and resize the native Gameface surface together. */
    function scale() {
        var s = Number(viewEnv.getScale());
        if (!isFinite(s) || s <= 0) s = 1;
        var w = Math.max(1, Math.round(W * s)),
            h = Math.max(1, Math.round(H * s));
        panel.style.transform = 'scale(' + s + ')';
        panel.style.transformOrigin = '0 0';
        document.documentElement.style.width = document.body.style.width = w + 'px';
        document.documentElement.style.height = document.body.style.height = h + 'px';
        viewEnv.resizeViewPx(w, h);
        if (window.model && typeof window.model.onReady === 'function') window.model.onReady();
    }

    /* Idempotence prevents duplicate handlers after repeated readiness signals. */
    function init() {
        if (initialized) return;
        initialized = true;
        render();
        engine.on('viewEnv.onDataChanged', render);
        dataChangedCallbackId = viewEnv.addDataChangedCallback('model', 0, true);
        engine.on('self.onScaleUpdated', scale);
        scale();
    }

    /* Remove exact listener identities and the explicit model callback. */
    function cleanup() {
        if (initialized) {
            engine.off('viewEnv.onDataChanged', render);
            engine.off('self.onScaleUpdated', scale);
            if (dataChangedCallbackId !== null) {
                viewEnv.removeDataChangedCallback(dataChangedCallbackId, 0);
                dataChangedCallbackId = null;
            }
            initialized = false;
        }
        if (domHandler) {
            engine.off('self.onDomBuilt', domHandler);
            domHandler = null;
        }
    }

    /* Engine readiness and DOM readiness are separate gates. */
    var dom = window.isDomBuilt ? Promise.resolve() :
        new Promise(function (resolve) {
            domHandler = function () {
                engine.off('self.onDomBuilt', domHandler);
                domHandler = null;
                resolve();
            };
            engine.on('self.onDomBuilt', domHandler);
        });
    window.addEventListener('unload', cleanup);
    Promise.all([engine.whenReady, dom]).then(function () {
        /* Two frames let layout settle before native resize/readiness. */
        requestAnimationFrame(function () { requestAnimationFrame(init); });
    });
}());

The root Wulf string property is a Gameface wrapper in this client generation. The renderer reads its value and explicitly subscribes to root-model changes; the callback ID is removed on unload so a recreated view owns one subscription.

<root><id>tutorial.personal_route_trace</id><version>1.0.1</version><name>Personal Route Trace</name><description>Draws bounded local samples of the player vehicle route.</description></root>

Minimal project structure

personal-route-trace/
├── mod_personal_route_trace.py
└── stage/
    ├── meta.xml
    └── res/
        ├── scripts/client/gui/mods/mod_personal_route_trace.pyc
        ├── gui/gen_resources/tutorial_personal_route_trace.json
        └── gui/gameface/mods/tutorial/PersonalRouteTrace/
            ├── PersonalRouteTrace.html
            ├── PersonalRouteTrace.css
            └── PersonalRouteTrace.js

Packaging and installation

  1. Target WG 2.4.1.0 and validate the native declaration before packaging.
  2. Compile with the target-compatible Python 2.7 compiler.
  3. Stage the exact VFS paths and aligned manifest.
  4. Create a stored-member personal_route_trace_1.0.1.wotmod.
  5. Inspect and extract-test it before user-side installation.

Window-layer scope: WindowLayer.WINDOW keeps native menus and queued popups above these passive panels. The minimal example does not add TAB/full-statistics hiding; that surface belongs to the battle page below the panel. Verify this limitation in the target client and follow window-layer and keyboard-routing guidance when adding visibility controls.

Finite geometry is a precondition for normalization: check copied bounds and position, computed positive extents, and normalized results before applying tolerance checks or clamping. NaN comparisons alone do not reject the value, and clamping can turn invalid arithmetic into an apparent edge point. Invalid geometry stays unavailable rather than becoming a fabricated coordinate.

Selected authored-helper controls use tuple bounds and inert entity substitutes for finite points, NaN/infinity, arithmetic overflow, degenerate bounds and outside points. They do not establish that the engine supplies nonfinite geometry or validate native vectors, current maps, rendering or Python 2 execution.

Testing and debugging

Deterministic tests

  • Test each bound corner, center, out-of-bounds rejection, degenerate bounds, nonfinite inputs and overflowed arithmetic.
  • Drive fake callbacks through cleanup and prove stale generations cannot reschedule.
  • Swap the player vehicle ID and prove the next tick resolves the replacement.
  • Append 120 samples and assert only the newest 96 remain.
  • Compare Python/CSS/JS dimensions, message fields, and manifest version.

Runtime matrix

ScenarioExpected
Drive to opposite map directionsHorizontal direction follows X; vertical UI direction is inverted from Z
StationaryTrail does not grow
RespawnSampling pauses and resumes from the current vehicle ID
Battle exit during callbackNo successor callback and no lobby overlay
Second battleFresh bounds and empty trail

Failure modes and edge cases

  • Asymmetric bounds: subtract the actual lower corner; never assume a centered map.
  • Wrong axis: using world Y produces elevation noise.
  • Double inversion: flip Z at one named boundary only.
  • Dead entity reference: resolve by ID every tick.
  • Cancellation race: generation validation remains required after cancelCallback.
  • Oversized payload: cap points and round floats.

Performance considerations

Four small lookups per second are intentionally slower than frame rate. Distance filtering, four-decimal rounding, a 96-point ring, and payload deduplication bound CPU, memory, bridge traffic, and SVG work.

Compatibility and version sensitivity

DependencyRecheck
arenaType.boundingBoxShape, units, mode availability
playerVehicleIDRespawn/observer semantics
Vehicle.position/isStarted/spaceIDReadiness and lifetime
SharedPage wrapperPopulate/dispose order
Gameface shellLayout, events, scaling, teardown

Why this architecture?

  • Why only the player? It teaches coordinates without creating a disguised radar.
  • Why normalized floats? Tactical state stays independent of CSS pixels and interface scale.
  • Why a sampled line? Position has no ordinary change event; bounded low-rate sampling matches the requirement.
  • Why not patch the minimap? A standalone view owns all of its resources and avoids native entry collisions.

Possible extensions

  • Split the route into distance-based segments.
  • Compute approximate distance from accepted world samples before discarding world coordinates.
  • Add explicit replay support only after rewind behavior is tested.
  • Advance to Level 7 by persisting a bounded, schema-versioned battle journal.

Maintenance checklist

  • Recheck arena-bounds, player-vehicle, entity, and callback API names/signatures.
  • Trace the current bounds producer and native row inversion.
  • Retest respawn, observer, replay, and space changes.
  • Verify single callback ownership and stale-generation rejection.
  • Check Python 2.7 syntax and Error Reporter traceback records.
  • Revalidate Gameface resource path, dimensions, payload fields, and listener cleanup.
  • Runtime-test the four map directions on the target client.

What you learned

Spatial mods become manageable when coordinate domains are explicit. World X/Z, normalized tactical coordinates, and SVG pixels are three different representations with one deliberate axis inversion between them. Lifecycle-safe sampling is equally important: a correct transform attached to a stale vehicle is still wrong.