Show account names (all locales) + Force English

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.

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址