EasyJoin

Join roblox job ids, games and private servers easily

// ==UserScript==
// @name         EasyJoin
// @namespace    https://mstudio45.com/
// @version      1.2.1
// @description  Join roblox job ids, games and private servers easily
// @author       mstudio45
// @license      MPL-2.0
// @match        https://www.roblox.com/*
// @match        https://web.roblox.com/*
// @match        https://roblox.com/*
// @icon         https://roblox.com/favicon.ico
// @grant        none
// ==/UserScript==

// DO NOT CHANGE ANYTHING BELOW //

const Roblox = globalThis.Roblox // this is useless but i want to remove stupid warnings
const jQuery = globalThis.jQuery // this is useless but i want to remove stupid warnings
const DataVersion = "v1"
const VERSION = "v1.2"

const pageUrl = window.location.href;
const pageInfo = {
    url: pageUrl,
    getPageName: function() {
        if (pageUrl.indexOf("roblox.com/games/") !== -1 || pageUrl.indexOf("roblox.com/private-server/") !== -1) return "games"
        if (pageUrl.indexOf("roblox.com/home")) return "home";
        if (pageUrl.indexOf("roblox.com/catalog")) return "catalog";
        return pageUrl.split("roblox.com/")[1];
    }
}

const menu = [document.getElementsByTagName('ul')[0], document.querySelector("#header > div > ul.nav.rbx-navbar.hidden-md.hidden-lg.col-xs-12")]
const html = `
<style>
    .privservercontainer { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr 1fr; }
    .privservercontainer::after, .privservercontainer::before { display:none !important; }
    .contentfix .modal-content { width: max-content; }

    .mstudio45JoinerMenuBody > li::hover {
        background: #191b1d;
    }
</style>

<li class='cursor-pointer bottomTooltip'>
    <a id="mstudio45JoinerMenuButton" style="color: green;" style="display: block;" class='font-header-2 nav-menu-title text-header' onclick='window.mstudio45.open()'>EasyJoin</a>
</li>

<body>
    <div id="mstudio45JoinerMenuBody" style="display: none; margin: 0 auto; min-width: 200px; z-index: 10; background: #393b3d; border: solid #111214;">
        <!--
            Only visible on the games page. ↓
        -->

        <li class="cursor-pointer only-gamespage" style="margin: 0 auto;"><a style="color:gray;" class='font-header-2 nav-menu-title text-header'>Place:</a></li>

        <li class="cursor-pointer only-gamespage" style="margin: 0 auto;">
            <a style="/*color:green;*/" class='font-header-2 nav-menu-title text-header' onclick='window.mstudio45.setup(1)'>Job ID</a>
        </li>

        <li class="cursor-pointer only-gamespage" style="margin: 0 auto;">
            <a style="/*color:green;*/" class='font-header-2 nav-menu-title text-header' onclick='window.mstudio45.setup(99)'>Most Players</a>
        </li>

        <li class="cursor-pointer only-gamespage" style="margin: 0 auto;">
            <a style="/*color:green;*/" class='font-header-2 nav-menu-title text-header' onclick='window.mstudio45.setup(98)'>Least Players</a>
        </li>

        <li class="cursor-pointer only-gamespage" style="margin: 0 auto;"><a style="/*color:green;*/" class='font-header-2 nav-menu-title text-header'></a></li>
        <li class="cursor-pointer only-gamespage" style="margin: 0 auto;"><a style="color:gray;" class='font-header-2 nav-menu-title text-header'>Private Servers:</a></li>

        <li class="cursor-pointer only-gamespage" style="margin: 0 auto;">
            <a style="/*color:green;*/" class='font-header-2 nav-menu-title text-header' onclick='window.mstudio45.setup(2)'>Code</a>
        </li>
        <li class="cursor-pointer only-gamespage" style="margin: 0 auto;">
            <a style="/*color:green;*/" class='font-header-2 nav-menu-title text-header' onclick='window.mstudio45.setup(3)'>Share URL Code</a>
        </li>

        <li class="cursor-pointer only-gamespage" style="margin: 0 auto;">
            <a style="/*color:green;*/" class='font-header-2 nav-menu-title text-header' onclick='window.mstudio45.setup(4)'>By User ID</a>
        </li>
        <li class="cursor-pointer only-gamespage" style="margin: 0 auto;">
            <a style="/*color:green;*/" class='font-header-2 nav-menu-title text-header' onclick='window.mstudio45.setup(5)'>By Username</a>
        </li>
        <li class="cursor-pointer only-gamespage" style="margin: 0 auto;">
            <a style="/*color:green;*/" class='font-header-2 nav-menu-title text-header' onclick='window.mstudio45.setup(6)'>By Display Name</a>
        </li>

        <li class="cursor-pointer only-gamespage" style="margin: 0 auto;"><a style="/*color:green;*/" class='font-header-2 nav-menu-title text-header'></a></li>

        <li class="cursor-pointer only-gamespage" style="margin: 0 auto;">
            <a id="mstudio45JoinerDropdownMenuButton" style="color:yellow;" class='font-header-2 nav-menu-title text-header' onclick='window.mstudio45.setup(101)'>Reload private servers</a>
        </li>

        <li class="cursor-pointer only-gamespage" style="margin: 0 auto;">
            <a id="mstudio45JoinerDropdownMenuButton" style="color:yellow;" class='font-header-2 nav-menu-title text-header' onclick='window.mstudio45.cache.clear("all")'>Clear Cache</a>
        </li>
        <li class="cursor-pointer only-gamespage" style="margin: 0 auto;">
            <a id="mstudio45JoinerDropdownMenuButton" style="color:yellow;" class='font-header-2 nav-menu-title text-header' onclick='window.mstudio45.cache.clear("servers")'>Clear Saved Servers</a>
        </li>

        <!--
            Visible all the time excpet on the games page. ↓
        -->

        <li class="cursor-pointer except-gamespage" style="margin: 0 auto;">
            <a style="/*color:green;*/" class='font-header-2 nav-menu-title text-header' onclick='window.mstudio45.setup(7)'>Join a Friend</a>
        </li>

        <!--
            Visible all the time. ↓
        -->
        <li class="cursor-pointer" style="margin: 0 auto;"><a style="/*color:green;*/" class='font-header-2 nav-menu-title text-header'></a></li>
        <li class="cursor-pointer" style="margin: 0 auto;">
            <a id="mstudio45JoinerDropdownMenuButton" style="color:red;" class='font-header-2 nav-menu-title text-header' onclick='window.mstudio45.toggle()'>Close</a>
        </li>
    </div>
</body>
`

