MacPS Button

Allows for people to join the private server whenever it's open.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         MacPS Button
// @author       Mac
// @description Allows for people to join the private server whenever it's open.
// @version      1.34
// @namespace    MacPS
// @license MIT
// @antifeature remote-config
// @match        https://cavegame.io/*
// @match        https://mineroyale.io/*
// @match        http://localhost:8080/*
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @connect      drive.google.com
// @connect      drive.usercontent.google.com
// @connect      trycloudflare.com
// @connect      *.trycloudflare.com
// ==/UserScript==

(() => {
    'use strict';

    const CONFIG_URL =
        'https://drive.google.com/uc?export=download&id=1F8AfmLdeXXcpdEGCm04hKYY2T22gbVmz';

    const REQUEST_TIMEOUT = 5000;
    const REFRESH_COOLDOWN = 1500;
    const CONNECT_TIMEOUT = 10000;

    const STATE = Object.freeze({
        CHECKING: 'Checking...',
        ONLINE: 'Online',
        IDLE: 'Idle',
        OFFLINE: 'Offline',
        ERROR: 'Error',
        CONNECTING: 'Connecting...'
    });

    let currentState = STATE.CHECKING;
    let websocketTarget = '';

    let button = null;
    let refreshBtn = null;

    let refreshCooldown = 0;
    let refreshBusy = false;

    const NativeWebSocket = unsafeWindow.WebSocket;

    let wsHookActive = false;
    let wasMenuOpen = false;
    let connectTimer = null;

    function isDarkMode() {
        const btn =
            document.getElementById('play-cavegame-io') ||
            document.getElementById('play-mineroyale-io');

        return btn?.classList.contains('dark');
    }

    function isLocked() {
        return currentState === STATE.CHECKING || currentState === STATE.CONNECTING;
    }

    function isMainDisabled() {
        return (
            isDarkMode() ||
            currentState === STATE.ERROR ||
            currentState === STATE.IDLE ||
            currentState === STATE.OFFLINE ||
            isLocked()
        );
    }

    function isRefreshDisabled() {
        return (
            isDarkMode() ||
            refreshBusy ||
            isLocked() ||
            Date.now() < refreshCooldown
        );
    }

    function getStateColor(state) {
        switch (state) {
            case STATE.ONLINE:
                return '#22c55e';

            case STATE.IDLE:
                return '#f59e0b';

            case STATE.CHECKING:
            case STATE.CONNECTING:
                return '#9ca3af';

            default:
                return '#ef4444';
        }
    }

    function stopWSHook() {
        wsHookActive = false;
        unsafeWindow.WebSocket = NativeWebSocket;
    }

    function enableWSHook() {
        wsHookActive = true;

        unsafeWindow.WebSocket = function (...args) {
            let url = args[0];

            if (!wsHookActive || currentState !== STATE.CONNECTING) {
                return new NativeWebSocket(url, ...args.slice(1));
            }

            if (
                typeof url === 'string' &&
                websocketTarget &&
                (url.startsWith('ws://') || url.startsWith('wss://'))
            ) {
                url = websocketTarget;
            }

            return new NativeWebSocket(url, ...args.slice(1));
        };

        unsafeWindow.WebSocket.prototype = NativeWebSocket.prototype;
    }

    function resetConnectionState() {
        stopWSHook();

        if (connectTimer) {
            clearTimeout(connectTimer);
            connectTimer = null;
        }

        if (currentState === STATE.CONNECTING) {
            currentState = STATE.IDLE;
        }

        updateUI();
    }

    function applyMainButtonState() {
        if (!button) return;

        const disabled = isMainDisabled();
        const status = button.querySelector('.macps-status');

        if (status) {
            status.textContent = currentState;
            status.style.color = getStateColor(currentState);
            status.style.fontWeight = '700';
        }

        button.style.background =
            'linear-gradient(-225deg,#4da3ff,#1e66d0)';

        button.style.color = '#fff';
        button.style.border = 'none';
        button.style.opacity = disabled ? '0.6' : '1';
        button.style.pointerEvents = disabled ? 'none' : 'auto';
        button.style.cursor = disabled ? 'not-allowed' : 'pointer';
        button.style.filter = disabled ? 'grayscale(0.25)' : 'none';
    }

    function updateRefresh() {
        if (!refreshBtn) return;

        const locked = isRefreshDisabled();

        refreshBtn.style.background = '#e5e7eb';
        refreshBtn.style.color = '#111';
        refreshBtn.style.border = 'none';
        refreshBtn.style.width = '55px';
        refreshBtn.style.height = '42px';
        refreshBtn.style.display = 'flex';
        refreshBtn.style.alignItems = 'center';
        refreshBtn.style.justifyContent = 'center';
        refreshBtn.style.cursor = locked ? 'not-allowed' : 'pointer';
        refreshBtn.style.opacity = locked ? '0.6' : '1';
        refreshBtn.style.filter = locked ? 'grayscale(1)' : 'none';
        refreshBtn.style.pointerEvents = locked ? 'none' : 'auto';
    }

    function updateUI() {
        applyMainButtonState();
        updateRefresh();
    }

    function setState(state, target = '') {
        currentState = state;

        if (target) {
            websocketTarget = target;
        }

        if (state !== STATE.CONNECTING) {
            stopWSHook();
        } else {
            if (connectTimer) {
                clearTimeout(connectTimer);
            }

            connectTimer = setTimeout(() => {
                if (currentState === STATE.CONNECTING) {
                    resetConnectionState();
                }
            }, CONNECT_TIMEOUT);
        }

        updateUI();
    }

    function isMenuOpen() {
        const overlay = document.getElementById('menu-overlay');

        return overlay && getComputedStyle(overlay).display !== 'none';
    }

    function watchMenu() {
        const open = isMenuOpen();

        if (open && !wasMenuOpen) {
            refreshServerStatus();
        }

        if (!open && wasMenuOpen) {
            resetConnectionState();
        }

        wasMenuOpen = open;
    }

    function request(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url,
                timeout: REQUEST_TIMEOUT,
                onload: resolve,
                onerror: reject,
                ontimeout: reject
            });
        });
    }

    function isValidWebSocketURL(url) {
        try {
            const u = new URL(url);

            return u.protocol === 'wss:' && u.hostname.length > 0;
        } catch {
            return false;
        }
    }

    async function fetchServerAddress() {
        try {
            const res = await request(CONFIG_URL);
            const text = (res.responseText || '').trim();

            return isValidWebSocketURL(text) ? text : null;
        } catch {
            return null;
        }
    }

    async function verifyServer(url) {
        try {
            if (!url || !url.trim()) {
                return STATE.OFFLINE;
            }

            const hostname = new URL(url).hostname;
            const httpsURL = `https://${hostname}`;

            const res = await request(httpsURL);

            const text = (res.responseText || '').trim();

            if (text === 'OK') {
                return STATE.ONLINE;
            }

            if (
                text.includes('502 Bad Gateway') ||
                text.includes('Unable to reach the origin service')
            ) {
                return STATE.IDLE;
            }

            return STATE.OFFLINE;

        } catch {
            return STATE.OFFLINE;
        }
    }

    async function refreshServerStatus() {
        if (isDarkMode()) return;

        refreshBusy = true;

        setState(STATE.CHECKING);

        const url = await fetchServerAddress();

        if (!url) {
            setState(STATE.OFFLINE);

            refreshBusy = false;
            refreshCooldown = Date.now() + REFRESH_COOLDOWN;

            updateUI();
            return;
        }

        const state = await verifyServer(url);

        setState(
            state,
            state === STATE.ONLINE ? url : ''
        );

        refreshBusy = false;
        refreshCooldown = Date.now() + REFRESH_COOLDOWN;

        updateUI();
    }

    function onMainClick(e) {
        e.preventDefault();
        e.stopPropagation();

        if (isMainDisabled()) return;
        if (!websocketTarget) return;

        setState(STATE.CONNECTING);

        enableWSHook();

        const play =
            document.getElementById('play-mineroyale-io') ||
            document.getElementById('play-cavegame-io');

        play?.click();
    }

    function onRefreshClick(e) {
        e.preventDefault();
        e.stopPropagation();

        if (isRefreshDisabled()) return;

        refreshServerStatus();
    }

    function createUI() {
        const container = document.getElementById('sub-play-btn-cont');

        if (!container || document.getElementById('cg-macps-wrap')) {
            return;
        }

        const wrap = document.createElement('div');

        wrap.id = 'cg-macps-wrap';
        wrap.style.display = 'inline-flex';
        wrap.style.alignItems = 'center';
        wrap.style.width = '94%';

        button = document.createElement('button');
        button.className = 'play-btn';

        button.innerHTML = `
            MacPS
            <div class="macps-status">${currentState}</div>
        `;

        button.addEventListener('click', onMainClick);

        refreshBtn = document.createElement('button');
        refreshBtn.textContent = '↻';

        refreshBtn.addEventListener('click', onRefreshClick);

        wrap.appendChild(button);
        wrap.appendChild(refreshBtn);

        container.appendChild(wrap);

        updateUI();
    }

    new MutationObserver(() => {
        if (!document.getElementById('cg-macps-wrap')) {
            createUI();
        }
    }).observe(document.body, {
        childList: true,
        subtree: true
    });

    setInterval(() => {
        watchMenu();
        updateUI();
    }, 50);

    createUI();
})();