MacPS Button

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

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