// ==UserScript== // @name Show account names (all locales) + Force English // @match https://*.esologs.com/* // @grant none // @version 1.9 // @author Xandaros (tweaked by Kwiebe-Kwibus) // @license BSD-2-Clause // @run-at document-start // @description Forces English, de-locale redirect, auto-presses Translate, then (after full load + 2s) replaces character names with account IDs using throttled, batched, text-node-only passes to avoid jamming the app. // @namespace io.inp // @downloadURL none // ==/UserScript== (function () { "use strict"; // ---------------- Config ---------------- const NAME_REPLACEMENT_DELAY_MS = 2000; // start heavy work 2s after full load const MAX_TEXT_NODES_PER_BATCH = 1200; // upper bound per initial pass const OBSERVER_DEBOUNCE_PER_FRAME = true; // coalesce repeated DOM mutations // ---------------------------------------- // Polyfills for idle/frame scheduling const raf = window.requestAnimationFrame.bind(window) || ((cb) => setTimeout(cb, 16)); const ric = window.requestIdleCallback || function (cb) { return setTimeout(() => cb({ didTimeout: false, timeRemaining: () => 50 }), 1); }; // 0) Redirect locale subdomain early (document-start) (function redirectLocaleSubdomain() { try { const host = location.hostname; // e.g., ru.esologs.com const m = host.match(/^([a-z]{2})\.esologs\.com$/i); // Redirect any 2-letter locale (ru, de, fr, etc.) EXCEPT "en" if (m && m[1].toLowerCase() !== "en") { const targetHost = "esologs.com"; const newUrl = location.protocol + "//" + targetHost + location.pathname + location.search + location.hash; location.replace(newUrl); return; } } catch (_) {} })(); // 1) Force site language to English (function ensureEnglish() { try { const desired = "en"; const hasEnCookie = (name) => document.cookie.split(";").some((c) => c.trim().startsWith(name + "=en")); const setCookie = (name, value, domain) => { const maxAge = 60 * 60 * 24 * 365; // 1 year const parts = [`${name}=${value}`, "path=/", `max-age=${maxAge}`, "samesite=lax", "secure"]; if (domain) parts.push(`domain=${domain}`); document.cookie = parts.join("; "); }; const needSet = !hasEnCookie("NEXT_LOCALE") || (typeof localStorage !== "undefined" && (localStorage.getItem("NEXT_LOCALE") !== desired || localStorage.getItem("locale") !== desired || localStorage.getItem("language") !== desired)); if (needSet) { setCookie("NEXT_LOCALE", desired, ".esologs.com"); setCookie("NEXT_LOCALE", desired, undefined); try { localStorage.setItem("NEXT_LOCALE", desired); localStorage.setItem("locale", desired); localStorage.setItem("language", desired); } catch (_) {} try { sessionStorage.setItem("NEXT_LOCALE", desired); } catch (_) {} if (!sessionStorage.getItem("esologs_forced_en_reloaded")) { sessionStorage.setItem("esologs_forced_en_reloaded", "1"); location.reload(); } } } catch (_) {} })(); // 2) Auto-press Translate/Show Original (lightweight, can run early) function autoPressTranslate() { function clickButton() { try { const btn = document.querySelector("input.translator-button"); if (btn && btn.value && /Show Original|Translate/i.test(btn.value)) { btn.click(); return true; } } catch (_) {} return false; } let tries = 0; const maxTries = 20; const timer = setInterval(() => { tries++; if (clickButton() || tries >= maxTries) clearInterval(timer); }, 500); const mo = new MutationObserver(() => clickButton()); const startObs = () => { if (document.body) { mo.observe(document.body, { childList: true, subtree: true }); } else { requestAnimationFrame(startObs); } }; startObs(); } // --- Helpers to discover player entries (friends + enemies) --- function looksLikeAccount(str) { return typeof str === "string" && /^@/.test(str); } function pickDisplayName(obj) { const candidates = [obj.displayName, obj.account, obj.userID, obj.owner, obj.id, obj.user].filter(Boolean); for (const v of candidates) { if (looksLikeAccount(v)) return v; } if (typeof obj.displayName === "string") return obj.displayName; return null; } function normalizeEntry(p) { if (!p || typeof p !== "object") return null; const name = p.name || p.characterName || p.charName || null; const displayName = pickDisplayName(p); const type = p.type || p.kind || p.entityType || null; const anonymous = !!p.anonymous; return { name, displayName, type, anonymous }; } function tryParseNextDataPlayers() { // Extra source: Next.js JSON payload (if present) try { const script = document.querySelector('script#__NEXT_DATA__'); if (!script?.textContent) return []; const data = JSON.parse(script.textContent); const buckets = ["players", "friendlyPlayers", "enemies", "enemyPlayers", "combatants", "participants", "units"]; const out = []; const crawl = (node) => { if (!node || typeof node !== "object") return; for (const k of Object.keys(node)) { const v = node[k]; if (Array.isArray(v) && buckets.includes(k)) { for (const raw of v) { const e = normalizeEntry(raw); if (e) out.push(e); } } else if (v && typeof v === "object") { crawl(v); } } }; crawl(data); return out; } catch (_) { return []; } } function getAllPlayerEntries() { const out = []; // Globals first const buckets = [ "players", "playerList", "friendlyPlayers", "enemies", "enemyPlayers", "opponents", "combatants", "participants", "units", ]; for (const key of buckets) { try { const arr = window[key]; if (Array.isArray(arr)) { for (const raw of arr) { const e = normalizeEntry(raw); if (e) out.push(e); } } } catch (_) {} } // Report-like nesting try { const maybeReport = window.report || window.currentReport || window.data; if (maybeReport) { const nestedKeys = ["players", "enemies", "enemyPlayers", "friendlyPlayers", "combatants", "participants", "units"]; for (const k of nestedKeys) { const arr = maybeReport[k]; if (Array.isArray(arr)) { for (const raw of arr) { const e = normalizeEntry(raw); if (e) out.push(e); } } } } } catch (_) {} // Next.js payload fallback try { out.push(...tryParseNextDataPlayers()); } catch (_) {} // Filter + dedupe const filtered = out.filter( (e) => e && e.name && e.displayName && !e.anonymous && String(e.type || "").toUpperCase() !== "NPC" ); const seen = new Set(); const unique = []; for (const e of filtered) { const k = e.name + "→" + e.displayName; if (!seen.has(k)) { seen.add(k); unique.push(e); } } return unique; } // Build replacers once per run function buildReplacers(players) { const out = []; for (const p of players) { try { if (!p.name || !p.displayName || p.name === p.displayName) continue; const esc = String(p.name).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const re = new RegExp(`(^|[^\\p{L}\\p{N}_])(${esc})(?=$|[^\\p{L}\\p{N}_])`, "gu"); out.push({ re, to: `$1${p.displayName}` }); } catch (_) {} } return out; } // Process text nodes only const processedNodes = new WeakSet(); function processTextNode(node, replacers) { try { if (!node || node.nodeType !== Node.TEXT_NODE) return; const parent = node.parentElement; if (!parent) return; const tag = parent.tagName; if (["SCRIPT", "STYLE", "TEXTAREA", "INPUT"].includes(tag)) return; const txt = node.textContent; if (!txt || processedNodes.has(node)) return; let out = txt; let changed = false; for (const { re, to } of replacers) { const newOut = out.replace(re, to); if (newOut !== out) { out = newOut; changed = true; } } if (changed) { node.textContent = out; processedNodes.add(node); } } catch (_) {} } function initialPass(root, replacers) { try { const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode: (n) => { if (!n || processedNodes.has(n)) return NodeFilter.FILTER_REJECT; const p = n.parentElement; if (!p) return NodeFilter.FILTER_REJECT; const tag = p.tagName; if (["SCRIPT", "STYLE", "TEXTAREA", "INPUT"].includes(tag)) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; }, }); let count = 0; const work = () => { ric(() => { let n; while ((n = walker.nextNode())) { processTextNode(n, replacers); count++; if (count >= MAX_TEXT_NODES_PER_BATCH) { // Yield and continue later to avoid blocking count = 0; raf(work); return; } } }); }; work(); } catch (_) {} } function observeMutations(root, replacers) { let scheduled = false; const queue = new Set(); const run = () => { scheduled = false; const nodes = Array.from(queue); queue.clear(); for (const n of nodes) { if (!n) continue; if (n.nodeType === Node.TEXT_NODE) { processTextNode(n, replacers); } else if (n.nodeType === Node.ELEMENT_NODE) { try { const walker = document.createTreeWalker(n, NodeFilter.SHOW_TEXT); let t; let processed = 0; while ((t = walker.nextNode())) { processTextNode(t, replacers); processed++; if (processed > 1500) break; // safety cap per mutation burst } } catch (_) {} } } }; const obs = new MutationObserver((mutations) => { try { for (const m of mutations) { if (m.type === "characterData") { queue.add(m.target); } else if (m.type === "childList") { m.addedNodes.forEach((n) => queue.add(n)); } } if (OBSERVER_DEBOUNCE_PER_FRAME) { if (!scheduled) { scheduled = true; raf(run); } } else { run(); } } catch (_) {} }); obs.observe(root, { childList: true, characterData: true, subtree: true }); return obs; } function startNameReplacement() { try { const players = getAllPlayerEntries(); if (!players.length) return false; const replacers = buildReplacers(players); if (!replacers.length) return false; const root = document.getElementById("__next") || document.querySelector("main") || document.body || document.documentElement; if (!root) return false; initialPass(root, replacers); observeMutations(root, replacers); return true; } catch (_) { return false; } } function bootAfterLoad() { // Start the lightweight thing immediately autoPressTranslate(); const kick = () => { setTimeout(() => { // Try a few times in case players data appears late let tries = 0; const maxTries = 8; const tryOnce = () => { if (startNameReplacement() || ++tries >= maxTries) return; setTimeout(tryOnce, 800); // back off a bit to let app settle and data hydrate }; tryOnce(); }, NAME_REPLACEMENT_DELAY_MS); }; if (document.readyState === "complete") { kick(); } else { window.addEventListener("load", kick, { once: true }); } } // Entry try { bootAfterLoad(); } catch (_) {} })();