Official TriX Proxy Bot Script

A multi-tab bot for territorial.io. Automatic reconnects are disabled; sub-tabs only act on command from the Main Tab.

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Official TriX Proxy Bot Script
// @version      0.5.4
// @description  A multi-tab bot for territorial.io. Automatic reconnects are disabled; sub-tabs only act on command from the Main Tab.
// @author       painsel
// @license      MIT
// @homepageURL  https://greasyfork.org/en/scripts/549132-trix-executor-beta-for-territorial-io
// @match        *://territorial.io/*
// @match        *://*/*?__cpo=*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        unsafeWindow
// @namespace    http://tampermonkey.net/
// ==/UserScript==

(async function() {
    'use strict';

    // --- 1. Early Helper Functions & Initial Routing ---
    function findMultiplayerButton(){for(const e of document.querySelectorAll("button"))if(e.innerText&&e.innerText.includes("Multiplayer"))return e;return null}
    function waitForGameUI(callback){if(findMultiplayerButton()){callback();return}const e=new MutationObserver((t,n)=>{findMultiplayerButton()&&(n.disconnect(),callback())});e.observe(document.documentElement,{childList:!0,subtree:!0})}
    function showProxyRequiredModal(){const modalCSS=`:root{--trix-bg:#1e1e1e;--trix-bg-light:#252526;--trix-blue-accent:#007acc;--trix-button-bg:#0e639c;--trix-button-hover-bg:#1177bb;--trix-border:#3c3c3c;--trix-text:#d4d4d4;}#trix-proxy-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.75);z-index:100000;display:flex;align-items:center;justify-content:center;font-family:'Consolas','Menlo','Courier New',monospace}#trix-proxy-modal{background-color:var(--trix-bg-light);color:var(--trix-text);padding:30px 40px;border-radius:8px;text-align:center;border:1px solid var(--trix-border);box-shadow:0 5px 25px rgba(0,0,0,.5)}#trix-proxy-modal h2{margin-top:0;color:var(--trix-blue-accent);font-size:24px}#trix-proxy-modal p{margin-bottom:25px;font-size:16px}#trix-proxy-button{background-color:var(--trix-button-bg);color:#fff;border:none;padding:12px 20px;border-radius:4px;font-family:inherit;font-size:16px;font-weight:700;cursor:pointer;text-decoration:none;transition:background-color .2s}#trix-proxy-button:hover{background-color:var(--trix-button-hover-bg)}`;GM_addStyle(modalCSS);const modalHTML=`<div id="trix-proxy-overlay"><div id="trix-proxy-modal"><h2>Proxy Required</h2><p>This script only works with CroxyProxy.</p><a href="https://www.croxyproxy.com/" target="_blank" id="trix-proxy-button">Go to CroxyProxy</a></div></div>`;document.body.insertAdjacentHTML("beforeend",modalHTML)}
    const isMainDomain=window.location.hostname==="territorial.io"&&!window.location.search.includes("__cpo="),isProxy=window.location.search.includes("__cpo=");if(isMainDomain){waitForGameUI(showProxyRequiredModal);return}if(isProxy){try{if(!atob(new URLSearchParams(window.location.search).get("__cpo")).includes("territorial.io"))return}catch(e){return}}else return;

    // --- 2. Bot Configuration ---
    const CONFIG = {
        STORAGE_KEY: 'd122_bot_settings', MAIN_TAB_KEY: 'trix_main_tab_global_v2', TABS_STATUS_KEY: 'trix_tabs_status_v2', COMMAND_QUEUE_KEY: 'trix_command_queue_v2',
        RELOAD_FLAG_KEY: 'trix_main_reloading_flag',
        HEARTBEAT_INTERVAL: 2500, MAIN_TAB_TIMEOUT: 7000, PROXY_TAB_TIMEOUT: 7000, MAIN_SOCKET_URL_PART: '/s52/',
        AI_NAMES: ["Apex","Vortex","Shadow","Nova","Cipher","Blaze","Reaper","Phantom","Genesis","Specter","Warden","Rogue","Titan","Fury","Serpent","Oracle","Zenith","Pulsar","Jester","Mirage","Nomad","Havoc","Crux","Wraith","Glitch","Vector","Flux","Byte","Pixel","Matrix","Kernel","Grid","Node","Syntax","Griffin","Wyvern","Goliath","Leviathan","Hydra","Phoenix","Golem","Paladin","Warlock","Rune","Storm","Quake","Inferno","Frost","Cyclone","Meteor","Comet","Abyss","Ember","Tempest","Striker","Raider","Vanguard","Sentinel","Commando","Juggernaut","Marshal","Legion","Phalanx","Blitz","Saber","Echo","Impulse","Catalyst","Paradox","Rift","Karma","Legacy","Valor","Creed","Requiem","Solstice","Equinox","Infinity","Axiom","Enigma","Viper","Cobra","Hawk","Falcon","Tiger","Wolf","Panther","Jackal","Goliath","Javelin","Overload","Torrent","Cascade","Helix","Ion","Ronin","Monarch","Zealot","Herald","Jinx","Quasar"]
    };
    let isMainTab = false;
    const tabId = Date.now() + Math.random().toString(36).substring(2);
    const tabOrigin = window.location.hostname;

    function simulateTyping(element,text,onComplete){let i=0;element.value="",element.focus();function typeCharacter(){i<text.length?(element.value+=text.charAt(i),element.dispatchEvent(new Event("input",{bubbles:!0})),i++,setTimeout(typeCharacter,50+150*Math.random())):(element.blur(),onComplete&&onComplete())}typeCharacter()}

    // --- 3. Main Tab Logic ---
    const mainTab = {
        uiInitialized: false,
        async init() {
            isMainTab = true; document.title = "[Main] TriX Bot"; sessionStorage.setItem('wasTrixMain', 'true');
            localStorage.removeItem(CONFIG.RELOAD_FLAG_KEY);
            setInterval(() => this.sendHeartbeat(), CONFIG.HEARTBEAT_INTERVAL);
            waitForGameUI(async () => { await this.initializeUI(); setInterval(() => this.updateAndCleanupTabs(), CONFIG.HEARTBEAT_INTERVAL); });
        },
        async sendHeartbeat() { await GM_setValue(CONFIG.MAIN_TAB_KEY, { id: tabId, timestamp: Date.now() }); },
        async updateAndCleanupTabs() {
            const now = Date.now(); const statuses = await GM_getValue(CONFIG.TABS_STATUS_KEY, {}); let changed = false;
            for (const id in statuses) { if (now - statuses[id].lastSeen > CONFIG.PROXY_TAB_TIMEOUT) { delete statuses[id]; changed = true; } }
            if (changed) { await GM_setValue(CONFIG.TABS_STATUS_KEY, statuses); }
            this.updateTabListUI(statuses);
        },
        updateTabListUI(statuses) {
            if (!this.uiInitialized) return;
            const list = document.getElementById('trix-tab-list'); const counter = document.getElementById('trix-tab-counter'); const statusEntries = Object.values(statuses);
            list.innerHTML = '';
            if (statusEntries.length === 0) { list.innerHTML = '<li class="trix-tab-item-empty">No other tabs found.</li>'; }
            else { statusEntries.forEach(tab => {
                const li = document.createElement('li'); li.className = `trix-tab-item status-${tab.status.toLowerCase()}`;
                let buttonHTML = '';
                if (tab.status === 'DISCONNECTED') { buttonHTML = `<button class="trix-reconnect-btn" data-target-id="${tab.id}">Reconnect</button>`; }
                li.innerHTML = `<span>${tab.origin}</span> <div class="status-and-btn"><span class="tab-status">${tab.status}</span> ${buttonHTML}</div>`;
                list.appendChild(li);
            });}
            counter.textContent = statusEntries.length;
        },
        showToast(title, message, duration = 3000) {
            const container = document.getElementById('trix-toast-container');
            const toast = document.createElement('div'); toast.className = 'trix-toast'; const startTime = performance.now();
            toast.innerHTML = `<div class="toast-header"><span>${title}</span><span class="toast-timer">${(duration / 1000).toFixed(1)}s</span></div><div class="toast-body">${message}</div><div class="toast-progress"></div>`;
            container.appendChild(toast);
            const timerEl = toast.querySelector('.toast-timer'); const progressEl = toast.querySelector('.toast-progress');
            const intervalId = setInterval(() => { const elapsed = performance.now() - startTime; const remaining = duration - elapsed; if (remaining <= 0) { clearInterval(intervalId); return; } timerEl.textContent = `${(remaining / 1000).toFixed(1)}s`; progressEl.style.width = `${(remaining / duration) * 100}%`; }, 100);
            const dismiss = () => { clearInterval(intervalId); clearTimeout(timeoutId); toast.classList.add('fade-out'); toast.addEventListener('animationend', () => toast.remove(), { once: true }); };
            const timeoutId = setTimeout(dismiss, duration);
            toast.addEventListener('click', dismiss);
        },
        async initializeUI() {
            if (this.uiInitialized) return; this.uiInitialized = true;
            const trixCSS=`:root{--trix-bg:#1e1e1e;--trix-bg-light:#252526;--trix-header-bg:#333333;--trix-border:#3c3c3c;--trix-text:#d4d4d4;--trix-text-secondary:#cccccc;--trix-blue-accent:#007acc;--trix-button-bg:#0e639c;--trix-button-hover-bg:#1177bb;--trix-input-bg:#3c3c3c}#trix-container{position:fixed;top:20px;left:20px;width:280px;background-color:var(--trix-bg-light);border:1px solid var(--trix-border);border-radius:6px;color:var(--trix-text);font-family:'Consolas','Menlo','Courier New',monospace;font-size:14px;z-index:99999;box-shadow:0 5px 20px rgba(0,0,0,0.5);user-select:none;overflow:hidden}#trix-header{background-color:var(--trix-header-bg);padding:8px 12px;cursor:move;font-weight:bold;border-bottom:1px solid var(--trix-border)}#trix-header a{color:var(--trix-blue-accent);text-decoration:none}#trix-header a:hover{text-decoration:underline}#trix-body{padding:15px;display:flex;flex-direction:column;gap:15px}.trix-input-group{display:flex;flex-direction:column;gap:5px}.trix-input-group label{font-size:13px;color:var(--trix-text-secondary)}.trix-input-group input[type="text"]{background-color:var(--trix-input-bg);border:1px solid var(--trix-border);color:var(--trix-text);padding:8px;border-radius:4px;font-family:inherit;outline:none;transition:border-color .2s,background-color .2s}.trix-input-group input[type="text"]:focus{border-color:var(--trix-blue-accent)}.trix-input-group input[type="text"]:disabled{background-color:#2a2a2a;color:#888;cursor:not-allowed}#trix-start-btn{background-color:var(--trix-button-bg);color:white;border:none;padding:10px;border-radius:4px;font-family:inherit;font-size:14px;font-weight:bold;cursor:pointer;transition:background-color .2s}#trix-start-btn:hover{background-color:var(--trix-button-hover-bg)}#trix-start-btn:disabled{background-color:#5a5a5a;cursor:not-allowed}.trix-radio-group{display:flex;gap:15px}.trix-radio-group label{display:flex;align-items:center;gap:5px;cursor:pointer}#trix-tab-manager{margin-top:15px;border-top:1px solid var(--trix-border);padding-top:10px}#trix-tab-header{display:flex;justify-content:space-between;align-items:center;font-weight:bold;margin-bottom:5px}#trix-tab-counter{background-color:var(--trix-blue-accent);color:white;padding:2px 6px;border-radius:10px;font-size:12px}#trix-tab-list{list-style:none;padding:0;margin:0;max-height:120px;overflow-y:auto;background-color:var(--trix-bg);border-radius:4px;border:1px solid var(--trix-border)}.trix-tab-item,.trix-tab-item-empty{padding:5px 8px;border-bottom:1px solid var(--trix-border);font-size:12px;display:flex;justify-content:space-between;align-items:center}.trix-tab-item:last-child{border-bottom:none}.trix-tab-item-empty{justify-content:center;color:var(--trix-text-secondary)}.status-and-btn{display:flex;align-items:center;gap:8px}.tab-status{font-weight:bold}.status-live .tab-status{color:#2ecc71}.status-connecting .tab-status{color:#f39c12}.status-idle .tab-status{color:#f1c40f}.status-disconnected .tab-status{color:#e74c3c}.trix-reconnect-btn{background-color:#3498db;color:white;border:none;font-size:10px;padding:2px 6px;border-radius:3px;cursor:pointer;transition:background-color .2s}.trix-reconnect-btn:hover{background-color:#2980b9}#trix-toast-container{position:fixed;bottom:20px;right:20px;z-index:100001;display:flex;flex-direction:column;gap:10px;align-items:flex-end}@keyframes trix-toast-in{from{transform:translateX(110%);opacity:0}to{transform:translateX(0);opacity:1}}@keyframes trix-toast-out{to{opacity:0;transform:scale(.9)}}.trix-toast{background-color:var(--trix-bg-light);color:var(--trix-text);border-left:5px solid var(--trix-blue-accent);padding:15px;border-radius:8px;box-shadow:0 5px 25px rgba(0,0,0,0.5);width:350px;animation:trix-toast-in .5s cubic-bezier(.25,.46,.45,.94);position:relative;overflow:hidden;cursor:pointer}.trix-toast.fade-out{animation:trix-toast-out .4s ease-in forwards}.toast-header{display:flex;justify-content:space-between;align-items:center;font-weight:bold}.toast-timer{font-size:11px;color:var(--trix-text-secondary)}.toast-body{font-size:14px;margin-top:5px;word-break:break-all;max-height:150px;overflow-y:auto}.toast-body code{background:var(--trix-bg);padding:2px 4px;border-radius:4px}.toast-progress{position:absolute;bottom:0;left:0;height:4px;background-color:var(--trix-blue-accent);width:100%;opacity:.7}`;
            GM_addStyle(trixCSS);
            const trixHTML=`<div id="trix-toast-container"></div><div id="trix-container"><div id="trix-header">Official TriX Proxy Bot [MAIN]</div><div id="trix-body"><div class="trix-input-group"><label for="trix-clan-tag">Clan Tag (Customizable in AI mode)</label><input type="text" id="trix-clan-tag" placeholder="e.g., TriX"></div><div class="trix-input-group"><label for="trix-username">Username</label><input type="text" id="trix-username" placeholder="Enter your name"></div><div class="trix-input-group"><label>Mode</label><div class="trix-radio-group"><label><input type="radio" name="trix-typing-style" value="custom" checked> Custom</label><label><input type="radio" name="trix-typing-style" value="ai"> AI</label></div></div><button id="trix-start-btn">Type & Join Game (All Tabs)</button><div id="trix-tab-manager"><div id="trix-tab-header"><span>Sub-Tabs</span><span id="trix-tab-counter">0</span></div><ul id="trix-tab-list"></ul></div></div></div>`;
            document.body.insertAdjacentHTML('beforeend',trixHTML);
            const clanTagInput=document.getElementById("trix-clan-tag"),usernameInput=document.getElementById("trix-username"),radioButtons=document.querySelectorAll('input[name="trix-typing-style"]'),startBtn=document.getElementById("trix-start-btn");const toggleUsernameInput=()=>{const e=document.querySelector('input[name="trix-typing-style"]:checked').value;usernameInput.disabled="ai"===e,usernameInput.placeholder="ai"===e?"AI will generate a name":"Enter your name"};radioButtons.forEach(e=>e.addEventListener("change",toggleUsernameInput));const saveSettings=async()=>{await GM_setValue(CONFIG.STORAGE_KEY,{clanTag:clanTagInput.value,username:usernameInput.value})},loadSettings=async()=>{const e=await GM_getValue(CONFIG.STORAGE_KEY,{});clanTagInput.value=e.clanTag||"",usernameInput.value=e.username||""};await loadSettings();toggleUsernameInput();clanTagInput.addEventListener("input",saveSettings);usernameInput.addEventListener("input",saveSettings);startBtn.addEventListener("click",async()=>{const e={type:'TYPE_AND_JOIN',clanTag:clanTagInput.value.trim(),mode:document.querySelector('input[name="trix-typing-style"]:checked').value,customUsername:usernameInput.value.trim(),timestamp:Date.now()};await GM_setValue(CONFIG.COMMAND_QUEUE_KEY,e)});
            document.getElementById('trix-tab-list').addEventListener('click', async (e) => { if (e.target.classList.contains('trix-reconnect-btn')) { const targetId = e.target.dataset.targetId; const command = { type: 'RECONNECT', targetTabId: targetId, timestamp: Date.now() }; await GM_setValue(CONFIG.COMMAND_QUEUE_KEY, command); this.showToast('Command Sent', `Reconnect command sent to tab ${targetId.slice(-6)}.`); } });
            const container=document.getElementById("trix-container"),header=document.getElementById("trix-header");let isDragging=!1,offsetX,offsetY;header.onmousedown=e=>{isDragging=!0,offsetX=e.clientX-container.offsetLeft,offsetY=e.clientY-container.offsetTop,document.onmousemove=e=>{isDragging&&(container.style.left=`${e.clientX-offsetX}px`,container.style.top=`${e.clientY-offsetY}px`)},document.onmouseup=()=>{isDragging=!1,document.onmousemove=null,document.onmouseup=null}};
        }
    };

    // --- 4. Sub-Tab Logic ---
    const subTab = {
        status: 'IDLE', previousStatus: 'IDLE', isReconnecting: false, lastCommandTimestamp: 0, lastKnownSocketUrl: null, uiInitialized: false,
        init() {
            document.title = "[Sub-Tab] TriX Bot";
            waitForGameUI(() => this.initializeUI());
            this.proxyWebSocket();
            setInterval(() => this.sendHeartbeat(), CONFIG.HEARTBEAT_INTERVAL);
            setInterval(() => this.checkMainTabHealth(), CONFIG.HEARTBEAT_INTERVAL);
            setInterval(() => this.listenForCommands(), 500);
        },
        initializeUI() {
            if(this.uiInitialized) return; this.uiInitialized = true;
            const subTabCSS=`:root{--trix-bg:#1e1e1e;--trix-bg-light:#252526;--trix-border:#3c3c3c;--trix-text:#d4d4d4}#trix-subtab-indicator{position:fixed;top:10px;right:10px;z-index:99998;background-color:var(--trix-bg-light);border:1px solid var(--trix-border);border-radius:5px;padding:8px 12px;font-family:'Consolas',monospace;font-size:12px;color:var(--trix-text);display:flex;flex-direction:column;gap:6px;box-shadow:0 3px 10px rgba(0,0,0,.4)}.indicator-row{display:flex;align-items:center;gap:8px}.indicator-status{font-weight:700}.status-live .indicator-status{color:#2ecc71}.status-connecting .indicator-status{color:#f39c12}.status-idle .indicator-status{color:#f1c40f}.status-disconnected .indicator-status{color:#e74c3c}#trix-subtab-ws-url{background-color:var(--trix-bg);padding:2px 4px;border-radius:3px;font-size:11px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#trix-subtab-reconnect-btn{background-color:#3498db;color:#fff;border:none;width:100%;padding:5px;font-size:11px;border-radius:3px;cursor:pointer;transition:background-color .2s}#trix-subtab-reconnect-btn:hover{background-color:#2980b9}`; GM_addStyle(subTabCSS);
            const subTabHTML=`<div id="trix-subtab-indicator" class="status-idle"><div class="indicator-row">Status: <span id="trix-subtab-status" class="indicator-status">IDLE</span></div><div class="indicator-row">WS: <code id="trix-subtab-ws-url">N/A</code></div><button id="trix-subtab-reconnect-btn">Establish New Connection</button></div>`;
            document.body.insertAdjacentHTML('beforeend', subTabHTML);
            document.getElementById('trix-subtab-reconnect-btn').addEventListener('click', () => this._performJoinGameLogic(true));
            this.injectToastAssets();
        },
        updateStatusUI() {
            if(!this.uiInitialized) return;
            const indicator = document.getElementById('trix-subtab-indicator'); const statusEl = document.getElementById('trix-subtab-status'); const urlEl = document.getElementById('trix-subtab-ws-url');
            indicator.className = `status-${this.status.toLowerCase()}`; statusEl.textContent = this.status; urlEl.textContent = this.lastKnownSocketUrl || 'N/A';
        },
        injectToastAssets(){if(document.getElementById("trix-toast-container"))return;const e=`:root{--trix-bg:#1e1e1e;--trix-bg-light:#252526;--trix-blue-accent:#007acc;--trix-text:#d4d4d4}#trix-toast-container{position:fixed;bottom:20px;right:20px;z-index:100001;display:flex;flex-direction:column;gap:10px;align-items:flex-end}@keyframes trix-toast-in{from{transform:translateX(110%);opacity:0}to{transform:translateX(0);opacity:1}}@keyframes trix-toast-out{to{opacity:0;transform:scale(.9)}}.trix-toast{background-color:var(--trix-bg-light);color:var(--trix-text);border-left:5px solid var(--trix-blue-accent);padding:15px;border-radius:8px;box-shadow:0 5px 25px rgba(0,0,0,.5);width:350px;animation:trix-toast-in .5s cubic-bezier(.25,.46,.45,.94);position:relative;overflow:hidden;cursor:pointer}.trix-toast.fade-out{animation:trix-toast-out .4s ease-in forwards}.toast-header{display:flex;justify-content:space-between;align-items:center;font-weight:700}.toast-timer{font-size:11px;color:var(--trix-text-secondary)}.toast-body{font-size:14px;margin-top:5px;word-break:break-all;max-height:150px;overflow-y:auto}.toast-body code{background:var(--trix-bg);padding:2px 4px;border-radius:4px}.toast-progress{position:absolute;bottom:0;left:0;height:4px;background-color:var(--trix-blue-accent);width:100%;opacity:.7}`;GM_addStyle(e);const t=document.createElement("div");t.id="trix-toast-container",document.body.appendChild(t)},
        showToast(title, message, duration = 20000) {
            const container = document.getElementById('trix-toast-container');
            const toast = document.createElement('div'); toast.className = 'trix-toast'; const startTime = performance.now();
            toast.innerHTML = `<div class="toast-header"><span>${title}</span><span class="toast-timer">${(duration / 1000).toFixed(1)}s</span></div><div class="toast-body">${message}</div><div class="toast-progress"></div>`;
            container.appendChild(toast);
            const timerEl = toast.querySelector('.toast-timer'); const progressEl = toast.querySelector('.toast-progress');
            const intervalId = setInterval(() => { const elapsed = performance.now() - startTime; const remaining = duration - elapsed; if (remaining <= 0) { clearInterval(intervalId); return; } timerEl.textContent = `${(remaining / 1000).toFixed(1)}s`; progressEl.style.width = `${(remaining / duration) * 100}%`; }, 100);
            const dismiss = () => { clearInterval(intervalId); clearTimeout(timeoutId); toast.classList.add('fade-out'); toast.addEventListener('animationend', () => toast.remove(), { once: true }); };
            const timeoutId = setTimeout(dismiss, duration);
            toast.addEventListener('click', dismiss);
        },
        async sendHeartbeat() {
            const statuses = await GM_getValue(CONFIG.TABS_STATUS_KEY, {});
            statuses[tabId] = { status: this.status, origin: tabOrigin, lastSeen: Date.now(), id: tabId, lastUrl: this.lastKnownSocketUrl };
            await GM_setValue(CONFIG.TABS_STATUS_KEY, statuses);
        },
        updateStatus(newStatus) {
            this.previousStatus = this.status;
            if (this.status !== newStatus) {
                this.status = newStatus; this.sendHeartbeat(); this.updateStatusUI();
                // A disconnect no longer triggers any action, it just waits for a command.
            }
        },
        findButtonByText(text) { for (const btn of document.querySelectorAll('button')) { if (btn.innerText && btn.innerText.toLowerCase().includes(text.toLowerCase())) return btn; } return null; },
        _performJoinGameLogic(isManual = false) {
            if (isManual) console.log('[TriX Sub-Tab] Manual join game initiated.');
            let multiplayerBtn = this.findButtonByText('Multiplayer');
            if (multiplayerBtn) {
                multiplayerBtn.click();
                setTimeout(() => {
                    const readyBtn = this.findButtonByText('Ready');
                    if (readyBtn) {
                        readyBtn.click();
                    } else {
                        console.error('[TriX Sub-Tab] Could not find "Ready" button. Tab may be stuck.');
                    }
                }, 500);
            } else {
                console.error('[TriX Sub-Tab] ACTION FAILED: Not on the main menu. Cannot join game. Please manually navigate this tab back to the main menu.');
            }
        },
        async checkMainTabHealth() {
            const mainTabInfo = await GM_getValue(CONFIG.MAIN_TAB_KEY, null);
            if (!mainTabInfo || Date.now() - mainTabInfo.timestamp > CONFIG.MAIN_TAB_TIMEOUT) {
                const reloadFlag = localStorage.getItem(CONFIG.RELOAD_FLAG_KEY);
                if (reloadFlag && Date.now() - parseInt(reloadFlag) < 5000) { return; }
                console.warn('[TriX Bot] Main tab timed out or crashed. Attempting re-election.');
                await GM_setValue(CONFIG.MAIN_TAB_KEY, { id: tabId, timestamp: Date.now() });
                setTimeout(async () => { const currentMain = await GM_getValue(CONFIG.MAIN_TAB_KEY); if (currentMain && currentMain.id === tabId) { window.location.reload(); } }, 500);
            }
        },
        async listenForCommands() {
            const command = await GM_getValue(CONFIG.COMMAND_QUEUE_KEY, null);
            if (command && command.timestamp > this.lastCommandTimestamp) {
                this.lastCommandTimestamp = command.timestamp;
                if (command.type === 'RECONNECT' && command.targetTabId === tabId) {
                    this._performJoinGameLogic(true);
                } else if (command.type === 'TYPE_AND_JOIN') {
                    const nameInput=document.querySelector("#input0");if(!nameInput)return;let fullName,username;if("ai"===command.mode){username=CONFIG.AI_NAMES[Math.floor(Math.random()*CONFIG.AI_NAMES.length)],fullName=command.clanTag?(()=>{const e=["standard","possessive","lowerTag","tagAfter"][Math.floor(4*Math.random())];switch(e){case"standard":return`[${command.clanTag}] ${username}`;case"possessive":return`[${command.clanTag}]'s ${username}`;case"lowerTag":return`[${command.clanTag.toLowerCase()}] ${username}`;case"tagAfter":return`${username} [${command.clanTag.toLowerCase()}]`}})():username}else{if(!(username=command.customUsername))return;fullName=command.clanTag?`[${command.clanTag}] ${username}`:username}
                    simulateTyping(nameInput, fullName, () => {
                        this._performJoinGameLogic();
                    });
                }
            }
        },
        proxyWebSocket() {
            const self = this; const OriginalWebSocket = unsafeWindow.WebSocket;
            unsafeWindow.WebSocket = function(url, protocols) {
                if (url.includes(CONFIG.MAIN_SOCKET_URL_PART)) {
                    self.lastKnownSocketUrl = url; self.updateStatus('CONNECTING');
                    const ws = new OriginalWebSocket(url, protocols);
                    ws.addEventListener('open', () => {
                        self.updateStatus('LIVE');
                        self.showToast('Successfully established a connection!', `WebSocket URL: <code>${self.lastKnownSocketUrl}</code>`);
                    });
                    ws.addEventListener('close', () => self.updateStatus('DISCONNECTED'));
                    ws.addEventListener('error', () => self.updateStatus('DISCONNECTED'));
                    return ws;
                }
                return new OriginalWebSocket(url, protocols);
            };
        }
    };

    // --- 5. Initialization and Role Election ---
    async function electRole() {
        if (sessionStorage.getItem('wasTrixMain') === 'true') {
            await GM_setValue(CONFIG.MAIN_TAB_KEY, { id: tabId, timestamp: Date.now() });
            mainTab.init();
            return;
        }
        const currentMain = await GM_getValue(CONFIG.MAIN_TAB_KEY, null);
        if (!currentMain || Date.now() - currentMain.timestamp > CONFIG.MAIN_TAB_TIMEOUT) {
            const reloadFlag = localStorage.getItem(CONFIG.RELOAD_FLAG_KEY);
            if (reloadFlag && Date.now() - parseInt(reloadFlag) < 5000) {
                 console.log('[TriX Bot] Main tab is reloading. Initializing as Sub-Tab and waiting.');
                 subTab.init();
                 return;
            }
            await GM_setValue(CONFIG.MAIN_TAB_KEY, { id: tabId, timestamp: Date.now() });
            mainTab.init();
        } else {
            subTab.init();
        }
    }
    window.addEventListener('beforeunload', async () => {
        if (isMainTab) {
            sessionStorage.setItem('wasTrixMain', 'true');
            localStorage.setItem(CONFIG.RELOAD_FLAG_KEY, Date.now());
        }
        const statuses = await GM_getValue(CONFIG.TABS_STATUS_KEY, {});
        delete statuses[tabId]; await GM_setValue(CONFIG.TABS_STATUS_KEY, statuses);
    });

    electRole();
})();