// Required Functions //
function randomString(length) {
    let result = '';
    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    const charactersLength = characters.length;
    let counter = 0;
    while (counter < length) {
        result += characters.charAt(Math.floor(Math.random() * charactersLength));
        counter += 1;
    }
    return result;
}

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

// Roblox Functions //
function generateProtocol(placeId = null, jobId = null, privServerId = null) {
    if (!placeId) return null;

    let protocol = "roblox://placeID=" + String(placeId);
    if (jobId && privServerId) {
        protocol += "&linkCode=" + String(privServerId);
    } else if (jobId && !privServerId) {
        protocol += "&gameInstanceId=" + jobId;
    }

    return protocol;
}

function join(placeId = null, jobId = null, privServerId = null, followUserId = null) {
    if (followUserId && !jobId && !privServerId) { // only works when placeId and followUserId is provided (placeId is not required either)
        window.mstudio45.close();
        Roblox.Dialog.close();
        Roblox.GameLauncher.followPlayerIntoGame(followUserId);
        return;
    }

    if (!placeId) return null;

    if (privServerId) {
        window.mstudio45.close();
        Roblox.Dialog.close();
        Roblox.GameLauncher.joinPrivateGame(placeId, privServerId, null);
        return;
    }

    if (jobId) {
        window.mstudio45.close();
        Roblox.Dialog.close();
        Roblox.GameLauncher.joinGameInstance(placeId, jobId);
        return;
    }

     Roblox.GameLauncher.joinGameInstance(placeId, null);
}

