A powerful, feature-rich executor with a modern UI. Includes multi-tab scripting, a network suite (Logger, Injector, WS Client), GreasyFork integration, and more.
当前为
// ==UserScript==
// @name TriX Executor (Revamped)
// @namespace https://greasyfork.org/en/users/COURTESYCOIL
// @version 5.0.0
// @description A powerful, feature-rich executor with a modern UI. Includes multi-tab scripting, a network suite (Logger, Injector, WS Client), GreasyFork integration, and more.
// @author Painsel
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_getClipboard
// @grant unsafeWindow
// @license MIT
// @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzRmNDZlNSI+PHBhdGggZD0iTTE4LjM2IDIyLjA1bC00LjQyLTQuNDJDMTMuNDcgMTcuODcgMTMgMTguMTcgMTMgMTguNUMxMyAyMC45OSAxNS4wMSAyMyAxNy41IDIzaDMuNTRjLTIuNDUtMS40OC00LjQyLTMuNDUtNS4LTUuOTV6TTggMTNjMS42NiAwIDMuMTgtLjU5IDQuMzgtMS42MkwxMCAxMy41VjIybDIuNS0yLjVMMTggMTMuOTZjLjM1LS40OC42NS0xIC44Ny0xLjU1QzE4LjYxIDEzLjQxIDE4IDEyLjM0IDE4IDEyVjBoLTJ2MTJjMCAuMzQtLjAyLjY3LS4wNiAxLS4zMy4xOC0uNjguMy0xLjA0LjM3LTEuNzMgMC0zLjI3LS45My00LjE2LTIuMzZMNiAxMy42VjVINXY4eiIvPjwvc3ZnPg==
// ==/UserScript==
(function () {
"use strict";
// --- TriX Core: WebSocket Logging & Global State ---
let isLoggerSuspended = false;
let customWs = null;
const monitoredConnections = new Map();
// Callbacks for UI updates
let updateConnectionStatus = () => {};
let updatePingDisplay = () => {};
let logPacketCallback = () => {};
let onConnectionStateChange = () => {};
const OriginalWebSocket = unsafeWindow.WebSocket;
unsafeWindow.WebSocket = function(url, protocols) {
let isGameSocket = false;
try {
const isTerritorial = window.location.hostname.includes('territorial.io');
const urlObj = new URL(url, window.location.href);
const urlString = urlObj.toString();
const proxyParam = urlObj.searchParams.get('u');
isGameSocket = isTerritorial && (urlString.includes('/s52/') || (proxyParam && atob(proxyParam).includes('/s52/')));
} catch (e) { /* Invalid URL, not a game socket */ }
if (!isGameSocket) return new OriginalWebSocket(url, protocols);
console.log(`[TriX] Intercepting WebSocket: ${url}`);
const ws = new OriginalWebSocket(url, protocols);
monitoredConnections.set(ws, { url, state: 'CONNECTING', log: [] });
onConnectionStateChange();
const originalSend = ws.send.bind(ws);
ws.send = function(data) {
logPacketCallback(ws, 'send', data);
return originalSend(data);
};
ws.addEventListener('message', event => {
logPacketCallback(ws, 'receive', event.data);
});
ws.addEventListener('open', () => {
const conn = monitoredConnections.get(ws);
if (conn) conn.state = 'OPEN';
onConnectionStateChange();
updateConnectionStatus('connected', 'Connection Established');
});
ws.addEventListener('close', () => {
monitoredConnections.delete(ws);
onConnectionStateChange();
updateConnectionStatus('disconnected', 'Disconnected');
});
ws.addEventListener('error', () => {
monitoredConnections.delete(ws);
onConnectionStateChange();
updateConnectionStatus('error', 'Connection Error');
});
unsafeWindow.trixSocket = ws;
return ws;
};
// --- Revamped UI: Global variables ---
let isMinimized = false;
let currentTab = "home";
let activeNetworkTab = "logger";
let scriptTabs = [{ id: "tab1", name: "Script 1", content: "// Welcome to the revamped TriX Executor!\nconsole.log('Phoenix Rising!');" }];
let activeScriptTab = "tab1";
// Initialize storage
if (!GM_getValue("scripts", null)) {
GM_setValue("scripts", JSON.stringify([]));
}
if (!GM_getValue("settings", null)) {
GM_setValue("settings", JSON.stringify({
theme: "dark",
antiScam: true,
autoUpdate: true,
}));
}
// CSS Styles
const styles = `
.trix-executor {
position: fixed; top: 50px; right: 50px; width: 650px; height: 500px;
background: linear-gradient(135deg, #1e1e2e 0%, #2d2d44 100%);
border-radius: 12px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); z-index: 999999;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; color: #ffffff;
overflow: hidden; transition: all 0.3s ease; display: flex; flex-direction: column;
}
.trix-header {
background: linear-gradient(90deg, #4f46e5 0%, #7c3aed 100%); height: 40px;
display: flex; justify-content: space-between; align-items: center; padding: 0 15px;
border-radius: 12px 12px 0 0; flex-shrink: 0; cursor: move; user-select: none;
}
.trix-title { font-weight: 600; font-size: 14px; color: white; }
.trix-header-info { display: flex; align-items: center; gap: 15px; font-size: 12px; color: rgba(255,255,255,0.8); }
#trix-conn-status { width: 10px; height: 10px; border-radius: 50%; background-color: #ef4444; transition: background-color 0.3s; }
#trix-conn-status.connected { background-color: #10b981; }
#trix-conn-status.error { background-color: #f59e0b; }
.trix-controls { display: flex; align-items: center; gap: 8px; }
.trix-btn {
width: 20px; height: 20px; border-radius: 4px; border: none; cursor: pointer; font-size: 12px; font-weight: bold;
display: flex; justify-content: center; align-items: center; padding: 0; transition: all 0.2s ease;
}
.minimize-btn { background: #fbbf24; color: #92400e; }
.maximize-btn { background: #10b981; color: #065f46; }
.close-btn { background: #ef4444; color: #991b1b; }
.trix-btn:hover { transform: scale(1.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); }
.trix-body { display: flex; height: 100%; overflow: hidden; }
.trix-sidebar {
width: 150px; background: rgba(0, 0, 0, 0.2); padding: 15px 0;
border-right: 1px solid rgba(255, 255, 255, 0.1); flex-shrink: 0;
}
.sidebar-item {
padding: 12px 20px; cursor: pointer; transition: all 0.2s ease;
font-size: 13px; border-left: 3px solid transparent;
}
.sidebar-item:hover { background: rgba(255, 255, 255, 0.1); border-left-color: #4f46e5; }
.sidebar-item.active { background: rgba(79, 70, 229, 0.3); border-left-color: #4f46e5; }
.trix-content { flex: 1; padding: 20px; overflow-y: auto; }
.content-section { display: none; }
.content-section.active { display: block; }
/* --- Component Styles --- */
.script-input, .network-textarea {
width: 100%; height: 200px; background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 8px; padding: 15px;
color: white; font-family: 'Courier New', monospace; font-size: 12px; resize: vertical; outline: none; box-sizing: border-box;
}
.script-tabs, .network-tabs { display: flex; gap: 5px; margin-bottom: 10px; flex-wrap: wrap; }
.script-tab, .network-tab {
background: rgba(0, 0, 0, 0.3); border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px; padding: 6px 12px; font-size: 11px; cursor: pointer;
transition: all 0.2s ease; position: relative;
}
.script-tab.active, .network-tab.active { background: rgba(79, 70, 229, 0.5); border-color: #4f46e5; }
.add-tab-btn {
background: rgba(16, 185, 129, 0.3); border: 1px solid #10b981; color: #10b981;
border-radius: 6px; padding: 6px 12px; font-size: 11px; cursor: pointer; transition: all 0.2s ease;
}
.executor-buttons { display: flex; gap: 10px; margin-top: 15px; flex-wrap: wrap; }
.exec-btn {
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); border: none;
border-radius: 6px; padding: 10px 20px; color: white; font-size: 12px;
font-weight: 600; cursor: pointer; transition: all 0.2s ease;
}
.exec-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(79, 70, 229, 0.4); }
.exec-btn.secondary { background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%); }
.exec-btn.success { background: linear-gradient(135deg, #10b981 0%, #059669 100%); }
.exec-btn.danger { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); }
.search-input {
width: 100%; background: rgba(0, 0, 0, 0.3); border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px; padding: 12px; color: white; font-size: 13px; margin-bottom: 15px; outline: none; box-sizing: border-box;
}
.script-cards { display: grid; gap: 15px; max-height: 300px; overflow-y: auto; }
.script-card {
background: rgba(0, 0, 0, 0.3); border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px; padding: 15px; cursor: pointer; transition: all 0.2s ease;
}
.script-card:hover { background: rgba(255, 255, 255, 0.1); transform: translateY(-2px); }
.card-title { font-weight: 600; margin-bottom: 5px; color: #818cf8; }
.card-description { font-size: 12px; color: rgba(255, 255, 255, 0.7); }
.settings-group { margin-bottom: 20px; }
.settings-label { display: block; margin-bottom: 8px; font-weight: 600; font-size: 13px; }
.settings-input, .settings-select {
width: 100%; background: rgba(0, 0, 0, 0.3); border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px; padding: 10px; color: white; font-size: 12px; outline: none; box-sizing: border-box;
}
.notification {
position: fixed; top: 20px; right: 20px; background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white; padding: 12px 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 1000000; font-size: 13px; font-weight: 600; opacity: 0;
transform: translateX(100%); transition: all 0.3s ease;
}
.notification.show { opacity: 1; transform: translateX(0); }
.minimized-icon {
position: fixed; width: 50px; height: 50px; background-color: #1e1e2e;
border: 3px solid #4f46e5; border-radius: 12px; cursor: pointer; z-index: 999999;
transition: all 0.2s ease; box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
display: flex; justify-content: center; align-items: center; font-size: 24px; font-weight: bold; color: #818cf8; user-select: none;
}
.minimized-icon:hover { transform: scale(1.05); box-shadow: 0 12px 35px rgba(79, 70, 229, 0.4); }
/* Network Tab Specific Styles */
.network-view-content { margin-top: 15px; height: calc(100% - 50px); display: flex; flex-direction: column; }
.packet-log { height: 100%; overflow-y: auto; background: rgba(0,0,0,0.2); padding: 10px; border-radius: 8px; font-family: 'Courier New', monospace; font-size: 12px; }
.log-item { padding: 4px; border-bottom: 1px solid rgba(255,255,255,0.1); word-break: break-all; }
.log-item.send { color: #6ee7b7; } .log-item.send-forwarded { color: #a7f3d0; }
.log-item.receive { color: #f0abfc; } .log-item.receive-forwarded { color: #f5d0fe; }
.log-item.send-blocked, .log-item.receive-blocked { color: #fda4af; text-decoration: line-through; }
.storage-table { display: grid; grid-template-columns: 150px 1fr auto; gap: 10px; align-items: center; font-size: 12px; padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.1); }
.storage-key { color: #f0abfc; font-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.storage-value { color: #6ee7b7; word-break: break-all; }
/* Scrollbar Styling */
.trix-content::-webkit-scrollbar, .script-cards::-webkit-scrollbar, .packet-log::-webkit-scrollbar { width: 8px; }
.trix-content::-webkit-scrollbar-track, .script-cards::-webkit-scrollbar-track, .packet-log::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.2); border-radius: 4px; }
.trix-content::-webkit-scrollbar-thumb, .script-cards::-webkit-scrollbar-thumb, .packet-log::-webkit-scrollbar-thumb { background: rgba(79, 70, 229, 0.6); border-radius: 4px; }
.trix-content::-webkit-scrollbar-thumb:hover, .script-cards::-webkit-scrollbar-thumb:hover, .packet-log::-webkit-scrollbar-thumb:hover { background: rgba(79, 70, 229, 0.8); }
`;
const styleSheet = document.createElement("style");
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);
// --- Notification System ---
function showNotification(message, type = "success") {
const notification = document.createElement("div");
notification.className = "notification";
notification.textContent = message;
if (type === "error") {
notification.style.background = "linear-gradient(135deg, #ef4444 0%, #dc2626 100%)";
}
document.body.appendChild(notification);
setTimeout(() => notification.classList.add("show"), 100);
setTimeout(() => {
notification.classList.remove("show");
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// --- UI Creation ---
function createExecutorPanel() {
const panel = document.createElement("div");
panel.className = "trix-executor";
panel.innerHTML = `
<div class="trix-header">
<div class="trix-title">TriX Executor v3.1</div>
<div class="trix-header-info">
<div id="trix-conn-status" title="Disconnected"></div>
<span id="trix-ping-display">Ping: ---</span>
<span id="trix-fps-display">FPS: --</span>
</div>
<div class="trix-controls">
<button class="trix-btn minimize-btn" title="Minimize">−</button>
<button class="trix-btn maximize-btn" title="Maximize">□</button>
<button class="trix-btn close-btn" title="Close">×</button>
</div>
</div>
<div class="trix-body">
<div class="trix-sidebar">
<div class="sidebar-item active" data-tab="home">🏠 Home</div>
<div class="sidebar-item" data-tab="main">⚡ Main</div>
<div class="sidebar-item" data-tab="network">📡 Network</div>
<div class="sidebar-item" data-tab="cloud">☁ Cloud</div>
<div class="sidebar-item" data-tab="settings">⚙ Settings</div>
</div>
<div class="trix-content">
${createHomeContent()}
${createMainContent()}
${createNetworkContent()}
${createCloudContent()}
${createSettingsContent()}
</div>
</div>
`;
document.body.appendChild(panel);
return panel;
}
// --- Content Section Creators ---
function createHomeContent() {
return `<div class="content-section active" id="home-content">
<h2>Script Store</h2>
<input type="text" class="search-input" placeholder="Search saved scripts..." id="script-search">
<div class="script-cards" id="saved-scripts"></div>
</div>`;
}
function createMainContent() {
return `<div class="content-section" id="main-content">
<h2>Live JS Executor</h2>
<div class="script-tabs" id="script-tabs"></div>
<textarea class="script-input" placeholder="Enter Script Here..." id="script-editor"></textarea>
<div class="executor-buttons">
<button class="exec-btn" id="execute-btn">Execute</button>
<button class="exec-btn secondary" id="execute-clipboard-btn">Execute Clipboard</button>
<button class="exec-btn success" id="save-script-btn">Save Script</button>
</div>
</div>`;
}
function createCloudContent() {
return `<div class="content-section" id="cloud-content">
<h2>GreasyFork Scripts</h2>
<input type="text" class="search-input" placeholder="Search GreasyFork..." id="greasyfork-search">
<div class="script-cards" id="cloud-results"></div>
</div>`;
}
function createSettingsContent() {
const settings = JSON.parse(GM_getValue("settings"));
return `<div class="content-section" id="settings-content">
<h2>Settings</h2>
<div class="settings-group">
<label class="settings-label">Theme</label>
<select class="settings-select" id="theme-select" disabled>
<option value="dark" ${settings.theme === "dark" ? "selected" : ""}>Dark (Default)</option>
</select>
</div>
<div class="settings-group">
<label class="settings-label">
<input type="checkbox" class="settings-checkbox" id="anti-scam" ${settings.antiScam ? "checked" : ""}>
Anti-Scam Protection (confirm potentially harmful scripts)
</label>
</div>
<div class="settings-group">
<label class="settings-label">
<input type="checkbox" class="settings-checkbox" id="auto-update" ${settings.autoUpdate ? "checked" : ""}>
Auto Update Scripts
</label>
</div>
</div>`;
}
function createNetworkContent() {
return `<div class="content-section" id="network-content">
<div class="network-tabs">
<div class="network-tab active" data-net-tab="logger">Logger</div>
<div class="network-tab" data-net-tab="injector">Injector</div>
<div class="network-tab" data-net-tab="ws_client">WS Client</div>
<div class="network-tab" data-net-tab="storage">Storage</div>
</div>
<div class="network-view-content"></div>
</div>`;
}
// --- Network View Renderers ---
function renderActiveNetworkView() {
const container = document.querySelector("#network-content .network-view-content");
if (!container) return;
container.innerHTML = "";
switch (activeNetworkTab) {
case "logger":
container.innerHTML = `<div class="executor-buttons" style="margin-top:0; margin-bottom:10px;">
<button class="exec-btn" id="suspend-log-btn">Suspend Log</button>
<button class="exec-btn secondary" id="clear-log-btn">Clear Log</button>
</div>
<div class="packet-log" id="packet-log-output">Waiting for connection...</div>`;
renderPacketLog();
break;
case "injector":
container.innerHTML = `<h3>Packet Injector</h3>
<p style="font-size:12px; color: #ccc; margin-bottom:10px;">Send a custom packet to the current game connection.</p>
<textarea class="network-textarea" id="injector-input" placeholder='e.g., 42["chat","Hello from TriX!"]' style="height: 150px;"></textarea>
<div class="executor-buttons">
<button class="exec-btn" id="inject-packet-btn">Inject Packet</button>
</div>`;
break;
case "ws_client":
container.innerHTML = `<h3>WebSocket Client</h3>
<input type="text" id="ws-client-url" class="search-input" placeholder="wss://your-socket-url.com">
<div class="executor-buttons" style="margin-top:0;">
<button class="exec-btn" id="ws-client-connect-btn">Connect</button>
</div>
<div class="packet-log" id="ws-client-log" style="height: 150px; margin-top:10px;"></div>
<textarea class="network-textarea" id="ws-client-input" placeholder="Message to send..." style="height: 80px; margin-top:10px;"></textarea>
<div class="executor-buttons">
<button class="exec-btn" id="ws-client-send-btn">Send</button>
</div>`;
break;
case "storage":
container.innerHTML = `<h3>Storage Explorer</h3>
<h4>Local Storage</h4>
<div id="local-storage-view"></div>
<h4 style="margin-top:20px;">Session Storage</h4>
<div id="session-storage-view"></div>`;
renderStorageView();
break;
}
}
function renderPacketLog() {
const logContainer = document.getElementById("packet-log-output");
if (!logContainer) return;
const activeConnection = Array.from(monitoredConnections.values()).find(c => c.state === 'OPEN' || c.log.length > 0);
if (!activeConnection) {
logContainer.textContent = 'Waiting for active connection...';
return;
}
if (isLoggerSuspended) return;
logContainer.innerHTML = '';
activeConnection.log.forEach(packet => {
const item = document.createElement('div');
item.className = `log-item ${packet.type}`;
item.textContent = `[${packet.type.toUpperCase()}] ${packet.data}`;
logContainer.appendChild(item);
});
logContainer.scrollTop = logContainer.scrollHeight;
}
function renderStorageView() { /* ... unchanged ... */ }
// --- Panel Management & Draggability ---
function minimizePanel() { /* ... unchanged ... */ }
function restorePanel() { /* ... unchanged ... */ }
function createMinimizedIcon() { /* ... unchanged ... */ }
function closePanel() { /* ... unchanged ... */ }
// --- Tab Management ---
function switchTab(tabName) { /* ... unchanged ... */ }
function updateScriptTabs() { /* ... unchanged ... */ }
function switchScriptTab(tabId) { /* ... unchanged ... */ }
function addScriptTab() { /* ... unchanged ... */ }
function renameScriptTab(tabId) { /* ... unchanged ... */ }
// --- Script Logic ---
function executeScript(code) { /* ... unchanged ... */ }
function saveScript() { /* ... unchanged ... */ }
function loadSavedScripts() { /* ... unchanged ... */ }
// --- Cloud Logic ---
function searchGreasyFork(query) { /* ... unchanged ... */ }
// --- FULL IMPLEMENTATIONS OF UNCHANGED FUNCTIONS ---
function createMinimizedIcon() {
const icon = document.createElement("div");
icon.className = "minimized-icon";
icon.title = "TriX Executor";
icon.style.top = "50px";
icon.style.right = "50px";
icon.textContent = "T";
document.body.appendChild(icon);
let isDraggingIcon = false, dragOffsetIcon = { x: 0, y: 0 };
icon.addEventListener("mousedown", e => {
isDraggingIcon = true;
const rect = icon.getBoundingClientRect();
dragOffsetIcon = { x: e.clientX - rect.left, y: e.clientY - rect.top };
e.preventDefault();
});
document.addEventListener("mousemove", e => {
if (isDraggingIcon) {
const x = e.clientX - dragOffsetIcon.x;
const y = e.clientY - dragOffsetIcon.y;
icon.style.left = Math.max(0, Math.min(x, window.innerWidth - icon.offsetWidth)) + "px";
icon.style.top = Math.max(0, Math.min(y, window.innerHeight - icon.offsetHeight)) + "px";
icon.style.right = "auto";
}
});
document.addEventListener("mouseup", e => {
if (isDraggingIcon) {
isDraggingIcon = false;
if (e.target === icon) restorePanel();
}
});
return icon;
}
function minimizePanel() {
const panel = document.querySelector(".trix-executor");
if (panel) { panel.style.display = "none"; isMinimized = true; createMinimizedIcon(); }
}
function restorePanel() {
const panel = document.querySelector(".trix-executor");
const icon = document.querySelector(".minimized-icon");
if (panel) panel.style.display = "flex";
if (icon) icon.remove();
isMinimized = false;
}
function closePanel() {
const panel = document.querySelector(".trix-executor");
if (panel) panel.remove();
}
function switchTab(tabName) {
document.querySelectorAll(".sidebar-item").forEach(item => item.classList.remove("active"));
document.querySelector(`[data-tab="${tabName}"]`).classList.add("active");
document.querySelectorAll(".content-section").forEach(section => section.classList.remove("active"));
document.getElementById(`${tabName}-content`).classList.add("active");
currentTab = tabName;
if (tabName === "home") loadSavedScripts();
else if (tabName === "main") updateScriptTabs();
else if (tabName === "network") renderActiveNetworkView();
}
function updateScriptTabs() {
const tabsContainer = document.getElementById("script-tabs");
tabsContainer.innerHTML = "";
scriptTabs.forEach((tab) => {
const tabElement = document.createElement("div");
tabElement.className = `script-tab ${tab.id === activeScriptTab ? "active" : ""}`;
tabElement.textContent = tab.name;
tabElement.onclick = () => switchScriptTab(tab.id);
tabElement.ondblclick = () => renameScriptTab(tab.id);
tabsContainer.appendChild(tabElement);
});
const addButton = document.createElement("div");
addButton.className = "add-tab-btn";
addButton.textContent = "+";
addButton.onclick = addScriptTab;
tabsContainer.appendChild(addButton);
const activeTab = scriptTabs.find((tab) => tab.id === activeScriptTab);
if (activeTab) {
document.getElementById("script-editor").value = activeTab.content;
}
}
function switchScriptTab(tabId) {
const currentTab = scriptTabs.find((tab) => tab.id === activeScriptTab);
if (currentTab) {
currentTab.content = document.getElementById("script-editor").value;
}
activeScriptTab = tabId;
updateScriptTabs();
}
function addScriptTab() {
const newId = "tab" + Date.now();
const newTab = { id: newId, name: `Script ${scriptTabs.length + 1}`, content: "" };
scriptTabs.push(newTab);
activeScriptTab = newId;
updateScriptTabs();
showNotification("New tab created");
}
function renameScriptTab(tabId) {
const tab = scriptTabs.find((t) => t.id === tabId);
if (tab) {
const newName = prompt("Enter new tab name:", tab.name);
if (newName && newName.trim()) {
tab.name = newName.trim();
updateScriptTabs();
showNotification("Tab renamed");
}
}
}
function executeScript(code) {
try {
const settings = JSON.parse(GM_getValue("settings"));
if (settings.antiScam) {
const suspiciousPatterns = [/document\.cookie/i, /localStorage\./i, /sessionStorage\./i, /\.send\(/i, /fetch\(/i, /XMLHttpRequest/i];
if (suspiciousPatterns.some((pattern) => pattern.test(code)) && !confirm("This script contains potentially suspicious code that could access your data. Are you sure you want to execute it?")) {
showNotification("Execution cancelled by user.", "error");
return;
}
}
(new Function(code))();
showNotification("Script executed successfully");
} catch (error) {
showNotification(`Execution error: ${error.message}`, "error");
console.error("[TriX] Script execution error:", error);
}
}
function saveScript() {
const code = document.getElementById("script-editor").value;
if (!code.trim()) {
showNotification("No script to save", "error");
return;
}
const name = prompt("Enter script name:");
if (!name || !name.trim()) return;
const scripts = JSON.parse(GM_getValue("scripts"));
const newScript = { id: Date.now().toString(), name: name.trim(), code: code, created: new Date().toISOString() };
scripts.push(newScript);
GM_setValue("scripts", JSON.stringify(scripts));
showNotification("Script saved successfully");
if (currentTab === "home") loadSavedScripts();
}
function loadSavedScripts() {
const scripts = JSON.parse(GM_getValue("scripts"));
const container = document.getElementById("saved-scripts");
container.innerHTML = "";
scripts.forEach((script) => {
const card = document.createElement("div");
card.className = "script-card";
card.innerHTML = `<div class="card-title">${script.name}</div>
<div class="card-description">Created: ${new Date(script.created).toLocaleDateString()}</div>
<button class="exec-btn danger delete-btn" style="margin-top: 10px; padding: 5px 10px; font-size: 11px;">Delete</button>`;
card.addEventListener("click", (e) => {
if (e.target.classList.contains("delete-btn")) return;
switchTab("main");
const newTab = { id: `loaded-${script.id}`, name: script.name, content: script.code };
const existingTab = scriptTabs.find(t => t.id === newTab.id);
if(!existingTab) scriptTabs.push(newTab);
activeScriptTab = newTab.id;
updateScriptTabs();
showNotification(`Script "${script.name}" loaded`);
});
card.querySelector(".delete-btn").addEventListener("click", (e) => {
e.stopPropagation();
if (confirm(`Are you sure you want to delete "${script.name}"?`)) {
const updatedScripts = scripts.filter((s) => s.id !== script.id);
GM_setValue("scripts", JSON.stringify(updatedScripts));
showNotification("Script deleted");
loadSavedScripts();
}
});
container.appendChild(card);
});
if (scripts.length === 0) {
container.innerHTML = '<div style="text-align: center; color: rgba(255,255,255,0.5); padding: 40px;">No saved scripts</div>';
}
}
function renderStorageView() {
const lsView = document.getElementById('local-storage-view');
const ssView = document.getElementById('session-storage-view');
if (!lsView || !ssView) return;
const createTable = (storage, type) => {
let html = '';
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
const value = storage.getItem(key);
html += `<div class="storage-table">
<div class="storage-key" title="${key}">${key}</div>
<div class="storage-value" title="${value}">${value}</div>
<button class="exec-btn danger storage-delete-btn" data-type="${type}" data-key="${key}" style="padding: 5px 8px; font-size:10px;">X</button>
</div>`;
}
return html || '<div style="font-size:12px; color:#888; padding: 10px 0;">Empty</div>';
};
lsView.innerHTML = createTable(localStorage, 'local');
ssView.innerHTML = createTable(sessionStorage, 'session');
}
async function searchGreasyFork(query) {
if (!query.trim()) return;
const resultsContainer = document.getElementById("cloud-results");
resultsContainer.innerHTML = '<div style="text-align:center; padding:20px;">Searching...</div>';
try {
const response = await fetch(`https://greasyfork.org/en/scripts.json?q=${encodeURIComponent(query)}`);
const scripts = await response.json();
resultsContainer.innerHTML = "";
if (!scripts.length) {
resultsContainer.innerHTML = '<div style="text-align:center; padding:20px; color: rgba(255,255,255,0.5);">No scripts found</div>';
return;
}
scripts.slice(0, 20).forEach(script => {
const item = document.createElement("div");
item.className = "script-card";
item.innerHTML = `<div class="card-title">${script.name}</div>
<div class="card-description">${script.description}</div>
<div style="font-size:11px; color: rgba(255,255,255,0.6); margin-top:8px;">By ${script.users[0].name} • ${script.total_installs} installs</div>
<button class="exec-btn success" data-url="${script.code_url}" data-name="${script.name}" style="margin-top:10px; padding: 5px 10px; font-size:11px;">Load Script</button>`;
item.querySelector("button").onclick = async (e) => {
showNotification("Loading script...");
const codeRes = await fetch(e.target.dataset.url);
const code = await codeRes.text();
switchTab("main");
addScriptTab();
const activeTab = scriptTabs.find(t => t.id === activeScriptTab);
activeTab.name = e.target.dataset.name;
activeTab.content = code;
updateScriptTabs();
showNotification("Script loaded into new tab!");
};
resultsContainer.appendChild(item);
});
} catch (err) {
resultsContainer.innerHTML = '<div style="text-align:center; padding:20px; color:red;">Failed to fetch scripts</div>';
}
}
// --- Initialization ---
function init() {
const panel = createExecutorPanel();
// Event Delegation for the whole panel
panel.addEventListener('click', async e => {
// Header Controls
if (e.target.matches('.minimize-btn')) minimizePanel();
if (e.target.matches('.maximize-btn')) {
if (panel.style.width === "100vw") {
Object.assign(panel.style, { width: "650px", height: "500px", top: "50px", left: "auto", right: "50px" });
} else {
Object.assign(panel.style, { width: "100vw", height: "100vh", top: "0", left: "0", right: "auto" });
}
}
if (e.target.matches('.close-btn')) closePanel();
// Main Tab
if (e.target.matches('#execute-btn')) executeScript(document.getElementById("script-editor").value);
if (e.target.matches('#execute-clipboard-btn')) {
try {
const text = await GM_getClipboard();
if (text && text.trim()) executeScript(text);
else showNotification("Clipboard is empty", "error");
} catch (err) { showNotification("Failed to read clipboard", "error"); }
}
if (e.target.matches('#save-script-btn')) saveScript();
// Network Tab
if (e.target.matches('#suspend-log-btn')) {
isLoggerSuspended = !isLoggerSuspended;
e.target.textContent = isLoggerSuspended ? "Resume Log" : "Suspend Log";
e.target.classList.toggle('secondary', isLoggerSuspended);
}
if (e.target.matches('#clear-log-btn')) {
const activeConnection = Array.from(monitoredConnections.values()).find(c => c.state === 'OPEN');
if(activeConnection) activeConnection.log = [];
renderPacketLog();
}
if (e.target.matches('#inject-packet-btn')) {
if(unsafeWindow.trixSocket && unsafeWindow.trixSocket.readyState === 1) {
unsafeWindow.trixSocket.send(document.getElementById("injector-input").value);
showNotification("Packet injected");
} else {
showNotification("Not connected to game server.", "error");
}
}
if (e.target.matches('.storage-delete-btn')) {
const { type, key } = e.target.dataset;
const storage = type === 'local' ? localStorage : sessionStorage;
if (confirm(`Delete "${key}" from ${type} storage?`)) {
storage.removeItem(key);
renderStorageView();
}
}
// Sidebar
if (e.target.matches('.sidebar-item')) switchTab(e.target.dataset.tab);
// Network Sub-tabs
if (e.target.matches('.network-tab')) {
panel.querySelectorAll('.network-tab').forEach(t => t.classList.remove('active'));
e.target.classList.add('active');
activeNetworkTab = e.target.dataset.netTab;
renderActiveNetworkView();
}
});
// Input/Change events
panel.addEventListener('input', e => {
if (e.target.matches('#script-editor')) {
const activeTab = scriptTabs.find(tab => tab.id === activeScriptTab);
if (activeTab) activeTab.content = e.target.value;
}
if (e.target.matches('#script-search')) {
const query = e.target.value.toLowerCase();
panel.querySelectorAll("#saved-scripts .script-card").forEach(card => {
const title = card.querySelector(".card-title").textContent.toLowerCase();
card.style.display = title.includes(query) ? "block" : "none";
});
}
});
panel.addEventListener('change', e => {
if (e.target.matches('#settings-content input, #settings-content select')) {
const settings = { theme: "dark", antiScam: document.getElementById("anti-scam").checked, autoUpdate: document.getElementById("auto-update").checked };
GM_setValue("settings", JSON.stringify(settings));
showNotification("Settings saved");
}
});
panel.addEventListener('keypress', e => {
if (e.target.matches('#greasyfork-search') && e.key === 'Enter') {
searchGreasyFork(e.target.value);
}
});
// Draggability
let isDragging = false, dragOffset = { x: 0, y: 0 };
panel.querySelector(".trix-header").onmousedown = (e) => {
if (e.target.classList.contains("trix-btn")) return;
isDragging = true;
const rect = panel.getBoundingClientRect();
dragOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top };
e.preventDefault();
};
document.onmousemove = (e) => {
if (isDragging && !isMinimized) {
const x = e.clientX - dragOffset.x;
const y = e.clientY - dragOffset.y;
panel.style.left = Math.max(0, Math.min(x, window.innerWidth - panel.offsetWidth)) + "px";
panel.style.top = Math.max(0, Math.min(y, window.innerHeight - panel.offsetHeight)) + "px";
panel.style.right = "auto";
}
};
document.onmouseup = () => { isDragging = false; };
// Keyboard shortcuts
document.onkeydown = (e) => {
if (e.ctrlKey && e.shiftKey && e.key === "E") {
e.preventDefault();
const panelExists = document.querySelector('.trix-executor');
if (!panelExists) init();
else if (isMinimized) restorePanel();
else minimizePanel();
}
};
// Connect TriX callbacks
logPacketCallback = (ws, type, data) => {
const conn = monitoredConnections.get(ws);
if (conn) {
conn.log.push({ type, data });
if (conn.log.length > 200) conn.log.shift(); // Limit log size
if (currentTab === 'network' && activeNetworkTab === 'logger' && !isLoggerSuspended) {
renderPacketLog();
}
}
};
onConnectionStateChange = () => {
if(currentTab === 'network' && activeNetworkTab === 'logger') {
renderPacketLog();
}
};
updateConnectionStatus = (status, title) => {
const el = document.getElementById('trix-conn-status');
if(el) { el.className = status; el.title = title; }
};
updatePingDisplay = async () => {
const el = document.getElementById('trix-ping-display');
if (!el) return;
const startTime = performance.now();
try {
await fetch(`${window.location.protocol}//${window.location.host}/favicon.ico?_=${Date.now()}`, { method: 'HEAD', cache: 'no-store' });
el.textContent = `Ping: ${Math.round(performance.now() - startTime)}`;
} catch {
el.textContent = 'Ping: ---';
}
};
setInterval(updatePingDisplay, 5000);
let lastFrameTime = performance.now(), frameCount = 0;
const fpsDisplay = document.getElementById('trix-fps-display');
function updateFPS(now) {
frameCount++;
if (now >= lastFrameTime + 1000) {
if(fpsDisplay) fpsDisplay.textContent = `FPS: ${frameCount}`;
lastFrameTime = now;
frameCount = 0;
}
requestAnimationFrame(updateFPS);
}
if (fpsDisplay) requestAnimationFrame(updateFPS);
// Initial load
loadSavedScripts();
updateScriptTabs();
showNotification("TriX Executor loaded! Press Ctrl+Shift+E to toggle.");
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();