MacPS Button

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

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==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();
})();