<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[APM monitoring]]></title><description><![CDATA[<p dir="auto">This tool is written entirely in python. I will attach the source code, as well as the script, to check for suspicious code sections. This APM tracker allows you to track your APM right during the game by dividing it into Mouse/Keyboard, and Total. And there are also internal parameters of the Micro/A macro APM that compares your APM with professional players at every moment of time every second, this is reflected in the colors in the legend on the AVG indicator, red is a bad APM, green is a good APM, purple is a perfect APM. Different timings are also loaded for different stages of the game (Early game, Mid-game, Late game, Ultralight game), where at each stage of the game the APM is compared with the pro level by different variables. There is also an IDLE line to track your inactivity, the color also matters, blue is perfect, green is good, red is bad. IDLE is also compared with the performance of pro players.</p>
<p dir="auto">Among other things, an internal APM graph is built in, as well as the amount of clicks is displayed. It is recommended to reset the tool after each game (F3 - played the game - F3) so the script will not break and all stages of the game will be considered correctly.</p>
<p dir="auto">Also, for visual lovers, there is a "wallpapers" folder in the root folder where you can add your own wallpapers for the tracker and change them by pressing the F4 key. By default, an anime girl picture is downloaded there.</p>
<p dir="auto">The archive with the software is too big, I'll upload it to my Google drive: <a href="https://drive.google.com/drive/folders/16TDPYbPbpfU8r8H6qHu1ACJ93QJnO58K?usp=sharing" rel="nofollow ugc">https://drive.google.com/drive/folders/16TDPYbPbpfU8r8H6qHu1ACJ93QJnO58K?usp=sharing</a></p>
<p dir="auto">CODE:<br />
import sys<br />
import time<br />
import os<br />
from collections import deque<br />
from datetime import datetime</p>
<p dir="auto">import numpy as np<br />
import matplotlib.pyplot as plt</p>
<p dir="auto">from PyQt6 import QtCore, QtGui, QtWidgets<br />
from pynput import keyboard, mouse</p>
<p dir="auto">WINDOW_SIZE = (600, 350)</p>
<p dir="auto">class Overlay(QtWidgets.QWidget):<br />
def <strong>init</strong>(self):<br />
super().<strong>init</strong>()</p>
<pre><code>    self.setWindowFlags(
        QtCore.Qt.WindowType.FramelessWindowHint |
        QtCore.Qt.WindowType.WindowStaysOnTopHint |
        QtCore.Qt.WindowType.Tool
    )

    self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground)
    self.setGeometry(300, 200, *WINDOW_SIZE)

    # =========================
    # DRAG
    # =========================
    self.dragging = False
    self.drag_offset = None

    # =========================
    # DATA
    # =========================
    self.session_active = False
    self.start_time = 0

    self.events = deque()
    self.last_event_time = None

    self.idle_time = 0.0

    self.k_hist = []
    self.m_hist = []
    self.t_hist = []

    # =========================
    # WALLPAPERS
    # =========================
    self.wallpapers = []
    self.wp_index = 0
    self.load_wallpapers()

    # =========================
    # TIMER
    # =========================
    self.timer = QtCore.QTimer()
    self.timer.timeout.connect(self.loop)
    self.timer.start(100)

    # =========================
    # INPUT
    # =========================
    self.k_listener = keyboard.Listener(on_press=self.on_key)
    self.k_listener.start()

    self.m_listener = mouse.Listener(on_click=self.on_mouse)
    self.m_listener.start()

    self.hot_listener = keyboard.Listener(on_press=self.hotkeys)
    self.hot_listener.start()

    self.camera_keys = {"w", "a", "s", "d", "W", "A", "S", "D"}
    self.arrow_keys = {keyboard.Key.up, keyboard.Key.down, keyboard.Key.left, keyboard.Key.right}

    # =========================
    # SCREAM
    # =========================
    self.scream_active = False
    self.scream_end = 0

    # =========================
    # CLOSE BUTTON
    # =========================
    self.close_rect = QtCore.QRect(WINDOW_SIZE[0]-25, 5, 20, 20)

# =========================
# WALLPAPER
# =========================
def load_wallpapers(self):
    base = os.path.dirname(os.path.abspath(__file__))
    folder = os.path.join(base, "wallpapers")
    if not os.path.exists(folder):
        os.makedirs(folder)
    for f in os.listdir(folder):
        if f.lower().endswith((".png", ".jpg", ".jpeg")):
            pix = QtGui.QPixmap(os.path.join(folder, f))
            if not pix.isNull():
                self.wallpapers.append(pix)

def current_wallpaper(self):
    if not self.wallpapers:
        return None
    return self.wallpapers[self.wp_index % len(self.wallpapers)]

def next_wallpaper(self):
    if self.wallpapers:
        self.wp_index += 1

# =========================
# DRAG
# =========================
def mousePressEvent(self, event):
    if event.button() == QtCore.Qt.MouseButton.LeftButton:
        if self.close_rect.contains(event.position().toPoint()):
            QtWidgets.QApplication.quit()
            return
        self.dragging = True
        self.drag_offset = event.globalPosition().toPoint() - self.frameGeometry().topLeft()

def mouseMoveEvent(self, event):
    if self.dragging:
        self.move(event.globalPosition().toPoint() - self.drag_offset)

def mouseReleaseEvent(self, event):
    self.dragging = False

# =========================
# INPUT
# =========================
def on_key(self, key):
    if not self.session_active:
        return

    if key in self.arrow_keys:
        return
    try:
        if hasattr(key, "char") and key.char in self.camera_keys:
            return
    except:
        pass

    self.events.append(("key", time.time()))
    self.last_event_time = time.time()

def on_mouse(self, x, y, button, pressed):
    if not self.session_active or not pressed:
        return
    if button == mouse.Button.middle:
        return
    self.events.append(("mouse", time.time()))
    self.last_event_time = time.time()

def hotkeys(self, key):
    try:
        if key == keyboard.Key.f3:
            self.toggle()
        if key == keyboard.Key.f4:
            self.next_wallpaper()
    except:
        pass

# =========================
# SESSION
# =========================
def toggle(self):
    if not self.session_active:
        self.session_active = True
        self.start_time = time.time()
        self.events.clear()
        self.k_hist = []
        self.m_hist = []
        self.t_hist = []
        self.idle_time = 0.0
        self.last_event_time = time.time()
    else:
        self.session_active = False
        self.save()

# =========================
# APM
# =========================
def calc_total(self):
    now = time.time()
    while self.events and now - self.events[0][1] &gt; 60:
        self.events.popleft()
    return len(self.events)

def calc_km(self):
    total = self.calc_total()
    k = len([e for e in self.events if e[0] == "key"])
    m = len([e for e in self.events if e[0] == "mouse"])
    return k, m, total

# =========================
# IDLE
# =========================
def update_idle(self):
    if self.last_event_time is None:
        return 0
    now = time.time()
    idle = max(0.0, now - self.last_event_time - 1.5)
    self.idle_time += idle * 0.1
    return self.idle_time

# =========================
# HUD
# =========================
def compute_hud(self):
    k, m, total = self.calc_km()
    micro = k + m
    macro = max(0, total - micro * 0.4)
    idle = self.update_idle()
    elapsed = max(1, time.time() - self.start_time)
    idle_pct = (self.idle_time / elapsed) * 100
    avg = np.mean(self.k_hist + self.m_hist) if self.k_hist else 0
    return k, m, total, micro, macro, idle_pct, avg

# =========================
# APS COLOR
# =========================
def aps_color(self, avg_apm):
    aps = avg_apm / 60.0
    if aps &gt;= 3.8:
        return (155, 89, 182)
    elif aps &gt;= 2.6:
        return (46, 204, 113)
    else:
        return (231, 76, 60)

# =========================
# LOOP
# =========================
def loop(self):
    if self.session_active:
        k, m, total, *_ = self.compute_hud()
        self.k_hist.append(k)
        self.m_hist.append(m)
        self.t_hist.append(time.time() - self.start_time)
    self.repaint()

# =========================
# SMOOTH
# =========================
def smooth(self, data, w=7):
    if len(data) &lt; w:
        return np.array(data)
    pad = w // 2
    padded = np.pad(data, (pad, pad), mode="edge")
    return np.convolve(padded, np.ones(w) / w, mode="valid")

# =========================
# SAVE
# =========================
def save(self):
    if len(self.t_hist) &lt; 5:
        return
    t = np.array(self.t_hist)
    k = np.array(self.k_hist)
    m = np.array(self.m_hist)
    total = k + m
    avg = np.mean(total)
    mx = np.max(total)

    base = os.path.dirname(os.path.abspath(__file__))
    folder = os.path.join(base, "sessions")
    os.makedirs(folder, exist_ok=True)
    path = os.path.join(folder, f"session_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png")

    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 6), dpi=120)
    fig.patch.set_facecolor("#0b0f14")
    ax1.set_facecolor("#0b0f14")
    ax2.set_facecolor("#0b0f14")

    wp = self.current_wallpaper()
    if wp:
        img = wp.toImage().convertToFormat(QtGui.QImage.Format.Format_RGBA8888)
        ptr = img.bits()
        ptr.setsize(img.sizeInBytes())
        arr = np.array(ptr).reshape(img.height(), img.width(), 4)
        fig.figimage(arr, xo=0, yo=0, alpha=0.1, zorder=-1)

    fig.suptitle("CEPHALON APM METRIC", color="white", fontsize=16, fontweight="bold", y=0.95)

    ax1.plot(t, k, color="#2ecc71", label="Keyboard")
    ax1.plot(t, m, color="#3498db", label="Mouse")
    ax1.plot(t, total, color="#f1c40f", label="Total")
    leg1 = ax1.legend(facecolor="#111", framealpha=0.8)
    for text in leg1.get_texts():
        text.set_color("white")
    ax1.tick_params(colors="white")
    ax1.grid(alpha=0.3)

    micro = k + m
    macro = total - micro * 0.4
    ax2.plot(t, micro, color="#9b59b6", label="Micro")
    ax2.plot(t, macro, color="#e74c3c", label="Macro")
    leg2 = ax2.legend(facecolor="#111", framealpha=0.8)
    for text in leg2.get_texts():
        text.set_color("white")
    ax2.tick_params(colors="white")
    ax2.grid(alpha=0.3)

    plt.tight_layout()
    plt.savefig(path, facecolor=fig.get_facecolor())
    plt.close()
    print(f"Session saved to {path}")