async function getAvatars(userIds) {
    const avatars = window.mstudio45.cache.get("avatars", true) || {}

    userIds = userIds.filter((id) => id in avatars ? avatars[id] == "placeholder" : true);
    try {
        for (let i = 0; i < userIds.length; i += 99) {
            const chunk = userIds.slice(i, i + 99);
            const img = await fetch("https://thumbnails.roblox.com/v1/users/avatar-headshot?userIds=" + chunk.join(",") + "&size=150x150&format=png", { method: 'GET', headers: { 'Content-Type': 'application/json' } }).then(r => r.json()).then((json) => { return json.data })

            img.forEach((x) => {
                avatars["" + x.targetId.toString()] = x.imageUrl || "placeholder"
            });
        }
    } catch (e) {
        window.mstudio45.console.log(e)
        userIds.forEach((x) => {
            avatars["" + x.toString()] = "placeholder"
        });
    }

    window.mstudio45.cache.set("avatars", avatars, true)
}

async function getServer(placeId, ownerId, type = 1, cursor = "") { // 1 = id, 2 = name, 3 = display name
    window.mstudio45.close();
    window.mstudio45.modals.loading("Trying to find the Private server(s)...");
    ownerId = ownerId == null ? "" : ownerId;

    const servers = window.mstudio45.cache.get("servers", true, true) || {}, privateServerData = window.mstudio45.cache.get("privateServer", true, true) || {},
          serverNameForInfo = DataVersion + "-" + ownerId.toString() + "-" + type.toString() + "-" + placeId.toString(),
          privateServerNameForInfo = DataVersion + "-" + placeId.toString();
    let finished = false, doLoop = false, reloadServerInfo = false;

    if (type == true && ownerId == "") {
        delete privateServerData[privateServerNameForInfo];
        delete servers[serverNameForInfo];

        doLoop = true;
        reloadServerInfo = true;
    }

    // Get Cache
    if (privateServerNameForInfo in privateServerData ? privateServerData[privateServerNameForInfo].length >= 1 : false) {
        window.mstudio45.console.log("Found '" + privateServerNameForInfo + "' private servers in cache!")
    } else {
        doLoop = true;
        reloadServerInfo = true;
        privateServerData[privateServerNameForInfo] = []
    }

    if (serverNameForInfo in servers ? servers[serverNameForInfo].length >= 1 : false) {
        window.mstudio45.console.log("Found '" + serverNameForInfo + "' in cache!")
    } else {
        reloadServerInfo = true;
        servers[serverNameForInfo] = [];
    }

    // Main
    function loop(placeId, ownerId, type, cursor) {
        window.mstudio45.modals.loading("Loading Private servers for this place... (" + privateServerData[privateServerNameForInfo].length + ")");

        fetch("https://games.roblox.com/v1/games/" + placeId + "/private-servers?cursor=" + cursor + "&sortOrder=Desc&excludeFullGames=false", {
            method: 'GET',
            mode: "cors",
            credentials: "include",
            headers: {
                'Content-Type': 'application/json',
                "Cookie": document.cookie,
            }
        }).then(r => r.json()).then((json) => {
            json.data.forEach((x) => privateServerData[privateServerNameForInfo].push(x));

            if (json.nextPageCursor == null) {
                finished = true;
            } else {
                setTimeout(function() { loop(placeId, ownerId, type, json.nextPageCursor) }, 1500)
            }
        });
    }

    try {
        if (doLoop) {
            loop(placeId, ownerId, type, "")
            while (finished == false) await sleep(500);
        }

        if (reloadServerInfo) {
            // Find Private Server(s)
            privateServerData[privateServerNameForInfo].forEach((x) => {
                let check = x.owner.id;
                switch (type) {
                    case 3:
                        check = x.owner.displayName;
                        break;

                    case 2:
                        check = x.owner.name;
                        break;

                    case 1:
                    default:
                        check = x.owner.id;
                        break
                }

                if (type == 1 ? check == ownerId : check.toString().toLowerCase().indexOf(ownerId.toString().toLowerCase()) !== -1) {
                    servers[serverNameForInfo].push({
                        callbackName: x.vipServerId.toString(),

                        name: x.owner.name,
                        displayName: x.owner.displayName,
                        id: x.owner.id,

                        accessCode: x.accessCode,
                        placeId: placeId
                    });
                }
            })
        }

        if (type == true && ownerId == "") {
            setTimeout(function() { window.mstudio45.modals.info("Private Server successfully reloaded for this place.") }, 250)
        } else {

            // Load Modal
            if (servers[serverNameForInfo].length <= 0) {
                delete servers[serverNameForInfo];

                setTimeout(function() { window.mstudio45.modals.error("Couldn't find any Private Server for this place.") }, 250)
            } else if (servers[serverNameForInfo].length >= 1) {
                await getAvatars(servers[serverNameForInfo].map((x) => x.id));

                window.mstudio45.modals.servers("Found Private servers. (" + servers[serverNameForInfo].length + "/" + privateServerData[privateServerNameForInfo].length + ")", servers[serverNameForInfo])
            }
        }

        window.mstudio45.cache.set("servers", servers, true); window.mstudio45.cache.set("privateServer", privateServerData, true);
    } catch (e) { window.mstudio45.modals.error("Couldn't find any Private Server for this place or something has failed."); window.mstudio45.console.warn("[EasyJoin]", e); }
}

