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())