Greasy Fork镜像 还支持 简体中文。

TriX Executor (BETA) for Territorial.io

Small update

目前為 2025-09-14 提交的版本,檢視 最新版本

// ==UserScript==
// @name         TriX Executor (BETA) for Territorial.io
// @namespace    https://gf.qytechs.cn/en/users/COURTESYCOIL
// @version      Beta-Indus-2024.09.07
// @description  Small update
// @author       Painsel
// @match        *://territorial.io/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        unsafeWindow
// @run-at       document-start
// @license      MIT
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzAwYWFmZiI+PHBhdGggZD0iTTE4LjM2IDIyLjA1bC00LjQyLTQuNDJDMTMuNDcgMTcuODcgMTMgMTguMTcgMTMgMTguNUMxMyAyMC45OSAxNS4wMSAyMyAxNy41IDIzaDMuNTRjLTIuNDUtMS40OC00LjQyLTMuNDUtNS5LTUuOTV6TTggMTNjMS42NiAwIDMuMTgtLjU5IDQuMzgtMS42MkwxMCAxMy41VjIybDIuNS0yLjVMMTggMTMuOTZjLjM1LS40OC42NS0xIC44Ny0xLjU1QzE4LjYxIDEzLjQxIDE4IDEyLjM0IDE4IDEyVjBoLTJ2MTJjMCAuMzQtLjAyLjY3LS4wNiAxLS4zMy4xOC0uNjguMy0xLjA0LjM3LTEuNzMgMC0zLjI3LS45My00LjE2LTIuMzZMNiAxMy42VjVINXY4eiIvPjwvc3ZnPg==
// ==/UserScript==

/* global Prism, unsafeWindow */

