// ==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
// ==/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 (_) {}
})();