// ==UserScript== // @name RoLocate // @namespace https://oqarshi.github.io/ // @version 21.4 // @description Adds filter options to roblox server page. Alternative to paid extensions like RoPro, RoGold (Ultimate), RoQol, and RoKit. // @author Oqarshi // @match https://www.roblox.com/games/* // @license MIT // @icon data:image/jpeg;base64,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 // @grant GM_xmlhttpRequest // @downloadURL none // ==/UserScript== (function() { 'use strict'; /********************************************************************************************************************************************************************************************************************************************* This is all of the functions for the filter button and the popup for the 8 buttons does not include the functions for the 8 buttons *********************************************************************************************************************************************************************************************************************************************/ function createPopup() { const popup = document.createElement('div'); popup.className = 'server-filters-dropdown-box'; // Unique class name popup.style.cssText = ` position: absolute; width: 210px; height: 382px; right: 0px; top: 30px; z-index: 1000; border-radius: 5px; background-color: rgb(30, 32, 34); display: flex; flex-direction: column; padding: 5px; `; // Create the header section const header = document.createElement('div'); header.style.cssText = ` display: flex; align-items: center; padding: 10px; border-bottom: 1px solid #444; margin-bottom: 5px; `; // Add the logo (base64 image) const logo = document.createElement('img'); logo.src = 'data:image/jpeg;base64,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'; // Replace with your base64 logo logo.style.cssText = ` width: 24px; height: 24px; margin-right: 10px; `; // Add the title const title = document.createElement('span'); title.textContent = 'RoLocate'; title.style.cssText = ` color: white; font-size: 18px; font-weight: bold; `; // Append logo and title to the header header.appendChild(logo); header.appendChild(title); // Append the header to the popup popup.appendChild(header); // Define unique names and tooltips for each button const buttonData = [{ name: "Smallest Servers", tooltip: "**Reverses the order of the server list.** The emptiest servers will be displayed first." }, { name: "Available Space", tooltip: "**Filters out servers which are full.** Servers with space will only be shown." }, { name: "Player Count", tooltip: "**Roblox Locator finds servers with your specified player count or fewer.** Searching for up to 3 minutes. If no exact match is found, it shows servers closest to the target." }, { name: "Random Shuffle", tooltip: "**Display servers in a completely random order.** Shows servers with space and servers with low player counts in a randomized order." }, { name: "Server Region", tooltip: "**Filters servers by region.** Offering more accuracy than 'Best Connection' in areas with fewer Roblox servers, like India, or in games with high player counts." }, { name: "Best Connection", tooltip: "**Automatically joins the fastest servers for you.** However, it may be less accurate in regions with fewer Roblox servers, like India, or in games with large player counts." }, { name: "Auto Join Small Server", tooltip: "**Automatically tries to join a server with a very low population.** On popular games servers may fill up very fast so you might not always get in alone." }, { name: "About", tooltip: "**The Credits.** Rolocate was created by Oqarshi. Special thanks to BTRoblox ❤️. Enjoy using Rolocate!" } ]; // Create buttons with unique names and tooltips buttonData.forEach((data, index) => { const buttonContainer = document.createElement('div'); buttonContainer.className = 'server-filter-option'; buttonContainer.style.cssText = ` width: 190px; height: 30px; background-color: #393B3D; margin: 5px; border-radius: 5px; padding: 3.5px; position: relative; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background-color 0.3s ease; `; const tooltip = document.createElement('div'); tooltip.className = 'filter-tooltip'; tooltip.style.cssText = ` display: none; position: absolute; top: -10px; left: 200px; width: auto; inline-size: 200px; height: auto; background-color: #191B1D; color: white; padding: 5px; border-radius: 5px; white-space: pre-wrap; font-size: 14px; `; // Parse tooltip text and replace **...** with bold HTML tags tooltip.innerHTML = data.tooltip.replace(/\*\*(.*?)\*\*/g, "$1"); const buttonText = document.createElement('p'); buttonText.style.cssText = ` margin: 0; color: white; font-size: 16px; `; buttonText.textContent = data.name; buttonContainer.appendChild(tooltip); buttonContainer.appendChild(buttonText); buttonContainer.addEventListener('mouseover', () => { tooltip.style.display = 'block'; buttonContainer.style.backgroundColor = '#4A4C4E'; // Hover effect }); buttonContainer.addEventListener('mouseout', () => { tooltip.style.display = 'none'; buttonContainer.style.backgroundColor = '#393B3D'; // Revert to original color }); buttonContainer.addEventListener('click', () => { switch (index) { case 0: smallest_servers(); break; case 1: available_space_servers(); break; case 2: player_count_tab(); break; case 3: random_servers(); break; case 4: createServerCountPopup((totalLimit) => { rebuildServerList(gameId, totalLimit); }); break; case 5: rebuildServerList(gameId, 50, true); break; case 6: auto_join_small_server(); break; case 7: credits(); break; } }); popup.appendChild(buttonContainer); }); return popup; } /******************************************************* name of function: An Observer for the filter button description: to put the filter button on the page *******************************************************/ // Wait for the server list options container to load const observer = new MutationObserver((mutations, obs) => { const serverListOptions = document.querySelector('.server-list-options'); if (serverListOptions) { // Create the filter button const filterButton = document.createElement('a'); filterButton.className = 'RL-filter-button'; // Unique class name filterButton.style.cssText = ` color: white; font-weight: bold; text-decoration: none; cursor: pointer; margin-left: 10px; padding: 5px 10px; display: flex; align-items: center; gap: 5px; position: relative; margin-top: 4px `; filterButton.addEventListener('mouseover', () => { filterButton.style.textDecoration = 'underline'; }); filterButton.addEventListener('mouseout', () => { filterButton.style.textDecoration = 'none'; }); // Add the "Filter" text const buttonText = document.createElement('span'); buttonText.className = 'RL-filter-text'; // Unique class name buttonText.textContent = 'Filters'; filterButton.appendChild(buttonText); // Add the icon (three horizontal dashes) const icon = document.createElement('span'); icon.className = 'RL-filter-icon'; // Unique class name icon.textContent = '≡'; icon.style.cssText = ` font-size: 18px; `; filterButton.appendChild(icon); // Append the button to the server list options container serverListOptions.appendChild(filterButton); // Handle click event to show/hide the popup let popup = null; filterButton.addEventListener('click', (event) => { event.stopPropagation(); // Prevent event bubbling if (popup) { popup.remove(); // Remove the popup if it already exists popup = null; } else { popup = createPopup(); // Position the popup next to the filter button popup.style.top = `${filterButton.offsetHeight}px`; popup.style.left = '0'; filterButton.appendChild(popup); } }); // Close the popup when clicking outside document.addEventListener('click', (event) => { if (popup && !filterButton.contains(event.target)) { popup.remove(); popup = null; } }); // Stop observing once the button is added obs.disconnect(); } }) /********************************************************************************************************************************************************************************************************************************************* The End of: This is all of the functions for the filter button and the popup for the 8 buttons does not include the functions for the 8 buttons *********************************************************************************************************************************************************************************************************************************************/ /********************************************************************************************************************************************************************************************************************************************* Functions for the 1st button *********************************************************************************************************************************************************************************************************************************************/ /******************************************************* name of function: smallest_servers FIRST FUNCTION description: Fetches the smallest servers, disables the "Load More" button, shows a loading bar, and recreates the server cards. *******************************************************/ async function smallest_servers() { // Disable the "Load More" button and show the loading bar Loadingbar(true); disableFilterButton(true); disableLoadMoreButton(); // Get the game ID from the URL const gameId = window.location.pathname.split('/')[2]; // Retry mechanism let retries = 3; let success = false; while (retries > 0 && !success) { try { // Fetch server data from the Roblox API const response = await fetch(`https://games.roblox.com/v1/games/${gameId}/servers/0?sortOrder=1&excludeFullGames=true&limit=100`); // Check if the response status is 429 (Too Many Requests) if (response.status === 429) { throw new Error('429: Too Many Requests'); } const data = await response.json(); // Process each server for (const server of data.data) { const { id: serverId, playerTokens, maxPlayers, playing } = server; // Pass the server data to the card creation function await rbx_card(serverId, playerTokens, maxPlayers, playing, gameId); } success = true; // Mark as successful if no errors occurred } catch (error) { retries--; // Decrement the retry count if (error.message === '429: Too Many Requests' && retries > 0) { console.log('Encountered a 429 error. Retrying in 10 seconds...'); await new Promise(resolve => setTimeout(resolve, 10000)); // Wait for 10 seconds } else { console.error('Error fetching server data:', error); break; // Exit the loop if it's not a 429 error or no retries left } } finally { if (success || retries === 0) { // Hide the loading bar and enable the filter button Loadingbar(false); disableFilterButton(false); } } } } /********************************************************************************************************************************************************************************************************************************************* Functions for the 2nd button *********************************************************************************************************************************************************************************************************************************************/ /******************************************************* name of function: available_space_servers description: Fetches servers with available space, disables the "Load More" button, shows a loading bar, and recreates the server cards. *******************************************************/ async function available_space_servers() { // Disable the "Load More" button and show the loading bar Loadingbar(true); disableLoadMoreButton(); disableFilterButton(true); // Get the game ID from the URL const gameId = window.location.pathname.split('/')[2]; // Retry mechanism let retries = 3; let success = false; while (retries > 0 && !success) { try { // Fetch server data from the Roblox API const response = await fetch(`https://games.roblox.com/v1/games/${gameId}/servers/0?sortOrder=2&excludeFullGames=true&limit=100`); // Check if the response status is 429 (Too Many Requests) if (response.status === 429) { throw new Error('429: Too Many Requests'); } const data = await response.json(); // Process each server for (const server of data.data) { const { id: serverId, playerTokens, maxPlayers, playing } = server; // Pass the server data to the card creation function await rbx_card(serverId, playerTokens, maxPlayers, playing, gameId); } success = true; // Mark as successful if no errors occurred } catch (error) { retries--; // Decrement the retry count if (error.message === '429: Too Many Requests' && retries > 0) { console.log('Encountered a 429 error. Retrying in 10 seconds...'); await new Promise(resolve => setTimeout(resolve, 10000)); // Wait for 10 seconds } else { console.error('Error fetching server data:', error); break; // Exit the loop if it's not a 429 error or no retries left } } finally { if (success || retries === 0) { // Hide the loading bar and enable the filter button Loadingbar(false); disableFilterButton(false); } } } } /********************************************************************************************************************************************************************************************************************************************* Functions for the 3rd button *********************************************************************************************************************************************************************************************************************************************/ /******************************************************* name of function: player_count_tab description: Opens a popup for the user to select the max player count using a slider and filters servers accordingly. *******************************************************/ function player_count_tab() { // Create the popup container const popup = document.createElement('div'); popup.className = 'player-count-popup'; popup.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: rgb(30, 32, 34); padding: 20px; border-radius: 5px; z-index: 10000; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); display: flex; flex-direction: column; align-items: center; gap: 10px; `; // Add a title const title = document.createElement('h3'); title.textContent = 'Select Max Player Count'; title.style.cssText = ` color: white; margin: 0; font-size: 18px; `; popup.appendChild(title); // Add a slider const slider = document.createElement('input'); slider.type = 'range'; slider.min = '1'; slider.max = '100'; slider.value = '1'; // Default value slider.style.cssText = ` width: 200px; cursor: pointer; `; popup.appendChild(slider); // Add a display for the slider value const sliderValue = document.createElement('span'); sliderValue.textContent = slider.value; sliderValue.style.cssText = ` color: white; font-size: 16px; `; popup.appendChild(sliderValue); // Update the slider value display when the slider changes slider.addEventListener('input', () => { sliderValue.textContent = slider.value; }); // Add a submit button const submitButton = document.createElement('button'); submitButton.textContent = 'Search'; submitButton.style.cssText = ` padding: 5px 10px; font-size: 16px; background-color: #00A2FF; color: white; border: none; border-radius: 3px; cursor: pointer; `; popup.appendChild(submitButton); // Add a close button const closeButton = document.createElement('button'); closeButton.textContent = 'Close'; closeButton.style.cssText = ` padding: 5px 10px; font-size: 16px; background-color: #555; color: white; border: none; border-radius: 3px; cursor: pointer; `; popup.appendChild(closeButton); // Append the popup to the body document.body.appendChild(popup); // Handle submit button click submitButton.addEventListener('click', () => { const maxPlayers = parseInt(slider.value, 10); if (!isNaN(maxPlayers) && maxPlayers > 0) { filterServersByPlayerCount(maxPlayers); popup.remove(); } else { notifications('Error: Please enter a number greater than 0', 'error', '⚠️'); } }); // Handle close button click closeButton.addEventListener('click', () => { popup.remove(); }); // Close the popup when clicking outside document.addEventListener('click', (event) => { if (!popup.contains(event.target)) { popup.remove(); } }); } /******************************************************* name of function: fetchServersWithRetry description: Fetches server data with retry logic and a delay between requests to avoid rate-limiting. Uses GM_xmlhttpRequest instead of fetch. *******************************************************/ async function fetchServersWithRetry(url, retries = 15, currentDelay = 750) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) { // Check for 429 Rate Limit error if (response.status === 429) { if (retries > 0) { const newDelay = currentDelay * 1; // Exponential backoff console.log(`[DEBUG] Rate limited. Waiting ${newDelay / 1000} seconds before retrying...`); setTimeout(() => { resolve(fetchServersWithRetry(url, retries - 1, newDelay)); // Retry with increased delay }, newDelay); } else { console.error('[DEBUG] Rate limit retries exhausted.'); notifications('Error: Rate limited please try again later.', 'error', '⚠️') reject(new Error('RateLimit')); } return; } // Handle other HTTP errors if (response.status < 200 || response.status >= 300) { console.error('[DEBUG] HTTP error:', response.status, response.statusText); reject(new Error(`HTTP error: ${response.status}`)); return; } // Parse and return the JSON data try { const data = JSON.parse(response.responseText); console.log('[DEBUG] Fetched data successfully:', data); resolve(data); } catch (error) { console.error('[DEBUG] Error parsing JSON:', error); reject(error); } }, onerror: function(error) { console.error('[DEBUG] Error in GM_xmlhttpRequest:', error); reject(error); } }); }); } /******************************************************* name of function: filterServersByPlayerCount description: Filters servers to show only those with a player count equal to or below the specified max. If no exact matches are found, prioritizes servers with player counts lower than the input. Keeps fetching until at least 8 servers are found, with a dynamic delay between requests. *******************************************************/ async function filterServersByPlayerCount(maxPlayers) { // Validate maxPlayers before proceeding if (isNaN(maxPlayers) || maxPlayers < 1 || !Number.isInteger(maxPlayers)) { console.error('[DEBUG] Invalid input for maxPlayers.'); notifications('Error: Please input a valid whole number greater than or equal to 1.', 'error', '⚠️'); return; } // Disable UI elements and clear the server list Loadingbar(true); disableLoadMoreButton(); disableFilterButton(true); const serverList = document.querySelector('#rbx-game-server-item-container'); serverList.innerHTML = ''; const gameId = window.location.pathname.split('/')[2]; let cursor = null; let serversFound = 0; let serverMaxPlayers = null; let isCloserToOne = null; let topDownServers = []; // Servers collected during top-down search let bottomUpServers = []; // Servers collected during bottom-up search let currentDelay = 500; // Initial delay of 0.5 seconds const timeLimit = 3 * 60 * 1000; // 3 minutes in milliseconds const startTime = Date.now(); // Record the start time notifications('Will search for a maximum of 3 minutes to find a server.', 'success', '🔎'); try { while (serversFound < 16) { // Check if the time limit has been exceeded if (Date.now() - startTime > timeLimit) { console.log('[DEBUG] Time limit reached. Proceeding to fallback servers.'); notifications('Warning: Time limit reached. Proceeding to fallback servers.', 'warning', '❗'); break; } // Fetch initial data to determine serverMaxPlayers and isCloserToOne if (!serverMaxPlayers) { const initialUrl = cursor ? `https://games.roblox.com/v1/games/${gameId}/servers/public?excludeFullGames=true&limit=100&cursor=${cursor}` : `https://games.roblox.com/v1/games/${gameId}/servers/public?excludeFullGames=true&limit=100`; const initialData = await fetchServersWithRetry(initialUrl); if (initialData.data.length > 0) { serverMaxPlayers = initialData.data[0].maxPlayers; isCloserToOne = maxPlayers <= (serverMaxPlayers / 2); } else { console.error('[DEBUG] No servers found in initial fetch.'); break; } } // Validate maxPlayers against serverMaxPlayers if (maxPlayers >= serverMaxPlayers) { console.error('[DEBUG] Invalid input: maxPlayers is greater than or equal to serverMaxPlayers.'); notifications(`Error: Please input a number between 1 through ${serverMaxPlayers - 1}`, 'error', '⚠️'); return; } // Adjust the URL based on isCloserToOne const baseUrl = isCloserToOne ? `https://games.roblox.com/v1/games/${gameId}/servers/public?sortOrder=1&excludeFullGames=true&limit=100` : `https://games.roblox.com/v1/games/${gameId}/servers/public?excludeFullGames=true&limit=100`; // why does this work lmao const url = cursor ? `${baseUrl}&cursor=${cursor}` : baseUrl; const data = await fetchServersWithRetry(url); // Safety check: Ensure the server list is valid and iterable if (!Array.isArray(data.data)) { console.error('[DEBUG] Invalid server list received. Waiting 1 second before retrying...'); await delay(1000); // Wait 1 second before retrying continue; // Skip the rest of the loop and retry } // Filter and process servers for (const server of data.data) { if (server.playing === maxPlayers) { await rbx_card(server.id, server.playerTokens, server.maxPlayers, server.playing, gameId); serversFound++; if (serversFound >= 16) { break; } } else if (!isCloserToOne && server.playing > maxPlayers) { topDownServers.push(server); // Add to top-down fallback list } else if (isCloserToOne && server.playing < maxPlayers) { bottomUpServers.push(server); // Add to bottom-up fallback list } } // Exit if no more servers are available if (!data.nextPageCursor) { break; } cursor = data.nextPageCursor; // Adjust delay dynamically if (currentDelay > 150) { currentDelay = Math.max(150, currentDelay / 2); // Gradually reduce delay } console.log(`[DEBUG] Waiting ${currentDelay / 1000} seconds before next request...`); await delay(currentDelay); } // If no exact matches were found or time limit reached, use fallback servers if (serversFound === 0 && (topDownServers.length > 0 || bottomUpServers.length > 0)) { // Sort top-down servers by player count (ascending) topDownServers.sort((a, b) => a.playing - b.playing); // Sort bottom-up servers by player count (descending) bottomUpServers.sort((a, b) => b.playing - a.playing); // Combine both fallback lists (prioritize top-down servers first) const combinedFallback = [...topDownServers, ...bottomUpServers]; for (const server of combinedFallback) { await rbx_card(server.id, server.playerTokens, server.maxPlayers, server.playing, gameId); serversFound++; if (serversFound >= 16) { break; } } } if (serversFound <= 0) { notifications('No Servers Found Within The Provided Criteria', 'info', '🔎'); } } catch (error) { console.error('[DEBUG] Error in filterServersByPlayerCount:', error); } finally { Loadingbar(false); disableFilterButton(false); } } /********************************************************************************************************************************************************************************************************************************************* Functions for the 4th button *********************************************************************************************************************************************************************************************************************************************/ /******************************************************* name of function: random_servers description: Fetches servers from two different URLs, combines the results, ensures no duplicates, shuffles the list, and passes the server information to the rbx_card function in a random order. Handles 429 errors with retries. *******************************************************/ async function random_servers() { // Disable the "Load More" button and show the loading bar Loadingbar(true); disableFilterButton(true); disableLoadMoreButton(); // Get the game ID from the URL const gameId = window.location.pathname.split('/')[2]; try { // Fetch servers from the first URL with retry logic const firstUrl = `https://games.roblox.com/v1/games/${gameId}/servers/public?excludeFullGames=true&limit=10`; const firstData = await fetchWithRetry(firstUrl, 3); // Retry up to 3 times // Wait for 5 seconds await delay(5000); // Fetch servers from the second URL with retry logic const secondUrl = `https://games.roblox.com/v1/games/${gameId}/servers/public?sortOrder=1&excludeFullGames=true&limit=10`; const secondData = await fetchWithRetry(secondUrl, 3); // Retry up to 3 times // Combine the servers from both URLs const combinedServers = [...firstData.data, ...secondData.data]; // Remove duplicates by server ID const uniqueServers = []; const seenServerIds = new Set(); for (const server of combinedServers) { if (!seenServerIds.has(server.id)) { seenServerIds.add(server.id); uniqueServers.push(server); } } // Shuffle the unique servers array const shuffledServers = shuffleArray(uniqueServers); // Get the first 16 shuffled servers const selectedServers = shuffledServers.slice(0, 16); // Process each server in random order for (const server of selectedServers) { const { id: serverId, playerTokens, maxPlayers, playing } = server; // Pass the server data to the card creation function await rbx_card(serverId, playerTokens, maxPlayers, playing, gameId); } } catch (error) { console.error('Error fetching server data:', error); notifications('Error: Failed to fetch server data. Please try again later.', 'error', '⚠️'); } finally { // Hide the loading bar and enable the filter button Loadingbar(false); disableFilterButton(false); } } /******************************************************* name of function: fetchWithRetry description: Fetches data from a URL with retry logic for 429 errors. this is for this unique function *******************************************************/ async function fetchWithRetry(url, retries) { for (let i = 0; i < retries; i++) { try { const response = await fetch(url); if (response.status === 429) { // If 429 error, wait 10 seconds and retry console.log(`Rate limited. Retrying in 10 seconds... (Attempt ${i + 1}/${retries})`); await delay(10000); // Wait 10 seconds continue; } if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return await response.json(); } catch (error) { if (i === retries - 1) { // If no retries left, throw the error throw error; } } } } /******************************************************* name of function: shuffleArray description: Shuffles an array using the Fisher-Yates algorithm. *******************************************************/ function shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); // Random index from 0 to i [array[i], array[j]] = [array[j], array[i]]; // Swap elements } return array; } /********************************************************************************************************************************************************************************************************************************************* Functions for the 5th button. taken from my other project *********************************************************************************************************************************************************************************************************************************************/ // so we inject css into the page. if ur on light mode some stuff may look weird so not my fault const style = document.createElement('style'); style.textContent = ` /* Overlay for the stupid thingy black screen*/ .overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); /* Dark semi-transparent background */ z-index: 1000; /* Ensure overlay is below the popup */ } /* Popup Container for the server region*/ .filter-popup { background-color: #2d2d2d; /* Dark background */ color: #ffffff; /* White text */ padding: 20px; border-radius: 10px; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); width: 300px; max-width: 90%; position: fixed; /* Fixed positioning */ top: 50%; /* Center vertically */ left: 50%; /* Center horizontally */ transform: translate(-50%, -50%); /* Offset to truly center */ text-align: center; z-index: 1001; /* Ensure popup is above the overlay */ } /* Close Button for the server selector*/ #closePopup { position: absolute; top: 10px; right: 10px; background: #ff4444; /* Red background */ border: none; color: white; font-size: 16px; cursor: pointer; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; } #closePopup:hover { background: #cc0000; /* Darker red on hover */ } /* Label */ .filter-popup label { display: block; margin-bottom: 10px; font-size: 16px; color: #ffffff; } /* Dropdown */ .filter-popup select { background-color: #444; /* Dark gray background */ color: #ffffff; /* White text */ padding: 8px; border-radius: 5px; border: 1px solid #666; /* Gray border */ width: 100%; margin-bottom: 10px; font-size: 14px; } .filter-popup select:focus { border-color: #888; /* Lighter border on focus */ outline: none; } /* Custom Input */ .filter-popup input[type="number"] { background-color: #444; /* Dark gray background */ color: #ffffff; /* White text */ padding: 8px; border-radius: 5px; border: 1px solid #666; /* Gray border */ width: 100%; margin-bottom: 10px; font-size: 14px; } .filter-popup input[type="number"]:focus { border-color: #888; /* Lighter border on focus */ outline: none; } /* Confirm Button */ #confirmServerCount { background-color: #444; /* Dark gray background */ color: #ffffff; /* White text */ padding: 8px 16px; border: 1px solid #666; /* Gray border */ border-radius: 5px; cursor: pointer; font-size: 14px; width: 100%; transition: background-color 0.3s ease; } #confirmServerCount:hover { background-color: #666; /* Lighter gray on hover */ } .rbx-game-server-item.highlighted { border: 2px solid green; border-radius: 8px; } .fetch-button:disabled { opacity: 0.5; cursor: not-allowed; } `; document.head.appendChild(style); // Function to show the message under the "Load More" button function showMessage(message) { const loadMoreButtonContainer = document.querySelector('.rbx-running-games-footer'); if (!loadMoreButtonContainer) { console.error("Load More button container not found!"); return; } // Create the message element const messageElement = document.createElement('div'); messageElement.className = 'filter-message'; messageElement.textContent = message; // Clear any existing message and append the new one const existingMessage = loadMoreButtonContainer.querySelector('.filter-message'); if (existingMessage) { existingMessage.remove(); // Remove the existing message if it exists } loadMoreButtonContainer.appendChild(messageElement); return messageElement; } // Function to hide the message of the showmessage functioon function hideMessage() { const messageElement = document.querySelector('.filter-message'); if (messageElement) messageElement.remove(); } // Function to show the popup for random stuff function showPopup() { const overlay = document.createElement('div'); overlay.className = 'overlay'; const popup = document.createElement('div'); popup.className = 'filter-popup'; popup.textContent = 'Filtering servers, please wait...'; document.body.appendChild(overlay); document.body.appendChild(popup); return popup; } // Function to hide the popup for the stuff function hidePopup() { const popup = document.querySelector('.filter-popup'); const overlay = document.querySelector('.overlay'); if (popup) popup.remove(); if (overlay) overlay.remove(); } // Function to fetch server details so game id and job id. yea! async function fetchServerDetails(gameId, jobId) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: "https://gamejoin.roblox.com/v1/join-game-instance", // url for game id headers: { // doesent need cookie cuase of magic "Content-Type": "application/json", "User-Agent": "Roblox/WinInet", }, data: JSON.stringify({ placeId: gameId, gameId: jobId }), onload: function(response) { const json = JSON.parse(response.responseText); console.log("API Response:", json); // This prints the full response // Check if the response indicates that the user needs to purchase the game if (json.status === 12 && json.message === 'You need to purchase access to this game before you can play.') { // yea error message! reject('purchase_required'); // Special error code for this case yea! return; } const address = json?.joinScript?.UdmuxEndpoints?.[0]?.Address ?? json?.joinScript?.MachineAddress; if (!address) { console.error("API Response (Unknown Location) Which means Full Server!:", json); // Log the API response for debug reject(`Unable to fetch server location: Status ${json.status}`); // debug return; } const location = serverRegionsByIp[address.replace(/^(128\.116\.\d+)\.\d+$/, "$1.0")]; // lmao all servers atart with this so yea dont argue with me if (!location) { console.error("API Response (Unknown Location):", json); // Log the API response into the chat. might remove it from production but idc rn reject(`Unknown server address ${address}`); return; } resolve(location); }, onerror: function(error) { console.error("API Request Failed:", error); // damn if this happpens idk what to tell u reject(`Failed to fetch server details: ${error}`); }, }); }); } // cusomt delay also known as sleep fucntion in js cause this language sucks and doesent have a default function function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Function to create a popup for selecting the number of servers // basically yea thats what it doesent function createServerCountPopup(callback) { const overlay = document.createElement('div'); overlay.className = 'overlay'; const popup = document.createElement('div'); popup.className = 'filter-popup'; // reason 100 is selected because thjats how many the api will show per request popup.innerHTML = ` `; document.body.appendChild(overlay); document.body.appendChild(popup); const serverCountDropdown = popup.querySelector('#serverCount'); const customServerCountInput = popup.querySelector('#customServerCount'); const confirmButton = popup.querySelector('#confirmServerCount'); const closeButton = popup.querySelector('#closePopup'); // Show/hide custom input based on dropdown selection serverCountDropdown.addEventListener('change', () => { if (serverCountDropdown.value === 'custom') { customServerCountInput.style.display = 'block'; } else { customServerCountInput.style.display = 'none'; } }); // button click on start or what ever confirmButton.addEventListener('click', () => { let serverCount; if (serverCountDropdown.value === 'custom') { serverCount = parseInt(customServerCountInput.value); // Validate custom input if (isNaN(serverCount) || serverCount < 1 || serverCount > 1000) { notifications('Error: Please enter a valid number between 1 and 1000.', 'error', '⚠️') return; } } else { serverCount = parseInt(serverCountDropdown.value); } // Show an alert if the user selects a number above 100 if (serverCount > 100) { // error cause people dont know about this maybe. idk yea so here. also if u think this is a stupid way i should have done it before the button press idc so yea notifications('Warning: Searching over 100 servers may take some time and you might get rate limited!', 'warning', '❗'); } // Pass the selected server count to the callback callback(serverCount); disableFilterButton(true); // disbale filter button disableLoadMoreButton(true); // disable load more button notifications('Note: Filter Button is disabled as this function is resource intensive. \nRefresh the page to call other functions/press other buttons.', 'info', '⚠️') hidePopup(); Loadingbar(true); // enable loading bar }); // Close button logic :)) closeButton.addEventListener('click', () => { hidePopup(); }); // Function to hide the popup // yea im dumb and used the same function name but it works and im too lazy to change it function hidePopup() { document.body.removeChild(overlay); document.body.removeChild(popup); } } // Function to fetch public servers // totallimit is amount of sevrers to fetch async function fetchPublicServers(gameId, totalLimit) { let servers = []; let cursor = null; while (servers.length < totalLimit) { // too lazy to comment any of this. hopefully i remember what this does in the future const url = `https://games.roblox.com/v1/games/${gameId}/servers/public?excludeFullGames=true&limit=100${cursor ? `&cursor=${cursor}` : ''}`; const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, onload: function(response) { resolve(JSON.parse(response.responseText)); }, onerror: function(error) { reject(`Failed to fetch public servers: ${error}`); }, }); }); servers = servers.concat(response.data); if (!response.nextPageCursor || servers.length >= totalLimit) { break; } cursor = response.nextPageCursor; await delay(3000); // wait 3 seconds before each page request. if u think this is slow i tried 1 second i got rate limited :| } return servers.slice(0, totalLimit); } // Function to create dropdown menus for filtering function createFilterDropdowns(servers) { const filterContainer = document.createElement('div'); filterContainer.className = 'filter-container'; const countryDropdown = document.createElement('select'); countryDropdown.id = 'countryFilter'; countryDropdown.innerHTML = ''; countryDropdown.style.backgroundColor = '#333'; // Dark gray background countryDropdown.style.color = '#fff'; // White text countryDropdown.style.borderRadius = '8px'; // Rounded corners countryDropdown.style.padding = '8px'; // Increase size countryDropdown.style.fontSize = '16px'; // Increase font size countryDropdown.style.border = 'none'; // Remove default border const cityDropdown = document.createElement('select'); cityDropdown.id = 'cityFilter'; cityDropdown.innerHTML = ''; cityDropdown.style.backgroundColor = '#333'; // Dark gray background cityDropdown.style.color = '#fff'; // White text cityDropdown.style.borderRadius = '8px'; // Rounded corners cityDropdown.style.padding = '8px'; // Increase size hehehehe cityDropdown.style.fontSize = '16px'; // Increase font size cityDropdown.style.border = 'none'; // Remove default border cityDropdown.style.marginLeft = '5px'; // move right cause im too lazy to fix // Count the number of servers per country and add them to the dropdown const countryCounts = {}; servers.forEach(server => { const country = server.location.country.name; countryCounts[country] = (countryCounts[country] || 0) + 1; }); // Populate country dropdown with server counts Object.keys(countryCounts).forEach(country => { const option = document.createElement('option'); option.value = country; option.textContent = `${country} (${countryCounts[country]})`; countryDropdown.appendChild(option); }); // add the city dropdown based on selected country countryDropdown.addEventListener('change', () => { const selectedCountry = countryDropdown.value; cityDropdown.innerHTML = ''; if (selectedCountry) { // Count the number of servers per city in the selected country const cityCounts = {}; servers .filter(server => server.location.country.name === selectedCountry) .forEach(server => { const city = server.location.city; const region = server.location.region?.name; const cityKey = region ? `${city}, ${region}` : city; cityCounts[cityKey] = (cityCounts[cityKey] || 0) + 1; }); // Populate city dropdown with server counts Object.keys(cityCounts).forEach(city => { const option = document.createElement('option'); option.value = city; option.textContent = `${city} (${cityCounts[city]})`; cityDropdown.appendChild(option); }); // Auto-select the city if there's only one make users life easier // wow ik i made the users life easier for once thats crazy!!! :OOOOO const cities = Object.keys(cityCounts); if (cities.length === 1) { cityDropdown.value = cities[0]; // displayFilteredServers(selectedCountry, cities[0]); // if this breaks something which it doesent seem like it i will enable it later } } }); filterContainer.appendChild(countryDropdown); filterContainer.appendChild(cityDropdown); return filterContainer; } // Function to filter servers based on selected country and city cause im lazy function filterServers(servers, country, city) { return servers.filter(server => { const matchesCountry = !country || server.location.country.name === country; const matchesCity = !city || `${server.location.city}${server.location.region?.name ? `, ${server.location.region.name}` : ''}` === city; return matchesCountry && matchesCity; }); } // Function to sort servers by ping. maybe inaccurate but thats roblox's problem not mine function sortServersByPing(servers) { return servers.sort((a, b) => a.server.ping - b.server.ping); } async function fetchPlayerThumbnails_servers(playerTokens) { const body = playerTokens.map(token => ({ requestId: `0:${token}:AvatarHeadshot:150x150:png:regular`, type: "AvatarHeadShot", targetId: 0, token, format: "png", size: "150x150", })); const response = await fetch("https://thumbnails.roblox.com/v1/batch", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify(body), }); const data = await response.json(); return data.data || []; } async function rebuildServerList(gameId, totalLimit, best_connection) { const serverListContainer = document.getElementById("rbx-game-server-item-container"); // If "Best Connection" is enabled // FUNCTION FOR THE 6TH BUTTON! if (best_connection === true) { // Ask for the user's location const userLocation = await getUserLocation(); if (!userLocation) { //notifications('Error: Unable to fetch your location. Please enable location access.', 'error', '⚠️'); return; } // Fetch 50 servers const servers = await fetchPublicServers(gameId, 50); if (servers.length === 0) { notifications('Error: No servers found. Please try again later.', 'error', '⚠️'); return; } // Calculate distances and find the closest server let closestServer = null; let minDistance = Infinity; let closestServerLocation = null; for (const server of servers) { const { id: serverId, maxPlayers, playing } = server; // Skip full servers if (playing >= maxPlayers) { continue; } try { // Fetch server location const location = await fetchServerDetails(gameId, serverId); // Calculate distance const distance = calculateDistance( userLocation.latitude, userLocation.longitude, location.latitude, location.longitude ); // Update closest server if (distance < minDistance) { minDistance = distance; closestServer = server; closestServerLocation = location; } } catch (error) { console.error(`Error fetching details for server ${serverId}:`, error); // Skip this server and continue with the next one continue; } } if (closestServer) { // Automatically join the closest server Roblox.GameLauncher.joinGameInstance(gameId, closestServer.id); notifications(`Joining nearest server! Server ID: ${closestServer.id} Distance: ${(minDistance / 1.609).toFixed(2)} miles | ${minDistance.toFixed(2)} km Location (Country): ${closestServerLocation.country.name}.`, 'success', '🚀'); disableFilterButton(false); Loadingbar(false); } else { notifications('No valid servers found. Please try again later after refreshing the webpage.', 'error', '⚠️'); } return; // Exit the function after joining the best server } // Rest of the original function (for non-"Best Connection" mode) if (!serverListContainer) { console.error("Server list container not found!"); const popup = showPopup(); notifications('Error: No Servers found. There is nobody playing this game. :(', 'warning', '❗'); return; } const messageElement = showMessage("Filtering servers, please wait..."); try { const servers = await fetchPublicServers(gameId, totalLimit); const totalServers = servers.length; let skippedServers = 0; messageElement.textContent = `Filtering servers, please do not leave this page as it slows down the search...\n${totalServers} servers found, 0 servers loaded.`; notifications(`Please do not leave this page as it slows down the search. \nFound a total of ${totalServers} servers found.`, 'success', '👍'); const serverDetails = []; for (let i = 0; i < servers.length; i++) { const server = servers[i]; const { id: serverId, maxPlayers, playing, ping, fps, playerTokens } = server; let location; try { location = await fetchServerDetails(gameId, serverId); } catch (error) { if (error === 'purchase_required') { messageElement.textContent = "Cannot access server data because you haven't purchased the game."; notifications('Error: Cannot access server data because you haven\'t purchased the game.', 'error', '⚠️'); Loadingbar(false); // disable loading bar return; } else { console.error(error); location = { city: "Unknown", country: { name: "Unknown", code: "??" } }; } } if (location.city === "Unknown" || playing >= maxPlayers) { console.log(`Skipping server ${serverId} because it is full or location is unknown.`); skippedServers++; continue; } // Fetch player thumbnails const playerThumbnails = playerTokens && playerTokens.length > 0 ? await fetchPlayerThumbnails_servers(playerTokens) : []; serverDetails.push({ server, location, playerThumbnails }); messageElement.textContent = `Filtering servers, please do not leave this page...\n${totalServers} servers found, ${i + 1} server locations found`; } if (serverDetails.length === 0) { messageElement.textContent = "No servers found. Please try again with an increase in the number of servers to search for."; notifications('Error: No servers found. Please try again with an increase in the number of servers to search for.', 'error', '⚠️'); Loadingbar(false); // disable loading bar return; } const loadedServers = totalServers - skippedServers; notifications(`Filtering complete!\n${totalServers} servers found, ${loadedServers} servers loaded, ${skippedServers} servers skipped (full).`, 'success', '👍'); messageElement.textContent = `Filtering complete!\n${totalServers} servers found, ${loadedServers} servers loaded, ${skippedServers} servers skipped (full).`; Loadingbar(false); // disable loading bar // Add filter dropdowns const filterContainer = createFilterDropdowns(serverDetails); serverListContainer.parentNode.insertBefore(filterContainer, serverListContainer); // Style the server list container to use a grid layout serverListContainer.style.display = "grid"; serverListContainer.style.gridTemplateColumns = "repeat(4, 1fr)"; // 4 columns serverListContainer.style.gap = "16px"; // Gap between cards const displayFilteredServers = (country, city) => { serverListContainer.innerHTML = ""; const filteredServers = filterServers(serverDetails, country, city); const sortedServers = sortServersByPing(filteredServers); sortedServers.forEach(({ server, location, playerThumbnails }) => { const serverCard = document.createElement("li"); serverCard.className = "rbx-game-server-item col-md-3 col-sm-4 col-xs-6"; // Set consistent width and height for the server card serverCard.style.width = "100%"; // Take up full width of the grid cell serverCard.style.minHeight = "400px"; // Set a minimum height serverCard.style.display = "flex"; serverCard.style.flexDirection = "column"; serverCard.style.justifyContent = "space-between"; serverCard.style.boxSizing = "border-box"; // Include padding and border in dimensions // Remove any conflicting outline (e.g., from .highlighted class) serverCard.style.outline = 'none'; // Determine the group and set the outline color let outlineColor; if (server.ping < 100) { outlineColor = 'green'; // Best ping } else if (server.ping < 200) { outlineColor = 'orange'; // Medium ping } else { outlineColor = 'red'; // Bad ping } // Apply the new outline and outlineOffset serverCard.style.outline = `3px solid ${outlineColor}`; serverCard.style.outlineOffset = '-6px'; serverCard.style.padding = '6px'; serverCard.style.borderRadius = '8px'; // Create a container for player thumbnails const thumbnailsContainer = document.createElement("div"); thumbnailsContainer.className = "player-thumbnails-container"; thumbnailsContainer.style.display = "grid"; thumbnailsContainer.style.gridTemplateColumns = "repeat(3, 60px)"; // 3 columns thumbnailsContainer.style.gridTemplateRows = "repeat(2, 60px)"; // 2 rows thumbnailsContainer.style.gap = "5px"; thumbnailsContainer.style.marginBottom = "10px"; // Add player thumbnails to the container (max 5) const maxThumbnails = 5; const displayedThumbnails = playerThumbnails.slice(0, maxThumbnails); displayedThumbnails.forEach(thumb => { if (thumb && thumb.imageUrl) { const img = document.createElement("img"); img.src = thumb.imageUrl; img.className = "avatar-card-image"; img.style.width = "60px"; img.style.height = "60px"; img.style.borderRadius = "50%"; thumbnailsContainer.appendChild(img); } }); // Add a placeholder for hidden players const hiddenPlayers = server.playing - displayedThumbnails.length; if (hiddenPlayers > 0) { const placeholder = document.createElement("div"); placeholder.className = "avatar-card-image"; placeholder.style.width = "60px"; placeholder.style.height = "60px"; placeholder.style.borderRadius = "50%"; placeholder.style.backgroundColor = "#BDBEBE80"; // Dark gray background placeholder.style.display = "flex"; placeholder.style.alignItems = "center"; placeholder.style.justifyContent = "center"; placeholder.style.color = "#fff"; // White text placeholder.style.fontSize = "14px"; placeholder.textContent = `+${hiddenPlayers}`; thumbnailsContainer.appendChild(placeholder); } // Server card content const cardItem = document.createElement("div"); cardItem.className = "card-item"; cardItem.style.display = "flex"; cardItem.style.flexDirection = "column"; cardItem.style.justifyContent = "space-between"; cardItem.style.height = "100%"; // Ensure the card content takes up the full height cardItem.innerHTML = ` ${thumbnailsContainer.outerHTML}
This project was created by: