Aternos Universal Plugin Loader

Universal loader for plugins, mods, and custom scripts on Aternos

// ==UserScript==
// @name         Aternos Universal Plugin Loader
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  Universal loader for plugins, mods, and custom scripts on Aternos
// @author       You
// @match        https://aternos.org/*
// @match        https://*.aternos.org/*
// @grant        GM.xmlHttpRequest
// @grant        GM.addStyle
// @grant        GM.notification
// @grant        GM.info
// @grant        GM.setValue
// @grant        GM.getValue
// @connect      *
// @run-at       document-end
// @license      MIT
// ==/UserScript==

class UniversalPluginLoader {
    constructor() {
        this.pluginsLoaded = 0;
        this.totalPlugins = 0;
        this.isInitialized = false;
        this.pluginRegistry = new Map();
        
        this.config = {
            timeout: 30000,
            retryAttempts: 2,
            retryDelay: 1000,
            sequentialLoading: true,
            enablePluginManager: true,
            autoUpdateCheck: false
        };
        
        // Enhanced plugin configuration with types
        this.plugins = Object.freeze([
            // === PLUGIN TYPES EXAMPLES ===
            
            // 1. JAVASCRIPT PLUGINS (Most common)
            {
                name: "Custom Chat Enhancer",
                url: "https://example.com/plugins/chat-enhancer.js",
                type: "script",
                enabled: true,
                description: "Enhances chat functionality"
            },
            
            // 2. CSS/THEME PLUGINS
            {
                name: "Dark Theme",
                url: "https://example.com/themes/dark-theme.css",
                type: "style",
                enabled: true,
                description: "Dark theme for Aternos"
            },
            
            // 3. JSON CONFIGURATION PLUGINS
            {
                name: "Server Settings",
                url: "https://example.com/configs/server-settings.json",
                type: "config",
                enabled: true,
                description: "Custom server configurations"
            },
            
            // 4. HTML WIDGET PLUGINS
            {
                name: "Status Widget",
                url: "https://example.com/widgets/status-widget.html",
                type: "widget",
                enabled: true,
                description: "Server status widget"
            },
            
            // 5. EXTERNAL LIBRARIES
            {
                name: "Utility Library",
                url: "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js",
                type: "library",
                enabled: true,
                description: "Utility functions library"
            },
            
            // 6. CUSTOM ELEMENTS
            {
                name: "Quick Actions",
                url: "https://example.com/plugins/quick-actions.js",
                type: "custom-element",
                enabled: true,
                description: "Adds quick action buttons"
            }
            
            // Add your actual plugin URLs here
            // Example for Minecraft server plugins:
            // {
            //     name: "EssentialsX Helper",
            //     url: "https://raw.githubusercontent.com/your-repo/essentials-helper/main/plugin.js",
            //     type: "script",
            //     enabled: true
            // }
        ]);
        
        this.bindMethods();
    }

    bindMethods() {
        this.init = this.init.bind(this);
        this.handleError = this.handleError.bind(this);
        this.handlePromiseRejection = this.handlePromiseRejection.bind(this);
        this.showStatusNotification = this.showStatusNotification.bind(this);
        this.togglePlugin = this.togglePlugin.bind(this);
        this.openPluginManager = this.openPluginManager.bind(this);
    }

    async init() {
        if (this.isInitialized) {
            this.log('Loader already initialized', 'warn');
            return;
        }

        try {
            this.isInitialized = true;
            await this.waitForDOMReady();
            await this.waitForAternosInterface();
            await this.loadPlugins();
            await this.injectPluginManager();
            this.setupErrorHandling();
            this.log('Universal Plugin Loader initialized successfully', 'success');
        } catch (error) {
            this.handleError('Initialization failed', error);
        }
    }

