MacPS Button

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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