# =========================
# OVERLAY
# =========================
def paintEvent(self, e):
    p = QtGui.QPainter(self)
    p.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
    wp = self.current_wallpaper()
    if wp:
        scaled = wp.scaled(self.size(),
                           QtCore.Qt.AspectRatioMode.IgnoreAspectRatio,
                           QtCore.Qt.TransformationMode.SmoothTransformation)
        p.drawPixmap(0, 0, scaled)

    p.setBrush(QtGui.QColor(0, 0, 0, 170))
    p.setPen(QtCore.Qt.PenStyle.NoPen)
    p.drawRoundedRect(0, 0, *WINDOW_SIZE, 14, 14)

    # =========================
    # CLOSE BUTTON
    # =========================
    p.setBrush(QtCore.Qt.BrushStyle.NoBrush)
    p.setPen(QtGui.QColor(0, 0, 0))
    p.drawRect(self.close_rect)
    p.setFont(QtGui.QFont("Arial", 12, QtGui.QFont.Weight.Bold))
    p.drawText(self.close_rect, QtCore.Qt.AlignmentFlag.AlignCenter, "✖")

    if not self.session_active:
        p.setFont(QtGui.QFont("Segoe UI", 15))
        p.setPen(QtGui.QColor(255, 255, 255))
        p.drawText(40, 70, "Press F3 to start")
        return

    k, m, total, micro, macro, idle_pct, avg = self.compute_hud()
    color = self.aps_color(avg)
    elapsed = int(time.time() - self.start_time)
    mm = elapsed // 60
    ss = elapsed % 60

    p.setPen(QtGui.QColor(255, 255, 255))
    p.setFont(QtGui.QFont("Segoe UI", 14))
    p.drawText(40, 90, f"Keyboard: {k}")
    p.drawText(40, 120, f"Mouse: {m}")
    p.drawText(40, 150, f"Total: {total}")

    # AVG
    p.setPen(QtGui.QColor(*color))
    p.drawText(40, 180, f"AVG {avg:.1f}")

    p.setPen(QtGui.QColor(200, 200, 200))
    p.drawText(40, 210, f"TIME: {mm:02d}:{ss:02d}")
    p.setPen(QtGui.QColor(52, 152, 219))
    p.drawText(40, 240, f"IDLE: {idle_pct:.1f}%")

    if len(self.k_hist) &gt; 10:
        k_s = self.smooth(np.array(self.k_hist))
        m_s = self.smooth(np.array(self.m_hist))
        t_s = k_s + m_s

        x0, y0, w, h = 260, 60, 300, 220
        mx = max(t_s) + 1

        def draw(data, color):
            for i in range(len(data) - 1):
                p.setPen(QtGui.QPen(color, 3))
                p.drawLine(
                    int(x0 + (i / len(data)) * w),
                    int(y0 + h - (data[i] / mx) * h),
                    int(x0 + ((i + 1) / len(data)) * w),
                    int(y0 + h - (data[i + 1] / mx) * h)
                )

        draw(k_s, QtGui.QColor(46, 204, 113))
        draw(m_s, QtGui.QColor(52, 152, 219))
        draw(t_s, QtGui.QColor(241, 196, 15))