// Main Code //
window.mstudio45 = {
    // Data
    servers: {},
    toggled: false,
    cache: {
        get: function(name, isJson, clearOldVersionData) {
            let data = localStorage.getItem("EJ_" + name);

            if (data) {
                if (isJson) {
                    data = JSON.parse(data);
                    if (clearOldVersionData) {
                        // only works if the index starts with the version (V1-...)
                        for (var i in data) {
                            if (!i.toString().startsWith(DataVersion + "-")) {
                                delete data[i];
                            }
                        }
                    }
                } else {
                    if (!data.startsWith(DataVersion + "-")) {
                        data = null;
                    }
                }
            }

            return data ? data : null;
        },

        set: function(name, data, isJson) {
            localStorage.setItem("EJ_" + name, isJson ? JSON.stringify(data) : data);
        },

        clear: function(name = "all") {
            var keys = Object.keys(localStorage), i = 0, key;
            for (; key = keys[i]; i++) {
                if (name == "all" ? key.startsWith("EJ_") : key == "EJ_" + name) localStorage.removeItem(key);
            }

            window.mstudio45.modals.info("The specified cache has been cleared successfully.")
        },
    },

    // Modals
    modals: {
        info: function(message) {
            window.mstudio45.console.log(message)
            Roblox.Dialog.open({
                titleText: "EasyJoin",
                bodyContent: '<p class="text-center">' + message + '</p>',
                acceptText: "Ok",
                showDecline: false,
                xToCancel: true,
                allowHtmlContentInBody: true
            });
        },

        loading: function(message) {
            window.mstudio45.console.log("Loading:", message)
            Roblox.Dialog.open({
                titleText: "EasyJoin",
                bodyContent: '<span class="spinner spinner-default"></span><p class="text-center">' + message + '</p>',
                showAccept: false,
                showDecline: false,
                xToCancel: true,
                allowHtmlContentInBody: true
            });
        },

        error: function(message) {
            window.mstudio45.console.warn("Error:", message)
            Roblox.Dialog.open({
                titleText: "EasyJoin",
                bodyContent: '<p class="text-error" style="font-size: 16px;">' + message + '</p>',
                acceptText: "Ok",
                showDecline: false,
                xToCancel: true,
                allowHtmlContentInBody: true
            });
        },

        input: function(placeholder, footer, btnText, callback) {
            const modalName = randomString(10);
            Roblox.Dialog.close();
            setTimeout(function() {
                Roblox.Dialog.open({
                    titleText: "EasyJoin",
                    bodyContent: "<input type='text' class='form-control input-field' placeholder='" + placeholder + "' id='" + modalName + "-text'/>",
                    footerText: footer,
                    acceptText: btnText,

                    showDecline: false,
                    xToCancel: true,

                    allowHtmlContentInBody: true,
                    fieldValidationRequired: true,

                    onAccept: function() {
                        var validationText = jQuery("#" + modalName + "-text");
                        if (validationText && validationText.val()) {
                            callback(true, validationText.val())
                        } else {
                            callback(false, null)
                        }
                    },
                    onDecline: function() { callback(false, null) },
                    onCancel: function() { callback(false, null) }
                });
            }, 10);
        },

        servers: async function(message, servers) {
            const avatars = window.mstudio45.cache.get("avatars", true) || {}

            Roblox.Dialog.close();
            await sleep(10);
            Roblox.Dialog.open({
                titleText: "EasyJoin",
                bodyContent: '<p class="text-center">' + message + '</p><br></br><ul class="hlist game-cards privservercontainer">' + servers.map((item) => {
                    if (item.name && item.id && item.callbackName) {
                        window.mstudio45.servers[item.callbackName] = function() {
                            Roblox.Dialog.close();
                            join(item.placeId, null, item.accessCode);
                        }

                        const img = avatars["" + item.id];
                        const html = `<li class="list-item game-card game-tile">
    <div class="game-card-container">
      <div class="game-card-link">
        <a href="https://www.roblox.com/users/${item.id}/profile" class="game-card-link" alt="${item.displayName} (${item.name})" title="${item.displayName} (${item.name})">
            <div class="game-card-thumb-container">
                ${img == "placeholder" ? '<span class="avatar-card-image icon-placeholder-avatar-headshot"></span>' : '<span class="avatar-card-image"><img class="" src="' + img + '" alt="" title=""></span>'}
            </div>
            <div class="game-card-name game-name-title" ng-non-bindable="">${item.displayName} (${item.name})</div>
        </a>
        <div class="game-card-info" style="position: absolute;bottom: 0;height: 15%;">
          <button type="button" class="info-label btn-common-play-game-lg btn-primary-md btn-full-width" style=" height: 100%; display: flex; align-items: center; justify-content: center;" onclick='window.mstudio45.servers[${item.callbackName}]()'>
              <span class="icon-common-play" style="text-align: center; transform: scale(0.6);"></span>
          </button>
        </div>
      </div>
    </div>
</li>`
                        return html;
                    }

                    return "";
                }).join('') + '</ul>',
                showAccept: false,
                showDecline: false,
                xToCancel: true,
                allowHtmlContentInBody: true,
                cssClass: "contentfix"
            });

            while (document.querySelector(".contentfix") == null) await sleep(150);
            const contentfix = document.querySelector(".contentfix");

            // i love roblox
            contentfix.parentElement.style.overflow = ""
            contentfix.parentElement.parentElement.style = "z-index: 1942; position: absolute; transform: translate(-60%, 35%);"
            window.dispatchEvent(new Event('resize'));
            contentfix.parentElement.parentElement.style.transform = "translate(-60%, 50%);"
            window.dispatchEvent(new Event('resize'));
        }
    },

    // Console
    console: {
        log: function() {
            console.log("[EasyJoin]", ...arguments)
        },

        warn: function() {
            console.warn("[EasyJoin]", ...arguments)
        },

        error: function() {
            console.error("[EasyJoin]", ...arguments)
        },
    },

    // Custom functions
    update: function() {
        document.querySelectorAll("#mstudio45JoinerMenuButton").forEach(x => {
            x.style.display = window.mstudio45.toggled === false ? "block" : "none";
        })

        document.querySelectorAll("#mstudio45JoinerMenuBody").forEach(x => {
            x.style.display = window.mstudio45.toggled === false ? "none" : "block";
        })
    },
    toggle: function() {
        window.mstudio45.toggled = !window.mstudio45.toggled
        window.mstudio45.update();
    },
    close: function() {
        window.mstudio45.toggled = false
        window.mstudio45.update();
    },
    open: function() {
        window.mstudio45.toggled = true
        window.mstudio45.update();
    },

    setup: async function(type = 1, customPlaceId = false) { // 1 = jobid, 2 = priv, 3 = least plrs, 4 = most plrs
        let placeId = pageInfo.getPageName() == "games" ? window.location.pathname.match(/\/(\d+)\/.+?$/)[1] : -1;

        if (placeId == -1 && customPlaceId) {
            window.mstudio45.modals.input("Input the Place ID here.", "", "Ok", async function(suc, text) {
                if (suc && text) {
                    if (!parseInt(text)) {
                        Roblox.Dialog.close();
                        await sleep(10);
                        window.mstudio45.modals.error("Invalid Place ID.")
                        return;
                    }

                    placeId = parseInt(text);
                }
            });

            while (placeId == -1) await sleep(150);
            if (placeId <= -1) return window.mstudio45.console.warn("Action stopped");
        }

        if (type == 1) {
            window.mstudio45.modals.input("Input the Job ID here.", "", "Ok", function(suc, text) {
                if (suc && text) join(placeId, text);
            });
            return;
        }

        // Private Server
        if (type == 2) {
            window.mstudio45.modals.input("Input the Private Server Code here.", "", "Ok", function(suc, text) {
                if (suc && text) join(placeId, null, text);
            });
            return;
        }

        if (type == 3) {
            window.mstudio45.modals.input("Input the Private Server Share Code here.", "", "Ok", function(suc, text) {
                if (suc && text) window.location.href = "https://roblox.com/share?code=" + text + "&type=Server";
            });
            return;
        }

        if (type == 4) {
            window.mstudio45.modals.input("Input the User ID here.", "", "Ok", async function(suc, text) {
                if (suc && text) {
                    if (!parseInt(text)) {
                        Roblox.Dialog.close();
                        await sleep(10);
                        window.mstudio45.modals.error("Invalid User ID.")
                        return;
                    }

                    await getServer(placeId, parseInt(text), 1)
                }
            });
            return;
        }
        if (type == 5) {
            window.mstudio45.modals.input("Input the Username here.", "", "Ok", async function(suc, text) {
                if (suc && text) await getServer(placeId, text, 2)
            });
            return;
        }
        if (type == 6) {
            window.mstudio45.modals.input("Input the Display Name here.", "", "Ok", async function(suc, text) {
                if (suc && text) await getServer(placeId, text, 3)
            });
            return;
        }

        // Follow User
        if (type == 7) {
            window.mstudio45.modals.input("Input the User ID here.", "", "Ok", async function(suc, text) {
                if (suc && text) {
                    if (!parseInt(text)) {
                        await sleep(10);
                        window.mstudio45.modals.error("Invalid User ID.")
                        return;
                    }

                    await join(placeId, null, null, parseInt(text))
                }
            });
            return;
        }

        // API
        if (type == 98) {
            fetch("https://games.roblox.com/v1/games/" + placeId + "/servers/Public?sortOrder=Asc&excludeFullGames=true&limit=10&cursor=", { method: 'GET', headers: { 'Content-Type': 'application/json'} }).then(r => r.json()).then((json) =>{
                join(placeId, json.data[0].id)
            });
            return;
        }

        if (type == 99) {
            fetch("https://games.roblox.com/v1/games/" + placeId + "/servers/Public?sortOrder=Desc&excludeFullGames=true&limit=10&cursor=", { method: 'GET', headers: { 'Content-Type': 'application/json'} }).then(r => r.json()).then((json) =>{
                join(placeId, json.data[0].id)
            });
            return;
        }

        if (type == 101) {
            getServer(placeId, null, true)
            return;
        }
    },
}

// Insert HTML
menu.forEach(e => { e.innerHTML += html });
document.querySelectorAll("#mstudio45JoinerMenuBody").forEach((el) => {
    el.children.forEach((el) => {
        if (el.className.indexOf("only-gamespage") !== -1) el.style.display = pageInfo.getPageName() == "games" ? "" : "none";
        else if (el.className.indexOf("except-gamespage") !== -1) el.style.display = pageInfo.getPageName() == "games" ? "none" : "";
    });
});
window.mstudio45.console.log("Initialized! (" + VERSION + ") - Created by mstudio45");

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址