Nitro Type Flag Check StarTrack+NTL:Legacy

Checks Nitro Type racers for flags/bans using NT StarTrack and NTL, showing color-coded icons with custom tooltips.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Nitro Type Flag Check StarTrack+NTL:Legacy
// @namespace    http://tampermonkey.net/
// @version      3.7
// @description  Checks Nitro Type racers for flags/bans using NT StarTrack and NTL, showing color-coded icons with custom tooltips.
// @match        https://www.nitrotype.com/team/*
// @match        https://www.nitrotype.com/racer/*
// @match        https://www.nitrotype.com/leagues
// @match        https://www.nitrotype.com/friends
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      ntleaderboards.com
// @connect      ntstartrack.org
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ===== SHARED OPTIMIZATION LAYER =====
    window.NTShared = window.NTShared || {
        cache: {
            individual: { data: null, timestamp: 0, expiresAt: 0 },
            team: { data: null, timestamp: 0, expiresAt: 0 },
            isbot: new Map()
        },

        setCache(type, data, expiresAt) {
            this.cache[type].data = data;
            this.cache[type].timestamp = Date.now();
            this.cache[type].expiresAt = expiresAt || (Date.now() + 3600000);
            window.dispatchEvent(new CustomEvent('nt-cache-updated', {
                detail: { type, data, expiresAt }
            }));
        },

        getCache(type, maxAge = 3600000) {
            const cached = this.cache[type];
            if (!cached.data) return null;

            const age = Date.now() - cached.timestamp;
            if (age < maxAge && Date.now() < cached.expiresAt) {
                return cached.data;
            }
            return null;
        },

        getBotStatus(username) {
            return this.cache.isbot.get(username.toLowerCase());
        },

        setBotStatus(username, status) {
            this.cache.isbot.set(username.toLowerCase(), status);
        }
    };

    // Consolidated MutationObserver Manager
    window.NTObserverManager = window.NTObserverManager || {
        callbacks: {},
        observer: null,
        debounceTimer: null,

        register(scriptName, callback) {
            this.callbacks[scriptName] = callback;

            if (!this.observer) {
                this.observer = new MutationObserver(() => {
                    clearTimeout(this.debounceTimer);
                    this.debounceTimer = setTimeout(() => {
                        Object.values(this.callbacks).forEach(cb => {
                            try { cb(); } catch(e) { console.error('[Observer Error]', e); }
                        });
                    }, 250);
                });
                this.observer.observe(document.body, {
                    childList: true,
                    subtree: true
                });
            }
        }
    };

    // =============================
    // 🎨 Constants & Configuration
    // =============================
    const STARTRACK_CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days

    const colorMap = {
        green: "#00FF00",
        red: "#FF0000",
        yellow: "#FFFF00",
        orange: "#FFA500",
        gray: "#696969"
    };

    let customTooltipElement = null;

    // =============================
    // 🔑 Token Management
    // =============================
    function getToken() {
        let token = GM_getValue('nt_token');
        if (!token) {
            token = localStorage.getItem("player_token");
            if (token) {
                GM_setValue('nt_token', token);
            }
        }
        return token;
    }

    function getTokenAndRetry(callback) {
        const token = localStorage.getItem("player_token");
        if (token) {
            GM_setValue('nt_token', token);
            callback();
        }
    }

    // =============================
    // 🛠️ Helper: Set Icon on Element
    // =============================
    function setIcon(element, color, tooltip, source) {
        let statusField;
        if (element.classList.contains('table-row')) {
            statusField = element.querySelector('.tsi.tc-lemon.tsxs');
        } else if (element.classList.contains('profile-title')) {
            statusField = element;
        } else {
            const allRows = document.querySelectorAll('.table-row');
            for (const row of allRows) {
                const racerContainer = row.querySelector('.player-name--container');
                if (racerContainer && racerContainer.getAttribute('title') === element.getAttribute('title')) {
                    statusField = row.querySelector('.tsi.tc-lemon.tsxs');
                    break;
                }
            }
        }

        if (!statusField) {
            return;
        }

        const existingIcons = statusField.querySelectorAll(`.status-icon-${color}`);
        existingIcons.forEach(icon => icon.remove());

        const iconSpan = document.createElement('span');
        iconSpan.classList.add('status-icon', `status-icon-${color}`);
        iconSpan.style.marginLeft = "5px";
        iconSpan.style.display = "inline-block";
        iconSpan.style.fontSize = "12px";
        iconSpan.style.fontWeight = "600";
        iconSpan.style.whiteSpace = "nowrap";
        iconSpan.style.cursor = "help";

        const emojiMap = {
            green: "🟢",
            red: "🔴",
            yellow: "🟡",
            orange: "🟠",
            gray: "⚫"
        };

        const label = source === "NTL" ? "NTL" : "ST";
        const emoji = emojiMap[color] || "⚪";
        const labelColor = colorMap[color] || color;

        iconSpan.innerHTML = `${emoji}<span style="color: ${labelColor}; margin-left: 2px;">${label}</span>`;

        iconSpan.addEventListener('mouseenter', function(event) {
            customTooltipElement = document.createElement('div');
            customTooltipElement.textContent = tooltip;
            customTooltipElement.style.position = 'absolute';
            customTooltipElement.style.backgroundColor = '#333';
            customTooltipElement.style.color = 'white';
            customTooltipElement.style.padding = '4px 8px';
            customTooltipElement.style.borderRadius = '4px';
            customTooltipElement.style.zIndex = '10001';
            customTooltipElement.style.fontSize = '12px';
            customTooltipElement.style.fontFamily = 'Arial, sans-serif';
            customTooltipElement.style.pointerEvents = 'none';
            customTooltipElement.style.left = (event.pageX + 10) + 'px';
            customTooltipElement.style.top = (event.pageY + 10) + 'px';
            document.body.appendChild(customTooltipElement);
        });

        iconSpan.addEventListener('mousemove', function(event) {
            if (customTooltipElement) {
                customTooltipElement.style.left = (event.pageX + 10) + 'px';
                customTooltipElement.style.top = (event.pageY + 10) + 'px';
            }
        });

        iconSpan.addEventListener('mouseleave', function() {
            if (customTooltipElement) {
                customTooltipElement.remove();
                customTooltipElement = null;
            }
        });

        // Use icon queue for coordinated loading if available
        if (window.NTIconQueue && window.NTIconQueue.add) {
            window.NTIconQueue.add(() => statusField.appendChild(iconSpan));
        } else {
            statusField.appendChild(iconSpan);
        }
    }

    // =============================
    // 🟢 Fetch NT StarTrack Status
    // =============================
    function fetchStarTrackStatus(username, element) {
        // Check shared cache first
        const sharedStatus = window.NTShared.getBotStatus(username);
        if (sharedStatus) {
            setIcon(element, sharedStatus.color, sharedStatus.tooltip, "ST");
            return Promise.resolve();
        }

        const cacheKey = `startrack_${username}`;
        const cached = GM_getValue(cacheKey);

        if (cached && (Date.now() - cached.timestamp) < STARTRACK_CACHE_DURATION) {
            setIcon(element, cached.color, cached.tooltip, "ST");
            return Promise.resolve();
        }

        const url = `http://ntstartrack.org:5001/api/isbot/${username}`;
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                onload: function(response) {
                    let color, tooltip;
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data.is_bot === true) {
                            color = "red";
                            tooltip = "StarTrack: Flagged";
                        } else if (data.is_bot === false) {
                            color = "green";
                            tooltip = "StarTrack: Not Flagged";
                        } else if (data.error) {
                            color = "yellow";
                            tooltip = "StarTrack: Untracked";
                        } else {
                            color = "gray";
                            tooltip = "StarTrack: Unexpected Response";
                        }
                        GM_setValue(cacheKey, { color, tooltip, timestamp: Date.now() });
                        window.NTShared.setBotStatus(username, { color, tooltip });
                        setIcon(element, color, tooltip, "ST");
                    } catch (err) {
                        if (response.status === 404) {
                            color = "yellow";
                            tooltip = "StarTrack: Untracked";
                            GM_setValue(cacheKey, { color, tooltip, timestamp: Date.now() });
                            window.NTShared.setBotStatus(username, { color, tooltip });
                            setIcon(element, color, tooltip, "ST");
                        } else {
                            setIcon(element, "gray", "StarTrack: Error", "ST");
                        }
                    }
                    resolve();
                },
                onerror: function() {
                    setIcon(element, "gray", "StarTrack: Network Error", "ST");
                    resolve();
                }
            });
        });
    }

    // =============================
    // 🟠 Fetch NTL Legacy Status
    // =============================
    function fetchNTLStatus(username, element) {
        const cacheKey = `ntl_${username}`;
        const cached = GM_getValue(cacheKey);

        if (cached) {
            setIcon(element, cached.color, cached.tooltip, "NTL");
            return Promise.resolve();
        }

        const url = `https://ntleaderboards.com/is_user_banned/${username}`;
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                onload: function(response) {
                    if (response.status === 200) {
                        const data = response.responseText.trim();
                        let color, tooltip;

                        if (data === "Y (ban)") {
                            color = "orange";
                            tooltip = "Legacy NTL (Banned)";
                        } else if (data === "Y (ban+flag)") {
                            color = "orange";
                            tooltip = "Legacy NTL (Flagged)";
                        } else {
                            resolve();
                            return;
                        }

                        GM_setValue(cacheKey, { color, tooltip });
                        setIcon(element, color, tooltip, "NTL");
                    }
                    resolve();
                }
            });
        });
    }

    // =============================
    // 🎯 Update Status (Dual Check)
    // =============================
    async function updateStatus(element, username) {
        element.setAttribute('data-status-processing', 'true');
        await Promise.all([
            fetchStarTrackStatus(username, element),
            fetchNTLStatus(username, element)
        ]);
        element.removeAttribute('data-status-processing');
    }

    // =============================
    // 🔍 Observe Main Content
    // =============================
    function observeMainContent() {
        window.NTObserverManager.register('botflag', () => {
            const path = window.location.pathname;
            if (path.startsWith("/team") && document.querySelector('.table-row')) {
                handleTeamPage();
            } else if (path.startsWith("/racer") && document.querySelector('.profile-title')) {
                handleRacerPage();
            } else if (path === "/leagues" && document.querySelector('.table-row')) {
                handleLeaguesPage();
            } else if (path === "/friends" && document.querySelector('.tab')) {
                handleFriendsPage();
            }
        });
    }

    // =============================
    // 👥 /team Page Handling
    // =============================
    function handleTeamPage() {
        checkUserBansTeam();
    }

    async function checkUserBansTeam() {
        const applicationsMap = await fetchTeamApplications();
        const userMap = await fetchTeamActivity();

        if (applicationsMap) {
            updateUsersTeamApplications(applicationsMap);
        }
        if (userMap) {
            updateUsersTeam(userMap);
        }
    }

    async function fetchTeamActivity() {
        try {
            const TEAM = window.location.pathname.split('/').pop();

            // Check cache first (5 minute cache like friends list)
            const cacheKey = `team_activity_${TEAM}`;
            const cached = GM_getValue(cacheKey);
            const TEAM_CACHE_DURATION = 300000; // 5 minutes

            if (cached && (Date.now() - cached.timestamp) < TEAM_CACHE_DURATION) {
                return cached.data;
            }

            const token = getToken();

            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: `https://www.nitrotype.com/api/v2/teams/${TEAM}`,
                    headers: {
                        "Authorization": `Bearer ${token}`,
                        "accept": "application/json, text/plain, */*"
                    },
                    onload: function(response) {
                        if (response.status === 200) {
                            const data = JSON.parse(response.responseText);
                            if (data.status === "OK") {
                                const memberMap = {};
                                data.results.members.forEach(member => {
                                    memberMap[member.displayName || member.username] = member.username;
                                });

                                // Cache the result
                                GM_setValue(cacheKey, {
                                    data: memberMap,
                                    timestamp: Date.now()
                                });

                                resolve(memberMap);
                            } else {
                                resolve(null);
                            }
                        } else if (response.status === 401) {
                            getTokenAndRetry(() => fetchTeamActivity().then(resolve));
                        } else {
                            resolve(null);
                        }
                    },
                    onerror: () => resolve(null)
                });
            });
        } catch (error) {
            return null;
        }
    }

    async function fetchTeamApplications() {
        try {
            // Check cache first (5 minute cache)
            const cacheKey = 'team_applications';
            const cached = GM_getValue(cacheKey);
            const APPLICATIONS_CACHE_DURATION = 300000; // 5 minutes

            if (cached && (Date.now() - cached.timestamp) < APPLICATIONS_CACHE_DURATION) {
                return cached.data;
            }

            const token = getToken();

            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: "https://www.nitrotype.com/api/v2/teams/applications",
                    headers: {
                        "Authorization": `Bearer ${token}`,
                        "accept": "application/json, text/plain, */*"
                    },
                    onload: function(response) {
                        if (response.status === 200) {
                            const data = JSON.parse(response.responseText);
                            if (data.results && data.results.length > 0) {
                                const memberMap = {};
                                data.results.forEach(member => {
                                    memberMap[member.displayName || member.username] = member.username;
                                });

                                // Cache the result
                                GM_setValue(cacheKey, {
                                    data: memberMap,
                                    timestamp: Date.now()
                                });

                                resolve(memberMap);
                            } else {
                                resolve(null);
                            }
                        } else if (response.status === 401) {
                            getTokenAndRetry(() => fetchTeamApplications().then(resolve));
                        } else {
                            resolve(null);
                        }
                    },
                    onerror: () => resolve(null)
                });
            });
        } catch (error) {
            return null;
        }
    }

    async function updateUsersTeam(userMap) {
        // Collect all promises to wait for batch completion
        const promises = [];

        for (const [displayName, username] of Object.entries(userMap)) {
            const promise = updateUserStatusTeam(username, displayName);
            if (promise) promises.push(promise);
        }

        // Wait for all icon additions to complete before flushing queue
        await Promise.all(promises);
    }

    async function updateUsersTeamApplications(userMap) {
        const promises = [];

        for (const [displayName, username] of Object.entries(userMap)) {
            const promise = updateUserStatusTeamApplications(username, displayName);
            if (promise) promises.push(promise);
        }

        await Promise.all(promises);
    }

    function updateUserStatusTeam(username, displayName) {
        const teamTable = document.querySelector('.table.table--striped.table--selectable.table--team.table--teamOverview');
        if (!teamTable) return null;

        const playerNameContainers = teamTable.querySelectorAll('.player-name--container[title]');
        const playerNameContainer = Array.from(playerNameContainers).find(container => {
            const nameSpan = container.querySelector('.type-ellip');
            return nameSpan && nameSpan.textContent.trim() === displayName.trim();
        });

        if (playerNameContainer) {
            const row = playerNameContainer.closest('.table-row');
            if (row && !row.classList.contains('status-processed')) {
                row.classList.add('status-processed');
                return updateStatus(row, username);
            }
        }
        return null;
    }

    function updateUserStatusTeamApplications(username, displayName) {
        const allRows = document.querySelectorAll('.row.row--o.well.well--b.well--l');
        let appSection = null;

        for (const row of allRows) {
            const h3 = row.querySelector('h3.mbf');
            if (h3 && h3.textContent.includes('Pending Applications')) {
                appSection = row;
                break;
            }
        }

        if (!appSection) return null;

        const playerNameContainers = appSection.querySelectorAll('.player-name--container[title]');
        const playerNameContainer = Array.from(playerNameContainers).find(container => {
            const nameSpan = container.querySelector('.type-ellip');
            return nameSpan && nameSpan.textContent.trim() === displayName.trim();
        });

        if (playerNameContainer) {
            const row = playerNameContainer.closest('.table-row');
            if (row && !row.classList.contains('status-processed')) {
                row.classList.add('status-processed');
                return updateStatus(row, username);
            }
        }
        return null;
    }

    // =============================
    // 🏁 /racer Page Handling
    // =============================
    function handleRacerPage() {
        const username = window.location.pathname.split('/').pop();
        const el = document.querySelector('.profile-title');
        if (username && el) updateStatus(el, username);
    }

    // =============================
    // 🏆 /leagues Page Handling
    // =============================
    async function handleLeaguesPage() {
        // Check cache first (5 minute cache)
        const cacheKey = 'leagues_user_activity';
        const cached = GM_getValue(cacheKey);
        const LEAGUES_CACHE_DURATION = 300000; // 5 minutes

        let userMap;

        if (cached && (Date.now() - cached.timestamp) < LEAGUES_CACHE_DURATION) {
            userMap = cached.data;
        } else {
            const token = getToken();
            if (!token) return;

            userMap = await new Promise((resolve) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: "https://www.nitrotype.com/api/v2/leagues/user/activity",
                    headers: {
                        "Authorization": `Bearer ${token}`,
                        "Content-Type": "application/json"
                    },
                    onload: function(response) {
                        if (response.status === 200) {
                            const data = JSON.parse(response.responseText);
                            if (data.status === "OK") {
                                const map = {};
                                data.results.standings.forEach(u => {
                                    map[u.displayName || u.username] = u.username;
                                });

                                // Cache the result
                                GM_setValue(cacheKey, {
                                    data: map,
                                    timestamp: Date.now()
                                });

                                resolve(map);
                            } else {
                                resolve(null);
                            }
                        } else if (response.status === 401) {
                            getTokenAndRetry(() => handleLeaguesPage());
                            resolve(null);
                        } else {
                            resolve(null);
                        }
                    },
                    onerror: () => resolve(null)
                });
            });
        }

        if (userMap) {
            await processLeagues(userMap);
        }
    }

    async function processLeagues(userMap) {
        const leaderboardRows = document.querySelectorAll('.table-row');
        const promises = [];

        leaderboardRows.forEach(row => {
            const playerElement = row.querySelector('.player-name--container[title]');
            if (!playerElement) return;

            const displayName = playerElement.getAttribute('title');
            const username = userMap[displayName];

            if (username && !row.classList.contains('status-processed')) {
                row.classList.add('status-processed');
                promises.push(updateStatus(row, username));
            }
        });

        // Wait for all icon additions to complete
        await Promise.all(promises);
    }

    // =============================
    // 👫 /friends Page Handling (v3.1 LOGIC)
    // =============================
    function handleFriendsPage() {
        function observeDOMChanges(callback) {
            const observer = new MutationObserver(() => callback());
            observer.observe(document.body, { childList: true, subtree: true });
        }

        observeDOMChanges(function() {
            const activeTab = getActiveTab();
            if (activeTab) {
                if (activeTab === "Friends") {
                    handleFriendsTab();
                } else if (activeTab === "Requests") {
                    handleRequestsTab();
                } else if (activeTab === "Search") {
                    handleSearchTab();
                } else if (activeTab === "Recent") {
                    handleRecentTab();
                }
            }
        });
    }

    function getActiveTab() {
        const activeTabElement = document.querySelector('.tab.is-active');
        if (!activeTabElement) return null;
        const bucketContent = activeTabElement.querySelector('.bucket-content');
        return bucketContent ? bucketContent.textContent.trim() : null;
    }

    function handleFriendsTab() {
        const rows = getRowsForTab();
        processFriends(rows, "Friends");
    }

    function handleRequestsTab() {
        const token = getToken();
        if (!token) return;

        GM_xmlhttpRequest({
            method: "GET",
            url: "https://www.nitrotype.com/api/v2/friend-requests",
            headers: {
                "Authorization": `Bearer ${token}`,
                "Content-Type": "application/json"
            },
            onload: function(response) {
                if (response.status === 200) {
                    const data = JSON.parse(response.responseText);
                    const requests = data.results.requests || [];

                    const rows = getRowsForTab();
                    rows.forEach(row => {
                        const playerElement = row.querySelector('.player-name--container');
                        if (!playerElement) return;

                        const displayName = playerElement.getAttribute('title') || playerElement.textContent.trim();
                        const match = requests.find(req => req.displayName === displayName || req.username === displayName);

                        if (match && !row.classList.contains('status-processed')) {
                            updateStatus(row, match.username);
                            row.classList.add('status-processed');
                        }
                    });
                } else if (response.status === 401) {
                    getTokenAndRetry(handleRequestsTab);
                }
            }
        });
    }

    function handleSearchTab() {
        const searchInput = document.querySelector('#friendsearch');
        if (!searchInput || !searchInput.value) return;

        const searchTerm = searchInput.value.trim();
        if (!searchTerm) return;

        const token = getToken();
        if (!token) return;

        GM_xmlhttpRequest({
            method: "POST",
            url: "https://www.nitrotype.com/api/v2/players/search",
            headers: {
                "Authorization": `Bearer ${token}`,
                "Content-Type": "application/x-www-form-urlencoded"
            },
            data: `term=${encodeURIComponent(searchTerm)}`,
            onload: function(response) {
                if (response.status === 200) {
                    const data = JSON.parse(response.responseText);
                    const results = data.results || [];

                    const rows = getRowsForTab();
                    rows.forEach(row => {
                        const playerElement = row.querySelector('.player-name--container');
                        if (!playerElement) return;

                        const displayName = playerElement.getAttribute('title') || playerElement.textContent.trim();
                        const match = results.find(r => r.displayName === displayName || r.username === displayName);

                        if (match && !row.classList.contains('status-processed')) {
                            updateStatus(row, match.username);
                            row.classList.add('status-processed');
                        }
                    });
                } else if (response.status === 401) {
                    getTokenAndRetry(handleSearchTab);
                }
            }
        });
    }

    function handleRecentTab() {
        const rows = getRowsForTab();
        rows.forEach(row => {
            if (row.classList.contains('status-processed')) return;
            const playerElement = row.querySelector('.player-name--container');
            if (!playerElement) return;

            const displayName = playerElement.getAttribute('title') || playerElement.textContent.trim();
            if (displayName) {
                updateStatus(row, displayName);
                row.classList.add('status-processed');
            }
        });
    }

    // Cache for friends list - fetch once, use for all lookups
    let friendsListCache = null;
    let friendsListTimestamp = 0;
    const FRIENDS_CACHE_DURATION = 300000; // 5 minutes

    async function getFriendsList() {
        // Check cache first
        if (friendsListCache && (Date.now() - friendsListTimestamp) < FRIENDS_CACHE_DURATION) {
            return friendsListCache;
        }

        const token = getToken();
        if (!token) return null;

        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: "https://www.nitrotype.com/api/v2/friends",
                headers: {
                    "Authorization": `Bearer ${token}`,
                    "Content-Type": "application/json"
                },
                onload: function(response) {
                    if (response.status === 200) {
                        const data = JSON.parse(response.responseText);
                        const fields = data.results.fields;
                        const values = data.results.values;

                        const displayNameIndex = fields.indexOf("displayName");
                        const usernameIndex = fields.indexOf("username");

                        // Create lookup map
                        const friendsMap = {};
                        values.forEach(friendData => {
                            const displayName = friendData[displayNameIndex];
                            const username = friendData[usernameIndex];
                            friendsMap[displayName] = username;
                            friendsMap[username] = username; // Also map username to itself
                        });

                        friendsListCache = friendsMap;
                        friendsListTimestamp = Date.now();
                        resolve(friendsMap);
                    } else if (response.status === 401) {
                        getTokenAndRetry(() => getFriendsList().then(resolve));
                    } else {
                        resolve(null);
                    }
                }
            });
        });
    }

    async function processFriends(rows, context) {
        // Fetch friends list once for all friends
        const friendsMap = await getFriendsList();
        if (!friendsMap) return;

        // Collect all promises to wait for batch completion
        const promises = [];

        // Process all friends using the cached list
        rows.forEach(row => {
            const playerElement = row.querySelector('.player-name--container');
            if (playerElement) {
                let playerName = playerElement.getAttribute('title');
                if (!playerName) {
                    playerName = playerElement.textContent.trim();
                }

                if (playerName && !row.classList.contains('status-processed')) {
                    const username = friendsMap[playerName];
                    if (username) {
                        row.classList.add('status-processed');
                        promises.push(updateStatus(row, username));
                    }
                }
            }
        });

        // Wait for all icon additions to complete
        await Promise.all(promises);
    }

    function getRowsForTab() {
        const rows = document.querySelectorAll('.table-row');
        return Array.from(rows).filter(row => !row.querySelector('th'));
    }

    // =============================
    // 🚀 Initialize Script
    // =============================
    observeMainContent();

})();