(function() {
    'use strict';

    // --- Global State for Advanced Features ---
    let interceptionEnabled = false;
    let isLoggerSuspended = false;
    let packetLogHistory = [];
    let queuedMessages = new Map();
    let renderInterceptorQueue = () => {};
    let customWs = null;

    let updateConnectionStatus = () => {};
    let updatePingDisplay = () => {};
    let logPacketCallback = () => {};

    // --- WebSocket Proxy Setup ---
    const OriginalWebSocket = unsafeWindow.WebSocket;
    unsafeWindow.WebSocket = function(url, protocols) {
        if (!url.includes('/s52/')) { return new OriginalWebSocket(url, protocols); }
        console.log(`[TriX] Intercepting WebSocket connection to: ${url}`);
        const ws = new OriginalWebSocket(url, protocols);
        let originalOnMessageHandler = null;
        const originalSend = ws.send.bind(ws);
        ws.send = function(data) {
            if (interceptionEnabled) {
                const messageId = `send-${Date.now()}-${Math.random()}`;
                const promise = new Promise(resolve => queuedMessages.set(messageId, { direction: 'send', data, resolve }));
                renderInterceptorQueue();
                promise.then(decision => {
                    queuedMessages.delete(messageId);
                    renderInterceptorQueue();
                    if (decision.action === 'forward') {
                        originalSend(decision.data);
                        logPacketCallback('send', `[Forwarded] ${decision.data}`);
                    } else { logPacketCallback('send', `[Blocked] ${data}`); }
                });
            } else { logPacketCallback('send', data); return originalSend(data); }
        };
        Object.defineProperty(ws, 'onmessage', {
            get: () => originalOnMessageHandler,
            set: (handler) => {
                originalOnMessageHandler = handler;
                ws.addEventListener('message', event => {
                    if (interceptionEnabled) {
                        const messageId = `recv-${Date.now()}-${Math.random()}`;
                        const promise = new Promise(resolve => queuedMessages.set(messageId, { direction: 'receive', data: event.data, resolve }));
                        renderInterceptorQueue();
                        promise.then(decision => {
                            queuedMessages.delete(messageId);
                            renderInterceptorQueue();
                            if (decision.action === 'forward') {
                                logPacketCallback('receive', `[Forwarded] ${decision.data}`);
                                if (originalOnMessageHandler) originalOnMessageHandler({ data: decision.data });
                            } else { logPacketCallback('receive', `[Blocked] ${event.data}`); }
                        });
                    } else { logPacketCallback('receive', event.data); if (originalOnMessageHandler) originalOnMessageHandler(event); }
                });
            },
            configurable: true
        });
        ws.addEventListener('open', () => updateConnectionStatus('connected', 'Connection established.'));
        ws.addEventListener('close', (event) => {
            if (unsafeWindow.webSocketManager && unsafeWindow.webSocketManager.activeSocket === ws) unsafeWindow.webSocketManager.activeSocket = null;
            updateConnectionStatus('disconnected', `Disconnected. Code: ${event.code}`);
            updatePingDisplay('---');
        });
        ws.addEventListener('error', () => updateConnectionStatus('error', 'A connection error occurred.'));
        unsafeWindow.webSocketManager = { activeSocket: ws };
        return ws;
    };


    // --- Main script logic ---
    function initialize() {
        const shadowHost = document.createElement('div');
        shadowHost.id = 'trix-host';
        document.body.appendChild(shadowHost);
        const shadowRoot = shadowHost.attachShadow({ mode: 'open' });

        const styleElement = document.createElement('style');
        styleElement.textContent = `
            /* --- Loading Screen Styles --- */
            #trix-loading-screen { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: #000; z-index: 999999; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #fff; font-family: 'Segoe UI', sans-serif; opacity: 0; transition: opacity 1.5s ease-in-out; }
            #trix-loading-screen.visible { opacity: 1; }
            .loading-content { opacity: 1; transition: opacity 1s ease-in-out; }
            .loading-content.fade-out { opacity: 0; }
            .loading-header { font-size: 5rem; font-weight: bold; color: #00aaff; text-shadow: 2px 2px 0px #0077cc, 4px 4px 0px #0055aa, 6px 6px 10px rgba(0,0,0,0.5); margin-bottom: 2rem; }
            .loading-bar-container { width: 50%; max-width: 600px; height: 20px; background-color: #2a2a30; border-radius: 10px; border: 1px solid #444; overflow: hidden; margin-bottom: 1rem; }
            .loading-bar-progress { width: 0%; height: 100%; background: linear-gradient(90deg, #00aaff, #3b82f6); border-radius: 10px; transition: width 0.1s linear; }
            .loading-percentage { font-size: 1.2rem; margin-bottom: 2rem; }
            .loading-message { font-style: italic; color: #9ca3af; min-height: 1.2em; }

            /* --- PrismJS Theme --- */
            code[class*="language-"],pre[class*="language-"]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;tab-size:4;}pre[class*="language-"]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*="language-"],pre[class*="language-"]{background:#2d2d2d}:not(pre)>code[class*="language-"]{padding:.1em;border-radius:.3em;white-space:normal}.token.comment{color:#999}.token.punctuation{color:#ccc}.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.number,.token.function{color:#f08d49}.token.property,.token.class-name,.token.constant,.token.symbol{color:#f8c555}.token.selector,.token.important,.token.atrule,.token.keyword,.token.builtin{color:#cc99cd}.token.string,.token.char,.attr-value,.token.regex,.token.variable{color:#7ec699}.token.operator,.token.entity,.token.url{color:#67cdcc}
            
            /* --- Refined UI Theme --- */
            :host { --accent-color: #3b82f6; --bg-darker: #1e1e22; --bg-dark: #2a2a30; --border-color: #444; }
            @keyframes trix-fade-in { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
            @keyframes trix-slide-in { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
            
            #trix-toggle-btn { position: fixed; top: 15px; right: 15px; z-index: 99999; width: 50px; height: 50px; background-color: rgba(30, 30, 34, 0.8); color: #00aaff; border: 2px solid #00aaff; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 24px; transition: all 0.3s ease; backdrop-filter: blur(5px); font-family: monospace; box-shadow: 0 0 10px rgba(0, 170, 255, 0.5); }
            #trix-toggle-btn:hover { transform: scale(1.1) rotate(15deg); box-shadow: 0 0 15px #00aaff; }

            #trix-container { position: fixed; top: 80px; right: 15px; width: 500px; min-height: 450px; z-index: 99998; color: #e5e7eb; font-family: 'Segoe UI', 'Roboto', sans-serif; border-radius: 10px; overflow: hidden; box-shadow: 0 5px 25px rgba(0,0,0,0.5); display: flex; flex-direction: column; backdrop-filter: blur(10px); animation: trix-slide-in 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; resize: both; background-color: rgba(30, 30, 34, 0.9); border: 1px solid var(--border-color); }
            #trix-container.hidden { display: none; }
            #trix-header, #trix-footer { cursor: move; user-select: none; flex-shrink: 0; }
            #trix-header { padding: 10px 15px; display: flex; justify-content: space-between; align-items: center; background-color: rgba(0,0,0,0.3); }
            .trix-title { font-size: 16px; font-weight: 600; }
            .trix-version-tag { font-size: 10px; font-weight: 300; opacity: 0.7; margin-left: 8px; }
            #trix-header-controls { display: flex; align-items: center; gap: 15px; font-size: 12px; }
            #trix-close-btn { cursor: pointer; font-size: 20px; font-weight: bold; opacity: 0.7; }
            #trix-close-btn:hover { opacity: 1; color: #ef4444; }

            #trix-conn-status { width: 10px; height: 10px; border-radius: 50%; }
            #trix-conn-status.connected { background-color: #28a745; } #trix-conn-status.disconnected { background-color: #dc3545; } #trix-conn-status.connecting { background-color: #ffc107; }
            .ping-good { color: #28a745; } .ping-ok { color: #ffc107; } .ping-bad { color: #dc3545; }

            #trix-content { padding: 0 15px 15px 15px; flex-grow: 1; display: flex; flex-direction: column; overflow: hidden; background-color: var(--bg-darker); }
            .trix-tabs { display: flex; flex-wrap: wrap; flex-shrink: 0; }
            .trix-tab { background: var(--bg-dark); color: #9ca3af; padding: 8px 12px; cursor: pointer; border-top: 2px solid transparent; border-left: 1px solid transparent; border-right: 1px solid transparent; border-radius: 5px 5px 0 0; margin-right: 2px; position: relative; }
            .trix-tab.active { background: var(--bg-darker); font-weight: 600; color: #e5e7eb; border-top-color: var(--accent-color); }
            .trix-tab-name { padding-right: 15px; } .trix-tab-close { position: absolute; top: 50%; right: 5px; transform: translateY(-50%); opacity: 0.6; } .trix-tab-close:hover { opacity: 1; color: #ef4444; }
            #trix-new-tab-btn { background: none; border: none; color: #9ca3af; font-size: 20px; cursor: pointer; padding: 5px 10px; }
            .trix-tab-rename-input { background: transparent; border: 1px solid var(--accent-color); color: inherit; font-family: inherit; font-size: inherit; padding: 0; margin: 0; width: 100px; }

            .trix-view { display: flex; flex-direction: column; flex-grow: 1; padding-top: 10px; overflow: hidden; }
            .trix-action-bar { display: flex; gap: 10px; margin-top: 10px; flex-shrink: 0; }
            .trix-button { background-color: var(--accent-color); color: white; border: none; padding: 10px 15px; cursor: pointer; border-radius: 6px; flex-grow: 1; transition: all 0.2s; font-weight: 600; }
            .trix-button:hover { filter: brightness(1.1); }
            .trix-button:disabled { background-color: #555; cursor: not-allowed; filter: none; }
            #trix-suspend-log-btn.suspended { background-color: #ffc107; color: #1e1e22; }
            #trix-suspend-log-btn.suspended:hover { background-color: #ffc107; filter: brightness(1.1); }
            .trix-input { background: #2d2d2d; border: 1px solid var(--border-color); color: #ccc; padding: 8px; border-radius: 5px; font-family: monospace; box-sizing: border-box; }
            .trix-status-bar { margin-top: 10px; padding: 5px; background: rgba(0,0,0,0.2); font-size: 12px; border-radius: 3px; min-height: 1em; }
            
            #trix-welcome-view h2 { margin-top: 0; color: #fff; }
            #trix-welcome-view p { color: #d1d5db; line-height: 1.6; }
            #trix-welcome-view code { background: var(--bg-dark); padding: 2px 5px; border-radius: 4px; font-family: Consolas, monospace; }

            #trix-ws-client-view { display: grid; grid-template-rows: auto auto 1fr auto; gap: 10px; height: 100%; }
            .trix-ws-client-grid { display: grid; grid-template-columns: 1fr auto; gap: 10px; align-items: start; }
            #trix-ws-url { width: 100%; }
            #trix-ws-connect-btn.connected { background-color: #dc3545; }
            #trix-ws-console { flex-grow: 1; background: #1e1e22; border: 1px solid var(--border-color); border-radius: 5px; padding: 10px; overflow-y: auto; font-family: monospace; font-size: 12px; white-space: pre-wrap; word-break: break-all; }
            .ws-console-send { color: #7ec699; } .ws-console-receive { color: #cc99cd; } .ws-console-system { color: #f08d49; }
            #trix-ws-message { width: 100%; min-height: 80px; resize: vertical; }
            .trix-ws-message-controls, .trix-injector-message-controls { display: flex; flex-direction: column; gap: 5px; }

            #trix-injector-view { display: flex; flex-direction: column; flex-grow: 1; }
            .trix-injector-grid { display: grid; grid-template-columns: 1fr auto; gap: 10px; }
            #trix-injector-message { flex-grow: 1; resize: vertical; min-height: 100px; }
            
            #trix-interceptor-view { flex-grow: 1; display: flex; flex-direction: column; }
            #trix-interceptor-controls { display: flex; gap: 10px; margin-bottom: 10px; }
            #trix-toggle-interception-btn.active { background-color: #dc3545; box-shadow: 0 0 8px #dc3545; }
            #trix-interceptor-queue { flex-grow: 1; background: #1e1e22; border: 1px solid var(--border-color); border-radius: 5px; padding: 10px; overflow-y: auto; font-family: monospace; font-size: 12px; }
            .interceptor-item { border: 1px solid var(--border-color); border-radius: 4px; padding: 8px; margin-bottom: 8px; background: var(--bg-dark); }
            .interceptor-item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; font-weight: bold; }
            .interceptor-item-header .send { color: #7ec699; } .interceptor-item-header .receive { color: #cc99cd; }
            .interceptor-item textarea { width: 100%; box-sizing: border-box; background: #2d2d2d; color: #ccc; border: 1px solid var(--border-color); min-height: 40px; font-family: inherit; }
            .interceptor-item-actions { display: flex; gap: 5px; margin-top: 5px; }
            .interceptor-btn { flex-grow: 1; padding: 4px; font-size: 11px; }
            .interceptor-forward-btn { background-color: #28a745; }
            .interceptor-block-btn { background-color: #dc3545; }

            #trix-packet-log-view, #trix-storage-view-container, #trix-script-injector-container { flex-grow: 1; display: flex; flex-direction: column; }
            #trix-packet-log { flex-grow: 1; background: #1e1e22; border: 1px solid var(--border-color); border-radius: 5px; padding: 10px; overflow-y: auto; font-family: monospace; font-size: 12px; }
            .packet-item { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #333; padding: 4px 2px; }
            .packet-data { flex-grow: 1; word-break: break-all; }
            .packet-actions { display: flex; gap: 5px; margin-left: 10px; }
            .packet-action-btn { background: #3a3a42; border: 1px solid #555; color: #ccc; cursor: pointer; font-size: 11px; padding: 2px 6px; border-radius: 3px; }
            .packet-action-btn:hover { background: var(--accent-color); color: white; border-color: var(--accent-color); }
            .packet-inject .packet-meta { color: #f08d49; }

            .trix-editor-area { position: relative; flex-grow: 1; background: #2d2d2d; border: 1px solid var(--border-color); border-radius: 5px; }
            .trix-editor-area textarea, .trix-editor-area pre { margin: 0; padding: 10px; font-family: 'Fira Code', 'Consolas', monospace; font-size: 14px; line-height: 1.5; width: 100%; height: 100%; box-sizing: border-box; position: absolute; top: 0; left: 0; overflow: auto; }
            .trix-editor-area textarea { z-index: 1; background: transparent; color: inherit; resize: none; border: none; outline: none; -webkit-text-fill-color: transparent; }
            .trix-editor-area pre { z-index: 0; pointer-events: none; }
            #trix-storage-view { flex-grow: 1; overflow-y: auto; font-family: monospace; font-size: 13px; }
            .storage-table { margin-top: 10px; } .storage-header { font-weight: bold; color: var(--accent-color); border-bottom: 1px solid var(--border-color); padding-bottom: 5px; margin-bottom: 5px; }
            .storage-row { display: flex; align-items: center; border-bottom: 1px solid #333; padding: 4px 2px; }
            .storage-key { color: #cc99cd; min-width: 100px; max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
            .storage-value { color: #7ec699; flex-grow: 1; cursor: pointer; word-break: break-all; }
            .storage-value-input { background: #3a3a42; color: #eee; border: 1px solid var(--accent-color); width: 100%; font-family: inherit; font-size: inherit; }
            .storage-delete { cursor: pointer; color: #ef4444; opacity: 0.5; margin-left: 10px; font-weight: bold; }
            .storage-delete:hover { opacity: 1; }
            
            #trix-music-toggle-btn { position: fixed; bottom: 20px; left: 20px; z-index: 99999; width: 50px; height: 50px; background-color: rgba(30, 30, 34, 0.8); color: #e5e7eb; border: 1px solid var(--border-color); border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(5px); transition: all 0.3s ease; }
            #trix-music-toggle-btn:hover { transform: scale(1.1); box-shadow: 0 0 10px var(--accent-color); color: var(--accent-color); }
            #trix-music-player { display: flex; flex-direction: column; position: fixed; bottom: 80px; left: 20px; width: 300px; background: rgba(30, 30, 34, 0.9); border: 1px solid var(--border-color); backdrop-filter: blur(10px); border-radius: 8px; padding: 15px; z-index: 99998; box-shadow: 0 5px 15px rgba(0,0,0,0.3); opacity: 0; transform: scale(0.95) translateY(10px); transition: opacity 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease; pointer-events: none; }
            #trix-music-player.visible { opacity: 1; transform: scale(1) translateY(0); pointer-events: auto; }
            
            @keyframes rainbow-glow { 0%{box-shadow:0 0 10px 2px hsl(0,100%,60%)}16%{box-shadow:0 0 10px 2px hsl(60,100%,60%)}33%{box-shadow:0 0 10px 2px hsl(120,100%,60%)}50%{box-shadow:0 0 10px 2px hsl(180,100%,60%)}66%{box-shadow:0 0 10px 2px hsl(240,100%,60%)}83%{box-shadow:0 0 10px 2px hsl(300,100%,60%)}100%{box-shadow:0 0 10px 2px hsl(360,100%,60%)} }
            #trix-music-player.rainbow-outline { animation: rainbow-glow 2s linear infinite; border-color: transparent; }
            
            .music-drag-handle { position: absolute; top: 0; left: 0; width: 100%; height: 20px; cursor: move; }
            .music-info { text-align: center; margin-bottom: 10px; }
            .music-title { font-size: 16px; font-weight: 600; display: block; }
            .music-artist { font-size: 12px; color: #9ca3af; }
            .music-progress-container { width: 100%; background: #4b5563; border-radius: 5px; cursor: pointer; height: 6px; margin-bottom: 5px; }
            .music-progress-bar { background: var(--accent-color); width: 0%; height: 100%; border-radius: 5px; }
            .music-time { display: flex; justify-content: space-between; font-size: 11px; color: #9ca3af; }
            .music-controls { display: flex; justify-content: space-around; align-items: center; margin-bottom: 10px; }
            .music-options-bar { display: flex; justify-content: space-around; align-items: center; width: 100%; margin-bottom: 10px; }
            .music-control-btn { background: none; border: none; color: #e5e7eb; cursor: pointer; padding: 5px; opacity: 0.8; transition: all 0.2s; }
            .music-control-btn:hover { opacity: 1; color: var(--accent-color); }
            .music-control-btn.active { color: var(--accent-color); }
            .music-play-btn { font-size: 24px; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; }
            .music-volume-container { display: flex; align-items: center; justify-content: center; position: relative; }
            .music-volume-slider { -webkit-appearance: none; appearance: none; width: 80px; height: 5px; background: #4b5563; border-radius: 5px; outline: none; transition: opacity 0.3s; }
            .music-volume-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 12px; height: 12px; background: #e5e7eb; cursor: pointer; border-radius: 50%; }
            #trix-volume-indicator { position: absolute; top: -30px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.7); color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px; opacity: 0; transition: opacity 0.3s; pointer-events: none; }
            #trix-volume-indicator.visible { opacity: 1; }
            #trix-music-menu, #trix-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 100000; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(5px); opacity: 0; transition: opacity 0.3s; pointer-events: none; }
            #trix-music-menu.visible, #trix-modal.visible { opacity: 1; pointer-events: auto; }
            .music-menu-content, .modal-content { background: var(--bg-darker); border: 1px solid var(--border-color); width: 90%; max-width: 500px; max-height: 80%; border-radius: 8px; display: flex; flex-direction: column; }
            .music-menu-header, .modal-header { padding: 15px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
            #music-search-input { width: 100%; box-sizing: border-box; }
            .music-song-list, .modal-body { overflow-y: auto; padding: 10px; }
            .modal-body pre { white-space: pre-wrap; word-break: break-all; margin: 0; }
            .music-song-item { padding: 10px 15px; cursor: pointer; border-radius: 4px; transition: background-color 0.2s; }
            .music-song-item:hover, .music-song-item.playing { background-color: var(--bg-dark); }
            .music-song-title { display: block; } .music-song-artist { font-size: 12px; color: #9ca3af; }
        `;
        shadowRoot.appendChild(styleElement);

        let state = {
            tabs: [], activeTabId: null,
            settings: { position: { top: '80px', right: '15px' }, size: { width: '500px', height: '450px' } },
            wsClient: { url: 'wss://echo.websocket.events', protocols: '', savedMessages: { 'Example JSON': '{"action":"ping","id":123}'} },
            injector: { savedMessages: { 'Example Chat': '42["chat","Hello from TriX!"]'} },
            musicPlayer: { volume: 0.5, currentSongIndex: 0, position: { bottom: '80px', left: '20px'} }
        };

        const $ = (selector, parent = shadowRoot) => parent.querySelector(selector);
        const debounce = (func, delay) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => func.apply(this, a), delay); }; };

        function loadState() {
            const savedState = GM_getValue('trixExecutorState');
            if (savedState) {
                state.tabs = savedState.tabs || [];
                state.activeTabId = savedState.activeTabId;
                state.settings = { ...state.settings, ...savedState.settings };
                state.wsClient = { ...state.wsClient, ...savedState.wsClient };
                state.injector = { ...state.injector, ...savedState.injector };
                state.musicPlayer = { ...state.musicPlayer, ...savedState.musicPlayer };
            }
            const defaultTabs = [
                { id: 'welcome', name: 'Welcome', isPermanent: true },
                { id: 'logger', name: 'Packet Logger', isPermanent: true },
                { id: 'interceptor', name: 'Interceptor', isPermanent: true },
                { id: 'injector', name: 'Injector', isPermanent: true },
                { id: 'ws_client', name: 'WS Client', isPermanent: true },
                { id: 'storage', name: 'Storage', isPermanent: true },
                { id: Date.now(), name: "My First Script", code: "// Welcome to TriX Executor!\nconsole.log('Hello from a user script!');" }
            ];
            ['welcome', 'logger', 'interceptor', 'injector', 'ws_client', 'storage'].forEach((id, index) => {
                const defaultTab = defaultTabs.find(d => d.id === id);
                if (!state.tabs.find(t => t.id === id)) { state.tabs.splice(index, 0, defaultTab); }
            });
             if (!state.tabs.find(t => t.code)) { state.tabs.push(defaultTabs.find(t => t.code)); }
            if (!state.activeTabId || !state.tabs.find(t => t.id === state.activeTabId)) { state.activeTabId = state.tabs[0].id; }
        }
        
        const saveState = debounce(() => {
            const container = $('#trix-container');
            const musicPlayer = $('#trix-music-player');
            if (container) {
                state.settings.position = { top: container.style.top, left: container.style.left, right: container.style.right, bottom: 'auto' };
                state.settings.size = { width: container.style.width, height: container.style.height };
            }
            if (musicPlayer) {
                state.musicPlayer.position = { top: musicPlayer.style.top, left: musicPlayer.style.left, bottom: musicPlayer.style.bottom, right: musicPlayer.style.right };
            }
            GM_setValue('trixExecutorState', state);
        }, 500);

        let ui, editor;

        function createUI() {
            const toggleBtn = document.createElement('div');
            toggleBtn.id = 'trix-toggle-btn';
            toggleBtn.title = 'Toggle TriX Executor (BETA)';
            toggleBtn.innerHTML = 'X';
            const container = document.createElement('div');
            container.id = 'trix-container';
            container.classList.add('hidden');
            container.innerHTML = `
                <div id="trix-header">
                    <div><span class="trix-title">TriX Executor</span><span class="trix-version-tag">(v${GM_info.script.version})</span></div>
                    <div id="trix-header-controls">
                        <span id="trix-conn-status" class="disconnected" title="No connection"></span>
                        <span id="trix-ping-display">Ping: ---</span>
                        <span id="trix-fps-display">FPS: --</span>
                        <span id="trix-close-btn" title="Close">✖</span>
                    </div>
                </div>
                <div id="trix-content"></div>
                <div id="trix-footer" style="padding:10px; text-align:center; background:rgba(0,0,0,0.2);"></div>`;
            
            const musicToggle = document.createElement('div');
            musicToggle.id = 'trix-music-toggle-btn';
            musicToggle.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>`;
            
            const musicPlayer = document.createElement('div');
            musicPlayer.id = 'trix-music-player';
            musicPlayer.innerHTML = `
                <div class="music-drag-handle"></div>
                <div class="music-info"><span class="music-title">Select a Song</span><span class="music-artist">...</span></div>
                <div class="music-progress-container"><div class="music-progress-bar"></div></div>
                <div class="music-time"><span id="music-current-time">0:00</span><span id="music-duration">0:00</span></div>
                <div class="music-controls">
                    <button class="music-control-btn" id="music-prev-btn" title="Previous"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6 8.5 6V6z"/></svg></button>
                    <button class="music-control-btn" id="music-rewind-btn" title="Back 10s"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M11.999 5v2.999l-4-3-4 3v10l4-3 4 3V19l-8-6 8-6z m8 0v2.999l-4-3-4 3v10l4-3 4 3V19l-8-6 8-6z"/></svg></button>
                    <button class="music-control-btn music-play-btn" id="music-play-pause-btn"><svg width="32" height="32" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg></button>
                    <button class="music-control-btn" id="music-forward-btn" title="Forward 10s"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="m3.999 19 8-6-8-6v12zm8 0 8-6-8-6v12z"/></svg></button>
                    <button class="music-control-btn" id="music-next-btn" title="Next"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg></button>
                </div>
                <div class="music-options-bar">
                    <button class="music-control-btn" id="music-playlist-btn" title="Playlist"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/></svg></button>
                    <button class="music-control-btn" id="music-loop-btn" title="Loop"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z"/></svg></button>
                    <button class="music-control-btn" id="music-autoplay-btn" title="Autoplay"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="m10 16.5 6-4.5-6-4.5v9zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"></path></svg></button>
                </div>
                <div class="music-volume-container">
                    <input type="range" min="0" max="1" step="0.01" class="music-volume-slider" id="music-volume-slider">
                    <div id="trix-volume-indicator">100%</div>
                </div>`;

            const musicMenu = document.createElement('div');
            musicMenu.id = 'trix-music-menu';
            musicMenu.innerHTML = `
                <div class="music-menu-content">
                    <div class="music-menu-header"><input type="text" id="music-search-input" class="trix-input" placeholder="Search songs..."></div>
                    <div class="music-song-list"></div>
                </div>`;
            
            const modal = document.createElement('div');
            modal.id = 'trix-modal';
            modal.innerHTML = `
                <div class="modal-content">
                    <div class="modal-header">
                        <span>Full Packet Data</span>
                        <span id="trix-modal-close" style="cursor:pointer;font-size:20px;">&times;</span>
                    </div>
                    <div class="modal-body">
                        <pre id="trix-modal-body-content"></pre>
                    </div>
                </div>`;

            shadowRoot.append(toggleBtn, container, musicToggle, musicPlayer, musicMenu, modal);
            return { toggleBtn, container, musicPlayer, modal };
        }
        
        function applySettings() {
            const container = $('#trix-container');
            const musicPlayer = $('#trix-music-player');
            if (container) { Object.assign(container.style, state.settings.position, state.settings.size); }
            if (musicPlayer && state.musicPlayer.position) { Object.assign(musicPlayer.style, state.musicPlayer.position); }
        }

        updateConnectionStatus = function(status, title) {
            const statusIndicator = $('#trix-conn-status');
            if (statusIndicator) {
                statusIndicator.className = '';
                statusIndicator.classList.add(status);
                statusIndicator.title = title;
            }
        };

        updatePingDisplay = function(ping) {
            const pingDisplay = $('#trix-ping-display');
            if (!pingDisplay) return;
            pingDisplay.className = '';
            if (typeof ping === 'number') {
                pingDisplay.textContent = `Ping: ${ping}`;
                if (ping < 100) pingDisplay.classList.add('ping-good');
                else if (ping < 200) pingDisplay.classList.add('ping-ok');
                else pingDisplay.classList.add('ping-bad');
            } else {
                pingDisplay.textContent = `Ping: ${ping}`;
            }
        };

        async function measurePing() {
            const startTime = performance.now();
            const url = `${window.location.protocol}//${window.location.host}/favicon.ico?_ts=${Date.now()}`;
            try {
                const response = await fetch(url, { method: 'HEAD', cache: 'no-store' });
                if (response.ok) {
                    const endTime = performance.now();
                    updatePingDisplay(Math.round(endTime - startTime));
                } else {
                    updatePingDisplay('Error');
                }
            } catch (error) {
                updatePingDisplay('Error');
            }
        }

        function renderTabs() {
            const tabsContainer = $('.trix-tabs');
            if (!tabsContainer) return;
            tabsContainer.innerHTML = '';
            state.tabs.forEach(tab => {
                const tabEl = document.createElement('div');
                tabEl.className = 'trix-tab';
                tabEl.dataset.tabId = tab.id;
                if (tab.id === state.activeTabId) tabEl.classList.add('active');
                tabEl.innerHTML = `<span class="trix-tab-name">${tab.name}</span>` + (!tab.isPermanent ? `<span class="trix-tab-close">x</span>` : '');
                tabsContainer.appendChild(tabEl);
            });
            const newTabBtn = document.createElement('button');
            newTabBtn.id = 'trix-new-tab-btn';
            newTabBtn.textContent = '+';
            tabsContainer.appendChild(newTabBtn);
            renderEditor();
        }

        function renderActiveView() {
            const content = $('#trix-content');
            const activeTab = state.tabs.find(t => t.id === state.activeTabId);
            content.innerHTML = '';
            
            if (activeTab.id === 'welcome') {
                 content.innerHTML = `
                    <div class="trix-view">
                        <div class="trix-tabs"></div>
                        <div id="trix-welcome-view" style="padding: 10px;">
                            <h2>Welcome to TriX Executor!</h2>
                            <p>This is a powerful tool for web developers and gamers. Here's what you can do:</p>
                            <ul>
                                <li><b>Packet Logger:</b> See all network traffic, with tools to copy, view, or suspend the log.</li>
                                <li><b>Interceptor:</b> Pause, edit, or block WebSocket messages in real-time.</li>
                                <li><b>Injector:</b> Send custom packets to the live game server.</li>
                                <li><b>WS Client:</b> Test any WebSocket server by sending custom messages.</li>
                                <li><b>Storage:</b> View and edit <code>localStorage</code> and <code>sessionStorage</code> directly.</li>
                                <li><b>Scripts:</b> Create and run your own JavaScript snippets on the page.</li>
                            </ul>
                        </div>
                    </div>`;
                renderTabs();
            } else if (activeTab.id === 'ws_client') {
                 content.innerHTML = `
                    <div id="trix-ws-client-view" class="trix-view">
                        <div class="trix-tabs"></div>
                        <div class="trix-ws-client-grid">
                            <input type="text" id="trix-ws-url" class="trix-input" placeholder="WebSocket URL (e.g., wss://...)" value="${state.wsClient.url}">
                            <button id="trix-ws-connect-btn" class="trix-button" style="flex-grow:0;">Connect</button>
                        </div>
                        <div id="trix-ws-console"><div class="ws-console-system">Status: Disconnected</div></div>
                        <div class="trix-ws-client-grid">
                            <textarea id="trix-ws-message" class="trix-input" placeholder="Enter message to send..."></textarea>
                            <div class="trix-ws-message-controls">
                                <button id="trix-ws-send-btn" class="trix-button">Send</button>
                                <select id="trix-ws-saved-messages" class="trix-input"><option value="">Load</option></select>
                                <button id="trix-ws-save-msg-btn" class="trix-button" style="background-color:#0d6efd;">Save</button>
                                <button id="trix-ws-del-msg-btn" class="trix-button" style="background-color:#6c757d;">Del</button>
                            </div>
                        </div>
                    </div>`;
                renderTabs();
                renderWsSavedMessages();
            } else if (activeTab.id === 'interceptor') {
                 content.innerHTML = `
                    <div id="trix-interceptor-view" class="trix-view">
                        <div class="trix-tabs"></div>
                        <div id="trix-interceptor-controls" class="trix-action-bar">
                            <button id="trix-toggle-interception-btn" class="trix-button">Enable Interception</button>
                            <button id="trix-forward-all-btn" class="trix-button" style="background-color:#28a745;">Forward All</button>
                            <button id="trix-block-all-btn" class="trix-button" style="background-color:#dc3545;">Block All</button>
                        </div>
                        <div id="trix-interceptor-queue"></div>
                        <div class="trix-status-bar">Warning: Enabling interception may cause disconnects if messages are not processed quickly.</div>
                    </div>`;
                renderTabs();
                renderInterceptorQueue();
                const toggleBtn = $('#trix-toggle-interception-btn');
                if (interceptionEnabled) {
                    toggleBtn.classList.add('active');
                    toggleBtn.textContent = 'Interception Enabled';
                }
            } else if (activeTab.id === 'injector') {
                content.innerHTML = `
                    <div id="trix-injector-view" class="trix-view">
                        <div class="trix-tabs"></div>
                        <div class="trix-injector-grid" style="flex-grow:1; display:flex; flex-direction:column;">
                            <textarea id="trix-injector-message" class="trix-input" placeholder="Enter packet data to inject..."></textarea>
                            <div class="trix-injector-grid">
                                <button id="trix-inject-btn" class="trix-button">Inject Packet</button>
                                <div class="trix-injector-message-controls">
                                    <select id="trix-injector-saved-messages" class="trix-input"><option value="">Load</option></select>
                                    <button id="trix-injector-save-msg-btn" class="trix-button" style="background-color:#0d6efd;">Save</button>
                                    <button id="trix-injector-del-msg-btn" class="trix-button" style="background-color:#6c757d;">Del</button>
                                </div>
                            </div>
                        </div>
                        <div class="trix-status-bar">Inject packets into the live game connection.</div>
                    </div>`;
                renderTabs();
                renderInjectorSavedMessages();
            } else if (activeTab.id === 'logger') {
                content.innerHTML = `
                    <div id="trix-packet-log-view" class="trix-view">
                        <div class="trix-tabs"></div>
                        <div id="trix-packet-log"></div>
                        <div class="trix-action-bar">
                             <button id="trix-suspend-log-btn" class="trix-button">Suspend Log</button>
                             <button id="trix-clear-log-btn" class="trix-button">Clear Log</button>
                        </div>
                    </div>`;
                renderTabs();
                const suspendBtn = $('#trix-suspend-log-btn');
                if (suspendBtn && isLoggerSuspended) {
                    suspendBtn.classList.add('suspended');
                    suspendBtn.textContent = 'Resume Log';
                }
                renderFullPacketLog();
            } else if (activeTab.id === 'storage') {
                content.innerHTML = `
                    <div id="trix-storage-view-container" class="trix-view">
                        <div class="trix-tabs"></div>
                        <div id="trix-storage-view"></div>
                        <div class="trix-action-bar">
                            <button id="trix-refresh-storage-btn" class="trix-button">Refresh</button>
                            <button id="trix-add-storage-btn" class="trix-button">Add Local Storage Entry</button>
                        </div>
                    </div>`;
                renderTabs();
                renderStorageView();
            } else {
                content.innerHTML = `
                    <div id="trix-script-injector-container" class="trix-view">
                        <div class="trix-tabs"></div>
                        <div class="trix-editor-area"></div>
                        <div class="trix-action-bar">
                            <button id="trix-execute-btn" class="trix-button">Execute</button>
                            <button id="trix-clear-btn" class="trix-button">Clear</button>
                        </div>
                        <div class="trix-status-bar">Ready.</div>
                    </div>`;
                renderTabs();
            }
        }
        
        renderInterceptorQueue = function() {
            const queueContainer = $('#trix-interceptor-queue');
            if (!queueContainer) return;
            queueContainer.innerHTML = '';
            if (queuedMessages.size === 0) { queueContainer.textContent = 'No messages pending.'; return; }
            for (const [id, msg] of queuedMessages.entries()) {
                const item = document.createElement('div');
                item.className = 'interceptor-item';
                item.dataset.messageId = id;
                item.innerHTML = `
                    <div class="interceptor-item-header"><span class="${msg.direction}">${msg.direction.toUpperCase()}</span></div>
                    <textarea>${msg.data}</textarea>
                    <div class="interceptor-item-actions">
                        <button class="trix-button interceptor-btn interceptor-forward-btn">Forward</button>
                        <button class="trix-button interceptor-btn interceptor-block-btn">Block</button>
                    </div>`;
                queueContainer.appendChild(item);
            }
        };

        function renderStorageView() {
            const view = $('#trix-storage-view');
            if (!view) return;
            view.innerHTML = '';
            const storages = { 'Local Storage': unsafeWindow.localStorage, 'Session Storage': unsafeWindow.sessionStorage };
            for (const [name, storage] of Object.entries(storages)) {
                const container = document.createElement('div');
                container.className = 'storage-table';
                container.innerHTML = `<div class="storage-header">${name} (${storage.length} items)</div>`;
                for (let i = 0; i < storage.length; i++) {
                    const key = storage.key(i);
                    const value = storage.getItem(key);
                    const row = document.createElement('div');
                    row.className = 'storage-row';
                    row.dataset.key = key;
                    row.dataset.storage = name === 'Local Storage' ? 'local' : 'session';
                    row.innerHTML = `<span class="storage-key" title="${key}">${key}</span><span class="storage-value">${value}</span><span class="storage-delete" title="Delete key">✖</span>`;
                    container.appendChild(row);
                }
                view.appendChild(container);
            }
        }
        
        function renderWsSavedMessages() {
            const select = $('#trix-ws-saved-messages');
            if (!select) return;
            select.innerHTML = '<option value="">Load Message</option>';
            for (const name in state.wsClient.savedMessages) {
                const option = document.createElement('option');
                option.value = name;
                option.textContent = name;
                select.appendChild(option);
            }
        }
        
        function renderInjectorSavedMessages() {
            const select = $('#trix-injector-saved-messages');
            if (!select) return;
            select.innerHTML = '<option value="">Load Packet</option>';
            for (const name in state.injector.savedMessages) {
                const option = document.createElement('option');
                option.value = name;
                option.textContent = name;
                select.appendChild(option);
            }
        }

        function logToWsClientConsole(message, type = 'system') {
            const consoleEl = $('#trix-ws-console');
            if (!consoleEl) return;
            const line = document.createElement('div');
            line.className = `ws-console-${type}`;
            line.textContent = `[${type.toUpperCase()}] ${message}`;
            consoleEl.appendChild(line);
            consoleEl.scrollTop = consoleEl.scrollHeight;
        }

        function connectCustomWs() {
            const urlInput = $('#trix-ws-url');
            const connectBtn = $('#trix-ws-connect-btn');
            if (customWs && customWs.readyState < 2) { customWs.close(); return; }
            state.wsClient.url = urlInput.value.trim();
            saveState();
            if (!state.wsClient.url) { logToWsClientConsole('Error: URL cannot be empty.', 'system'); return; }
            connectBtn.textContent = 'Connecting...';
            connectBtn.disabled = true;
            logToWsClientConsole(`Connecting to ${state.wsClient.url}...`, 'system');
            customWs = new OriginalWebSocket(state.wsClient.url);
            customWs.onopen = () => { logToWsClientConsole('Connection established.', 'system'); connectBtn.textContent = 'Disconnect'; connectBtn.classList.add('connected'); connectBtn.disabled = false; };
            customWs.onmessage = (event) => { logToWsClientConsole(event.data, 'receive'); };
            customWs.onclose = () => { logToWsClientConsole('Connection closed.', 'system'); connectBtn.textContent = 'Connect'; connectBtn.classList.remove('connected'); connectBtn.disabled = false; customWs = null; };
            customWs.onerror = () => { logToWsClientConsole('Connection error.', 'system'); connectBtn.textContent = 'Connect'; connectBtn.classList.remove('connected'); connectBtn.disabled = false; customWs = null; };
        }

        function renderEditor() {
            const editorArea = $('.trix-editor-area');
            if (!editorArea) { editor = null; return; }
            const activeTab = state.tabs.find(t => t.id === state.activeTabId);
            if (!activeTab || typeof activeTab.code !== 'string') { editorArea.innerHTML = ''; editor = null; return; }
            editorArea.innerHTML = `<textarea spellcheck="false" autocapitalize="off" autocomplete="off" autocorrect="off"></textarea><pre class="language-js"><code></code></pre>`;
            editor = { textarea: $('textarea', editorArea), pre: $('pre', editorArea), code: $('code', editorArea) };
            editor.textarea.value = activeTab.code;
            highlightCode(activeTab.code);
            addEditorEventListeners();
        }

        function highlightCode(code) { if (editor) editor.code.innerHTML = Prism.highlight(code + '\n', Prism.languages.javascript, 'javascript'); }

        function addEditorEventListeners() {
            if (!editor) return;
            editor.textarea.addEventListener('input', () => {
                const activeTab = state.tabs.find(t => t.id === state.activeTabId);
                if (activeTab) { activeTab.code = editor.textarea.value; highlightCode(activeTab.code); saveState(); }
            });
            editor.textarea.addEventListener('scroll', () => { if (editor) { editor.pre.scrollTop = editor.textarea.scrollTop; editor.pre.scrollLeft = editor.textarea.scrollLeft; }});
            editor.textarea.addEventListener('keydown', e => {
                if (e.key === 'Tab') {
                    e.preventDefault(); const s = e.target.selectionStart, end = e.target.selectionEnd;
                    e.target.value = e.target.value.substring(0, s) + '  ' + e.target.value.substring(end);
                    e.target.selectionStart = e.target.selectionEnd = s + 2;
                    editor.textarea.dispatchEvent(new Event('input'));
                }
            });
        }

        function initEventListeners() {
            ui.toggleBtn.addEventListener('click', () => ui.container.classList.toggle('hidden'));
            $('#trix-close-btn').addEventListener('click', () => ui.container.classList.add('hidden'));
            initDraggable(ui.container, [$('#trix-header'), $('#trix-footer')]);
            ui.container.addEventListener('click', handleContainerClick);
            ui.container.addEventListener('dblclick', handleContainerDblClick);
            const resizeObserver = new ResizeObserver(debounce(saveState, 500));
            resizeObserver.observe(ui.container);

            ui.modal.addEventListener('click', (e) => {
                if (e.target.id === 'trix-modal' || e.target.id === 'trix-modal-close') {
                    ui.modal.classList.remove('visible');
                }
            });
        }

        function handleContainerClick(e) {
            const target = e.target;
            const tabEl = target.closest('.trix-tab');
            if (tabEl && !target.classList.contains('trix-tab-close')) {
                const tabId = isNaN(parseInt(tabEl.dataset.tabId)) ? tabEl.dataset.tabId : parseInt(tabEl.dataset.tabId);
                if (tabId !== state.activeTabId) { state.activeTabId = tabId; renderActiveView(); saveState(); }
            }
            if (target.classList.contains('trix-tab-close')) {
                const tabId = parseInt(tabEl.dataset.tabId, 10);
                state.tabs = state.tabs.filter(t => t.id !== tabId);
                if (state.activeTabId === tabId) state.activeTabId = state.tabs[0].id;
                renderActiveView(); saveState();
            }
            if (target.id === 'trix-new-tab-btn') {
                const newId = Date.now(), newName = `Script ${state.tabs.length}`;
                state.tabs.push({ id: newId, name: newName, code: `// ${newName}` });
                state.activeTabId = newId;
                renderActiveView(); saveState();
            }
            if (target.id === 'trix-execute-btn') executeScript();
            if (target.id === 'trix-clear-btn') {
                 const activeTab = state.tabs.find(t => t.id === state.activeTabId);
                 if (activeTab) { activeTab.code = ''; renderEditor(); saveState(); }
            }
            if (target.id === 'trix-inject-btn') injectPacket();
            if (target.classList.contains('storage-delete')) {
                const row = target.closest('.storage-row');
                const { key, storage } = row.dataset;
                if (confirm(`Delete key "${key}"?`)) {
                    (storage === 'local' ? unsafeWindow.localStorage : unsafeWindow.sessionStorage).removeItem(key);
                    renderStorageView();
                }
            } else if (target.classList.contains('storage-value')) {
                const row = target.closest('.storage-row');
                const { key, storage } = row.dataset;
                const input = document.createElement('input');
                input.type = 'text';
                input.className = 'storage-value-input';
                input.value = target.textContent;
                target.replaceWith(input);
                input.focus();
                const finishEdit = () => {
                    (storage === 'local' ? unsafeWindow.localStorage : unsafeWindow.sessionStorage).setItem(key, input.value);
                    renderStorageView();
                };
                input.addEventListener('blur', finishEdit, { once: true });
                input.addEventListener('keydown', e => { if (e.key === 'Enter') finishEdit(); if(e.key === 'Escape') renderStorageView(); });
            }
            if (target.id === 'trix-toggle-interception-btn') {
                interceptionEnabled = !interceptionEnabled;
                target.classList.toggle('active', interceptionEnabled);
                target.textContent = interceptionEnabled ? 'Interception Enabled' : 'Enable Interception';
            }
            if (target.id === 'trix-forward-all-btn') {
                for (const msg of queuedMessages.values()) { msg.resolve({ action: 'forward', data: msg.data }); }
            }
            if (target.id === 'trix-block-all-btn') {
                for (const msg of queuedMessages.values()) { msg.resolve({ action: 'block' }); }
            }
            const messageItem = target.closest('.interceptor-item');
            if (messageItem) {
                const messageId = messageItem.dataset.messageId;
                const message = queuedMessages.get(messageId);
                if (!message) return;
                if (target.classList.contains('interceptor-forward-btn')) {
                    const editedData = messageItem.querySelector('textarea').value;
                    message.resolve({ action: 'forward', data: editedData });
                } else if (target.classList.contains('interceptor-block-btn')) {
                    message.resolve({ action: 'block' });
                }
            }
            if (target.id === 'trix-ws-connect-btn') connectCustomWs();
            if (target.id === 'trix-ws-send-btn') {
                if (customWs && customWs.readyState === 1) {
                    const message = $('#trix-ws-message').value;
                    customWs.send(message);
                    logToWsClientConsole(message, 'send');
                } else { logToWsClientConsole('Error: Not connected.', 'system'); }
            }
            if (target.id === 'trix-ws-save-msg-btn') {
                const message = $('#trix-ws-message').value;
                if (!message) return;
                const name = prompt('Enter a name for this message:', 'My Message');
                if (name) { state.wsClient.savedMessages[name] = message; saveState(); renderWsSavedMessages(); }
            }
            if (target.id === 'trix-ws-del-msg-btn') {
                const select = $('#trix-ws-saved-messages');
                const name = select.value;
                if (name && confirm(`Delete saved message "${name}"?`)) { delete state.wsClient.savedMessages[name]; saveState(); renderWsSavedMessages(); }
            }
            if (target.id === 'trix-injector-save-msg-btn') {
                const message = $('#trix-injector-message').value;
                if (!message) return;
                const name = prompt('Enter a name for this packet:', 'My Packet');
                if (name) { state.injector.savedMessages[name] = message; saveState(); renderInjectorSavedMessages(); }
            }
            if (target.id === 'trix-injector-del-msg-btn') {
                const select = $('#trix-injector-saved-messages');
                const name = select.value;
                if (name && confirm(`Delete saved packet "${name}"?`)) { delete state.injector.savedMessages[name]; saveState(); renderInjectorSavedMessages(); }
            }
            if (target.id === 'trix-refresh-storage-btn') renderStorageView();
            if (target.id === 'trix-add-storage-btn') {
                const key = prompt('Enter new key:');
                if (key) { const value = prompt(`Enter value for "${key}":`); unsafeWindow.localStorage.setItem(key, value); renderStorageView(); }
            }
            if (target.id === 'trix-clear-log-btn') {
                const log = $('#trix-packet-log');
                if (log) log.innerHTML = '';
                packetLogHistory = []; // Clear the history array
            }
            if (target.id === 'trix-suspend-log-btn') {
                isLoggerSuspended = !isLoggerSuspended;
                target.classList.toggle('suspended', isLoggerSuspended);
                target.textContent = isLoggerSuspended ? 'Resume Log' : 'Suspend Log';
            }
            
            const wsSavedMessagesSelect = target.closest('#trix-ws-saved-messages');
            if(wsSavedMessagesSelect && wsSavedMessagesSelect.value) {
                $('#trix-ws-message').value = state.wsClient.savedMessages[wsSavedMessagesSelect.value];
            }
            const injectorSavedMessagesSelect = target.closest('#trix-injector-saved-messages');
            if(injectorSavedMessagesSelect && injectorSavedMessagesSelect.value) {
                $('#trix-injector-message').value = state.injector.savedMessages[injectorSavedMessagesSelect.value];
            }
        }
        
        function handleContainerDblClick(e) {
            const nameEl = e.target.closest('.trix-tab-name');
            if (!nameEl) return;
            const tabEl = nameEl.closest('.trix-tab');
            const tabId = parseInt(tabEl.dataset.tabId, 10);
            const tab = state.tabs.find(t => t.id === tabId);
            if (!tab || tab.isPermanent) return;
            const input = document.createElement('input');
            input.type = 'text';
            input.className = 'trix-tab-rename-input';
            input.value = tab.name;
            nameEl.replaceWith(input);
            input.focus();
            input.select();
            const finishEditing = () => {
                const newName = input.value.trim();
                if (newName) tab.name = newName;
                saveState();
                renderTabs();
            };
            input.addEventListener('blur', finishEditing, { once: true });
            input.addEventListener('keydown', e => { if (e.key === 'Enter') finishEditing(); else if (e.key === 'Escape') renderTabs(); });
        }

        function initDraggable(container, handles) {
            let isDragging = false, offsetX, offsetY;
            handles.forEach(handle => {
                handle.addEventListener('mousedown', e => {
                    if (e.target.closest('button, input, select, textarea')) return;
                    isDragging = true;
                    offsetX = e.clientX - container.offsetLeft;
                    offsetY = e.clientY - container.offsetTop;
                    container.style.right = 'auto';
                    container.style.bottom = 'auto';
                    document.body.style.userSelect = 'none';
                });
            });
            document.addEventListener('mousemove', e => {
                if (isDragging) {
                    container.style.left = `${e.clientX - offsetX}px`;
                    container.style.top = `${Math.max(0, e.clientY - offsetY)}px`;
                }
            });
            document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; document.body.style.userSelect = ''; saveState(); } });
        }

        function injectPacket() {
            const statusBar = $('#trix-injector-view .trix-status-bar');
            const messageInput = $('#trix-injector-message');
            if (!statusBar || !messageInput) return;

            const ws = unsafeWindow.webSocketManager?.activeSocket;
            if (ws && ws.readyState === 1) {
                const message = messageInput.value;
                ws.send(message);
                logPacketCallback('inject', message);
                statusBar.textContent = `Success: Packet injected at ${new Date().toLocaleTimeString()}`;
            } else {
                statusBar.textContent = 'Error: Not connected to the game server.';
            }
        }

        function executeScript() {
            const statusBar = $('#trix-script-injector-container .trix-status-bar');
            if (!statusBar) return;
            const activeTab = state.tabs.find(t => t.id === state.activeTabId);
            if (!activeTab || !activeTab.code) { statusBar.textContent = 'Nothing to execute.'; return; }
            try {
                new Function(activeTab.code)();
                statusBar.textContent = `Success: Executed '${activeTab.name}' at ${new Date().toLocaleTimeString()}`;
            } catch (error) {
                console.error('TriX Executor Error:', error);
                statusBar.textContent = `Error: ${error.message}`;
            }
        }

        function createPacketElement({ type, data }) {
            const item = document.createElement('div');
            item.className = `packet-item packet-${type}`;
            item.dataset.fullData = data;
        
            const dataSpan = document.createElement('span');
            dataSpan.className = 'packet-data';
            dataSpan.innerHTML = `<span class="packet-meta">[${type.toUpperCase()}]</span> ${data}`;
        
            const actionsDiv = document.createElement('div');
            actionsDiv.className = 'packet-actions';
        
            const copyBtn = document.createElement('button');
            copyBtn.className = 'packet-action-btn';
            copyBtn.textContent = 'Copy';
            copyBtn.onclick = (e) => {
                navigator.clipboard.writeText(data).then(() => {
                    e.target.textContent = 'Copied!';
                    setTimeout(() => e.target.textContent = 'Copy', 1000);
                });
            };
            actionsDiv.appendChild(copyBtn);
        
            if (type === 'receive') {
                const viewBtn = document.createElement('button');
                viewBtn.className = 'packet-action-btn';
                viewBtn.textContent = 'View';
                viewBtn.onclick = () => {
                    $('#trix-modal-body-content').textContent = data;
                    ui.modal.classList.add('visible');
                };
                actionsDiv.appendChild(viewBtn);
            }
        
            item.appendChild(dataSpan);
            item.appendChild(actionsDiv);
            return item;
        }

        function renderFullPacketLog() {
            const logContainer = $('#trix-packet-log');
            if (!logContainer) return;
            logContainer.innerHTML = '';
            const fragment = document.createDocumentFragment();
            packetLogHistory.forEach(packet => {
                fragment.appendChild(createPacketElement(packet));
            });
            logContainer.appendChild(fragment);
            logContainer.scrollTop = logContainer.scrollHeight;
        }

        logPacketCallback = function(type, data) {
            const packet = { type, data };
            packetLogHistory.push(packet);

            if (state.activeTabId === 'logger' && !isLoggerSuspended) {
                const logContainer = $('#trix-packet-log');
                if (logContainer) {
                    const item = createPacketElement(packet);
                    logContainer.appendChild(item);
                    logContainer.scrollTop = logContainer.scrollHeight;
                }
            }
        };
        
        loadState();
        ui = createUI();
        applySettings();
        renderActiveView();
        initEventListeners();
        
        const musicPlayer = initMusicPlayer();
        initDraggable(ui.musicPlayer, [$('.music-drag-handle', ui.musicPlayer)]);

        setInterval(measurePing, 2000);

        let lastFrameTime = performance.now(), frameCount = 0;
        const fpsDisplay = $('#trix-fps-display');
        function updateFPS(now) {
            frameCount++;
            if (now >= lastFrameTime + 1000) {
                if (fpsDisplay) fpsDisplay.textContent = `FPS: ${frameCount}`;
                lastFrameTime = now;
                frameCount = 0;
            }
            requestAnimationFrame(updateFPS);
        }
        requestAnimationFrame(updateFPS);
    }

    function initMusicPlayer() {
        const player = {
            audio: document.createElement('audio'),
            songList: [
                { title: "Animal I Have Become", artist: "Skillet", url: "https://www.dropbox.com/scl/fi/b3lgo2fkz85h0m3nklx3b/Skilet_-_Animal_I_Have_Become_-mp3.pm-1.mp3?rlkey=yok2i5r5in404ili6ozf776px&st=5xmvo7c5&dl=1" },
                { title: "Passo Bem Solto", artist: "ATLXS", url: "https://evzxirgylircpnblikrw.supabase.co/storage/v1/object/public/MOOZIK/ATLXS_-_PASSO_BEM_SOLTO_-_Slowed_@BaseNaija%20(2).mp3" },
                { title: "FROM THE SCREEN TO THE-", artist: "KSI (Audio: Ben4062)", url: "https://www.myinstants.com/media/sounds/from-the-screen-to-the-ring.mp3" },
                { title: "Honeypie", artist: "JAWNY", url: "https://evzxirgylircpnblikrw.supabase.co/storage/v1/object/public/MOOZIK/JAWNY_-_Honeypie_muzonov.net_(mp3.pm).mp3" },
                { title: "Kamin", artist: "EMIN ft. JONY", url: "https://evzxirgylircpnblikrw.supabase.co/storage/v1/object/public/MOOZIK/EMIN_Ft_JONY_-_.mp3" }
            ],
            isPlaying: false,
            currentSongIndex: 0,
            volume: 0.5,
            loop: false,
            autoplay: true
        };

        const $ = (sel) => document.querySelector('#trix-host').shadowRoot.querySelector(sel);
        const playerEl = $('#trix-music-player');
        const titleEl = $('.music-title');
        const artistEl = $('.music-artist');
        const playPauseBtn = $('#music-play-pause-btn');
        const progressBar = $('.music-progress-bar');
        const progressContainer = $('.music-progress-container');
        const currentTimeEl = $('#music-current-time');
        const durationEl = $('#music-duration');
        const volumeSlider = $('#music-volume-slider');
        const volumeIndicator = $('#trix-volume-indicator');
        const loopBtn = $('#music-loop-btn');
        const autoplayBtn = $('#music-autoplay-btn');
        const menuEl = $('#trix-music-menu');
        const songListEl = $('.music-song-list');
        let volumeTimeout;
        
        const updateSpecialEffects = () => {
            playerEl.classList.toggle('rainbow-outline', player.isPlaying);
        };

        const loadSong = (index) => {
            player.currentSongIndex = index;
            const song = player.songList[index];
            player.audio.src = song.url;
            player.audio.loop = player.loop;
            titleEl.textContent = song.title;
            artistEl.textContent = song.artist;
            renderSongList();
            updateSpecialEffects();
        };
        
        const playSong = () => {
            player.isPlaying = true;
            player.audio.play().catch(e => console.error("Audio play failed:", e));
            playPauseBtn.innerHTML = `<svg width="32" height="32" viewBox="0 0 24 24" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>`;
            updateSpecialEffects();
        };
        const pauseSong = () => {
            player.isPlaying = false;
            player.audio.pause();
            playPauseBtn.innerHTML = `<svg width="32" height="32" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>`;
            updateSpecialEffects();
        };
        
        const nextSong = () => { let newIndex = player.currentSongIndex + 1; if (newIndex >= player.songList.length) newIndex = 0; loadSong(newIndex); playSong(); };
        const prevSong = () => { let newIndex = player.currentSongIndex - 1; if (newIndex < 0) newIndex = player.songList.length - 1; loadSong(newIndex); playSong(); };

        const setVolume = (value) => {
            player.volume = value;
            player.audio.volume = value;
            volumeSlider.value = value;
            volumeIndicator.textContent = `${Math.round(value * 100)}%`;
            volumeIndicator.classList.add('visible');
            clearTimeout(volumeTimeout);
            volumeTimeout = setTimeout(() => volumeIndicator.classList.remove('visible'), 1000);
        };
        
        const renderSongList = (filter = '') => {
            songListEl.innerHTML = '';
            const filteredList = player.songList.filter(song => song.title.toLowerCase().includes(filter) || song.artist.toLowerCase().includes(filter));
            filteredList.forEach((song) => {
                const originalIndex = player.songList.indexOf(song);
                const item = document.createElement('div');
                item.className = 'music-song-item';
                if (originalIndex === player.currentSongIndex) item.classList.add('playing');
                item.innerHTML = `<span class="music-song-title">${song.title}</span><span class="music-song-artist">${song.artist}</span>`;
                item.addEventListener('click', () => { loadSong(originalIndex); playSong(); menuEl.classList.remove('visible'); });
                songListEl.appendChild(item);
            });
        };

        $('#trix-music-toggle-btn').addEventListener('click', () => playerEl.classList.toggle('visible'));
        playPauseBtn.addEventListener('click', () => player.isPlaying ? pauseSong() : playSong());
        $('#music-next-btn').addEventListener('click', nextSong);
        $('#music-prev-btn').addEventListener('click', prevSong);
        $('#music-forward-btn').addEventListener('click', () => player.audio.currentTime += 10);
        $('#music-rewind-btn').addEventListener('click', () => player.audio.currentTime -= 10);
        volumeSlider.addEventListener('input', (e) => setVolume(e.target.value));

        loopBtn.addEventListener('click', () => {
            player.loop = !player.loop;
            player.audio.loop = player.loop;
            loopBtn.classList.toggle('active', player.loop);
        });

        autoplayBtn.addEventListener('click', () => {
            player.autoplay = !player.autoplay;
            autoplayBtn.classList.toggle('active', player.autoplay);
        });
        
        player.audio.addEventListener('timeupdate', () => {
            const { currentTime, duration } = player.audio;
            if (duration) {
                progressBar.style.width = `${(currentTime / duration) * 100}%`;
                const formatTime = (s) => `${Math.floor(s/60)}:${String(Math.floor(s%60)).padStart(2,'0')}`;
                currentTimeEl.textContent = formatTime(currentTime);
                durationEl.textContent = formatTime(duration);
            }
        });
        player.audio.addEventListener('ended', () => {
            if (player.autoplay) {
                nextSong();
            } else {
                pauseSong();
                player.audio.currentTime = 0;
            }
        });
        progressContainer.addEventListener('click', (e) => {
            if (!isNaN(player.audio.duration)) {
                player.audio.currentTime = (e.offsetX / progressContainer.clientWidth) * player.audio.duration;
            }
        });
        $('#music-playlist-btn').addEventListener('click', () => menuEl.classList.add('visible'));
        $('#music-search-input').addEventListener('input', (e) => renderSongList(e.target.value.toLowerCase()));
        menuEl.addEventListener('click', (e) => { if(e.target.id === 'trix-music-menu') menuEl.classList.remove('visible'); });

        // Initialize UI states
        setVolume(player.volume);
        autoplayBtn.classList.toggle('active', player.autoplay);
        loadSong(player.currentSongIndex);
        renderSongList();
        
        return player;
    }

    function showLoadingScreen(callback) {
        const loadingScreen = document.createElement('div');
        loadingScreen.id = 'trix-loading-screen';
        const loadingMessages = [
            "Disable all other extensions to reduce disconnections (Error 1009).",
            "the worst it can do is crash your game, right?",
            "Definitely not a cheating tool...",
            "did you know? I didnt know",
            "you can report any harmful scripts to me. (my discord: painsel)",
            "wanna break from the ads?",
            "I'm not sure how some features even work, if you're a developer pls help me (my discord: painsel)"
        ];
        loadingScreen.innerHTML = `
            <div class="loading-content">
                <div class="loading-header">TriX Executor</div>
                <div class="loading-bar-container">
                    <div class="loading-bar-progress"></div>
                </div>
                <div class="loading-percentage">0.0%</div>
                <div class="loading-message">${loadingMessages[0]}</div>
            </div>`;
        document.body.appendChild(loadingScreen);
        
        // Force reflow to enable animation
        setTimeout(() => loadingScreen.classList.add('visible'), 10);

        const progressBar = loadingScreen.querySelector('.loading-bar-progress');
        const percentageText = loadingScreen.querySelector('.loading-percentage');
        const messageText = loadingScreen.querySelector('.loading-message');
        const loadingContent = loadingScreen.querySelector('.loading-content');

        const randomDuration = Math.random() * (50000 - 10000) + 10000;
        let startTime = null;
        let lastMessageChange = 0;
        let currentMessageIndex = 0;

        function loadingLoop(timestamp) {
            if (!startTime) startTime = timestamp;
            const elapsed = timestamp - startTime;
            const progress = Math.min(elapsed / randomDuration, 1);
            
            progressBar.style.width = `${progress * 100}%`;
            percentageText.textContent = `${(progress * 100).toFixed(1)}%`;

            if (elapsed > lastMessageChange + 4000) {
                lastMessageChange = elapsed;
                let newMessageIndex;
                do {
                    newMessageIndex = Math.floor(Math.random() * loadingMessages.length);
                } while (newMessageIndex === currentMessageIndex);
                currentMessageIndex = newMessageIndex;
                messageText.textContent = loadingMessages[currentMessageIndex];
            }

            if (progress < 1) {
                requestAnimationFrame(loadingLoop);
            } else {
                percentageText.textContent = '100.0%';
                setTimeout(() => {
                    loadingContent.classList.add('fade-out');
                    setTimeout(() => {
                        loadingScreen.classList.remove('visible');
                        setTimeout(() => {
                            loadingScreen.remove();
                            callback();
                        }, 1500);
                    }, 1000);
                }, 500);
            }
        }
        requestAnimationFrame(loadingLoop);
    }

    function waitForElement(selector, callback) {
        const maxRetries = 40; const retryInterval = 250; let retryCount = 0;
        const checkInterval = setInterval(() => {
            const element = document.querySelector(selector);
            if (element) {
                clearInterval(checkInterval);
                showLoadingScreen(callback);
            } else {
                retryCount++;
                if (retryCount >= maxRetries) {
                    clearInterval(checkInterval);
                    console.error(`[TriX] Failed to find target element '${selector}' after ${maxRetries} retries. Aborting initialization.`);
                }
            }
        }, retryInterval);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => waitForElement('#canvasA', initialize));
    } else {
        waitForElement('#canvasA', initialize);
    }

})();

QingJ © 2025

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