Checks Nitro Type racers for flags/bans using NT StarTrack and NTL, showing color-coded icons with custom tooltips.
// ==UserScript==
// @name Nitro Type - Flagged Racers
// @namespace https://nitrotype.info
// @version 5.1.2
// @description Checks Nitro Type racers for flags/bans using NT StarTrack and NTL, showing color-coded icons with custom tooltips.
// @author Captain.Loveridge & SuperJoelzy
// @match https://www.nitrotype.com/team/*
// @match https://www.nitrotype.com/racer/*
// @match https://www.nitrotype.com/leagues
// @match https://www.nitrotype.com/friends
// @match https://www.nitrotype.com/race
// @match https://www.nitrotype.com/race/*
// @match https://www.nitrotype.com/racelog*
// @match https://www.nitrotype.com/stats*
// @match *://*.nitrotype.com/settings/mods*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant unsafeWindow
// @connect ntleaderboards.com
// @connect ntstartrack.org
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const pageWindow = (typeof unsafeWindow !== 'undefined' && unsafeWindow) ? unsafeWindow : window;
const SCRIPT_SINGLETON_KEY = '__ntBotFlagSingleton';
if (pageWindow[SCRIPT_SINGLETON_KEY]) {
try { console.info('[BOTFLAG] Duplicate instance detected; skipping.'); } catch (e) { }
return;
}
pageWindow[SCRIPT_SINGLETON_KEY] = true;
// ===== SHARED OPTIMIZATION LAYER (Compat across scripts) =====
function initNTShared() {
const shared = window.NTShared || {};
shared.version = '2.0.0';
const existingCache = shared.cache;
const hasMapCache = existingCache instanceof Map;
const cacheMap = shared.cacheMap || (hasMapCache ? existingCache : new Map());
const cacheObj = shared.cacheObj || (!hasMapCache && existingCache && typeof existingCache === 'object' ? existingCache : {
individual: { data: null, timestamp: 0, expiresAt: 0 },
team: { data: null, timestamp: 0, expiresAt: 0 },
isbot: new Map()
});
if (!cacheObj.individual) cacheObj.individual = { data: null, timestamp: 0, expiresAt: 0 };
if (!cacheObj.team) cacheObj.team = { data: null, timestamp: 0, expiresAt: 0 };
if (!cacheObj.isbot) cacheObj.isbot = new Map();
const isbot = shared.isbot || cacheObj.isbot || new Map();
cacheObj.isbot = isbot;
if (!shared.cache) shared.cache = cacheObj;
shared.cacheMap = cacheMap;
shared.cacheObj = cacheObj;
shared.isbot = isbot;
shared._makeKey = shared._makeKey || function(view, timeframe, startKey, endKey) {
return `${view}_${timeframe}_${startKey}_${endKey}`;
};
const BOT_STATUS_TTL_MS = shared.BOT_STATUS_TTL_MS || (24 * 60 * 60 * 1000);
const BOT_STATUS_MAX_ENTRIES = shared.BOT_STATUS_MAX_ENTRIES || 5000;
shared.BOT_STATUS_TTL_MS = BOT_STATUS_TTL_MS;
shared.BOT_STATUS_MAX_ENTRIES = BOT_STATUS_MAX_ENTRIES;
const syncState = shared._syncState || {
initialized: false,
tabId: `tab_${Math.random().toString(36).slice(2)}_${Date.now()}`,
broadcastChannel: null,
storageKey: 'ntSharedCacheSyncV1'
};
shared._syncState = syncState;
function emitLocalUpdate(detail) {
window.dispatchEvent(new CustomEvent('nt-cache-updated', { detail: detail }));
}
function normalizeBotEntry(raw, now) {
if (!raw) return null;
if (raw && typeof raw === 'object' && raw.status && typeof raw.ts === 'number') return raw;
return { status: raw, ts: now };
}
function pruneBotCache() {
const now = Date.now();
isbot.forEach((value, key) => {
const entry = normalizeBotEntry(value, now);
if (!entry) {
isbot.delete(key);
return;
}
if (now - entry.ts > BOT_STATUS_TTL_MS) {
isbot.delete(key);
return;
}
if (entry !== value) isbot.set(key, entry);
});
if (isbot.size <= BOT_STATUS_MAX_ENTRIES) return;
const entries = Array.from(isbot.entries())
.map(([key, value]) => [key, normalizeBotEntry(value, now)])
.filter(([, value]) => !!value)
.sort((a, b) => a[1].ts - b[1].ts);
const overflow = entries.length - BOT_STATUS_MAX_ENTRIES;
for (let i = 0; i < overflow; i++) {
isbot.delete(entries[i][0]);
}
}
function postCrossTabUpdate(payload) {
if (!payload || !syncState.initialized) return;
// Respect CROSS_TAB_SYNC setting if the bot-flag bridge is available
if (typeof readNtcfgBotFlagValue === 'function' && readNtcfgBotFlagValue('CROSS_TAB_SYNC') === false) return;
const message = Object.assign({}, payload, {
__ntSharedSync: true,
origin: syncState.tabId,
sentAt: Date.now()
});
if (syncState.broadcastChannel) {
try { syncState.broadcastChannel.postMessage(message); } catch (e) {}
}
try {
localStorage.setItem(syncState.storageKey, JSON.stringify(message));
localStorage.removeItem(syncState.storageKey);
} catch (e) {}
}
function setLegacyCacheInternal(type, data, expiresAt, options = {}) {
if (!cacheObj[type]) cacheObj[type] = { data: null, timestamp: 0, expiresAt: 0 };
const now = options.timestamp || Date.now();
cacheObj[type].data = data;
cacheObj[type].timestamp = now;
cacheObj[type].expiresAt = expiresAt || (now + 3600000);
emitLocalUpdate({ type, data, expiresAt: cacheObj[type].expiresAt, source: options.source || 'local' });
if (options.broadcast !== false) {
postCrossTabUpdate({
kind: 'legacy',
type: type,
data: data,
expiresAt: cacheObj[type].expiresAt,
timestamp: now
});
}
}
function setKeyedCacheInternal(key, data, expiresAt, options = {}) {
const now = options.timestamp || Date.now();
const ttl = expiresAt || (now + 3600000);
cacheMap.set(key, { data: data, timestamp: now, expiresAt: ttl });
emitLocalUpdate({ key, data, expiresAt: ttl, source: options.source || 'local' });
if (options.broadcast !== false) {
postCrossTabUpdate({
kind: 'keyed',
key: key,
data: data,
expiresAt: ttl,
timestamp: now
});
}
}
function setBotStatusInternal(username, status, options = {}) {
if (!username) return;
const key = username.toLowerCase();
const now = options.timestamp || Date.now();
isbot.set(key, { status: status, ts: now });
pruneBotCache();
emitLocalUpdate({ type: 'isbot', username: key, data: status, source: options.source || 'local' });
if (options.broadcast !== false) {
postCrossTabUpdate({
kind: 'isbot',
username: key,
status: status,
timestamp: now
});
}
}
function applyCrossTabMessage(message, source) {
if (!message || message.origin === syncState.tabId) return;
if (message.kind === 'legacy' && message.type) {
setLegacyCacheInternal(message.type, message.data, message.expiresAt, {
broadcast: false,
timestamp: message.timestamp,
source: source || 'remote'
});
return;
}
if (message.kind === 'keyed' && message.key) {
setKeyedCacheInternal(message.key, message.data, message.expiresAt, {
broadcast: false,
timestamp: message.timestamp,
source: source || 'remote'
});
return;
}
if (message.kind === 'isbot' && message.username) {
setBotStatusInternal(message.username, message.status, {
broadcast: false,
timestamp: message.timestamp,
source: source || 'remote'
});
}
}
if (!syncState.initialized) {
syncState.initialized = true;
if (typeof BroadcastChannel !== 'undefined') {
try {
syncState.broadcastChannel = new BroadcastChannel('ntSharedCacheSyncV1');
syncState.broadcastChannel.onmessage = function(event) {
const message = event ? event.data : null;
if (!message || !message.__ntSharedSync) return;
applyCrossTabMessage(message, 'broadcast');
};
} catch (e) {}
}
window.addEventListener('storage', (event) => {
if (!event || event.key !== syncState.storageKey || !event.newValue) return;
try {
const message = JSON.parse(event.newValue);
if (!message || !message.__ntSharedSync) return;
applyCrossTabMessage(message, 'storage');
} catch (e) {}
});
}
shared.setLegacyCache = function(type, data, expiresAt) {
setLegacyCacheInternal(type, data, expiresAt);
};
shared.setCache = function(key, data, expiresAt) {
if (typeof key === 'string' && cacheObj[key]) {
setLegacyCacheInternal(key, data, expiresAt);
return;
}
setKeyedCacheInternal(key, data, expiresAt);
};
shared.getCache = function(key, maxAge) {
const now = Date.now();
const maxAgeMs = typeof maxAge === 'number' ? maxAge : Number.POSITIVE_INFINITY;
if (typeof key === 'string' && cacheObj[key]) {
const cached = cacheObj[key];
if (!cached || !cached.data) return null;
const age = now - (cached.timestamp || 0);
if (age < maxAgeMs && now < (cached.expiresAt || 0)) return cached.data;
return null;
}
const cached = cacheMap.get(key);
if (!cached || !cached.data) return null;
const age = now - (cached.timestamp || 0);
if (age < maxAgeMs && now < cached.expiresAt) return cached.data;
return null;
};
shared.getTimestamp = function(key) {
if (typeof key === 'string' && cacheObj[key]) return cacheObj[key].timestamp || null;
const cached = cacheMap.get(key);
return cached?.timestamp || null;
};
shared.getLegacyCache = function(type, maxAge) {
return shared.getCache(type, maxAge);
};
shared.getBotStatus = function(username) {
if (!username) return null;
const key = username.toLowerCase();
const value = isbot.get(key);
if (!value) return null;
const entry = normalizeBotEntry(value, Date.now());
if (!entry) return null;
if (Date.now() - entry.ts > BOT_STATUS_TTL_MS) {
isbot.delete(key);
return null;
}
if (entry !== value) isbot.set(key, entry);
return entry.status;
};
shared.setBotStatus = function(username, status) {
setBotStatusInternal(username, status);
};
window.NTShared = shared;
return shared;
}
initNTShared();
function clearSharedBotStatusCache() {
const shared = window.NTShared;
if (!shared || typeof shared !== 'object') return;
if (shared.isbot && typeof shared.isbot.clear === 'function') {
shared.isbot.clear();
}
const cacheObj = shared.cacheObj || shared.cache;
if (cacheObj && cacheObj.isbot && cacheObj.isbot !== shared.isbot && typeof cacheObj.isbot.clear === 'function') {
cacheObj.isbot.clear();
}
}
function initObserverManager() {
const existing = window.NTObserverManager || {};
if (existing.version !== '1.0.0' && existing.observer && typeof existing.observer.disconnect === 'function') {
try { existing.observer.disconnect(); } catch (e) { }
existing.observer = null;
}
if (existing.bodyWaitObserver && typeof existing.bodyWaitObserver.disconnect === 'function') {
try { existing.bodyWaitObserver.disconnect(); } catch (e) { }
existing.bodyWaitObserver = null;
}
if (existing.debounceTimer) {
clearTimeout(existing.debounceTimer);
existing.debounceTimer = null;
}
if (existing.bodyWaitTimer) {
clearTimeout(existing.bodyWaitTimer);
existing.bodyWaitTimer = null;
}
existing.callbacks = existing.callbacks || {};
existing.version = '1.0.0';
existing.flushCallbacks = function() {
Object.values(this.callbacks).forEach((cb) => {
try { cb(); } catch (e) { console.error('[Observer Error]', e); }
});
};
existing.startObserver = function() {
if (this.observer || !document.body) return !!this.observer;
this.observer = new MutationObserver(() => {
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => {
this.flushCallbacks();
}, 250);
});
this.observer.observe(document.body, {
childList: true,
subtree: true
});
if (this.bodyWaitObserver && typeof this.bodyWaitObserver.disconnect === 'function') {
try { this.bodyWaitObserver.disconnect(); } catch (e) { }
this.bodyWaitObserver = null;
}
if (this.bodyWaitTimer) {
clearTimeout(this.bodyWaitTimer);
this.bodyWaitTimer = null;
}
setTimeout(() => {
this.flushCallbacks();
}, 0);
return true;
};
existing.ensureObserver = function() {
if (this.startObserver()) return;
if (this.bodyWaitObserver) return;
const tryStart = () => {
if (!this.startObserver()) return false;
if (this.bodyWaitObserver && typeof this.bodyWaitObserver.disconnect === 'function') {
try { this.bodyWaitObserver.disconnect(); } catch (e) { }
this.bodyWaitObserver = null;
}
if (this.bodyWaitTimer) {
clearTimeout(this.bodyWaitTimer);
this.bodyWaitTimer = null;
}
return true;
};
const root = document.documentElement || document;
if (!root) return;
this.bodyWaitObserver = new MutationObserver(() => {
tryStart();
});
this.bodyWaitObserver.observe(root, {
childList: true,
subtree: true
});
this.bodyWaitTimer = setTimeout(() => {
tryStart();
}, 0);
};
existing.register = function(scriptName, callback) {
const observerAlreadyStarted = !!this.observer;
this.callbacks[scriptName] = callback;
this.ensureObserver();
if (observerAlreadyStarted) {
setTimeout(() => {
try { callback(); } catch (e) { console.error('[Observer Error]', e); }
}, 0);
}
};
window.NTObserverManager = existing;
}
initObserverManager();
// ─── Mod Menu Manifest Bridge ───────────────────────────────────────────────
const NTCFG_BOT_FLAG_MANIFEST_ID = "bot-flag";
const NTCFG_BOT_FLAG_MANIFEST_KEY = `ntcfg:manifest:${NTCFG_BOT_FLAG_MANIFEST_ID}`;
const NTCFG_BOT_FLAG_VALUE_PREFIX = `ntcfg:${NTCFG_BOT_FLAG_MANIFEST_ID}:`;
const NTCFG_BOT_FLAG_BRIDGE_VERSION = "1.0.0-bridge.1";
const BOT_FLAG_STORAGE_VERSION = 1;
const BOT_FLAG_STORAGE_VERSION_KEY = `${NTCFG_BOT_FLAG_VALUE_PREFIX}__storage_version`;
const BOT_FLAG_LIVE_REFRESH_KEYS = new Set([
'SHOW_TEAM_PAGE_FLAGS',
'SHOW_FRIENDS_PAGE_FLAGS',
'SHOW_LEAGUE_PAGE_FLAGS',
'SHOW_RACE_RESULT_FLAGS',
'SHOW_NOT_FLAGGED',
'SHOW_UNTRACKED',
'USE_ROBOT_ICON',
'USE_STARTRACK',
'USE_NTL_LEGACY'
]);
const BOT_FLAG_SHARED_SETTINGS = {
SHOW_TEAM_PAGE_FLAGS: {
type: 'boolean',
label: 'Show Team Page Flags',
default: true,
group: 'Display',
description: 'Display bot-flag icons on team member lists.'
},
SHOW_FRIENDS_PAGE_FLAGS: {
type: 'boolean',
label: 'Show Friends Page Flags',
default: true,
group: 'Display',
description: 'Display bot-flag icons on the friends page.'
},
SHOW_LEAGUE_PAGE_FLAGS: {
type: 'boolean',
label: 'Show League Page Flags',
default: true,
group: 'Display',
description: 'Display bot-flag icons on the leagues page.'
},
SHOW_RACE_RESULT_FLAGS: {
type: 'boolean',
label: 'Show Race Result Flags',
default: true,
group: 'Display',
description: 'Display bot-flag icons on race results.'
},
SHOW_NOT_FLAGGED: {
type: 'boolean',
label: 'Show Not Flagged',
default: true,
group: 'Display',
description: 'Show the green icon for racers that are not flagged. Disable to only see flagged/banned racers.'
},
SHOW_UNTRACKED: {
type: 'boolean',
label: 'Show Untracked',
default: true,
group: 'Display',
description: 'Show the gold icon for racers that are not tracked by StarTrack. Disable to hide untracked indicators.'
},
USE_ROBOT_ICON: {
type: 'boolean',
label: 'Robot Icon for Flagged',
default: false,
group: 'Display',
description: 'Use a robot head icon instead of a flag for StarTrack-flagged racers.'
},
USE_STARTRACK: {
type: 'boolean',
label: 'Use StarTrack',
default: true,
group: 'Sources',
description: 'Query NT StarTrack for flag data.'
},
USE_NTL_LEGACY: {
type: 'boolean',
label: 'Use NTL Legacy',
default: true,
group: 'Sources',
description: 'Query NTL Legacy for flag data.'
},
NETWORK_CONCURRENCY_LIMIT: {
type: 'number',
label: 'Network Concurrency Limit',
default: 6,
group: 'Sources',
description: 'Maximum number of simultaneous network requests for flag lookups.',
min: 1,
max: 20,
step: 1
},
STARTRACK_CACHE_DAYS: {
type: 'number',
label: 'StarTrack Cache Duration (days)',
default: 7,
group: 'Cache',
description: 'Number of days to keep cached StarTrack results before re-fetching.',
min: 1,
max: 30,
step: 1
},
CROSS_TAB_SYNC: {
type: 'boolean',
label: 'Cross-Tab Sync',
default: true,
group: 'Cache',
description: 'Synchronize flag cache across browser tabs via localStorage.'
},
DEBUG_LOGGING: {
type: 'boolean',
label: 'Debug Logging',
default: false,
group: 'Advanced',
description: 'Enable verbose console logging for troubleshooting.'
}
};
const getNtcfgBotFlagStorageKey = (settingKey) => `${NTCFG_BOT_FLAG_VALUE_PREFIX}${settingKey}`;
const coerceNtcfgBotFlagValue = (settingKey, value) => {
const meta = BOT_FLAG_SHARED_SETTINGS[settingKey];
if (!meta) return value;
if (meta.type === 'boolean') {
if (typeof value === 'string') {
const raw = value.trim().toLowerCase();
if (raw === 'false' || raw === '0' || raw === 'off') return false;
if (raw === 'true' || raw === '1' || raw === 'on') return true;
}
return !!value;
}
if (meta.type === 'number') {
const fallback = Number(meta.default);
const parsed = Number(value);
let normalized = Number.isFinite(parsed) ? parsed : fallback;
const min = Number(meta.min);
const max = Number(meta.max);
const step = Number(meta.step);
if (Number.isFinite(step) && step >= 1) {
normalized = Math.round(normalized);
}
if (Number.isFinite(min)) {
normalized = Math.max(min, normalized);
}
if (Number.isFinite(max)) {
normalized = Math.min(max, normalized);
}
return normalized;
}
if (meta.type === 'select') {
const raw = String(value ?? '').trim();
const options = Array.isArray(meta.options) ? meta.options : [];
return options.some((option) => String(option.value) === raw) ? raw : meta.default;
}
return String(value ?? meta.default);
};
const readNtcfgBotFlagValue = (settingKey) => {
const meta = BOT_FLAG_SHARED_SETTINGS[settingKey];
if (!meta) return undefined;
try {
const raw = localStorage.getItem(getNtcfgBotFlagStorageKey(settingKey));
if (raw == null) return meta.default;
const parsed = JSON.parse(raw);
return coerceNtcfgBotFlagValue(settingKey, parsed);
} catch {
return meta.default;
}
};
const writeNtcfgBotFlagValue = (settingKey, value) => {
try {
const serialized = JSON.stringify(value);
if (localStorage.getItem(getNtcfgBotFlagStorageKey(settingKey)) !== serialized) {
localStorage.setItem(getNtcfgBotFlagStorageKey(settingKey), serialized);
}
} catch {
// ignore storage sync failures
}
};
const syncNtcfgBotFlagSettingFromGM = (settingKey) => {
const meta = BOT_FLAG_SHARED_SETTINGS[settingKey];
if (!meta) return;
const normalized = coerceNtcfgBotFlagValue(settingKey, GM_getValue(settingKey, meta.default));
writeNtcfgBotFlagValue(settingKey, normalized);
// Sync DEBUG_LOGGING bidirectionally with the existing ntBotFlagDebug localStorage key
if (settingKey === 'DEBUG_LOGGING') {
syncBotFlagDebugToggle(normalized);
}
};
const syncAllNtcfgBotFlagSettingsFromGM = () => {
Object.keys(BOT_FLAG_SHARED_SETTINGS).forEach(syncNtcfgBotFlagSettingFromGM);
};
const registerNtcfgBotFlagManifest = () => {
try {
const manifest = {
id: NTCFG_BOT_FLAG_MANIFEST_ID,
name: 'Flagged Racers',
version: NTCFG_BOT_FLAG_BRIDGE_VERSION,
scriptVersion: typeof GM_info !== 'undefined' ? GM_info.script.version : '',
storageVersion: BOT_FLAG_STORAGE_VERSION,
supportsGlobalReset: true,
description: 'Flag and ban detection using NT StarTrack and NTL Legacy sources.',
sections: [
{ id: 'display', title: 'Display', subtitle: 'Visual placement and on-page behavior.', resetButton: true },
{ id: 'sources', title: 'Sources', subtitle: 'Data source preferences and fetch policy.', resetButton: true },
{ id: 'cache', title: 'Cache', subtitle: 'How long flag results should be kept around.', resetButton: true },
{ id: 'advanced', title: 'Advanced', subtitle: 'Debug and diagnostic controls.', resetButton: true }
],
settings: BOT_FLAG_SHARED_SETTINGS
};
const serialized = JSON.stringify(manifest);
if (localStorage.getItem(NTCFG_BOT_FLAG_MANIFEST_KEY) !== serialized) {
localStorage.setItem(NTCFG_BOT_FLAG_MANIFEST_KEY, serialized);
}
} catch {
// ignore manifest registration failures
}
};
const setNtcfgBotFlagValue = (settingKey, value) => {
const meta = BOT_FLAG_SHARED_SETTINGS[settingKey];
if (!meta) return value;
const normalized = coerceNtcfgBotFlagValue(settingKey, value);
GM_setValue(settingKey, normalized);
writeNtcfgBotFlagValue(settingKey, normalized);
// Sync DEBUG_LOGGING bidirectionally with the existing ntBotFlagDebug localStorage key
if (settingKey === 'DEBUG_LOGGING') {
syncBotFlagDebugToggle(normalized);
}
applyBotFlagSettingSideEffects(settingKey);
return normalized;
};
// Direct apply: always writes through to GM (used for same-tab ntcfg:change events from mod menu)
const applyNtcfgBotFlagValueDirect = (settingKey, value) => {
const meta = BOT_FLAG_SHARED_SETTINGS[settingKey];
if (!meta) return;
const normalized = coerceNtcfgBotFlagValue(settingKey, value);
setNtcfgBotFlagValue(settingKey, normalized);
};
// Deduped apply: compares against GM before writing (used for cross-tab storage events)
const applyNtcfgBotFlagValueIfChanged = (settingKey, value) => {
const meta = BOT_FLAG_SHARED_SETTINGS[settingKey];
if (!meta) return;
const normalized = coerceNtcfgBotFlagValue(settingKey, value);
const currentValue = coerceNtcfgBotFlagValue(settingKey, GM_getValue(settingKey, meta.default));
if (JSON.stringify(currentValue) !== JSON.stringify(normalized)) {
setNtcfgBotFlagValue(settingKey, normalized);
}
};
// Bidirectional sync for DEBUG_LOGGING <-> ntBotFlagDebug localStorage key
const syncBotFlagDebugToggle = (enabled) => {
try {
localStorage.setItem('ntBotFlagDebug', enabled ? '1' : '0');
} catch {
// ignore storage errors
}
};
// Helper to check if a specific bot-flag feature is enabled via ntcfg
function isNtcfgBotFlagFeatureEnabled(settingKey) {
const val = readNtcfgBotFlagValue(settingKey);
return val !== false;
}
const dispatchBotFlagActionResult = (requestId, status, error = '') => {
if (!requestId) return;
try {
document.dispatchEvent(new CustomEvent('ntcfg:action-result', {
detail: {
requestId,
script: NTCFG_BOT_FLAG_MANIFEST_ID,
status,
error
}
}));
} catch {
// ignore dispatch failures
}
};
const resetBotFlagSettingsToDefaults = () => {
Object.entries(BOT_FLAG_SHARED_SETTINGS).forEach(([settingKey, meta]) => {
if (!meta || meta.type === 'note' || meta.type === 'action') return;
setNtcfgBotFlagValue(settingKey, meta.default);
});
try { GM_deleteValue('nt_token'); } catch { /* ignore */ }
try {
if (typeof GM_listValues === 'function') {
GM_listValues().forEach((key) => {
if (key.startsWith('startrack_')
|| key.startsWith('ntl_')
|| key.startsWith('team_activity_v2_')
|| key === 'team_applications_v2'
|| key === 'leagues_user_activity_v2') {
GM_deleteValue(key);
}
});
}
} catch { /* ignore */ }
try { localStorage.removeItem('ntBotFlagDebug'); } catch { /* ignore */ }
clearSharedBotStatusCache();
};
// Listen for mod menu changes (same tab)
document.addEventListener('ntcfg:change', (event) => {
if (event?.detail?.script !== NTCFG_BOT_FLAG_MANIFEST_ID) return;
applyNtcfgBotFlagValueDirect(event.detail.key, event.detail.value);
});
document.addEventListener('ntcfg:action', (event) => {
const detail = event?.detail || {};
if (detail.script !== '*') return;
if (detail.key !== 'clear-settings' || detail.scope !== 'prefs+caches') return;
try {
resetBotFlagSettingsToDefaults();
GM_setValue(BOT_FLAG_STORAGE_VERSION_KEY, BOT_FLAG_STORAGE_VERSION);
registerNtcfgBotFlagManifest();
syncAllNtcfgBotFlagSettingsFromGM();
document.dispatchEvent(new CustomEvent('ntcfg:manifest-updated', {
detail: { script: NTCFG_BOT_FLAG_MANIFEST_ID }
}));
dispatchBotFlagActionResult(detail.requestId, 'success');
} catch (error) {
dispatchBotFlagActionResult(detail.requestId, 'error', error?.message || String(error));
}
});
// Listen for cross-tab changes
window.addEventListener('storage', (event) => {
const storageKey = String(event?.key || '');
if (!storageKey.startsWith(NTCFG_BOT_FLAG_VALUE_PREFIX) || event.newValue == null) return;
const settingKey = storageKey.slice(NTCFG_BOT_FLAG_VALUE_PREFIX.length);
if (!BOT_FLAG_SHARED_SETTINGS[settingKey]) return;
try {
applyNtcfgBotFlagValueIfChanged(settingKey, JSON.parse(event.newValue));
} catch {
// ignore invalid synced payloads
}
});
// Register manifest and sync settings
registerNtcfgBotFlagManifest();
syncAllNtcfgBotFlagSettingsFromGM();
try { GM_setValue(BOT_FLAG_STORAGE_VERSION_KEY, BOT_FLAG_STORAGE_VERSION); } catch { /* ignore */ }
const publishNtcfgBotFlagManifestHeartbeat = () => {
try { localStorage.setItem('ntcfg:alive:' + NTCFG_BOT_FLAG_MANIFEST_ID, String(Date.now())); } catch { /* ignore */ }
try {
document.dispatchEvent(new CustomEvent('ntcfg:manifest-updated', {
detail: { script: NTCFG_BOT_FLAG_MANIFEST_ID }
}));
} catch {
// ignore event dispatch failures
}
};
publishNtcfgBotFlagManifestHeartbeat();
// ─── End Mod Menu Manifest Bridge ───────────────────────────────────────────
// =============================
// 🎨 Constants & Configuration
// =============================
function getStarTrackCacheDuration() {
const days = readNtcfgBotFlagValue('STARTRACK_CACHE_DAYS');
return (typeof days === 'number' && days > 0 ? days : 7) * 24 * 60 * 60 * 1000;
}
function getNetworkConcurrencyLimit() {
const limit = readNtcfgBotFlagValue('NETWORK_CONCURRENCY_LIMIT');
return typeof limit === 'number' && limit > 0 ? limit : 6;
}
const FRIENDS_PAGE_RUN_COOLDOWN_MS = 400;
const BOT_FLAG_VIEW_REFRESH_DELAYS = [100, 320, 800];
const DEBUG_FLAG_KEY = 'ntBotFlagDebug';
const DEBUG_PARAM_ENABLED = /(?:\?|&)ntbotdebug=1(?:&|$)/.test(window.location.search);
const SCRIPT_VERSION = typeof GM_info !== 'undefined' && GM_info?.script?.version
? String(GM_info.script.version)
: '5.1.2';
const networkQueue = [];
const startrackInflight = new Map();
const ntlInflight = new Map();
let activeNetworkRequests = 0;
let lastFriendsPageRunAt = 0;
let lastBotFlagViewSignature = '';
const colorMap = {
green: "#4ade80",
red: "#d62f3a",
yellow: "#f3a81b",
orange: "#e8796b",
gray: "#a6aac1"
};
let customTooltipElement = null;
function normalizeUsername(username) {
return String(username || '').trim().toLowerCase();
}
function normalizeDisplayName(displayName) {
return String(displayName || '').trim();
}
function normalizeTeamTag(teamTag) {
return String(teamTag || '').replace(/[\[\]\s]/g, '').trim().toLowerCase();
}
function normalizeBotFlagViewSignatureText(value) {
return String(value || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
function buildBotFlagViewSignatureSample(values, limit = 8) {
return values
.map((value) => normalizeBotFlagViewSignatureText(value))
.filter(Boolean)
.slice(0, limit)
.join('|');
}
function isIdentityLookup(value) {
return !!(value
&& typeof value === 'object'
&& value.byDisplay
&& value.byDisplayAndTag
&& value.byUsername
&& value.ambiguousDisplay);
}
function createIdentityLookup(entries) {
const lookup = {
byDisplay: Object.create(null),
byDisplayAndTag: Object.create(null),
byUsername: Object.create(null),
ambiguousDisplay: Object.create(null)
};
(Array.isArray(entries) ? entries : []).forEach((entry) => {
const displayName = normalizeDisplayName(entry?.displayName || entry?.username || '');
const username = normalizeUsername(entry?.username || '');
const teamTag = normalizeTeamTag(entry?.tag || entry?.teamTag || entry?.team || '');
if (username) {
lookup.byUsername[username] = username;
}
if (displayName && username && teamTag) {
lookup.byDisplayAndTag[`${displayName}::${teamTag}`] = username;
}
if (!displayName || !username) return;
const existing = lookup.byDisplay[displayName];
if (!existing) {
lookup.byDisplay[displayName] = username;
return;
}
if (existing !== username) {
delete lookup.byDisplay[displayName];
lookup.ambiguousDisplay[displayName] = true;
}
});
return lookup;
}
function resolveUsernameFromLookup(lookup, options = {}) {
if (!isIdentityLookup(lookup)) return '';
const explicitUsername = normalizeUsername(options.username);
if (explicitUsername && lookup.byUsername[explicitUsername]) {
return lookup.byUsername[explicitUsername];
}
const displayName = normalizeDisplayName(options.displayName);
const teamTag = normalizeTeamTag(options.teamTag);
if (displayName && teamTag) {
const combined = lookup.byDisplayAndTag[`${displayName}::${teamTag}`];
if (combined) return combined;
}
if (displayName && lookup.byDisplay[displayName]) {
return lookup.byDisplay[displayName];
}
return '';
}
function isAmbiguousDisplayMatch(lookup, displayName) {
if (!isIdentityLookup(lookup)) return false;
const normalizedDisplay = normalizeDisplayName(displayName);
return !!(normalizedDisplay && lookup.ambiguousDisplay[normalizedDisplay]);
}
function getDisplayNameFromElement(element) {
if (!element) return '';
const titled = normalizeDisplayName(element.getAttribute && element.getAttribute('title'));
if (titled) return titled;
return normalizeDisplayName(element.textContent);
}
function extractTeamTagFromElement(element) {
if (!element) return '';
const root = element?.classList?.contains('player-name--container')
? element.closest('.table-row, .gridTable-row, .raceResults-body.row, .row') || element.parentElement || element
: element;
const tagCandidates = [
root?.querySelector?.('.player-name--tag'),
root?.querySelector?.('.team-tag'),
root?.querySelector?.('.team-name--tag'),
element?.previousElementSibling
].filter(Boolean);
for (const candidate of tagCandidates) {
if (!candidate) continue;
const text = normalizeTeamTag(candidate.textContent || '');
if (text) return text;
}
const linkCandidates = [];
if (typeof root?.querySelectorAll === 'function') {
root.querySelectorAll('a[href*="/team/"]').forEach((link) => linkCandidates.push(link));
}
if (typeof element?.closest === 'function') {
const direct = element.closest('a[href*="/team/"]');
if (direct) linkCandidates.push(direct);
}
for (const link of linkCandidates) {
const href = link?.getAttribute?.('href') || '';
const match = href.match(/\/team\/([^/?#]+)/i);
const normalized = normalizeTeamTag(match ? match[1] : '');
if (normalized) return normalized;
}
return '';
}
function extractExplicitUsernameFromElement(element) {
if (!element) return '';
const attrCandidates = [
element,
element.closest?.('[data-nt-username]'),
element.closest?.('[data-username]'),
element.closest?.('[data-user]')
].filter(Boolean);
for (const candidate of attrCandidates) {
const attrValue = candidate.getAttribute('data-nt-username')
|| candidate.getAttribute('data-username')
|| candidate.getAttribute('data-user');
const normalized = normalizeUsername(attrValue);
if (normalized) return normalized;
}
const linkCandidates = [];
const directLink = element.closest?.('a[href*="/racer/"]');
if (directLink) linkCandidates.push(directLink);
const searchRoots = [
element,
element.closest?.('.player-name--container'),
element.closest?.('.table-row'),
element.closest?.('.gridTable-row'),
element.closest?.('.modal--raceResults'),
element.closest?.('.profile-title')
].filter(Boolean);
searchRoots.forEach((root) => {
if (typeof root.querySelectorAll !== 'function') return;
root.querySelectorAll('a[href*="/racer/"]').forEach((link) => linkCandidates.push(link));
});
for (const link of linkCandidates) {
const href = link?.getAttribute?.('href') || '';
const match = href.match(/\/racer\/([^/?#]+)/i);
const normalized = normalizeUsername(match ? match[1] : '');
if (normalized) return normalized;
}
if (window.location.pathname.startsWith('/racer/')) {
return normalizeUsername(window.location.pathname.split('/').pop());
}
return '';
}
function extractRecentTabUsernameFromElement(element) {
const explicitUsername = extractExplicitUsernameFromElement(element);
if (explicitUsername) return explicitUsername;
const namedContainer = element?.closest?.('.player-name--container')
|| element?.querySelector?.('.player-name--container');
if (namedContainer) {
const titledUsername = normalizeUsername(namedContainer.getAttribute('title'));
if (titledUsername) return titledUsername;
}
const visibleName = normalizeUsername(getDisplayNameFromElement(
element?.classList?.contains?.('player-name--container')
? element
: element?.querySelector?.('.player-name--container') || element
));
return visibleName || '';
}
function resolveUsernameForElement(element, lookup, surfaceLabel) {
const explicitUsername = extractExplicitUsernameFromElement(element);
if (explicitUsername) return explicitUsername;
const playerElement = element?.classList?.contains('player-name--container')
? element
: element?.querySelector?.('.player-name--container') || element;
const displayName = getDisplayNameFromElement(playerElement);
if (!displayName) return '';
const teamTag = extractTeamTagFromElement(playerElement || element);
const mappedUsername = resolveUsernameFromLookup(lookup, {
displayName: displayName,
teamTag: teamTag
});
if (!mappedUsername && isAmbiguousDisplayMatch(lookup, displayName)) {
debugLog('Skipped ambiguous display-name match', {
surface: surfaceLabel,
displayName: displayName,
teamTag: teamTag || ''
});
}
return mappedUsername;
}
function isDebugEnabled() {
return DEBUG_PARAM_ENABLED || localStorage.getItem(DEBUG_FLAG_KEY) === '1' || readNtcfgBotFlagValue('DEBUG_LOGGING') === true;
}
function debugLog(event, payload) {
if (!isDebugEnabled()) return;
const ts = new Date().toISOString();
if (payload === undefined) {
console.log(`[BotFlag][DBG ${ts}] ${event}`);
return;
}
console.log(`[BotFlag][DBG ${ts}] ${event}`, payload);
}
function queueNetworkRequest(requestFn, meta = {}) {
const label = meta.label || 'request';
const username = meta.username ? normalizeUsername(meta.username) : null;
return new Promise((resolve, reject) => {
networkQueue.push({ requestFn, resolve, reject, label, username });
debugLog('Queue enqueue', {
label: label,
username: username,
queueDepth: networkQueue.length,
activeNetworkRequests: activeNetworkRequests,
concurrencyLimit: getNetworkConcurrencyLimit()
});
drainNetworkQueue();
});
}
function drainNetworkQueue() {
if (activeNetworkRequests >= getNetworkConcurrencyLimit()) return;
const next = networkQueue.shift();
if (!next) return;
activeNetworkRequests += 1;
debugLog('Queue start', {
label: next.label,
username: next.username,
queueDepth: networkQueue.length,
activeNetworkRequests: activeNetworkRequests
});
Promise.resolve()
.then(next.requestFn)
.then(next.resolve, next.reject)
.finally(() => {
activeNetworkRequests -= 1;
debugLog('Queue complete', {
label: next.label,
username: next.username,
queueDepth: networkQueue.length,
activeNetworkRequests: activeNetworkRequests
});
drainNetworkQueue();
});
}
function initDebugAPI() {
const debugApi = {
enable: function() {
localStorage.setItem(DEBUG_FLAG_KEY, '1');
console.log('[BotFlag][DBG] Enabled.');
},
disable: function() {
localStorage.removeItem(DEBUG_FLAG_KEY);
console.log('[BotFlag][DBG] Disabled.');
},
status: function() {
const status = {
enabled: isDebugEnabled(),
paramEnabled: DEBUG_PARAM_ENABLED,
scriptVersion: SCRIPT_VERSION,
queueDepth: networkQueue.length,
activeNetworkRequests: activeNetworkRequests,
concurrencyLimit: getNetworkConcurrencyLimit(),
startrackInflight: startrackInflight.size,
ntlInflight: ntlInflight.size
};
console.log('[BotFlag][DBG] Status', status);
return status;
},
inspectUser: function(username) {
const normalized = normalizeUsername(username);
const result = {
username: normalized,
ntSharedStatus: window.NTShared && window.NTShared.getBotStatus ? window.NTShared.getBotStatus(normalized) : null,
startrackCache: GM_getValue(`startrack_${normalized}`),
ntlCache: GM_getValue(`ntl_${normalized}`),
startrackInflight: startrackInflight.has(normalized),
ntlInflight: ntlInflight.has(normalized)
};
console.log('[BotFlag][DBG] Inspect user', result);
return result;
},
clearRowState: function() {
const targets = document.querySelectorAll('[data-status-processing],[data-status-processing-for],[data-status-processed-for],[data-nt-username],[data-botflag-processed]');
targets.forEach((el) => {
el.removeAttribute('data-status-processing');
el.removeAttribute('data-status-processing-for');
el.removeAttribute('data-status-processed-for');
el.removeAttribute('data-nt-username');
el.removeAttribute('data-botflag-processed');
});
console.log(`[BotFlag][DBG] Cleared row state on ${targets.length} elements.`);
return targets.length;
},
clearTeamActivityCache: function(teamTag) {
const fromPath = (window.location.pathname.split('/').filter(Boolean).pop() || '').trim();
const raw = String(teamTag || fromPath || '').trim();
if (!raw) {
console.log('[BotFlag][DBG] No team tag available to clear cache.');
return [];
}
const upper = raw.toUpperCase();
const lower = raw.toLowerCase();
const keys = [
`team_activity_${upper}`,
`team_activity_${lower}`,
`team_activity_v2_${upper}`,
`team_activity_v2_${lower}`
];
keys.forEach((key) => GM_setValue(key, null));
console.log('[BotFlag][DBG] Cleared team activity cache keys', keys);
return keys;
},
refreshTeamActivityNow: async function(teamTag) {
this.clearTeamActivityCache(teamTag);
this.clearRowState();
const data = await fetchTeamActivity();
if (data) await updateUsersTeam(data);
const summary = {
refreshed: !!data,
users: data ? Object.keys(data).length : 0
};
console.log('[BotFlag][DBG] Team activity refresh result', summary);
return summary;
}
};
window.NTBotFlagDebug = debugApi;
try {
if (typeof unsafeWindow !== 'undefined') {
unsafeWindow.NTBotFlagDebug = debugApi;
}
} catch (e) {}
if (isDebugEnabled()) {
console.log('[BotFlag][DBG] Enabled. API: window.NTBotFlagDebug.status(), .inspectUser("username"), .clearRowState(), .clearTeamActivityCache("TAG"), .refreshTeamActivityNow("TAG"), .disable()');
}
}
// =============================
// 🔑 Token Management
// =============================
function clearLegacyStoredToken() {
try { GM_deleteValue('nt_token'); } catch { /* ignore */ }
}
function getToken() {
const token = String(localStorage.getItem("player_token") || "").trim();
clearLegacyStoredToken();
return token || null;
}
function getTokenAndRetry(callback) {
const token = getToken();
if (token) {
callback();
}
}
// =============================
// 🛠️ Helper: Set Icon on Element
// =============================
function findPreferredRaceStatusField(element) {
if (!element) return null;
const directMatches = [
element.closest('.profile-title'),
element.closest('.tsxs.tc-fuel')
].filter(Boolean);
if (directMatches.length > 0) {
return directMatches[0];
}
const containers = [
element.closest('.gridTable-cell'),
element.closest('.gridTable-row'),
element.closest('.raceResults-playerName'),
element.closest('.raceResults'),
element.closest('.race-results'),
element.closest('.modal--raceResults')
].filter(Boolean);
for (const container of containers) {
if (!container || container === element) continue;
const titleField = container.querySelector('.tsxs.tc-fuel');
if (titleField) return titleField;
const profileTitle = container.querySelector('.profile-title');
if (profileTitle) return profileTitle;
}
let node = element.parentElement;
while (node && node !== document.body) {
if (node.classList?.contains('profile-title')) {
return node;
}
if (node.classList?.contains('tc-fuel') && node.classList.contains('tsxs')) {
return node;
}
const titleField = node.querySelector('.tsxs.tc-fuel');
if (titleField) return titleField;
const profileTitle = node.querySelector('.profile-title');
if (profileTitle) return profileTitle;
node = node.parentElement;
}
return null;
}
function getStatusField(element) {
if (!element) return null;
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 if (element.classList.contains('player-name--container')) {
statusField = findPreferredRaceStatusField(element) || element;
} else if (element.classList.contains('raceResults-playerName')) {
statusField = findPreferredRaceStatusField(element) || element;
} else if (element.classList.contains('tc-fuel')) {
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;
}
}
}
return statusField;
}
function clearStatusIcons(element) {
const statusField = getStatusField(element);
if (!statusField) return;
statusField.querySelectorAll('.status-icon').forEach((icon) => icon.remove());
}
let botFlagRefreshTimer = null;
let botFlagViewRefreshTimers = new Set();
let botFlagDelegatedViewListenersInstalled = false;
let racelogModalMonitorTimer = null;
let racePagePollerTimer = null;
let racePagePollerStopTimer = null;
let nonRacePagePollerTimer = null;
let nonRacePagePollerStopTimer = null;
let lastRacelogResultsModalKey = '';
function clearRenderedBotFlags() {
document.querySelectorAll('.status-icon').forEach((icon) => icon.remove());
document.querySelectorAll('[data-status-processing],[data-status-processing-for],[data-status-processed-for],[data-nt-username],[data-botflag-processed]').forEach((el) => {
el.removeAttribute('data-status-processing');
el.removeAttribute('data-status-processing-for');
el.removeAttribute('data-status-processed-for');
el.removeAttribute('data-nt-username');
el.removeAttribute('data-botflag-processed');
});
if (customTooltipElement && typeof customTooltipElement.remove === 'function') {
customTooltipElement.remove();
customTooltipElement = null;
}
}
function clearTrackedBotFlagViewRefreshTimers() {
botFlagViewRefreshTimers.forEach((timerId) => {
clearTimeout(timerId);
});
botFlagViewRefreshTimers.clear();
}
function rerunCurrentPageBotFlags() {
runBotFlagHandlersForCurrentRoute({ clearExisting: true, requireDomReady: false });
}
function scheduleBotFlagRefresh() {
if (botFlagRefreshTimer) {
clearTimeout(botFlagRefreshTimer);
}
botFlagRefreshTimer = setTimeout(() => {
botFlagRefreshTimer = null;
rerunCurrentPageBotFlags();
}, 50);
}
function scheduleBotFlagViewRefresh(path = window.location.pathname, delays = BOT_FLAG_VIEW_REFRESH_DELAYS) {
clearTrackedBotFlagViewRefreshTimers();
delays.forEach((delay) => {
const timerId = setTimeout(() => {
botFlagViewRefreshTimers.delete(timerId);
if (window.location.pathname !== path) return;
if (path === '/friends') {
lastFriendsPageRunAt = 0;
}
lastBotFlagViewSignature = '';
runBotFlagHandlersForCurrentRoute({ clearExisting: true, requireDomReady: false });
}, delay);
botFlagViewRefreshTimers.add(timerId);
});
}
function installDelegatedBotFlagViewListeners() {
if (botFlagDelegatedViewListenersInstalled) return;
botFlagDelegatedViewListenersInstalled = true;
const handleViewEvent = (event) => {
const target = event.target;
if (!(target instanceof Element)) return;
if (window.location.pathname === '/friends') {
if (target.closest('.tab') || target.closest('input[name="onlineOnly"]')) {
scheduleBotFlagViewRefresh('/friends');
}
return;
}
if (window.location.pathname === '/leagues') {
if (target.closest('input[type="radio"][name="showteam"]') || target.closest('label[for="showteam"]') || target.closest('label[for="showindividual"]')) {
scheduleBotFlagViewRefresh('/leagues');
}
}
};
document.addEventListener('click', handleViewEvent, true);
document.addEventListener('change', handleViewEvent, true);
}
function applyBotFlagSettingSideEffects(settingKey) {
if (!settingKey || !BOT_FLAG_LIVE_REFRESH_KEYS.has(settingKey)) return;
scheduleBotFlagRefresh();
}
function isRaceRoute(path = window.location.pathname) {
return path === '/race' || path.startsWith('/race/');
}
function isRacelogResultsRoute(path = window.location.pathname) {
return path.startsWith('/racelog') || path.startsWith('/stats');
}
function isNonRaceBotFlagRoute(path = window.location.pathname) {
return path.startsWith('/team') || path === '/friends' || path === '/leagues';
}
function getRelevantNonRaceBotFlagRows(path = window.location.pathname) {
if (path.startsWith('/team')) {
const teamTable = document.querySelector('.table.table--striped.table--selectable.table--team.table--teamOverview');
if (!teamTable) return [];
return Array.from(teamTable.querySelectorAll('.table-row')).filter((row) => !!row.querySelector('.player-name--container'));
}
if (path === '/friends') {
const friendsTable = document.querySelector('.table.table--selectable.table--striped.table--friends');
if (!friendsTable) return [];
return Array.from(friendsTable.querySelectorAll('.table-row')).filter((row) => !!row.querySelector('.player-name--container'));
}
if (path === '/leagues') {
return Array.from(document.querySelectorAll('.table-row')).filter((row) => !!row.querySelector('.player-name--container'));
}
return [];
}
function isNonRaceBotFlagDomReady(path = window.location.pathname) {
return getRelevantNonRaceBotFlagRows(path).length > 0;
}
function hasPendingNonRaceBotFlagRows(path = window.location.pathname) {
return getRelevantNonRaceBotFlagRows(path).some((row) => {
const statusField = getStatusField(row);
const alreadyTouched = row.hasAttribute('data-status-processing-for') || row.hasAttribute('data-status-processed-for');
const hasIcon = !!statusField?.querySelector('.status-icon');
return !alreadyTouched && !hasIcon;
});
}
function stopRacePagePollingFallback() {
if (racePagePollerTimer) {
clearInterval(racePagePollerTimer);
racePagePollerTimer = null;
}
if (racePagePollerStopTimer) {
clearTimeout(racePagePollerStopTimer);
racePagePollerStopTimer = null;
}
}
function stopNonRacePagePollingFallback() {
if (nonRacePagePollerTimer) {
clearInterval(nonRacePagePollerTimer);
nonRacePagePollerTimer = null;
}
if (nonRacePagePollerStopTimer) {
clearTimeout(nonRacePagePollerStopTimer);
nonRacePagePollerStopTimer = null;
}
}
function stopRacelogResultsModalMonitor() {
if (racelogModalMonitorTimer) {
clearInterval(racelogModalMonitorTimer);
racelogModalMonitorTimer = null;
}
lastRacelogResultsModalKey = '';
}
function syncNonRacePagePollingFallback(path = window.location.pathname) {
const normalizedPath = path.replace(/\/+$/, '') || '/';
const routeEnabled = (normalizedPath.startsWith('/team') && isNtcfgBotFlagFeatureEnabled('SHOW_TEAM_PAGE_FLAGS'))
|| (normalizedPath === '/friends' && isNtcfgBotFlagFeatureEnabled('SHOW_FRIENDS_PAGE_FLAGS'))
|| (normalizedPath === '/leagues' && isNtcfgBotFlagFeatureEnabled('SHOW_LEAGUE_PAGE_FLAGS'));
if (!isNonRaceBotFlagRoute(normalizedPath) || !routeEnabled) {
stopNonRacePagePollingFallback();
return;
}
if (nonRacePagePollerTimer) return;
nonRacePagePollerTimer = setInterval(() => {
try {
const currentPath = window.location.pathname.replace(/\/+$/, '') || '/';
if (currentPath !== normalizedPath) {
stopNonRacePagePollingFallback();
return;
}
if (!isNonRaceBotFlagDomReady(currentPath)) return;
runBotFlagHandlersForCurrentRoute({ requireDomReady: false });
if (!hasPendingNonRaceBotFlagRows(currentPath)) {
stopNonRacePagePollingFallback();
}
} catch (e) { /* ignore */ }
}, 500);
nonRacePagePollerStopTimer = setTimeout(() => {
stopNonRacePagePollingFallback();
}, 20000);
}
function syncBotFlagRouteFallbacks() {
const racePath = window.location.pathname.replace(/\/+$/, '') || '/';
const isTopRaceShell = isRaceRoute(racePath)
&& window.top === window
&& !document.getElementById('raceContainer');
if (isRaceRoute(racePath) && !isTopRaceShell) {
if (!racePagePollerTimer) {
racePagePollerTimer = setInterval(() => {
try {
if (!isNtcfgBotFlagFeatureEnabled('SHOW_RACE_RESULT_FLAGS')) return;
if (document.querySelector('.race-results')) {
handleRacePage();
}
} catch (e) { /* ignore */ }
}, 500);
racePagePollerStopTimer = setTimeout(() => {
stopRacePagePollingFallback();
}, 600000);
}
} else {
stopRacePagePollingFallback();
}
if (!isRacelogResultsRoute(window.location.pathname) || !isNtcfgBotFlagFeatureEnabled('SHOW_RACE_RESULT_FLAGS')) {
stopRacelogResultsModalMonitor();
}
syncNonRacePagePollingFallback(racePath);
}
function buildBotFlagViewSignature(path = window.location.pathname) {
const normalizedPath = path.replace(/\/+$/, '') || '/';
if (normalizedPath === '/friends') {
const activeTab = getActiveTab() || '';
const filterInput = document.querySelector('input[name="onlineOnly"]:checked');
const filterValue = filterInput ? `${filterInput.name}:${filterInput.value}` : '';
const names = getRowsForTab()
.map((row) => {
const nameContainer = row.querySelector('.player-name--container');
return normalizeBotFlagViewSignatureText(nameContainer?.textContent || '');
});
return ['friends', normalizeBotFlagViewSignatureText(activeTab), filterValue, buildBotFlagViewSignatureSample(names)].join('::');
}
if (normalizedPath === '/leagues') {
const checkedRadio = document.querySelector('input[name="showteam"]:checked');
const tabValue = normalizeBotFlagViewSignatureText(checkedRadio?.value || checkedRadio?.id || '');
if (tabValue === 'team' || tabValue.includes('team')) {
const teamTags = Array.from(document.querySelectorAll('td.table-cell.leagues--standings--team'))
.map((cell) => {
const match = String(cell.textContent || '').match(/\[([^\]]+)\]/);
return normalizeTeamTag(match ? match[1] : '') || normalizeBotFlagViewSignatureText(cell.textContent || '');
});
return ['leagues', 'team', buildBotFlagViewSignatureSample(teamTags)].join('::');
}
const names = getRowsForTab()
.map((row) => {
const nameContainer = row.querySelector('.player-name--container');
return normalizeBotFlagViewSignatureText(nameContainer?.textContent || '');
});
return ['leagues', 'personal', buildBotFlagViewSignatureSample(names)].join('::');
}
if (normalizedPath.startsWith('/team')) {
const names = getRelevantNonRaceBotFlagRows(normalizedPath)
.map((row) => {
const nameContainer = row.querySelector('.player-name--container');
return normalizeBotFlagViewSignatureText(nameContainer?.textContent || '');
});
return ['team', normalizedPath, buildBotFlagViewSignatureSample(names)].join('::');
}
return normalizedPath;
}
function runBotFlagHandlersForCurrentRoute(options = {}) {
let {
clearExisting = false,
requireDomReady = true
} = options;
const path = window.location.pathname;
const currentViewSignature = buildBotFlagViewSignature(path);
if (currentViewSignature && currentViewSignature !== lastBotFlagViewSignature) {
clearExisting = true;
if (path === '/friends') {
lastFriendsPageRunAt = 0;
}
}
lastBotFlagViewSignature = currentViewSignature || path;
if (clearExisting) {
clearRenderedBotFlags();
}
if (path.startsWith("/team")) {
if (!isNtcfgBotFlagFeatureEnabled('SHOW_TEAM_PAGE_FLAGS')) return;
if (!requireDomReady || document.querySelector('.table-row')) {
handleTeamPage();
}
return;
}
if (path.startsWith("/racer")) {
if (!requireDomReady || document.querySelector('.profile-title')) {
handleRacerPage();
}
return;
}
if (path === "/leagues") {
if (!isNtcfgBotFlagFeatureEnabled('SHOW_LEAGUE_PAGE_FLAGS')) return;
if (!requireDomReady || document.querySelector('.table-row')) {
handleLeaguesPage();
}
return;
}
if (path === "/friends") {
if (!isNtcfgBotFlagFeatureEnabled('SHOW_FRIENDS_PAGE_FLAGS')) return;
if (!requireDomReady || document.querySelector('.tab')) {
handleFriendsPage();
}
return;
}
if (isRaceRoute(path)) {
if (!isNtcfgBotFlagFeatureEnabled('SHOW_RACE_RESULT_FLAGS')) return;
if (!requireDomReady || document.querySelector('.race-results')) {
handleRacePage();
}
return;
}
if (isRacelogResultsRoute(path)) {
if (!isNtcfgBotFlagFeatureEnabled('SHOW_RACE_RESULT_FLAGS')) return;
lastRacelogResultsModalKey = '';
if (!requireDomReady || document.querySelector('.modal--raceResults.is-active')) {
void handleRacelogResultsModalPage();
}
ensureRacelogResultsModalMonitor();
}
}
function shouldProcessStatusFor(element, username) {
if (!element) return false;
const normalized = normalizeUsername(username);
if (!normalized) return false;
const processingFor = normalizeUsername(element.getAttribute('data-status-processing-for'));
if (processingFor === normalized) return false;
const processedFor = normalizeUsername(element.getAttribute('data-status-processed-for'));
if (processedFor !== normalized) return true;
const statusField = getStatusField(element);
if (!statusField) return true;
return !statusField.querySelector('.status-icon-source-st');
}
function markStatusProcessing(element, username) {
const normalized = normalizeUsername(username);
const processedFor = normalizeUsername(element.getAttribute('data-status-processed-for'));
if (processedFor && processedFor !== normalized) {
clearStatusIcons(element);
}
element.setAttribute('data-status-processing', 'true');
element.setAttribute('data-status-processing-for', normalized);
element.setAttribute('data-nt-username', normalized);
}
function markStatusProcessed(element, username) {
const normalized = normalizeUsername(username);
const processingFor = normalizeUsername(element.getAttribute('data-status-processing-for'));
if (processingFor && processingFor !== normalized) return;
element.removeAttribute('data-status-processing');
element.removeAttribute('data-status-processing-for');
element.setAttribute('data-status-processed-for', normalized);
}
// SVG icon builders for each status type
function getFlagSvg(fillColor) {
return `<svg viewBox="0 0 24 24" width="14" height="14" fill="${fillColor}" stroke="none" style="vertical-align:-2px;"><path d="M4 2v20h2v-8h4l1 2h8V4h-8l-1 2H6V2z"/></svg>`;
}
function getRobotSvg(fillColor) {
return `<svg viewBox="0 0 24 24" width="1em" height="1em" fill="${fillColor}" stroke="none" style="vertical-align:-0.15em;"><path d="M12 2a1 1 0 0 1 1 1v2h3a3 3 0 0 1 3 3v2h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-1v2a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3v-2H4a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8a3 3 0 0 1 3-3h3V3a1 1 0 0 1 1-1zM9.5 10a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm5 0a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM9 15a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2H9z"/></svg>`;
}
function getStatusSvg(color, source) {
const fillColor = colorMap[color] || '#a6aac1';
// Use robot icon for red StarTrack flags when the setting is enabled
if (color === 'red' && source === 'ST' && readNtcfgBotFlagValue('USE_ROBOT_ICON') === true) {
return getRobotSvg(fillColor);
}
return getFlagSvg(fillColor);
}
function setIcon(element, color, tooltip, source) {
// Respect visibility toggles
if (color === 'green' && readNtcfgBotFlagValue('SHOW_NOT_FLAGGED') === false) return;
if (color === 'yellow' && readNtcfgBotFlagValue('SHOW_UNTRACKED') === false) return;
const statusField = getStatusField(element);
if (!statusField) return;
const sourceClass = source === "NTL" ? 'status-icon-source-ntl' : 'status-icon-source-st';
const existingIcons = statusField.querySelectorAll(`.${sourceClass}`);
existingIcons.forEach(icon => icon.remove());
const iconSpan = document.createElement('span');
iconSpan.classList.add('status-icon', `status-icon-${color}`, sourceClass);
iconSpan.style.marginLeft = "5px";
iconSpan.style.display = "inline-flex";
iconSpan.style.alignItems = "center";
iconSpan.style.gap = "3px";
iconSpan.style.fontSize = "12px";
iconSpan.style.fontWeight = "600";
iconSpan.style.whiteSpace = "nowrap";
iconSpan.style.cursor = "help";
const label = source === "NTL" ? "NTL" : "ST";
const labelColor = colorMap[color] || color;
const useRobot = color === 'red' && source === 'ST' && readNtcfgBotFlagValue('USE_ROBOT_ICON') === true;
// Robot icon: render inline like text, inherit surrounding font size
if (useRobot) {
iconSpan.style.display = "inline";
iconSpan.style.fontSize = "inherit";
iconSpan.innerHTML = getRobotSvg(labelColor);
} else {
iconSpan.innerHTML = `<span style="color: ${labelColor};">${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 getStarTrackStatusFromResponse(response) {
try {
const data = JSON.parse(response.responseText || '{}');
if (data.is_bot === true) return { color: "red", tooltip: "StarTrack: Flagged" };
if (data.is_bot === false) return { color: "green", tooltip: "StarTrack: Not Flagged" };
if (data.error || response.status === 404) return { color: "yellow", tooltip: "StarTrack: Untracked" };
return { color: "gray", tooltip: "StarTrack: Unexpected Response" };
} catch (err) {
if (response.status === 404) return { color: "yellow", tooltip: "StarTrack: Untracked" };
return { color: "gray", tooltip: "StarTrack: Error" };
}
}
function fetchStarTrackStatusData(username) {
const normalized = normalizeUsername(username);
if (!normalized) {
debugLog('StarTrack skipped (invalid username)', { username: username });
return Promise.resolve(null);
}
const sharedStatus = window.NTShared.getBotStatus(normalized);
if (sharedStatus) {
debugLog('StarTrack cache hit (NTShared)', { username: normalized, status: sharedStatus });
return Promise.resolve(sharedStatus);
}
const cacheKey = `startrack_${normalized}`;
const cached = GM_getValue(cacheKey);
if (cached && (Date.now() - cached.timestamp) < getStarTrackCacheDuration()) {
debugLog('StarTrack cache hit (GM)', { username: normalized });
return Promise.resolve({ color: cached.color, tooltip: cached.tooltip });
}
if (startrackInflight.has(normalized)) {
debugLog('StarTrack dedupe hit (inflight)', { username: normalized });
return startrackInflight.get(normalized);
}
const requestPromise = queueNetworkRequest(() => new Promise((resolve) => {
const url = `http://ntstartrack.org:5001/api/isbot/${encodeURIComponent(normalized)}`;
debugLog('StarTrack network start', { username: normalized, url: url });
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: function(response) {
const status = getStarTrackStatusFromResponse(response);
debugLog('StarTrack network success', {
username: normalized,
httpStatus: response.status,
status: status
});
if (status && status.color !== "gray") {
GM_setValue(cacheKey, { color: status.color, tooltip: status.tooltip, timestamp: Date.now() });
window.NTShared.setBotStatus(normalized, status);
}
resolve(status);
},
onerror: function() {
debugLog('StarTrack network error', { username: normalized, url: url });
resolve({ color: "gray", tooltip: "StarTrack: Network Error" });
}
});
}), { label: 'startrack', username: normalized }).finally(() => {
startrackInflight.delete(normalized);
debugLog('StarTrack inflight cleared', { username: normalized, remainingInflight: startrackInflight.size });
});
startrackInflight.set(normalized, requestPromise);
return requestPromise;
}
function fetchStarTrackStatus(username, element) {
return fetchStarTrackStatusData(username).then((status) => {
if (status && status.color && status.tooltip) {
setIcon(element, status.color, status.tooltip, "ST");
}
});
}
// =============================
// 🟠 Fetch NTL Legacy Status
// =============================
function fetchNTLStatusData(username) {
const normalized = normalizeUsername(username);
if (!normalized) {
debugLog('NTL skipped (invalid username)', { username: username });
return Promise.resolve(null);
}
const cacheKey = `ntl_${normalized}`;
const cached = GM_getValue(cacheKey);
if (cached && cached.color && cached.tooltip) {
debugLog('NTL cache hit (GM)', { username: normalized });
return Promise.resolve({ color: cached.color, tooltip: cached.tooltip });
}
if (ntlInflight.has(normalized)) {
debugLog('NTL dedupe hit (inflight)', { username: normalized });
return ntlInflight.get(normalized);
}
const requestPromise = queueNetworkRequest(() => new Promise((resolve) => {
const url = `https://ntleaderboards.com/is_user_banned/${encodeURIComponent(normalized)}`;
debugLog('NTL network start', { username: normalized, url: url });
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: function(response) {
if (response.status !== 200) {
debugLog('NTL network non-200', { username: normalized, httpStatus: response.status });
resolve(null);
return;
}
const data = response.responseText.trim();
let status = null;
if (data === "Y (ban)") {
status = { color: "orange", tooltip: "Legacy NTL (Banned)" };
} else if (data === "Y (ban+flag)") {
status = { color: "orange", tooltip: "Legacy NTL (Flagged)" };
}
if (status) {
GM_setValue(cacheKey, status); // Intentionally indefinite cache
}
debugLog('NTL network success', {
username: normalized,
httpStatus: response.status,
status: status
});
resolve(status);
},
onerror: function() {
debugLog('NTL network error', { username: normalized, url: url });
resolve(null);
}
});
}), { label: 'ntl', username: normalized }).finally(() => {
ntlInflight.delete(normalized);
debugLog('NTL inflight cleared', { username: normalized, remainingInflight: ntlInflight.size });
});
ntlInflight.set(normalized, requestPromise);
return requestPromise;
}
function fetchNTLStatus(username, element) {
return fetchNTLStatusData(username).then((status) => {
if (status && status.color && status.tooltip) {
setIcon(element, status.color, status.tooltip, "NTL");
}
});
}
// =============================
// 🎯 Update Status (Dual Check)
// =============================
async function updateStatus(element, username) {
const normalized = normalizeUsername(username);
if (!normalized || !element) {
debugLog('Update skipped (missing element/username)', { username: username });
return;
}
if (!shouldProcessStatusFor(element, normalized)) {
debugLog('Update skipped (already processed)', { username: normalized });
return;
}
markStatusProcessing(element, normalized);
debugLog('Update start', { username: normalized });
try {
const checks = [];
if (isNtcfgBotFlagFeatureEnabled('USE_STARTRACK')) {
checks.push(fetchStarTrackStatus(normalized, element));
}
if (isNtcfgBotFlagFeatureEnabled('USE_NTL_LEGACY')) {
checks.push(fetchNTLStatus(normalized, element));
}
await Promise.all(checks);
} finally {
markStatusProcessed(element, normalized);
debugLog('Update complete', { username: normalized });
}
}
function registerSharedBotStatusListener() {
window.addEventListener('nt-cache-updated', (event) => {
const detail = event && event.detail ? event.detail : null;
if (!detail || detail.type !== 'isbot' || !detail.username || !detail.data) return;
const status = detail.data;
if (!status.color || !status.tooltip) return;
const username = String(detail.username).toLowerCase();
const taggedElements = document.querySelectorAll('[data-nt-username]');
let updatedCount = 0;
taggedElements.forEach((element) => {
const taggedUsername = (element.getAttribute('data-nt-username') || '').toLowerCase();
if (taggedUsername === username) {
setIcon(element, status.color, status.tooltip, "ST");
updatedCount += 1;
}
});
if (updatedCount > 0) {
debugLog('Shared ST update applied', { username: username, updatedElements: updatedCount });
}
});
}
// =============================
// 🔍 Observe Main Content
// =============================
function observeMainContent() {
window.NTObserverManager.register('botflag', () => {
syncBotFlagRouteFallbacks();
runBotFlagHandlersForCurrentRoute({ requireDomReady: true });
});
syncBotFlagRouteFallbacks();
}
function extractTeamTagFromNameContainer(nameContainer) {
if (!nameContainer) return '';
const teamLink = nameContainer.querySelector('a[href*="/team/"]');
const href = teamLink?.getAttribute('href') || '';
const match = href.match(/\/team\/([^\/?#]+)/i);
return match ? String(match[1]).trim().toUpperCase() : '';
}
function isNativeBotRaceResultRow(nameContainer) {
const row = nameContainer?.closest?.('.gridTable-row, .raceResults-body.row, .race-results .row');
if (!row) return false;
const statusField = row.querySelector('.tsxs.tc-fuel');
const statusText = normalizeDisplayName(statusField?.textContent || '');
if (/nitro type bot/i.test(statusText)) {
return true;
}
const taggedCandidates = [nameContainer, statusField].filter(Boolean);
for (let i = 0; i < taggedCandidates.length; i++) {
const candidate = taggedCandidates[i];
const username = normalizeUsername(
candidate.getAttribute?.('data-nt-username')
|| candidate.getAttribute?.('data-username')
|| candidate.getAttribute?.('data-status-processed-for')
|| candidate.dataset?.ntUsername
|| candidate.dataset?.username
|| candidate.dataset?.statusProcessedFor
);
if (username === 'bot' || /^robot\d/i.test(username)) {
return true;
}
}
return false;
}
function clearRaceResultStatusState(element) {
if (!element) return;
clearStatusIcons(element);
element.removeAttribute('data-status-processing');
element.removeAttribute('data-status-processing-for');
element.removeAttribute('data-status-processed-for');
element.removeAttribute('data-nt-username');
element.removeAttribute('data-botflag-processed');
}
function fetchTeamMembersByTag(teamTagRaw) {
const normalizedTag = String(teamTagRaw || '').trim().toUpperCase();
if (!normalizedTag) return Promise.resolve(null);
const cacheKey = `team_activity_v2_${normalizedTag}`;
const cached = GM_getValue(cacheKey);
const TEAM_CACHE_DURATION = 300000;
if (cached && isIdentityLookup(cached.data) && (Date.now() - cached.timestamp) < TEAM_CACHE_DURATION) {
return Promise.resolve(cached.data);
}
const token = getToken();
if (!token) return Promise.resolve(null);
return new Promise((resolve) => {
const teamUrl = `https://www.nitrotype.com/api/v2/teams/${encodeURIComponent(normalizedTag)}`;
debugLog('Team activity request start', {
teamApi: normalizedTag,
url: teamUrl,
cacheKey: cacheKey
});
GM_xmlhttpRequest({
method: "GET",
url: teamUrl,
headers: {
"Authorization": `Bearer ${token}`,
"accept": "application/json, text/plain, */*"
},
onload: function(response) {
debugLog('Team activity request complete', {
teamApi: normalizedTag,
httpStatus: response.status
});
if (response.status === 200) {
let data;
try {
data = JSON.parse(response.responseText);
} catch (err) {
debugLog('Team activity parse error', { teamApi: normalizedTag });
resolve(null);
return;
}
if (data.status === "OK") {
const members = Array.isArray(data?.results?.members) ? data.results.members : [];
const memberLookup = createIdentityLookup(
members.map((member) => ({
displayName: member.displayName || member.username,
username: member.username,
tag: normalizedTag
}))
);
GM_setValue(cacheKey, {
data: memberLookup,
timestamp: Date.now()
});
resolve(memberLookup);
return;
}
} else if (response.status === 401) {
getTokenAndRetry(() => fetchTeamMembersByTag(normalizedTag).then(resolve));
return;
}
resolve(null);
},
onerror: () => resolve(null)
});
});
}
// =============================
// 👥 /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 pathParts = window.location.pathname.split('/').filter(Boolean);
const teamTagFromUrl = pathParts.length >= 2 ? pathParts[pathParts.length - 1] : '';
const teamTag = String(teamTagFromUrl || '').trim();
if (!teamTag) {
debugLog('Team activity skipped (missing team tag in URL)', { pathname: window.location.pathname });
return null;
}
return fetchTeamMembersByTag(teamTag);
} catch (error) {
return null;
}
}
async function fetchTeamApplications() {
try {
// Check cache first (5 minute cache)
const cacheKey = 'team_applications_v2';
const cached = GM_getValue(cacheKey);
const APPLICATIONS_CACHE_DURATION = 300000; // 5 minutes
if (cached && isIdentityLookup(cached.data) && (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) {
let data;
try {
data = JSON.parse(response.responseText);
} catch (err) {
resolve(null);
return;
}
const results = Array.isArray(data?.results) ? data.results : [];
if (results.length > 0) {
const memberLookup = createIdentityLookup(
results.map((member) => ({
displayName: member.displayName || member.username,
username: member.username,
tag: member.tag || member.teamTag || member.team
}))
);
// Cache the result
GM_setValue(cacheKey, {
data: memberLookup,
timestamp: Date.now()
});
resolve(memberLookup);
} 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(userLookup) {
// Collect all promises to wait for batch completion
const promises = [];
const teamTable = document.querySelector('.table.table--striped.table--selectable.table--team.table--teamOverview');
if (!teamTable) return;
teamTable.querySelectorAll('.table-row').forEach((row) => {
const username = resolveUsernameForElement(row, userLookup, 'team');
if (username) {
const promise = updateStatus(row, username);
if (promise) promises.push(promise);
}
});
// Wait for all icon additions to complete before flushing queue
await Promise.all(promises);
}
async function updateUsersTeamApplications(userLookup) {
const promises = [];
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;
appSection.querySelectorAll('.table-row').forEach((row) => {
const username = resolveUsernameForElement(row, userLookup, 'team-applications');
if (username) {
const promise = updateStatus(row, username);
if (promise) promises.push(promise);
}
});
await Promise.all(promises);
}
// =============================
// 🏁 /racer Page Handling
// =============================
function handleRacerPage() {
const username = window.location.pathname.split('/').pop();
const el = document.querySelector('.profile-title');
if (username && el) updateStatus(el, username);
}
// =============================
// 🏎️ /race Results Handling
// =============================
const findReact = (dom) => {
if (!dom) return null;
const key = Object.keys(dom).find((k) => k.startsWith('__reactFiber$'));
const domFiber = dom[key];
if (!domFiber) return null;
let parentFiber = domFiber?.return;
while (typeof parentFiber?.type === 'string') {
parentFiber = parentFiber?.return;
}
return parentFiber?.stateNode;
};
function handleRacePage() {
const raceContainer = document.getElementById('raceContainer');
if (!raceContainer) return;
const results = raceContainer.querySelector('.race-results');
if (!results) return;
// Get racers from React state for username lookup
const raceObj = findReact(raceContainer);
const racers = raceObj?.state?.racers;
if (!racers) return;
const racerLookup = createIdentityLookup(
racers.map((r) => ({
displayName: r.profile?.displayName || r.profile?.username,
username: r.profile?.username,
tag: r.profile?.tag || r.profile?.team || r.profile?.teamTag || r.tag || r.team
}))
);
const botUsernames = Object.create(null);
racers.forEach(r => {
const username = normalizeUsername(r.profile?.username || '');
// Skip NT native bots (username "bot", starts with "robot", or title is "Nitro Type Bot")
const isBot = username === 'bot' || /^robot\d/i.test(username) || r.profile?.title === 'Nitro Type Bot';
if (username && isBot) botUsernames[username] = true;
});
const resolveRaceTarget = (nameContainer) => {
const preferred = findPreferredRaceStatusField(nameContainer);
if (preferred) {
return preferred;
}
const resultCell = nameContainer.closest('.gridTable-cell');
if (resultCell && results.contains(resultCell)) {
return resultCell.querySelector('.tsxs.tc-fuel')
|| resultCell.querySelector('.raceResults-playerName')
|| nameContainer;
}
let node = nameContainer;
while (node && node !== document.body) {
if (node.classList?.contains('profile-title')) {
return node;
}
if (node !== nameContainer) {
const profileTitle = node.querySelector('.profile-title');
if (profileTitle) return profileTitle;
const titleField = node.querySelector('.tsxs.tc-fuel');
if (titleField) return titleField;
}
node = node.parentElement;
}
return nameContainer;
};
const playerNameContainers = document.querySelectorAll('.player-name--container[title]');
playerNameContainers.forEach(nameContainer => {
const targetEl = resolveRaceTarget(nameContainer) || nameContainer;
if (isNativeBotRaceResultRow(nameContainer)) {
clearRaceResultStatusState(targetEl);
return;
}
const username = resolveUsernameForElement(nameContainer, racerLookup, 'race-results');
if (!username || botUsernames[username]) return;
if (!targetEl) return;
if (targetEl.hasAttribute('data-botflag-processed')) return;
targetEl.setAttribute('data-botflag-processed', '');
targetEl.setAttribute('data-nt-username', username);
updateStatus(targetEl, username);
});
}
async function handleRacelogResultsModalPage() {
const modal = document.querySelector('.modal--raceResults.is-active');
if (!modal) return;
const playerNameContainers = Array.from(modal.querySelectorAll('.player-name--container[title]'));
if (playerNameContainers.length === 0) return;
const pendingNameContainers = playerNameContainers.filter((nameContainer) => {
const targetEl = findPreferredRaceStatusField(nameContainer) || nameContainer;
return !targetEl
|| !targetEl.hasAttribute('data-botflag-processed')
|| !normalizeUsername(targetEl.getAttribute('data-nt-username') || '');
});
if (pendingNameContainers.length === 0) return;
const teamTags = Array.from(new Set(
pendingNameContainers
.map(extractTeamTagFromNameContainer)
.filter(Boolean)
));
const teamMemberMaps = Object.create(null);
await Promise.all(teamTags.map(async (teamTag) => {
teamMemberMaps[teamTag] = await fetchTeamMembersByTag(teamTag);
}));
pendingNameContainers.forEach((nameContainer) => {
const teamTag = extractTeamTagFromNameContainer(nameContainer);
const resolvedUsername = extractExplicitUsernameFromElement(nameContainer)
|| ((teamTag && teamMemberMaps[teamTag])
? resolveUsernameFromLookup(teamMemberMaps[teamTag], {
displayName: getDisplayNameFromElement(nameContainer)
})
: '');
if (!resolvedUsername) return;
const targetEl = findPreferredRaceStatusField(nameContainer) || nameContainer;
if (!targetEl) return;
const normalizedResolved = normalizeUsername(resolvedUsername);
if (targetEl.hasAttribute('data-botflag-processed') && normalizeUsername(targetEl.getAttribute('data-nt-username')) === normalizedResolved) {
return;
}
targetEl.setAttribute('data-botflag-processed', '');
targetEl.setAttribute('data-nt-username', normalizedResolved);
updateStatus(targetEl, resolvedUsername);
});
}
function ensureRacelogResultsModalMonitor() {
if (racelogModalMonitorTimer) return;
const tick = () => {
const path = window.location.pathname;
if (!(path.startsWith('/racelog') || path.startsWith('/stats'))) {
stopRacelogResultsModalMonitor();
return;
}
if (!isNtcfgBotFlagFeatureEnabled('SHOW_RACE_RESULT_FLAGS')) {
stopRacelogResultsModalMonitor();
return;
}
const modal = document.querySelector('.modal--raceResults.is-active');
if (!modal) {
lastRacelogResultsModalKey = '';
return;
}
const modalKey = modal.querySelector('.raceResults')?.getAttribute('data-estats-racelog-modal') || 'active';
if (modalKey === lastRacelogResultsModalKey) {
return;
}
lastRacelogResultsModalKey = modalKey;
void handleRacelogResultsModalPage();
};
tick();
racelogModalMonitorTimer = setInterval(tick, 300);
}
// =============================
// 🏆 /leagues Page Handling
// =============================
async function handleLeaguesPage() {
// Check cache first (5 minute cache)
const cacheKey = 'leagues_user_activity_v2';
const cached = GM_getValue(cacheKey);
const LEAGUES_CACHE_DURATION = 300000; // 5 minutes
let userMap;
if (cached && isIdentityLookup(cached.data) && (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) {
let data;
try {
data = JSON.parse(response.responseText);
} catch (err) {
resolve(null);
return;
}
if (data.status === "OK") {
const standings = Array.isArray(data?.results?.standings) ? data.results.standings : [];
const lookup = createIdentityLookup(
standings.map((u) => ({
displayName: u.displayName || u.username,
username: u.username,
tag: u.tag || u.teamTag || u.team
}))
);
// Cache the result
GM_setValue(cacheKey, {
data: lookup,
timestamp: Date.now()
});
resolve(lookup);
} 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 username = resolveUsernameForElement(row, userMap, 'leagues');
if (username) {
promises.push(updateStatus(row, username));
}
});
// Wait for all icon additions to complete
await Promise.all(promises);
}
// =============================
// 👫 /friends Page Handling (v3.1 LOGIC)
// =============================
function handleFriendsPage() {
const now = Date.now();
if (now - lastFriendsPageRunAt < FRIENDS_PAGE_RUN_COOLDOWN_MS) {
debugLog('Friends handler skipped (cooldown)', {
cooldownMs: FRIENDS_PAGE_RUN_COOLDOWN_MS,
elapsedMs: now - lastFriendsPageRunAt
});
return;
}
lastFriendsPageRunAt = now;
const activeTab = getActiveTab();
if (!activeTab) return;
debugLog('Friends handler run', { activeTab: 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 requestLookup = createIdentityLookup(
requests.map((req) => ({
displayName: req.displayName || req.username,
username: req.username,
tag: req.tag || req.teamTag || req.team
}))
);
const rows = getRowsForTab();
rows.forEach(row => {
const username = resolveUsernameForElement(row, requestLookup, 'friend-requests');
if (username) {
updateStatus(row, username);
}
});
} 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 resultLookup = createIdentityLookup(
results.map((r) => ({
displayName: r.displayName || r.username,
username: r.username,
tag: r.tag || r.teamTag || r.team
}))
);
const rows = getRowsForTab();
rows.forEach(row => {
const username = resolveUsernameForElement(row, resultLookup, 'friend-search');
if (username) {
updateStatus(row, username);
}
});
} else if (response.status === 401) {
getTokenAndRetry(handleSearchTab);
}
}
});
}
function handleRecentTab() {
const rows = getRowsForTab();
rows.forEach(row => {
const username = extractRecentTabUsernameFromElement(row);
if (username) {
updateStatus(row, username);
}
});
}
// 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) {
let data;
try {
data = JSON.parse(response.responseText);
} catch (err) {
resolve(null);
return;
}
const fields = Array.isArray(data?.results?.fields) ? data.results.fields : [];
const values = Array.isArray(data?.results?.values) ? data.results.values : [];
const displayNameIndex = fields.indexOf("displayName");
const usernameIndex = fields.indexOf("username");
if (displayNameIndex < 0 || usernameIndex < 0) {
resolve(null);
return;
}
const friendsMap = createIdentityLookup(
values.map((friendData) => ({
displayName: friendData[displayNameIndex],
username: friendData[usernameIndex],
tag: friendData[fields.indexOf("tag")]
}))
);
friendsListCache = friendsMap;
friendsListTimestamp = Date.now();
resolve(friendsMap);
} else if (response.status === 401) {
getTokenAndRetry(() => getFriendsList().then(resolve));
} else {
resolve(null);
}
},
onerror: function() {
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) {
const username = resolveUsernameForElement(row, friendsMap, 'friends');
if (username) {
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
// =============================
function startBotFlagLiveRuntime() {
installDelegatedBotFlagViewListeners();
registerSharedBotStatusListener();
observeMainContent();
}
initDebugAPI();
debugLog('Script loaded', { version: SCRIPT_VERSION });
startBotFlagLiveRuntime();
})();