MacPS Button

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Advertisement:

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

Advertisement:

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