    waitForDOMReady() {
        return new Promise((resolve) => {
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', resolve, { once: true });
            } else {
                resolve();
            }
        });
    }

    waitForAternosInterface() {
        const aternosSelectors = [
            '.server-status',
            '.navigation',
            '[class*="aternos"]',
            '[id*="aternos"]',
            'body'
        ];

        return new Promise((resolve) => {
            const checkInterface = () => {
                const isLoaded = aternosSelectors.some(selector => {
                    try {
                        return document.querySelector(selector);
                    } catch (e) {
                        return false;
                    }
                });
                
                if (isLoaded) {
                    resolve();
                } else {
                    setTimeout(checkInterface, 500);
                }
            };
            
            checkInterface();
            setTimeout(resolve, 10000);
        });
    }

    async loadPlugins() {
        if (!this.plugins || !Array.isArray(this.plugins)) {
            this.log('Invalid plugins configuration', 'error');
            return;
        }

        const enabledPlugins = this.plugins.filter(plugin => 
            plugin && plugin.enabled !== false && plugin.url
        );

        this.totalPlugins = enabledPlugins.length;
        
        if (this.totalPlugins === 0) {
            this.log('No enabled plugins configured', 'info');
            return;
        }

        this.log(`Loading ${this.totalPlugins} plugins...`, 'info');

        try {
            if (this.config.sequentialLoading) {
                for (const [index, plugin] of enabledPlugins.entries()) {
                    await this.loadSinglePlugin(plugin, index);
                    await this.delay(100);
                }
            } else {
                const concurrencyLimit = 3;
                for (let i = 0; i < enabledPlugins.length; i += concurrencyLimit) {
                    const chunk = enabledPlugins.slice(i, i + concurrencyLimit);
                    await Promise.allSettled(
                        chunk.map((plugin, index) => 
                            this.loadSinglePlugin(plugin, i + index)
                        )
                    );
                }
            }

            this.log(`Completed loading ${this.pluginsLoaded}/${this.totalPlugins} plugins`, 'success');
        } catch (error) {
            this.handleError('Plugin loading process failed', error);
        }
    }

    async loadSinglePlugin(plugin, index) {
        try {
            this.log(`Loading plugin ${index + 1}/${this.totalPlugins}: ${plugin.name}`, 'info');
            
            switch (plugin.type) {
                case 'script':
                case 'library':
                case 'custom-element':
                    await this.loadScriptPlugin(plugin);
                    break;
                    
                case 'style':
                case 'theme':
                    await this.loadStylePlugin(plugin);
                    break;
                    
                case 'config':
                    await this.loadConfigPlugin(plugin);
                    break;
                    
                case 'widget':
                    await this.loadWidgetPlugin(plugin);
                    break;
                    
                default:
                    // Auto-detect type by extension
                    await this.autoDetectAndLoad(plugin);
            }
            
            this.pluginRegistry.set(plugin.name, {
                ...plugin,
                loaded: true,
                loadTime: new Date()
            });
            
        } catch (error) {
            this.log(`Failed to load plugin: ${plugin.name} - ${error.message}`, 'error');
            this.pluginRegistry.set(plugin.name, {
                ...plugin,
                loaded: false,
                error: error.message
            });
        }
    }

    async loadScriptPlugin(plugin) {
        await this.loadScriptWithRetry(plugin.url);
        this.pluginsLoaded++;
        this.log(`✅ Script plugin loaded: ${plugin.name}`, 'success');
        
        // Initialize plugin if it has an init function
        if (typeof window[`init${plugin.name.replace(/\s+/g, '')}`] === 'function') {
            try {
                window[`init${plugin.name.replace(/\s+/g, '')}`]();
                this.log(`🔧 Plugin initialized: ${plugin.name}`, 'success');
            } catch (e) {
                this.log(`⚠️ Plugin init failed: ${plugin.name}`, 'warn');
            }
        }
    }

    async loadStylePlugin(plugin) {
        await this.loadCSSWithRetry(plugin.url);
        this.pluginsLoaded++;
        this.log(`🎨 Style plugin loaded: ${plugin.name}`, 'success');
    }

    async loadConfigPlugin(plugin) {
        const config = await this.fetchJSON(plugin.url);
        if (config && typeof config === 'object') {
            // Store config in global namespace
            window[`config${plugin.name.replace(/\s+/g, '')}`] = config;
            this.pluginsLoaded++;
            this.log(`⚙️ Config plugin loaded: ${plugin.name}`, 'success');
        } else {
            throw new Error('Invalid JSON configuration');
        }
    }

    async loadWidgetPlugin(plugin) {
        const html = await this.fetchText(plugin.url);
        const widgetContainer = document.createElement('div');
        widgetContainer.innerHTML = html;
        widgetContainer.setAttribute('data-plugin-widget', plugin.name);
        widgetContainer.style.cssText = 'position: fixed; z-index: 10000;';
        document.body.appendChild(widgetContainer);
        this.pluginsLoaded++;
        this.log(`🧩 Widget plugin loaded: ${plugin.name}`, 'success');
    }

    async autoDetectAndLoad(plugin) {
        const url = plugin.url.toLowerCase();
        if (url.endsWith('.js')) {
            await this.loadScriptPlugin(plugin);
        } else if (url.endsWith('.css')) {
            await this.loadStylePlugin(plugin);
        } else if (url.endsWith('.json')) {
            await this.loadConfigPlugin(plugin);
        } else if (url.endsWith('.html')) {
            await this.loadWidgetPlugin(plugin);
        } else {
            // Default to script
            await this.loadScriptPlugin(plugin);
        }
    }

    async loadScriptWithRetry(url, attempt = 0) {
        try {
            await this.loadScript(url);
        } catch (error) {
            if (attempt < this.config.retryAttempts) {
                this.log(`Retrying script load (${attempt + 1}/${this.config.retryAttempts}): ${url}`, 'warn');
                await this.delay(this.config.retryDelay * (attempt + 1));
                return this.loadScriptWithRetry(url, attempt + 1);
            }
            throw error;
        }
    }

    async loadCSSWithRetry(url, attempt = 0) {
        try {
            await this.loadCSS(url);
        } catch (error) {
            if (attempt < this.config.retryAttempts) {
                this.log(`Retrying CSS load (${attempt + 1}/${this.config.retryAttempts}): ${url}`, 'warn');
                await this.delay(this.config.retryDelay * (attempt + 1));
                return this.loadCSSWithRetry(url, attempt + 1);
            }
            throw error;
        }
    }

    loadScript(url) {
        return new Promise((resolve, reject) => {
            if (typeof GM?.xmlHttpRequest !== 'function') {
                reject(new Error('GM.xmlHttpRequest is not available'));
                return;
            }

            GM.xmlHttpRequest({
                method: 'GET',
                url: url,
                timeout: this.config.timeout,
                responseType: 'text',
                onload: (response) => {
                    if (response.status >= 200 && response.status < 300) {
                        try {
                            const script = document.createElement('script');
                            script.textContent = response.responseText;
                            script.setAttribute('data-plugin-url', url);
                            script.setAttribute('data-loaded-by', 'UniversalPluginLoader');
                            script.setAttribute('type', 'text/javascript');
                            
                            script.onerror = (e) => {
                                this.log(`Script execution error: ${url}`, 'error');
                            };
                            
                            document.head.appendChild(script);
                            resolve(response);
                        } catch (e) {
                            reject(new Error(`Script execution failed: ${e.message}`));
                        }
                    } else {
                        reject(new Error(`HTTP ${response.status}: ${response.statusText}`));
                    }
                },
                onerror: (error) => reject(new Error(`Network error: ${error}`)),
                ontimeout: () => reject(new Error('Request timeout'))
            });
        });
    }

    loadCSS(url) {
        return new Promise((resolve, reject) => {
            if (typeof GM?.xmlHttpRequest !== 'function') {
                reject(new Error('GM.xmlHttpRequest is not available'));
                return;
            }

            GM.xmlHttpRequest({
                method: 'GET',
                url: url,
                timeout: this.config.timeout,
                responseType: 'text',
                onload: (response) => {
                    if (response.status >= 200 && response.status < 300) {
                        try {
                            if (typeof GM.addStyle === 'function') {
                                GM.addStyle(response.responseText);
                            } else {
                                const style = document.createElement('style');
                                style.textContent = response.responseText;
                                style.setAttribute('data-plugin-url', url);
                                document.head.appendChild(style);
                            }
                            resolve(response);
                        } catch (e) {
                            reject(new Error(`CSS application failed: ${e.message}`));
                        }
                    } else {
                        reject(new Error(`HTTP ${response.status}: ${response.statusText}`));
                    }
                },
                onerror: (error) => reject(new Error(`Network error: ${error}`)),
                ontimeout: () => reject(new Error('Request timeout'))
            });
        });
    }

    async fetchJSON(url) {
        const response = await this.fetchText(url);
        return JSON.parse(response);
    }

    fetchText(url) {
        return new Promise((resolve, reject) => {
            GM.xmlHttpRequest({
                method: 'GET',
                url: url,
                timeout: this.config.timeout,
                onload: (response) => {
                    if (response.status >= 200 && response.status < 300) {
                        resolve(response.responseText);
                    } else {
                        reject(new Error(`HTTP ${response.status}: ${response.statusText}`));
                    }
                },
                onerror: reject,
                ontimeout: () => reject(new Error('Request timeout'))
            });
        });
    }

    // Plugin Manager UI
    async injectPluginManager() {
        if (!this.config.enablePluginManager) return;

        try {
            this.createStatusIndicator();
            this.createPluginManagerPanel();
            this.log('Plugin manager injected successfully', 'success');
        } catch (error) {
            this.handleError('Failed to inject plugin manager', error);
        }
    }

    createStatusIndicator() {
        if (!document.body) {
            this.log('Document body not available for status indicator', 'warn');
            return;
        }

        try {
            const indicator = document.createElement('div');
            indicator.innerHTML = `
                <div class="universal-plugin-indicator">
                    <div class="plugin-stats">
                        <span class="plugin-icon">🔌</span>
                        <span class="plugin-count">${this.pluginsLoaded}/${this.totalPlugins}</span>
                    </div>
                    <div class="plugin-status">PLUGINS ACTIVE</div>
                </div>
            `;

            const style = `
                .universal-plugin-indicator {
                    position: fixed;
                    top: 20px;
                    right: 20px;
                    z-index: 10000;
                    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                    color: white;
                    padding: 12px 16px;
                    border-radius: 12px;
                    font-family: 'Segoe UI', system-ui, sans-serif;
                    font-size: 12px;
                    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
                    backdrop-filter: blur(10px);
                    border: 1px solid rgba(255,255,255,0.2);
                    cursor: pointer;
                    transition: all 0.3s ease;
                    user-select: none;
                }
                
                .universal-plugin-indicator:hover {
                    transform: translateY(-2px);
                    box-shadow: 0 6px 20px rgba(0,0,0,0.2);
                }
                
                .plugin-stats {
                    display: flex;
                    align-items: center;
                    gap: 8px;
                    margin-bottom: 4px;
                }
                
                .plugin-icon {
                    font-size: 14px;
                }
                
                .plugin-count {
                    font-weight: 600;
                    font-size: 14px;
                }
                
                .plugin-status {
                    opacity: 0.9;
                    font-size: 10px;
                    letter-spacing: 0.5px;
                }
            `;

            try {
                if (typeof GM.addStyle === 'function') {
                    GM.addStyle(style);
                } else {
                    const styleElement = document.createElement('style');
                    styleElement.textContent = style;
                    document.head.appendChild(styleElement);
                }
            } catch (styleError) {
                this.log('Failed to add status indicator styles', 'warn');
            }

            document.body.appendChild(indicator);
            indicator.addEventListener('click', this.openPluginManager);
            
        } catch (error) {
            this.handleError('Failed to create status indicator', error);
        }
    }

    createPluginManagerPanel() {
        // Advanced plugin manager UI
        const panel = document.createElement('div');
        panel.style.cssText = `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            border-radius: 12px;
            box-shadow: 0 10px 30px rgba(0,0,0,0.3);
            z-index: 10001;
            padding: 20px;
            min-width: 500px;
            max-height: 80vh;
            overflow-y: auto;
            font-family: 'Segoe UI', sans-serif;
            display: none;
        `;

        panel.innerHTML = `
            <div class="plugin-manager">
                <h2 style="margin: 0 0 20px 0; color: #333;">🔌 Plugin Manager</h2>
                <div class="plugins-list">
                    ${this.plugins.map(plugin => `
                        <div class="plugin-item" data-plugin-name="${plugin.name}">
                            <div class="plugin-info">
                                <h3>${plugin.name}</h3>
                                <p>${plugin.description || 'No description'}</p>
                                <small>Type: ${plugin.type} | URL: ${plugin.url}</small>
                            </div>
                            <div class="plugin-controls">
                                <button class="toggle-btn ${plugin.enabled ? 'enabled' : 'disabled'}">
                                    ${plugin.enabled ? 'Disable' : 'Enable'}
                                </button>
                                <span class="status ${this.pluginRegistry.get(plugin.name)?.loaded ? 'loaded' : 'failed'}">
                                    ${this.pluginRegistry.get(plugin.name)?.loaded ? '✅' : '❌'}
                                </span>
                            </div>
                        </div>
                    `).join('')}
                </div>
                <div style="margin-top: 20px; text-align: right;">
                    <button id="close-plugin-manager" style="padding: 8px 16px; background: #667eea; color: white; border: none; border-radius: 6px; cursor: pointer;">
                        Close
                    </button>
                </div>
            </div>
        `;

        document.body.appendChild(panel);

        // Add panel styles
        const panelStyle = `
            .plugin-manager {
                color: #333;
            }
            .plugins-list {
                max-height: 400px;
                overflow-y: auto;
            }
            .plugin-item {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 12px;
                border: 1px solid #ddd;
                border-radius: 8px;
                margin-bottom: 8px;
                background: #f9f9f9;
            }
            .plugin-info h3 {
                margin: 0 0 5px 0;
                font-size: 14px;
            }
            .plugin-info p {
                margin: 0 0 5px 0;
                font-size: 12px;
                color: #666;
            }
            .plugin-info small {
                color: #999;
            }
            .toggle-btn {
                padding: 6px 12px;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                font-size: 12px;
            }
            .toggle-btn.enabled {
                background: #4CAF50;
                color: white;
            }
            .toggle-btn.disabled {
                background: #f44336;
                color: white;
            }
        `;

        GM.addStyle(panelStyle);

        // Event listeners
        panel.querySelector('#close-plugin-manager').addEventListener('click', () => {
            panel.style.display = 'none';
        });

        // Toggle buttons
        panel.querySelectorAll('.toggle-btn').forEach(btn => {
            btn.addEventListener('click', (e) => {
                const pluginName = e.target.closest('.plugin-item').dataset.pluginName;
                this.togglePlugin(pluginName);
            });
        });
    }

    openPluginManager() {
        const panel = document.querySelector('.plugin-manager')?.closest('div');
        if (panel) {
            panel.style.display = 'block';
        }
    }

    togglePlugin(pluginName) {
        this.log(`Toggling plugin: ${pluginName}`, 'info');
        // Implementation for dynamically enabling/disabling plugins
        // This would require more advanced state management
    }

    // Utility methods
    isValidUrl(string) {
        try {
            new URL(string);
            return true;
        } catch (_) {
            return false;
        }
    }

    delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    log(message, type = 'info') {
        const emojis = { info: 'ℹ️', success: '✅', warn: '⚠️', error: '❌' };
        const emoji = emojis[type] || '📝';
        const styles = { info: 'color: blue;', success: 'color: green;', warn: 'color: orange;', error: 'color: red;' };
        console.log(`%c${emoji} Aternos Plugin Loader: ${message}`, styles[type] || '');
    }

    handleError(context, error) {
        this.log(`${context}: ${error?.message || error}`, 'error');
        console.error(context, error);
    }

    handlePromiseRejection(event) {
        this.log(`Unhandled promise rejection: ${event.reason}`, 'error');
    }

    setupErrorHandling() {
        window.addEventListener('error', (event) => {
            if (event.target?.tagName === 'SCRIPT' && event.target.hasAttribute('data-plugin-url')) {
                this.log(`Runtime error in plugin: ${event.target.getAttribute('data-plugin-url')}`, 'error');
            }
        });

        window.addEventListener('unhandledrejection', this.handlePromiseRejection);
    }
}

// Initialize the universal plugin loader
(async function() {
    'use strict';
    
    if (window.aternosPluginLoaderInstance) {
        console.log('Aternos Plugin Loader already running');
        return;
    }
    
    try {
        if (document.readyState === 'loading') {
            await new Promise(resolve => 
                document.addEventListener('DOMContentLoaded', resolve, { once: true })
            );
        }
        
        await new Promise(resolve => setTimeout(resolve, 100));
        
        const loader = new UniversalPluginLoader();
        window.aternosPluginLoaderInstance = loader;
        await loader.init();
        
    } catch (error) {
        console.error('🚨 Aternos Universal Plugin Loader: Critical initialization error', error);
    }
})();