// ==UserScript==
// @name RoLocate
// @namespace https://oqarshi.github.io/
// @version 38.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/*
// @license CC-BY-NC-SA-4.0; https://creativecommons.org/licenses/by-nc-sa/4.0/
// @icon https://oqarshi.github.io/Invite/rolocate/assets/logo.svg
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_listValues
// @grant GM_setValue
// @grant GM_deleteValue
// @require https://update.greasyfork.icu/scripts/535590/1586769/Rolocate%20Base64%20Image%20Library%2020.js
// @require https://update.greasyfork.icu/scripts/539427/1607754/Rolocate%20Server%20Region%20Data.js
// @downloadURL none
// ==/UserScript==
(function() {
'use strict';
/*******************************************************
name of function: ConsoleLogEnabled
description: console.logs eveyrthing if settings is turned
on
*******************************************************/
function ConsoleLogEnabled(...args) {
if (localStorage.getItem("ROLOCATE_enableLogs") === "true") {
console.log("[ROLOCATE]", ...args);
}
}
/*******************************************************
name of function: notifications
description: the notifications function
*******************************************************/
function notifications(message, type = 'info', emoji = '', duration = 3000) {
if (localStorage.getItem('ROLOCATE_enablenotifications') !== 'true') {
return;
}
// Inject minimal CSS once
if (!document.getElementById('sleek-toast-styles')) {
const style = document.createElement('style');
style.id = 'sleek-toast-styles';
style.innerHTML = `
@keyframes slideIn {
from { opacity: 0; transform: translateX(100%); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes slideOut {
from { opacity: 1; transform: translateX(0); }
to { opacity: 0; transform: translateX(100%); }
}
@keyframes shrink {
from { width: 100%; }
to { width: 0%; }
}
#toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 999999;
display: flex;
flex-direction: column;
gap: 8px;
pointer-events: none;
}
.toast {
background: rgba(45, 45, 45, 0.95);
color: #e8e8e8;
padding: 12px 16px;
border-radius: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
font-weight: 500;
min-width: 280px;
max-width: 400px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
animation: slideIn 0.3s ease-out;
pointer-events: auto;
position: relative;
overflow: hidden;
}
.toast.removing {
animation: slideOut 0.3s ease-in forwards;
}
.toast:hover {
background: rgba(55, 55, 55, 0.98);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
}
.toast-content {
display: flex;
align-items: center;
gap: 10px;
}
.toast-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.toast-emoji {
font-size: 16px;
flex-shrink: 0;
}
.toast-message {
flex: 1;
line-height: 1.4;
}
.toast-close {
position: absolute;
top: 4px;
right: 6px;
width: 20px;
height: 20px;
cursor: pointer;
opacity: 0.6;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
}
.toast-close:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.1);
}
.toast-close::before,
.toast-close::after {
content: '';
position: absolute;
width: 10px;
height: 1px;
background: #ccc;
}
.toast-close::before { transform: rotate(45deg); }
.toast-close::after { transform: rotate(-45deg); }
.progress-bar {
position: absolute;
bottom: 0;
left: 0;
height: 2px;
background: rgba(255, 255, 255, 0.25);
animation: shrink linear forwards;
}
.toast.success { border-left: 3px solid #4CAF50; }
.toast.error { border-left: 3px solid #F44336; }
.toast.warning { border-left: 3px solid #FF9800; }
.toast.info { border-left: 3px solid #2196F3; }
`;
document.head.appendChild(style);
}
// Get or create container
let container = document.getElementById('toast-container');
if (!container) {
container = document.createElement('div');
container.id = 'toast-container';
document.body.appendChild(container);
}
// Create toast
const toast = document.createElement('div');
toast.className = `toast ${type}`;
// Create content
const content = document.createElement('div');
content.className = 'toast-content';
// Add icon
const iconMap = {
success: ' ',
error: ' ',
warning: ' ',
info: ' '
};
const icon = document.createElement('div');
icon.className = 'toast-icon';
icon.innerHTML = iconMap[type] || iconMap.info;
content.appendChild(icon);
// Add emoji if provided
if (emoji) {
const emojiSpan = document.createElement('span');
emojiSpan.className = 'toast-emoji';
emojiSpan.textContent = emoji;
content.appendChild(emojiSpan);
}
// Add message
const messageSpan = document.createElement('span');
messageSpan.className = 'toast-message';
messageSpan.textContent = message;
content.appendChild(messageSpan);
toast.appendChild(content);
// Add close button
const closeBtn = document.createElement('div');
closeBtn.className = 'toast-close';
closeBtn.addEventListener('click', () => removeToast());
toast.appendChild(closeBtn);
// Add progress bar
const progressBar = document.createElement('div');
progressBar.className = 'progress-bar';
progressBar.style.animationDuration = `${duration}ms`;
toast.appendChild(progressBar);
container.appendChild(toast);
// Auto remove
let timeout = setTimeout(removeToast, duration);
// Pause on hover
toast.addEventListener('mouseenter', () => {
progressBar.style.animationPlayState = 'paused';
clearTimeout(timeout);
});
toast.addEventListener('mouseleave', () => {
progressBar.style.animationPlayState = 'running';
const remaining = (progressBar.offsetWidth / toast.offsetWidth) * duration;
timeout = setTimeout(removeToast, remaining);
});
function removeToast() {
clearTimeout(timeout);
toast.classList.add('removing');
setTimeout(() => toast.remove(), 300);
}
// Return control object
return {
remove: removeToast,
update: (newMessage) => messageSpan.textContent = newMessage,
setType: (newType) => {
toast.className = `toast ${newType}`;
icon.innerHTML = iconMap[newType] || iconMap.info;
},
setDuration: (newDuration) => {
clearTimeout(timeout);
progressBar.style.animation = `shrink ${newDuration}ms linear forwards`;
timeout = setTimeout(removeToast, newDuration);
},
updateEmoji: (newEmoji) => {
const emojiEl = toast.querySelector('.toast-emoji');
if (emojiEl) emojiEl.textContent = newEmoji;
}
};
}
/*******************************************************
name of function: Update_Popup
description: the update popup if an update is released
*******************************************************/
function Update_Popup() {
const VERSION = "V38.4";
const PREV_VERSION = "V37.4";
// Check if a version other than V38.4 exists and show the popup
const currentVersion = localStorage.getItem('version') || "V0.0"; // Get saved version or default to "V0.0"
if (currentVersion !== VERSION) {
localStorage.setItem('version', VERSION); // Set the new version
} else {
return; // If the current version is the latest, do not show the popup
}
// Remove any previous version flag if present
if (localStorage.getItem(PREV_VERSION)) {
localStorage.removeItem(PREV_VERSION);
}
const css = `
.first-time-popup {
display: flex;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.25); /* Increased opacity for darker background without blur */
justify-content: center;
align-items: center;
z-index: 1000;
opacity: 0;
animation: fadeIn 0.5s ease-in-out forwards;
}
.first-time-popup-content {
background: linear-gradient(135deg, rgba(30, 30, 40, 0.95) 0%, rgba(15, 15, 25, 0.98) 100%);
border-radius: 24px;
padding: 35px;
width: 450px;
max-width: 90%;
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.1);
text-align: center;
color: #fff;
transform: scale(0.85);
animation: scaleUp 0.6s cubic-bezier(0.165, 0.84, 0.44, 1) forwards;
position: relative;
overflow: hidden;
}
.first-time-popup-content::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #4da6ff, #9966ff, #4da6ff);
background-size: 200% 100%;
animation: shimmer 3s infinite linear;
}
.popup-header {
font-size: 24px;
font-weight: 800;
color: #fff;
text-transform: uppercase;
letter-spacing: 1.5px;
margin-bottom: 8px;
text-align: center;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.popup-version {
font-size: 18px;
font-weight: bold;
color: #ffcc00;
margin-bottom: 20px;
display: inline-block;
padding: 5px 15px;
border-radius: 20px;
background: rgba(255, 204, 0, 0.1);
box-shadow: 0 0 0 1px rgba(255, 204, 0, 0.3);
}
.popup-info {
font-size: 15px;
color: #e0e0e0;
margin-bottom: 25px;
line-height: 1.7;
padding: 18px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.03);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1), inset 0 0 0 1px rgba(255, 255, 255, 0.05);
}
.popup-info p {
margin: 12px 0;
}
.popup-info a {
color: #4da6ff;
text-decoration: none;
font-weight: bold;
transition: all 0.3s ease;
padding: 2px 5px;
border-radius: 4px;
background: rgba(77, 166, 255, 0.1);
}
.popup-info a:hover {
color: #80bfff;
text-decoration: none;
background: rgba(77, 166, 255, 0.2);
box-shadow: 0 0 0 1px rgba(77, 166, 255, 0.3);
}
.popup-footer {
font-size: 14px;
color: rgba(255, 255, 255, 0.6);
font-weight: 500;
margin-top: 15px;
transition: opacity 0.4s ease-out;
padding: 8px;
border-radius: 8px;
background: rgba(0, 0, 0, 0.2);
}
.popup-footer.hidden {
opacity: 0;
visibility: hidden;
}
.popup-note {
font-size: 13px;
font-weight: bold;
color: #ff6666;
margin-top: 12px;
}
.popup-logo {
display: block;
margin: 0 auto 20px;
width: 90px;
height: auto;
border-radius: 18px;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1);
transform: translateY(0);
transition: transform 0.3s ease;
}
.popup-logo:hover {
transform: translateY(-3px);
}
.developer-message {
display: inline-block;
padding: 10px 15px;
margin: 10px 0;
background: rgba(40, 167, 69, 0.1);
border-left: 3px solid #28a745;
color: #bfffca;
border-radius: 3px;
font-weight: 500;
text-align: left;
line-height: 1.5;
}
.feature-item {
display: flex;
align-items: center;
margin: 12px 0;
text-align: left;
}
.feature-icon {
margin-right: 10px;
color: #4da6ff;
font-size: 18px;
}
.feature-highlight {
display: inline-block;
padding: 2px 8px;
background: rgba(77, 166, 255, 0.15);
border-radius: 4px;
color: #ffffff;
font-weight: bold;
}
.first-time-popup-close {
position: absolute;
top: 15px;
right: 20px;
font-size: 26px;
font-weight: bold;
cursor: pointer;
color: rgba(255, 255, 255, 0.6);
opacity: 0.4;
transition: all 0.3s ease;
pointer-events: none;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.first-time-popup-close.active {
opacity: 1;
pointer-events: auto;
background: rgba(255, 255, 255, 0.05);
}
.first-time-popup-close:hover {
color: #ff4d4d;
transform: rotate(90deg);
background: rgba(255, 77, 77, 0.1);
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes scaleUp {
0% { transform: scale(0.85); }
70% { transform: scale(1.03); }
100% { transform: scale(1); }
}
@keyframes scaleDown {
from { transform: scale(1); }
to { transform: scale(0.85); opacity: 0; }
}
@keyframes shimmer {
0% { background-position: 0% 0; }
100% { background-position: 200% 0; }
}
`;
const style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = css;
document.head.appendChild(style);
const popupHTML = `
`;
const popupContainer = document.createElement('div');
popupContainer.innerHTML = popupHTML;
document.body.appendChild(popupContainer);
const closeButton = popupContainer.querySelector('.first-time-popup-close');
const popup = popupContainer.querySelector('.first-time-popup');
if (closeButton && popup) {
closeButton.addEventListener('click', () => {
popup.style.animation = 'fadeOut 0.4s ease-in-out forwards';
popup.querySelector('.first-time-popup-content').style.animation = 'scaleDown 0.4s ease-in-out forwards';
setTimeout(() => {
popup.remove();
}, 400);
});
} else {
ConsoleLogEnabled('Popup initialization failed: close button or popup not found.');
}
}
/*******************************************************
name of function: initializeLocalStorage
description: adds default settings
*******************************************************/
function initializeLocalStorage() {
// define default settings
const defaultSettings = {
enableLogs: false, // disabled by default
removeads: true, // enabled by default
togglefilterserversbutton: true, // enable by default
toggleserverhopbutton: true, // enable by default
AutoRunServerRegions: false, // disabled by default
ShowOldGreeting: false, // disabled by default
togglerecentserverbutton: true, // enable by default
quicknav: false, // disabled by default
prioritylocation: "automatic", // automatic by default
fastservers: false, // disabled by default
invertplayercount: false, // disabled by default
enablenotifications: true, // enabled by default
disabletrailer: true, // enabled by default
gamequalityfilter: false, // disabled by default
experiencedtime: false // disabled by default not in settings
};
// Loop through default settings and set them in localStorage if they don't exist
Object.entries(defaultSettings).forEach(([key, value]) => {
const storageKey = `ROLOCATE_${key}`;
if (localStorage.getItem(storageKey) === null) {
localStorage.setItem(storageKey, value);
}
});
}
//// testing for locations not in production
//(async () => {
// ConsoleLogEnabled("[GM Storage Dump] --- Start ---");
// const keys = await GM_listValues();
// for (const key of keys) {
// ConsoleLogEnabled(`[GM] ${key}:`, await GM_getValue(key));
// }
// ConsoleLogEnabled("[GM Storage Dump] --- End ---");
//})();//
//// testing for locations
//(async () => {
// const keys = await GM_listValues();
// for (const key of keys) {
// GM_deleteValue(key);
// ConsoleLogEnabled(`Deleted ${key}`);
// }
//})();
/*******************************************************
name of function: initializeCoordinatesStorage
description: finds coordinates
*******************************************************/
function initializeCoordinatesStorage() {
// coors alredyt in there
try {
const storedCoords = GM_getValue("ROLOCATE_coordinates");
if (!storedCoords) {
// make empty
GM_setValue("ROLOCATE_coordinates", JSON.stringify({
lat: "",
lng: ""
}));
} else {
// yea
const parsedCoords = JSON.parse(storedCoords);
if ((!parsedCoords.lat || !parsedCoords.lng) && localStorage.getItem("ROLOCATE_prioritylocation") === "manual") {
// if manual mode but no coordinates, revert to automatic
localStorage.setItem("ROLOCATE_prioritylocation", "automatic");
}
}
} catch (e) {
ConsoleLogEnabled("Error initializing coordinates storage:", e);
// not commenting this cause im bored
GM_setValue("ROLOCATE_coordinates", JSON.stringify({
lat: "",
lng: ""
}));
}
}
/*******************************************************
name of function: getSettingsContent
description: adds section to settings page
*******************************************************/
function getSettingsContent(section) {
if (section === "home") {
return `
`;
}
if (section === "appearance") {
return `
Show Old Greeting
?
Disable Trailer Autoplay
New
Just Released/Updated
?
Remove All Roblox Ads
?
Game Quality Filter
New
Just Released/Updated
Edit
?
Quick Navigation
Edit
?
`;
}
if (section === "advanced") {
return `
`;
}
if (section === "about") {
return `
Credits
This project was created by:
`;
}
if (section === "help") {
return `
General Tab
Auto Server Regions: Replaces Roblox's 8 default servers with at least 8 servers, providing detailed info such as location and ping.
Fast Server Search: Boosts server search speed up to 100x (experimental). Replaces player thumbnails with Builderman/Roblox icons to bypass rate limits.
Invert Player Count: For server regions: shows low-player servers when enabled, high-player servers when disabled. You can also control this on the Roblox server popup.
Recent Servers: Shows the most recent servers you have joined in the past 3 days.
Appearance Tab
Show Old Greeting: Shows the old greeting Roblox had on their home page.
Disable Trailer Autoplay: Prevents trailers from autoplaying on Roblox game pages.
Remove All Roblox Ads: Blocks most ads on the Roblox site.
Game Quality Filter: Removes games from the charts/discover page based on your settings.
Quick Nav: Ability to add quick navigations to the leftside panel of the Roblox page.
Advanced Tab
Enable Console Logs: Enables console.log messages from the script.
Enable Server Filters: Enables server filter features on the game page.
Enable Server Hop Button: Enables server hop feature on the game page.
Enable Notifications: Enables helpful notifications from the script.
Set default location: Enables the user to set a default location for Roblox server regions. Turn this on if the script cannot automatically detect your location.
Need more help?
You can visit the
troubleshooting
or create an issue on
greasyfork
for more assistance.
`;
}
// General tab (default)
return `
Auto Server Regions
Experimental
Still being tested
?
Fast Server Search
Experimental
Still being tested
?
Invert Player Count
New
Just Released/Updated
?
Recent Servers
?
`;
}
/*******************************************************
name of function: openSettingsMenu
description: opens setting menu and makes it look good
*******************************************************/
function openSettingsMenu() {
if (document.getElementById("userscript-settings-menu")) return;
// storage make go uyea
initializeLocalStorage();
initializeCoordinatesStorage();
const overlay = document.createElement("div");
overlay.id = "userscript-settings-menu";
overlay.innerHTML = `
✖
Home
${getSettingsContent("home")}
`;
document.body.appendChild(overlay);
// put css in
const style = document.createElement("style");
style.textContent = `
.grayish-center {
color: white;
font-weight: bold;
text-align: center;
position: relative;
display: inline-block;
font-size: 18px !important; /* idk whats overriding this but screw finding that */
}
.grayish-center::after {
content: "";
display: block;
margin: 4px auto 0;
width: 50%;
border-bottom: 2px solid #888888;
opacity: 0.6;
border-radius: 2px;
}
li a.about-link {
position: relative !important;
font-weight: bold !important;
color: #dc2626 !important;
text-decoration: none !important;
cursor: pointer !important;
transition: color 0.2s ease !important;
}
li a.about-link::after {
content: '' !important;
position: absolute !important;
left: 0 !important;
bottom: -2px !important;
height: 2px !important;
width: 100% !important;
background-color: #dc2626 !important;
transform: scaleX(0) !important;
transform-origin: left !important;
transition: transform 0.3s ease !important;
}
li a.about-link:hover {
color: #b91c1c !important;
}
li a.about-link:hover::after {
transform: scaleX(1) !important;
}
.about-section ul li a {
position: relative;
font-weight: bold;
color: #dc2626; /* red-600 */
text-decoration: none;
cursor: pointer;
transition: color 0.2s ease;
}
.about-section ul li a::after {
content: '';
position: absolute;
left: 0;
bottom: -2px;
height: 2px;
width: 100%;
background-color: #dc2626; /* red underline */
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s ease;
}
.about-section ul li a:hover {
color: #b91c1c; /* darker red on hover */
}
.about-section ul li a:hover::after {
transform: scaleX(1);
}
.license-note {
font-size: 0.8em;
color: #888;
margin-top: 8px;
}
.license-note a {
color: #888;
text-decoration: underline;
}
.edit-button {
margin-left: auto;
padding: 2px 8px;
font-size: 12px;
border: none;
border-radius: 6px;
background: linear-gradient(145deg, #3a3a3a, #2c2c2c);
color: #f0f0f0;
cursor: pointer;
font-weight: 500;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05),
0 2px 4px rgba(0, 0, 0, 0.25);
transition: all 0.2s ease;
}
.edit-button:hover {
background: linear-gradient(145deg, #4a4a4a, #343434);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08),
0 3px 6px rgba(0, 0, 0, 0.35);
transform: translateY(-0.5px);
}
.help-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: rgba(220, 53, 69, 0.15);
border-radius: 50%;
font-size: 12px;
font-weight: 600;
color: #e02d3c;
cursor: pointer;
transition: all 0.2s ease;
margin-left: auto; /* Pushes it to the right */
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
position: relative;
border: 1px solid rgba(220, 53, 69, 0.2);
}
.help-icon:hover {
background: rgba(220, 53, 69, 0.25);
transform: translateY(-1px);
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.15);
cursor: pointer;
}
/* Add tooltip on hover */
.help-icon::after {
content: "Click for help";
position: absolute;
bottom: -30px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
pointer-events: none;
}
.help-icon:hover::after {
opacity: 1;
visibility: visible;
}
.help-icon:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.4); }
70% { box-shadow: 0 0 0 6px rgba(220, 53, 69, 0); }
100% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0); }
}
.help-icon.attention {
animation: pulse 2s infinite;
}
.highlight-help-item {
animation: highlight 1.5s ease;
background: rgba(76, 175, 80, 0.1); /* Green highlight */
border-left: 3px solid #4CAF50; /* Green border */
}
@keyframes highlight {
0% { background: rgba(76, 175, 80, 0.3); } /* Green start */
100% { background: rgba(76, 175, 80, 0.1); } /* Green end */
}
.new_label .new {
margin-left: 8px;
color: #32cd32; /* LimeGreen */
font-size: 12px;
font-weight: bold;
background-color: rgba(50, 205, 50, 0.1); /* soft green background */
padding: 2px 6px;
border-radius: 3px;
position: relative;
z-index: 10001;
}
.new_label .tooltip {
visibility: hidden;
background-color: rgba(0, 0, 0, 0.75);
color: #fff;
font-size: 12px;
padding: 6px;
border-radius: 5px;
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
z-index: 10001;
opacity: 0;
transition: opacity 0.3s;
}
.new_label .new:hover .tooltip {
visibility: visible;
opacity: 1;
z-index: 10001;
}
.experiment_label .experimental {
margin-left: 8px;
color: gold;
font-size: 12px;
font-weight: bold;
background-color: rgba(255, 215, 0, 0.1);
padding: 2px 6px;
border-radius: 3px;
position: relative; /* Needed for positioning the tooltip */
z-index: 10001;
}
.experiment_label .tooltip {
visibility: hidden;
background-color: rgba(0, 0, 0, 0.7);
color: #fff;
font-size: 12px;
padding: 6px;
border-radius: 5px;
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
z-index: 10001;
opacity: 0;
transition: opacity 0.3s;
}
.experiment_label .experimental:hover .tooltip {
visibility: visible;
opacity: 1;
z-index: 10001;
}
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.96); }
to { opacity: 1; transform: scale(1); }
}
@keyframes fadeOut {
from { opacity: 1; transform: scale(1); }
to { opacity: 0; transform: scale(0.96); }
}
@keyframes sectionFade {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideIn {
from { transform: translateX(-20px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
#userscript-settings-menu {
position: fixed;
top: 0; left: 0;
width: 100vw; height: 100vh;
background: rgba(0,0,0,0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
animation: fadeIn 0.7s cubic-bezier(0.19, 1, 0.22, 1);
}
.settings-container {
display: flex;
position: relative;
width: 580px; /* Reduced from 680px */
height: 420px; /* Reduced from 480px */
background: linear-gradient(145deg, #1a1a1a, #232323);
border-radius: 12px; /* Slightly smaller radius */
overflow: hidden;
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.7);
font-family: 'Inter', 'Segoe UI', Arial, sans-serif;
border: 1px solid rgba(255, 255, 255, 0.05);
}
#close-settings {
position: absolute;
top: 12px; /* Reduced from 16px */
right: 12px; /* Reduced from 16px */
background: transparent;
border: none;
color: #c0c0c0;
font-size: 20px; /* Reduced from 22px */
cursor: pointer;
z-index: 10001;
transition: all 0.5s ease;
width: 30px; /* Reduced from 34px */
height: 30px; /* Reduced from 34px */
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
#close-settings:hover {
color: #ff3b47;
background: rgba(255, 59, 71, 0.1);
transform: rotate(90deg);
}
.settings-sidebar {
width: 32%; /* Reduced from 35% */
background: #272727;
padding: 18px 12px; /* Reduced from 24px 15px */
color: white;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 6px 0 12px -6px rgba(0,0,0,0.3);
position: relative;
overflow: hidden;
}
.settings-sidebar h2 {
margin-bottom: 16px; /* Reduced from 20px */
font-weight: 600;
font-size: 22px; /* Reduced from 24px */
text-shadow: 0 1px 3px rgba(0,0,0,0.5);
text-decoration: none;
position: relative;
text-align: center;
}
.settings-sidebar h2::after {
content: "";
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: -6px; /* Reduced from -8px */
width: 36px; /* Reduced from 40px */
height: 3px;
background: white;
border-radius: 2px;
}
.settings-sidebar ul {
list-style: none;
padding: 0;
width: 100%;
margin-top: 5px; /* Reduced from 10px */
}
.settings-sidebar li {
padding: 10px 12px; /* Reduced from 14px */
margin: 6px 0; /* Reduced from 8px 0 */
text-align: left;
cursor: pointer;
transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1);
border-radius: 8px;
font-weight: 500;
font-size: 17px; /* increased from 15px */
position: relative;
animation: slideIn 0.5s cubic-bezier(0.19, 1, 0.22, 1);
animation-fill-mode: both;
display: flex;
align-items: center;
}
.settings-sidebar li:hover {
background: #444;
transform: translateX(5px);
}
.settings-sidebar .active {
background: #444;
color: white;
transform: translateX(0);
}
.settings-sidebar .active:hover {
transform: translateX(0);
}
.settings-sidebar li:hover::before {
height: 100%;
}
.settings-sidebar .active::before {
background: #dc3545;
}
/* Custom Scrollbar */
.settings-content {
flex: 1;
padding: 24px; /* Reduced from 32px */
color: white;
text-align: center;
max-height: 100%;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: darkgreen black;
background: #1e1e1e;
position: relative;
}
/* Webkit (Chrome, Safari) Scrollbar */
.settings-content::-webkit-scrollbar {
width: 6px; /* Reduced from 8px */
}
.settings-content::-webkit-scrollbar-track {
background: #333;
border-radius: 3px; /* Reduced from 4px */
}
.settings-content::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, #dc3545, #b02a37);
border-radius: 3px; /* Reduced from 4px */
}
.settings-content::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, #ff3b47, #dc3545);
}
.settings-content h2 {
margin-bottom: 24px; /* Reduced from 30px */
font-weight: 600;
font-size: 22px; /* Reduced from 24px */
color: white;
text-shadow: 0 1px 3px rgba(0,0,0,0.4);
letter-spacing: 0.5px;
position: relative;
display: inline-block;
padding-bottom: 6px; /* Reduced from 8px */
}
.settings-content h2::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
background: white;
border-radius: 2px;
}
.settings-content div {
animation: sectionFade 0.7s cubic-bezier(0.19, 1, 0.22, 1);
}
/* Toggle Slider Styles */
.toggle-slider {
display: flex;
align-items: center;
margin: 12px 0; /* Reduced from 16px 0 */
cursor: pointer;
padding: 8px 14px; /* Reduced from 10px 16px */
background: rgba(255, 255, 255, 0.03);
border-radius: 6px; /* Reduced from 8px */
transition: all 0.5s ease;
user-select: none;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.toggle-slider:hover {
background: rgba(255, 255, 255, 0.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.toggle-slider input {
display: none;
}
.toggle-slider .slider {
position: relative;
display: inline-block;
width: 42px; /* Reduced from 48px */
height: 22px; /* Reduced from 24px */
background-color: rgba(255, 255, 255, 0.2);
border-radius: 22px;
margin-right: 12px; /* Reduced from 14px */
transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
}
.toggle-slider .slider::before {
content: "";
position: absolute;
height: 16px; /* Reduced from 18px */
width: 16px; /* Reduced from 18px */
left: 3px;
bottom: 3px;
background-color: white;
border-radius: 50%;
transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.toggle-slider input:checked + .slider {
background-color: #4CAF50;
box-shadow: 0 0 0 1px rgba(220, 53, 69, 0.05), inset 0 1px 3px rgba(0, 0, 0, 0.2);
}
.toggle-slider input:checked + .slider::before {
transform: translateX(20px); /* Reduced from 24px */
}
.toggle-slider input:checked + .slider::after {
opacity: 1;
}
.rolocate-logo {
width: 90px !important; /* Reduced from 110px */
height: 90px !important; /* Reduced from 110px */
object-fit: contain;
border-radius: 14px; /* Reduced from 16px */
display: block;
margin: 0 auto 16px auto; /* Reduced from 20px */
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
transition: all 0.5s ease;
border: 2px solid rgba(220, 53, 69, 0.4);
}
.rolocate-logo:hover {
transform: scale(1.05);
}
.version {
font-size: 13px; /* Reduced from 14px */
color: #aaa;
margin-bottom: 24px; /* Reduced from 30px */
display: inline-block;
padding: 5px 14px; /* Reduced from 6px 16px */
background: rgba(220, 53, 69, 0.1);
border-radius: 18px; /* Reduced from 20px */
border: 1px solid rgba(220, 53, 69, 0.2);
}
.settings-content ul {
text-align: left;
list-style-type: none;
padding: 0;
margin-top: 16px; /* Reduced from 20px */
}
.settings-content ul li {
margin: 12px 0; /* Reduced from 16px 0 */
padding: 10px 14px; /* Reduced from 12px 16px */
background: rgba(255, 255, 255, 0.03);
border-radius: 6px; /* Reduced from 8px */
transition: all 0.4s ease;
}
.settings-content ul li:hover {
background: rgba(255, 255, 255, 0.05);
border-left: 3px solid #4CAF50;
transform: translateX(5px);
}
.settings-content ul li strong {
color: #4CAF50;
}
.warning_advanced {
font-size: 14px; /* Reduced from 16px */
color: #ff3b47;
font-weight: bold;
padding: 8px 14px; /* Reduced from 10px 16px */
background: rgba(220, 53, 69, 0.1);
border-radius: 6px;
margin-bottom: 16px; /* Reduced from 20px */
display: inline-block;
border: 1px solid rgba(220, 53, 69, 0.2);
}
.average_text {
font-size: 16px; /* Reduced from 18px */
color: #e0e0e0;
font-weight: 500;
margin-top: 12px; /* Reduced from 15px */
line-height: 1.5;
letter-spacing: 0.3px;
background: linear-gradient(90deg, #ff3b47, #ff6b74);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
display: inline-block;
}
.edit-nav-button {
padding: 6px 14px; /* Reduced from 8px 16px */
background: #4CAF50;
color: white;
border: none;
border-radius: 6px; /* Reduced from 8px */
cursor: pointer;
font-family: 'Inter', 'Helvetica', sans-serif;
font-size: 12px; /* Reduced from 13px */
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1);
height: auto;
line-height: 1.5;
position: relative;
overflow: hidden;
}
.edit-nav-button:hover {
transform: translateY(-3px);
background: linear-gradient(135deg, #1e8449 0%, #196f3d 100%);
}
.edit-nav-button:hover::before {
left: 100%;
}
.edit-nav-button:active {
background: linear-gradient(135deg, #1e8449 0%, #196f3d 100%);
transform: translateY(1px);
}
/* Dropdown styling */
select {
width: 100%;
padding: 10px 14px; /* Reduced from 12px 16px */
border-radius: 6px; /* Reduced from 8px */
background: rgba(255, 255, 255, 0.05);
color: #e0e0e0;
font-size: 14px; /* Reduced from 14px */
appearance: none;
background-image: url('data:image/svg+xml;utf8, ');
background-repeat: no-repeat;
background-position: right 14px center;
background-size: 14px;
transition: all 0.5s ease;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
border-color: rgba(255, 255, 255, 0.05);
}
/* Dropdown hint styling */
#location-hint {
margin-top: 10px; /* Reduced from 12px */
font-size: 12px; /* Reduced from 13px */
color: #c0c0c0;
background: rgba(255, 255, 255, 0.05);
border-radius: 6px; /* Reduced from 8px */
padding: 10px 14px; /* Reduced from 12px 16px */
border: 1px solid rgba(255, 255, 255, 0.05);
line-height: 1.6;
transition: all 0.5s ease;
}
/* Section separator */
.section-separator {
width: 100%;
height: 1px;
background: linear-gradient(90deg, transparent, #272727, transparent);
margin: 24px 0; /* Reduced from 30px 0 */
}
/* Help section styles */
.help-section h3, .about-section h3 {
color: white;
margin-top: 20px; /* Reduced from 25px */
margin-bottom: 12px; /* Reduced from 15px */
font-size: 16px; /* Reduced from 18px */
text-align: left;
}
/* Hint text styling */
.hint-text {
font-size: 13px; /* Reduced from 14px */
color: #a0a0a0;
margin-top: 6px; /* Reduced from 8px */
margin-left: 16px; /* Reduced from 20px */
text-align: left;
}
/* Location settings styling */
.location-settings {
background: rgba(255, 255, 255, 0.03);
border-radius: 6px; /* Reduced from 8px */
padding: 14px; /* Reduced from 16px */
margin-top: 16px; /* Reduced from 20px */
border: 1px solid rgba(255, 255, 255, 0.05);
transition: all 0.5s ease;
}
.setting-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px; /* Reduced from 12px */
}
.setting-header span {
font-size: 14px; /* Reduced from 15px */
font-weight: 500;
}
.help-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px; /* Reduced from 20px */
height: 18px; /* Reduced from 20px */
background: rgba(220, 53, 69, 0.2);
border-radius: 50%;
font-size: 11px; /* Reduced from 12px */
color: #ff3b47;
cursor: help;
transition: all 0.5s ease;
}
/* Manual coordinates input styling */
#manual-coordinates {
margin-top: 12px !important; /* Reduced from 15px */
}
.coordinates-inputs {
gap: 8px !important; /* Reduced from 10px */
margin-bottom: 10px !important; /* Reduced from 12px */
}
#manual-coordinates input {
padding: 8px 10px !important; /* Reduced from 10px 12px */
border-radius: 6px !important; /* Reduced from 8px */
font-size: 13px !important; /* Reduced from default */
}
#manual-coordinates label {
margin-bottom: 6px !important; /* Reduced from 8px */
font-size: 13px !important; /* Reduced from 14px */
}
#save-coordinates {
margin-top: 6px !important; /* Reduced from 8px */
}
/* Animated content */
.animated-content {
animation: sectionFade 0.7s cubic-bezier(0.19, 1, 0.22, 1);
}
`;
document.head.appendChild(style);
// hopefully this works
document.querySelectorAll(".settings-sidebar li").forEach((li, index) => {
// aniamtions stuff
li.style.animationDelay = `${0.05 * (index + 1)}s`;
li.addEventListener("click", function() {
const currentActive = document.querySelector(".settings-sidebar .active");
if (currentActive) currentActive.classList.remove("active");
this.classList.add("active");
const section = this.getAttribute("data-section");
const settingsBody = document.getElementById("settings-body");
const settingsTitle = document.getElementById("settings-title");
// aniamtions stuff
settingsBody.style.opacity = "0";
settingsBody.style.transform = "translateY(10px)";
settingsTitle.style.opacity = "0";
settingsTitle.style.transform = "translateY(10px)";
setTimeout(() => {
// aniamtions stuff
settingsTitle.textContent = section.charAt(0).toUpperCase() + section.slice(1);
settingsBody.innerHTML = getSettingsContent(section);
// quick nav stuff
if (section === "appearance") {
const quickNavCheckbox = document.getElementById("quicknav");
const editButton = document.getElementById("edit-quicknav-btn");
if (quickNavCheckbox && editButton) {
// Set initial display based on localStorage
editButton.style.display = localStorage.getItem("ROLOCATE_quicknav") === "true" ? "block" : "none";
// Update localStorage and edit button visibility when checkbox changes
quickNavCheckbox.addEventListener("change", function() {
const isEnabled = this.checked;
localStorage.setItem("ROLOCATE_quicknav", isEnabled);
editButton.style.display = isEnabled ? "block" : "none";
});
}
}
if (section === "appearance") {
const gameQualityCheckbox = document.getElementById("gamequalityfilter");
const editButton = document.getElementById("edit-gamequality-btn");
if (gameQualityCheckbox && editButton) {
// Set visibility on load
editButton.style.display = localStorage.getItem("ROLOCATE_gamequalityfilter") === "true" ? "block" : "none";
// Toggle visibility when the checkbox changes
gameQualityCheckbox.addEventListener("change", function() {
const isEnabled = this.checked;
editButton.style.display = isEnabled ? "block" : "none";
});
}
}
settingsBody.style.transition = "all 0.4s cubic-bezier(0.19, 1, 0.22, 1)";
settingsTitle.style.transition = "all 0.4s cubic-bezier(0.19, 1, 0.22, 1)";
void settingsBody.offsetWidth;
void settingsTitle.offsetWidth;
settingsBody.style.opacity = "1";
settingsBody.style.transform = "translateY(0)";
settingsTitle.style.opacity = "1";
settingsTitle.style.transform = "translateY(0)";
applyStoredSettings();
}, 200);
});
});
// Close button with enhanced animation
document.getElementById("close-settings").addEventListener("click", function() {
// Check if manual mode is selected with empty coordinates
const priorityLocation = localStorage.getItem("ROLOCATE_prioritylocation");
if (priorityLocation === "manual") {
try {
const coords = JSON.parse(GM_getValue("ROLOCATE_coordinates", '{"lat":"","lng":""}'));
if (!coords.lat || !coords.lng) {
notifications('Please set the latitude and longitude values for the manual location, or set it to automatic.', 'error', '⚠️', 8000);
return; // Prevent closing
}
} catch (e) {
ConsoleLogEnabled("Error checking coordinates:", e);
notifications('Error checking location settings', 'error', '⚠️', 8000);
return; // Prevent closing
}
}
// Proceed with closing if validation passes
const menu = document.getElementById("userscript-settings-menu");
menu.style.animation = "fadeOut 0.4s cubic-bezier(0.19, 1, 0.22, 1) forwards";
// Add rotation to close button when closing
this.style.transform = "rotate(90deg)";
setTimeout(() => menu.remove(), 400);
});
// Apply stored settings immediately when opened
applyStoredSettings();
// Add ripple effect to buttons
const buttons = document.querySelectorAll(".edit-nav-button, .settings-button");
buttons.forEach(button => {
button.addEventListener("mousedown", function(e) {
const ripple = document.createElement("span");
const rect = this.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = e.clientX - rect.left - size / 2;
const y = e.clientY - rect.top - size / 2;
ripple.style.cssText = `
position: absolute;
background: rgba(255,255,255,0.4);
border-radius: 50%;
pointer-events: none;
width: ${size}px;
height: ${size}px;
top: ${y}px;
left: ${x}px;
transform: scale(0);
transition: transform 0.6s, opacity 0.6s;
`;
this.appendChild(ripple);
setTimeout(() => {
ripple.style.transform = "scale(2)";
ripple.style.opacity = "0";
setTimeout(() => ripple.remove(), 600);
}, 10);
});
});
// Handle help icon clicks
document.addEventListener('click', function(e) {
if (e.target.classList.contains('help-icon')) {
// Prevent the event from bubbling up to the toggle button
e.stopPropagation();
e.preventDefault();
const helpItem = e.target.getAttribute('data-help');
if (helpItem) {
// Switch to help tab
const helpTab = document.querySelector('.settings-sidebar li[data-section="help"]');
if (helpTab) helpTab.click();
// Scroll to the corresponding help item after a short delay
setTimeout(() => {
const helpElement = document.getElementById(`help-${helpItem}`);
if (helpElement) {
helpElement.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
helpElement.classList.add('highlight-help-item');
setTimeout(() => {
helpElement.classList.remove('highlight-help-item');
}, 1500);
}
}, 300);
}
}
});
}
/*******************************************************
name of function: showQuickNavPopup
description: quick nav popup menu
*******************************************************/
function showQuickNavPopup() {
// Remove existing quick nav if it exists
const existingNav = document.getElementById("premium-quick-nav");
if (existingNav) existingNav.remove();
// POPUP CREATION
// Create overlay
const overlay = document.createElement("div");
overlay.id = "quicknav-overlay";
overlay.style.position = "fixed";
overlay.style.top = "0";
overlay.style.left = "0";
overlay.style.width = "100%";
overlay.style.height = "100%";
overlay.style.backgroundColor = "rgba(0,0,0,0)"; // Darker overlay for dark mode
overlay.style.backdropFilter = "blur(1px)";
overlay.style.zIndex = "10000";
overlay.style.opacity = "0";
overlay.style.transition = "opacity 0.3s ease";
// Create popup
const popup = document.createElement("div");
popup.id = "premium-quick-nav-popup";
popup.style.position = "fixed";
popup.style.top = "50%";
popup.style.left = "50%";
popup.style.transform = "translate(-50%, -50%) scale(0.95)";
popup.style.opacity = "0";
popup.style.background = "linear-gradient(145deg, #0a0a0a, #121212)"; // Darker background for dark mode
popup.style.color = "white";
popup.style.padding = "32px";
popup.style.borderRadius = "16px";
popup.style.boxShadow = "0 20px 40px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.05)";
popup.style.zIndex = "10001";
popup.style.width = "600px";
popup.style.maxWidth = "90%";
popup.style.maxHeight = "85vh";
popup.style.overflowY = "auto";
popup.style.transition = "transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.4s ease";
// Get saved quick navs (if any)
const saved = JSON.parse(localStorage.getItem("ROLOCATE_quicknav_settings") || "[]");
// Build header
const header = `
Quick Navigation
Configure up to 9 custom navigation shortcuts
`;
// Build inputs for 9 links in a 3x3 grid
const inputsGrid = `
`;
// Build footer with buttons
const footer = `
Cancel
Save Changes
`;
// Combine all sections
popup.innerHTML = header + inputsGrid + footer;
// Add elements to DOM
document.body.appendChild(overlay);
document.body.appendChild(popup);
// POPUP EVENTS
// Add input hover and focus effects
popup.querySelectorAll('input').forEach(input => {
input.addEventListener('focus', () => {
input.style.background = 'rgba(255,255,255,0.1)';
input.style.boxShadow = '0 0 0 2px rgba(76, 175, 80, 0.4)';
});
input.addEventListener('blur', () => {
input.style.background = 'rgba(255,255,255,0.05)';
input.style.boxShadow = 'none';
});
input.addEventListener('mouseover', () => {
if (document.activeElement !== input) {
input.style.background = 'rgba(255,255,255,0.08)';
}
});
input.addEventListener('mouseout', () => {
if (document.activeElement !== input) {
input.style.background = 'rgba(255,255,255,0.05)';
}
});
});
// Add button hover effects
const saveBtn = popup.querySelector('#save-quicknav');
saveBtn.addEventListener('mouseover', () => {
saveBtn.style.background = 'linear-gradient(90deg, #66BB6A, #4CAF50)';
saveBtn.style.boxShadow = '0 4px 15px rgba(76, 175, 80, 0.4)';
saveBtn.style.transform = 'translateY(-1px)';
});
saveBtn.addEventListener('mouseout', () => {
saveBtn.style.background = 'linear-gradient(90deg, #4CAF50, #388E3C)';
saveBtn.style.boxShadow = '0 4px 12px rgba(76, 175, 80, 0.3)';
saveBtn.style.transform = 'translateY(0)';
});
const cancelBtn = popup.querySelector('#cancel-quicknav');
cancelBtn.addEventListener('mouseover', () => {
cancelBtn.style.background = 'rgba(255,255,255,0.05)';
});
cancelBtn.addEventListener('mouseout', () => {
cancelBtn.style.background = 'transparent';
});
// Animate in
setTimeout(() => {
overlay.style.opacity = "1";
popup.style.opacity = "1";
popup.style.transform = "translate(-50%, -50%) scale(1)";
}, 10);
// POPUP CLOSE FUNCTION
function closePopup() {
overlay.style.opacity = "0";
popup.style.opacity = "0";
popup.style.transform = "translate(-50%, -50%) scale(0.95)";
setTimeout(() => {
overlay.remove();
popup.remove();
}, 300);
}
// Save on click
popup.querySelector("#save-quicknav").addEventListener("click", () => {
const quickNavSettings = [];
for (let i = 0; i < 9; i++) {
const name = document.getElementById(`quicknav-name-${i}`).value.trim();
const link = document.getElementById(`quicknav-link-${i}`).value.trim();
if (name && link) {
quickNavSettings.push({
name,
link
});
}
}
localStorage.setItem("ROLOCATE_quicknav_settings", JSON.stringify(quickNavSettings));
closePopup();
});
// Cancel button
popup.querySelector("#cancel-quicknav").addEventListener("click", closePopup);
// Close when clicking overlay
overlay.addEventListener("click", (e) => {
if (e.target === overlay) {
closePopup();
}
});
// Close with ESC key
document.addEventListener("keydown", function escClose(e) {
if (e.key === "Escape") {
closePopup();
document.removeEventListener("keydown", escClose);
}
});
// AUTO-INIT AND KEYBOARD SHORTCUT
// Set up keyboard shortcut (Alt+Q)
document.addEventListener("keydown", function keyboardShortcut(e) {
if (e.altKey && e.key === "q") {
showQuickNavPopup();
}
});
}
/*******************************************************
name of function: applyStoredSettings
description: makes sure local storage is stored in correctly
*******************************************************/
function applyStoredSettings() {
// Handle all checkboxes
document.querySelectorAll("input[type='checkbox']").forEach(checkbox => {
const storageKey = `ROLOCATE_${checkbox.id}`;
const savedValue = localStorage.getItem(storageKey);
checkbox.checked = savedValue === "true";
checkbox.addEventListener("change", () => {
localStorage.setItem(storageKey, checkbox.checked);
});
});
// Handle dropdown for prioritylocation-select
const prioritySelect = document.getElementById("prioritylocation-select");
if (prioritySelect) {
const storageKey = "ROLOCATE_prioritylocation";
const savedValue = localStorage.getItem(storageKey) || "automatic";
prioritySelect.value = savedValue;
// Show/hide coordinates inputs based on selected value
const manualCoordinates = document.getElementById("manual-coordinates");
if (manualCoordinates) {
manualCoordinates.style.display = savedValue === "manual" ? "block" : "none";
// Set input values from stored coordinates if available
if (savedValue === "manual") {
try {
const savedCoords = JSON.parse(GM_getValue("ROLOCATE_coordinates", '{"lat":"","lng":""}'));
document.getElementById("latitude").value = savedCoords.lat || "";
document.getElementById("longitude").value = savedCoords.lng || "";
// If manual mode but no coordinates saved, revert to automatic
if (!savedCoords.lat || !savedCoords.lng) {
prioritySelect.value = "automatic";
localStorage.setItem(storageKey, "automatic");
manualCoordinates.style.display = "none";
}
} catch (e) {
ConsoleLogEnabled("Error loading saved coordinates:", e);
}
}
}
prioritySelect.addEventListener("change", () => {
const newValue = prioritySelect.value;
localStorage.setItem(storageKey, newValue);
// Show/hide coordinates inputs based on new value
if (manualCoordinates) {
manualCoordinates.style.display = newValue === "manual" ? "block" : "none";
// When switching to manual mode, load any saved coordinates
if (newValue === "manual") {
try {
const savedCoords = JSON.parse(GM_getValue("ROLOCATE_coordinates", '{"lat":"","lng":""}'));
document.getElementById("latitude").value = savedCoords.lat || "";
document.getElementById("longitude").value = savedCoords.lng || "";
// If no coordinates exist, keep the inputs empty
} catch (e) {
ConsoleLogEnabled("Error loading saved coordinates:", e);
}
}
}
});
}
// Button click handlers
const editQuickNavBtn = document.getElementById("edit-quicknav-btn");
if (editQuickNavBtn) {
editQuickNavBtn.addEventListener("click", () => {
showQuickNavPopup();
});
}
const editQualityGameBtn = document.getElementById("edit-gamequality-btn")
if (editQualityGameBtn) {
editQualityGameBtn.addEventListener("click", () => {
openGameQualitySettings();
});
}
const fastServersToggle = document.getElementById("fastservers");
if (fastServersToggle) {
fastServersToggle.addEventListener("change", () => {
if (fastServersToggle.checked) {
notifications('Fast Server Search: 100x faster on Violentmonkey, ~2x on Tampermonkey. Replaces thumbnails with builderman to bypass rate limits.', 'info', '🧪', 2000);
}
});
}
const AutoRunServerRegions = document.getElementById("AutoRunServerRegions");
if (AutoRunServerRegions) {
AutoRunServerRegions.addEventListener("change", () => {
if (AutoRunServerRegions.checked) {
notifications('Auto Server Regions works best when paired with Fast Server Search in Advanced Settings.', 'info', '🧪', 2000);
}
});
}
// Save coordinates button handler
const saveCoordinatesBtn = document.getElementById("save-coordinates");
if (saveCoordinatesBtn) {
saveCoordinatesBtn.addEventListener("click", () => {
const latInput = document.getElementById("latitude");
const lngInput = document.getElementById("longitude");
const lat = latInput.value.trim();
const lng = lngInput.value.trim();
// If manual mode but no coordinates provided, revert to automatic
if (!lat || !lng) {
const prioritySelect = document.getElementById("prioritylocation-select");
if (prioritySelect) {
prioritySelect.value = "automatic";
localStorage.setItem("ROLOCATE_prioritylocation", "automatic");
document.getElementById("manual-coordinates").style.display = "none";
// show feedback to user even if they dont see it
saveCoordinatesBtn.textContent = "Reverted to Automatic!";
saveCoordinatesBtn.style.background = "#4CAF50";
setTimeout(() => {
saveCoordinatesBtn.textContent = "Save Coordinates";
saveCoordinatesBtn.style.background = "background: #4CAF50;";
}, 2000);
}
return;
}
// Validate coordinates
const latNum = parseFloat(lat);
const lngNum = parseFloat(lng);
if (isNaN(latNum) || isNaN(lngNum) || latNum < -90 || latNum > 90 || lngNum < -180 || lngNum > 180) {
notifications('Invalid coordinates! Latitude must be between -90 and 90, and longitude between -180 and 180.', 'error', '⚠️', '8000');
return;
}
// Save valid coordinates
const coordinates = {
lat,
lng
};
GM_setValue("ROLOCATE_coordinates", JSON.stringify(coordinates));
// Ensure we're in manual mode
localStorage.setItem("ROLOCATE_prioritylocation", "manual");
if (prioritySelect) {
prioritySelect.value = "manual";
}
// Provide feedback
saveCoordinatesBtn.textContent = "Saved!";
saveCoordinatesBtn.style.background = "linear-gradient(135deg, #1e8449 0%, #196f3d 100%);";
setTimeout(() => {
saveCoordinatesBtn.textContent = "Save Coordinates";
saveCoordinatesBtn.style.background = "background: #4CAF50;";
}, 2000);
});
}
}
/*******************************************************
name of function: AddSettingsButton
description: adds settings button
*******************************************************/
function AddSettingsButton() {
const base64Logo = window.Base64Images.logo;
const navbarGroup = document.querySelector('.nav.navbar-right.rbx-navbar-icon-group');
if (!navbarGroup || document.getElementById('custom-logo')) return;
const li = document.createElement('li');
li.id = 'custom-logo-container';
li.style.position = 'relative';
li.innerHTML = `
Settings
`;
const logo = li.querySelector('#custom-logo');
const tooltip = li.querySelector('#custom-tooltip');
logo.addEventListener('click', () => openSettingsMenu());
logo.addEventListener('mouseover', () => {
logo.style.width = '30px';
logo.style.border = '2px solid white';
tooltip.style.visibility = 'visible';
tooltip.style.opacity = '1';
});
logo.addEventListener('mouseout', () => {
logo.style.width = '26px';
logo.style.border = 'none';
tooltip.style.visibility = 'hidden';
tooltip.style.opacity = '0';
});
navbarGroup.appendChild(li);
}
/*******************************************************
name of function: removeAds
description: remove roblox ads. This one is for checking if
settings are turned on.
*******************************************************/
function removeAds() {
if (localStorage.getItem("ROLOCATE_removeads") !== "true") {
return;
}
const iframeSelector = `.ads-container iframe,.abp iframe,.abp-spacer iframe,.abp-container iframe,.top-abp-container iframe,
#AdvertisingLeaderboard iframe,#AdvertisementRight iframe,#MessagesAdSkyscraper iframe,.Ads_WideSkyscraper iframe,
.profile-ads-container iframe, #ad iframe, iframe[src*="roblox.com/user-sponsorship/"]`;
const iframes = document.getElementsByTagName("iframe");
const scripts = document.getElementsByTagName("script");
const doneMap = new WeakMap();
/*******************************************************
name of function: removeElements
description: remove the roblox elements where ads are in
*******************************************************/
function removeElements() {
// Remove Iframes
for (let i = iframes.length; i--;) {
const iframe = iframes[i];
if (!doneMap.get(iframe) && iframe.matches(iframeSelector)) {
iframe.remove();
doneMap.set(iframe, true);
}
}
// Remove Scripts
for (let i = scripts.length; i--;) {
const script = scripts[i];
if (doneMap.get(script)) {
continue;
}
doneMap.set(script, true);
if (script.src && (
script.src.includes("imasdk.googleapis.com") ||
script.src.includes("googletagmanager.com") ||
script.src.includes("radar.cedexis.com") ||
script.src.includes("ns1p.net")
)) {
script.remove();
} else {
const cont = script.textContent;
if (!cont.includes("ContentJS") && (
cont.includes("scorecardresearch.com") ||
cont.includes("cedexis.com") ||
cont.includes("pingdom.net") ||
cont.includes("ns1p.net") ||
cont.includes("Roblox.Hashcash") ||
cont.includes("Roblox.VideoPreRollDFP") ||
cont.includes("Roblox.AdsHelper=") ||
cont.includes("googletag.enableServices()") ||
cont.includes("gtag('config'")
)) {
script.remove();
} else if (cont.includes("Roblox.EventStream.Init")) {
script.textContent = cont.replace(/"[^"]*"/g, "\"\"");
}
}
}
// Hide Sponsored Game Cards (existing method)
document.querySelectorAll(".game-card-native-ad").forEach(ad => {
const gameCard = ad.closest(".game-card-container");
if (gameCard) {
gameCard.style.display = "none";
}
});
// Block Sponsored Ads Game Card
document.querySelectorAll("div.gamecardcontainer").forEach(container => {
if (container.querySelector("div.game-card-native-ad")) {
container.style.display = "none";
}
});
// Block Sponsored Section On HomePage
document.querySelectorAll(".game-sort-carousel-wrapper").forEach(wrapper => {
const sponsoredLink = wrapper.querySelector('a[href*="Sponsored"]');
if (sponsoredLink) {
wrapper.style.display = "none";
}
});
// NEW: Remove elements with class "sdui-feed-item-container"
document.querySelectorAll(".sdui-feed-item-container").forEach(node => {
node.remove();
});
}
// Observe DOM for dynamically added elements
new MutationObserver(removeElements).observe(document.body, {
childList: true,
subtree: true
});
removeElements(); // Initial run
}
function openGameQualitySettings() {
// Prevent multiple modals from opening
if (document.getElementById('game-settings-modal')) {
return;
}
// Create modal overlay
const overlay = document.createElement('div');
overlay.id = 'game-settings-modal';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.setAttribute('aria-labelledby', 'modal-title');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
opacity: 0;
transition: opacity 0.2s ease;
`;
// Create modal container
const modal = document.createElement('div');
modal.style.cssText = `
background: #1a1a1a;
border-radius: 16px;
padding: 32px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
width: 480px;
max-width: 90vw;
max-height: 90vh;
overflow-y: auto;
transform: scale(0.95) translateY(20px);
transition: all 0.2s ease;
color: #ffffff;
border: 1px solid #404040;
`;
// Create form element for better semantics
const form = document.createElement('form');
form.setAttribute('novalidate', '');
// Create title
const title = document.createElement('h2');
title.id = 'modal-title';
title.textContent = 'Game Quality Settings';
title.style.cssText = `
margin: 0 0 24px 0;
font-size: 24px;
font-weight: 600;
color: #e0e0e0;
text-align: center;
line-height: 1.3;
`;
// Create game rating section
const ratingSection = document.createElement('div');
ratingSection.style.cssText = `
margin-bottom: 32px;
padding: 24px;
background: #2a2a2a;
border-radius: 10px;
border: 1px solid #404040;
`;
const ratingFieldset = document.createElement('fieldset');
ratingFieldset.style.cssText = `
border: none;
padding: 0;
margin: 0;
`;
const ratingLegend = document.createElement('legend');
ratingLegend.textContent = 'Game Rating Threshold';
ratingLegend.style.cssText = `
font-weight: 600;
color: #e0e0e0;
font-size: 16px;
margin-bottom: 16px;
padding: 0;
`;
const ratingContainer = document.createElement('div');
ratingContainer.style.cssText = `
display: flex;
align-items: center;
gap: 16px;
`;
const ratingSlider = document.createElement('input');
ratingSlider.type = 'range';
ratingSlider.id = 'game-rating-slider';
ratingSlider.name = 'gameRating';
ratingSlider.min = '1';
ratingSlider.max = '100';
ratingSlider.step = '1';
ratingSlider.value = localStorage.getItem('ROLOCATE_gamerating') || '75';
ratingSlider.setAttribute('aria-label', 'Game rating threshold percentage');
ratingSlider.style.cssText = `
flex: 1;
height: 6px;
border-radius: 3px;
background: #333333;
outline: none;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
`;
// Add slider styling
const sliderStyles = document.createElement('style');
sliderStyles.textContent = `
#game-rating-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #166534;
cursor: pointer;
border: 2px solid #ffffff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
#game-rating-slider::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: #166534;
cursor: pointer;
border: 2px solid #ffffff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
#game-rating-slider:focus::-webkit-slider-thumb {
box-shadow: 0 0 0 3px rgba(22, 101, 52, 0.25);
}
#game-rating-slider:focus::-moz-range-thumb {
box-shadow: 0 0 0 3px rgba(22, 101, 52, 0.25);
}
`;
document.head.appendChild(sliderStyles);
const ratingDisplay = document.createElement('div');
ratingDisplay.style.cssText = `
min-width: 60px;
text-align: center;
font-weight: 600;
color: #cccccc;
font-size: 16px;
`;
const ratingValue = document.createElement('span');
ratingValue.id = 'rating-value';
ratingValue.textContent = `${ratingSlider.value}%`;
ratingValue.setAttribute('aria-live', 'polite');
const ratingDescription = document.createElement('p');
ratingDescription.style.cssText = `
margin: 12px 0 0 0;
font-size: 14px;
color: #b0b0b0;
line-height: 1.4;
`;
ratingDescription.textContent = 'Show games with ratings at or above this threshold';
ratingSlider.addEventListener('input', function() {
ratingValue.textContent = `${this.value}%`;
});
ratingDisplay.appendChild(ratingValue);
ratingContainer.appendChild(ratingSlider);
ratingContainer.appendChild(ratingDisplay);
ratingFieldset.appendChild(ratingLegend);
ratingFieldset.appendChild(ratingContainer);
ratingFieldset.appendChild(ratingDescription);
ratingSection.appendChild(ratingFieldset);
// Create player count section
const playerSection = document.createElement('div');
playerSection.style.cssText = `
margin-bottom: 32px;
padding: 24px;
background: #2a2a2a;
border-radius: 10px;
border: 1px solid #404040;
`;
const playerFieldset = document.createElement('fieldset');
playerFieldset.style.cssText = `
border: none;
padding: 0;
margin: 0;
`;
const playerLegend = document.createElement('legend');
playerLegend.textContent = 'Player Count Range';
playerLegend.style.cssText = `
font-weight: 600;
color: #e0e0e0;
font-size: 16px;
margin-bottom: 16px;
padding: 0;
`;
const inputGrid = document.createElement('div');
inputGrid.style.cssText = `
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 12px;
`;
// Minimum player count input
const minContainer = document.createElement('div');
const minLabel = document.createElement('label');
minLabel.textContent = 'Minimum Players';
minLabel.setAttribute('for', 'min-players');
minLabel.style.cssText = `
display: block;
margin-bottom: 6px;
font-weight: 500;
color: #e0e0e0;
font-size: 14px;
`;
// Load existing player count settings
const existingPlayerCount = localStorage.getItem('ROLOCATE_playercount');
let minPlayerValue = '2500';
let maxPlayerValue = 'unlimited';
if (existingPlayerCount) {
try {
const playerCountData = JSON.parse(existingPlayerCount);
minPlayerValue = playerCountData.min || '2500';
maxPlayerValue = playerCountData.max || 'unlimited';
} catch (e) {
// If parsing fails, use defaults
console.warn('Failed to parse player count data, using defaults');
}
}
const minInput = document.createElement('input');
minInput.type = 'number';
minInput.id = 'min-players';
minInput.name = 'minPlayers';
minInput.min = '0';
minInput.max = '1000000';
minInput.value = minPlayerValue;
minInput.setAttribute('aria-describedby', 'player-count-desc');
minInput.style.cssText = `
width: 100%;
padding: 12px;
background: #333333;
border: 2px solid #555555;
border-radius: 8px;
color: #ffffff;
font-size: 14px;
transition: border-color 0.15s ease;
outline: none;
box-sizing: border-box;
`;
// Maximum player count input
const maxContainer = document.createElement('div');
const maxLabel = document.createElement('label');
maxLabel.textContent = 'Maximum Players';
maxLabel.setAttribute('for', 'max-players');
maxLabel.style.cssText = `
display: block;
margin-bottom: 6px;
font-weight: 500;
color: #495057;
font-size: 14px;
`;
const maxInput = document.createElement('input');
maxInput.type = 'text';
maxInput.id = 'max-players';
maxInput.name = 'maxPlayers';
maxInput.value = maxPlayerValue;
maxInput.setAttribute('aria-describedby', 'player-count-desc');
maxInput.placeholder = 'Enter number or "unlimited"';
maxInput.style.cssText = `
width: 100%;
padding: 12px;
background: #333333;
border: 2px solid #555555;
border-radius: 8px;
color: #ffffff;
font-size: 14px;
transition: border-color 0.15s ease;
outline: none;
box-sizing: border-box;
`;
const playerDescription = document.createElement('p');
playerDescription.id = 'player-count-desc';
playerDescription.style.cssText = `
margin: 0;
font-size: 14px;
color: #b0b0b0;
line-height: 1.4;
`;
playerDescription.textContent = 'Filter games by active player count. Use "unlimited" for no upper limit.';
// Error message container
const errorContainer = document.createElement('div');
errorContainer.style.cssText = `
margin-top: 12px;
padding: 8px 12px;
background: #2a2a2a;
color: #ff4757;
border: 1px solid #ff6b6b;
border-radius: 8px;
font-size: 14px;
display: none;
`;
// Input validation and focus effects
[minInput, maxInput].forEach(input => {
input.addEventListener('focus', function() {
this.style.borderColor = '#166534';
this.style.boxShadow = '0 0 0 3px rgba(22, 101, 52, 0.25)';
});
input.addEventListener('blur', function() {
this.style.borderColor = '#555555';
this.style.boxShadow = 'none';
validateInputs();
});
input.addEventListener('input', validateInputs);
});
function validateInputs() {
errorContainer.style.display = 'none';
const minValue = parseInt(minInput.value);
const maxValue = maxInput.value.toLowerCase() === 'unlimited' ? Infinity : parseInt(maxInput.value);
let isValid = true;
let errorMessage = '';
if (isNaN(minValue) || minValue < 0) {
isValid = false;
errorMessage = 'Minimum player count must be a valid number greater than or equal to 0.';
} else if (maxInput.value.toLowerCase() !== 'unlimited' && (isNaN(maxValue) || maxValue < 0)) {
isValid = false;
errorMessage = 'Maximum player count must be a valid number or "unlimited".';
} else if (maxValue !== Infinity && minValue > maxValue) {
isValid = false;
errorMessage = 'Minimum player count cannot be greater than maximum player count.';
}
if (!isValid) {
errorContainer.textContent = errorMessage;
errorContainer.style.display = 'block';
}
return isValid;
}
minContainer.appendChild(minLabel);
minContainer.appendChild(minInput);
maxContainer.appendChild(maxLabel);
maxContainer.appendChild(maxInput);
inputGrid.appendChild(minContainer);
inputGrid.appendChild(maxContainer);
playerFieldset.appendChild(playerLegend);
playerFieldset.appendChild(inputGrid);
playerFieldset.appendChild(playerDescription);
playerFieldset.appendChild(errorContainer);
playerSection.appendChild(playerFieldset);
// Create button container
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 32px;
`;
// Create cancel button
const cancelButton = document.createElement('button');
cancelButton.type = 'button';
cancelButton.textContent = 'Cancel';
cancelButton.style.cssText = `
padding: 12px 24px;
background: #333333;
color: #cccccc;
border: 2px solid #555555;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.15s ease;
outline: none;
`;
cancelButton.addEventListener('mouseenter', function() {
this.style.backgroundColor = '#404040';
this.style.borderColor = '#666666';
});
cancelButton.addEventListener('mouseleave', function() {
this.style.backgroundColor = '#333333';
this.style.borderColor = '#555555';
});
cancelButton.addEventListener('focus', function() {
this.style.boxShadow = '0 0 0 3px rgba(108, 117, 125, 0.25)';
});
cancelButton.addEventListener('blur', function() {
this.style.boxShadow = 'none';
});
// Create save button
const saveButton = document.createElement('button');
saveButton.type = 'submit';
saveButton.textContent = 'Save Settings';
saveButton.style.cssText = `
padding: 12px 24px;
background: #166534;
color: white;
border: 2px solid #166534;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.15s ease;
outline: none;
`;
saveButton.addEventListener('mouseenter', function() {
this.style.backgroundColor = '#14532d';
this.style.borderColor = '#14532d';
});
saveButton.addEventListener('mouseleave', function() {
this.style.backgroundColor = '#166534';
this.style.borderColor = '#166534';
});
saveButton.addEventListener('focus', function() {
this.style.boxShadow = '0 0 0 3px rgba(22, 101, 52, 0.25)';
});
saveButton.addEventListener('blur', function() {
this.style.boxShadow = 'none';
});
// Form submission handler
form.addEventListener('submit', function(e) {
e.preventDefault();
if (!validateInputs()) {
return;
}
try {
const playerCountData = {
min: minInput.value,
max: maxInput.value
};
localStorage.setItem('ROLOCATE_gamerating', ratingSlider.value);
localStorage.setItem('ROLOCATE_playercount', JSON.stringify(playerCountData));
closeModal();
} catch (error) {
console.error('Failed to save settings:', error);
errorContainer.textContent = 'Failed to save settings. Please try again.';
errorContainer.style.display = 'block';
}
});
cancelButton.addEventListener('click', closeModal);
function closeModal() {
modal.style.transform = 'scale(0.95) translateY(20px)';
overlay.style.opacity = '0';
setTimeout(() => {
if (document.body.contains(overlay)) {
document.body.removeChild(overlay);
}
if (document.head.contains(sliderStyles)) {
document.head.removeChild(sliderStyles);
}
}, 200);
}
buttonContainer.appendChild(cancelButton);
buttonContainer.appendChild(saveButton);
// Assemble form
form.appendChild(title);
form.appendChild(ratingSection);
form.appendChild(playerSection);
form.appendChild(buttonContainer);
modal.appendChild(form);
overlay.appendChild(modal);
// Close modal on overlay click
overlay.addEventListener('click', function(e) {
if (e.target === overlay) {
closeModal();
}
});
// Keyboard navigation support
overlay.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeModal();
}
if (e.key === 'Tab') {
const focusableElements = modal.querySelectorAll(
'input, button, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
});
// Add modal to page and trigger animation
document.body.appendChild(overlay);
// Trigger entrance animation
requestAnimationFrame(() => {
overlay.style.opacity = '1';
modal.style.transform = 'scale(1) translateY(0)';
});
// Focus first input for accessibility
setTimeout(() => {
ratingSlider.focus();
}, 250);
}
/*******************************************************
name of function: qualityfilterRobloxGames
description: game quality filtererererer
*******************************************************/
function qualityfilterRobloxGames() {
// Check if game quality filter is enabled
const filterEnabled = localStorage.getItem('ROLOCATE_gamequalityfilter');
if (filterEnabled !== 'true') {
return; // Do nothing if filter is not enabled
}
// Check if we're on a valid Roblox page
const currentUrl = window.location.href.toLowerCase();
const validPaths = [
'/charts/',
'/charts#/',
'/home/',
'/discover',
'/games/'
];
// Check for language prefix (e.g., /en/, /es/, /fr/, etc.)
const languagePattern = /roblox\.com\/[a-z]{2}\/|roblox\.com\//;
let isValidPage = false;
if (languagePattern.test(currentUrl)) {
for (let path of validPaths) {
// Check both with and without language prefix
if (currentUrl.includes('roblox.com' + path) ||
currentUrl.match(new RegExp('roblox\\.com/[a-z]{2}' + path.replace('/', '\\/')))) {
isValidPage = true;
break;
}
}
}
if (!isValidPage) {
return; // Do nothing if not on a valid page
}
// Get filter criteria from localStorage
const gameRatingThreshold = parseInt(localStorage.getItem('ROLOCATE_gamerating') || '80');
const playerCountData = JSON.parse(localStorage.getItem('ROLOCATE_playercount') || '{"min":"5000","max":"unlimited"}');
const minPlayerCount = parseInt(playerCountData.min);
const maxPlayerCount = playerCountData.max === 'unlimited' ? Infinity : parseInt(playerCountData.max);
// Function to parse player count from text (handles K, M suffixes)
function parsePlayerCount(countText) {
if (!countText) return 0;
const cleanText = countText.replace(/[,\s]/g, '').toLowerCase();
let multiplier = 1;
let numberPart = cleanText;
if (cleanText.includes('k')) {
multiplier = 1000;
numberPart = cleanText.replace('k', '');
} else if (cleanText.includes('m')) {
multiplier = 1000000;
numberPart = cleanText.replace('m', '');
}
const number = parseFloat(numberPart);
return isNaN(number) ? 0 : number * multiplier;
}
// Function to filter games
function filterGameCards() {
// Find all game cards using multiple selectors
const gameCards = document.querySelectorAll([
'li.game-card',
'li[data-testid="wide-game-tile"]',
'.grid-item-container.game-card-container'
].join(', '));
gameCards.forEach(card => {
try {
// Find rating percentage - try multiple selectors
let ratingElement = card.querySelector('.vote-percentage-label');
let rating = 0;
if (ratingElement) {
const ratingText = ratingElement.textContent || ratingElement.innerText;
const ratingMatch = ratingText.match(/(\d+)%/);
if (ratingMatch) {
rating = parseInt(ratingMatch[1]);
}
} else {
// Try alternative selectors for different card types
const altSelectors = [
'[data-testid="game-tile-stats-rating"] .vote-percentage-label',
'.game-card-info .vote-percentage-label',
'.base-metadata .vote-percentage-label'
];
for (let selector of altSelectors) {
ratingElement = card.querySelector(selector);
if (ratingElement) {
const ratingText = ratingElement.textContent || ratingElement.innerText;
const ratingMatch = ratingText.match(/(\d+)%/);
if (ratingMatch) {
rating = parseInt(ratingMatch[1]);
break;
}
}
}
}
// Find player count
const playerCountElement = card.querySelector('.playing-counts-label');
let playerCount = 0;
let hasPlayerCount = false;
if (playerCountElement) {
const countText = playerCountElement.textContent || playerCountElement.innerText;
playerCount = parsePlayerCount(countText);
hasPlayerCount = true;
}
// Apply filters - BOTH requirements must be met to show the game
let shouldShow = true;
// Check rating filter - game must meet rating threshold
if (rating < gameRatingThreshold) {
shouldShow = false;
}
// Check player count filter - game must meet player count requirements
// Only apply player count filter if the card has player count info
if (hasPlayerCount && (playerCount < minPlayerCount || playerCount > maxPlayerCount)) {
shouldShow = false;
}
// Hide or show the game card
if (shouldShow) {
card.style.display = '';
} else {
card.style.display = 'none';
}
} catch (error) {
console.warn('Error filtering game card:', error);
}
});
}
// Initial filter
filterGameCards();
// Set up observer to watch for new game cards being added
const observer = new MutationObserver(function(mutations) {
let shouldRefilter = false;
mutations.forEach(function(mutation) {
// Check if new nodes were added
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// Check if any of the added nodes contain game cards
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1) { // Element node
if (node.matches && (
node.matches('li.game-card') ||
node.matches('li[data-testid="wide-game-tile"]') ||
node.matches('.grid-item-container.game-card-container') ||
node.querySelector('li.game-card, li[data-testid="wide-game-tile"], .grid-item-container.game-card-container')
)) {
shouldRefilter = true;
}
}
});
}
});
if (shouldRefilter) {
// Debounce the filtering to avoid excessive calls
clearTimeout(observer.timeoutId);
observer.timeoutId = setTimeout(filterGameCards, 100);
}
});
// Start observing
observer.observe(document.body, {
childList: true,
subtree: true
});
// Store observer reference for potential cleanup
window.robloxGameFilterObserver = observer;
}
/*******************************************************
name of function: showOldRobloxGreeting
description: shows old roblox greeting if setting is
turned on
*******************************************************/
async function showOldRobloxGreeting() {
ConsoleLogEnabled("Function showOldRobloxGreeting() started.");
// Check if the URL is roblox.com/home
if (!window.location.href.includes("roblox.com/home")) {
ConsoleLogEnabled("Not on roblox.com/home. Exiting function.");
return; // stops execution if not on the home page
}
// Check LocalStorage before proceeding
if (localStorage.getItem("ROLOCATE_ShowOldGreeting") !== "true") {
ConsoleLogEnabled("ShowOldGreeting is disabled. Exiting function.");
return; // stops execution if setting is off
}
ConsoleLogEnabled("Waiting 500ms before proceeding.");
await new Promise(r => setTimeout(r, 500));
/*******************************************************
name of function: observeElement
description: Finds pecific element to place old greeting
*******************************************************/
function observeElement(selector) {
ConsoleLogEnabled(`Observing element: ${selector}`);
return new Promise((resolve) => {
const observer = new MutationObserver(() => {
const element = document.querySelector(selector);
if (element) {
ConsoleLogEnabled(`Element found: ${selector}`);
observer.disconnect();
resolve(element);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
/*******************************************************
name of function: fetchAvatar
description: find user avatar from the page
*******************************************************/
async function fetchAvatar(selector, fallbackImage) {
ConsoleLogEnabled(`Fetching avatar from selector: ${selector}`);
for (let attempt = 0; attempt < 3; attempt++) {
ConsoleLogEnabled(`Attempt ${attempt + 1} to fetch avatar.`);
const imgElement = document.querySelector(selector);
if (imgElement && imgElement.src !== fallbackImage) {
ConsoleLogEnabled(`Avatar found: ${imgElement.src}`);
return imgElement.src;
}
await new Promise(r => setTimeout(r, 1500));
}
ConsoleLogEnabled("Avatar not found, using fallback image.");
return fallbackImage;
}
let homeContainer = await observeElement("#HomeContainer .section:first-child");
ConsoleLogEnabled("Home container located.");
let userNameElement = document.querySelector("#navigation.rbx-left-col > ul > li > a .font-header-2");
ConsoleLogEnabled(`User name found: ${userNameElement ? userNameElement.innerText : "Unknown"}`);
let user = {
name: userNameElement ? `Hello, ${userNameElement.innerText}!` : "Hello, Roblox User!",
avatar: await fetchAvatar("#navigation.rbx-left-col > ul > li > a img", window.Base64Images.image_place_holder)
};
ConsoleLogEnabled(`Final user details: Name - ${user.name}, Avatar - ${user.avatar}`);
let headerContainer = document.createElement("div");
headerContainer.classList.add("new-header");
// Removed fade-in opacity initialization here
let profileFrame = document.createElement("div");
profileFrame.classList.add("profile-frame");
let profileImage = document.createElement("img");
profileImage.src = user.avatar;
profileImage.classList.add("profile-img");
profileFrame.appendChild(profileImage);
let userDetails = document.createElement("div");
userDetails.classList.add("user-details");
let userName = document.createElement("h1");
userName.classList.add("user-name");
userName.textContent = user.name;
userDetails.appendChild(userName);
headerContainer.appendChild(profileFrame);
headerContainer.appendChild(userDetails);
ConsoleLogEnabled("Replacing old home container with new header.");
homeContainer.replaceWith(headerContainer);
let styleTag = document.createElement("style");
styleTag.textContent = `
.new-header {
display: flex;
align-items: center;
margin-bottom: 30px;
/* Removed opacity transition */
}
.profile-frame {
width: 150px;
height: 150px;
border-radius: 50%;
overflow: hidden;
border: 3px solid #121215;
display: flex;
justify-content: center;
align-items: center;
}
.profile-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.user-details {
margin-left: 20px;
display: flex;
align-items: center;
}
.user-name {
font-size: 1.2em;
font-weight: bold;
color: white;
}
`;
document.head.appendChild(styleTag);
ConsoleLogEnabled("Style tag added.");
}
/*******************************************************
name of function: observeURLChanges
description: observes url changes for the old old greeting
*******************************************************/
let lastUrl = window.location.href.split("#")[0]; // Store only the base URL
function observeURLChanges() {
let lastUrl = window.location.href.split("#")[0];
const checkUrl = () => {
const currentUrl = window.location.href.split("#")[0];
if (currentUrl !== lastUrl) {
ConsoleLogEnabled(`URL changed from ${lastUrl} to ${currentUrl}`);
lastUrl = currentUrl;
if (currentUrl.includes("roblox.com/home")) {
ConsoleLogEnabled("Detected return to home page. Reloading greeting.");
showOldRobloxGreeting();
}
}
};
// Intercept pushState and replaceState
const interceptHistoryMethod = (method) => {
const original = history[method];
history[method] = function(...args) {
original.apply(this, args);
checkUrl();
};
};
interceptHistoryMethod('pushState');
interceptHistoryMethod('replaceState');
window.addEventListener('popstate', checkUrl); // For back/forward navigation
}
/*******************************************************
name of function: quicknavbutton
description: Adds the quick nav buttons to the side panel
if it is turned on
*******************************************************/
function quicknavbutton() {
if (localStorage.getItem('ROLOCATE_quicknav') === 'true') {
const settingsRaw = localStorage.getItem('ROLOCATE_quicknav_settings');
if (!settingsRaw) return;
let settings;
try {
settings = JSON.parse(settingsRaw);
} catch (e) {
ConsoleLogEnabled('Failed to parse ROLOCATE_quicknav_settings:', e);
return;
}
const sidebar = document.querySelector('.left-col-list');
if (!sidebar) return;
const premiumButton = sidebar.querySelector('.rbx-upgrade-now');
const style = document.createElement('style');
style.textContent = `
.rolocate-icon-custom {
display: inline-block;
width: 24px;
height: 24px;
margin-left: 3px;
background-image: url("${window.Base64Images.quicknav}");
background-size: contain;
background-repeat: no-repeat;
transition: filter 0.2s ease;
}
`;
document.head.appendChild(style);
settings.forEach(({
name,
link
}) => {
const li = document.createElement('li');
const a = document.createElement('a');
a.className = 'dynamic-overflow-container text-nav';
a.href = link;
a.target = '_self';
const divIcon = document.createElement('div');
const spanIcon = document.createElement('span');
spanIcon.className = 'rolocate-icon-custom';
divIcon.appendChild(spanIcon);
const spanText = document.createElement('span');
spanText.className = 'font-header-2 dynamic-ellipsis-item';
spanText.title = name;
spanText.textContent = name;
a.appendChild(divIcon);
a.appendChild(spanText);
li.appendChild(a);
if (premiumButton && premiumButton.parentElement === sidebar) {
sidebar.insertBefore(li, premiumButton);
} else {
sidebar.appendChild(li);
}
});
}
}
/*******************************************************
name of function: validateManualMode
description: Check if user set their location manually
or if it is still in automatic. Some error handling also
*******************************************************/
function validateManualMode() {
// Check if in manual mode
if (localStorage.getItem("ROLOCATE_prioritylocation") === "manual") {
ConsoleLogEnabled("Manual mode detected");
try {
// Get stored coordinates
const coords = JSON.parse(GM_getValue("ROLOCATE_coordinates", '{"lat":"","lng":""}'));
ConsoleLogEnabled("Coordinates fetched:", coords);
// If coordinates are empty, switch to automatic
if (!coords.lat || !coords.lng) {
localStorage.setItem("ROLOCATE_prioritylocation", "automatic");
ConsoleLogEnabled("No coordinates set. Switched to automatic mode.");
return true; // Indicates that a switch occurred
}
} catch (e) {
ConsoleLogEnabled("Error checking coordinates:", e);
// If there's an error reading coordinates, switch to automatic
localStorage.setItem("ROLOCATE_prioritylocation", "automatic");
ConsoleLogEnabled("Error encountered while fetching coordinates. Switched to automatic mode.");
return true;
}
}
ConsoleLogEnabled("No Errors detected.");
return false; // No switch occurred
}
/*******************************************************
name of function: event listener
description: Note a function but runs the initial setup for the scirpt to actually
start working. Very important
*******************************************************/
window.addEventListener("load", () => {
loadBase64Library(() => {
ConsoleLogEnabled("Loaded Base64Images. It is ready to use!");
});
AddSettingsButton(() => {
ConsoleLogEnabled("Loaded Settings button!");
});
Update_Popup();
initializeLocalStorage();
removeAds();
showOldRobloxGreeting();
ConsoleLogEnabled("Loaded Settings!");
quicknavbutton();
validateManualMode();
qualityfilterRobloxGames();
// Start observing URL changes
observeURLChanges();
});
/*******************************************************
name of function: loadBase64Library
description: Loads base64 images
*******************************************************/
function loadBase64Library(callback, timeout = 5000) {
let elapsed = 0;
(function waitForLibrary() {
if (typeof window.Base64Images === "undefined") {
if (elapsed < timeout) {
elapsed += 50;
setTimeout(waitForLibrary, 50);
} else {
ConsoleLogEnabled("Base64Images did not load within the timeout.");
notifications('An error occurred! No icons will show. Please refresh the page.', 'error', '⚠️', '8000')
}
} else {
if (callback) callback();
}
})();
}
/*******************************************************
The code for the random hop button and the filter button on roblox.com/games/*
*******************************************************/
if (window.location.href.includes("/games/") && (localStorage.getItem("ROLOCATE_togglefilterserversbutton") === "true" || localStorage.getItem("ROLOCATE_toggleserverhopbutton") === "true" || localStorage.getItem("ROLOCATE_togglerecentserverbutton") === "true")) {
let Isongamespage = true;
if (window.location.href.includes("/games/")) { // saftey check and lazy load data to save the 2mb of ram lmao
loadServerRegions(); // loads the data
if (window.serverRegionsByIp) {
ConsoleLogEnabled("Server regions data loaded successfully.");
} else {
ConsoleLogEnabled("Failed to load server regions data.");
}
}
/*********************************************************************************************************************************************************************************************************************************************
This is all of the functions for the filter button and the popup for the 7 buttons does not include the functions for the 8 buttons
*********************************************************************************************************************************************************************************************************************************************/
//Testing
//HandleRecentServersAddGames("126884695634066", "853e79a5-1a2b-4178-94bf-a242de1aecd6");
//HandleRecentServersAddGames("126884695634066", "a08849f1-40e32-4b3215c-31231231a268-e948519caf39");
//HandleRecentServersAddGames("126884695634066", "a08849f1-40e32-4b5c-31236541231a268-e948519caf39");
//HandleRecentServersAddGames("126884695634066", "a08849f1-40e32-4b5c-31231287631a268-e948519caf39");
//HandleRecentServersAddGames("126884695634066", "a08849f1-40e32-4b5c-31231231a268-87e948519caf39");
//HandleRecentServersAddGames("126884695634066", "a08849f1-40e32-4b5c-31231231a268089-e948519caf39");
//document.querySelector('.recent-servers-section')?.remove(); // remove old list
//HandleRecentServers(); // re-render with updated order
/*******************************************************
name of function: InitRobloxLaunchHandler
description: Basically detects if the user joins a
roblox server and then adds that to recent servers
*******************************************************/
function InitRobloxLaunchHandler() {
if (!/^https:\/\/www\.roblox\.com(\/[a-z]{2})?\/games\//.test(window.location.href)) return;
if (window._robloxJoinInterceptorInitialized) return;
window._robloxJoinInterceptorInitialized = true;
const originalJoin = Roblox.GameLauncher.joinGameInstance;
Roblox.GameLauncher.joinGameInstance = function(gameId, serverId) {
ConsoleLogEnabled(`Intercepted join: Game ID = ${gameId}, Server ID = ${serverId}`);
HandleRecentServersAddGames(gameId, serverId);
document.querySelector('.recent-servers-section')?.remove(); // remove old list
HandleRecentServers(); // re-render with updated order
return originalJoin.apply(this, arguments);
};
}
/*******************************************************
name of function: HandleRecentServersAddGames
description: Adds recent servers to localstorage for safe
keeping
*******************************************************/
function HandleRecentServersAddGames(gameId, serverId) {
const storageKey = "ROLOCATE_recentservers_button";
const stored = JSON.parse(localStorage.getItem(storageKey) || "{}");
const key = `${gameId}_${serverId}`;
stored[key] = Date.now(); // Always update timestamp
localStorage.setItem(storageKey, JSON.stringify(stored));
}
/*******************************************************
name of function: HandleRecentServersURL
description: Detects recent servers from the url if
user joins server from invite url
*******************************************************/
function HandleRecentServersURL() {
// Static-like variable to remember if we've already found an invalid URL
if (HandleRecentServersURL.alreadyInvalid) {
return; // Skip if previously marked as invalid
}
const url = window.location.href;
// Regex pattern to match ROLOCATE_GAMEID and SERVERID from the hash
const match = url.match(/ROLOCATE_GAMEID=(\d+)_SERVERID=([a-f0-9-]+)/i);
if (match && match.length === 3) {
const gameId = match[1];
const serverId = match[2];
// Call the handler with extracted values
HandleRecentServersAddGames(gameId, serverId);
InitRobloxLaunchHandler();
} else {
ConsoleLogEnabled("No gameId and serverId found in URL.");
InitRobloxLaunchHandler();
HandleRecentServersURL.alreadyInvalid = true; // Set internal flag
}
}
/*******************************************************
name of function: HandleRecentServers
description: Detects if recent servers are in localstoage
and then adds them to the page with css styles
*******************************************************/
function HandleRecentServers() {
const serverList = document.querySelector('.server-list-options');
if (!serverList || document.querySelector('.recent-servers-section')) return;
const match = window.location.href.match(/\/games\/(\d+)\//);
if (!match) return;
const currentGameId = match[1];
const allHeaders = document.querySelectorAll('.server-list-header');
let friendsSectionHeader = null;
allHeaders.forEach(header => {
if (header.textContent.trim() === 'Servers My Friends Are In') {
friendsSectionHeader = header.closest('.container-header');
}
});
if (!friendsSectionHeader) return;
// Custom premium dark theme CSS variables
const theme = {
bgDark: '#14161a',
bgCard: '#1c1f25',
bgCardHover: '#22262e',
bgGradient: 'linear-gradient(145deg, #1e2228, #18191e)',
bgGradientHover: 'linear-gradient(145deg, #23272f, #1c1f25)',
accentPrimary: '#4d85ee',
accentSecondary: '#3464c9',
accentGradient: 'linear-gradient(to bottom, #4d85ee, #3464c9)',
accentGradientHover: 'linear-gradient(to bottom, #5990ff, #3b6fdd)',
textPrimary: '#e8ecf3',
textSecondary: '#a0a8b8',
textMuted: '#6c7484',
borderLight: 'rgba(255, 255, 255, 0.06)',
borderLightHover: 'rgba(255, 255, 255, 0.12)',
shadow: '0 5px 15px rgba(0, 0, 0, 0.25)',
shadowHover: '0 8px 25px rgba(0, 0, 0, 0.3)',
dangerColor: '#ff5b5b',
dangerColorHover: '#ff7575',
dangerGradient: 'linear-gradient(to bottom, #ff5b5b, #e04444)',
dangerGradientHover: 'linear-gradient(to bottom, #ff7575, #f55)'
};
const recentSection = document.createElement('div');
recentSection.className = 'recent-servers-section premium-dark';
recentSection.style.marginBottom = '24px';
const headerContainer = document.createElement('div');
headerContainer.className = 'container-header';
const headerInner = document.createElement('div');
headerInner.className = 'server-list-container-header';
headerInner.style.padding = '0 4px';
const headerTitle = document.createElement('h2');
headerTitle.className = 'server-list-header';
headerTitle.textContent = 'Recent Servers';
headerTitle.style.cssText = `
font-weight: 600;
color: ${theme.textPrimary};
letter-spacing: 0.5px;
position: relative;
display: inline-block;
padding-bottom: 4px;
`;
// Add premium underline accent to header
const headerAccent = document.createElement('span');
headerAccent.style.cssText = `
position: absolute;
bottom: 0;
left: 0;
width: 40px;
height: 2px;
background: ${theme.accentGradient};
border-radius: 2px;
`;
headerTitle.appendChild(headerAccent);
headerInner.appendChild(headerTitle);
headerContainer.appendChild(headerInner);
const contentContainer = document.createElement('div');
contentContainer.className = 'section-content-off empty-game-instances-container';
contentContainer.style.padding = '8px 4px';
const storageKey = "ROLOCATE_recentservers_button";
let stored = JSON.parse(localStorage.getItem(storageKey) || "{}");
// Auto-remove servers older than 3 days
const currentTime = Date.now();
const threeDaysInMs = 3 * 24 * 60 * 60 * 1000; // 3days in miliseconds
let storageUpdated = false;
Object.keys(stored).forEach(key => {
const serverTime = stored[key];
if (currentTime - serverTime > threeDaysInMs) {
delete stored[key];
storageUpdated = true;
}
});
if (storageUpdated) {
localStorage.setItem(storageKey, JSON.stringify(stored));
}
const keys = Object.keys(stored).filter(key => key.startsWith(`${currentGameId}_`));
if (keys.length === 0) {
const emptyMessage = document.createElement('div');
emptyMessage.className = 'no-servers-message';
emptyMessage.innerHTML = `
No Recent Servers Found`;
emptyMessage.style.cssText = `
color: ${theme.textSecondary};
text-align: center;
padding: 28px 0;
font-size: 14px;
letter-spacing: 0.3px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
background: rgba(20, 22, 26, 0.4);
backdrop-filter: blur(5px);
border-radius: 12px;
border: 1px solid rgba(77, 133, 238, 0.15);
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.2);
`;
contentContainer.appendChild(emptyMessage);
} else {
keys.sort((a, b) => stored[b] - stored[a]);
// Create server cards wrapper
const cardsWrapper = document.createElement('div');
cardsWrapper.style.cssText = `
display: flex;
flex-direction: column;
gap: 12px;
margin: 2px 0;
`;
keys.forEach((key, index) => {
const [gameId, serverId] = key.split("_");
const timeStored = stored[key];
const date = new Date(timeStored);
const formattedTime = date.toLocaleString(undefined, {
hour: '2-digit',
minute: '2-digit',
year: 'numeric',
month: 'short',
day: 'numeric'
});
const serverCard = document.createElement('div');
serverCard.className = 'recent-server-card premium-dark';
serverCard.dataset.serverKey = key;
serverCard.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 22px;
height: 76px;
border-radius: 14px;
background: ${theme.bgGradient};
box-shadow: ${theme.shadow};
color: ${theme.textPrimary};
font-family: 'Segoe UI', 'Helvetica Neue', sans-serif;
font-size: 14px;
box-sizing: border-box;
width: 100%;
position: relative;
overflow: hidden;
border: 1px solid ${theme.borderLight};
transition: all 0.2s ease-out;
`;
// Add hover effect
serverCard.onmouseover = function() {
this.style.boxShadow = theme.shadowHover;
this.style.transform = 'translateY(-2px)';
this.style.borderColor = theme.borderLightHover;
this.style.background = theme.bgGradientHover;
};
serverCard.onmouseout = function() {
this.style.boxShadow = theme.shadow;
this.style.transform = 'translateY(0)';
this.style.borderColor = theme.borderLight;
this.style.background = theme.bgGradient;
};
// Add glass effect overlay
const glassOverlay = document.createElement('div');
glassOverlay.style.cssText = `
position: absolute;
left: 0;
top: 0;
right: 0;
height: 50%;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0));
border-radius: 14px 14px 0 0;
pointer-events: none;
`;
serverCard.appendChild(glassOverlay);
// Server icon with glow
const serverIconWrapper = document.createElement('div');
serverIconWrapper.style.cssText = `
position: absolute;
left: 14px;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
`;
const serverIcon = document.createElement('div');
serverIcon.innerHTML = `
`;
serverIconWrapper.appendChild(serverIcon);
// Add subtle glow to the server icon
const iconGlow = document.createElement('div');
iconGlow.style.cssText = `
position: absolute;
width: 24px;
height: 24px;
border-radius: 50%;
background: ${theme.accentPrimary};
opacity: 0.15;
filter: blur(8px);
z-index: -1;
`;
serverIconWrapper.appendChild(iconGlow);
const left = document.createElement('div');
left.style.cssText = `
display: flex;
flex-direction: column;
justify-content: center;
margin-left: 12px;
`;
const lastPlayed = document.createElement('div');
lastPlayed.textContent = `Last Played: ${formattedTime}`;
lastPlayed.style.cssText = `
font-weight: 600;
font-size: 14px;
color: ${theme.textPrimary};
line-height: 1.3;
letter-spacing: 0.3px;
margin-left: 40px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
`;
const metaInfo = document.createElement('div');
metaInfo.innerHTML = `Game ID: ${gameId} • Server ID: ${serverId}`;
metaInfo.style.cssText = `
font-size: 12px;
color: ${theme.textSecondary};
margin-top: 5px;
opacity: 0.9;
margin-left: 40px;
`;
left.appendChild(lastPlayed);
left.appendChild(metaInfo);
serverCard.appendChild(serverIconWrapper);
const buttonGroup = document.createElement('div');
buttonGroup.style.cssText = `
display: flex;
gap: 12px;
align-items: center;
z-index: 2;
`;
// Create the smaller remove button to be positioned on the left
const removeButton = document.createElement('button');
removeButton.innerHTML = `
`;
removeButton.className = 'btn-control-xs remove-button';
removeButton.style.cssText = `
background: ${theme.dangerGradient};
color: white;
border: none;
padding: 6px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
letter-spacing: 0.4px;
box-shadow: 0 2px 8px rgba(255, 91, 91, 0.3);
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
`;
// Add remove button hover effect
removeButton.onmouseover = function() {
this.style.background = theme.dangerGradientHover;
this.style.boxShadow = '0 4px 10px rgba(255, 91, 91, 0.4)';
this.style.transform = 'translateY(-1px)';
};
removeButton.onmouseout = function() {
this.style.background = theme.dangerGradient;
this.style.boxShadow = '0 2px 8px rgba(255, 91, 91, 0.3)';
this.style.transform = 'translateY(0)';
};
// Add remove button functionality
removeButton.addEventListener('click', function(e) {
e.stopPropagation();
const serverKey = this.closest('.recent-server-card').dataset.serverKey;
// Animate removal
serverCard.style.transition = 'all 0.3s ease-out';
serverCard.style.opacity = '0';
serverCard.style.height = '0';
serverCard.style.margin = '0';
serverCard.style.padding = '0';
setTimeout(() => {
serverCard.remove();
// Update localStorage
const storedData = JSON.parse(localStorage.getItem(storageKey) || "{}");
delete storedData[serverKey];
localStorage.setItem(storageKey, JSON.stringify(storedData));
// If no servers left, show empty message
if (document.querySelectorAll('.recent-server-card').length === 0) {
const emptyMessage = document.createElement('div');
emptyMessage.className = 'no-servers-message';
emptyMessage.innerHTML = `
No Recent Servers Found`;
emptyMessage.style.cssText = `
color: ${theme.textSecondary};
text-align: center;
padding: 28px 0;
font-size: 14px;
letter-spacing: 0.3px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
background: rgba(20, 22, 26, 0.4);
backdrop-filter: blur(5px);
border-radius: 12px;
border: 1px solid rgba(77, 133, 238, 0.15);
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.2);
`;
cardsWrapper.appendChild(emptyMessage);
}
}, 300);
});
// Create a separator element
const separator = document.createElement('div');
separator.style.cssText = `
height: 24px;
width: 1px;
background-color: rgba(255, 255, 255, 0.15);
margin: 0 2px;
`;
const joinButton = document.createElement('button');
joinButton.innerHTML = `
Join
`;
joinButton.className = 'btn-control-xs join-button';
joinButton.style.cssText = `
background: ${theme.accentGradient};
color: white;
border: none;
padding: 8px 18px;
border-radius: 10px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
letter-spacing: 0.4px;
box-shadow: 0 2px 10px rgba(52, 100, 201, 0.3);
display: flex;
align-items: center;
justify-content: center;
`;
// Add join button functionality
joinButton.addEventListener('click', function() {
try {
Roblox.GameLauncher.joinGameInstance(gameId, serverId);
showLoadingOverlay();
} catch (error) {
ConsoleLogEnabled("Error joining game:", error);
}
});
// Add hover effect for join button
joinButton.onmouseover = function() {
this.style.background = theme.accentGradientHover;
this.style.boxShadow = '0 4px 12px rgba(77, 133, 238, 0.4)';
this.style.transform = 'translateY(-1px)';
};
joinButton.onmouseout = function() {
this.style.background = theme.accentGradient;
this.style.boxShadow = '0 2px 10px rgba(52, 100, 201, 0.3)';
this.style.transform = 'translateY(0)';
};
const inviteButton = document.createElement('button');
inviteButton.innerHTML = `
Invite
`;
inviteButton.className = 'btn-control-xs invite-button';
inviteButton.style.cssText = `
background: rgba(28, 31, 37, 0.6);
color: ${theme.textPrimary};
border: 1px solid rgba(255, 255, 255, 0.12);
padding: 8px 18px;
border-radius: 10px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
`;
// Add invite button functionality
inviteButton.addEventListener('click', function() {
const inviteUrl = `https://oqarshi.github.io/Invite/?placeid=${gameId}&serverid=${serverId}`;
// Disable the button temporarily
inviteButton.disabled = true;
// Copy to clipboard
navigator.clipboard.writeText(inviteUrl).then(
function() {
// Show feedback that URL was copied
const originalText = inviteButton.innerHTML;
inviteButton.innerHTML = `
Copied!
`;
ConsoleLogEnabled(`Invite link copied to clipboard`);
notifications('Success! Invite link copied to clipboard!', 'success', '🎉', '2000');
// Reset button after 1 second
setTimeout(() => {
inviteButton.innerHTML = originalText;
inviteButton.disabled = false;
}, 1000);
},
function(err) {
ConsoleLogEnabled('Could not copy text: ', err);
inviteButton.disabled = false; // re-enable in case of error
}
);
});
// Add hover effect for invite button
inviteButton.onmouseover = function() {
this.style.background = 'rgba(35, 39, 46, 0.8)';
this.style.borderColor = 'rgba(255, 255, 255, 0.18)';
this.style.transform = 'translateY(-1px)';
};
inviteButton.onmouseout = function() {
this.style.background = 'rgba(28, 31, 37, 0.6)';
this.style.borderColor = 'rgba(255, 255, 255, 0.12)';
this.style.transform = 'translateY(0)';
};
// MODIFIED: Now add buttons in the new order: Remove, Separator, Join, Invite
buttonGroup.appendChild(removeButton);
buttonGroup.appendChild(separator);
buttonGroup.appendChild(joinButton);
buttonGroup.appendChild(inviteButton);
serverCard.appendChild(left);
serverCard.appendChild(buttonGroup);
cardsWrapper.appendChild(serverCard);
// Add subtle line accent
const lineAccent = document.createElement('div');
lineAccent.style.cssText = `
position: absolute;
left: 0;
top: 16px;
bottom: 16px;
width: 3px;
background: ${theme.accentGradient};
border-radius: 0 2px 2px 0;
`;
serverCard.appendChild(lineAccent);
// Add subtle corner accent
if (index === 0) {
const cornerAccent = document.createElement('div');
cornerAccent.style.cssText = `
position: absolute;
right: 0;
top: 0;
width: 40px;
height: 40px;
overflow: hidden;
pointer-events: none;
`;
const cornerInner = document.createElement('div');
cornerInner.style.cssText = `
position: absolute;
right: -20px;
top: -20px;
width: 40px;
height: 40px;
background: ${theme.accentPrimary};
transform: rotate(45deg);
opacity: 0.15;
`;
cornerAccent.appendChild(cornerInner);
serverCard.appendChild(cornerAccent);
}
});
contentContainer.appendChild(cardsWrapper);
}
recentSection.appendChild(headerContainer);
recentSection.appendChild(contentContainer);
friendsSectionHeader.parentNode.insertBefore(recentSection, friendsSectionHeader);
}
/*******************************************************
name of function: disableYouTubeAutoplayInIframes
Description:
Disable autoplay in YouTube and youtube-nocookie iframes inside a container element.
@param {HTMLElement|Document} rootElement - The root element to search for iframes (defaults to document).
@param {boolean} [observeMutations=false] - Whether to watch for dynamically added iframes.
@returns {MutationObserver|null} Returns the MutationObserver if observing, else null.
*******************************************************/
function disableYouTubeAutoplayInIframes(rootElement = document, observeMutations = false) {
const processedFlag = 'data-autoplay-blocked';
function disableAutoplay(iframe) {
if (iframe.hasAttribute(processedFlag)) return;
const src = iframe.src;
if (!src) return;
if (!src.includes('youtube.com') && !src.includes('youtube-nocookie.com')) return;
iframe.removeAttribute('allow');
try {
const url = new URL(src);
url.searchParams.delete('autoplay');
url.searchParams.set('enablejsapi', '0');
const newSrc = url.toString();
if (src !== newSrc) {
iframe.src = newSrc;
}
iframe.setAttribute(processedFlag, 'true');
} catch (e) {
// If URL parsing fails, just skip safely
ConsoleLogEnabled('Failed to parse iframe src URL', e);
}
}
function processAll() {
const selector = 'iframe[src*="youtube.com"], iframe[src*="youtube-nocookie.com"]';
const iframes = (rootElement.querySelectorAll) ? rootElement.querySelectorAll(selector) : [];
iframes.forEach(disableAutoplay);
}
processAll();
if (!observeMutations) return null;
// Setup mutation observer if requested
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (!(node instanceof HTMLElement)) return;
if (node.tagName === 'IFRAME') {
disableAutoplay(node);
} else if (node.querySelectorAll) {
node.querySelectorAll('iframe[src*="youtube.com"], iframe[src*="youtube-nocookie.com"]')
.forEach(disableAutoplay);
}
});
});
});
observer.observe(rootElement.body || rootElement, {
childList: true,
subtree: true
});
return observer;
}
/*******************************************************
name of function: createPopup
description: Creates a popup with server filtering options and interactive 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;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
`;
// 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 = window.Base64Images.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, tooltips, experimental status, and explanations for each button
const buttonData = [{
name: "Smallest Servers",
tooltip: "**Reverses the order of the server list.** The emptiest servers will be displayed first.",
experimental: false,
new: false,
popular: false,
},
{
name: "Available Space",
tooltip: "**Filters out servers which are full.** Servers with space will only be shown.",
experimental: false,
new: false,
popular: false,
},
{
name: "Player Count",
tooltip: "**Rolocate will find 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.",
experimental: false,
new: false,
popular: false,
},
{
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.",
experimental: false,
new: false,
popular: false,
},
{
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.",
experimental: true,
experimentalExplanation: "**Experimental**: Still in development and testing. Sometimes user location cannot be detected.",
new: false,
popular: false,
},
{
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.",
experimental: true,
experimentalExplanation: "**Experimental**: Still in development and testing. it may be less accurate in regions with fewer Roblox servers",
new: false,
popular: false,
},
{
name: "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.",
experimental: false,
new: false,
popular: false,
},
{
name: "Newest Server",
tooltip: "**Tries to find Roblox servers that are less than 5 minute old.** This may take longer for very popular games or games with few players.",
experimental: false,
new: true,
popular: false,
},
];
// Create buttons with unique names, tooltips, experimental status, and explanations
buttonData.forEach((data, index) => {
const buttonContainer = document.createElement('div');
buttonContainer.className = 'server-filter-option';
buttonContainer.classList.add(data.disabled ? "disabled" : "enabled");
// Create a wrapper for the button content that can have opacity applied
const buttonContentWrapper = document.createElement('div');
buttonContentWrapper.style.cssText = `
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
${data.disabled ? 'opacity: 0.7;' : ''}
`;
buttonContainer.style.cssText = `
width: 190px;
height: 30px;
background-color: ${data.disabled ? '#2c2c2c' : '#393B3D'};
margin: 5px;
border-radius: 5px;
padding: 3.5px;
position: relative;
cursor: ${data.disabled ? 'not-allowed' : 'pointer'};
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
transform: translateY(-30px);
opacity: 0;
`;
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;
opacity: 1;
z-index: 1001;
`;
// 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;
// Add "DISABLED" style if the button is disabled
if (data.disabled) {
// Show explanation tooltip (left side like experimental)
const disabledTooltip = document.createElement('div');
disabledTooltip.className = 'disabled-tooltip';
disabledTooltip.style.cssText = `
display: none;
position: absolute;
top: 0;
right: 200px;
width: 200px;
background-color: #191B1D;
color: white;
padding: 5px;
border-radius: 5px;
font-size: 14px;
white-space: pre-wrap;
z-index: 1001;
opacity: 1;
`;
disabledTooltip.innerHTML = data.disabledExplanation.replace(/\*\*(.*?)\*\*/g, '$1 ');
buttonContainer.appendChild(disabledTooltip);
// Add disabled indicator
const disabledIndicator = document.createElement('span');
disabledIndicator.textContent = 'DISABLED';
disabledIndicator.style.cssText = `
margin-left: 8px;
color: #ff5555;
font-size: 10px;
font-weight: bold;
background-color: rgba(255, 85, 85, 0.1);
padding: 1px 4px;
border-radius: 3px;
`;
buttonText.appendChild(disabledIndicator);
// Show on hover
buttonContainer.addEventListener('mouseenter', () => {
disabledTooltip.style.display = 'block';
});
buttonContainer.addEventListener('mouseleave', () => {
disabledTooltip.style.display = 'none';
});
}
// Add "EXP" label if the button is experimental
if (data.experimental) {
const expLabel = document.createElement('span');
expLabel.textContent = 'EXP';
expLabel.style.cssText = `
margin-left: 8px;
color: gold;
font-size: 12px;
font-weight: bold;
background-color: rgba(255, 215, 0, 0.1);
padding: 2px 6px;
border-radius: 3px;
`;
buttonText.appendChild(expLabel);
}
// Add "POPULAR" label if the button is popular
if (data.popular) {
const popularLabel = document.createElement('span');
popularLabel.textContent = 'Popular';
popularLabel.style.cssText = `
margin-left: 8px;
color: #4CAF50;
font-size: 10px;
font-weight: bold;
background-color: rgba(76, 175, 80, 0.1);
padding: 2px 6px;
border-radius: 3px;
`;
buttonText.appendChild(popularLabel);
}
// add new tooltip
let newTooltip = null;
if (data.new) {
const newLabel = document.createElement('span');
newLabel.textContent = 'NEW';
newLabel.style.cssText = `
margin-left: 8px;
color: #2196F3;
font-size: 12px;
font-weight: bold;
background-color: rgba(33, 150, 243, 0.1);
padding: 2px 6px;
border-radius: 3px;
`;
buttonText.appendChild(newLabel);
// Add NEW explanation tooltip (left side)
const newTooltip = document.createElement('div');
newTooltip.className = 'new-tooltip';
newTooltip.style.cssText = `
display: none;
position: absolute;
top: 0;
right: 200px;
width: 200px;
background-color: #191B1D;
color: white;
padding: 5px;
border-radius: 5px;
font-size: 14px;
white-space: pre-wrap;
z-index: 1001;
opacity: 1;
`;
newTooltip.innerHTML = "New Feature : This feature was recently added. There is no guarantee it will work.";
buttonContainer.appendChild(newTooltip);
// Show on hover
buttonContainer.addEventListener('mouseenter', () => {
newTooltip.style.display = 'block';
});
buttonContainer.addEventListener('mouseleave', () => {
newTooltip.style.display = 'none';
});
}
// Add experimental explanation tooltip (left side)
let experimentalTooltip = null;
if (data.experimental) {
experimentalTooltip = document.createElement('div');
experimentalTooltip.className = 'experimental-tooltip';
experimentalTooltip.style.cssText = `
display: none;
position: absolute;
top: 0;
right: 200px;
width: 200px;
background-color: #191B1D;
color: white;
padding: 5px;
border-radius: 5px;
font-size: 14px;
white-space: pre-wrap;
z-index: 1001;
opacity: 1;
`;
// Function to replace **text** with bold and gold styled text
const formatText = (text) => {
return text.replace(/\*\*(.*?)\*\*/g, '$1 ');
};
// Apply the formatting to the experimental explanation
experimentalTooltip.innerHTML = formatText(data.experimentalExplanation);
buttonContainer.appendChild(experimentalTooltip);
}
// Append tooltip directly to button container so it won't inherit opacity
buttonContainer.appendChild(tooltip);
// Append button text to content wrapper
buttonContentWrapper.appendChild(buttonText);
// Append content wrapper to button container
buttonContainer.appendChild(buttonContentWrapper);
// In the event listeners:
buttonContainer.addEventListener('mouseover', () => {
tooltip.style.display = 'block';
if (data.experimental && experimentalTooltip) {
experimentalTooltip.style.display = 'block';
}
if (data.new && newTooltip) { // <-- Only show if it exists
newTooltip.style.display = 'block';
}
if (!data.disabled) {
buttonContainer.style.backgroundColor = '#4A4C4E';
buttonContainer.style.transform = 'translateY(0px) scale(1.02)';
}
});
buttonContainer.addEventListener('mouseout', () => {
tooltip.style.display = 'none';
if (data.experimental && experimentalTooltip) {
experimentalTooltip.style.display = 'none';
}
if (data.new && newTooltip) { // <-- Only hide if it exists
newTooltip.style.display = 'none';
}
if (!data.disabled) {
buttonContainer.style.backgroundColor = '#393B3D';
buttonContainer.style.transform = 'translateY(0px) scale(1)';
}
});
buttonContainer.addEventListener('click', () => {
// Prevent click functionality for disabled buttons
if (data.disabled) {
return;
}
// Add click animation
buttonContainer.style.transform = 'translateY(0px) scale(0.95)';
setTimeout(() => {
buttonContainer.style.transform = 'translateY(0px) scale(1)';
}, 150);
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:
scanRobloxServers();
break;
}
});
popup.appendChild(buttonContainer);
});
// trigger the button animations after DOM insertion
// this should be called after the popup is added to the DOM
setTimeout(() => {
// animate buttons in sequence from top to bottom
const buttons = popup.querySelectorAll('.server-filter-option');
buttons.forEach((button, index) => {
setTimeout(() => {
button.style.transform = 'translateY(0px)';
button.style.opacity = '1';
}, index * 30); // 30 ms from each button
});
}, 20);
return popup;
}
/*******************************************************
name of function: ServerHop
description: Handles server hopping by fetching and joining a random server, excluding recently joined servers.
*******************************************************/
function ServerHop() {
ConsoleLogEnabled("Starting server hop...");
showLoadingOverlay();
// Extract the game ID from the URL
const url = window.location.href;
const gameId = (url.split("/").indexOf("games") !== -1) ? url.split("/")[url.split("/").indexOf("games") + 1] : null;
ConsoleLogEnabled(`Game ID: ${gameId}`);
// Array to store server IDs
let serverIds = [];
let nextPageCursor = null;
let pagesRequested = 0;
// Get the list of all recently joined servers in localStorage
const allStoredServers = Object.keys(localStorage)
.filter(key => key.startsWith("ROLOCATE_recentServers_"))
.map(key => JSON.parse(localStorage.getItem(key)));
// Remove any expired servers for all games (older than 15 minutes)
const currentTime = new Date().getTime();
allStoredServers.forEach(storedServers => {
const validServers = storedServers.filter(server => {
const lastJoinedTime = new Date(server.timestamp).getTime();
return (currentTime - lastJoinedTime) <= 15 * 60 * 1000; // 15 minutes
});
// Update localStorage with the valid (non-expired) servers
localStorage.setItem(`ROLOCATE_recentServers_${gameId}`, JSON.stringify(validServers));
});
// Get the list of recently joined servers for the current game
const storedServers = JSON.parse(localStorage.getItem(`ROLOCATE_recentServers_${gameId}`)) || [];
// Check if there are any recently joined servers and exclude them from selection
const validServers = storedServers.filter(server => {
const lastJoinedTime = new Date(server.timestamp).getTime();
return (currentTime - lastJoinedTime) <= 15 * 60 * 1000; // 15 minutes
});
if (validServers.length > 0) {
ConsoleLogEnabled(`Excluding servers joined in the last 15 minutes: ${validServers.map(s => s.serverId).join(', ')}`);
} else {
ConsoleLogEnabled("No recently joined servers within the last 15 minutes. Proceeding to pick a new server.");
}
let currentDelay = 150; // Start with 0.15 seconds
let isRateLimited = false;
/*******************************************************
name of function: fetchServers
description: Function to fetch servers
*******************************************************/
function fetchServers(cursor) {
// Randomly choose between sortOrder=1 and sortOrder=2 (50% chance each)
const sortOrder = Math.random() < 0.5 ? 1 : 2;
const url = `https://games.roblox.com/v1/games/${gameId}/servers/0?sortOrder=${sortOrder}&excludeFullGames=true&limit=100${cursor ? `&cursor=${cursor}` : ""}`;
ConsoleLogEnabled(`Using sortOrder: ${sortOrder}`);
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: function(response) {
ConsoleLogEnabled("API Response:", response.responseText);
if (response.status === 429) {
ConsoleLogEnabled("Rate limited! Slowing down requests.");
isRateLimited = true;
currentDelay = 750; // Switch to 0.75 seconds
setTimeout(() => fetchServers(cursor), currentDelay);
return;
} else if (isRateLimited && response.status === 200) {
ConsoleLogEnabled("Recovered from rate limiting. Restoring normal delay.");
isRateLimited = false;
currentDelay = 150; // Back to normal
}
try {
const data = JSON.parse(response.responseText);
if (data.errors) {
ConsoleLogEnabled("Skipping unreadable response:", data.errors[0].message);
return;
}
setTimeout(() => {
if (!data || !data.data) {
ConsoleLogEnabled("Invalid response structure: 'data' is missing or undefined", data);
return;
}
data.data.forEach(server => {
if (validServers.some(vs => vs.serverId === server.id)) {
ConsoleLogEnabled(`Skipping previously joined server ${server.id}.`);
} else {
serverIds.push(server.id);
}
});
if (data.nextPageCursor && pagesRequested < 4) {
pagesRequested++;
ConsoleLogEnabled(`Fetching page ${pagesRequested}...`);
fetchServers(data.nextPageCursor);
} else {
pickRandomServer();
}
}, currentDelay);
} catch (error) {
ConsoleLogEnabled("Error parsing response:", error);
}
},
onerror: function(error) {
ConsoleLogEnabled("Error fetching server data:", error);
}
});
}
/*******************************************************
name of function: pickRandomServer
description: Function to pick a random server and join it
*******************************************************/
function pickRandomServer() {
if (serverIds.length > 0) {
const randomServerId = serverIds[Math.floor(Math.random() * serverIds.length)];
ConsoleLogEnabled(`Joining server: ${randomServerId}`);
// Join the game instance with the selected server ID
Roblox.GameLauncher.joinGameInstance(gameId, randomServerId);
// Store the selected server ID with the time and date in localStorage
const timestamp = new Date().toISOString();
const newServer = {
serverId: randomServerId,
timestamp
};
validServers.push(newServer);
// Save the updated list of recently joined servers to localStorage
localStorage.setItem(`ROLOCATE_recentServers_${gameId}`, JSON.stringify(validServers));
ConsoleLogEnabled(`Server ${randomServerId} stored with timestamp ${timestamp}`);
} else {
ConsoleLogEnabled("No servers found to join.");
notifications("You have joined all the servers recently. No servers found to join.", "error", "⚠️", "5000");
}
}
// Start the fetching process
fetchServers();
}
/*******************************************************
name of function: Bulk of functions for observer stuff
description: adds lots of stuff like autoserver regions and stuff
*******************************************************/
if (/^https:\/\/www\.roblox\.com(\/[a-z]{2})?\/games\//.test(window.location.href)) {
if (localStorage.ROLOCATE_AutoRunServerRegions === "true") {
(() => {
/*******************************************************
name of function: waitForElement
description: waits for a specific element to load onto
the page
*******************************************************/
function waitForElement(selector, timeout = 5000) {
return new Promise((resolve, reject) => {
const intervalTime = 100;
let elapsed = 0;
const interval = setInterval(() => {
const el = document.querySelector(selector);
if (el) {
clearInterval(interval);
resolve(el);
} else if (elapsed >= timeout) {
clearInterval(interval);
reject(new Error(`Element "${selector}" not found after ${timeout}ms`));
}
elapsed += intervalTime;
}, intervalTime);
});
}
/*******************************************************
name of function: waitForAnyElement
description: waits for any element on the page to load
*******************************************************/
function waitForAnyElement(selector, timeout = 5000) {
return new Promise((resolve, reject) => {
const intervalTime = 100;
let elapsed = 0;
const interval = setInterval(() => {
const elements = document.querySelectorAll(selector);
if (elements.length > 0) {
clearInterval(interval);
resolve(elements);
} else if (elapsed >= timeout) {
clearInterval(interval);
reject(new Error(`No elements matching "${selector}" found after ${timeout}ms`));
}
elapsed += intervalTime;
}, intervalTime);
});
}
/*******************************************************
name of function: waitForDivWithStyleSubstring
description: waits for server tab to show up, if this doesent
happen then it just spits out an error
*******************************************************/
function waitForDivWithStyleSubstring(substring, timeout = 5000) {
return new Promise((resolve, reject) => {
const intervalTime = 100;
let elapsed = 0;
const interval = setInterval(() => {
const divs = Array.from(document.querySelectorAll("div[style]"));
const found = divs.find(div => div.style && div.style.background && div.style.background.includes(substring));
if (found) {
clearInterval(interval);
resolve(found);
} else if (elapsed >= timeout) {
clearInterval(interval);
reject(new Error(`No div with style containing "${substring}" found after ${timeout}ms`));
}
elapsed += intervalTime;
}, intervalTime);
});
}
/*******************************************************
name of function: clickServersTab
description: clicks server tab on game page
*******************************************************/
async function clickServersTab() {
try {
const serversTab = await waitForElement("#tab-game-instances a");
serversTab.click();
ConsoleLogEnabled("[Auto] Servers tab clicked.");
return true;
} catch (err) {
ConsoleLogEnabled("[Auto] Servers tab not found:", err.message);
return false;
}
}
/*******************************************************
name of function: waitForServerListContainer
description: Waits for server list container to load onto the page
*******************************************************/
async function waitForServerListContainer() {
try {
const container = await waitForElement("#rbx-public-running-games");
ConsoleLogEnabled("[Auto] Server list container (#rbx-public-running-games) detected.");
return container;
} catch (err) {
ConsoleLogEnabled("[Auto] Server list container not found:", err.message);
return null;
}
}
/*******************************************************
name of function: waitForServerItems
description: Detects the server item for the functions to start
*******************************************************/
async function waitForServerItems() {
try {
const items = await waitForAnyElement(".rbx-public-game-server-item");
ConsoleLogEnabled(`[Auto] Detected ${items.length} server item(s) (.rbx-public-game-server-item)`);
return items;
} catch (err) {
ConsoleLogEnabled("[Auto] Server items not found:", err.message);
return null;
}
}
/*******************************************************
name of function: runServerRegions
description: Runs auto server regions
*******************************************************/
async function runServerRegions() {
const notifFlag = localStorage.ROLOCATE_enablenotifications;
if (notifFlag === "true") {
localStorage.ROLOCATE_enablenotifications = "false";
ConsoleLogEnabled("[Auto] Notifications disabled.");
} else {
ConsoleLogEnabled("[Auto] Notifications already disabled; leaving flag untouched.");
}
const gameId = /^https:\/\/www\.roblox\.com(\/[a-z]{2})?\/games\//.test(window.location.href) ? (window.location.href.match(/\/games\/(\d+)/) || [])[1] || null : null;
if (!gameId) {
ConsoleLogEnabled("[Auto] Game ID not found, aborting runServerRegions.");
if (notifFlag === "true") {
localStorage.ROLOCATE_enablenotifications = "true";
ConsoleLogEnabled("[Auto] Notifications enabled (early abort).");
}
return;
}
if (typeof Loadingbar === "function") Loadingbar(true);
if (typeof disableFilterButton === "function") disableFilterButton(true);
if (typeof disableLoadMoreButton === "function") disableLoadMoreButton();
if (typeof rebuildServerList === "function") {
rebuildServerList(gameId, 16);
ConsoleLogEnabled(`[Auto] Server list rebuilt for game ID: ${gameId}`);
} else {
ConsoleLogEnabled("[Auto] rebuildServerList function not found.");
}
if (notifFlag === "true") {
try {
await waitForDivWithStyleSubstring(
"radial-gradient(circle, rgba(255, 40, 40, 0.4)",
5000
);
localStorage.ROLOCATE_enablenotifications = "true";
ConsoleLogEnabled("[Auto] Notifications enabled (style div detected).");
} catch (err) {
ConsoleLogEnabled("[Auto] Style div not detected in time:", err.message);
}
}
}
window.addEventListener("load", async () => {
const clicked = await clickServersTab();
if (!clicked) return;
const container = await waitForServerListContainer();
if (!container) return;
const items = await waitForServerItems();
if (!items) return;
if (localStorage.ROLOCATE_fastservers === "false") {
if (typeof notifications === "function") {
notifications(
"Fastservers seem to be turned off. Turn it on to get almost instant server region results",
"info",
"📙",
"2000"
);
} else {
ConsoleLogEnabled("[Auto] notifications function not found.");
}
}
await runServerRegions();
});
})();
} else {
ConsoleLogEnabled("[Auto] ROLOCATE_AutoRunServerRegions is not true. Script skipped.");
}
/*******************************************************
name of function: An observer
description: Not a function, but an observer which ads the
filter button, server hop button, recent servers, and disables
trailer autoplay if settings are true
*******************************************************/
const observer = new MutationObserver((mutations, obs) => {
const serverListOptions = document.querySelector('.server-list-options');
const playButton = document.querySelector('.btn-common-play-game-lg.btn-primary-md');
// debug
//ConsoleLogEnabled("Checking Filter Button Insertion:");
//ConsoleLogEnabled("serverListOptions:", serverListOptions);
//ConsoleLogEnabled("RL-filter-button exists:", !!document.querySelector('.RL-filter-button'));
//ConsoleLogEnabled("Filter button enabled?", localStorage.getItem("ROLOCATE_togglefilterserversbutton"));
if (serverListOptions && !document.querySelector('.RL-filter-button') && localStorage.getItem("ROLOCATE_togglefilterserversbutton") === "true") {
ConsoleLogEnabled("Added Filter Button");
const filterButton = document.createElement('a'); // yes lmao
filterButton.className = 'RL-filter-button';
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';
});
const buttonText = document.createElement('span');
buttonText.className = 'RL-filter-text';
buttonText.textContent = 'Filters';
filterButton.appendChild(buttonText);
const icon = document.createElement('span');
icon.className = 'RL-filter-icon';
icon.textContent = '≡';
icon.style.cssText = `font-size: 18px;`;
filterButton.appendChild(icon);
serverListOptions.appendChild(filterButton);
let popup = null;
filterButton.addEventListener('click', (event) => {
event.stopPropagation();
if (popup) {
popup.remove();
popup = null;
} else {
popup = createPopup();
popup.style.top = `${filterButton.offsetHeight}px`;
popup.style.left = '0';
filterButton.appendChild(popup);
}
});
document.addEventListener('click', (event) => {
if (popup && !filterButton.contains(event.target)) {
popup.remove();
popup = null;
}
});
}
// new condition to trigger recent server logic
if (localStorage.getItem("ROLOCATE_togglerecentserverbutton") === "true") {
HandleRecentServers();
HandleRecentServersURL();
}
// new condition to trigger recent server logic
if (localStorage.getItem("ROLOCATE_disabletrailer") === "true") {
disableYouTubeAutoplayInIframes();
}
if (playButton && !document.querySelector('.custom-play-button') && localStorage.getItem("ROLOCATE_toggleserverhopbutton") === "true") {
ConsoleLogEnabled("Added Server Hop Button");
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
gap: 10px;
align-items: center;
width: 100%;
`;
playButton.style.cssText += `
flex: 3;
padding: 10px 12px;
text-align: center;
`;
const serverHopButton = document.createElement('button');
serverHopButton.className = 'custom-play-button';
serverHopButton.style.cssText = `
background-color: #335fff;
color: white;
border: none;
padding: 7.5px 12px;
cursor: pointer;
font-weight: bold;
border-radius: 8px;
flex: 1;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
position: relative;
`;
const tooltip = document.createElement('div');
tooltip.textContent = 'Join Random Server / Server Hop';
tooltip.style.cssText = `
position: absolute;
background-color: rgba(51, 95, 255, 0.8);
color: white;
padding: 5px 10px;
border-radius: 5px;
font-size: 12px;
visibility: hidden;
opacity: 0;
transition: opacity 0.2s ease-in-out;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
`;
serverHopButton.appendChild(tooltip);
serverHopButton.addEventListener('mouseover', () => {
tooltip.style.visibility = 'visible';
tooltip.style.opacity = '1';
});
serverHopButton.addEventListener('mouseout', () => {
tooltip.style.visibility = 'hidden';
tooltip.style.opacity = '0';
});
const logo = document.createElement('img');
logo.src = window.Base64Images.icon_serverhop;
logo.style.cssText = `
width: 45px;
height: 45px;
`;
serverHopButton.appendChild(logo);
playButton.parentNode.insertBefore(buttonContainer, playButton);
buttonContainer.appendChild(playButton);
buttonContainer.appendChild(serverHopButton);
serverHopButton.addEventListener('click', () => {
ServerHop();
});
}
const filterEnabled = localStorage.getItem("ROLOCATE_togglefilterserversbutton") === "true";
const hopEnabled = localStorage.getItem("ROLOCATE_toggleserverhopbutton") === "true";
const recentEnabled = localStorage.getItem("ROLOCATE_togglerecentserverbutton") === "true";
const filterPresent = !filterEnabled || document.querySelector('.RL-filter-button');
const hopPresent = !hopEnabled || document.querySelector('.custom-play-button');
const recentPresent = !recentEnabled || document.querySelector('.recent-servers-section');
if (filterPresent && hopPresent && recentPresent) {
obs.disconnect();
ConsoleLogEnabled("Disconnected Observer");
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
/*********************************************************************************************************************************************************************************************************************************************
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
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();
notifications("Finding small servers...", "success", "🧐");
// Get the game ID from the URL
const gameId = ((p => {
const i = p.indexOf('games');
return i !== -1 && p.length > i + 1 ? p[i + 1] : null;
})(window.location.pathname.split('/')));
// Retry mechanism
let retries = 3;
let success = false;
while (retries > 0 && !success) {
try {
// Use GM_xmlhttpRequest to fetch server data from the Roblox API
const response = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://games.roblox.com/v1/games/${gameId}/servers/0?sortOrder=1&excludeFullGames=true&limit=100`,
onload: function(response) {
if (response.status === 429) {
reject(new Error('429: Too Many Requests'));
} else if (response.status >= 200 && response.status < 300) {
resolve(response);
} else {
reject(new Error(`HTTP error! status: ${response.status}`));
}
},
onerror: function(error) {
reject(error);
}
});
});
const data = JSON.parse(response.responseText);
// 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) {
ConsoleLogEnabled('Encountered a 429 error. Retrying in 5 seconds...');
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait for 5 seconds
} else {
ConsoleLogEnabled('Error fetching server data:', error);
notifications('Error: Failed to fetch server data. Please try again later.', 'error', '⚠️', '5000');
Loadingbar(false);
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);
notifications("Finding servers with space...", "success", "🧐");
// Get the game ID from the URL
const gameId = ((p => {
const i = p.indexOf('games');
return i !== -1 && p.length > i + 1 ? p[i + 1] : null;
})(window.location.pathname.split('/')));
// Retry mechanism
let retries = 3;
let success = false;
while (retries > 0 && !success) {
try {
// Use GM_xmlhttpRequest to fetch server data from the Roblox API
const response = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://games.roblox.com/v1/games/${gameId}/servers/0?sortOrder=2&excludeFullGames=true&limit=100`,
onload: function(response) {
if (response.status === 429) {
reject(new Error('429: Too Many Requests'));
} else if (response.status >= 200 && response.status < 300) {
resolve(response);
} else {
reject(new Error(`HTTP error! status: ${response.status}`));
}
},
onerror: function(error) {
reject(error);
}
});
});
const data = JSON.parse(response.responseText);
// 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) {
ConsoleLogEnabled('Encountered a 429 error. Retrying in 10 seconds...');
await new Promise(resolve => setTimeout(resolve, 10000)); // Wait for 10 seconds
} else {
ConsoleLogEnabled('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. Maybe one of my best functions lowkey.
*******************************************************/
function player_count_tab() {
// Check if the max player count has already been determined
if (!player_count_tab.maxPlayers) {
// Try to find the element containing the player count information
const playerCountElement = document.querySelector('.text-info.rbx-game-status.rbx-game-server-status.text-overflow');
if (playerCountElement) {
const playerCountText = playerCountElement.textContent.trim();
const match = playerCountText.match(/(\d+) of (\d+) people max/);
if (match) {
const maxPlayers = parseInt(match[2], 10);
if (!isNaN(maxPlayers) && maxPlayers > 1) {
player_count_tab.maxPlayers = maxPlayers;
ConsoleLogEnabled("Found text element with max playercount");
}
}
} else {
// If the element is not found, extract the gameId from the URL
const gameIdMatch = window.location.href.match(/\/(?:[a-z]{2}\/)?games\/(\d+)/);
if (gameIdMatch && gameIdMatch[1]) {
const gameId = gameIdMatch[1];
// Send a request to the Roblox API to get server information
GM_xmlhttpRequest({
method: 'GET',
url: `https://games.roblox.com/v1/games/${gameId}/servers/public?sortOrder=1&excludeFullGames=true&limit=100`,
onload: function(response) {
try {
if (response.status === 429) {
// Rate limit error, default to 100
ConsoleLogEnabled("Rate limited defaulting to 100.");
player_count_tab.maxPlayers = 100;
} else {
ConsoleLogEnabled("Valid api response");
const data = JSON.parse(response.responseText);
if (data.data && data.data.length > 0) {
const maxPlayers = data.data[0].maxPlayers;
if (!isNaN(maxPlayers) && maxPlayers > 1) {
player_count_tab.maxPlayers = maxPlayers;
}
}
}
// Update the slider range if the popup is already created
const slider = document.querySelector('.player-count-popup input[type="range"]');
if (slider) {
slider.max = player_count_tab.maxPlayers ? (player_count_tab.maxPlayers - 1).toString() : '100';
slider.style.background = `
linear-gradient(
to right,
#00A2FF 0%,
#00A2FF ${slider.value}%,
#444 ${slider.value}%,
#444 100%
);
`;
}
} catch (error) {
ConsoleLogEnabled('Failed to parse API response:', error);
// Default to 100 if parsing fails
player_count_tab.maxPlayers = 100;
const slider = document.querySelector('.player-count-popup input[type="range"]');
if (slider) {
slider.max = '100';
slider.style.background = `
linear-gradient(
to right,
#00A2FF 0%,
#00A2FF ${slider.value}%,
#444 ${slider.value}%,
#444 100%
);
`;
}
}
},
onerror: function(error) {
ConsoleLogEnabled('Failed to fetch server information:', error);
ConsoleLogEnabled('Fallback to 100 players.');
// Default to 100 if the request fails
player_count_tab.maxPlayers = 100;
const slider = document.querySelector('.player-count-popup input[type="range"]');
if (slider) {
slider.max = '100';
slider.style.background = `
linear-gradient(
to right,
#00A2FF 0%,
#00A2FF ${slider.value}%,
#444 ${slider.value}%,
#444 100%
);
`;
}
}
});
}
}
}
// Create the overlay (backdrop)
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 9999;
opacity: 0;
transition: opacity 0.3s ease;
`;
document.body.appendChild(overlay);
// 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: 10px;
z-index: 10000;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
width: 300px;
opacity: 0;
transition: opacity 0.3s ease, transform 0.3s ease;
`;
// Add a close button in the top-right corner (bigger size)
const closeButton = document.createElement('button');
closeButton.innerHTML = '×'; // Using '×' for the close icon
closeButton.style.cssText = `
position: absolute;
top: 10px;
right: 10px;
background: transparent;
border: none;
color: #ffffff;
font-size: 24px; /* Increased font size */
cursor: pointer;
width: 36px; /* Increased size */
height: 36px; /* Increased size */
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s ease, color 0.3s ease;
`;
closeButton.addEventListener('mouseenter', () => {
closeButton.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
closeButton.style.color = '#ff4444';
});
closeButton.addEventListener('mouseleave', () => {
closeButton.style.backgroundColor = 'transparent';
closeButton.style.color = '#ffffff';
});
// Add a title
const title = document.createElement('h3');
title.textContent = 'Select Max Player Count';
title.style.cssText = `
color: white;
margin: 0;
font-size: 18px;
font-weight: 500;
`;
popup.appendChild(title);
// Add a slider with improved functionality and styling
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '1';
slider.max = player_count_tab.maxPlayers ? (player_count_tab.maxPlayers - 1).toString() : '100';
slider.value = '1'; // Default value
slider.step = '1'; // Step for better accuracy
slider.style.cssText = `
width: 80%;
cursor: pointer;
margin: 10px 0;
-webkit-appearance: none; /* Remove default styling */
background: transparent;
`;
// Custom slider track
slider.style.background = `
linear-gradient(
to right,
#00A2FF 0%,
#00A2FF ${slider.value}%,
#444 ${slider.value}%,
#444 100%
);
border-radius: 5px;
height: 6px;
`;
// Custom slider thumb
slider.style.setProperty('--thumb-size', '20px'); /* Larger thumb */
slider.style.setProperty('--thumb-color', '#00A2FF');
slider.style.setProperty('--thumb-hover-color', '#0088cc');
slider.style.setProperty('--thumb-border', '2px solid #fff');
slider.style.setProperty('--thumb-shadow', '0 0 5px rgba(0, 0, 0, 0.5)');
slider.addEventListener('input', () => {
slider.style.background = `
linear-gradient(
to right,
#00A2FF 0%,
#00A2FF ${slider.value}%,
#444 ${slider.value}%,
#444 100%
);
`;
sliderValue.textContent = slider.value; // Update the displayed value
});
// Keyboard support for better accuracy (fixed to increment/decrement by 1)
slider.addEventListener('keydown', (e) => {
e.preventDefault(); // Prevent default behavior (which might cause jumps)
let newValue = parseInt(slider.value, 10);
if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
newValue = Math.max(1, newValue - 1); // Decrease by 1
} else if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
newValue = Math.min(100, newValue + 1); // Increase by 1
}
slider.value = newValue;
slider.dispatchEvent(new Event('input')); // Trigger input event to update UI
});
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;
font-weight: bold;
`;
popup.appendChild(sliderValue);
// Add a submit button with dark, blackish style
const submitButton = document.createElement('button');
submitButton.textContent = 'Search';
submitButton.style.cssText = `
padding: 8px 20px;
font-size: 16px;
background-color: #1a1a1a; /* Dark blackish color */
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
`;
submitButton.addEventListener('mouseenter', () => {
submitButton.style.backgroundColor = '#333'; /* Slightly lighter on hover */
submitButton.style.transform = 'scale(1.05)';
});
submitButton.addEventListener('mouseleave', () => {
submitButton.style.backgroundColor = '#1a1a1a';
submitButton.style.transform = 'scale(1)';
});
// Add a yellow box with a tip under the submit button
const tipBox = document.createElement('div');
tipBox.style.cssText = `
width: 100%;
padding: 10px;
background-color: rgba(255, 204, 0, 0.15);
border-radius: 5px;
text-align: center;
font-size: 14px;
color: #ffcc00;
transition: background-color 0.3s ease;
`;
tipBox.textContent = 'Tip: Click the slider and use the arrow keys for more accuracy.';
tipBox.addEventListener('mouseenter', () => {
tipBox.style.backgroundColor = 'rgba(255, 204, 0, 0.25)';
});
tipBox.addEventListener('mouseleave', () => {
tipBox.style.backgroundColor = 'rgba(255, 204, 0, 0.15)';
});
popup.appendChild(tipBox);
// Append the popup to the body
document.body.appendChild(popup);
// Fade in the overlay and popup
setTimeout(() => {
overlay.style.opacity = '1';
popup.style.opacity = '1';
popup.style.transform = 'translate(-50%, -50%) scale(1)';
}, 10);
/*******************************************************
name of function: fadeOutAndRemove
description: Fades out and removes the popup and overlay.
*******************************************************/
function fadeOutAndRemove(popup, overlay) {
popup.style.opacity = '0';
popup.style.transform = 'translate(-50%, -50%) scale(0.9)';
overlay.style.opacity = '0';
setTimeout(() => {
popup.remove();
overlay.remove();
}, 300); // Match the duration of the transition
}
// Close the popup when clicking outside
overlay.addEventListener('click', () => {
fadeOutAndRemove(popup, overlay);
});
// Close the popup when the close button is clicked
closeButton.addEventListener('click', () => {
fadeOutAndRemove(popup, overlay);
});
// Handle submit button click
submitButton.addEventListener('click', () => {
const maxPlayers = parseInt(slider.value, 10);
if (!isNaN(maxPlayers) && maxPlayers > 0) {
filterServersByPlayerCount(maxPlayers);
fadeOutAndRemove(popup, overlay);
} else {
notifications('Error: Please enter a number greater than 0', 'error', '⚠️', '5000');
}
});
popup.appendChild(submitButton);
popup.appendChild(closeButton);
}
/*******************************************************
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
ConsoleLogEnabled(`[DEBUG] Rate limited. Waiting ${newDelay / 1000} seconds before retrying...`);
setTimeout(() => {
resolve(fetchServersWithRetry(url, retries - 1, newDelay)); // Retry with increased delay
}, newDelay);
} else {
ConsoleLogEnabled('[DEBUG] Rate limit retries exhausted.');
notifications('Error: Rate limited please try again later.', 'error', '⚠️', '5000')
reject(new Error('RateLimit'));
}
return;
}
// Handle other HTTP errors
if (response.status < 200 || response.status >= 300) {
ConsoleLogEnabled('[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);
ConsoleLogEnabled('[DEBUG] Fetched data successfully:', data);
resolve(data);
} catch (error) {
ConsoleLogEnabled('[DEBUG] Error parsing JSON:', error);
reject(error);
}
},
onerror: function(error) {
ConsoleLogEnabled('[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)) {
ConsoleLogEnabled('[DEBUG] Invalid input for maxPlayers.');
notifications('Error: Please input a valid whole number greater than or equal to 1.', 'error', '⚠️', '5000');
return;
}
// Disable UI elements and clear the server list
Loadingbar(true);
disableLoadMoreButton();
disableFilterButton(true);
const serverList = document.querySelector('#rbx-public-game-server-item-container');
serverList.innerHTML = '';
const gameId = ((p = window.location.pathname.split('/')) => {
const i = p.indexOf('games');
return i !== -1 && p.length > i + 1 ? p[i + 1] : null;
})();
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', '🔎', '5000');
try {
while (serversFound < 16) {
// Check if the time limit has been exceeded
if (Date.now() - startTime > timeLimit) {
ConsoleLogEnabled('[DEBUG] Time limit reached. Proceeding to fallback servers.');
notifications('Warning: Time limit reached. Proceeding to fallback servers.', 'warning', '❗', '5000');
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 {
notifications("No servers found in initial fetch.", "error", "⚠️", "5000")
ConsoleLogEnabled('[DEBUG] No servers found in initial fetch.', 'warning', '❗');
break;
}
}
// Validate maxPlayers against serverMaxPlayers
if (maxPlayers >= serverMaxPlayers) {
ConsoleLogEnabled('[DEBUG] Invalid input: maxPlayers is greater than or equal to serverMaxPlayers.');
notifications(`Error: Please input a number between 1 through ${serverMaxPlayers - 1}`, 'error', '⚠️', '5000');
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)) {
ConsoleLogEnabled('[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
}
ConsoleLogEnabled(`[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)) {
notifications(`There are no servers with ${maxPlayers} players. Showing servers closest to ${maxPlayers} players.`, 'warning', '😔', '8000');
// 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', '🔎', '5000');
}
} catch (error) {
ConsoleLogEnabled('[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() {
notifications('Finding Random Servers. Please wait 2-5 seconds', 'success', '🔎', '5000');
// Disable the "Load More" button and show the loading bar
Loadingbar(true);
disableFilterButton(true);
disableLoadMoreButton();
// Get the game ID from the URL ik reduent function
const gameId = ((p = window.location.pathname.split('/')) => {
const i = p.indexOf('games');
return i !== -1 && p.length > i + 1 ? p[i + 1] : null;
})();
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, 10); // Retry up to 3 times
// Wait for 5 seconds
await delay(1500);
// 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, 10); // Retry up to 3 times
// Combine the servers from both URLs. Yea im kinda proud of this lmao
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) {
ConsoleLogEnabled('Error fetching server data:', error);
notifications('Error: Failed to fetch server data. Please try again later.', 'error', '⚠️', '5000');
} 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 using GM_xmlhttpRequest.
*******************************************************/
function fetchWithRetry(url, retries) {
return new Promise((resolve, reject) => {
const attemptFetch = (attempt = 0) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: function(response) {
if (response.status === 429) {
if (attempt < retries) {
ConsoleLogEnabled(`Rate limited. Retrying in 2.5 seconds... (Attempt ${attempt + 1}/${retries})`);
setTimeout(() => attemptFetch(attempt + 1), 1500); // Wait 1.5 seconds and retry
} else {
reject(new Error('Rate limit exceeded after retries'));
}
} else if (response.status >= 200 && response.status < 300) {
try {
const data = JSON.parse(response.responseText);
resolve(data);
} catch (error) {
reject(new Error('Failed to parse JSON response'));
}
} else {
reject(new Error(`HTTP error: ${response.status}`));
}
},
onerror: function(error) {
if (attempt < retries) {
ConsoleLogEnabled(`Error occurred. Retrying in 10 seconds... (Attempt ${attempt + 1}/${retries})`);
setTimeout(() => attemptFetch(attempt + 1), 10000); // Wait 10 seconds and retry
} else {
reject(error);
}
}
});
};
attemptFetch();
});
}
/*******************************************************
name of function: shuffleArray
description: Shuffles an array using the Fisher-Yates algorithm. This ronald fisher guy was kinda smart
*******************************************************/
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
*********************************************************************************************************************************************************************************************************************************************/
/*******************************************************
name of function: Isongamespage
description: not a function but if on game page inject styles
*******************************************************/
if (Isongamespage) {
// Create a