</code></pre>
<p dir="auto">if <strong>name</strong> == "<strong>main</strong>":<br />
app = QtWidgets.QApplication(sys.argv)<br />
w = Overlay()<br />
w.show()<br />
sys.exit(app.exec())</p>
]]></description><link>https://forum.faforever.com/topic/10110/apm-monitoring</link><generator>RSS for Node</generator><lastBuildDate>Fri, 05 Jun 2026 03:28:54 GMT</lastBuildDate><atom:link href="https://forum.faforever.com/topic/10110.rss" rel="self" type="application/rss+xml"/><pubDate>Sun, 10 May 2026 08:54:44 GMT</pubDate><ttl>60</ttl><item><title><![CDATA[Reply to APM monitoring on Sun, 10 May 2026 21:14:36 GMT]]></title><description><![CDATA[<blockquote>
<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/indexlibrorum" aria-label="Profile: IndexLibrorum">@<bdi>IndexLibrorum</bdi></a> <a href="/post/73049">said</a>:</p>
<p dir="auto">Perhaps you can put the code on Github? Easier to review.<br />
<a href="https://gist.github.com/cephalonlabel-commits/67460d85f23fbb7c1c39f3fd8c117996" rel="nofollow ugc">https://gist.github.com/cephalonlabel-commits/67460d85f23fbb7c1c39f3fd8c117996</a></p>
</blockquote>
<p dir="auto">think I did it.</p>
]]></description><link>https://forum.faforever.com/post/73062</link><guid isPermaLink="true">https://forum.faforever.com/post/73062</guid><dc:creator><![CDATA[CEPHALON]]></dc:creator><pubDate>Sun, 10 May 2026 21:14:36 GMT</pubDate></item><item><title><![CDATA[Reply to APM monitoring on Sun, 10 May 2026 21:11:57 GMT]]></title><description><![CDATA[<blockquote>
<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/indexlibrorum" aria-label="Profile: IndexLibrorum">@<bdi>IndexLibrorum</bdi></a> <a href="/post/73049">said</a>:</p>
<p dir="auto">Perhaps you can put the code on Github? Easier to review.</p>
</blockquote>
<p dir="auto">I'm essentially a novice coder, I've been doing this for less than a week, I've never used a github for hosting. I can attach the python script itself that runs the EXE so that you can look through editing</p>
]]></description><link>https://forum.faforever.com/post/73061</link><guid isPermaLink="true">https://forum.faforever.com/post/73061</guid><dc:creator><![CDATA[CEPHALON]]></dc:creator><pubDate>Sun, 10 May 2026 21:11:57 GMT</pubDate></item><item><title><![CDATA[Reply to APM monitoring on Sun, 10 May 2026 09:37:31 GMT]]></title><description><![CDATA[<p dir="auto">Perhaps you can put the code on Github? Easier to review.</p>
]]></description><link>https://forum.faforever.com/post/73049</link><guid isPermaLink="true">https://forum.faforever.com/post/73049</guid><dc:creator><![CDATA[IndexLibrorum]]></dc:creator><pubDate>Sun, 10 May 2026 09:37:31 GMT</pubDate></item><item><title><![CDATA[Reply to APM monitoring on Sun, 10 May 2026 09:14:38 GMT]]></title><description><![CDATA[<p dir="auto">The overlay looks like this<br />
<img src="/assets/uploads/files/1778404445750-photo_5190417219752498709_y.jpg" alt="photo_5190417219752498709_y.jpg" class=" img-fluid img-markdown" /></p>
]]></description><link>https://forum.faforever.com/post/73047</link><guid isPermaLink="true">https://forum.faforever.com/post/73047</guid><dc:creator><![CDATA[CEPHALON]]></dc:creator><pubDate>Sun, 10 May 2026 09:14:38 GMT</pubDate></item></channel></rss>