Question About Legality/Compliance of a Custom 8111 Telemetry Plugin

This tool only visualizes the player aircraft position and trail using the official local 8111 HTTP endpoint, without reading or displaying any enemy/friendly units.


import sys
import math
import time
import requests
from collections import deque

from PySide6.QtCore import Qt, QTimer, QPointF
from PySide6.QtGui import QPainter, QPen, QFont
from PySide6.QtWidgets import QApplication, QWidget

WT_URL = "http://127.0.0.1:8111"


def safe_get_json(url):
    """Safely fetch JSON data from War Thunder local 8111 HTTP server."""
    try:
        r = requests.get(url, timeout=0.2)
        if r.status_code == 200:
            return r.json()
    except Exception:
        return None
    return None


class RadarWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("WT Circular Coordinate Display (Player Only)")
        self.resize(800, 800)

        # Map parameters
        self.map_center_x = 0.0
        self.map_center_y = 0.0
        self.map_radius = 20000.0  # meters

        # Player state (player aircraft only)
        self.player_x = None
        self.player_y = None
        self.player_heading = None

        # Flight trail buffer: stores (timestamp, x, y)
        self.trail = deque()

        # Refresh timer
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update_data)
        self.timer.start(50)  # 20Hz update rate

    def update_data(self):
        map_info = safe_get_json(f"{WT_URL}/map_info.json")
        map_obj = safe_get_json(f"{WT_URL}/map_obj.json")

        # --- Update map center / map radius ---
        if map_info:
            if "map_min" in map_info and "map_max" in map_info:
                mn = map_info["map_min"]
                mx = map_info["map_max"]

                # Map center is computed as midpoint between min/max boundaries
                self.map_center_x = (mn[0] + mx[0]) / 2.0
                self.map_center_y = (mn[1] + mx[1]) / 2.0

                # Radius uses the larger dimension of the map
                w = abs(mx[0] - mn[0])
                h = abs(mx[1] - mn[1])
                self.map_radius = max(w, h) / 2.0

            elif "grid_size" in map_info:
                # Fallback if only grid_size is available
                gs = float(map_info.get("grid_size", 20000))
                self.map_radius = gs / 2.0

        # --- Find player object only ---
        self.player_x = None
        self.player_y = None
        self.player_heading = None

        if map_obj and "objects" in map_obj:
            objects = map_obj["objects"]

            for obj in objects:
                icon = str(obj.get("icon", "")).lower()
                obj_type = str(obj.get("type", "")).lower()
                name = str(obj.get("name", "")).lower()

                # Common player identifiers in map_obj.json
                if "player" in icon or "player" in obj_type or "player" in name:
                    self.player_x = float(obj.get("x", 0))
                    self.player_y = float(obj.get("y", 0))
                    self.player_heading = obj.get("dir", None)
                    break

        # --- Update player trail (last 30 seconds) ---
        if self.player_x is not None and self.player_y is not None:
            now = time.time()
            self.trail.append((now, self.player_x, self.player_y))

            # Remove points older than 30 seconds
            while self.trail and (now - self.trail[0][0] > 30.0):
                self.trail.popleft()
        else:
            # If player is not detected, clear trail buffer
            self.trail.clear()

        self.update()

    def world_to_screen(self, x, y):
        """Convert world/map coordinates into widget screen coordinates."""
        w = self.width()
        h = self.height()

        margin = 70
        size = min(w, h) - margin * 2
        scale = size / (2 * self.map_radius)

        dx = x - self.map_center_x
        dy = y - self.map_center_y

        # Screen Y axis is inverted (downwards), so dy must be negated
        sx = w / 2 + dx * scale
        sy = h / 2 - dy * scale
        return QPointF(sx, sy)

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)

        w = self.width()
        h = self.height()
        painter.fillRect(0, 0, w, h, Qt.black)

        # Radar circle parameters
        margin = 70
        radar_size = min(w, h) - margin * 2
        radar_radius_px = radar_size / 2
        cx = w / 2
        cy = h / 2

        # Outer circle
        painter.setPen(QPen(Qt.white, 2))
        painter.drawEllipse(QPointF(cx, cy), radar_radius_px, radar_radius_px)

        # Crosshair axes
        painter.setPen(QPen(Qt.gray, 1))
        painter.drawLine(cx - radar_radius_px, cy, cx + radar_radius_px, cy)
        painter.drawLine(cx, cy - radar_radius_px, cx, cy + radar_radius_px)

        # Range rings + labels
        painter.setFont(QFont("Consolas", 10))
        step = 5000.0  # 5km per ring
        r = step
        while r < self.map_radius:
            rr = (r / self.map_radius) * radar_radius_px
            painter.setPen(QPen(Qt.darkGray, 1))
            painter.drawEllipse(QPointF(cx, cy), rr, rr)

            painter.setPen(QPen(Qt.white, 1))
            painter.drawText(int(cx + 6), int(cy - rr + 16), f"{r/1000:.0f}km")

            r += step

        # Cardinal directions
        painter.setPen(QPen(Qt.white, 1))
        painter.setFont(QFont("Consolas", 14))
        painter.drawText(cx - 8, cy - radar_radius_px + 25, "N")
        painter.drawText(cx + radar_radius_px - 25, cy + 5, "E")
        painter.drawText(cx - 8, cy + radar_radius_px - 10, "S")
        painter.drawText(cx - radar_radius_px + 10, cy + 5, "W")

        # Player trail (last 30 seconds)
        if len(self.trail) >= 2:
            painter.setPen(QPen(Qt.darkCyan, 2))
            prev = None
            for _, tx, ty in self.trail:
                p = self.world_to_screen(tx, ty)
                if prev is not None:
                    painter.drawLine(prev, p)
                prev = p

        # Player marker + heading line
        if self.player_x is not None and self.player_y is not None:
            p = self.world_to_screen(self.player_x, self.player_y)

            painter.setPen(QPen(Qt.cyan, 2))
            painter.setBrush(Qt.cyan)
            painter.drawEllipse(p, 7, 7)

            # Heading arrow
            if self.player_heading is not None:
                try:
                    hdg = float(self.player_heading)
                    ang = math.radians(hdg)

                    arrow_len = 30
                    ax = p.x() + math.sin(ang) * arrow_len
                    ay = p.y() - math.cos(ang) * arrow_len

                    painter.setPen(QPen(Qt.cyan, 2))
                    painter.drawLine(p.x(), p.y(), ax, ay)
                except Exception:
                    pass

            # Display relative coordinates (for radio callouts)
            dx = self.player_x - self.map_center_x
            dy = self.player_y - self.map_center_y
            dist = math.sqrt(dx * dx + dy * dy)

            painter.setPen(QPen(Qt.white, 1))
            painter.setFont(QFont("Consolas", 11))
            painter.drawText(
                10, 20,
                f"REL: X={dx:.0f}m  Y={dy:.0f}m  | R={dist/1000:.2f}km"
            )
            painter.drawText(
                10, 40,
                f"ABS: X={self.player_x:.0f}  Y={self.player_y:.0f}  | MapRadius={self.map_radius/1000:.1f}km"
            )

        else:
            painter.setPen(QPen(Qt.red, 1))
            painter.setFont(QFont("Consolas", 12))
            painter.drawText(
                10, 20,
                "Player coordinates not detected. Enter a match and ensure 8111 telemetry is enabled."
            )


if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = RadarWidget()
    w.show()
    sys.exit(app.exec())