// ==UserScript==
// @name RoLocate
// @namespace https://oqarshi.github.io/
// @version 41.3
// @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 Custom - Personal Use Only
// @icon https://oqarshi.github.io/Invite/rolocate/assets/logo.svg
// @supportURL https://greasyfork.org/en/scripts/523727-rolocate/feedback
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_listValues
// @grant GM_setValue
// @grant GM_deleteValue
// @require https://update.greasyfork.org/scripts/535590/Rolocate%20Base64%20Image%20Library%2020.js
// @require https://update.greasyfork.org/scripts/539427/Rolocate%20Server%20Region%20Data.js
// @require https://update.greasyfork.org/scripts/540553/Rolocate%20Flag%20Base64%20Data.js
// @connect thumbnails.roblox.com
// @connect games.roblox.com
// @connect gamejoin.roblox.com
// @connect presence.roblox.com
// @connect www.roblox.com
// @connect friends.roblox.com
// @connect apis.roblox.com
// @connect groups.roblox.com
// ==/UserScript==
/*
* RoLocate userscript by Oqarshi
* License: Custom - Personal Use Only
*
* Copyright (c) 2025 Oqarshi
*
* This license grants limited rights to end users and does not imply any transfer of copyright ownership.
* By using this script, you agree to these license terms.
*
* You are permitted to use and modify this script **for personal, non-commercial use only**.
*
* You are **NOT permitted** to:
* - Redistribute or reupload this script, in original or modified form
* - Publish it on any website (e.g., GreasyFork, GitHub, UserScripts.org)
* - Include it in any commercial, monetized, or donation-based tools
* - Remove or alter this license or attribution
*
* Attribution to the original author (Oqarshi) must always be preserved.
*
* Violations may result in takedown notices under the DMCA or applicable copyright law.
*/
/*jshint esversion: 6 */
/*jshint esversion: 11 */
(function() {
'use strict';
// ===============================
// TODO LIST
// ===============================
/*
* NEXT UP:
* - Fix Localstorage bugs not saving
* - ui change stuff idk
* - preferred region
* - make smartsearch find items and other stuff
*/
/*
* NICE TO HAVE / IDEAS / NOT IMPORTANT:
* - Improve Server Amount pick UI
* - Have a global function for GameID
* - Move functions out of blocks
* - Custom theme builder
*/
/*******************************************************
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: notifications function
*******************************************************/
function notifications(message, type = 'info', emoji = '', duration = 3000) {
if (localStorage.getItem('ROLOCATE_enablenotifications') !== 'true') return;
// Inject CSS once
if (!document.getElementById('toast-styles')) {
const style = document.createElement('style');
style.id = '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: #2d2d2d; color: #e8e8e8; padding: 12px 16px; border-radius: 8px;
font: 500 14px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
min-width: 280px; max-width: 400px; 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; will-change: transform;
}
.toast.removing { animation: slideOut 0.3s ease-in forwards; }
.toast:hover { background: #373737; }
.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: opacity 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}`;
// Icon map
const icons = {
success: '<svg width="16" height="16" fill="none" stroke="#4CAF50" stroke-width="2" viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg>',
error: '<svg width="16" height="16" fill="none" stroke="#F44336" stroke-width="2" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></svg>',
warning: '<svg width="16" height="16" fill="none" stroke="#FF9800" stroke-width="2" viewBox="0 0 24 24"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0zM12 9v4M12 17h.01"/></svg>',
info: '<svg width="16" height="16" fill="none" stroke="#2196F3" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>'
};
// Build content
toast.innerHTML = `
<div class="toast-content">
<div class="toast-icon">${icons[type] || icons.info}</div>
${emoji ? `<span class="toast-emoji">${emoji}</span>` : ''}
<span class="toast-message">${message.replace(/\n/g, '<br>')}</span>
</div>
<div class="toast-close"></div>
<div class="progress-bar" style="animation-duration: ${duration}ms;"></div>
`;
container.appendChild(toast);
// Auto remove functionality
let timeout = setTimeout(removeToast, duration);
const progressBar = toast.querySelector('.progress-bar');
// Hover pause/resume
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);
});
// Close button
toast.querySelector('.toast-close').addEventListener('click', removeToast);
function removeToast() {
clearTimeout(timeout);
toast.classList.add('removing');
setTimeout(() => toast.remove(), 300);
}
// Return control object
return {
remove: removeToast,
update: (newMessage) => {
toast.querySelector('.toast-message').innerHTML = newMessage.replace(/\n/g, '<br>');
},
setType: (newType) => {
toast.className = `toast ${newType}`;
toast.querySelector('.toast-icon').innerHTML = icons[newType] || icons.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;
}
};
}
function Update_Popup() {
const VERSION = "V41.3",
PREV_VERSION = "V41.2";
const currentVersion = localStorage.getItem('version') || "V0.0";
if (currentVersion === VERSION) return;
localStorage.setItem('version', VERSION);
if (localStorage.getItem(PREV_VERSION)) {
localStorage.removeItem(PREV_VERSION);
}
const style = document.createElement('style');
style.innerHTML = `
.rup-popup {
display: flex; position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5);
justify-content: center; align-items: center; z-index: 1000; opacity: 0;
animation: rup-fadeIn 0.5s cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
}
.rup-content {
background: #2a2a2a; border-radius: 20px; padding: 0; width: 900px; max-width: 95%;
max-height: 85vh; overflow: hidden; box-shadow: 0 25px 50px rgba(0, 0, 0, 0.4);
border: 1px solid #404040; color: #e8e8e8; transform: scale(0.95);
animation: rup-scaleUp 0.6s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards;
position: relative; display: flex; flex-direction: column; will-change: transform;
}
.rup-header {
padding: 24px 32px; border-bottom: 1px solid #404040; display: flex;
align-items: center; gap: 16px; background: #1f1f1f; position: relative;
}
.rup-logo { width: 56px; height: 56px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); flex-shrink: 0; }
.rup-header-content { flex: 1; }
.rup-title {
font-size: 24px; font-weight: 600; color: #ffffff; margin: 0 0 4px;
letter-spacing: -0.5px;
}
.rup-version {
display: inline-block; background: #1a1a1a; color: #ffffff; padding: 6px 12px;
border-radius: 6px; font-size: 13px; font-weight: 500; border: 1px solid #404040;
}
.rup-main { display: flex; flex: 1; min-height: 0; }
.rup-left {
flex: 1; padding: 24px; border-right: 1px solid #404040;
overflow-y: auto; background: #252525;
}
.rup-right {
flex: 1; padding: 24px; overflow-y: auto; background: #2a2a2a;
display: flex; flex-direction: column;
}
.rup-close {
position: absolute; top: 16px; right: 16px; width: 32px; height: 32px;
display: flex; align-items: center; justify-content: center; cursor: pointer;
color: #888888; font-size: 18px; font-weight: 300; border-radius: 8px;
transition: all 0.4s cubic-bezier(0.22, 0.61, 0.36, 1);
background: rgba(255, 255, 255, 0.05); border: 1px solid transparent; z-index: 10;
}
.rup-close:hover {
color: #ffffff; background: rgba(255, 255, 255, 0.1);
border-color: #555555; transform: rotate(90deg);
}
.rup-features-title {
font-size: 18px; font-weight: 600; color: #ffffff; margin-bottom: 16px;
display: flex; align-items: center; gap: 8px;
}
.rup-feature-item {
margin-bottom: 12px; border-radius: 10px; overflow: hidden; border: 1px solid #404040;
transition: all 0.4s cubic-bezier(0.22, 0.61, 0.36, 1); cursor: pointer;
}
.rup-feature-item:hover {
border-color: #555555; background: #303030; transform: translateY(-2px);
}
.rup-feature-item.rup-active { border-color: #666666; background: #303030; }
.rup-feature-header {
display: flex; align-items: center; padding: 16px; background: #1f1f1f;
transition: all 0.4s cubic-bezier(0.22, 0.61, 0.36, 1); user-select: none;
}
.rup-feature-item:hover .rup-feature-header { background: #2a2a2a; }
.rup-feature-item.rup-active .rup-feature-header { background: #333333; }
.rup-feature-icon {
font-size: 20px; margin-right: 12px; min-width: 24px;
transition: transform 0.3s ease;
}
.rup-feature-item:hover .rup-feature-icon { transform: scale(1.1); }
.rup-feature-title { flex: 1; font-size: 15px; font-weight: 500; color: #ffffff; margin: 0; }
.rup-feature-badge {
background: #404040; color: #cccccc; padding: 4px 8px; border-radius: 4px;
font-size: 11px; font-weight: 500; text-transform: uppercase;
letter-spacing: 0.5px; transition: all 0.3s ease;
}
.rup-feature-item:hover .rup-feature-badge { transform: translateX(3px); }
.rup-detail-panel {
background: #1f1f1f; border-radius: 12px; padding: 24px; margin-bottom: 20px;
border: 1px solid #404040; flex: 1; display: flex; flex-direction: column;
opacity: 0; transform: translateY(15px);
animation: rup-fadeInUp 0.6s cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
will-change: transform, opacity;
}
.rup-detail-title {
font-size: 20px; font-weight: 600; color: #ffffff; margin: 0 0 8px;
display: flex; align-items: center; gap: 10px;
}
.rup-detail-subtitle {
font-size: 13px; color: #999999; margin-bottom: 16px;
text-transform: uppercase; letter-spacing: 0.5px;
}
.rup-detail-description {
font-size: 14px; color: #cccccc; line-height: 1.6; margin-bottom: 16px; flex: 1;
}
.rup-detail-settings {
padding: 16px; background: #252525; border-radius: 8px; border: 1px solid #404040; margin-top: auto;
}
.rup-setting-row {
display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;
}
.rup-setting-row:last-child { margin-bottom: 0; }
.rup-setting-label { font-size: 13px; color: #cccccc; font-weight: 500; }
.rup-setting-value {
font-size: 12px; color: #999999; padding: 4px 8px; background: #1a1a1a;
border-radius: 4px; border: 1px solid #404040;
}
.rup-welcome-panel {
text-align: center; padding: 40px 20px; color: #999999; display: flex;
flex-direction: column; align-items: center; justify-content: center; height: 100%;
}
.rup-welcome-icon {
font-size: 48px; margin-bottom: 16px; opacity: 0.5;
animation: rup-float 4s ease-in-out infinite;
}
.rup-welcome-text { font-size: 16px; margin-bottom: 8px; }
.rup-welcome-subtext { font-size: 13px; color: #666666; }
.rup-developer-message {
background: #1a1a1a; border-radius: 8px; padding: 16px; margin-bottom: 20px;
border-left: 3px solid #555555; transition: all 0.4s ease;
}
.rup-developer-message:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); }
.rup-developer-message-title {
font-weight: 600; color: #ffffff; margin-bottom: 8px; font-size: 14px;
}
.rup-developer-message-text { font-size: 13px; color: #cccccc; line-height: 1.5; }
.rup-help-section {
background: #1f1f1f; border-radius: 8px; padding: 16px; border: 1px solid #404040;
}
.rup-help-title { font-size: 14px; font-weight: 600; color: #ffffff; margin-bottom: 12px; }
.rup-help-link {
color: #70a5ff; text-decoration: none; font-size: 13px; display: flex;
align-items: center; gap: 8px; padding: 10px 12px; border-radius: 6px;
transition: all 0.4s cubic-bezier(0.22, 0.61, 0.36, 1);
background: rgba(112, 165, 255, 0.1); border: 1px solid rgba(112, 165, 255, 0.2);
}
.rup-help-link:hover {
color: #ffffff; background: rgba(112, 165, 255, 0.2);
border-color: rgba(112, 165, 255, 0.4); transform: translateY(-2px);
}
.rup-help-link-icon { font-size: 16px; transition: transform 0.3s ease; }
.rup-help-link:hover .rup-help-link-icon { transform: translateY(-2px); }
.rup-footer {
padding: 16px 32px; border-top: 1px solid #404040; background: #1f1f1f; text-align: center;
}
.rup-note { font-size: 12px; color: #999999; margin: 0; }
/* Scrollbars */
.rup-left::-webkit-scrollbar, .rup-right::-webkit-scrollbar { width: 6px; }
.rup-left::-webkit-scrollbar-track, .rup-right::-webkit-scrollbar-track { background: #1a1a1a; }
.rup-left::-webkit-scrollbar-thumb, .rup-right::-webkit-scrollbar-thumb {
background: #555555; border-radius: 3px; transition: background 0.3s ease;
}
.rup-left::-webkit-scrollbar-thumb:hover, .rup-right::-webkit-scrollbar-thumb:hover {
background: #666666;
}
/* Animations */
@keyframes rup-fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes rup-fadeOut { from { opacity: 1; } to { opacity: 0; } }
@keyframes rup-scaleUp { 0% { transform: scale(0.95) translateY(10px); } 100% { transform: scale(1) translateY(0); } }
@keyframes rup-scaleDown { from { transform: scale(1); } to { transform: scale(0.9); opacity: 0; } }
@keyframes rup-fadeInUp { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
@keyframes rup-float { 0% { transform: translateY(0px); } 50% { transform: translateY(-5px); } 100% { transform: translateY(0px); } }
/* Responsive */
@media (max-width: 768px) {
.rup-content { width: 95%; flex-direction: column; }
.rup-main { flex-direction: column; }
.rup-left, .rup-right { flex: none; }
.rup-left { border-right: none; border-bottom: 1px solid #404040; }
}
`;
document.head.appendChild(style);
const featureData = {
recentservers: {
title: "Recent Servers",
icon: "🕘",
subtitle: "See Your Recent Servers",
description: "Fixed Recent Servers not showing up after Roblox changed Friends to Connections. Also fixed a bug where classic terms would interfere with recent servers. Note: All recent server data should still have been saved.",
settings: [{
label: "Enabled by default",
value: "True"
},
{
label: "Toggle Location",
value: "General Tab"
},
{
label: "Scope",
value: "Roblox.com/games/*"
}
]
},
restoreclassicterms: {
title: "Restore Classic Terms",
icon: "🕘",
subtitle: "Remove Corporate Buzzwords",
description: "Reverts corporate buzzwords Roblox has added. Example: “Connections” becomes “Friends”.",
settings: [{
label: "Enabled by default",
value: "True"
},
{
label: "Toggle Location",
value: "Appearance Tab"
},
{
label: "Scope",
value: "Roblox.com/*"
}
]
}
};
const popupHTML = `
<div class="rup-popup">
<div class="rup-content">
<div class="rup-header">
<img class="rup-logo" src="${window.Base64Images.logo}" alt="Rolocate Logo">
<div class="rup-header-content">
<h1 class="rup-title">Rolocate Update</h1>
<div class="rup-version">${VERSION}</div>
</div>
<span class="rup-close">×</span>
</div>
<div class="rup-main">
<div class="rup-left">
<div class="rup-developer-message">
<div class="rup-developer-message-title">From Oqarshi:</div>
<div class="rup-developer-message-text">Please report any issues on GreasyFork if something breaks! Thank you! RoLocate is designed to be used with Roblox's dark mode or dark theme.</div>
</div>
<div class="rup-features-title">✨ V41.3 🚀</div>
<div class="rup-feature-item" data-feature="restoreclassicterms">
<div class="rup-feature-header">
<span class="rup-feature-icon">🔄</span>
<div class="rup-feature-title">Restore Classic Terms</div>
<span class="rup-feature-badge">New</span>
</div>
</div>
<div class="rup-feature-item" data-feature="recentservers">
<div class="rup-feature-header">
<span class="rup-feature-icon">🕘</span>
<div class="rup-feature-title">Recent Servers</div>
<span class="rup-feature-badge">Bug Fix</span>
</div>
</div>
</div>
<div class="rup-right">
<div class="rup-welcome-panel" id="rup-welcome-panel">
<div class="rup-welcome-icon">🚀</div>
<div class="rup-welcome-text">Select a feature to learn more</div>
<div class="rup-welcome-subtext">Click on any feature from the left to see detailed information</div>
</div>
<div class="rup-detail-panel" id="rup-detail-panel" style="display: none;"></div>
<div class="rup-help-section">
<div class="rup-help-title">Need Help?</div>
<a href="https://oqarshi.github.io/Invite/rolocate/docs/" target="_blank" class="rup-help-link">
<span class="rup-help-link-icon">📖</span>
<span>Documentation</span>
</a>
<a> </a>
<a href="https://greasyfork.org/en/scripts/523727-rolocate/feedback" target="_blank" class="rup-help-link">
<span class="rup-help-link-icon">🛡️</span>
<span>Greasyfork Support</span>
</a>
</div>
</div>
</div>
<div class="rup-footer">
<p class="rup-note">This notification will not appear again until the next version release.</p>
</div>
</div>
</div>
`;
const popupContainer = document.createElement('div');
popupContainer.innerHTML = popupHTML;
document.body.appendChild(popupContainer);
const closeButton = popupContainer.querySelector('.rup-close');
const popup = popupContainer.querySelector('.rup-popup');
const featureItems = popupContainer.querySelectorAll('.rup-feature-item');
const welcomePanel = popupContainer.querySelector('#rup-welcome-panel');
const detailPanel = popupContainer.querySelector('#rup-detail-panel');
featureItems.forEach(item => {
item.addEventListener('click', (e) => {
featureItems.forEach(i => i.classList.remove('rup-active'));
item.classList.add('rup-active');
const featureKey = item.dataset.feature;
const feature = featureData[featureKey];
if (feature) {
welcomePanel.style.display = 'none';
detailPanel.style.display = 'flex';
detailPanel.classList.remove('rup-detail-panel');
void detailPanel.offsetWidth;
detailPanel.classList.add('rup-detail-panel');
detailPanel.innerHTML = `
<div class="rup-detail-title">
<span>${feature.icon}</span>
<span>${feature.title}</span>
</div>
<div class="rup-detail-subtitle">${feature.subtitle.replace(/\n/g, '<br>')}</div>
<div class="rup-detail-description">${feature.description.replace(/\\n/g, '<br>')}</div>
<div class="rup-detail-settings">
${feature.settings.map(setting => `
<div class="rup-setting-row">
<span class="rup-setting-label">${setting.label}:</span>
<span class="rup-setting-value">${setting.value}</span>
</div>
`).join('')}
</div>
`;
}
});
});
closeButton.addEventListener('click', (e) => {
popup.style.animation = 'rup-fadeOut 0.5s cubic-bezier(0.22, 0.61, 0.36, 1) forwards';
popup.querySelector('.rup-content').style.animation = 'rup-scaleDown 0.5s cubic-bezier(0.22, 0.61, 0.36, 1) forwards';
setTimeout(() => {
popup.parentNode.removeChild(popup);
const refreshPopup = document.createElement('div');
refreshPopup.innerHTML = `<style>
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
</style>
<div style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 999999; display: flex; align-items: center; justify-content: center; animation: fadeIn 0.3s ease-out;">
<div style="background: #1a1c23; padding: 35px; border-radius: 16px; max-width: 420px; text-align: center; color: #fff; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; box-shadow: 0 20px 40px rgba(0,0,0,0.5); animation: slideIn 0.3s ease-out;">
<h3 style="margin: 0 0 16px 0; font-size: 22px; font-weight: 600; color: #f8f9fa;">RoLocate</h3>
<p style="margin: 0 0 28px 0; font-size: 16px; line-height: 1.5; color: #e9ecef;">RoLocate needs to refresh the page to enable some features.</p>
<button onclick="this.style.transform='scale(0.95)';setTimeout(()=>location.reload(),100)" onmouseover="this.style.background='#1d4ed8'" onmouseout="this.style.background='#2563eb'" style="background: #2563eb; color: #fff; border: none; padding: 14px 28px; border-radius: 8px; font-size: 16px; cursor: pointer; font-weight: 500; transition: all 0.2s ease; transform: scale(1);">Refresh Now</button>
</div>
</div>`;
document.body.appendChild(refreshPopup);
}, 300);
});
}
/*******************************************************
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: true, // enabled 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
mutualfriends: true, // enabled by default
disablechat: false, // disabled by default
smartsearch: true, // enabled by default
quicklaunchgames: true, // enabled by default
smartjoinpopup: true, // enabled by default
betterfriends: true, // enabled by default
restoreclassicterms: true, // enabled by default
};
// 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);
}
});
}
/*******************************************************
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 `
<div class="home-section">
<img class="rolocate-logo" src="${window.Base64Images.logo}" alt="ROLOCATE Logo">
<div class="version">Rolocate: Version 41.3</div>
<div class="section-separator"></div>
<p>Rolocate by Oqarshi.</p>
<p class="license-note">
Licensed under a <strong>Custom License – Personal Use Only</strong>. No redistribution.
</p>
</div>
`;
}
if (section === "appearance") {
return `
<div class="appearance-section">
<label class="toggle-slider section-hover">
<input type="checkbox" id="disabletrailer">
<span class="slider"></span>
Disable Trailer Autoplay
<span class="help-icon" data-help="Disable Trailer Autoplay">?</span>
</label>
<label class="toggle-slider section-hover new_label">
<input type="checkbox" id="smartjoinpopup">
<span class="slider"></span>
Smart Join Popup
<span class="new">New
<span class="tooltip">Just Released/Updated</span>
</span>
<span class="help-icon" data-help="Smart Join Popup">?</span>
</label>
<label class="toggle-slider section-hover">
<input type="checkbox" id="removeads">
<span class="slider"></span>
Remove All Roblox Ads
<span class="help-icon" data-help="Remove All Roblox Ads">?</span>
</label>
<label class="toggle-slider section-hover new_label">
<input type="checkbox" id="restoreclassicterms">
<span class="slider"></span>
Restore Classic Terms
<span class="new">New
<span class="tooltip">Just Released/Updated</span>
</span>
<span class="help-icon" data-help="Restore Classic Terms">?</span>
</label>
<label class="toggle-slider section-hover">
<input type="checkbox" id="quicknav">
<span class="slider"></span>
Quick Navigation
<button id="edit-quicknav-btn" class="edit-button" type="button" style="display: none;">Edit</button>
<span class="help-icon" data-help="Quick Navigation">?</span>
</label>
</div>
`;
}
if (section === "advanced") {
return `
<div class="advanced-section">
<span class="warning_advanced">For Experienced Users Only🧠🙃</span>
<div class="section-separator"></div>
<label class="toggle-slider section-hover">
<input type="checkbox" id="enableLogs">
<span class="slider"></span>
Enable Console Logs
<span class="help-icon" data-help="Enable Console Logs">?</span>
</label>
<label class="toggle-slider section-hover">
<input type="checkbox" id="togglefilterserversbutton">
<span class="slider"></span>
Enable Server Filters
<span class="help-icon" data-help="Enable Server Filters">?</span>
</label>
<label class="toggle-slider section-hover">
<input type="checkbox" id="toggleserverhopbutton">
<span class="slider"></span>
Enable Server Hop Button
<span class="help-icon" data-help="Enable Server Hop Button">?</span>
</label>
<label class="toggle-slider section-hover">
<input type="checkbox" id="enablenotifications">
<span class="slider"></span>
Enable Notifications
<span class="help-icon" data-help="Enable Notifications">?</span>
</label>
<div class="location-settings section-hover">
<div class="setting-header">
<span>Set Default Location Mode</span>
<span class="help-icon" data-help="Set default location">?</span>
</div>
<select id="prioritylocation-select">
<option value="manual" style="color: rgb(255, 40, 40);">Manual</option>
<option value="automatic" style="color: rgb(255, 40, 40);">Automatic</option>
</select>
<div id="location-hint">
<strong>Manual:</strong> Set your location manually below
<strong>Automatic:</strong> Auto detect your device's location
</div>
<div id="manual-coordinates" style="margin-top: 15px; display: none;">
<div class="coordinates-inputs" style="display: flex; gap: 10px; margin-bottom: 12px;">
<div style="flex: 1;">
<label for="latitude" style="display: block; margin-bottom: 8px; font-size: 14px;">Latitude</label>
<input type="text" id="latitude" placeholder="e.g. 34.0549"
style="width: 100%; padding: 10px 12px; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.05);; background: rgba(255,255,255,0.05); color: #e0e0e0;">
</div>
<div style="flex: 1;">
<label for="longitude" style="display: block; margin-bottom: 8px; font-size: 14px;">Longitude</label>
<input type="text" id="longitude" placeholder="e.g. -118.2426"
style="width: 100%; padding: 10px 12px; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.05);; background: rgba(255,255,255,0.05); color: #e0e0e0;">
</div>
</div>
<button id="save-coordinates" class="edit-nav-button" style="width: 100%; margin-top: 8px;">
Save Coordinates
</button>
<div class="hint-text" style="margin-top: 12px; font-size: 13px; color: #a0a0a0;">
Enter your location's decimal coordinates, or if you're not comfortable sharing them, use the nearest Roblox server coordinates (e.g., Los Angeles: 34.0549, -118.2426).
</div>
</div>
</div>
</div>
`;
}
if (section === "extras") {
return `
<div class="extras-section">
<span class="extras_section">Features that might be useful!</span>
<label class="toggle-slider section-hover">
<input type="checkbox" id="gamequalityfilter">
<span class="slider"></span>
Game Quality Filter
<button id="edit-gamequality-btn" class="edit-button" type="button" style="display: none;">Edit</button>
<span class="help-icon" data-help="Game Quality Filter">?</span>
</label>
<label class="toggle-slider section-hover new_label">
<input type="checkbox" id="mutualfriends">
<span class="slider"></span>
Mutual Friends
<span class="new">New
<span class="tooltip">Just Released/Updated</span>
</span>
<span class="help-icon" data-help="Enable Mutual Friends">?</span>
</label>
<label class="toggle-slider section-hover new_label">
<input type="checkbox" id="disablechat">
<span class="slider"></span>
Disable Chat
<span class="new">New
<span class="tooltip">Just Released/Updated</span>
</span>
<span class="help-icon" data-help="Disable Chat">?</span>
</label>
<label class="toggle-slider section-hover new_label">
<input type="checkbox" id="quicklaunchgames">
<span class="slider"></span>
Quick Launch Games
<span class="new">New
<span class="tooltip">Just Released/Updated</span>
</span>
<span class="help-icon" data-help="Quick Launch Games">?</span>
</label>
<label class="toggle-slider section-hover new_label">
<input type="checkbox" id="ShowOldGreeting">
<span class="slider"></span>
Show Old Greeting
<span class="new">New
<span class="tooltip">Just Released/Updated</span>
</span>
<span class="help-icon" data-help="Show Old Greeting">?</span>
</label>
<label class="toggle-slider section-hover new_label">
<input type="checkbox" id="betterfriends">
<span class="slider"></span>
Better Friends
<span class="new">New
<span class="tooltip">Just Released/Updated</span>
</span>
<span class="help-icon" data-help="Better Friends">?</span>
</label>
</div>
`;
}
if (section === "about") {
return `
<div class="about-section">
<h3 class="grayish-center">Credits</h3>
<p>This project was created by:</p>
<ul>
<li><strong>Developer:</strong> <a href="https://www.roblox.com/users/545334824/profile" target="_blank">Oqarshi</a></li>
<li><strong>Rolocate Source Code:</strong> <a href="https://greasyfork.org/en/scripts/523727-rolocate/code" target="_blank">GreasyFork</a></li>
<li><strong>Invite & FAQ Source Code:</strong> <a href="https://github.com/Oqarshi/Invite" target="_blank">GitHub</a></li>
<li><strong>Official Website:</strong> <a href="https://oqarshi.github.io/Invite/rolocate/index.html" target="_blank">RoLocate Website</a></li>
<li><strong>Suggest or Report Issues:</strong> <a href="https://greasyfork.org/en/scripts/523727-rolocate/feedback" target="_blank">Submit Feedback</a></li>
<li><strong>Inspiration:</strong> <a href="https://chromewebstore.google.com/detail/btroblox-making-roblox-be/hbkpclpemjeibhioopcebchdmohaieln" target="_blank">Btroblox Team</a></li>
</ul>
</div>
`;
}
if (section === "help") {
return `
<div class="help-section">
<div class="section-separator"></div>
<h3 class="grayish-center">⚙️ General Tab</h3>
<ul>
<li id="help-Smart Search"><strong>SmartSearch:</strong> <span>Improves the Roblox website’s search bar by enabling instant searches for games, users, and groups.</span></li>
<li id="help-Auto Server Regions"><strong>Auto Server Regions:</strong> <span>Replaces Roblox's 8 default servers with at least 8 servers, providing detailed info such as location and ping.</span></li>
<li id="help-Fast Server Search"><strong>Fast Server Search:</strong> <span>Boosts server search speed up to 100x (experimental). Replaces player thumbnails with Builderman/Roblox icons to bypass rate limits.</span></li>
<li id="help-Invert Player Count"><strong>Invert Player Count:</strong> <span>For server regions: shows low-player servers when enabled, high-player servers when disabled. You can also control this on the Roblox server popup.</span></li>
<li id="help-Recent Servers"><strong>Recent Servers:</strong> <span>Shows the most recent servers you have joined in the past 3 days.</span></li>
</ul>
<div class="section-separator"></div>
<h3 class="grayish-center">🎨 Appearance Tab</h3>
<ul>
<li id="help-Disable Trailer Autoplay"><strong>Disable Trailer Autoplay:</strong> <span>Prevents trailers from autoplaying on Roblox game pages.</span></li>
<li id="help-Smart Join Popup"><strong>Smart Join Popup:</strong> <span>Shows a custom join popup that displays server location about the server before joining it.</span></li>
<li id="help-Remove All Roblox Ads"><strong>Remove All Roblox Ads:</strong> <span>Blocks most ads on the Roblox site.</span></li>
<li id="help-Restore Classic Terms"><strong>Restore Classic Terms:</strong> <span>Reverts corporate buzzwords Roblox has added. Example: “Connections” becomes “Friends”.</span></li>
<li id="help-Quick Navigation"><strong>Quick Nav:</strong> <span>Ability to add quick navigations to the leftside panel of the Roblox page.</span></li>
</ul>
<div class="section-separator"></div>
<h3 class="grayish-center">🚀 Advanced Tab</h3>
<ul>
<li id="help-Enable Console Logs"><strong>Enable Console Logs:</strong> <span>Enables console.log messages from the script.</span></li>
<li id="help-Enable Server Filters"><strong>Enable Server Filters:</strong> <span>Enables server filter features on the game page.</span></li>
<li id="help-Enable Server Hop Button"><strong>Enable Server Hop Button:</strong> <span>Enables server hop feature on the game page.</span></li>
<li id="help-Enable Notifications"><strong>Enable Notifications:</strong> <span>Enables helpful notifications from the script.</span></li>
<li id="help-Set default location"><strong>Set default location:</strong> <span>Enables the user to set a default location for Roblox server regions. Turn this on if the script cannot automatically detect your location.</span></li>
</ul>
<h3 class="grayish-center">✨ Extra Tab</h3>
<ul>
<li id="help-Game Quality Filter"><strong>Game Quality Filter:</strong> <span>Removes games from the charts/discover page based on your settings.</span></li>
<li id="help-Enable Mutual Friends"><strong>Mutual Friends:</strong> <span>Displays friends you share with a certain person on their profile page.</span></li>
<li id="help-Disable Chat"><strong>Disable Chat:</strong> <span>Disables the chat feature on the roblox website.</span></li>
<li id="help-Quick Launch Games"><strong>Quick Launch Games:</strong> <span>Adds the ability to quickly launch your favorite games from the homepage.</span></li>
<li id="help-Show Old Greeting"><strong>Show Old Greeting:</strong> <span>Shows the old greeting Roblox had on their home page.</span></li>
<li id="help-Better Friends"><strong>Better Friends:</strong> <span>Improves the look of the friends section on the homepage and adds Best Friends option.</span></li>
</ul>
<div class="section-separator"></div>
<h3 class="grayish-center">Need more help?</h3>
<li>
For help, see the
<a href="https://oqarshi.github.io/Invite/rolocate/docs/#troubleshooting" target="_blank" class="about-link">troubleshooting</a>
page or report an issue on
<a href="https://greasyfork.org/en/scripts/523727-rolocate/feedback" target="_blank" class="about-link">GreasyFork</a>.
</li>
</div>
`;
}
// General tab (default)
return `
<div class="general-section">
<label class="toggle-slider section-hover new_label experiment_label">
<input type="checkbox" id="smartsearch">
<span class="slider"></span>
SmartSearch
<span class="new">New
<span class="tooltip">Just Released/Updated</span>
</span>
<span class="experimental">Experimental
<span class="tooltip">Still being tested</span>
</span>
<span class="help-icon" data-help="Smart Search">?</span>
</label>
<label class="toggle-slider section-hover">
<input type="checkbox" id="AutoRunServerRegions">
<span class="slider"></span>
Auto Server Regions
<span class="help-icon" data-help="Auto Server Regions">?</span>
</label>
<label class="toggle-slider section-hover experiment_label">
<input type="checkbox" id="fastservers">
<span class="slider"></span>
Fast Server Search
<span class="experimental">Experimental
<span class="tooltip">Still being tested</span>
</span>
<span class="help-icon" data-help="Fast Server Search">?</span>
</label>
<label class="toggle-slider section-hover">
<input type="checkbox" id="invertplayercount">
<span class="slider"></span>
Invert Player Count
<span class="help-icon" data-help="Invert Player Count">?</span>
</label>
<label class="toggle-slider section-hover">
<input type="checkbox" id="togglerecentserverbutton">
<span class="slider"></span>
Recent Servers
<span class="help-icon" data-help="Recent Servers">?</span>
</label>
</div>
`;
}
/*******************************************************
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 = `
<div class="settings-container">
<button id="close-settings">✖</button>
<div class="settings-sidebar">
<h2>RoLocate</h2>
<ul>
<li class="active" data-section="home">🏠 Home</li>
<li data-section="general">⚙️ General</li>
<li data-section="appearance">🎨 Appearance</li>
<li data-section="advanced">🚀 Advanced</li>
<li data-section="extras">✨ Extras</li>
<li data-section="help">📙 Help</li>
<li data-section="about">ℹ️ About</li>
</ul>
</div>
<div class="settings-content">
<h2 id="settings-title">Home</h2>
<div id="settings-body" class="animated-content">${getSettingsContent("home")}</div>
</div>
</div>
`;
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.65em;
color: #999;
margin-top: 12px;
font-style: italic;
text-align: center;
}
.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: 440px; /* 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;
color: #ff3b47;
font-weight: bold;
padding: 8px 14px;
background: rgba(220, 53, 69, 0.1);
border-radius: 6px;
margin-bottom: 16px;
display: inline-block;
border: 1px solid rgba(220, 53, 69, 0.2);
box-shadow: 0 0 6px rgba(220, 53, 69, 0.3);
transition: box-shadow 0.3s ease;
}
.warning_advanced:hover {
box-shadow: 0 0 12px rgba(220, 53, 69, 0.6);
}
.extras_section {
font-size: 14px;
color: #0d6efd; /* vibrant blue */
font-weight: bold;
padding: 8px 14px;
background: rgba(13, 110, 253, 0.1);
border-radius: 6px;
margin-bottom: 16px;
display: inline-block;
border: 1px solid rgba(13, 110, 253, 0.3);
box-shadow: 0 0 6px rgba(13, 110, 253, 0.3);
transition: box-shadow 0.3s ease;
}
.extras_section:hover {
box-shadow: 0 0 12px rgba(13, 110, 253, 0.6);
}
.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 */
#prioritylocation-select {
width: 100%;
padding: 10px 14px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.05);
color: #e0e0e0;
font-size: 14px
appearance: none;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="%23dc3545" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>');
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 === "extras") {
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 = `
<div style="position: relative; margin-bottom: 24px; text-align: center;">
<h2 style="margin: 0 0 8px; font-size: 28px; font-weight: 600; background: linear-gradient(90deg, #4CAF50, #8BC34A); -webkit-background-clip: text; background-clip: text; color: transparent;">Quick Navigation</h2>
<p style="margin: 0; font-size: 16px; color: #a0a0a0; font-weight: 300;">Configure up to 9 custom navigation shortcuts</p>
<div style="width: 60px; height: 4px; background: linear-gradient(90deg, #4CAF50, #8BC34A); margin: 16px auto; border-radius: 2px;"></div>
</div>
`;
// Build inputs for 9 links in a 3x3 grid
const inputsGrid = `
<div class="quicknav-inputs-grid" style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px;">
${Array.from({length: 9}, (_, i) => `
<div class="quicknav-input-group" style="background: rgba(255,255,255,0.03); padding: 16px; border-radius: 12px; border: 1px solid rgba(255,255,255,0.05);">
<p style="font-weight: 500; font-size: 14px; margin: 0 0 8px; color: #A5D6A7;">${i + 1}</p>
<input type="text" id="quicknav-name-${i}" placeholder="Name" value="${saved[i]?.name || ""}"
style="width: 100%; padding: 10px 12px; margin-bottom: 8px; border-radius: 8px; border: none; background: rgba(255,255,255,0.05); color: white; font-size: 14px; transition: all 0.2s;">
<input type="text" id="quicknav-link-${i}" placeholder="URL" value="${saved[i]?.link || ""}"
style="width: 100%; padding: 10px 12px; border-radius: 8px; border: none; background: rgba(255,255,255,0.05); color: white; font-size: 14px; transition: all 0.2s;">
</div>
`).join("")}
</div>
`;
// 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)); // store coordinates in secure storage
// 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 = `
<img id="custom-logo"
style="
margin-top: 6px;
margin-left: 6px;
width: 26px;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s ease-in-out;
"
src="${base64Logo}">
<span id="custom-tooltip"
style="
visibility: hidden;
background-color: black;
color: white;
text-align: center;
padding: 5px;
border-radius: 5px;
position: absolute;
top: 35px;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
font-size: 12px;
opacity: 0;
transition: opacity 0.2s ease-in-out;
">
Settings
</span>
`;
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 including sponsored sections,
"Today's Picks", and "Recommended For You" from the homepage.
no network/script blocking to avoid ublock conflicts
*******************************************************/
function removeAds() {
if (localStorage.getItem("ROLOCATE_removeads") !== "true") {
return;
}
const doneMap = new WeakMap();
let isRunning = false;
/*******************************************************
name of function: removeElements
description: remove the roblox elements where ads and specific sections are in
no script removal to avoid conflicts
*******************************************************/
function removeElements() {
// prevent multiple runs at same time
if (isRunning) return;
isRunning = true;
try {
// be more specific with iframe removal - only target ad containers
const adIframes = document.querySelectorAll(`
.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/"]
`);
adIframes.forEach(iframe => {
if (!doneMap.get(iframe)) {
// hide instead of remove to be less aggressive
iframe.style.display = "none";
iframe.style.visibility = "hidden";
doneMap.set(iframe, true);
}
});
// skip all script removal to avoid conflicts with ublock
// hide sponsored game cards instead of messing with containers
document.querySelectorAll(".game-card-native-ad").forEach(ad => {
if (!doneMap.get(ad)) {
const gameCard = ad.closest(".game-card-container");
if (gameCard) {
gameCard.style.display = "none";
}
doneMap.set(ad, true);
}
});
// block sponsored sections but check if already processed
document.querySelectorAll(".game-sort-carousel-wrapper").forEach(wrapper => {
if (doneMap.get(wrapper)) return;
const sponsoredLink = wrapper.querySelector('a[href*="Sponsored"]');
if (sponsoredLink) {
wrapper.style.display = "none";
doneMap.set(wrapper, true);
}
});
// remove "today's picks" section
document.querySelectorAll('.game-sort-carousel-wrapper').forEach(wrapper => {
if (doneMap.get(wrapper)) return;
const headerText = wrapper.querySelector('[data-testid="text-icon-row-text"]');
if (headerText && /today's picks(:|$)/i.test(headerText.textContent.trim())) {
wrapper.style.display = "none";
doneMap.set(wrapper, true);
}
});
// remove "recommended for you" section
document.querySelectorAll('[data-testid="home-page-game-grid"]').forEach(grid => {
if (!doneMap.get(grid)) {
grid.style.display = "none";
doneMap.set(grid, true);
}
});
// remove feed items
document.querySelectorAll(".sdui-feed-item-container").forEach(node => {
if (!doneMap.get(node)) {
node.style.display = "none";
doneMap.set(node, true);
}
});
} finally {
isRunning = false;
}
}
// use a throttled observer to reduce conflicts
let timeoutId;
const observer = new MutationObserver(() => {
clearTimeout(timeoutId);
timeoutId = setTimeout(removeElements, 100);
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// wait a bit before initial run to let ublock do its thing first
setTimeout(removeElements, 100);
}
// opens game quality gui - dont open if already open
function openGameQualitySettings() {
if (document.getElementById('game-settings-modal')) return;
// make the dark overlay thing
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;
`;
// the actual modal box
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;
`;
const form = document.createElement('form');
form.setAttribute('novalidate', '');
// title text
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;
`;
// rating slider 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;
`;
// slider thumb styles
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);
// 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;
`;
// get existing player count or defaults
const existingPlayerCount = localStorage.getItem('ROLOCATE_playercount');
let minPlayerValue = '2500',
maxPlayerValue = 'unlimited';
if (existingPlayerCount) {
try {
const playerCountData = JSON.parse(existingPlayerCount);
minPlayerValue = playerCountData.min || '2500';
maxPlayerValue = playerCountData.max || 'unlimited';
} catch (e) {
ConsoleLogEnabled('Failed to parse player count data, using defaults');
}
}
// helper function to create input containers
function createInputContainer(labelText, inputType, inputId, inputName, inputValue, extraAttrs = {}) {
const container = document.createElement('div');
const label = document.createElement('label');
label.textContent = labelText;
label.setAttribute('for', inputId);
label.style.cssText = `
display: block;
margin-bottom: 6px;
font-weight: 500;
color: #e0e0e0;
font-size: 14px;
`;
const input = document.createElement('input');
input.type = inputType;
input.id = inputId;
input.name = inputName;
input.value = inputValue;
input.setAttribute('aria-describedby', 'player-count-desc');
input.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;
`;
// add extra attributes
Object.entries(extraAttrs).forEach(([key, value]) => {
input.setAttribute(key, value);
});
container.appendChild(label);
container.appendChild(input);
return {
container,
input
};
}
// min player input
const minData = createInputContainer('Minimum Players', 'number', 'min-players', 'minPlayers', minPlayerValue, {
min: '0',
max: '1000000'
});
// max player input
const maxData = createInputContainer('Maximum Players', 'text', 'max-players', 'maxPlayers', maxPlayerValue, {
placeholder: 'Enter number or "unlimited"'
});
// fix max label color
maxData.container.querySelector('label').style.color = '#495057';
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 thing
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;
`;
// validation and focus effects for inputs
[minData.input, maxData.input].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(minData.input.value);
const maxValue = maxData.input.value.toLowerCase() === 'unlimited' ? Infinity : parseInt(maxData.input.value);
if (isNaN(minValue) || minValue < 0) {
errorContainer.textContent = 'Minimum player count must be a valid number greater than or equal to 0.';
errorContainer.style.display = 'block';
return false;
}
if (maxData.input.value.toLowerCase() !== 'unlimited' && (isNaN(maxValue) || maxValue < 0)) {
errorContainer.textContent = 'Maximum player count must be a valid number or "unlimited".';
errorContainer.style.display = 'block';
return false;
}
if (maxValue !== Infinity && minValue > maxValue) {
errorContainer.textContent = 'Minimum player count cannot be greater than maximum player count.';
errorContainer.style.display = 'block';
return false;
}
return true;
}
inputGrid.appendChild(minData.container);
inputGrid.appendChild(maxData.container);
playerFieldset.appendChild(playerLegend);
playerFieldset.appendChild(inputGrid);
playerFieldset.appendChild(playerDescription);
playerFieldset.appendChild(errorContainer);
playerSection.appendChild(playerFieldset);
// buttons
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 32px;
`;
// helper for button creation
function createButton(text, type, bgColor, borderColor, hoverBg, hoverBorder) {
const button = document.createElement('button');
button.type = type;
button.textContent = text;
button.style.cssText = `
padding: 12px 24px;
background: ${bgColor};
color: ${type === 'submit' ? 'white' : '#cccccc'};
border: 2px solid ${borderColor};
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.15s ease;
outline: none;
`;
button.addEventListener('mouseenter', function() {
this.style.backgroundColor = hoverBg;
this.style.borderColor = hoverBorder;
});
button.addEventListener('mouseleave', function() {
this.style.backgroundColor = bgColor;
this.style.borderColor = borderColor;
});
button.addEventListener('focus', function() {
this.style.boxShadow = type === 'submit' ? '0 0 0 3px rgba(22, 101, 52, 0.25)' : '0 0 0 3px rgba(108, 117, 125, 0.25)';
});
button.addEventListener('blur', function() {
this.style.boxShadow = 'none';
});
return button;
}
const cancelButton = createButton('Cancel', 'button', '#333333', '#555555', '#404040', '#666666');
const saveButton = createButton('Save Settings', 'submit', '#166534', '#166534', '#14532d', '#14532d');
// form submit handler
form.addEventListener('submit', function(e) {
e.preventDefault();
if (!validateInputs()) return;
try {
const playerCountData = {
min: minData.input.value,
max: maxData.input.value
};
localStorage.setItem('ROLOCATE_gamerating', ratingSlider.value);
localStorage.setItem('ROLOCATE_playercount', JSON.stringify(playerCountData));
closeModal();
} catch (error) {
ConsoleLogEnabled('Failed to save settings:', error);
errorContainer.textContent = 'Failed to save settings. Please try again.';
errorContainer.style.display = 'block';
}
});
cancelButton.addEventListener('click', closeModal);
// close modal with animation
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);
// put it all together
form.appendChild(title);
form.appendChild(ratingSection);
form.appendChild(playerSection);
form.appendChild(buttonContainer);
modal.appendChild(form);
overlay.appendChild(modal);
document.body.appendChild(overlay);
// show modal with animation
requestAnimationFrame(() => {
overlay.style.opacity = '1';
modal.style.transform = 'scale(1) translateY(0)';
});
// focus first input
setTimeout(() => ratingSlider.focus(), 250);
}
function qualityfilterRobloxGames() {
// Prevent multiple observers
if (window.robloxGameFilterObserver) {
window.robloxGameFilterObserver.disconnect();
}
const seenCards = new WeakSet();
function parsePlayerCount(text) {
if (!text) return 0;
const clean = text.replace(/[,\s]/g, '').toLowerCase();
const multiplier = clean.includes('k') ? 1000 : clean.includes('m') ? 1000000 : 1;
const number = parseFloat(clean.replace(/[km]/, ''));
return isNaN(number) ? 0 : number * multiplier;
}
function getFilterSettings() {
return {
enabled: localStorage.getItem('ROLOCATE_gamequalityfilter') === 'true',
rating: parseInt(localStorage.getItem('ROLOCATE_gamerating') || '80'),
playerCount: (() => {
const data = JSON.parse(localStorage.getItem('ROLOCATE_playercount') || '{"min":"5000","max":"unlimited"}');
return {
min: parseInt(data.min),
max: data.max === 'unlimited' ? Infinity : parseInt(data.max)
};
})()
};
}
function filterCard(card, settings) {
if (seenCards.has(card)) return;
seenCards.add(card);
let rating = 0;
const ratingSelectors = [
'.vote-percentage-label',
'[data-testid="game-tile-stats-rating"] .vote-percentage-label',
'.game-card-info .vote-percentage-label',
'.base-metadata .vote-percentage-label'
];
for (const sel of ratingSelectors) {
const el = card.querySelector(sel);
if (el) {
const match = el.textContent.match(/(\d+)%/);
if (match) {
rating = parseInt(match[1]);
break;
}
}
}
let playerCount = 0;
let hasPlayerCount = false;
const pcEl = card.querySelector('.playing-counts-label');
if (pcEl) {
playerCount = parsePlayerCount(pcEl.textContent);
hasPlayerCount = true;
}
const shouldShow = (
rating >= settings.rating &&
(!hasPlayerCount || (playerCount >= settings.playerCount.min && playerCount <= settings.playerCount.max))
);
card.style.display = shouldShow ? '' : 'none';
}
function filterAllCards() {
const settings = getFilterSettings();
if (!settings.enabled) return;
const cards = document.querySelectorAll(`
li.game-card,
li[data-testid="wide-game-tile"],
.grid-item-container.game-card-container
`);
cards.forEach(card => filterCard(card, settings));
}
// Run filtering every second to pick up new cards and setting changes
const intervalId = setInterval(() => {
try {
filterAllCards();
} catch (err) {
ConsoleLogEnabled('[ROLOCATE] Filter error:', err);
}
}, 1000);
// MutationObserver for extra responsiveness on new DOM nodes
const observer = new MutationObserver(() => {
filterAllCards();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Store refs for cleanup
window.robloxGameFilterObserver = observer;
window.robloxGameFilterInterval = intervalId;
}
/***************************************************************
* name of function: showOldRobloxGreeting
* description: shows old roblox greeting if setting is turned on
****************************************************************/
async function showOldRobloxGreeting() {
// Private implementation with isolated scope
const implementation = async () => {
ConsoleLogEnabled("Function showOldRobloxGreeting() started.");
// Check if we're on the Roblox home page
if (!/^https?:\/\/(www\.)?roblox\.com(\/[a-z]{2})?\/home\/?$/i.test(window.location.href)) {
ConsoleLogEnabled("Not on roblox.com/home. Exiting function.");
return;
}
// Check if the feature is enabled
if (localStorage.getItem("ROLOCATE_ShowOldGreeting") !== "true") {
ConsoleLogEnabled("ShowOldGreeting is disabled. Exiting function.");
return;
}
// Wait for page to load
await new Promise(r => setTimeout(r, 500));
// Private helper functions
const observeElement = (selector) => {
return new Promise((resolve) => {
const element = document.querySelector(selector);
if (element) {
ConsoleLogEnabled(`Element found immediately: ${selector}`);
return resolve(element);
}
ConsoleLogEnabled(`Observing for element: ${selector}`);
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
});
});
};
const fetchAvatar = async (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;
};
const getTimeBasedGreeting = (username) => {
const hour = new Date().getHours();
if (hour < 12) return `Morning, ${username}!`;
if (hour < 18) return `Afternoon, ${username}!`;
return `Evening, ${username}!`;
};
try {
// Get required elements
const homeContainer = await observeElement("#HomeContainer .section:first-child");
ConsoleLogEnabled("Home container located.");
const userNameElement = document.querySelector("#navigation.rbx-left-col > ul > li > a .font-header-2");
const rawUsername = userNameElement ? userNameElement.innerText : "Robloxian";
ConsoleLogEnabled(`User name found: ${rawUsername}`);
// Create isolated styles with unique class names
const styleId = 'rolocate-greeting-styles';
if (!document.getElementById(styleId)) {
const styleTag = document.createElement("style");
styleTag.id = styleId; // HERE
styleTag.textContent = `
.rolocate-greeting-header {
display: flex;
align-items: center;
margin-bottom: 16px;
padding: 30px;
background: #1a1c23;
border-radius: 12px;
border: 1px solid #2a2a30;
min-height: 180px;
}
.rolocate-profile-frame {
width: 140px;
height: 140px;
border-radius: 50%;
overflow: hidden;
border: 3px solid #2a2a30;
}
.rolocate-profile-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.rolocate-user-details {
margin-left: 25px;
}
.rolocate-user-name {
font-size: 2em;
font-weight: 600;
color: #ffffff;
margin: 0;
font-family: 'Segoe UI', Roboto, sans-serif;
}
`;
document.head.appendChild(styleTag);
}
// Create the greeting header with unique class names
const headerContainer = document.createElement("div");
headerContainer.className = "rolocate-greeting-header";
// Create profile picture
const profileFrame = document.createElement("div");
profileFrame.className = "rolocate-profile-frame";
const profileImage = document.createElement("img");
profileImage.className = "rolocate-profile-img";
profileImage.src = await fetchAvatar("#navigation.rbx-left-col > ul > li > a img",
window.Base64Images?.image_place_holder || "https://www.roblox.com/Thumbs/Asset.ashx?width=100&height=100&assetId=0");
profileFrame.appendChild(profileImage);
// Create greeting text
const userDetails = document.createElement("div");
userDetails.className = "rolocate-user-details";
const userName = document.createElement("h1");
userName.className = "rolocate-user-name";
userName.textContent = getTimeBasedGreeting(rawUsername);
userDetails.appendChild(userName);
// Combine elements
headerContainer.appendChild(profileFrame);
headerContainer.appendChild(userDetails);
// Replace existing content
homeContainer.replaceWith(headerContainer);
ConsoleLogEnabled("Greeting header created successfully.");
} catch (error) {
ConsoleLogEnabled(`Error creating greeting: ${error.message}`);
}
};
// Execute the isolated implementation
implementation().catch(error => {
ConsoleLogEnabled("Error in showOldRobloxGreeting:", error);
});
}
/*******************************************************
name of function: observeURLChanges
description: observes url changes for the old old greeting,
quality game filter, and betterfriends
*******************************************************/
function observeURLChanges() {
// dont run this twice
if (window.urlObserverActive) return;
window.urlObserverActive = true;
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;
// clean up the game filter stuff
if (window.robloxGameFilterObserver) {
window.robloxGameFilterObserver.disconnect();
window.robloxGameFilterObserver = null;
}
if (window.robloxGameFilterInterval) {
clearInterval(window.robloxGameFilterInterval);
window.robloxGameFilterInterval = null;
}
// if we go back to home page do the stuff
if (/roblox\.com(\/[a-z]{2})?\/home/.test(currentUrl)) {
ConsoleLogEnabled("back on home page");
betterfriends();
quicklaunchgamesfunction();
showOldRobloxGreeting();
}
}
};
// hook into history changes if not already done
if (!window.historyIntercepted) {
const interceptHistoryMethod = (method) => {
const original = history[method];
history[method] = function(...args) {
const result = original.apply(this, args);
setTimeout(checkUrl, 0);
return result;
};
};
interceptHistoryMethod('pushState');
interceptHistoryMethod('replaceState');
window.historyIntercepted = true;
}
// save handler so we can remove it later if needed
window.urlChangeHandler = checkUrl;
// get rid of old popstate if it exists to avoid duplicates
if (window.urlChangeHandler) {
window.removeEventListener('popstate', window.urlChangeHandler);
}
window.addEventListener('popstate', checkUrl);
}
/*******************************************************
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: 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();
}
})();
}
/*******************************************************
name of function: loadmutualfriends
description: shows mutual friends. a huge function so its harder to copy.
*******************************************************/
async function loadmutualfriends() {
// check if mutualfriends is enabled in localStorage and double check if url is the correct one.
if (localStorage.getItem("ROLOCATE_mutualfriends") !== "true" || !/^\/(?:[a-z]{2}\/)?users\/\d+\/profile$/.test(window.location.pathname)) return;
// Local cache for storing avatar data per page visit
let localAvatarCache = {};
// Helper function to get current user ID
const getCurrentUserId = () => Roblox?.CurrentUser?.userId || null;
// Helper function to fetch friends via GM_xmlhttpRequest
const gmFetchFriends = (userId) => {
const url = `https://friends.roblox.com/v1/users/${userId}/friends`;
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url,
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
try {
const data = JSON.parse(response.responseText);
resolve(data.data);
} catch (e) {
ConsoleLogEnabled(`[gmFetchFriends] Failed to parse response for user ${userId}`, e);
resolve(null);
}
} else {
ConsoleLogEnabled(`[gmFetchFriends] Request failed for user ${userId} with status ${response.status}`);
resolve(null);
}
},
onerror: function(err) {
ConsoleLogEnabled(`[gmFetchFriends] Network error for user ${userId}`, err);
resolve(null);
}
});
});
};
// helper function to fetch user avatars
const fetchUserAvatars = (userIds) => {
return new Promise((resolve) => {
const requests = userIds.map(userId => ({
requestId: userId.toString(),
targetId: userId,
type: "AvatarHeadShot",
size: "150x150",
format: "Png",
isCircular: false
}));
GM_xmlhttpRequest({
method: "POST",
url: "https://thumbnails.roblox.com/v1/batch",
headers: {
"Content-Type": "application/json"
},
data: JSON.stringify(requests),
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
try {
const data = JSON.parse(response.responseText);
const avatarMap = {};
data.data.forEach(item => {
if (item.state === "Completed" && item.imageUrl) {
avatarMap[item.targetId] = item.imageUrl;
}
});
resolve(avatarMap);
} catch (e) {
ConsoleLogEnabled("[fetchUserAvatars] Failed to parse response", e);
resolve({});
}
} else {
ConsoleLogEnabled(`[fetchUserAvatars] Request failed with status ${response.status}`);
resolve({});
}
},
onerror: function(err) {
ConsoleLogEnabled("[fetchUserAvatars] Network error", err);
resolve({});
}
});
});
};
// function to fetch and cache all avatars at once
const fetchAndCacheAllAvatars = async (mutualFriends) => {
if (Object.keys(localAvatarCache).length > 0) {
ConsoleLogEnabled('[fetchAndCacheAllAvatars] Using cached avatars');
return localAvatarCache;
}
ConsoleLogEnabled('[fetchAndCacheAllAvatars] Fetching avatars for the first time');
const avatarPromises = [];
for (let i = 0; i < mutualFriends.length; i += 5) {
const batch = mutualFriends.slice(i, i + 5);
const userIds = batch.map(friend => friend.id);
avatarPromises.push(fetchUserAvatars(userIds));
}
const avatarResults = await Promise.all(avatarPromises);
localAvatarCache = Object.assign({}, ...avatarResults);
ConsoleLogEnabled(`[fetchAndCacheAllAvatars] Cached ${Object.keys(localAvatarCache).length} avatars`);
return localAvatarCache;
};
// function to create the mutual friends element with all styles
const createMutualFriendsElement = () => {
if (!document.querySelector('#mutual-friends-styles')) { // css stuff
const style = document.createElement('style');
style.id = 'mutual-friends-styles';
style.textContent = `
.mutual-friends-container {
background: linear-gradient(135deg, #111114 0%, #1a1a1d 100%);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 20px;
margin: 20px 0;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
transition: all 0.2s ease;
position: relative;
overflow: hidden;
animation: slideInUp 0.3s ease-out;
}
.mutual-friends-container:hover {
background: linear-gradient(135deg, #1a1a1d 0%, #222226 100%);
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.4);
transform: translateY(-1px);
border-color: rgba(255, 255, 255, 0.2);
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.mutual-friends-header {
display: flex;
align-items: center;
margin-bottom: 16px;
color: #ffffff;
font-size: 18px;
font-weight: 700;
font-family: "Source Sans Pro", Arial, sans-serif;
position: relative;
z-index: 1;
}
.mutual-friends-icon {
width: 24px;
height: 24px;
margin-right: 12px;
fill: url(#iconGradient);
flex-shrink: 0;
}
.mutual-friends-count {
background: linear-gradient(45deg, #4a90e2, #357abd);
color: white;
padding: 8px 14px;
border-radius: 20px;
font-size: 14px;
font-weight: 800;
margin-left: 12px;
box-shadow: 0 4px 15px rgba(74, 144, 226, 0.3);
animation: bounceIn 0.3s ease-out;
min-width: 40px;
text-align: center;
border: 2px solid rgba(255, 255, 255, 0.2);
}
@keyframes bounceIn {
0% {
transform: scale(0.5);
opacity: 0;
}
60% {
transform: scale(1.05);
}
100% {
transform: scale(1);
opacity: 1;
}
}
.mutual-friends-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.mutual-friend-tag {
background: rgba(255, 255, 255, 0.08);
color: #ffffff;
padding: 8px 16px;
border-radius: 25px;
font-size: 14px;
font-weight: 600;
border: 1px solid rgba(255, 255, 255, 0.15);
transition: all 0.15s ease;
cursor: pointer;
font-family: "Source Sans Pro", Arial, sans-serif;
white-space: nowrap;
position: relative;
overflow: hidden;
animation: fadeInScale 0.2s ease-out backwards;
}
.mutual-friend-tag:nth-child(1) {
animation-delay: 0.05s;
}
.mutual-friend-tag:nth-child(2) {
animation-delay: 0.1s;
}
.mutual-friend-tag:nth-child(3) {
animation-delay: 0.15s;
}
.mutual-friend-tag:nth-child(4) {
animation-delay: 0.2s;
}
.mutual-friend-tag:nth-child(5) {
animation-delay: 0.25s;
}
.mutual-friend-tag:nth-child(6) {
animation-delay: 0.3s;
}
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.9) translateY(10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.mutual-friend-tag:hover {
background: linear-gradient(
45deg,
rgba(255, 255, 255, 0.15),
rgba(255, 255, 255, 0.12)
);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-2px) scale(1.02);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
}
.mutual-friends-more {
background: linear-gradient(45deg, #ff6b35, #f7931e) !important;
border-color: rgba(255, 255, 255, 0.3) !important;
color: white !important;
font-weight: 700 !important;
box-shadow: 0 4px 15px rgba(255, 107, 53, 0.4) !important;
}
.mutual-friends-more:hover {
background: linear-gradient(45deg, #ff5722, #e68900) !important;
border-color: rgba(255, 255, 255, 0.5) !important;
box-shadow: 0 6px 20px rgba(255, 107, 53, 0.6) !important;
}
.mutual-friends-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.mutual-friends-popup {
background: linear-gradient(135deg, #111114 0%, #1a1a1d 100%);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 16px;
width: 90%;
max-width: 700px;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
animation: popupSlideIn 0.2s ease-out;
}
@keyframes popupSlideIn {
from {
opacity: 0;
transform: scale(0.95) translateY(20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.mutual-friends-popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background: linear-gradient(90deg, rgba(255, 255, 255, 0.05), transparent);
}
.mutual-friends-popup-header h3 {
color: #ffffff;
margin: 0;
font-family: "Source Sans Pro", Arial, sans-serif;
font-size: 20px;
font-weight: 700;
}
.mutual-friends-close {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #ffffff;
font-size: 20px;
cursor: pointer;
padding: 8px;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.15s ease;
}
.mutual-friends-close:hover {
background: rgba(255, 59, 59, 0.2);
border-color: rgba(255, 59, 59, 0.4);
transform: rotate(90deg);
}
.mutual-friends-popup-grid {
padding: 24px;
max-height: 60vh;
overflow-y: auto;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
}
.mutual-friends-popup-item {
display: flex;
align-items: center;
padding: 16px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
cursor: pointer;
transition: all 0.15s ease;
animation: itemSlideIn 0.2s ease-out backwards;
}
.mutual-friends-popup-item:nth-child(odd) {
animation-delay: 0.05s;
}
.mutual-friends-popup-item:nth-child(even) {
animation-delay: 0.1s;
}
@keyframes itemSlideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.mutual-friends-popup-item:hover {
background: linear-gradient(
45deg,
rgba(255, 255, 255, 0.1),
rgba(255, 255, 255, 0.08)
);
border-color: rgba(255, 255, 255, 0.25);
transform: translateY(-2px) scale(1.01);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.mutual-friend-avatar {
width: 48px;
height: 48px;
background: linear-gradient(
45deg,
rgba(255, 255, 255, 0.1),
rgba(255, 255, 255, 0.08)
);
border: 2px solid rgba(255, 255, 255, 0.15);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
font-size: 20px;
flex-shrink: 0;
overflow: hidden;
transition: all 0.15s ease;
}
.mutual-friend-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
.mutual-friends-popup-item:hover .mutual-friend-avatar {
transform: scale(1.05);
border-color: rgba(255, 255, 255, 0.3);
}
.mutual-friend-name {
color: #ffffff;
font-family: "Source Sans Pro", Arial, sans-serif;
font-size: 16px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mutual-friends-loading {
display: flex;
align-items: center;
color: rgba(255, 255, 255, 0.8);
font-size: 16px;
font-family: "Source Sans Pro", Arial, sans-serif;
font-weight: 500;
}
.loading-spinner {
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.2);
border-top: 3px solid #ffffff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-right: 12px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.no-mutual-friends {
color: rgba(255, 255, 255, 0.6);
font-style: italic;
font-size: 16px;
font-family: "Source Sans Pro", Arial, sans-serif;
text-align: center;
padding: 20px;
}
.mutual-friends-popup-grid::-webkit-scrollbar {
width: 8px;
}
.mutual-friends-popup-grid::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.mutual-friends-popup-grid::-webkit-scrollbar-thumb {
background: linear-gradient(45deg, #555555, #666666);
border-radius: 4px;
}
.mutual-friends-popup-grid::-webkit-scrollbar-thumb:hover {
background: linear-gradient(45deg, #666666, #777777);
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
`;
document.head.appendChild(style);
const svgDefs = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svgDefs.style.width = '0';
svgDefs.style.height = '0';
svgDefs.style.position = 'absolute';
svgDefs.innerHTML = `<defs><linearGradient id="iconGradient" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#cccccc;stop-opacity:1" /><stop offset="100%" style="stop-color:#999999;stop-opacity:1" /></linearGradient></defs>`;
document.body.appendChild(svgDefs);
}
const container = document.createElement('div');
container.className = 'mutual-friends-container';
container.style.display = 'none';
const header = document.createElement('div');
header.className = 'mutual-friends-header';
header.innerHTML = `<svg class="mutual-friends-icon" viewBox="0 0 24 24"><path transform="translate(0,-5)" d="M17.25 20.5c1.281 0.719 2 1.906 1.875 3.125-0.063 0.75-0.031 0.75-1 0.875-0.594 0.063-4.375 0.094-8.219 0.094-4.375 0-8.938-0.031-9.281-0.125-1.281-0.344-0.531-2.719 1.156-3.844 1.344-0.844 4.063-2.156 4.813-2.313 1.031-0.219 1.156-0.875 0-2.844-0.25-0.469-0.531-1.813-0.563-3.25-0.031-2.313 0.375-3.875 2.406-4.656 0.375-0.125 0.813-0.188 1.219-0.188 1.344 0 2.594 0.75 3.125 1.844 0.719 1.469 0.375 5.313-0.375 6.719-0.906 1.594-0.813 2.094 0.188 2.344 0.625 0.156 2.688 1.125 4.656 2.219zM24.094 18.531c1 0.531 1.563 1.5 1.469 2.438-0.031 0.563-0.031 0.594-0.781 0.688-0.375 0.063-2.344 0.094-4.656 0.094-0.406-0.969-1.188-1.844-2.25-2.406-1.219-0.688-2.656-1.406-3.75-1.875 0.719-0.344 1.344-0.625 1.625-0.688 0.781-0.188 0.875-0.625 0-2.188-0.219-0.375-0.469-1.438-0.5-2.563-0.031-1.813 0.375-3.063 1.938-3.656 0.313-0.094 0.656-0.156 0.969-0.156 1.031 0 2 0.563 2.406 1.438 0.531 1.156 0.281 4.156-0.281 5.281-0.688 1.25-0.625 1.625 0.156 1.813 0.5 0.125 2.094 0.906 3.656 1.781z"/></svg>Mutual Friends`;
const content = document.createElement('div');
content.className = 'mutual-friends-content';
container.appendChild(header);
container.appendChild(content);
return container;
};
// Function to show loading state
const showMutualFriendsLoading = (contentElement) => {
contentElement.innerHTML = `<div class="mutual-friends-loading"><div class="loading-spinner"></div>Finding mutual friends...</div>`;
};
// Function to create mutual friends popup
const createMutualFriendsPopup = async (mutualFriends) => {
const overlay = document.createElement('div');
overlay.className = 'mutual-friends-overlay';
const popup = document.createElement('div');
popup.className = 'mutual-friends-popup';
const header = document.createElement('div');
header.className = 'mutual-friends-popup-header';
header.innerHTML = `<h3>All Mutual Friends (${mutualFriends.length})</h3><button class="mutual-friends-close">×</button>`;
const grid = document.createElement('div');
grid.className = 'mutual-friends-popup-grid';
const avatarMap = localAvatarCache;
mutualFriends.forEach(friend => {
const friendItem = document.createElement('div');
friendItem.className = 'mutual-friends-popup-item';
const avatarUrl = avatarMap[friend.id];
const avatarContent = avatarUrl ? `<img src="${avatarUrl}" alt="${friend.name}">` : '👤';
friendItem.innerHTML = `<div class="mutual-friend-avatar">${avatarContent}</div><span class="mutual-friend-name">${friend.name}</span>`;
friendItem.onclick = () => {
window.open(`https://www.roblox.com/users/${friend.id}/profile`, '_blank');
};
grid.appendChild(friendItem);
});
popup.appendChild(header);
popup.appendChild(grid);
overlay.appendChild(popup);
header.querySelector('.mutual-friends-close').onclick = () => {
overlay.style.animation = 'fadeOut 0.2s ease-out forwards';
setTimeout(() => overlay.remove(), 200);
};
return overlay;
};
// Function to display mutual friends
const displayMutualFriends = async (contentElement, mutualFriends) => {
contentElement.innerHTML = '';
if (mutualFriends.length === 0) {
contentElement.innerHTML = '<div class="no-mutual-friends">No mutual friends found. RoLocate by Oqarshi</div>';
return;
}
const header = contentElement.parentElement.querySelector('.mutual-friends-header');
const countBadge = document.createElement('span');
countBadge.className = 'mutual-friends-count';
countBadge.textContent = mutualFriends.length;
header.appendChild(countBadge);
const friendsList = document.createElement('div');
friendsList.className = 'mutual-friends-list';
const maxVisible = 6;
const friendsToShow = mutualFriends.slice(0, maxVisible);
friendsToShow.forEach(friend => {
const friendTag = document.createElement('div');
friendTag.className = 'mutual-friend-tag';
friendTag.textContent = friend.name;
friendTag.onclick = () => {
window.open(`https://www.roblox.com/users/${friend.id}/profile`, '_blank');
};
friendsList.appendChild(friendTag);
});
if (mutualFriends.length > maxVisible) {
const moreButton = document.createElement('div');
moreButton.className = 'mutual-friend-tag mutual-friends-more';
moreButton.textContent = `+${mutualFriends.length - maxVisible} more`;
moreButton.onclick = async () => {
const popup = await createMutualFriendsPopup(mutualFriends);
document.body.appendChild(popup);
};
friendsList.appendChild(moreButton);
}
contentElement.appendChild(friendsList);
};
// Function to find profile insertion point
const findProfileInsertionPoint = () => {
const profileHeader = document.querySelector('.profile-header-main');
if (profileHeader) return profileHeader.parentElement;
return document.querySelector('[class*="profile"]');
};
// Main execution logic
try {
const currentUserId = getCurrentUserId();
if (!currentUserId) return;
const urlMatch = window.location.pathname.match(/^\/(?:[a-z]{2}\/)?users\/(\d+)\/profile$/); // check if path name is right. if not then return
if (!urlMatch) return;
const otherUserId = urlMatch[1];
if (otherUserId === String(currentUserId)) return;
// Clear local cache for new page visit
localAvatarCache = {};
const mutualFriendsElement = createMutualFriendsElement();
const insertionPoint = findProfileInsertionPoint();
if (!insertionPoint) {
ConsoleLogEnabled('[Mutual Friends] Could not find suitable insertion point');
return;
}
insertionPoint.appendChild(mutualFriendsElement);
mutualFriendsElement.style.display = 'block';
const contentElement = mutualFriendsElement.querySelector('.mutual-friends-content');
showMutualFriendsLoading(contentElement);
const [currentUserFriends, otherUserFriends] = await Promise.all([
gmFetchFriends(currentUserId),
gmFetchFriends(otherUserId),
]);
if (!currentUserFriends || !otherUserFriends) {
contentElement.innerHTML = '<div class="no-mutual-friends">Failed to load friend data</div>';
return;
}
const mutualFriends = currentUserFriends.filter(currentFriend =>
otherUserFriends.some(otherFriend => otherFriend.id === currentFriend.id)
);
await fetchAndCacheAllAvatars(mutualFriends);
await displayMutualFriends(contentElement, mutualFriends);
} catch (error) {
ConsoleLogEnabled('[executeMutualFriendsFeature] Error occurred:', error);
}
}
/*******************************************************
name of function: manageRobloxChatBar
description: Disables roblox chat when ROLOCATE_disablechat is true
*******************************************************/
// kills roblox chat when ROLOCATE_disablechat is true
function manageRobloxChatBar() {
if (localStorage.getItem('ROLOCATE_disablechat') !== "true") return;
const CHAT_ID = 'chat-container';
let chatObserver = null;
// cleanup stuff so we dont leak memory
const cleanup_managechatbar = () => chatObserver?.disconnect();
// remove the chat bar
const removeChatBar = () => {
const chat = document.getElementById(CHAT_ID);
if (chat) {
chat.remove();
ConsoleLogEnabled('Roblox chat bar removed');
cleanup_managechatbar();
return true;
}
return false;
};
// try removing it right away
if (removeChatBar()) return;
// if not found yet, watch for it
chatObserver = new MutationObserver(mutations => {
for (const mutation of mutations) {
if (!mutation.addedNodes) continue;
for (const node of mutation.addedNodes) {
if (node.nodeType === 1 && (node.id === CHAT_ID || node.querySelector(`#${CHAT_ID}`))) {
if (removeChatBar()) return;
}
}
}
});
// start watching
document.body && chatObserver.observe(document.body, {
childList: true,
subtree: true
});
// give up after 30 seconds
const timeout = setTimeout(() => {
cleanup_managechatbar();
ConsoleLogEnabled('Chat removal observer timeout');
}, 30000);
// return cleanup function
return () => {
cleanup_managechatbar();
clearTimeout(timeout);
};
}
/*******************************************************
name of function: SmartSearch
description: Enhanced Smart Search with friend integration
*******************************************************/
function SmartSearch() {
if (localStorage.ROLOCATE_smartsearch !== "true") {
return;
}
const SMARTSEARCH_getCurrentUserId = () => Roblox?.CurrentUser?.userId || null;
// Friend list caching variables
let friendList = [];
let friendIdSet = new Set();
let friendListFetched = false;
let friendListFetching = false;
// Helper function to fetch friend list
async function fetchFriendList(userId) {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://friends.roblox.com/v1/users/${userId}/friends`,
headers: {
"Accept": "application/json"
},
onload: function(response) {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
resolve(data.data || []);
} catch (e) {
resolve([]);
}
} else {
resolve([]);
}
},
onerror: function() {
resolve([]);
}
});
});
}
// Helper function to check substring match (3+ consecutive characters)
function hasSubstringMatch(str, query) {
if (query.length < 3) return false;
return str.toLowerCase().includes(query.toLowerCase());
}
// helper function to chunk arrays for batch processing
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
// yea i dont even know hoiw this works but it works. thx google
function levenshteinDistance(a, b) {
const matrix = Array(b.length + 1).fill().map(() => Array(a.length + 1).fill(0));
for (let i = 0; i <= a.length; i++) matrix[0][i] = i;
for (let j = 0; j <= b.length; j++) matrix[j][0] = j;
for (let j = 1; j <= b.length; j++) {
for (let i = 1; i <= a.length; i++) {
const indicator = a[i - 1] === b[j - 1] ? 0 : 1;
matrix[j][i] = Math.min(
matrix[j][i - 1] + 1,
matrix[j - 1][i] + 1,
matrix[j - 1][i - 1] + indicator
);
}
}
return matrix[b.length][a.length];
}
function getSimilarityScore(str1, str2) {
ConsoleLogEnabled("Original strings:", {
str1,
str2
});
// Remove emojis using a general emoji regex and clean the string
const removeEmojisAndClean = (str) =>
str
.replace(/[\u{1F300}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu, '')
.toLowerCase()
.replace(/[^a-z0-9]/g, '');
const cleanStr1 = removeEmojisAndClean(str1);
const cleanStr2 = removeEmojisAndClean(str2);
ConsoleLogEnabled("Cleaned strings:", {
cleanStr1,
cleanStr2
});
if (cleanStr1.includes(cleanStr2) || cleanStr2.includes(cleanStr1)) {
ConsoleLogEnabled("One string includes the other.");
const longer = cleanStr1.length > cleanStr2.length ? cleanStr1 : cleanStr2;
const shorter = cleanStr1.length > cleanStr2.length ? cleanStr2 : cleanStr1;
ConsoleLogEnabled("Longer string:", longer);
ConsoleLogEnabled("Shorter string:", shorter);
let baseScore = 0.8 + (shorter.length / longer.length) * 0.15;
ConsoleLogEnabled("Base score (inclusion case):", baseScore);
if (cleanStr1 === cleanStr2) {
ConsoleLogEnabled("Exact match.");
return 1.0;
}
const result = Math.min(0.95, baseScore);
ConsoleLogEnabled("Inclusion final score:", result);
return result;
}
const maxLength = Math.max(cleanStr1.length, cleanStr2.length);
if (maxLength === 0) {
ConsoleLogEnabled("Both strings are empty after cleaning. Returning 1.");
return 1;
}
const distance = levenshteinDistance(cleanStr1, cleanStr2);
const levenshteinScore = 1 - (distance / maxLength);
ConsoleLogEnabled("Levenshtein distance:", distance);
ConsoleLogEnabled("Levenshtein score:", levenshteinScore);
const minLength = Math.min(cleanStr1.length, cleanStr2.length);
let substringBoost = 0;
let longestMatch = 0;
for (let i = 0; i < cleanStr1.length; i++) {
for (let j = 0; j < cleanStr2.length; j++) {
let k = 0;
while (
i + k < cleanStr1.length &&
j + k < cleanStr2.length &&
cleanStr1[i + k] === cleanStr2[j + k]
) {
k++;
}
if (k > longestMatch) {
longestMatch = k;
}
}
}
ConsoleLogEnabled("Longest matching substring length:", longestMatch);
if (longestMatch >= 3) {
substringBoost = (longestMatch / minLength) * 0.5;
ConsoleLogEnabled("Substring boost applied:", substringBoost);
} else {
ConsoleLogEnabled("No substring boost applied.");
}
const finalScore = Math.min(0.95, levenshteinScore + substringBoost);
ConsoleLogEnabled("Final similarity score:", finalScore);
return finalScore;
}
function formatNumberCount(num) {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M+';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K+';
} else {
return num.toString();
}
}
function formatDate(dateString) {
const date = new Date(dateString);
const options = {
year: 'numeric',
month: 'short',
day: 'numeric'
};
return date.toLocaleDateString('en-US', options);
}
/*******************************************************
Optimized thumbnail fetching functions
*******************************************************/
async function fetchGameIconsBatch(universeIds) {
if (!universeIds.length) return [];
const apiUrl = `https://thumbnails.roblox.com/v1/games/icons?universeIds=${universeIds.join(',')}&size=512x512&format=Png&isCircular=false`;
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url: apiUrl,
headers: {
"Accept": "application/json"
},
onload: function(response) {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
resolve(data.data || []);
} catch (error) {
resolve([]);
}
} else {
resolve([]);
}
},
onerror: function() {
resolve([]);
}
});
});
}
async function fetchPlayerThumbnailsBatch(userIds) {
if (!userIds.length) return [];
const params = new URLSearchParams({
userIds: userIds.join(","),
size: "150x150",
format: "Png",
isCircular: "false"
});
const url = `https://thumbnails.roblox.com/v1/users/avatar-headshot?${params.toString()}`;
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
headers: {
"Accept": "application/json"
},
onload: function(response) {
try {
if (response.status === 200) {
const data = JSON.parse(response.responseText);
resolve(data.data || []);
} else {
resolve([]);
}
} catch (error) {
resolve([]);
}
},
onerror: function() {
resolve([]);
}
});
});
}
async function fetchGroupIconsBatch(groupIds) {
if (!groupIds.length) return [];
const params = new URLSearchParams({
groupIds: groupIds.join(","),
size: "150x150",
format: "Png",
isCircular: "false"
});
const url = `https://thumbnails.roblox.com/v1/groups/icons?${params.toString()}`;
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
headers: {
"Accept": "application/json"
},
onload: function(response) {
try {
if (response.status === 200) {
const data = JSON.parse(response.responseText);
resolve(data.data || []);
} else {
resolve([]);
}
} catch (error) {
resolve([]);
}
},
onerror: function() {
resolve([]);
}
});
});
}
/*******************************************************
Search functions with dynamic loading
*******************************************************/
async function fetchGameSearchResults(query) {
const sessionId = Date.now();
const apiUrl = `https://apis.roblox.com/search-api/omni-search?searchQuery=${encodeURIComponent(query)}&pageToken=&sessionId=${sessionId}&pageType=all`;
contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_loading">Loading games...</div>';
try {
const response = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: apiUrl,
headers: {
"Accept": "application/json"
},
onload: resolve,
onerror: reject
});
});
if (response.status === 200) {
const data = JSON.parse(response.responseText);
const searchResults = data.searchResults || [];
const allGames = searchResults.map(result => result.contents[0]);
const gamesWithSimilarity = allGames.map(game => ({
...game,
similarity: getSimilarityScore(query, game.name)
}));
const sortedGames = gamesWithSimilarity.sort((a, b) => {
const similarityA = a.similarity;
const similarityB = b.similarity;
if ((similarityA >= 0.80 && similarityB >= 0.80) || Math.abs(similarityA - similarityB) < 0.0001) {
return b.playerCount - a.playerCount;
}
return similarityB - similarityA;
});
const games = sortedGames.slice(0, 30);
const activeTab = document.querySelector('.ROLOCATE_SMARTSEARCH_dropdown-tab.ROLOCATE_SMARTSEARCH_active')?.textContent;
if (activeTab !== "Games") return;
if (games.length === 0) {
contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_no-results">No results found</div>';
return;
}
// Render cards with play button
contentArea.innerHTML = games.map(game => `
<div class="ROLOCATE_SMARTSEARCH_game-card-container">
<a href="https://www.roblox.com/games/${game.rootPlaceId}" class="ROLOCATE_SMARTSEARCH_game-card-link" target="_self">
<div class="ROLOCATE_SMARTSEARCH_game-card">
<div class="ROLOCATE_SMARTSEARCH_thumbnail-loading" data-universe-id="${game.universeId}"></div>
<div class="ROLOCATE_SMARTSEARCH_game-info">
<h3 class="ROLOCATE_SMARTSEARCH_game-name">${game.name}</h3>
<p class="ROLOCATE_SMARTSEARCH_game-stats">
Players: ${formatNumberCount(game.playerCount)} |
<span class="ROLOCATE_SMARTSEARCH_thumbs-up">👍 ${formatNumberCount(game.totalUpVotes)}</span> |
<span class="ROLOCATE_SMARTSEARCH_thumbs-down">👎 ${formatNumberCount(game.totalDownVotes)}</span>
</p>
</div>
</div>
</a>
<button class="ROLOCATE_SMARTSEARCH_play-button"
data-place-id="${game.rootPlaceId}"
title="Quick Join">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 5V19L19 12L8 5Z" fill="#4CAF50"/>
</svg>
</button>
</div>
`).join('');
// Add event listeners to play buttons
setTimeout(() => {
document.querySelectorAll('.ROLOCATE_SMARTSEARCH_play-button').forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const placeId = this.getAttribute('data-place-id');
window.location.href = `https://www.roblox.com/games/${placeId}#?ROLOCATE_QUICKJOIN`;
});
});
}, 100);
// Load thumbnails in batches
const universeIds = games.map(game => game.universeId);
const thumbnailBatches = chunkArray(universeIds, 10);
for (const batch of thumbnailBatches) {
try {
const thumbnails = await fetchGameIconsBatch(batch);
thumbnails.forEach(thumb => {
const loadingElement = document.querySelector(`.ROLOCATE_SMARTSEARCH_thumbnail-loading[data-universe-id="${thumb.targetId}"]`);
if (loadingElement) {
loadingElement.outerHTML = `
<img src="${thumb.imageUrl}"
alt="${games.find(g => g.universeId == thumb.targetId)?.name || 'Game'}"
class="ROLOCATE_SMARTSEARCH_game-thumbnail">
`;
}
});
} catch (error) {
ConsoleLogEnabled('Error fetching game thumbnails:', error);
}
}
} else {
contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_error">Error loading results</div>';
}
} catch (error) {
ConsoleLogEnabled('Error in game search:', error);
contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_error">Error loading results</div>';
}
}
async function fetchUserSearchResults(query) {
const sessionId = Date.now();
const apiUrl = `https://apis.roblox.com/search-api/omni-search?verticalType=user&searchQuery=${encodeURIComponent(query)}&pageToken=&globalSessionId=${sessionId}&sessionId=${sessionId}`;
contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_loading">Loading users...</div>';
try {
const response = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: apiUrl,
headers: {
"Accept": "application/json"
},
onload: resolve,
onerror: reject
});
});
if (response.status === 200) {
const data = JSON.parse(response.responseText);
const userGroup = data.searchResults?.find(group => group.contentGroupType === "User");
const apiUsers = userGroup?.contents || [];
// Get current user ID for friend list
const currentUserId = SMARTSEARCH_getCurrentUserId();
// Fetch friend list if not already fetched
if (currentUserId && !friendListFetched && !friendListFetching) {
friendListFetching = true;
friendList = await fetchFriendList(currentUserId);
friendIdSet = new Set(friendList.map(friend => friend.id));
friendListFetched = true;
friendListFetching = false;
}
// Process friend matches
const matchedFriends = [];
if (query.length >= 3 && friendListFetched) {
friendList.forEach(friend => {
const nameMatch = hasSubstringMatch(friend.name, query);
const displayMatch = friend.displayName && hasSubstringMatch(friend.displayName, query);
if (nameMatch || displayMatch) {
matchedFriends.push({
contentId: friend.id,
username: friend.name,
displayName: friend.displayName || friend.name,
isFriend: true,
});
}
});
}
// using modern js magic here
let combinedResults = [
...apiUsers.map(user => ({
...user,
isFriend: friendIdSet.has(user.contentId),
})),
...matchedFriends.filter(friend =>
!apiUsers.some(u => u.contentId === friend.contentId)
)
];
// sort friends first, then others
combinedResults.sort((a, b) => {
if (a.isFriend && !b.isFriend) return -1;
if (!a.isFriend && b.isFriend) return 1;
return 0;
});
// Limit to 30 results
const users = combinedResults.slice(0, 30);
if (users.length === 0) {
contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_no-results">No users found</div>';
return;
}
// Render user cards
contentArea.innerHTML = users.map(user => `
<a href="https://www.roblox.com/users/${user.contentId}/profile" class="ROLOCATE_SMARTSEARCH_user-card-link" target="_self">
<div class="ROLOCATE_SMARTSEARCH_user-card">
<div class="ROLOCATE_SMARTSEARCH_thumbnail-loading" data-user-id="${user.contentId}"></div>
<div class="ROLOCATE_SMARTSEARCH_user-info">
<h3 class="ROLOCATE_SMARTSEARCH_user-display-name">${user.displayName || user.username}</h3>
<p class="ROLOCATE_SMARTSEARCH_user-username">
@${user.username}
${user.isFriend ? '<span class="ROLOCATE_SMARTSEARCH_friend-badge">Friend</span>' : ''}
</p>
</div>
</div>
</a>
`).join('');
// Load thumbnails
const userIds = users.map(user => user.contentId);
const thumbnailBatches = chunkArray(userIds, 10);
for (const batch of thumbnailBatches) {
try {
const thumbnails = await fetchPlayerThumbnailsBatch(batch);
thumbnails.forEach(thumb => {
const loadingElement = document.querySelector(`.ROLOCATE_SMARTSEARCH_thumbnail-loading[data-user-id="${thumb.targetId}"]`);
if (loadingElement) {
loadingElement.outerHTML = `
<img src="${thumb.imageUrl}"
alt="${users.find(u => u.contentId == thumb.targetId)?.username || 'User'}"
class="ROLOCATE_SMARTSEARCH_user-thumbnail">
`;
}
});
} catch (error) {
ConsoleLogEnabled('Error fetching user thumbnails:', error);
}
}
} else {
contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_error">Error loading user results</div>';
}
} catch (error) {
ConsoleLogEnabled('Error in user search:', error);
contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_error">Error loading user results</div>';
}
}
async function fetchGroupSearchResults(query) {
const apiUrl = `https://groups.roblox.com/v1/groups/search?cursor=&keyword=${encodeURIComponent(query)}&limit=25&prioritizeExactMatch=true&sortOrder=Asc`;
contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_loading">Loading groups...</div>';
try {
const response = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: apiUrl,
headers: {
"Accept": "application/json"
},
onload: resolve,
onerror: reject
});
});
if (response.status === 200) {
const data = JSON.parse(response.responseText);
const groups = data.data || [];
if (groups.length === 0) {
contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_no-results">No groups found</div>';
return;
}
// Render cards immediately with loading state
contentArea.innerHTML = groups.map(group => `
<a href="https://www.roblox.com/groups/${group.id}" class="ROLOCATE_SMARTSEARCH_group-card-link" target="_self">
<div class="ROLOCATE_SMARTSEARCH_group-card">
<div class="ROLOCATE_SMARTSEARCH_thumbnail-loading" data-group-id="${group.id}"></div>
<div class="ROLOCATE_SMARTSEARCH_group-info">
<h3 class="ROLOCATE_SMARTSEARCH_group-name">${group.name}</h3>
<p class="ROLOCATE_SMARTSEARCH_group-members">Members: ${formatNumberCount(group.memberCount)}</p>
<p class="ROLOCATE_SMARTSEARCH_group-created">Created: ${formatDate(group.created)}</p>
</div>
</div>
</a>
`).join('');
// Load thumbnails in batches
const groupIds = groups.map(group => group.id);
const thumbnailBatches = chunkArray(groupIds, 10);
for (const batch of thumbnailBatches) {
try {
const thumbnails = await fetchGroupIconsBatch(batch);
thumbnails.forEach(thumb => {
const loadingElement = document.querySelector(`.ROLOCATE_SMARTSEARCH_thumbnail-loading[data-group-id="${thumb.targetId}"]`);
if (loadingElement) {
loadingElement.outerHTML = `
<img src="${thumb.imageUrl}"
alt="${groups.find(g => g.id == thumb.targetId)?.name || 'Group'}"
class="ROLOCATE_SMARTSEARCH_group-thumbnail">
`;
}
});
} catch (error) {
ConsoleLogEnabled('Error fetching group thumbnails:', error);
}
}
} else {
contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_error">Error loading group results</div>';
}
} catch (error) {
ConsoleLogEnabled('Error in group search:', error);
contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_error">Error loading group results</div>';
}
}
const originalSearchContainer = document.querySelector('[data-testid="navigation-search-input"]');
if (!originalSearchContainer) {
ConsoleLogEnabled('Search container not found');
return false;
}
originalSearchContainer.remove();
const customSearchContainer = document.createElement('div');
customSearchContainer.className = 'navbar-left navbar-search col-xs-5 col-sm-6 col-md-2 col-lg-3 shown';
customSearchContainer.setAttribute('role', 'search');
customSearchContainer.style.marginTop = '4px';
customSearchContainer.style.position = 'relative';
const form = document.createElement('form');
form.name = 'custom-search-form';
form.addEventListener('submit', (e) => {
e.preventDefault();
const query = searchInput.value.trim();
if (!query) return;
const activeTab = document.querySelector('.ROLOCATE_SMARTSEARCH_dropdown-tab.ROLOCATE_SMARTSEARCH_active')?.dataset.tab;
let url = '';
switch (activeTab) {
case 'games':
url = `https://www.roblox.com/discover/?Keyword=${encodeURIComponent(query)}`;
break;
case 'users':
url = `https://www.roblox.com/search/users?keyword=${encodeURIComponent(query)}`;
break;
case 'groups':
url = `https://www.roblox.com/search/communities?keyword=${encodeURIComponent(query)}`;
break;
default:
url = `https://www.roblox.com/discover/?Keyword=${encodeURIComponent(query)}`;
break;
}
window.location.href = url;
});
const formWrapper = document.createElement('div');
formWrapper.className = 'ROLOCATE_SMARTSEARCH_form-has-feedback';
const searchInput = document.createElement('input');
let wasPreviouslyBlurred = true;
let lastInputValue = '';
searchInput.addEventListener('focus', () => {
if (wasPreviouslyBlurred) {
const activeTab = document.querySelector('.ROLOCATE_SMARTSEARCH_dropdown-tab.ROLOCATE_SMARTSEARCH_active')?.textContent || 'Unknown';
const typedText = searchInput.value.trim();
ConsoleLogEnabled(`[SmartSearch] Search bar focused | Tab: ${activeTab} | Input: "${typedText}"`);
wasPreviouslyBlurred = false;
}
});
searchInput.addEventListener('blur', () => {
wasPreviouslyBlurred = true;
});
searchInput.id = 'custom-navbar-search-input';
searchInput.type = 'search';
searchInput.className = 'form-control input-field ROLOCATE_SMARTSEARCH_custom-search-input';
searchInput.placeholder = 'SmartSearch | RoLocate by Oqarshi';
searchInput.maxLength = 120;
searchInput.autocomplete = 'off';
const searchIcon = document.createElement('span');
searchIcon.className = 'icon-common-search-sm ROLOCATE_SMARTSEARCH_custom-search-icon';
const dropdownMenu = document.createElement('div');
dropdownMenu.className = 'ROLOCATE_SMARTSEARCH_search-dropdown-menu';
dropdownMenu.style.display = 'none';
const navTabs = document.createElement('div');
navTabs.className = 'ROLOCATE_SMARTSEARCH_dropdown-nav-tabs';
const tabs = ['Games', 'Users', 'Groups'];
const tabButtons = [];
tabs.forEach((tabName, index) => {
const tabButton = document.createElement('button');
tabButton.className = `ROLOCATE_SMARTSEARCH_dropdown-tab ${index === 0 ? 'ROLOCATE_SMARTSEARCH_active' : ''}`;
tabButton.textContent = tabName;
tabButton.type = 'button';
tabButton.dataset.tab = tabName.toLowerCase();
tabButtons.push(tabButton);
navTabs.appendChild(tabButton);
});
const contentArea = document.createElement('div');
contentArea.className = 'ROLOCATE_SMARTSEARCH_dropdown-content';
contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_content-text">Quickly search for <strong>games</strong> above!</div>';
dropdownMenu.appendChild(navTabs);
dropdownMenu.appendChild(contentArea);
formWrapper.appendChild(searchInput);
formWrapper.appendChild(searchIcon);
form.appendChild(formWrapper);
customSearchContainer.appendChild(form);
customSearchContainer.appendChild(dropdownMenu);
let isMenuOpen = false;
searchInput.addEventListener('click', showDropdownMenu);
searchInput.addEventListener('focus', showDropdownMenu);
searchInput.addEventListener('input', function() {
const currentValue = this.value.trim();
if (currentValue && currentValue !== lastInputValue && !isMenuOpen) {
showDropdownMenu();
}
lastInputValue = currentValue;
});
tabButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
tabButtons.forEach(tab => tab.classList.remove('ROLOCATE_SMARTSEARCH_active'));
button.classList.add('ROLOCATE_SMARTSEARCH_active');
const query = searchInput.value.trim();
if (query) {
if (button.textContent === "Games") {
fetchGameSearchResults(query);
} else if (button.textContent === "Users") {
fetchUserSearchResults(query);
} else if (button.textContent === "Groups") {
fetchGroupSearchResults(query);
}
} else {
if (button.textContent === "Games") {
contentArea.innerHTML = `
<div class="ROLOCATE_SMARTSEARCH_content-text">
Quickly search for <strong>games</strong> above!
</div>
`;
} else if (button.textContent === "Users") {
contentArea.innerHTML = `
<div class="ROLOCATE_SMARTSEARCH_content-text">
Instantly find the <strong>user</strong> you're looking for!
</div>
`;
} else if (button.textContent === "Groups") {
contentArea.innerHTML = `
<div class="ROLOCATE_SMARTSEARCH_content-text">
Search for <strong>groups</strong> rapidly.
</div>
`;
}
}
});
});
document.addEventListener('click', (e) => {
if (!customSearchContainer.contains(e.target)) {
hideDropdownMenu();
}
});
dropdownMenu.addEventListener('click', (e) => {
e.stopPropagation();
});
function showDropdownMenu() {
isMenuOpen = true;
dropdownMenu.style.display = 'block';
formWrapper.classList.add('ROLOCATE_SMARTSEARCH_menu-open');
setTimeout(() => {
dropdownMenu.classList.add('ROLOCATE_SMARTSEARCH_show');
}, 10);
const activeTab = document.querySelector('.ROLOCATE_SMARTSEARCH_dropdown-tab.ROLOCATE_SMARTSEARCH_active')?.textContent;
const query = searchInput.value.trim();
if (query) {
if (activeTab === "Games" && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_game-card') === null && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_no-results') === null) {
fetchGameSearchResults(query);
} else if (activeTab === "Users" && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_user-card') === null && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_no-results') === null) {
fetchUserSearchResults(query);
} else if (activeTab === "Groups" && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_group-card') === null && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_no-results') === null) {
fetchGroupSearchResults(query);
}
}
}
function hideDropdownMenu() {
isMenuOpen = false;
dropdownMenu.classList.remove('ROLOCATE_SMARTSEARCH_show');
formWrapper.classList.remove('ROLOCATE_SMARTSEARCH_menu-open');
setTimeout(() => {
if (!isMenuOpen) {
dropdownMenu.style.display = 'none';
}
}, 200);
}
const rightNavigation = document.getElementById('right-navigation-header');
if (rightNavigation) {
rightNavigation.insertBefore(customSearchContainer, rightNavigation.firstChild);
}
let debounceTimeout;
searchInput.addEventListener('input', () => {
if (searchInput.value.trim() && !isMenuOpen) {
showDropdownMenu();
}
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(() => {
const query = searchInput.value.trim();
const activeTab = document.querySelector('.ROLOCATE_SMARTSEARCH_dropdown-tab.ROLOCATE_SMARTSEARCH_active')?.textContent;
if (!query) {
if (activeTab === "Games") {
contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_content-text">Quickly search for <strong>games</strong> above!</div>';
} else if (activeTab === "Users") {
contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_content-text">Instantly find the <strong>user</strong> you\'re looking for!</div>';
} else if (activeTab === "Groups") {
contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_content-text">Search for <strong>groups</strong> rapidly.</div>';
}
return;
}
if (activeTab === "Games") {
fetchGameSearchResults(query);
} else if (activeTab === "Users") {
fetchUserSearchResults(query);
} else if (activeTab === "Groups") {
fetchGroupSearchResults(query);
}
}, 250);
});
const style = document.createElement('style');
style.textContent = `
.ROLOCATE_SMARTSEARCH_form-has-feedback {
position: relative !important;
display: flex !important;
align-items: center !important;
border: 2px solid #2c2f36 !important;
border-radius: 8px !important;
background-color: #191a1f !important;
transition: all 0.3s ease !important;
z-index: 1000 !important;
}
.ROLOCATE_SMARTSEARCH_form-has-feedback:focus-within,
.ROLOCATE_SMARTSEARCH_form-has-feedback.ROLOCATE_SMARTSEARCH_menu-open {
border-color: #00b2ff !important;
}
.ROLOCATE_SMARTSEARCH_form-has-feedback.ROLOCATE_SMARTSEARCH_menu-open {
border-bottom-left-radius: 0 !important;
border-bottom-right-radius: 0 !important;
border-bottom-color: transparent !important;
position: relative !important;
}
.ROLOCATE_SMARTSEARCH_form-has-feedback.ROLOCATE_SMARTSEARCH_menu-open::after {
content: '' !important;
position: absolute !important;
bottom: -12px !important;
left: -2px !important;
right: -2px !important;
height: 12px !important;
border-left: 2px solid #00b2ff !important;
border-right: 2px solid #00b2ff !important;
background-color: transparent !important;
z-index: 1000 !important;
}
.ROLOCATE_SMARTSEARCH_custom-search-input {
width: 100% !important;
border: none !important;
background-color: transparent !important;
color: #ffffff !important;
padding: 8px 36px 8px 12px !important;
font-size: 16px !important;
height: 27px !important;
border-radius: 8px !important;
}
.ROLOCATE_SMARTSEARCH_custom-search-input:focus {
outline: none !important;
box-shadow: none !important;
}
.ROLOCATE_SMARTSEARCH_custom-search-input::placeholder {
color: #8a8d93 !important;
opacity: 1 !important;
}
.ROLOCATE_SMARTSEARCH_custom-search-icon {
position: absolute !important;
right: 10px !important;
top: 50% !important;
transform: translateY(-50%) !important;
pointer-events: none !important;
font-size: 14px !important;
color: #8a8d93 !important;
}
.ROLOCATE_SMARTSEARCH_form-has-feedback:focus-within .ROLOCATE_SMARTSEARCH_custom-search-icon,
.ROLOCATE_SMARTSEARCH_form-has-feedback.ROLOCATE_SMARTSEARCH_menu-open .ROLOCATE_SMARTSEARCH_custom-search-icon {
color: #00b2ff !important;
}
.ROLOCATE_SMARTSEARCH_search-dropdown-menu {
position: absolute !important;
top: calc(100% - 2px) !important;
left: 0 !important;
width: 100% !important;
background-color: #191a1f !important;
border-left: 2px solid #00b2ff !important;
border-right: 2px solid #00b2ff !important;
border-bottom: 2px solid #00b2ff !important;
border-top: none !important;
border-radius: 0 0 8px 8px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
z-index: 999 !important;
opacity: 0 !important;
transform: translateY(-10px) !important;
transition: all 0.2s ease !important;
box-sizing: border-box !important;
}
.ROLOCATE_SMARTSEARCH_search-dropdown-menu.ROLOCATE_SMARTSEARCH_show {
opacity: 1 !important;
transform: translateY(0) !important;
}
.ROLOCATE_SMARTSEARCH_dropdown-nav-tabs {
display: flex !important;
background-color: #1e2025 !important;
border-bottom: 1px solid #2c2f36 !important;
}
.ROLOCATE_SMARTSEARCH_dropdown-tab {
flex: 1 !important;
padding: 12px 16px !important;
background: none !important;
border: none !important;
color: #8a8d93 !important;
font-size: 14px !important;
font-weight: 500 !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
border-bottom: 2px solid transparent !important;
}
.ROLOCATE_SMARTSEARCH_dropdown-tab:hover {
color: #ffffff !important;
background-color: rgba(255, 255, 255, 0.05) !important;
}
.ROLOCATE_SMARTSEARCH_dropdown-tab.ROLOCATE_SMARTSEARCH_active {
color: #00b2ff !important;
border-bottom-color: #00b2ff !important;
background-color: rgba(0, 178, 255, 0.1) !important;
}
.ROLOCATE_SMARTSEARCH_dropdown-content {
padding: 10px !important;
max-height: 350px !important;
overflow-y: auto !important;
display: block !important;
}
.ROLOCATE_SMARTSEARCH_content-text {
color: #ffffff !important;
font-size: 16px !important;
text-align: center !important;
}
.ROLOCATE_SMARTSEARCH_content-text strong {
color: #00b2ff !important;
}
.navbar-left.navbar-search {
z-index: 1001 !important;
position: relative !important;
}
/* Game card styles with play button */
.ROLOCATE_SMARTSEARCH_game-card-container {
position: relative;
margin: 6px 0;
}
.ROLOCATE_SMARTSEARCH_game-card-link {
display: block;
text-decoration: none;
color: inherit;
}
.ROLOCATE_SMARTSEARCH_game-card {
display: flex;
align-items: center;
padding: 8px;
background-color: #1e2025;
border-radius: 8px;
transition: background-color 0.2s ease;
}
.ROLOCATE_SMARTSEARCH_game-card:hover {
background-color: #2c2f36;
}
.ROLOCATE_SMARTSEARCH_thumbnail-loading {
width: 50px;
height: 50px;
border-radius: 4px;
margin-right: 10px;
background-color: #2c2f36;
position: relative;
overflow: hidden;
}
.ROLOCATE_SMARTSEARCH_thumbnail-loading::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
animation: loading 1.5s infinite;
}
@keyframes loading {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.ROLOCATE_SMARTSEARCH_game-thumbnail {
width: 50px;
height: 50px;
border-radius: 4px;
margin-right: 10px;
object-fit: cover;
}
.ROLOCATE_SMARTSEARCH_game-info {
flex: 1;
overflow: hidden;
padding-right: 40px !important;
}
.ROLOCATE_SMARTSEARCH_game-name {
font-size: 14px;
color: #ffffff;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: calc(100% - 40px);
}
.ROLOCATE_SMARTSEARCH_game-stats {
font-size: 12px;
color: #8a8d93;
margin: 2px 0 0 0;
}
.ROLOCATE_SMARTSEARCH_thumbs-up {
color: #4caf50;
}
.ROLOCATE_SMARTSEARCH_thumbs-down {
color: #f44336;
}
/* Play button styles - square with rounded edges */
.ROLOCATE_SMARTSEARCH_play-button {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
width: 36px;
height: 36px;
border-radius: 6px;
background: rgba(76, 175, 80, 0.2);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
z-index: 2;
}
.ROLOCATE_SMARTSEARCH_play-button:hover {
background: rgba(76, 175, 80, 0.3);
transform: translateY(-50%) scale(1.05);
}
.ROLOCATE_SMARTSEARCH_play-button svg {
width: 18px;
height: 18px;
}
/* User card styles */
.ROLOCATE_SMARTSEARCH_user-card-link {
display: block;
text-decoration: none;
color: inherit;
}
.ROLOCATE_SMARTSEARCH_user-card {
display: flex;
align-items: center;
padding: 8px;
margin: 6px 0;
background-color: #1e2025;
border-radius: 8px;
transition: background-color 0.2s ease;
}
.ROLOCATE_SMARTSEARCH_user-card:hover {
background-color: #2c2f36;
}
.ROLOCATE_SMARTSEARCH_user-thumbnail {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 12px;
object-fit: cover;
}
.ROLOCATE_SMARTSEARCH_user-info {
flex: 1;
overflow: hidden;
}
.ROLOCATE_SMARTSEARCH_user-display-name {
font-size: 14px;
font-weight: 500;
color: #ffffff;
margin: 0 0 2px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ROLOCATE_SMARTSEARCH_user-username {
font-size: 12px;
color: #8a8d93;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Group card styles */
.ROLOCATE_SMARTSEARCH_group-card-link {
display: block;
text-decoration: none;
color: inherit;
}
.ROLOCATE_SMARTSEARCH_group-card {
display: flex;
align-items: center;
padding: 8px;
margin: 6px 0;
background-color: #1e2025;
border-radius: 8px;
transition: background-color 0.2s ease;
}
.ROLOCATE_SMARTSEARCH_group-card:hover {
background-color: #2c2f36;
}
.ROLOCATE_SMARTSEARCH_group-thumbnail {
width: 50px;
height: 50px;
border-radius: 4px;
margin-right: 12px;
object-fit: cover;
}
.ROLOCATE_SMARTSEARCH_group-info {
flex: 1;
overflow: hidden;
}
.ROLOCATE_SMARTSEARCH_group-name {
font-size: 14px;
font-weight: 500;
color: #ffffff;
margin: 0 0 4px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ROLOCATE_SMARTSEARCH_group-members {
font-size: 12px;
color: #8a8d93;
margin: 0 0 2px 0;
}
.ROLOCATE_SMARTSEARCH_group-created {
font-size: 11px;
color: #6d717a;
margin: 0;
}
/* Status messages */
.ROLOCATE_SMARTSEARCH_loading,
.ROLOCATE_SMARTSEARCH_no-results,
.ROLOCATE_SMARTSEARCH_error {
text-align: center;
color: #8a8d93;
padding: 20px;
font-size: 14px;
}
/* Friend badge styles */
.ROLOCATE_SMARTSEARCH_friend-badge {
display: inline-block;
background-color: #6b7280;
color: #ffffff;
font-size: 12px;
font-weight: 500;
padding: 3px 8px;
border-radius: 4px;
margin-left: 8px;
vertical-align: middle;
line-height: 1.2;
letter-spacing: 0.025em;
transform: translateY(-3px);
border: 1px solid #d1d5db;
}
`;
document.head.appendChild(style);
ConsoleLogEnabled('Enhanced search bar with friend integration added successfully!');
const urlParams = new URLSearchParams(window.location.search);
const keywordParam = urlParams.get('keyword') || urlParams.get('Keyword');
if (keywordParam) {
searchInput.value = decodeURIComponent(keywordParam);
if (window.location.href.includes('/search/users')) {
setActiveTab('users');
} else if (window.location.href.includes('/search/communities')) {
setActiveTab('groups');
} else {
setActiveTab('games');
}
}
function setActiveTab(tabKey) {
tabButtons.forEach(btn => {
if (btn.dataset.tab === tabKey) {
btn.classList.add('ROLOCATE_SMARTSEARCH_active');
if (btn.textContent === "Games") {
contentArea.innerHTML = `
<div class="ROLOCATE_SMARTSEARCH_content-text">
Quickly search for <strong>games</strong> above!
</div>
`;
} else if (btn.textContent === "Users") {
contentArea.innerHTML = `
<div class="ROLOCATE_SMARTSEARCH_content-text">
Instantly find the <strong>user</strong> you're looking for!
</div>
`;
} else if (btn.textContent === "Groups") {
contentArea.innerHTML = `
<div class="ROLOCATE_SMARTSEARCH_content-text">
Search for <strong>groups</strong> rapidly.
</div>
`;
}
} else {
btn.classList.remove('ROLOCATE_SMARTSEARCH_active');
}
});
}
return true;
}
/*******************************************************
name of function: quicklaunchgamesfunction
description: adds quick launch
*******************************************************/
function quicklaunchgamesfunction() {
if (!/^https?:\/\/(www\.)?roblox\.com(\/[a-z]{2})?\/home\/?$/i.test(window.location.href)) return;
if (localStorage.getItem('ROLOCATE_quicklaunchgames') === 'true') {
const observer = new MutationObserver((mutations, obs) => {
const friendsSection = document.querySelector('.friend-carousel-container');
const friendTiles = document.querySelectorAll('.friends-carousel-tile');
if (friendsSection && friendTiles.length > 1) {
obs.disconnect();
// Create new games section with premium styling
const newGamesContainer = document.createElement('div');
newGamesContainer.className = 'ROLOCATE_QUICKLAUNCHGAMES_new-games-container';
newGamesContainer.innerHTML = `
<div class="container-header people-list-header">
<div class="ROLOCATE_QUICKLAUNCHGAMES_header-content">
<div class="ROLOCATE_QUICKLAUNCHGAMES_title">Quick Launch Games</div>
<div class="ROLOCATE_QUICKLAUNCHGAMES_subtitle">Quickly play your games from here!</div>
</div>
</div>
<div class="ROLOCATE_QUICKLAUNCHGAMES_game-grid-container">
<div class="ROLOCATE_QUICKLAUNCHGAMES_game-grid">
<div class="ROLOCATE_QUICKLAUNCHGAMES_add-tile" id="ROLOCATE_QUICKLAUNCHGAMES_add-button">
<div class="ROLOCATE_QUICKLAUNCHGAMES_add-content">
<svg class="ROLOCATE_QUICKLAUNCHGAMES_add-icon" width="28" height="28" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 5V19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 12H19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div class="ROLOCATE_QUICKLAUNCHGAMES_add-text">Add Game</div>
</div>
</div>
</div>
</div>
`;
// CSS styles
const style = document.createElement('style');
style.textContent = `
.ROLOCATE_QUICKLAUNCHGAMES_new-games-container {
background: #1a1c23;
padding: 20px;
margin: 16px 0;
margin-bottom: 32px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
border-radius: 12px;
border: 1px solid #2a2a30;
}
.container-header.people-list-header {
margin-bottom: 18px;
}
.ROLOCATE_QUICKLAUNCHGAMES_header-content {
display: flex;
flex-direction: column;
gap: 4px;
}
.ROLOCATE_QUICKLAUNCHGAMES_title {
font-size: 22px !important;
font-weight: 700 !important;
color: #f7f8fa !important;
margin: 0 !important;
letter-spacing: -0.3px !important;
background: linear-gradient(to right, #8a9cff, #5d78ff) !important;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
}
.ROLOCATE_QUICKLAUNCHGAMES_subtitle {
font-size: 12px !important;
color: #a0a5b1 !important;
font-weight: 500 !important;
letter-spacing: 0.2px !important;
}
.ROLOCATE_QUICKLAUNCHGAMES_game-grid-container {
margin-top: 16px;
}
.ROLOCATE_QUICKLAUNCHGAMES_game-grid {
display: flex;
gap: 20px;
overflow-x: auto;
padding-bottom: 12px;
scrollbar-width: thin;
scrollbar-color: #5d78ff #2d2f36;
}
.ROLOCATE_QUICKLAUNCHGAMES_game-grid::-webkit-scrollbar {
height: 6px;
}
.ROLOCATE_QUICKLAUNCHGAMES_game-grid::-webkit-scrollbar-track {
background: #23252d;
border-radius: 3px;
}
.ROLOCATE_QUICKLAUNCHGAMES_game-grid::-webkit-scrollbar-thumb {
background: linear-gradient(to right, #5d78ff, #8a9cff);
border-radius: 3px;
}
.ROLOCATE_QUICKLAUNCHGAMES_game-grid::-webkit-scrollbar-thumb:hover {
background: linear-gradient(to right, #6d85ff, #9aabff);
}
.ROLOCATE_QUICKLAUNCHGAMES_add-tile {
flex: 0 0 auto;
width: 170px;
height: 230px;
background: linear-gradient(145deg, #23252d, #1e2028);
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25);
position: relative;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.ROLOCATE_QUICKLAUNCHGAMES_add-tile::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(93, 120, 255, 0.1), rgba(138, 156, 255, 0.05));
opacity: 0;
transition: opacity 0.3s ease;
}
.ROLOCATE_QUICKLAUNCHGAMES_add-tile:hover {
transform: translateY(4px) scale(1.03);
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.35);
}
.ROLOCATE_QUICKLAUNCHGAMES_add-tile:hover::before {
opacity: 1;
}
.ROLOCATE_QUICKLAUNCHGAMES_add-content {
text-align: center;
color: #8b8d94;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.ROLOCATE_QUICKLAUNCHGAMES_add-icon {
width: 32px;
height: 32px;
stroke-width: 2;
color: #5d78ff;
transition: all 0.3s ease;
}
.ROLOCATE_QUICKLAUNCHGAMES_add-tile:hover .ROLOCATE_QUICKLAUNCHGAMES_add-icon {
color: #8a9cff;
transform: scale(1.2) rotate(90deg);
}
.ROLOCATE_QUICKLAUNCHGAMES_add-text {
font-size: 15px;
font-weight: 600;
color: #d0d4e0;
letter-spacing: 0.3px;
}
.ROLOCATE_QUICKLAUNCHGAMES_game-tile {
flex: 0 0 auto;
width: 170px;
background: linear-gradient(145deg, #23252d, #1e2028);
border-radius: 14px;
overflow: hidden;
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.4s ease;
cursor: pointer;
position: relative;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.ROLOCATE_QUICKLAUNCHGAMES_game-tile:hover {
transform: translateY(-7px) scale(1.04);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
z-index: 10;
}
.ROLOCATE_QUICKLAUNCHGAMES_game-tile .thumbnail-container {
width: 100%;
height: 150px;
display: block;
position: relative;
overflow: hidden;
}
.ROLOCATE_QUICKLAUNCHGAMES_game-tile img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.6s ease;
}
.ROLOCATE_QUICKLAUNCHGAMES_game-tile:hover img {
transform: scale(1.12);
}
.ROLOCATE_QUICKLAUNCHGAMES_game-name {
padding: 14px 16px;
font-size: 14px;
font-weight: 600;
color: #f0f2f6;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
background: transparent;
position: relative;
z-index: 1;
}
.ROLOCATE_QUICKLAUNCHGAMES_game-info {
padding: 10px 16px;
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(28, 30, 38, 0.85);
position: relative;
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.ROLOCATE_QUICKLAUNCHGAMES_game-stat {
display: flex;
align-items: center;
font-size: 12px;
color: #b8b9bf;
gap: 4px;
font-weight: 500;
}
.ROLOCATE_QUICKLAUNCHGAMES_player-count::before {
content: "👤";
margin-right: 4px;
filter: drop-shadow(0 1px 1px rgba(0,0,0,0.3));
}
.ROLOCATE_QUICKLAUNCHGAMES_like-ratio {
display: flex;
align-items: center;
gap: 4px;
}
.ROLOCATE_QUICKLAUNCHGAMES_like-ratio .thumb {
font-size: 12px;
filter: drop-shadow(0 1px 1px rgba(0,0,0,0.3));
}
/* Premium X Button Styles */
.ROLOCATE_QUICKLAUNCHGAMES_remove-button {
position: absolute;
top: 10px;
right: 10px;
width: 26px;
height: 26px;
background: rgba(20, 22, 30, 0.85);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
z-index: 2;
border: 1px solid rgba(255,255,255,0.1);
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
}
.ROLOCATE_QUICKLAUNCHGAMES_remove-button::before,
.ROLOCATE_QUICKLAUNCHGAMES_remove-button::after {
content: '';
position: absolute;
width: 14px;
height: 2px;
background: #f0f2f6;
border-radius: 1px;
transition: all 0.2s ease;
}
.ROLOCATE_QUICKLAUNCHGAMES_remove-button::before {
transform: rotate(45deg);
}
.ROLOCATE_QUICKLAUNCHGAMES_remove-button::after {
transform: rotate(-45deg);
}
.ROLOCATE_QUICKLAUNCHGAMES_remove-button:hover {
background: rgba(255, 75, 66, 0.95);
transform: rotate(90deg) scale(1.1);
}
.ROLOCATE_QUICKLAUNCHGAMES_remove-button:hover::before,
.ROLOCATE_QUICKLAUNCHGAMES_remove-button:hover::after {
background: white;
}
.ROLOCATE_QUICKLAUNCHGAMES_game-tile:hover .ROLOCATE_QUICKLAUNCHGAMES_remove-button {
opacity: 1;
}
/* Animations */
@keyframes fadeIn {
to { opacity: 1; }
}
@keyframes popupIn {
to { transform: scale(1); opacity: 1; }
}
@keyframes popupOut {
to { transform: scale(0.9); opacity: 0; }
}
@keyframes tileAppear {
0% { transform: translateY(10px) scale(0.95); opacity: 0; }
100% { transform: translateY(0) scale(1); opacity: 1; }
}
@keyframes tileRemove {
0% { transform: translateY(0) scale(1); opacity: 1; }
50% { transform: translateY(-20px) scale(0.9); opacity: 0.5; }
100% { transform: translateY(40px) scale(0.8); opacity: 0; }
}
@keyframes buttonClick {
0% { transform: scale(1); }
50% { transform: scale(0.95); }
100% { transform: scale(1); }
}
@keyframes cancelButtonPulse {
0% { background: rgba(60, 64, 78, 0.5); }
50% { background: rgba(100, 104, 118, 0.7); }
100% { background: rgba(60, 64, 78, 0.5); }
}
@keyframes cancelButtonClick {
0% { transform: scale(1); }
50% { transform: scale(0.95); background: rgba(100, 104, 118, 0.8); }
100% { transform: scale(1); }
}
.ROLOCATE_QUICKLAUNCHGAMES_game-tile {
animation: tileAppear 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
.ROLOCATE_QUICKLAUNCHGAMES_game-tile.removing {
animation: tileRemove 0.4s cubic-bezier(0.55, 0.085, 0.68, 0.53) forwards;
pointer-events: none;
}
/* Popup Styles */
.ROLOCATE_QUICKLAUNCHGAMES_popup-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
opacity: 0;
animation: fadeIn 0.3s ease forwards;
}
.ROLOCATE_QUICKLAUNCHGAMES_popup {
background: linear-gradient(to bottom, #1f2128, #1a1c23);
border-radius: 18px;
padding: 32px;
width: 440px;
max-width: 90vw;
box-shadow: 0 40px 70px rgba(0, 0, 0, 0.7);
border: 1px solid rgba(255, 255, 255, 0.08);
transform: scale(0.9);
animation: popupIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
position: relative;
overflow: hidden;
}
.ROLOCATE_QUICKLAUNCHGAMES_popup::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(to right, #5d78ff, #8a9cff);
}
.ROLOCATE_QUICKLAUNCHGAMES_popup h3 {
color: #f7f8fa;
font-size: 22px;
font-weight: 700;
margin: 0 0 24px 0;
text-align: center;
letter-spacing: -0.3px;
}
.ROLOCATE_QUICKLAUNCHGAMES_popup label {
color: #a0a5b1;
font-size: 15px;
font-weight: 500;
display: block;
margin-bottom: 10px;
}
.ROLOCATE_QUICKLAUNCHGAMES_popup input {
width: 100%;
padding: 15px;
background: rgba(40, 42, 50, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
color: #f7f8fa;
font-size: 15px;
margin-bottom: 28px;
outline: none;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.ROLOCATE_QUICKLAUNCHGAMES_popup input::placeholder {
color: #6a6e7d;
}
.ROLOCATE_QUICKLAUNCHGAMES_popup input:focus {
border-color: #5d78ff;
box-shadow: 0 0 0 4px rgba(93, 120, 255, 0.25);
}
.ROLOCATE_QUICKLAUNCHGAMES_popup-buttons {
display: flex;
gap: 16px;
justify-content: flex-end;
}
.ROLOCATE_QUICKLAUNCHGAMES_popup-button {
padding: 14px 28px;
border: none;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
letter-spacing: 0.3px;
}
.ROLOCATE_QUICKLAUNCHGAMES_popup-button.cancel {
background: rgba(60, 64, 78, 0.5);
color: #d0d4e0;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.ROLOCATE_QUICKLAUNCHGAMES_popup-button.cancel:hover {
background: rgba(80, 84, 98, 0.7);
transform: translateY(-3px);
box-shadow: 0 6px 12px rgba(0,0,0,0.25);
animation: cancelButtonPulse 1.5s infinite;
}
.ROLOCATE_QUICKLAUNCHGAMES_popup-button.confirm {
background: linear-gradient(135deg, #5d78ff, #8a9cff);
color: white;
box-shadow: 0 6px 16px rgba(93, 120, 255, 0.4);
}
.ROLOCATE_QUICKLAUNCHGAMES_popup-button.confirm:hover {
background: linear-gradient(135deg, #6d85ff, #9aabff);
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(93, 120, 255, 0.5);
}
.ROLOCATE_QUICKLAUNCHGAMES_popup-button:active {
transform: translateY(1px);
}
.ROLOCATE_QUICKLAUNCHGAMES_popup-button.cancel:active {
animation: cancelButtonClick 0.3s ease;
background: rgba(80, 84, 98, 0.8) !important;
}
@keyframes popupFadeOut {
0% {
transform: scale(1);
opacity: 1;
}
100% {
transform: scale(0.95);
opacity: 0;
}
}
.ROLOCATE_QUICKLAUNCHGAMES_popup.fade-out {
animation: popupFadeOut 0.3s ease forwards;
}
.ROLOCATE_QUICKLAUNCHGAMES_add-tile:active {
transform: translateY(2px) scale(0.97) !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2) !important;
}
.ROLOCATE_QUICKLAUNCHGAMES_add-tile.clicked {
animation: buttonClick 0.3s ease;
}
`;
document.head.appendChild(style);
// Insert after friends section
friendsSection.parentNode.insertBefore(newGamesContainer, friendsSection.nextSibling);
// Add game functions
function getUniverseIdFromPlaceId_quicklaunch(placeId) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://games.roblox.com/v1/games/multiget-place-details?placeIds=${placeId}`,
headers: {
"Accept": "application/json"
},
onload: function(response) {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
if (Array.isArray(data) && data.length > 0 && data[0].universeId) {
resolve(data[0].universeId);
} else {
reject(new Error("Universe ID not found"));
}
} catch (e) {
reject(e);
}
} else {
reject(new Error(`HTTP error: ${response.status}`));
}
},
onerror: function(err) {
reject(err);
}
});
});
}
function getGameIconFromUniverseId_quicklaunch(universeId) {
return new Promise((resolve, reject) => {
const apiUrl = `https://thumbnails.roblox.com/v1/games/icons?universeIds=${universeId}&size=512x512&format=Png&isCircular=false`;
GM_xmlhttpRequest({
method: "GET",
url: apiUrl,
headers: {
"Accept": "application/json"
},
onload: function(response) {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
if (data.data && data.data.length > 0 && data.data[0].imageUrl) {
resolve(data.data[0].imageUrl);
} else {
reject(new Error("Image URL not found"));
}
} catch (err) {
reject(err);
}
} else {
reject(new Error(`HTTP error: ${response.status}`));
}
},
onerror: function(err) {
reject(err);
}
});
});
}
async function getGameDetails(universeId) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://games.roblox.com/v1/games?universeIds=${universeId}`,
headers: {
"Accept": "application/json"
},
onload: function(response) {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
if (data.data && data.data.length > 0) {
resolve(data.data[0]);
} else {
reject(new Error("Game data not found"));
}
} catch (e) {
reject(e);
}
} else {
reject(new Error(`HTTP error: ${response.status}`));
}
},
onerror: function(err) {
reject(err);
}
});
});
}
function formatNumber(num) {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return num;
}
// Show add game popup
function showAddGamePopup() {
const existingGames = document.querySelectorAll('.ROLOCATE_QUICKLAUNCHGAMES_game-tile').length;
if (existingGames >= 10) {
notifications('Maximum 10 games allowed', 'error', '⚠️', '4000');
return;
}
// Add click animation to add button
const addButton = document.getElementById('ROLOCATE_QUICKLAUNCHGAMES_add-button');
addButton.classList.add('clicked');
setTimeout(() => {
addButton.classList.remove('clicked');
}, 300);
const overlay = document.createElement('div');
overlay.className = 'ROLOCATE_QUICKLAUNCHGAMES_popup-overlay';
overlay.innerHTML = `
<div class="ROLOCATE_QUICKLAUNCHGAMES_popup">
<h3>Add New Game</h3>
<label for="gameIdInput">Game ID:</label>
<input type="text" id="gameIdInput" placeholder="Enter game ID | RoLocate by Oqarshi">
<small style="display:block; margin-top:4px; color:#aaa;">
Example: roblox.com/games/<b style="color:#4da6ff;">17625359962</b>/RIVALS
</small>
<div class="ROLOCATE_QUICKLAUNCHGAMES_popup-buttons" style="margin-top:12px;">
<button class="ROLOCATE_QUICKLAUNCHGAMES_popup-button cancel">Cancel</button>
<button class="ROLOCATE_QUICKLAUNCHGAMES_popup-button confirm">Add Game</button>
</div>
</div>
`;
document.body.appendChild(overlay);
setTimeout(() => {
document.getElementById('gameIdInput').focus();
}, 100);
// Event listeners
const cancelBtn = overlay.querySelector('.cancel');
const confirmBtn = overlay.querySelector('.confirm');
cancelBtn.addEventListener('click', () => {
overlay.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_popup').classList.add('fade-out');
setTimeout(() => overlay.remove(), 300);
});
confirmBtn.addEventListener('click', async () => {
const gameId = document.getElementById('gameIdInput').value.trim();
if (!gameId) {
notifications('Please enter a game ID', 'error', '⚠️', '4000');
return;
}
if (!/^\d+$/.test(gameId)) {
notifications('Game ID must be numeric', 'error', '⚠️', '4000');
return;
}
const games = JSON.parse(localStorage.getItem('ROLOCATE_quicklaunch_games_storage') || '[]');
if (games.includes(gameId)) {
notifications('Game already added!', 'error', '⚠️', '4000');
return;
}
// Show loading state
confirmBtn.textContent = 'Adding...';
confirmBtn.disabled = true;
try {
// Get game details
const universeId = await getUniverseIdFromPlaceId_quicklaunch(gameId);
const gameDetails = await getGameDetails(universeId);
games.push(gameId);
localStorage.setItem('ROLOCATE_quicklaunch_games_storage', JSON.stringify(games));
addGameTile(gameId, gameDetails);
// Only fade out on success
overlay.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_popup').classList.add('fade-out');
setTimeout(() => overlay.remove(), 300);
} catch (error) {
notifications('Error adding game: ' + error.message, 'error', '⚠️', '4000');
confirmBtn.textContent = 'Add Game';
confirmBtn.disabled = false;
}
// Remove these two lines - they were causing the problem
});
}
// Add game tile with animations and API data
function addGameTile(gameId, gameDetails = null) {
const gameGrid = document.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_game-grid');
if (!gameGrid) return;
const gameTile = document.createElement('div');
gameTile.className = 'ROLOCATE_QUICKLAUNCHGAMES_game-tile';
gameTile.dataset.gameId = gameId;
// Create tile with placeholder content
gameTile.innerHTML = `
<a href="https://www.roblox.com/games/${gameId}#?ROLOCATE_QUICKJOIN" target="_blank">
<div class="thumbnail-container">
<div style="width:100%;height:100%;background:linear-gradient(135deg,#23252d,#1e2028);display:flex;align-items:center;justify-content:center;">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 8H20V16H4V8Z" stroke="#4a4d56" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 4V8" stroke="#4a4d56" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 4V8" stroke="#4a4d56" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</div>
<div class="ROLOCATE_QUICKLAUNCHGAMES_game-name">Loading...</div>
<div class="ROLOCATE_QUICKLAUNCHGAMES_game-info">
<div class="ROLOCATE_QUICKLAUNCHGAMES_like-ratio">
<span class="thumb">👍</span> -
</div>
<div class="ROLOCATE_QUICKLAUNCHGAMES_game-stat ROLOCATE_QUICKLAUNCHGAMES_player-count">-</div>
</div>
</a>
<div class="ROLOCATE_QUICKLAUNCHGAMES_remove-button"></div>
`;
gameGrid.insertBefore(gameTile, gameGrid.firstChild);
// Add remove functionality
const removeBtn = gameTile.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_remove-button');
removeBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
// Animated removal with bounce effect
gameTile.classList.add('removing');
setTimeout(() => {
const games = JSON.parse(localStorage.getItem('ROLOCATE_quicklaunch_games_storage') || '[]');
const updatedGames = games.filter(id => id !== gameId);
localStorage.setItem('ROLOCATE_quicklaunch_games_storage', JSON.stringify(updatedGames));
gameTile.remove();
}, 400);
});
// Load game details asynchronously
const loadGameDetails = async () => {
try {
const universeId = await getUniverseIdFromPlaceId_quicklaunch(gameId);
const [iconUrl, details] = await Promise.all([
getGameIconFromUniverseId_quicklaunch(universeId),
gameDetails || getGameDetails(universeId)
]);
// Update thumbnail
const thumbContainer = gameTile.querySelector('.thumbnail-container');
thumbContainer.innerHTML = `<img src="${iconUrl}" alt="${details.name}" onerror="this.src='https://via.placeholder.com/160x160?text=No+Image'">`;
// Update game name
const gameName = gameTile.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_game-name');
gameName.textContent = details.name || 'Unknown Game';
// Update stats
const playerCount = gameTile.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_player-count');
const likeRatio = gameTile.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_like-ratio');
playerCount.textContent = formatNumber(details.playing);
// Calculate like ratio (using favorites as proxy)
const ratio = details.favoritedCount > 0 ?
Math.round((details.favoritedCount / (details.favoritedCount + (details.favoritedCount * 0.1))) * 100) : 0;
likeRatio.innerHTML = `<span class="thumb">👍</span> ${ratio}%`;
} catch (error) {
ConsoleLogEnabled('Error loading game details:', error);
const playerCount = gameTile.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_player-count');
playerCount.textContent = 'Error';
}
};
loadGameDetails();
}
// Add event to add button
const addButton = document.getElementById('ROLOCATE_QUICKLAUNCHGAMES_add-button');
addButton.addEventListener('click', showAddGamePopup);
addButton.addEventListener('mousedown', function() {
this.classList.add('active');
});
addButton.addEventListener('mouseup', function() {
this.classList.remove('active');
});
addButton.addEventListener('mouseleave', function() {
this.classList.remove('active');
});
// Load saved games
function loadSavedGames() {
const savedGames = JSON.parse(localStorage.getItem('ROLOCATE_quicklaunch_games_storage') || '[]');
savedGames.forEach(gameId => {
addGameTile(gameId);
});
}
// Initial load
setTimeout(loadSavedGames, 100);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
setTimeout(() => {
observer.disconnect();
if (!document.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_new-games-container')) {
quicklaunchgamesfunction();
}
}, 5000);
}
}
/*******************************************************
name of function: betterfriends
description: betterfriends and yea
*******************************************************/
// make sure to remove ROLOCATE_checkBestFriendsStatus();
// WARNING: Do not republish this script. Licensed for personal use only.
function betterfriends() {
// check if in right url
if (!/^https?:\/\/(www\.)?roblox\.com(\/[a-z]{2})?\/home\/?$/i.test(window.location.href)) return;
// check localStorage
if (localStorage.getItem('ROLOCATE_betterfriends') !== 'true') {
return;
}
// global state management vars
let dropdownObserver = null;
let avatarObserver = null;
let mainObserver = null;
let observerTimeout = null;
let isStylesAdded = false;
let bestFriendsButtonObserver = null;
let localAvatarCache = {};
// class names for styling
const CLASSES = {
STYLES_ID: 'ROLOCATE_friend-status-styles',
STATUS_ONLINE: 'ROLOCATE_friend-status-online',
STATUS_GAME: 'ROLOCATE_friend-status-game',
STATUS_OFFLINE: 'ROLOCATE_friend-status-offline',
STATUS_OTHER: 'ROLOCATE_friend-status-other',
DROPDOWN_STYLED: 'ROLOCATE_dropdown-styled',
TILE_STYLED: 'ROLOCATE_tile-styled',
BEST_FRIENDS_BUTTON: 'ROLOCATE_best-friends-button',
BEST_FRIEND_STAR: 'ROLOCATE-best-friend-star'
};
const addStatusStyles = () => {
if (isStylesAdded || document.getElementById(CLASSES.STYLES_ID)) return;
const styleSheet = document.createElement('style');
styleSheet.id = CLASSES.STYLES_ID; // save space
styleSheet.textContent = `
.${CLASSES.STATUS_ONLINE},
.${CLASSES.STATUS_GAME},
.${CLASSES.STATUS_OFFLINE},
.${CLASSES.STATUS_OTHER} {
border: 4px solid !important;
border-radius: 50% !important;
}
.${CLASSES.STATUS_ONLINE} { border-color: #00a2ff !important; }
.${CLASSES.STATUS_GAME} { border-color: #02b757 !important; }
.${CLASSES.STATUS_OFFLINE}{ border-color: #6b7280 !important; }
.${CLASSES.STATUS_OTHER} { border-color: #f68802 !important; }
.friend-tile-dropdown {
background: #1a1c23 !important;
border: 1px solid rgba(148, 163, 184, 0.2) !important;
border-radius: 8px !important;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important;
overflow: hidden !important;
}
.friend-tile-dropdown {
transition: opacity 0.15s ease, transform 0.15s ease !important;
}
.friend-tile-dropdown ul {
padding: 8px !important;
margin: 0 !important;
list-style: none !important;
}
.friend-tile-dropdown li {
margin: 0 !important;
padding: 0 !important;
}
.friend-tile-dropdown-button {
width: 100% !important;
padding: 10px 14px !important;
background: transparent !important;
border: none !important;
border-radius: 6px !important;
color: #e2e8f0 !important;
font-size: 14px !important;
font-weight: 500 !important;
text-align: left !important;
cursor: pointer !important;
display: flex !important;
align-items: center !important;
gap: 10px !important;
transition: background-color 0.15s ease !important;
}
.friend-tile-dropdown-button:hover {
background: rgba(37, 99, 235, 0.08) !important;
}
.friend-tile-dropdown-button:active {
background: rgba(37, 99, 235, 0.15) !important;
}
.friend-tile-dropdown-button .icon {
flex-shrink: 0 !important;
}
.${CLASSES.BEST_FRIENDS_BUTTON} {
background: transparent !important;
border: 1px solid #2563eb !important;
border-radius: 6px !important;
color: #3b82f6 !important;
font-size: 13px !important;
font-weight: 500 !important;
padding: 6px 12px !important;
cursor: pointer !important;
display: inline-flex !important;
align-items: center !important;
gap: 6px !important;
transition: background-color 0.15s ease, border-color 0.15s ease !important;
margin-left: 12px !important;
margin-top: -2px !important;
text-decoration: none !important;
}
.${CLASSES.BEST_FRIENDS_BUTTON}:hover {
background: rgba(37, 99, 235, 0.08) !important;
border-color: #3b82f6 !important;
}
.${CLASSES.BEST_FRIENDS_BUTTON}:active {
background: rgba(37, 99, 235, 0.15) !important;
}
.${CLASSES.BEST_FRIENDS_BUTTON} svg {
width: 14px !important;
height: 14px !important;
flex-shrink: 0 !important;
}
/* BEST FRIENDS POPUP STYLES */
.best-friends-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.best-friends-popup {
background: linear-gradient(135deg, #111114 0%, #1a1a1d 100%);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 16px;
width: 90%;
max-width: 700px;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
animation: popupSlideIn 0.2s ease-out;
}
@keyframes popupSlideIn {
from {
opacity: 0;
transform: scale(0.95) translateY(20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.best-friends-popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.best-friends-popup-header h3 {
color: #ffffff;
margin: 0;
font-family: "Source Sans Pro", Arial, sans-serif;
font-size: 20px;
font-weight: 700;
}
.best-friends-close {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #ffffff;
font-size: 20px;
cursor: pointer;
padding: 8px;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.15s ease;
}
.best-friends-close:hover {
background: rgba(255, 59, 59, 0.2);
border-color: rgba(255, 59, 59, 0.4);
transform: rotate(90deg);
}
.best-friends-popup-grid {
padding: 24px;
max-height: 60vh;
overflow-y: auto;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
}
.best-friends-popup-item {
display: flex;
align-items: center;
padding: 16px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
cursor: pointer;
transition: all 0.15s ease;
animation: itemSlideIn 0.2s ease-out backwards;
position: relative;
}
@keyframes itemSlideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.best-friends-popup-item:hover {
background: linear-gradient(
45deg,
rgba(255, 255, 255, 0.1),
rgba(255, 255, 255, 0.08)
);
border-color: rgba(255, 255, 255, 0.25);
transform: translateY(-2px) scale(1.01);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.best-friend-avatar {
width: 48px;
height: 48px;
border: 2px solid rgba(255, 255, 255, 0.15);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
font-size: 20px;
flex-shrink: 0;
overflow: hidden;
transition: all 0.15s ease;
}
.best-friend-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
.best-friends-popup-item:hover .best-friend-avatar {
transform: scale(1.05);
border-color: rgba(255, 255, 255, 0.3);
}
.best-friend-name {
color: #ffffff;
font-family: "Source Sans Pro", Arial, sans-serif;
font-size: 16px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-grow: 1;
}
.${CLASSES.BEST_FRIEND_STAR} {
position: absolute;
top: 8px;
right: 8px;
width: 26px;
height: 26px;
color: #ffd700;
fill: currentColor;
filter: drop-shadow(0 0 8px rgba(255, 215, 0, 0.6))
drop-shadow(0 2px 4px rgba(0, 0, 0, 0.8));
animation: starGlow 2s ease-in-out infinite alternate;
opacity: 0;
transform: scale(0.8);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.${CLASSES.BEST_FRIEND_STAR}.star-visible {
opacity: 1;
transform: scale(1);
}
.${CLASSES.BEST_FRIEND_STAR}:hover {
transform: scale(1.1);
filter: drop-shadow(0 0 12px rgba(255, 215, 0, 0.8))
drop-shadow(0 2px 6px rgba(0, 0, 0, 0.9));
}
@keyframes starGlow {
0% {
filter: drop-shadow(0 0 8px rgba(255, 215, 0, 0.6))
drop-shadow(0 2px 4px rgba(0, 0, 0, 0.8));
}
100% {
filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.9))
drop-shadow(0 2px 4px rgba(0, 0, 0, 0.8));
}
}
.best-friends-loading {
display: flex;
align-items: center;
color: rgba(255, 255, 255, 0.8);
font-size: 16px;
font-family: "Source Sans Pro", Arial, sans-serif;
font-weight: 500;
}
.loading-spinner {
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.2);
border-top: 3px solid #ffffff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-right: 12px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.no-best-friends {
color: rgba(255, 255, 255, 0.6);
font-style: italic;
font-size: 16px;
font-family: "Source Sans Pro", Arial, sans-serif;
text-align: center;
padding: 20px;
}
.best-friends-popup-grid::-webkit-scrollbar {
width: 8px;
}
.best-friends-popup-grid::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.best-friends-popup-grid::-webkit-scrollbar-thumb {
background: linear-gradient(45deg, #555555, #666666);
border-radius: 4px;
}
.best-friends-popup-grid::-webkit-scrollbar-thumb:hover {
background: linear-gradient(45deg, #666666, #777777);
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.best-friends-search-container {
border: 2px solid #2563eb;
border-radius: 8px;
flex: 1;
margin: 0 20px;
}
.best-friends-search {
width: 100%;
padding: 10px 15px;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: white;
font-size: 14px;
outline: none;
}
`;
document.head.appendChild(styleSheet);
isStylesAdded = true;
};
// create best friends section
const createBestFriendsSection = () => {
const existingBestFriendsSection = document.querySelector('.best-friends-section');
if (existingBestFriendsSection) return;
const friendsContainer = document.querySelector('.friend-carousel-container');
if (!friendsContainer) return;
const bestFriends = getBestFriends();
if (bestFriends.size === 0) return;
// Create best friends section
const bestFriendsSection = document.createElement('div');
bestFriendsSection.className = 'best-friends-section';
bestFriendsSection.style.cssText = `
background-color: #1a1c23;
border-radius: 12px;
border: 1px solid #2a2a30;
padding: 12px;
box-sizing: border-box;
margin: 0 0 16px 0;
`;
// Create header
const headerDiv = document.createElement('div');
headerDiv.className = 'container-header people-list-header';
headerDiv.style.cssText = `
display: flex;
align-items: center;
margin-bottom: 12px;
`;
const headerTitle = document.createElement('h2');
headerTitle.textContent = 'Best Friends';
headerTitle.style.cssText = `
color: #ffffff;
font-size: 18px;
font-weight: 600;
margin: 0;
font-family: "Source Sans Pro", Arial, sans-serif;
`;
headerDiv.appendChild(headerTitle);
// Create carousel container
const carouselContainer = document.createElement('div');
carouselContainer.className = 'friends-carousel-container';
carouselContainer.style.cssText = `
background: transparent;
border: none;
padding: 0;
margin: 0;
`;
// Create carousel
const carousel = document.createElement('div');
carousel.className = 'friends-carousel';
carousel.style.cssText = `
display: flex;
gap: 12px;
overflow-x: auto;
padding: 4px;
`;
bestFriendsSection.appendChild(headerDiv);
carouselContainer.appendChild(carousel);
bestFriendsSection.appendChild(carouselContainer);
// Insert before regular friends section
friendsContainer.parentNode.insertBefore(bestFriendsSection, friendsContainer);
// Populate with best friends
populateBestFriendsSection();
};
const populateBestFriendsSection = async () => {
const bestFriendsCarousel = document.querySelector('.best-friends-section .friends-carousel');
if (!bestFriendsCarousel) return;
const bestFriends = getBestFriends();
if (bestFriends.size === 0) return;
bestFriendsCarousel.innerHTML = '';
try {
const currentUserId = Roblox?.CurrentUser?.userId;
if (!currentUserId) return;
const allFriends = await gmFetchFriends(currentUserId);
if (!allFriends) return;
const onlineFriends = await ROLOCATE_fetchOnlineFriends(currentUserId);
const onlineStatusMap = {};
onlineFriends.forEach(friend => {
const presence = friend.userPresence;
if (presence.UserPresenceType === 'Online') {
onlineStatusMap[friend.id] = 'online';
} else if (presence.UserPresenceType === 'InGame') {
onlineStatusMap[friend.id] = 'game';
} else {
onlineStatusMap[friend.id] = 'other';
}
});
// Filter and sort best friends - online/game first
const bestFriendsList = allFriends
.filter(friend => bestFriends.has(friend.id))
.sort((a, b) => {
const aStatus = onlineStatusMap[a.id] || 'offline';
const bStatus = onlineStatusMap[b.id] || 'offline';
// priority: game > online > other (studio) > offline
const priority = {
'game': 3,
'other': 2,
'online': 1,
'offline': 0
};
return priority[bStatus] - priority[aStatus];
});
if (bestFriendsList.length === 0) return;
const friendIds = bestFriendsList.map(friend => friend.id);
const avatarMap = await fetchUserAvatars(friendIds);
bestFriendsList.forEach(friend => {
const tile = createBestFriendTile(friend, avatarMap[friend.id]);
const status = onlineStatusMap[friend.id] || 'offline';
// Add hover functionality only if online/offline (not ingame)
if (status === 'online' || status === 'offline') {
tile.classList.add('ROLOCATE_hover-enabled');
}
const statusIcon = tile.querySelector('[data-testid="presence-icon"]');
if (statusIcon) {
statusIcon.className = '';
statusIcon.classList.add(`icon-${status}`);
const statusTitles = {
'online': 'Online',
'other': 'In Studio', // other is studio
'game': 'In Game',
'offline': 'Offline'
};
statusIcon.setAttribute('title', statusTitles[status]);
const statusColors = {
'online': '#00a2ff',
'other': '#f68802',
'game': '#02b757',
'offline': '#6b7280'
};
statusIcon.style.background = statusColors[status];
}
bestFriendsCarousel.appendChild(tile);
});
setTimeout(() => applyFriendStatusStyling(), 100);
} catch (error) {
ConsoleLogEnabled('[populateBestFriendsSection] Error:', error);
}
};
// remove best friends from regular friends section
const removeBestFriendsFromRegularSection = () => {
const bestFriends = getBestFriends();
if (bestFriends.size === 0) return;
const regularFriendsTiles = document.querySelectorAll('.friend-carousel-container:not(.best-friends-section .friends-carousel-container) .friends-carousel-tile');
regularFriendsTiles.forEach(tile => {
const nameElement = tile.querySelector('.friend-name');
if (!nameElement) return;
// Try to find friend ID from tile (you might need to adjust this based on how friend IDs are stored)
const profileLink = tile.querySelector('a[href*="/users/"]');
if (profileLink) {
const match = profileLink.href.match(/\/users\/(\d+)/);
if (match) {
const friendId = parseInt(match[1]);
if (bestFriends.has(friendId)) {
tile.style.display = 'none';
}
}
}
});
};
// create individual best friend tile
const createBestFriendTile = (friend, avatarUrl) => {
const tile = document.createElement('div');
tile.className = 'friends-carousel-tile';
tile.style.cssText = `
flex: 0 0 auto;
width: 100px;
text-align: center;
cursor: pointer;
padding: 8px;
border-radius: 8px;
transition: background-color 0.2s ease;
`;
// Create avatar card
const avatarCard = document.createElement('div');
avatarCard.className = 'avatar-card';
avatarCard.style.cssText = `
position: relative;
margin-bottom: 8px;
`;
const avatarCardImage = document.createElement('div');
avatarCardImage.className = 'avatar-card-image';
avatarCardImage.style.cssText = `
position: relative;
width: 84px;
height: 84px;
margin: 0 auto;
`;
const avatarImg = document.createElement('img');
avatarImg.src = avatarUrl || window.Base64Images.builderman_avatar; // default to builderman if thumbnails fail for some reason
avatarImg.alt = friend.displayName || friend.name;
avatarImg.style.cssText = `
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
`;
// Add status indicator with proper structure for existing status detection
const avatarStatus = document.createElement('div');
avatarStatus.className = 'avatar-status';
avatarStatus.style.cssText = `
position: absolute;
bottom: 2px;
right: 2px;
width: 24px;
height: 24px;
background: #1a1c23;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid #1a1c23;
`;
const statusIcon = document.createElement('span');
statusIcon.setAttribute('data-testid', 'presence-icon');
statusIcon.className = 'icon-offline'; // Default to offline, will be updated by status detection
statusIcon.setAttribute('title', 'Offline');
statusIcon.style.cssText = `
width: 16px;
height: 16px;
border-radius: 50%;
background: #6b7280;
display: block;
`;
avatarStatus.appendChild(statusIcon);
avatarCardImage.appendChild(avatarImg);
avatarCardImage.appendChild(avatarStatus);
avatarCard.appendChild(avatarCardImage);
// Create name label
const nameLabel = document.createElement('div');
nameLabel.className = 'friend-name';
nameLabel.textContent = friend.displayName || friend.name;
nameLabel.style.cssText = `
color: #ffffff;
font-size: 12px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100px;
`;
tile.appendChild(avatarCard);
tile.appendChild(nameLabel);
// Add click handler to go to profile
tile.addEventListener('click', () => {
window.open(`https://www.roblox.com/users/${friend.id}/profile`, '_blank');
});
return tile;
};
// get friend status from tile element
const getFriendStatusFromTile = (tile) => {
const avatarStatusElement = tile.querySelector('.avatar-status');
if (!avatarStatusElement) {
return 'offline';
}
const statusIconElement = avatarStatusElement.querySelector('span[data-testid="presence-icon"]');
if (!statusIconElement) {
return 'offline';
}
const statusClassList = statusIconElement.className || '';
const statusTitleAttribute = statusIconElement.getAttribute('title') || '';
// comprehensive status detection logic
if (statusClassList.includes('icon-game') ||
statusClassList.includes('game') ||
statusTitleAttribute.toLowerCase().includes('game') ||
statusTitleAttribute.toLowerCase().includes('playing')) {
return 'game';
}
if (statusClassList.includes('icon-online') ||
statusClassList.includes('online') ||
statusTitleAttribute.toLowerCase().includes('website') ||
statusTitleAttribute.toLowerCase().includes('active')) {
return 'online';
}
if (statusClassList.includes('icon-offline') ||
statusClassList.includes('offline') ||
statusTitleAttribute.toLowerCase().includes('offline')) {
return 'offline';
}
// if status exists but doesnt match known patterns, its "other" (studio)
return statusClassList.trim() ? 'other' : 'offline';
};
// apply status outline styling to avatars
const applyFriendStatusStyling = () => {
const friendTileElements = document.querySelectorAll('.friends-carousel-tile');
friendTileElements.forEach(tileElement => {
const avatarImageElement = tileElement.querySelector('.avatar-card-image img');
if (!avatarImageElement) return;
// remove existing status classes
Object.values(CLASSES).forEach(className => {
if (className.startsWith('ROLOCATE_friend-status-')) {
avatarImageElement.classList.remove(className);
}
});
const currentFriendStatus = getFriendStatusFromTile(tileElement);
const statusClassToApply = CLASSES[`STATUS_${currentFriendStatus.toUpperCase()}`];
if (statusClassToApply) {
avatarImageElement.classList.add(statusClassToApply);
}
tileElement.setAttribute(`data-${CLASSES.TILE_STYLED}`, 'true');
});
};
// style dropdown menu elements
const styleDropdownMenus = () => {
const dropdownElements = document.querySelectorAll(`.friend-tile-dropdown:not([data-${CLASSES.DROPDOWN_STYLED}])`);
dropdownElements.forEach(dropdownElement => {
const parentTileElement = dropdownElement.closest('.friends-carousel-tile');
let friendStatusForDropdown = 'offline';
if (parentTileElement) {
friendStatusForDropdown = getFriendStatusFromTile(parentTileElement);
}
dropdownElement.setAttribute('data-friend-status', friendStatusForDropdown);
dropdownElement.setAttribute(`data-${CLASSES.DROPDOWN_STYLED}`, 'true');
// preserve icon styling for dropdown buttons
const iconElements = dropdownElement.querySelectorAll('.friend-tile-dropdown-button .icon');
iconElements.forEach(iconElement => {
iconElement.style.transition = 'opacity 0.2s ease';
iconElement.style.flexShrink = '0';
});
});
};
// helper function to fetch friends
const gmFetchFriends = (userId) => {
const url = `https://friends.roblox.com/v1/users/${userId}/friends`;
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url,
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
try {
const data = JSON.parse(response.responseText);
resolve(data.data);
} catch (e) {
ConsoleLogEnabled(`[gmFetchFriends] Failed to parse response for user ${userId}`, e);
resolve(null);
}
} else {
ConsoleLogEnabled(`[gmFetchFriends] Request failed for user ${userId} with status ${response.status}`);
resolve(null);
}
},
onerror: function(err) {
ConsoleLogEnabled(`[gmFetchFriends] Network error for user ${userId}`, err);
resolve(null);
}
});
});
};
// helper function to fetch user avatars
const fetchUserAvatars = (userIds) => {
return new Promise((resolve) => {
const requests = userIds.map(userId => ({
requestId: userId.toString(),
targetId: userId,
type: "AvatarHeadShot",
size: "150x150",
format: "Png",
isCircular: false
}));
GM_xmlhttpRequest({
method: "POST",
url: "https://thumbnails.roblox.com/v1/batch",
headers: {
"Content-Type": "application/json"
},
data: JSON.stringify(requests),
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
try {
const data = JSON.parse(response.responseText);
const avatarMap = {};
data.data.forEach(item => {
if (item.state === "Completed" && item.imageUrl) {
avatarMap[item.targetId] = item.imageUrl;
}
});
resolve(avatarMap);
} catch (e) {
ConsoleLogEnabled("[fetchUserAvatars] Failed to parse response", e);
resolve({});
}
} else {
ConsoleLogEnabled(`[fetchUserAvatars] Request failed with status ${response.status}`);
resolve({});
}
},
onerror: function(err) {
ConsoleLogEnabled("[fetchUserAvatars] Network error", err);
resolve({});
}
});
});
};
// create star icon for best friends
const createStarIcon = () => {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('class', CLASSES.BEST_FRIEND_STAR);
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('fill', 'currentColor');
svg.setAttribute('stroke', 'none');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M12 .587l3.668 7.568 8.332 1.151-6.064 5.828 1.48 8.279-7.416-3.967-7.417 3.967 1.481-8.279-6.064-5.828 8.332-1.151z');
svg.appendChild(path);
// Fade in animation
setTimeout(() => {
svg.classList.add('star-visible');
}, 50);
return svg;
};
// get best friends from localStorage
const getBestFriends = () => {
try {
const stored = localStorage.getItem('ROLOCATE_BEST_FRIENDS_IDS');
return stored ? new Set(JSON.parse(stored)) : new Set();
} catch (e) {
return new Set();
}
};
// save best friends to localStorage
const saveBestFriends = (bestFriends) => {
localStorage.setItem('ROLOCATE_BEST_FRIENDS_IDS', JSON.stringify([...bestFriends]));
};
// fetch online friends status from API
const ROLOCATE_fetchOnlineFriends = async (userId) => {
try {
const url = `https://friends.roblox.com/v1/users/${userId}/friends/online`;
const response = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: resolve,
onerror: reject
});
});
if (response.status >= 200 && response.status < 300) {
return JSON.parse(response.responseText).data || [];
}
ConsoleLogEnabled(`ROLOCATE: Online friends API error: ${response.status}`);
return [];
} catch (error) {
ConsoleLogEnabled('ROLOCATE: Failed to fetch online friends:', error);
return [];
}
};
// check best friends online status
const ROLOCATE_checkBestFriendsStatus = async () => {
const currentUserId = Roblox?.CurrentUser?.userId;
if (!currentUserId) {
ConsoleLogEnabled('ROLOCATE: Current user ID not available');
return;
}
const bestFriends = getBestFriends();
if (bestFriends.size === 0) {
ConsoleLogEnabled('ROLOCATE: No best friends set');
return;
}
const onlineFriends = await ROLOCATE_fetchOnlineFriends(currentUserId);
const onlineIds = new Set(onlineFriends.map(friend => friend.id));
bestFriends.forEach(bfId => {
const friend = onlineFriends.find(f => f.id === bfId);
if (friend) {
const presence = friend.userPresence;
if (presence.UserPresenceType === 'Online') {
ConsoleLogEnabled(`ROLOCATE: Best friend ${bfId} is online (Website)`);
} else if (presence.UserPresenceType === 'InGame') {
ConsoleLogEnabled(`ROLOCATE: Best friend ${bfId} is in-game: ${presence.lastLocation}`);
} else { // else user is in studio
ConsoleLogEnabled(`ROLOCATE: Best friend ${bfId} is in-studio: ${presence.UserPresenceType}`);
}
} else {
ConsoleLogEnabled(`ROLOCATE: Best friend ${bfId} is offline`);
}
});
};
const showBestFriendsPopup = async () => {
const overlay = document.createElement('div');
overlay.className = 'best-friends-overlay';
const popup = document.createElement('div');
popup.className = 'best-friends-popup';
const header = document.createElement('div');
header.className = 'best-friends-popup-header';
header.innerHTML = `<h3>Pick Your Best Friends</h3>`;
// add search container
const searchContainer = document.createElement('div');
searchContainer.className = 'best-friends-search-container';
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.className = 'best-friends-search';
searchInput.placeholder = 'Search friends';
searchContainer.appendChild(searchInput);
header.appendChild(searchContainer);
// Add close button
const closeButton = document.createElement('button');
closeButton.className = 'best-friends-close';
closeButton.innerHTML = '×';
header.appendChild(closeButton);
popup.appendChild(header);
const grid = document.createElement('div');
grid.className = 'best-friends-popup-grid';
const loading = document.createElement('div');
loading.className = 'best-friends-loading';
loading.innerHTML = `<div class="loading-spinner"></div>Loading friends...`;
grid.appendChild(loading);
popup.appendChild(grid);
overlay.appendChild(popup);
document.body.appendChild(overlay);
// get current best friends
let bestFriends = getBestFriends();
closeButton.addEventListener('click', () => {
overlay.style.animation = 'fadeOut 0.2s ease-out forwards';
setTimeout(() => overlay.remove(), 200);
});
// search functionality
let allFriends = [];
const performSearch = () => {
const searchTerm = searchInput.value.toLowerCase();
if (!allFriends.length) return;
grid.innerHTML = '';
const filtered = allFriends.filter(friend =>
friend.displayName.toLowerCase().includes(searchTerm)
);
if (filtered.length === 0) {
grid.innerHTML = '<div class="no-best-friends">No friends match your search</div>';
return;
}
filtered.forEach(friend => {
const friendItem = createFriendItem(friend, bestFriends.has(friend.id));
grid.appendChild(friendItem);
});
};
searchInput.addEventListener('input', performSearch);
try {
const currentUserId = Roblox?.CurrentUser?.userId || null;
if (!currentUserId) {
loading.innerHTML = 'Failed to get current user ID.';
return;
}
const friends = await gmFetchFriends(currentUserId);
if (!friends || friends.length === 0) {
loading.innerHTML = 'You have no friends.';
return;
}
// Get friend IDs
const friendIds = friends.map(friend => friend.id);
// Fetch avatars in batches
const avatarMap = {};
const batchSize = 5;
for (let i = 0; i < friendIds.length; i += batchSize) {
const batch = friendIds.slice(i, i + batchSize);
const batchAvatars = await fetchUserAvatars(batch);
Object.assign(avatarMap, batchAvatars);
}
// Clear loading and populate grid
grid.innerHTML = '';
allFriends = friends.map(friend => ({
id: friend.id,
displayName: friend.displayName || friend.name,
avatarUrl: avatarMap[friend.id]
}));
// Store all friends for search
allFriends.forEach(friend => {
const friendItem = createFriendItem(friend, bestFriends.has(friend.id));
grid.appendChild(friendItem);
});
} catch (error) {
ConsoleLogEnabled('[showBestFriendsPopup] Error:', error);
grid.innerHTML = '<div class="no-best-friends">Failed to load friends</div>';
}
// Create friend item element
function createFriendItem(friend, isBestFriend) {
const friendItem = document.createElement('div');
friendItem.className = 'best-friends-popup-item';
const avatarDiv = document.createElement('div');
avatarDiv.className = 'best-friend-avatar';
if (friend.avatarUrl) {
const img = document.createElement('img');
img.src = friend.avatarUrl;
img.alt = friend.displayName;
avatarDiv.appendChild(img);
} else {
avatarDiv.textContent = '👤';
}
const nameSpan = document.createElement('span');
nameSpan.className = 'best-friend-name';
nameSpan.textContent = friend.displayName;
friendItem.appendChild(avatarDiv);
friendItem.appendChild(nameSpan);
// Add star if best friend
if (isBestFriend) {
const star = createStarIcon();
friendItem.appendChild(star);
}
// Click handler
friendItem.addEventListener('click', (e) => {
e.stopPropagation();
// Toggle best friend status
if (bestFriends.has(friend.id)) {
bestFriends.delete(friend.id);
const star = friendItem.querySelector(`.${CLASSES.BEST_FRIEND_STAR}`);
if (star) {
star.classList.remove('star-visible');
setTimeout(() => star.remove(), 300);
}
} else {
// check if adding would exceed the limit
if (bestFriends.size >= 20) {
notifications('Maximum of 20 best friends allowed!', 'error', '⚠️', '2000');
return;
}
bestFriends.add(friend.id);
const star = createStarIcon();
friendItem.appendChild(star);
}
// Save to localStorage
saveBestFriends(bestFriends);
});
return friendItem;
}
};
// handle best friends button click event
const handleBestFriendsButtonClick = () => {
showBestFriendsPopup();
notifications('Once you pick your best friends, make sure to refresh the page for it to show best friends!', 'info', '', '6000');
notifications('This feature is still buggy and incomplete. Remove best friends if it causes any issues.', 'warning', '👤', '12000');
};
// create person icon SVG
const createPersonIcon = () => {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'currentColor');
svg.setAttribute('stroke-width', '2');
svg.setAttribute('stroke-linecap', 'round');
svg.setAttribute('stroke-linejoin', 'round');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2');
svg.appendChild(path);
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', '12');
circle.setAttribute('cy', '7');
circle.setAttribute('r', '4');
svg.appendChild(circle);
return svg;
};
// create and insert best friends button
const createAndInsertBestFriendsButton = () => {
const existingBestFriendsButton = document.querySelector(`.${CLASSES.BEST_FRIENDS_BUTTON}`);
if (existingBestFriendsButton) return;
const friendsHeaderElement = document.querySelector('.container-header.people-list-header h2');
if (!friendsHeaderElement) return;
const bestFriendsButton = document.createElement('button');
bestFriendsButton.className = CLASSES.BEST_FRIENDS_BUTTON;
// Add the person icon
const personIcon = createPersonIcon();
bestFriendsButton.appendChild(personIcon);
// Add the text
const textNode = document.createTextNode('Best Friends');
bestFriendsButton.appendChild(textNode);
bestFriendsButton.addEventListener('click', handleBestFriendsButtonClick);
// insert button right after the friends header element (next to it, not inside)
friendsHeaderElement.insertAdjacentElement('afterend', bestFriendsButton);
};
// setup observer for best friends button creation
const setupBestFriendsButtonObserver = () => {
if (bestFriendsButtonObserver) {
bestFriendsButtonObserver.disconnect();
}
bestFriendsButtonObserver = new MutationObserver(() => {
createAndInsertBestFriendsButton();
});
bestFriendsButtonObserver.observe(document.body, {
childList: true,
subtree: true
});
};
// setup dropdown observer for dynamic content
const setupDropdownMutationObserver = () => {
if (dropdownObserver) {
dropdownObserver.disconnect();
}
dropdownObserver = new MutationObserver((mutations) => {
let needsDropdownStylingUpdate = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((addedNode) => {
if (addedNode.nodeType === 1 &&
(addedNode.classList?.contains('friend-tile-dropdown') ||
addedNode.querySelector?.('.friend-tile-dropdown'))) {
needsDropdownStylingUpdate = true;
}
});
}
});
if (needsDropdownStylingUpdate) {
styleDropdownMenus();
}
});
dropdownObserver.observe(document.body, {
childList: true,
subtree: true
});
};
// setup avatar observer for status changes
const setupAvatarMutationObserver = () => {
if (avatarObserver) {
avatarObserver.disconnect();
}
const friendsContainerElement = document.querySelector('.friend-carousel-container');
if (!friendsContainerElement) return;
avatarObserver = new MutationObserver((mutations) => {
let needsAvatarStylingUpdate = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((addedNode) => {
if (addedNode.nodeType === 1 &&
(addedNode.classList?.contains('friends-carousel-tile') ||
addedNode.querySelector?.('.friends-carousel-tile') ||
addedNode.classList?.contains('avatar-card-image') ||
addedNode.classList?.contains('avatar-status'))) {
needsAvatarStylingUpdate = true;
}
});
} else if (mutation.type === 'attributes') {
const targetElement = mutation.target;
if (targetElement.classList?.contains('avatar-status') ||
targetElement.getAttribute('data-testid') === 'presence-icon' ||
targetElement.closest('.avatar-status') ||
targetElement.closest('.friends-carousel-tile')) {
needsAvatarStylingUpdate = true;
}
}
});
if (needsAvatarStylingUpdate) {
// small delay to ensure dom is ready
setTimeout(applyFriendStatusStyling, 100);
}
});
avatarObserver.observe(friendsContainerElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class', 'title', 'src']
});
};
// apply main container styling
const applyFriendsContainerStyling = () => {
const friendsContainerElement = document.querySelector('.friend-carousel-container');
if (!friendsContainerElement) return false;
friendsContainerElement.style.backgroundColor = '#1a1c23';
friendsContainerElement.style.borderRadius = '12px';
friendsContainerElement.style.border = '1px solid #2a2a30';
friendsContainerElement.style.padding = '12px';
friendsContainerElement.style.boxSizing = 'border-box';
friendsContainerElement.style.margin = '0 0 16px 0';
return true;
};
const initializeBetterFriendsFeatures = () => {
if (!applyFriendsContainerStyling()) return false;
addStatusStyles();
applyFriendStatusStyling();
setupDropdownMutationObserver();
setupAvatarMutationObserver();
setupBestFriendsButtonObserver();
createAndInsertBestFriendsButton();
// Add best friends section
createBestFriendsSection();
removeBestFriendsFromRegularSection();
// Immediate check when DOM is ready
const checkWhenReady = () => {
if (Roblox?.CurrentUser?.userId) {
ROLOCATE_checkBestFriendsStatus();
} else {
requestAnimationFrame(checkWhenReady);
}
};
checkWhenReady();
return true;
};
// cleanup function for observers
const cleanupAllObservers = () => {
if (dropdownObserver) dropdownObserver.disconnect();
if (avatarObserver) avatarObserver.disconnect();
if (mainObserver) mainObserver.disconnect();
if (bestFriendsButtonObserver) bestFriendsButtonObserver.disconnect();
if (observerTimeout) clearTimeout(observerTimeout);
};
// check if friends section exists
const checkForFriendsSectionExistence = () => {
return document.querySelector('.friend-carousel-container') ||
document.querySelector('.add-friends-icon-container');
};
// main execution logic
if (checkForFriendsSectionExistence()) {
initializeBetterFriendsFeatures();
return cleanupAllObservers;
}
// timeout for cleanup if friends section doesnt appear
observerTimeout = setTimeout(cleanupAllObservers, 15000);
// main observer for waiting for friends section
mainObserver = new MutationObserver(() => {
if (checkForFriendsSectionExistence()) {
if (initializeBetterFriendsFeatures()) {
mainObserver.disconnect();
if (observerTimeout) clearTimeout(observerTimeout);
}
}
});
mainObserver.observe(document.body, {
childList: true,
subtree: true
});
return cleanupAllObservers;
}
/*******************************************************
name of function: restoreclassicterms
description: restores the classic terms that roblox removed
*******************************************************/
function restoreclassicterms() {
if (localStorage.getItem("ROLOCATE_restoreclassicterms") !== "true") return;
const classicTermReplacementsList = [
{ from: /\bCommunities\b/g, to: "Groups" }, { from: /\bcommunities\b/g, to: "groups" },
{ from: /\bCommunity\b/g, to: "Group" }, { from: /\bcommunity\b/g, to: "group" },
{ from: /\bConnections\b/g, to: "Friends" }, { from: /\bconnections\b/g, to: "friends" },
{ from: /\bConnection\b/g, to: "Friend" }, { from: /\bconnection\b/g, to: "friend" },
{ from: /\bConnect\b/g, to: "Friends" }, { from: /\bconnect\b/g, to: "friends" }
];
const attributesToCheckForTextContent = ["placeholder", "title", "aria-label", "alt"];
const htmlTagsToTargetForReplacement = [
"span", "div", "a", "button", "label", "input", "textarea",
"h1", "h2", "h3", "li", "p"
];
// these containers get special treatment and bypass game context blocking
function elementIsInOverrideContainer(element) {
return !!element.closest(".container-header.people-list-header, .server-list-container-header, .profile-header-social-count, .create-server-banner-text, .play-with-others-text, .announcement-display-body-content, .profile-header-buttons");
}
function elementIsInsideBlockedGameContext(element) {
if (elementIsInOverrideContainer(element)) return false;
while (element) {
const elementIdLower = (element.id || "").toLowerCase();
if (elementIdLower.includes("game")) return true;
const classList = element.classList;
if (classList) {
for (const className of classList) {
const lowerClassName = className.toLowerCase();
if (
lowerClassName.includes("game") ||
lowerClassName.includes("shopping-cart") ||
lowerClassName.includes("catalog-item-container") ||
lowerClassName.includes("catalog") ||
lowerClassName.includes("profile-header-details") ||
lowerClassName.includes("rolocate_smartsearch_") ||
lowerClassName.includes("avatar-card-container") ||
lowerClassName.includes("dialog-container") ||
lowerClassName.includes("friends-carousel-tile-label") ||
lowerClassName.includes("chat-container") ||
lowerClassName.includes("profile") ||
lowerClassName.includes("mutual-friends-container")
) {
return true;
}
}
}
element = element.parentElement;
}
return false;
}
function replaceClassicTermsInTextNode(textNode) {
if (!textNode || textNode.nodeType !== Node.TEXT_NODE) return;
let originalText = textNode.textContent;
let modifiedText = originalText;
for (const { from, to } of classicTermReplacementsList) {
modifiedText = modifiedText.replace(from, to);
}
if (modifiedText !== originalText) {
textNode.textContent = modifiedText;
}
}
function processSingleHTMLElementForTermReplacement(element) {
if (!element || (!elementIsInOverrideContainer(element) && elementIsInsideBlockedGameContext(element))) return;
// Process child text nodes
element.childNodes.forEach(childNode => {
if (childNode.nodeType === Node.TEXT_NODE) {
replaceClassicTermsInTextNode(childNode);
}
});
// Process attributes
attributesToCheckForTextContent.forEach(attribute => {
const attributeValue = element.getAttribute(attribute);
if (attributeValue && typeof attributeValue === "string") {
let updatedValue = attributeValue;
for (const { from, to } of classicTermReplacementsList) {
updatedValue = updatedValue.replace(from, to);
}
if (updatedValue !== attributeValue) {
element.setAttribute(attribute, updatedValue);
}
}
});
}
function processAllInitialPageContent() {
htmlTagsToTargetForReplacement.forEach(tag => {
document.querySelectorAll(tag).forEach(processSingleHTMLElementForTermReplacement);
});
}
// initial scan
processAllInitialPageContent();
// observe future changes
const observeDOMForNewNodes = new MutationObserver(mutationRecords => {
for (const mutation of mutationRecords) {
// Handle added nodes
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(addedNode => {
if (addedNode.nodeType === Node.ELEMENT_NODE) {
processSingleHTMLElementForTermReplacement(addedNode);
htmlTagsToTargetForReplacement.forEach(tag => {
addedNode.querySelectorAll(tag).forEach(processSingleHTMLElementForTermReplacement);
});
} else if (addedNode.nodeType === Node.TEXT_NODE && addedNode.parentElement) {
const parent = addedNode.parentElement;
if (elementIsInOverrideContainer(parent) || !elementIsInsideBlockedGameContext(parent)) {
replaceClassicTermsInTextNode(addedNode);
}
}
});
}
// Handle text changes
else if (mutation.type === 'characterData') {
const textNode = mutation.target;
if (textNode.nodeType === Node.TEXT_NODE) {
const parent = textNode.parentElement;
if (parent && (elementIsInOverrideContainer(parent) || !elementIsInsideBlockedGameContext(parent))) {
replaceClassicTermsInTextNode(textNode);
}
}
}
// Handle attribute changes
else if (mutation.type === 'attributes') {
const element = mutation.target;
const attrName = mutation.attributeName;
if (attributesToCheckForTextContent.includes(attrName)) {
if (elementIsInOverrideContainer(element) || !elementIsInsideBlockedGameContext(element)) {
const value = element.getAttribute(attrName);
let newValue = value;
for (const { from, to } of classicTermReplacementsList) {
newValue = newValue.replace(from, to);
}
if (newValue !== value) {
element.setAttribute(attrName, newValue);
}
}
}
}
}
});
observeDOMForNewNodes.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
attributeFilter: attributesToCheckForTextContent
});
}
/*******************************************************
name of function: event listener
description: Note a function but runs the initial setup for the script to actually
start working. Very important
*******************************************************/
window.addEventListener("load", () => {
const startTime = performance.now();
loadBase64Library(() => {
ConsoleLogEnabled("Loaded Base64Images. It is ready to use!");
});
AddSettingsButton(() => {
ConsoleLogEnabled("Loaded Settings button!");
});
betterfriends();
SmartSearch(); // love this function btw lmao
restoreclassicterms();
quicklaunchgamesfunction();
manageRobloxChatBar();
loadmutualfriends();
Update_Popup();
initializeLocalStorage();
removeAds();
showOldRobloxGreeting();
quicknavbutton();
validateManualMode();
qualityfilterRobloxGames();
// start observing URL changes cuase its cool
observeURLChanges();
const endTime = performance.now();
const elapsed = Math.round(endTime - startTime);
console.log(`%cRoLocate by Oqarshi - loaded in ${elapsed} ms. Personal use only.`, "color: #FFD700; font-size: 18px; font-weight: bold;");
});
/*******************************************************
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(); // lazy loads the server region data to save 2mb of ram lol
if (window.serverRegionsByIp) {
ConsoleLogEnabled("Server regions data loaded successfully.");
} else {
ConsoleLogEnabled("Failed to load server regions data.");
}
getFlagEmoji(); // lazy loads the flag emoji base64 to save some ram i guess
InitRobloxLaunchHandler(); // listens for game join and if true shows popup.
}
/*********************************************************************************************************************************************************************************************************************************************
This is all of the functions for the filter button and the popup for the 8 buttons
*********************************************************************************************************************************************************************************************************************************************/
/*******************************************************
name of function: InitRobloxLaunchHandler
description: Detects when the user joins a Roblox server,
adds it to recent servers (if enabled), and—only when
SmartSearch is on—shows a loading overlay and waits 1.5s.
*******************************************************/
// WARNING: Do not republish this script. Licensed for personal use only.
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 = async function(gameId, serverId) {
ConsoleLogEnabled(`Intercepted join: Game ID = ${gameId}, Server ID = ${serverId}`);
/* ---------- recent‑servers handling (always runs) ---------- */
if (localStorage.getItem("ROLOCATE_togglerecentserverbutton") === "true") {
await HandleRecentServersAddGames(gameId, serverId);
document.querySelector(".recent-servers-section")?.remove();
HandleRecentServers();
}
/* ---------- smartserver join---------- */
if (localStorage.getItem("ROLOCATE_smartjoinpopup") === "true") {
showLoadingOverlay(gameId, serverId); // visual feedback
await new Promise(res => setTimeout(res, 1500)); // 1.5s delay
}
/* ---------- finally join the game ---------- */
return originalJoin.apply(this, arguments);
};
}
/*******************************************************
name of function: HandleRecentServersAddGames
description: Adds recent servers to localstorage for safe
keeping
*******************************************************/
// WARNING: Do not republish this script. Licensed for personal use only.
async function HandleRecentServersAddGames(gameId, serverId) {
const storageKey = "ROLOCATE_recentservers_button";
const stored = JSON.parse(localStorage.getItem(storageKey) || "{}");
const key = `${gameId}_${serverId}`;
// Check if we already have region data for this server
if (!stored[key] || !stored[key].region) {
try {
// Fetch server region if not already stored
const region = await fetchServerDetails(gameId, serverId);
stored[key] = {
timestamp: Date.now(),
region: region
};
} catch (error) {
ConsoleLogEnabled("Failed to fetch server region:", error);
// Store without region data if fetch fails
stored[key] = {
timestamp: Date.now(),
region: null
};
}
} else {
// Update timestamp but keep existing region data
stored[key].timestamp = Date.now();
}
localStorage.setItem(storageKey, JSON.stringify(stored));
}
/*******************************************************
name of function: HandleRecentServersURL
description: Detects recent servers from the url if
user joins server from invite url and cleans up the URL
*******************************************************/
// WARNING: Do not republish this script. Licensed for personal use only.
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];
// Clean up the URL (remove the hash part) while preserving query parameters
const cleanURL = window.location.pathname + window.location.search;
history.replaceState(null, null, cleanURL);
// Call the handler with extracted values
HandleRecentServersAddGames(gameId, serverId);
} else {
ConsoleLogEnabled("No gameId and serverId found in URL.");
HandleRecentServersURL.alreadyInvalid = true; // Set internal flag
}
}
/*******************************************************
name of function: getFlagEmoji
description: Guves Flag Emoji
*******************************************************/
function getFlagEmoji(countryCode) {
// Static variables to maintain state without globals
if (!getFlagEmoji.flagsData) {
ConsoleLogEnabled("[getFlagEmoji] Initializing static variables.");
getFlagEmoji.flagsData = null;
getFlagEmoji.isLoaded = false;
}
// If no countryCode provided, lazy load all data
if (!countryCode) {
ConsoleLogEnabled("[getFlagEmoji] No country code provided.");
if (!getFlagEmoji.isLoaded) {
ConsoleLogEnabled("[getFlagEmoji] Loading flag data (no countryCode).");
getFlagEmoji.flagsData = loadFlagsData(); // This function comes from @require
getFlagEmoji.isLoaded = true;
ConsoleLogEnabled("[getFlagEmoji] Flag data loaded successfully.");
} else {
ConsoleLogEnabled("[getFlagEmoji] Flag data already loaded.");
}
return;
}
// If data not loaded yet, load it now
if (!getFlagEmoji.isLoaded) {
ConsoleLogEnabled(`[getFlagEmoji] Lazy loading flag data for country: ${countryCode}`);
getFlagEmoji.flagsData = loadFlagsData();
getFlagEmoji.isLoaded = true;
ConsoleLogEnabled("[getFlagEmoji] Flag data loaded successfully.");
}
const src = getFlagEmoji.flagsData[countryCode];
ConsoleLogEnabled(`[getFlagEmoji] Creating flag image for country code: ${countryCode}`);
const img = document.createElement('img');
img.src = src;
img.alt = countryCode;
img.width = 24;
img.height = 18;
img.style.verticalAlign = 'middle';
img.style.marginRight = '4px';
return img;
}
/*******************************************************
name of function: HandleRecentServers
description: Detects if recent servers are in localstorage
and then adds them to the page with css styles
*******************************************************/
// WARNING: Do not republish this script. Licensed for personal use only.
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 => { // fix so restore classic terms would not interfere
const text = header.textContent.trim();
const match = ['Servers My Connections Are In', 'Servers My Friends Are In'].some(
label => text === label
);
if (match) {
friendsSectionHeader = header.closest('.container-header');
}
});
function formatLastPlayedWithRelative(lastPlayed, mode) {
const lastPlayedDate = new Date(lastPlayed);
const now = new Date();
const diffMs = now - lastPlayedDate;
const diffSeconds = Math.floor(diffMs / 1000);
const diffMinutes = Math.floor(diffSeconds / 60);
const diffHours = Math.floor(diffMinutes / 60);
const diffDays = Math.floor(diffHours / 24);
let relativeTime = '';
if (diffDays > 0) {
relativeTime = diffDays === 1 ? '1 day ago' : `${diffDays} days ago`;
} else if (diffHours > 0) {
relativeTime = diffHours === 1 ? '1 hour ago' : `${diffHours} hours ago`;
} else if (diffMinutes > 0) {
relativeTime = diffMinutes === 1 ? '1 minute ago' : `${diffMinutes} minutes ago`;
} else {
relativeTime = diffSeconds <= 1 ? 'just now' : `${diffSeconds} seconds ago`;
}
if (mode === "relativeOnly") {
return relativeTime;
}
return `${lastPlayed} (${relativeTime})`;
}
if (!friendsSectionHeader) return;
const theme = {
bgGradient: 'linear-gradient(145deg, #1e2228, #18191e)',
bgGradientHover: 'linear-gradient(145deg, #23272f, #1c1f25)',
accentPrimary: '#4d85ee',
accentGradient: 'linear-gradient(to bottom, #4d85ee, #3464c9)',
accentGradientHover: 'linear-gradient(to bottom, #5990ff, #3b6fdd)',
textPrimary: '#e8ecf3',
textSecondary: '#a0a8b8',
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)',
dangerGradient: 'linear-gradient(to bottom, #ff5b5b, #e04444)',
dangerGradientHover: 'linear-gradient(to bottom, #ff7575, #f55)',
popupBg: 'rgba(20, 22, 26, 0.95)',
popupBorder: 'rgba(77, 133, 238, 0.2)'
};
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';
headerInner.style.display = 'flex';
headerInner.style.justifyContent = 'space-between';
headerInner.style.alignItems = 'center';
const headerTitleContainer = document.createElement('div');
headerTitleContainer.style.display = 'flex';
headerTitleContainer.style.alignItems = 'center';
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;
`;
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);
headerTitleContainer.appendChild(headerTitle);
const clearAllButton = document.createElement('button');
clearAllButton.textContent = 'Clear All'; // this button is in the popup in recent servers
clearAllButton.style.cssText = `
background: transparent;
color: ${theme.textSecondary};
border: 1px solid ${theme.borderLight};
padding: 4px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 4px;
margin-left: 12px;
`;
clearAllButton.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 4px;">
<path d="M3 6H5H21" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 6V4C8 3.46957 8.21071 2.96086 8.58579 2.58579C8.96086 2.21071 9.46957 2 10 2H14C14.5304 2 15.0391 2.21071 15.4142 2.58579C15.7893 2.96086 16 3.46957 16 4V6M19 6V20C19 20.5304 18.7893 21.0391 18.4142 21.4142C18.0391 21.7893 17.5304 22 17 22H7C6.46957 22 5.96086 21.7893 5.58579 21.4142C5.21071 21.0391 5 20.5304 5 20V6H19Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 11V17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 11V17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Clear All
`;
clearAllButton.onmouseover = function() {
this.style.background = 'rgba(100, 0, 0, 0.85)'; // dark red
this.style.color = 'white';
this.style.borderColor = 'rgba(100, 0, 0, 0.85)'; // boarder color
this.style.transform = 'scale(1.02)';
};
clearAllButton.onmouseout = function() {
this.style.background = 'transparent';
this.style.color = theme.textSecondary;
this.style.borderColor = theme.borderLight;
this.style.transform = 'scale(1)';
};
clearAllButton.addEventListener('click', function() {
const popup = document.createElement('div');
popup.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
background: rgba(0, 0, 0, 0.3);
opacity: 0;
transition: opacity 0.3s ease;
`;
const popupContent = document.createElement('div');
popupContent.style.cssText = `
background: ${theme.popupBg};
border-radius: 12px;
padding: 20px;
width: 360px;
max-width: 90%;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
border: 1px solid ${theme.popupBorder};
text-align: center;
transform: translateY(20px);
transition: transform 0.3s ease, opacity 0.3s ease;
opacity: 0;
`;
const popupTitle = document.createElement('h3');
popupTitle.textContent = 'Clear All Recent Servers';
popupTitle.style.cssText = `
color: ${theme.textPrimary};
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
`;
const popupMessage = document.createElement('p');
popupMessage.textContent = 'Are you sure you want to clear all recent servers? This action cannot be undone.';
popupMessage.style.cssText = `
color: ${theme.textSecondary};
margin: 0 0 24px 0;
font-size: 13px;
line-height: 1.5;
`;
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
justify-content: center;
gap: 12px;
`;
const cancelButton = document.createElement('button');
cancelButton.textContent = 'Cancel';
cancelButton.style.cssText = `
background: rgba(28, 31, 37, 0.6);
color: ${theme.textPrimary};
border: 1px solid rgba(255, 255, 255, 0.12);
padding: 8px 20px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
`;
cancelButton.onmouseover = function() {
this.style.background = 'rgba(35, 39, 46, 0.8)';
this.style.borderColor = 'rgba(255, 255, 255, 0.18)';
this.style.transform = 'scale(1.05)';
};
cancelButton.onmouseout = function() {
this.style.background = 'rgba(28, 31, 37, 0.6)';
this.style.borderColor = 'rgba(255, 255, 255, 0.12)';
this.style.transform = 'scale(1)';
};
cancelButton.addEventListener('click', function() {
popup.style.opacity = '0';
setTimeout(() => {
popup.remove();
}, 300);
});
const confirmButton = document.createElement('button');
confirmButton.textContent = 'Clear All'; // this one is in the popup
confirmButton.style.cssText = `
background: rgba(100, 0, 0, 0.85); /* solid dark red */
color: white;
border: none;
padding: 8px 20px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(100, 0, 0, 0.3);
`;
confirmButton.onmouseover = function() {
this.style.background = 'rgba(80, 0, 0, 0.95)'; /* slightly darker solid red on hover */
this.style.boxShadow = '0 4px 10px rgba(80, 0, 0, 0.4)';
this.style.transform = 'scale(1.02)';
};
confirmButton.onmouseout = function() {
this.style.background = 'rgba(100, 0, 0, 0.85)'; /* revert to original */
this.style.boxShadow = '0 2px 8px rgba(100, 0, 0, 0.3)';
this.style.transform = 'scale(1)';
};
confirmButton.addEventListener('click', function() {
const cardsWrapper = document.querySelector('.recent-servers-section .section-content-off');
if (cardsWrapper) {
cardsWrapper.querySelectorAll('.recent-server-card').forEach(card => {
card.style.transition = 'all 0.3s ease-out';
card.style.opacity = '0';
card.style.height = '0';
card.style.margin = '0';
card.style.padding = '0';
setTimeout(() => card.remove(), 300);
});
}
const storageKey = "ROLOCATE_recentservers_button";
localStorage.setItem(storageKey, JSON.stringify({}));
const emptyMessage = document.createElement('div');
emptyMessage.className = 'no-servers-message';
emptyMessage.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="opacity: 0.7; margin-right: 10px;">
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.09 9C9.3251 8.33167 9.78915 7.76811 10.4 7.40913C11.0108 7.05016 11.7289 6.91894 12.4272 7.03871C13.1255 7.15849 13.7588 7.52152 14.2151 8.06353C14.6713 8.60553 14.9211 9.29152 14.92 10C14.92 12 11.92 13 11.92 13" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 17H12.01" stroke="${theme.accentPrimary}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>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);
border-radius: 12px;
border: 1px solid rgba(77, 133, 238, 0.15);
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.2);
`;
if (cardsWrapper) {
cardsWrapper.innerHTML = '';
cardsWrapper.appendChild(emptyMessage);
}
popup.style.opacity = '0';
shutdownTimeout(() => {
popup.remove();
}, 300);
});
buttonContainer.appendChild(cancelButton);
buttonContainer.appendChild(confirmButton);
popupContent.appendChild(popupTitle);
popupContent.appendChild(popupMessage);
popupContent.appendChild(buttonContainer);
popup.appendChild(popupContent);
document.body.appendChild(popup);
setTimeout(() => {
popup.style.opacity = '1';
popupContent.style.transform = 'translateY(0)';
popupContent.style.opacity = '1';
}, 10);
popup.addEventListener('click', function(e) {
if (e.target === popup) {
popup.style.opacity = '0';
setTimeout(() => {
popup.remove();
}, 300);
}
});
});
headerInner.appendChild(headerTitleContainer);
headerInner.appendChild(clearAllButton);
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) || "{}");
const currentTime = Date.now();
const threeDaysInMs = 3 * 24 * 60 * 60 * 1000;
let storageUpdated = false;
Object.keys(stored).forEach(key => {
const serverData = stored[key];
const serverTime = typeof serverData === 'object' ? serverData.timestamp : serverData;
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 = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="opacity: 0.7; margin-right: 10px;">
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.09 9C9.3251 8.33167 9.78915 7.76811 10.4 7.40913C11.0108 7.05016 11.7289 6.91894 12.4272 7.03871C13.1255 7.15849 13.7588 7.52152 14.2151 8.06353C14.6713 8.60553 14.9211 9.29152 14.92 10C14.92 12 11.92 13 11.92 13" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 17H12.01" stroke="${theme.accentPrimary}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>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);
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) => {
const aData = stored[a];
const bData = stored[b];
const aTime = typeof aData === 'object' ? aData.timestamp : aData;
const bTime = typeof bData === 'object' ? bData.timestamp : bData;
return bTime - aTime;
});
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 serverData = stored[key];
const timeStored = typeof serverData === 'object' ? serverData.timestamp : serverData;
const regionData = typeof serverData === 'object' ? serverData.region : null;
const date = new Date(timeStored);
const formattedTime = date.toLocaleString(undefined, {
hour: '2-digit',
minute: '2-digit',
year: 'numeric',
month: 'short',
day: 'numeric'
});
let regionDisplay = '';
let flagElement = null;
if (regionData && regionData !== null) {
const city = regionData.city || 'Unknown';
const countryCode = (regionData.country && regionData.country.code) || '';
flagElement = getFlagEmoji(countryCode);
} else {
flagElement = getFlagEmoji('');
regionDisplay = 'Unknown';
}
if (!flagElement) {
flagElement = document.createTextNode('🌍');
regionDisplay = regionDisplay || 'Unknown';
}
if (flagElement && flagElement.tagName === 'IMG') {
flagElement.style.cssText = `
width: 24px;
height: 18px;
vertical-align: middle;
margin-right: 4px;
display: inline-block;
`;
}
if (!regionDisplay) {
if (regionData && regionData !== null && regionData.city) {
regionDisplay = regionData.city;
} else {
regionDisplay = 'Unknown';
}
}
const serverCard = document.createElement('div');
serverCard.className = 'recent-server-card premium-dark';
serverCard.dataset.serverKey = key;
serverCard.dataset.gameId = gameId;
serverCard.dataset.serverId = serverId;
serverCard.dataset.region = regionDisplay;
serverCard.dataset.lastPlayed = formattedTime;
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;
`;
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;
};
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);
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 = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 17L12 22L22 17" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 12L12 17L22 12" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 7L12 12L22 7L12 2L2 7Z" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`;
serverIconWrapper.appendChild(serverIcon);
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;
width: calc(100% - 180px);
`;
const lastPlayed = document.createElement('div');
lastPlayed.textContent = `Last Played: ${formatLastPlayedWithRelative(formattedTime, "relativeOnly")}`;
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);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const regionInfo = document.createElement('div');
regionInfo.style.cssText = `
font-size: 12px;
color: ${theme.textSecondary};
margin-top: 2px;
opacity: 0.9;
margin-left: 40px;
line-height: 18px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
regionInfo.innerHTML = `<span style="color: ${theme.accentPrimary};">Region:</span> `;
if (flagElement && (flagElement.nodeType === Node.ELEMENT_NODE || flagElement.nodeType === Node.TEXT_NODE)) {
if (flagElement.nodeType === Node.ELEMENT_NODE) {
flagElement.style.position = 'relative';
flagElement.style.top = '-2px';
}
regionInfo.appendChild(flagElement);
} else {
regionInfo.appendChild(document.createTextNode('🌍'));
}
const regionText = document.createElement('span');
regionText.textContent = ` ${regionDisplay}`;
regionText.style.position = 'relative';
regionText.style.left = '-4px';
regionInfo.appendChild(regionText);
left.appendChild(lastPlayed);
left.appendChild(regionInfo);
const buttonGroup = document.createElement('div');
buttonGroup.style.cssText = `
display: flex;
gap: 12px;
align-items: center;
z-index: 2;
`;
const removeButton = document.createElement('button');
removeButton.innerHTML = `
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`;
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(211, 47, 47, 0.3);
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
`;
removeButton.onmouseover = function() {
this.style.background = theme.dangerGradientHover;
this.style.boxShadow = '0 4px 10px rgba(211, 47, 47, 0.4)';
this.style.transform = 'translateY(-1px)';
};
removeButton.onmouseout = function() {
this.style.background = theme.dangerGradient;
this.style.boxShadow = '0 2px 8px rgba(211, 47, 47, 0.3)';
this.style.transform = 'translateY(0)';
};
removeButton.addEventListener('click', function(e) {
e.stopPropagation();
const serverKey = this.closest('.recent-server-card').dataset.serverKey;
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();
const storedData = JSON.parse(localStorage.getItem(storageKey) || "{}");
delete storedData[serverKey];
localStorage.setItem(storageKey, JSON.stringify(storedData));
if (document.querySelectorAll('.recent-server-card').length === 0) {
const emptyMessage = document.createElement('div');
emptyMessage.className = 'no-servers-message';
emptyMessage.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="opacity: 0.7; margin-right: 10px;">
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.09 9C9.3251 8.33167 9.78915 7.76811 10.4 7.40913C11.0108 7.05016 11.7289 6.91894 12.4272 7.03871C13.1255 7.15849 13.7588 7.52152 14.2151 8.06353C14.6713 8.60553 14.9211 9.29152 14.92 10C14.92 12 11.92 13 11.92 13" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 17H12.01" stroke="${theme.accentPrimary}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>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);
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);
});
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 = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 6px;">
<path d="M5 12H19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 5L19 12L12 19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
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;
`;
joinButton.addEventListener('click', function() {
try {
Roblox.GameLauncher.joinGameInstance(gameId, serverId);
} catch (error) {
ConsoleLogEnabled("Error joining game:", error);
}
});
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 = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 6px;">
<path d="M16 18L18 20L22 16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20 12V13.4C20 13.4 19.5 13 19 13C18.5 13 18 13.5 18 14C18 14.5 18.5 15 19 15C19.5 15 20 14.6 20 14.6V16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 20C4 17 7 17 8 17C9 17 13 17 13 17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path d="M9.5 10C10.8807 10 12 8.88071 12 7.5C12 6.11929 10.8807 5 9.5 5C8.11929 5 7 6.11929 7 7.5C7 8.88071 8.11929 10 9.5 10Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
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;
`;
inviteButton.addEventListener('click', function() {
const inviteUrl = `https://oqarshi.github.io/Invite/?placeid=${gameId}&serverid=${serverId}`;
inviteButton.disabled = true;
navigator.clipboard.writeText(inviteUrl).then(
function() {
const originalText = inviteButton.innerHTML;
inviteButton.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 6px;">
<path d="M20 6L9 17L4 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Copied!
`;
ConsoleLogEnabled(`Invite link copied to clipboard`);
notifications('Success! Invite link copied to clipboard!', 'success', '🎉', '2000');
setTimeout(() => {
inviteButton.innerHTML = originalText;
inviteButton.disabled = false;
}, 1000);
},
function(err) {
ConsoleLogEnabled('Could not copy text: ', err);
inviteButton.disabled = false;
}
);
});
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)';
};
const moreInfoButton = document.createElement('button');
moreInfoButton.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 8V12V8ZM12 16H12.01H12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="currentColor" stroke-width="1.5"/>
</svg>
`;
moreInfoButton.className = 'btn-control-xs more-info-button';
moreInfoButton.style.cssText = `
background: rgba(28, 31, 37, 0.6);
color: ${theme.textPrimary};
border: 1px solid rgba(255, 255, 255, 0.12);
padding: 8px;
border-radius: 10px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
`;
moreInfoButton.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)';
this.style.color = theme.accentPrimary;
};
moreInfoButton.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)';
this.style.color = theme.textPrimary;
};
moreInfoButton.addEventListener('click', function(e) {
e.stopPropagation();
const card = this.closest('.recent-server-card');
const gameId = card.dataset.gameId;
const serverId = card.dataset.serverId;
const region = card.dataset.region;
const lastPlayed = card.dataset.lastPlayed;
const existingPopup = document.querySelector('.server-info-popup');
if (existingPopup) existingPopup.remove();
const popup = document.createElement('div');
popup.className = 'server-info-popup';
popup.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
background: rgba(0, 0, 0, 0.3);
opacity: 0;
transition: opacity 0.2s ease-out;
`;
const popupContent = document.createElement('div');
popupContent.style.cssText = `
background: ${theme.popupBg};
border-radius: 16px;
width: 420px;
max-width: 90%;
padding: 24px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.4);
border: 1px solid ${theme.popupBorder};
transform: translateY(20px);
opacity: 0;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
`;
const popupHeader = document.createElement('div');
popupHeader.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
`;
const popupTitle = document.createElement('h3');
popupTitle.textContent = 'Server Information';
popupTitle.style.cssText = `
color: ${theme.textPrimary};
font-size: 18px;
font-weight: 600;
margin: 0;
display: flex;
align-items: center;
gap: 10px;
`;
const serverIconPopup = document.createElement('div');
serverIconPopup.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 17L12 22L22 17" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 12L12 17L22 12" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 7L12 12L22 7L12 2L2 7Z" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`;
popupTitle.prepend(serverIconPopup);
popupHeader.appendChild(popupTitle);
const infoItems = document.createElement('div');
infoItems.style.cssText = `
display: flex;
flex-direction: column;
gap: 16px;
`;
function createInfoItem(label, value, icon) {
const item = document.createElement('div');
item.style.cssText = `
display: flex;
gap: 12px;
align-items: flex-start;
`;
const iconContainer = document.createElement('div');
iconContainer.style.cssText = `
background: rgba(77, 133, 238, 0.15);
border-radius: 8px;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
`;
iconContainer.innerHTML = icon || `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 8V12V8ZM12 16H12.01H12Z" stroke="${theme.accentPrimary}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="${theme.accentPrimary}" stroke-width="1.5"/>
</svg>
`;
const textContainer = document.createElement('div');
const labelEl = document.createElement('div');
labelEl.textContent = label;
labelEl.style.cssText = `
color: ${theme.textSecondary};
font-size: 12px;
font-weight: 500;
margin-bottom: 4px;
`;
const valueEl = document.createElement('div');
valueEl.textContent = value;
valueEl.style.cssText = `
color: ${theme.textPrimary};
font-size: 14px;
font-weight: 600;
word-break: break-all;
`;
textContainer.appendChild(labelEl);
textContainer.appendChild(valueEl);
item.appendChild(iconContainer);
item.appendChild(textContainer);
return item;
}
infoItems.appendChild(createInfoItem('Game ID', gameId, `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.27 6.96L12 12.01L20.73 6.96" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 22.08V12" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`));
infoItems.appendChild(createInfoItem('Server ID', serverId, `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 12.55L11 17.75L19 6.95" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="${theme.accentPrimary}" stroke-width="1.5"/>
</svg>
`));
infoItems.appendChild(createInfoItem('Region', region, `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C8.13 2 5 5.13 5 9C5 14.25 12 22 12 22C12 22 19 14.25 19 9C19 5.13 15.87 2 12 2Z" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 11.5C13.3807 11.5 14.5 10.3807 14.5 9C14.5 7.61929 13.3807 6.5 12 6.5C10.6193 6.5 9.5 7.61929 9.5 9C9.5 10.3807 10.6193 11.5 12 11.5Z" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`));
const formattedLastPlayed = formatLastPlayedWithRelative(lastPlayed);
infoItems.appendChild(createInfoItem('Last Played', formattedLastPlayed, `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 8V12L15 15" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="${theme.accentPrimary}" stroke-width="1.5"/>
</svg>
`));
const popupFooter = document.createElement('div');
popupFooter.style.cssText = `
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
`;
const copyButton = document.createElement('button');
copyButton.textContent = 'Copy Info';
copyButton.style.cssText = `
background: rgba(28, 31, 37, 0.6);
color: ${theme.textPrimary};
border: 1px solid rgba(255, 255, 255, 0.12);
padding: 8px 16px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
gap: 6px;
`;
copyButton.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Copy Info
`;
copyButton.addEventListener('click', function() {
const infoText = `Game ID: ${gameId}\nServer ID: ${serverId}\nRegion: ${region}\nLast Played: ${lastPlayed}`;
navigator.clipboard.writeText(infoText);
copyButton.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 6L9 17L4 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Copied!
`;
setTimeout(() => {
copyButton.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Copy Info
`;
}, 2000);
});
const closeButton = document.createElement('button');
closeButton.textContent = 'Close';
closeButton.style.cssText = `
background: rgba(77, 133, 238, 0.15);
color: ${theme.accentPrimary};
border: none;
padding: 8px 24px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
`;
closeButton.addEventListener('click', function() {
popup.style.opacity = '0';
setTimeout(() => {
popup.remove();
}, 200);
});
popupFooter.appendChild(copyButton);
popupFooter.appendChild(closeButton);
popupContent.appendChild(popupHeader);
popupContent.appendChild(infoItems);
popupContent.appendChild(popupFooter);
popup.appendChild(popupContent);
document.body.appendChild(popup);
setTimeout(() => {
popup.style.opacity = '1';
popupContent.style.opacity = '1';
popupContent.style.transform = 'translateY(0)';
}, 10);
popup.addEventListener('click', function(e) {
if (e.target === popup) {
popup.style.opacity = '0';
setTimeout(() => {
popup.remove();
}, 200);
}
});
});
buttonGroup.appendChild(removeButton);
buttonGroup.appendChild(separator);
buttonGroup.appendChild(joinButton);
buttonGroup.appendChild(inviteButton);
buttonGroup.appendChild(moreInfoButton);
serverCard.appendChild(serverIconWrapper);
serverCard.appendChild(left);
serverCard.appendChild(buttonGroup);
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);
if (index === 0) { // makes it feel premium. trust me its not a waste of space hehe
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);
}
cardsWrapper.appendChild(serverCard);
});
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.
*******************************************************/
// stops youtube autoplay in iframes
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 || (!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) {
// url parsing failed, just skip it
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;
// watch for new iframes if needed
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, "<b style='color: #068f00;'>$1</b>");
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, '<span style="font-weight: bold; color: #ff5555;">$1</span>');
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 = "<span style='font-weight: bold; color: #2196F3;'>New Feature</span>: 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, '<span style="font-weight: bold; color: gold;">$1</span>');
};
// 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_")) // server go after!
.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() {
// Store the original state at the beginning using getItem/setItem
const originalNotifFlag = window.localStorage.getItem('ROLOCATE_enablenotifications');
ConsoleLogEnabled("[DEBUG] Original state:", originalNotifFlag);
if (originalNotifFlag === "true") {
window.localStorage.setItem('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.");
// Restore original state before early return
if (originalNotifFlag !== null) {
window.localStorage.setItem('ROLOCATE_enablenotifications', originalNotifFlag);
}
ConsoleLogEnabled("[DEBUG] Restored to:", window.localStorage.getItem('ROLOCATE_enablenotifications'));
ConsoleLogEnabled("[Auto] Notifications restored to original state (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 (originalNotifFlag === "true") {
try {
await waitForDivWithStyleSubstring(
"radial-gradient(circle, rgba(255, 40, 40, 0.4)",
5000
);
// Restore original state
window.localStorage.setItem('ROLOCATE_enablenotifications', originalNotifFlag);
ConsoleLogEnabled("[DEBUG] Restored to:", window.localStorage.getItem('ROLOCATE_enablenotifications'));
ConsoleLogEnabled("[Auto] Notifications restored to original state (style div detected).");
} catch (err) {
ConsoleLogEnabled("[Auto] Style div not detected in time:", err.message);
// Restore original state even if there's an error
window.localStorage.setItem('ROLOCATE_enablenotifications', originalNotifFlag);
ConsoleLogEnabled("[DEBUG] Restored to:", window.localStorage.getItem('ROLOCATE_enablenotifications'));
ConsoleLogEnabled("[Auto] Notifications restored to original state (error occurred).");
}
}
// Final restoration to ensure it's always restored
if (originalNotifFlag !== null) {
window.localStorage.setItem('ROLOCATE_enablenotifications', originalNotifFlag);
}
ConsoleLogEnabled("[DEBUG] Final restore to:", window.localStorage.getItem('ROLOCATE_enablenotifications'));
ConsoleLogEnabled("[Auto] Function completed - notifications restored to original state.");
}
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;
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: rgba(51, 95, 255, 0.9);
color: white;
padding: 6px 10px;
border-radius: 8px;
font-size: 12px;
font-weight: 500;
letter-spacing: 0.025em;
visibility: hidden;
opacity: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%) translateY(4px);
white-space: nowrap;
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04),
0 0 0 1px rgba(255, 255, 255, 0.05);
border: 1px solid rgba(148, 163, 184, 0.1);
z-index: 1000;
/* Arrow */
&::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid rgba(51, 95, 255, 0.9);
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
}
`;
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
*********************************************************************************************************************************************************************************************************************************************/
// Quick join handler for smartsearch
if (window.location.hash === '#?ROLOCATE_QUICKJOIN') {
if (localStorage.ROLOCATE_smartsearch === 'true' || localStorage.ROLOCATE_quicklaunchgames === 'true') { // fixed this
// Extract gameId from URL path (assuming format: /games/gameId)
const gameIdMatch = window.location.pathname.match(/\/games\/(\d+)/);
if (gameIdMatch && gameIdMatch[1]) {
const gameId = gameIdMatch[1];
rebuildServerList(gameId, 50, false, true); // Quick join mode
} else {
ConsoleLogEnabled('[RoLocate] Could not extract gameId from URL');
notifications('Error: Failed to extract gameid. Please try again later.', 'error', '⚠️', '5000');
}
// Clean up the URL
history.replaceState(null, null, window.location.pathname + window.location.search);
} else {
ConsoleLogEnabled('[RoLocate] Quick Join detected but smartsearch is disabled');
}
}
/*********************************************************************************************************************************************************************************************************************************************
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'));
});
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 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);
document.querySelector('#rbx-public-game-server-item-container').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,
serversFound = 0,
serverMaxPlayers = null,
isCloserToOne = null;
let topDownServers = [],
bottomUpServers = []; // Servers collected during searches
let currentDelay = 500; // Initial delay of 0.5 seconds
const timeLimit = 3 * 60 * 1000,
startTime = Date.now(); // 3 minutes limit
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`;
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);
continue;
}
// 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);
} else if (isCloserToOne && server.playing < maxPlayers) {
bottomUpServers.push(server);
}
}
if (!data.nextPageCursor) break;
cursor = data.nextPageCursor;
// Adjust delay dynamically
if (currentDelay > 150) {
currentDelay = Math.max(150, currentDelay / 2);
}
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');
topDownServers.sort((a, b) => a.playing - b.playing);
bottomUpServers.sort((a, b) => b.playing - a.playing);
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 <style> element
const style = document.createElement('style');
style.textContent = `
/* Overlay for the modal background */
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85); /* Solid black overlay */
z-index: 1000; /* Ensure overlay is below the popup */
opacity: 0; /* Start invisible */
animation: fadeIn 0.3s ease forwards; /* Fade-in animation */
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* Popup Container for the server region */
.filter-popup {
background-color: #1e1e1e; /* Darker background */
color: #ffffff; /* White text */
padding: 25px;
border-radius: 12px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.5);
width: 320px;
max-width: 90%;
position: fixed; /* Fixed positioning */
top: 50%; /* Center vertically */
left: 50%; /* Center horizontally */
transform: translate(-50%, -50%); /* Offset to truly center */
text-align: center;
z-index: 1001; /* Ensure popup is above the overlay */
border: 1px solid #444; /* Subtle border */
opacity: 0; /* Start invisible */
animation: fadeInPopup 0.3s ease 0.1s forwards; /* Fade-in animation with delay */
}
@keyframes fadeInPopup {
from {
opacity: 0;
transform: translate(-50%, -55%); /* Slight upward offset */
}
to {
opacity: 1;
transform: translate(-50%, -50%); /* Center position */
}
}
/* Fade-out animation for overlay and popup */
.overlay.fade-out {
animation: fadeOut 0.3s ease forwards;
}
.filter-popup.fade-out {
animation: fadeOutPopup 0.3s ease forwards;
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes fadeOutPopup {
from {
opacity: 1;
transform: translate(-50%, -50%); /* Center position */
}
to {
opacity: 0;
transform: translate(-50%, -55%); /* Slight upward offset */
}
}
/* Label */
.filter-popup label {
display: block;
margin-bottom: 12px;
font-size: 16px;
color: #ffffff;
font-weight: 500; /* Slightly bolder text */
}
/* Dropdown */
.filter-popup select {
background-color: #333; /* Darker gray background */
color: #ffffff; /* White text */
padding: 10px;
border-radius: 6px;
border: 1px solid #555; /* Darker border */
width: 100%;
margin-bottom: 12px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.filter-popup select:focus {
border-color: #888; /* Lighter border on focus */
outline: none;
}
/* Custom Input */
.filter-popup input[type="number"] {
background-color: #333; /* Darker gray background */
color: #ffffff; /* White text */
padding: 10px;
border-radius: 6px;
border: 1px solid #555; /* Darker border */
width: 100%;
margin-bottom: 12px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.filter-popup input[type="number"]:focus {
border-color: #888; /* Lighter border on focus */
outline: none;
}
/* Confirm Button */
#confirmServerCount {
background-color: #444; /* Dark gray background */
color: #ffffff; /* White text */
padding: 10px 20px;
border: 1px solid #666; /* Gray border */
border-radius: 6px;
cursor: pointer;
font-size: 14px;
width: 100%;
transition: background-color 0.3s ease, transform 0.2s ease;
}
#confirmServerCount:hover {
background-color: #555; /* Lighter gray on hover */
transform: translateY(-1px); /* Slight lift effect */
}
#confirmServerCount:active {
transform: translateY(0); /* Reset lift effect on click */
}
/* Highlighted server item */
.rbx-game-server-item.highlighted {
border: 2px solid #4caf50; /* Green border */
border-radius: 8px;
background-color: rgba(76, 175, 80, 0.1); /* Subtle green background */
}
/* Disabled fetch button */
.fetch-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Popup Header for server coutnodwn */
.popup-header {
margin-bottom: 24px;
text-align: left;
padding: 16px;
background-color: rgba(255, 255, 255, 0.05); /* Subtle background for contrast */
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1); /* Subtle border */
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.popup-header:hover {
background-color: rgba(255, 255, 255, 0.08); /* Slightly brighter on hover */
border-color: rgba(255, 255, 255, 0.2);
}
.popup-header h3 {
margin: 0 0 12px 0;
font-size: 22px;
color: #ffffff;
font-weight: 700; /* Bolder for emphasis */
letter-spacing: -0.5px; /* Tighter letter spacing for modern look */
}
.popup-header p {
margin: 0;
font-size: 14px;
color: #cccccc;
line-height: 1.6; /* Improved line height for readability */
opacity: 0.9; /* Slightly transparent for a softer look */
}
/* Popup Footer */
.popup-footer {
margin-top: 20px;
text-align: left;
font-size: 14px;
color: #ffcc00; /* Yellow color for warnings */
background-color: rgba(255, 204, 0, 0.15); /* Lighter yellow background */
padding: 12px;
border-radius: 8px;
border: 1px solid rgba(255, 204, 0, 0.15); /* Subtle border */
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.popup-footer:hover {
background-color: rgba(255, 204, 0, 0.25); /* Slightly brighter on hover */
border-color: rgba(255, 204, 0, 0.25);
}
.popup-footer p {
margin: 0;
line-height: 1.5;
font-weight: 500; /* Slightly bolder for emphasis */
}
/* Label */
.filter-popup label {
display: block;
margin-bottom: 12px;
font-size: 15px;
color: #ffffff;
font-weight: 500;
text-align: left;
opacity: 0.9; /* Slightly transparent for a softer look */
transition: opacity 0.3s ease;
}
.filter-popup label:hover {
opacity: 1; /* Fully opaque on hover */
}
select:hover, select:focus {
border-color: #ffffff;
outline: none;
}
`;
// Append the <style> element to the document head
document.head.appendChild(style);
}
/*******************************************************
name of function: showMessage
description: Shows the good looking messages on the bottom of server region search
*******************************************************/
function showMessage(message) {
const loadMoreButtonContainer = document.querySelector('.rbx-public-running-games-footer');
if (!loadMoreButtonContainer) {
ConsoleLogEnabled("Error: 'Load More' button container not found! Ensure the element exists in the DOM.");
return;
}
const existingMessage = loadMoreButtonContainer.querySelector('.premium-message-container');
// If message is "END", remove any existing message and exit
if (message === "END") {
if (existingMessage) {
existingMessage.remove();
ConsoleLogEnabled("Message container removed.");
} else {
ConsoleLogEnabled("No message container found to remove.");
}
return;
}
// Remove existing message if present before showing a new one
if (existingMessage) {
existingMessage.remove();
ConsoleLogEnabled("Warning: An existing message was found and replaced.");
}
// Inject CSS only once
if (!document.getElementById('premium-message-styles')) {
const style = document.createElement('style');
style.id = 'premium-message-styles';
style.textContent = `
.premium-message-container {
margin-top: 20px;
padding: 18px 26px;
background: linear-gradient(145deg, #2b0000, #1a0000);
border-radius: 14px;
box-shadow: 0 6px 20px rgba(255, 0, 0, 0.2);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 16px;
color: #ffdddd;
transition: all 0.3s ease-in-out, transform 0.3s ease, box-shadow 0.3s ease;
opacity: 0;
animation: fadeIn 0.6s ease forwards;
border: 1px solid #440000;
display: flex;
align-items: center;
gap: 16px;
cursor: default;
user-select: none;
}
.premium-message-container:hover {
transform: scale(1.015);
box-shadow: 0 8px 24px rgba(255, 0, 0, 0.25);
background: linear-gradient(145deg, #330000, #220000);
color: #ffe5e5;
}
.premium-message-logo {
width: 28px;
height: 28px;
border-radius: 6px;
object-fit: contain;
box-shadow: 0 0 8px rgba(255, 0, 0, 0.2);
background-color: #000;
}
.premium-message-text {
flex: 1;
text-align: left;
font-weight: 500;
letter-spacing: 0.3px;
}
@keyframes fadeIn {
to { opacity: 1; }
}
`;
document.head.appendChild(style);
}
// Create the message container
const container = document.createElement('div');
container.className = 'premium-message-container';
// Create and insert the logo
const logo = document.createElement('img');
logo.className = 'premium-message-logo';
logo.src = window.Base64Images.logo;
// Create and insert the message text
const messageText = document.createElement('div');
messageText.className = 'premium-message-text';
messageText.textContent = message;
// Build the full component
container.appendChild(logo);
container.appendChild(messageText);
loadMoreButtonContainer.appendChild(container);
ConsoleLogEnabled("Message displayed successfully:", message);
return container;
}
/*******************************************************
name of function: fetchServerDetails
description: Function to fetch server details so game id and job id. yea!
*******************************************************/
// WARNING: Do not republish this script. Licensed for personal use only.
async function fetchServerDetails(gameId, jobId) { //here!
const useBatching = localStorage.ROLOCATE_fastservers === "true";
if (!useBatching) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "POST",
url: "https://gamejoin.roblox.com/v1/join-game-instance",
headers: {
"Content-Type": "application/json",
"User-Agent": "Roblox/WinInet",
},
data: JSON.stringify({
placeId: gameId,
gameId: jobId
}),
onload: function(response) {
const json = JSON.parse(response.responseText);
ConsoleLogEnabled("API Response:", json);
if (json.status === 12 && json.message === 'You need to purchase access to this game before you can play.') {
reject('purchase_required');
return;
}
if (json.status === 12 && json.message === 'Cannot join this non-root place due to join restrictions') {
reject('subplace_join_restriction');
return;
}
const address = json?.joinScript?.UdmuxEndpoints?.[0]?.Address ?? json?.joinScript?.MachineAddress;
if (!address) {
ConsoleLogEnabled("API Response (Unknown Location) Which means Full Server!:", json);
reject(`Unable to fetch server location: Status ${json.status}`);
return;
}
function findBestMatchingIp(address) {
// If IP starts with 128.116.*, replace last octet with 0
if (/^128\.116\.\d+\.\d+$/.test(address)) {
return address.replace(/^(128\.116\.\d+)\.\d+$/, "$1.0");
}
// Otherwise, find any IP in serverRegionsByIp sharing first two octets
const firstTwoOctetsMatch = address.match(/^(\d+\.\d+)\./);
if (firstTwoOctetsMatch) {
const firstTwoOctets = firstTwoOctetsMatch[1];
const match = Object.keys(serverRegionsByIp).find(ip => ip.startsWith(firstTwoOctets + "."));
return match || address;
}
// If format unexpected, fallback to original IP
return address;
}
const lookupIp = findBestMatchingIp(address);
const location = serverRegionsByIp[lookupIp];
if (!location) {
ConsoleLogEnabled("API Response (Unknown Location):", json);
reject(`Unknown server address ${address}`);
return;
}
resolve(location);
},
onerror: function(error) {
ConsoleLogEnabled("API Request Failed:", error);
reject(`Failed to fetch server details: ${error}`);
},
});
});
}
// Batching logic with rate limit handling
const queue = fetchServerDetails._queue || [];
const concurrencyLimit = 100; // this can be any value from 1 to 2000 (integer)
if (!fetchServerDetails._queue) {
fetchServerDetails._queue = queue;
fetchServerDetails._activeCount = 0;
fetchServerDetails._rateLimited = false;
}
return new Promise((resolve, reject) => {
const makeRequest = async (gameId, jobId) => {
return new Promise((innerResolve, innerReject) => {
GM_xmlhttpRequest({
method: "POST",
url: "https://gamejoin.roblox.com/v1/join-game-instance",
headers: {
"Content-Type": "application/json",
"User-Agent": "Roblox/WinInet",
},
data: JSON.stringify({
placeId: gameId,
gameId: jobId
}),
onload: function(response) {
const json = JSON.parse(response.responseText);
ConsoleLogEnabled("API Response:", json);
// Check if we got rate limited (status undefined)
if (json.status === undefined) {
ConsoleLogEnabled("Rate limited detected - status undefined");
innerReject('rate_limited');
return;
}
if (json.status === 12 && json.message === 'You need to purchase access to this game before you can play.') {
innerReject('purchase_required');
return;
}
if (json.status === 12 && json.message === 'Cannot join this non-root place due to join restrictions') {
innerReject('subplace_join_restriction');
return;
}
const address = json?.joinScript?.UdmuxEndpoints?.[0]?.Address ?? json?.joinScript?.MachineAddress;
if (!address) {
ConsoleLogEnabled("API Response (Unknown Location) Which means Full Server!:", json);
innerReject(`Unable to fetch server location: Status ${json.status}`);
return;
}
function findBestMatchingIp(address) {
// If IP starts with 128.116.*, replace last octet with 0
if (/^128\.116\.\d+\.\d+$/.test(address)) {
ConsoleLogEnabled("using 128.116 rule");
return address.replace(/^(128\.116\.\d+)\.\d+$/, "$1.0");
}
// Otherwise, find any IP in serverRegionsByIp sharing first two octets
const firstTwoOctetsMatch = address.match(/^(\d+\.\d+)\./);
if (firstTwoOctetsMatch) {
ConsoleLogEnabled("using two octect rule");
const firstTwoOctets = firstTwoOctetsMatch[1];
const match = Object.keys(serverRegionsByIp).find(ip => ip.startsWith(firstTwoOctets + "."));
return match || address;
}
// If format unexpected, fallback to original IP
return address;
}
const lookupIp = findBestMatchingIp(address);
const location = serverRegionsByIp[lookupIp];
if (!location) {
ConsoleLogEnabled("API Response (Unknown Location):", json);
innerReject(`Unknown server address ${address}`);
return;
}
innerResolve(location);
},
onerror: function(error) {
ConsoleLogEnabled("API Request Failed:", error);
innerReject(`Failed to fetch server details: ${error}`);
},
});
});
};
const task = async () => {
try {
fetchServerDetails._activeCount++;
let result;
let attempts = 0;
const maxAttempts = 100; // Prevent infinite loops
while (attempts < maxAttempts) {
try {
result = await makeRequest(gameId, jobId);
// If we get here, request was successful
if (fetchServerDetails._rateLimited) {
ConsoleLogEnabled("Rate limit cleared, resuming normal operation");
fetchServerDetails._rateLimited = false;
}
break;
} catch (err) {
if (err === 'rate_limited') {
if (!fetchServerDetails._rateLimited) {
ConsoleLogEnabled("Rate limited - retrying every second until cleared");
fetchServerDetails._rateLimited = true;
}
ConsoleLogEnabled(`Rate limit retry attempt ${attempts + 1}`);
await delay(1000); // Wait 1 second before retry
attempts++;
} else {
// For other errors, don't retry
throw err;
}
}
}
if (attempts >= maxAttempts) {
throw new Error(`Rate limited for too long, exceeded ${maxAttempts} attempts`);
}
resolve(result);
} catch (err) {
reject(err);
} finally {
fetchServerDetails._activeCount--;
if (queue.length > 0) {
const next = queue.shift();
next();
}
}
};
if (fetchServerDetails._activeCount < concurrencyLimit) {
task();
} else {
queue.push(task);
}
});
}
/*******************************************************
name of function: delay
description: custom delay also known as sleep function in js cause this language sucks and doesent have a default built-in sleep.
*******************************************************/
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/*******************************************************
name of function: createServerCountPopup
description: Creates the first time popup and allows user to pick the amount of servers they want.
*******************************************************/
// WARNING: Do not republish this script. Licensed for personal use only.
function createServerCountPopup(callback) {
const overlay = document.createElement('div');
overlay.className = 'overlay';
const popup = document.createElement('div');
popup.className = 'filter-popup';
popup.style.width = '460px';
// get current player count preference from localStorage
const currentPlayerCountPreference = localStorage.getItem('ROLOCATE_invertplayercount');
const isLowPlayerCount = currentPlayerCountPreference === 'true';
// inject styles for dropdown icon
const style = document.createElement('style');
style.textContent = `
/* NEW: Grid container for the dropdowns */
.filter-grid {
display: grid;
grid-template-columns: 1fr 1fr; /* Create two equal columns */
gap: 20px; /* Space between the columns */
margin-bottom: 15px;
}
.dropdown-wrapper {
position: relative;
display: inline-block;
width: 100%;
}
.dropdown-wrapper select {
width: 100%;
padding-right: 30px;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
.dropdown-wrapper .dropdown-icon {
position: absolute;
right: 10px;
top: 40%;
transform: translateY(-50%);
pointer-events: none;
font-size: 12px;
color: #fff;
}
.filter-section label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
#cancelServerCount {
background-color: #2a1f1f;
border: 1px solid #3d2626;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
}
#cancelServerCount:hover {
background-color: #332222;
transform: translateY(-1px); /* Slight lift effect */
}
#cancelServerCount:active {
transform: translateY(0); /* Reset lift effect on click */
}
`;
document.head.appendChild(style);
popup.innerHTML = `
<div class="popup-header">
<h3>Select Number of Servers</h3>
<p><strong>More servers = more variety, but longer search times.</strong></p>
</div>
<div class="filter-grid">
<div class="filter-section">
<label for="serverCount">Number of Servers:</label>
<div class="dropdown-wrapper">
<select id="serverCount">
<option value="10">10 Servers</option>
<option value="25" selected>25 Servers</option>
<option value="100">100 Servers</option>
<option value="200">200 Servers</option>
<option value="500">500 Servers</option>
<option value="1000">1000 Servers</option>
<option value="2000">2000 Servers</option>
<option value="custom">Custom</option>
</select>
<span class="dropdown-icon">▼</span>
</div>
<input id="customServerCount" type="number" min="1" max="2000" placeholder="Enter number (1–2000)" style="display: none; margin-top: 5px; width: calc(100% - 10px);">
</div>
<div class="filter-section">
<label for="playerCountFilter">Find Servers with:</label>
<div class="dropdown-wrapper">
<select id="playerCountFilter">
<option value="high" ${!isLowPlayerCount ? 'selected' : ''}>High Player Counts</option>
<option value="low" ${isLowPlayerCount ? 'selected' : ''}>Low Player Counts</option>
</select>
<span class="dropdown-icon">▼</span>
</div>
</div>
</div>
<div class="popup-footer" style="text-align: left; margin-top: 0;">
<p><strong>Note:</strong> If you have fast servers on, the buildman thumbnails are intentional! It's because it saves time for the search.</p>
</div>
<div style="display: flex; gap: 10px; margin-top: 15px;">
<button id="cancelServerCount" style="width:25%;">Cancel</button>
<button id="confirmServerCount" style="width: 75%;">Confirm</button>
</div>
`;
document.body.appendChild(overlay);
document.body.appendChild(popup);
const serverCountDropdown = popup.querySelector('#serverCount');
const customServerCountInput = popup.querySelector('#customServerCount');
const playerCountFilter = popup.querySelector('#playerCountFilter');
const confirmButton = popup.querySelector('#confirmServerCount');
const cancelButton = popup.querySelector('#cancelServerCount');
serverCountDropdown.addEventListener('change', () => {
if (serverCountDropdown.value === 'custom') {
customServerCountInput.style.display = 'block';
} else {
customServerCountInput.style.display = 'none';
}
});
confirmButton.addEventListener('click', () => {
let serverCount;
if (serverCountDropdown.value === 'custom') {
serverCount = parseInt(customServerCountInput.value);
if (isNaN(serverCount) || serverCount < 1 || serverCount > 2000) {
notifications('Error: Please enter a valid number between 1 and 2000.', 'error', '⚠️', '5000');
return;
}
} else {
serverCount = parseInt(serverCountDropdown.value);
}
const playerCountPreference = playerCountFilter.value;
localStorage.setItem('ROLOCATE_invertplayercount', playerCountPreference === 'low' ? 'true' : 'false');
callback(serverCount);
disableFilterButton(true);
disableLoadMoreButton(true);
hidePopup();
Loadingbar(true);
});
cancelButton.addEventListener('click', () => {
hidePopup();
});
function hidePopup() {
const overlay = document.querySelector('.overlay');
const popup = document.querySelector('.filter-popup');
overlay.classList.add('fade-out');
popup.classList.add('fade-out');
setTimeout(() => {
overlay.remove();
popup.remove();
}, 300);
}
}
/*******************************************************
name of function: fetchPublicServers
description: Function to fetch public servers with rate limtiing and stuff (Server regions)
*******************************************************/
// WARNING: Do not republish this script. Licensed for personal use only.
async function fetchPublicServers(gameId, totalLimit) {
let servers = [];
let cursor = null;
let delayTime = 250; // Start with 0.25 seconds
let retryingDueToRateLimit = false;
let pageCount = 0;
const invertPlayerCount = localStorage.getItem("ROLOCATE_invertplayercount") === "true";
ConsoleLogEnabled(`Starting to fetch up to ${totalLimit} public servers for game ${gameId}...`);
ConsoleLogEnabled(`Invert player count: ${invertPlayerCount}`);
while (servers.length < totalLimit) {
const url = `https://games.roblox.com/v1/games/${gameId}/servers/public?excludeFullGames=true&limit=100${invertPlayerCount ? '&sortOrder=1' : ''}${cursor ? `&cursor=${cursor}` : ''}`;
pageCount++;
ConsoleLogEnabled(`Fetching page ${pageCount}... (Current delay: ${delayTime}ms)`);
let responseData;
try {
responseData = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: function(response) {
if (response.status === 429 || !response.responseText) {
reject({
rateLimited: true
});
} else {
try {
const json = JSON.parse(response.responseText);
resolve(json);
} catch (err) {
reject({
rateLimited: true
});
}
}
},
onerror: function(error) {
reject({
rateLimited: false,
error
});
},
});
});
if (retryingDueToRateLimit) {
delayTime = 250;
retryingDueToRateLimit = false;
ConsoleLogEnabled(`Rate limit cleared. Resuming normal delay (${delayTime}ms).`);
}
const newServers = responseData.data || [];
servers = servers.concat(newServers);
ConsoleLogEnabled(`Fetched ${newServers.length} servers (Total: ${servers.length}/${totalLimit})`);
if (!responseData.nextPageCursor || servers.length >= totalLimit) {
ConsoleLogEnabled("No more pages or reached limit.");
break;
}
cursor = responseData.nextPageCursor;
} catch (err) {
if (err.rateLimited) {
delayTime = 750;
retryingDueToRateLimit = true;
ConsoleLogEnabled("⚠️ Rate limited. Increasing delay to 0.75s...");
} else {
ConsoleLogEnabled("❌ Failed to fetch due to error:", err.error);
break;
}
}
await delay(delayTime);
}
ConsoleLogEnabled(`✅ Done. Fetched ${servers.length} servers in total.`);
return servers.slice(0, totalLimit);
}
/*******************************************************
name of function: createFilterDropdowns
description: Creates the server selecting dropdown with country flags.
*******************************************************/
function createFilterDropdowns(servers) {
// get flag data
getFlagEmoji(); // load flag data without country code
// create the main filter container with premium styling
const filterContainer = document.createElement('div');
Object.assign(filterContainer.style, {
display: 'flex',
gap: '32px',
alignItems: 'center',
padding: '40px 48px',
background: 'linear-gradient(145deg, rgba(12,12,12,0.98) 0%, rgba(8,8,8,0.98) 25%, rgba(15,10,10,0.98) 75%, rgba(10,8,8,0.98) 100%)',
borderRadius: '28px',
boxShadow: '0 32px 64px rgba(0,0,0,0.6), 0 0 0 1px rgba(200,30,30,0.15), inset 0 1px 0 rgba(255,255,255,0.02)',
opacity: '0',
transform: 'translateY(-50px) scale(0.94)',
transition: 'all 1.2s cubic-bezier(0.16, 1, 0.3, 1)',
position: 'relative',
border: '1px solid rgba(200,30,30,0.12)',
margin: '40px',
fontFamily: "'Inter', 'SF Pro Display', system-ui, -apple-system, sans-serif",
fontSize: '16px',
overflow: 'hidden'
});
// Premium animated border with subtle red glow
const borderGlow = document.createElement('div');
Object.assign(borderGlow.style, {
position: 'absolute',
inset: '-2px',
borderRadius: '30px',
pointerEvents: 'none',
background: 'linear-gradient(60deg, rgba(200,25,25,0.25), rgba(50,50,50,0.1), rgba(200,25,25,0.15), rgba(30,30,30,0.1), rgba(200,25,25,0.2))',
backgroundSize: '300% 300%',
zIndex: '-1',
animation: 'premiumFlow 20s ease infinite',
opacity: '0.7',
filter: 'blur(0.5px)'
});
filterContainer.appendChild(borderGlow);
// Add premium CSS animations and styling
const style = document.createElement('style');
style.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
@keyframes premiumFlow {
0% { background-position: 0% 50%; transform: rotate(0deg); }
25% { background-position: 100% 25%; }
50% { background-position: 100% 100%; transform: rotate(0.5deg); }
75% { background-position: 0% 75%; }
100% { background-position: 0% 50%; transform: rotate(0deg); }
}
@keyframes premiumPulse {
0% { box-shadow: 0 0 0 0 rgba(200, 30, 30, 0.4); }
50% { box-shadow: 0 0 0 20px rgba(200, 30, 30, 0); }
100% { box-shadow: 0 0 0 0 rgba(200, 30, 30, 0); }
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
@keyframes iconFloat {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-2px); }
}
.premium-select {
scrollbar-width: thin;
scrollbar-color: rgba(200,30,30,0.6) rgba(20,20,20,0.4);
}
.premium-select::-webkit-scrollbar {
width: 6px;
}
.premium-select::-webkit-scrollbar-track {
background: rgba(15,15,15,0.8);
border-radius: 10px;
}
.premium-select::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(200,30,30,0.8), rgba(150,25,25,0.6));
border-radius: 10px;
border: 1px solid rgba(0,0,0,0.2);
}
.premium-select::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, rgba(220,35,35,0.9), rgba(170,30,30,0.7));
}
.logo-premium-pulse {
animation: premiumPulse 3s infinite;
}
.shimmer-effect {
position: relative;
overflow: hidden;
}
.shimmer-effect::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.08), transparent);
animation: shimmer 3s infinite;
}
.premium-icon {
animation: iconFloat 3s ease-in-out infinite;
}
/* Enhanced flag styling for dropdowns */
.flag-option-container {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
transition: all 0.3s ease;
}
.flag-option-container:hover {
background: rgba(200,30,30,0.1);
border-radius: 6px;
}
.flag-image {
width: 26px !important; /* Slightly larger */
height: 20px !important; /* Slightly larger */
object-fit: cover;
object-position: center;
overflow: hidden;
border-radius: 3px;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
flex-shrink: 0;
/* Crop the edges to hide outline */
clip-path: inset(1px 1px 1px 1px);
}
/* Custom select styling for flags */
.premium-select option {
padding: 12px 16px;
background: rgba(15,15,15,0.98) !important;
color: rgba(200,30,30,0.9) !important;
border-radius: 8px;
margin: 2px;
display: flex;
align-items: center;
}
`;
document.head.appendChild(style);
// Enhanced premium logo with sophisticated hover effects
const logoWrapper = document.createElement('div');
Object.assign(logoWrapper.style, {
position: 'relative',
marginRight: '36px',
display: 'flex',
alignItems: 'center',
cursor: 'pointer'
});
const logoContainer = document.createElement('div');
Object.assign(logoContainer.style, {
position: 'relative',
padding: '8px',
borderRadius: '20px',
background: 'linear-gradient(145deg, rgba(25,25,25,0.8), rgba(15,15,15,0.9))',
border: '1px solid rgba(200,30,30,0.2)',
transition: 'all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1)'
});
const logo = document.createElement('img');
logo.src = window.Base64Images.logo;
Object.assign(logo.style, {
width: '64px',
height: '64px',
borderRadius: '14px',
transition: 'all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1)',
filter: 'drop-shadow(0 12px 24px rgba(200,30,30,0.4))',
border: '2px solid rgba(200,30,30,0.3)',
});
const logoGlow = document.createElement('div');
Object.assign(logoGlow.style, {
position: 'absolute',
inset: '-6px',
borderRadius: '24px',
background: 'radial-gradient(circle at center, rgba(200,30,30,0.5) 0%, rgba(200,30,30,0.1) 50%, transparent 70%)',
opacity: '0',
transition: 'all 0.6s ease',
pointerEvents: 'none',
zIndex: '-1',
});
// Premium logo interactions
logoContainer.addEventListener('mouseover', () => {
logo.style.transform = 'rotate(-6deg) scale(1.12)';
logo.style.filter = 'drop-shadow(0 16px 32px rgba(200,30,30,0.6))';
logo.style.border = '2px solid rgba(200,30,30,0.7)';
logoContainer.style.background = 'linear-gradient(145deg, rgba(35,35,35,0.9), rgba(20,20,20,0.95))';
logoContainer.style.border = '1px solid rgba(200,30,30,0.4)';
logoGlow.style.opacity = '1';
logo.classList.add('logo-premium-pulse');
});
logoContainer.addEventListener('mouseout', () => {
logo.style.transform = 'rotate(0) scale(1)';
logo.style.filter = 'drop-shadow(0 12px 24px rgba(200,30,30,0.4))';
logo.style.border = '2px solid rgba(200,30,30,0.3)';
logoContainer.style.background = 'linear-gradient(145deg, rgba(25,25,25,0.8), rgba(15,15,15,0.9))';
logoContainer.style.border = '1px solid rgba(200,30,30,0.2)';
logoGlow.style.opacity = '0';
logo.classList.remove('logo-premium-pulse');
});
logoContainer.appendChild(logoGlow);
logoContainer.appendChild(logo);
logoWrapper.appendChild(logoContainer);
filterContainer.appendChild(logoWrapper);
// Function to create premium icon
const createIcon = (type) => {
const iconMap = {
globe: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="m16 12 4-4-4-4"/><path d="m8 12-4 4 4 4"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>`,
city: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z"/><path d="M6 12H4a2 2 0 0 0-2 2v8h4"/><path d="M18 9h2a2 2 0 0 1 2 2v11h-4"/><path d="M10 6h4"/><path d="M10 10h4"/><path d="M10 14h4"/><path d="M10 18h4"/></svg>`,
chevron: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>`
};
return iconMap[type] || '';
};
// Helper function to get country code from country name
const getCountryCode = (countryName) => {
// Common country name to code mappings - extend as needed
const countryCodeMap = {
'Australia': 'AU',
'Brazil': 'BR',
'Germany': 'DE',
'France': 'FR',
'United Kingdom': 'GB',
'Hong Kong': 'HK',
'India': 'IN',
'Japan': 'JP',
'Netherlands': 'NL',
'Poland': 'PL',
'Singapore': 'SG',
'United States': 'US'
};
// Return the country code or the first two letters of the country name as fallback
return countryCodeMap[countryName] || countryName.substring(0, 2).toUpperCase();
};
// Function to create a premium dropdown with enhanced styling and icons
const createDropdown = (id, placeholder, iconType) => {
const wrapper = document.createElement('div');
Object.assign(wrapper.style, {
position: 'relative',
minWidth: '280px',
flex: '1'
});
// Premium label with icon
const labelContainer = document.createElement('div');
Object.assign(labelContainer.style, {
display: 'flex',
alignItems: 'center',
gap: '10px',
marginBottom: '14px',
opacity: '0',
transform: 'translateX(-10px)',
transition: 'all 0.6s ease'
});
const labelIcon = document.createElement('span');
labelIcon.innerHTML = createIcon(iconType);
labelIcon.className = 'premium-icon';
Object.assign(labelIcon.style, {
color: 'rgba(200,30,30,0.8)',
display: 'flex',
alignItems: 'center',
filter: 'drop-shadow(0 2px 4px rgba(200,30,30,0.3))'
});
const label = document.createElement('div');
label.textContent = placeholder.replace('All ', '').toUpperCase();
Object.assign(label.style, {
color: 'rgba(255,255,255,0.85)',
fontSize: '13px',
fontWeight: '600',
letterSpacing: '1px',
transition: 'all 0.4s ease',
fontFamily: "'Inter', sans-serif"
});
labelContainer.appendChild(labelIcon);
labelContainer.appendChild(label);
wrapper.appendChild(labelContainer);
// Premium dropdown with enhanced design
const dropdownContainer = document.createElement('div');
dropdownContainer.className = 'shimmer-effect';
Object.assign(dropdownContainer.style, {
position: 'relative',
borderRadius: '16px',
background: 'linear-gradient(145deg, rgba(20,20,20,0.95), rgba(12,12,12,0.98))',
border: '1px solid rgba(200,30,30,0.15)',
overflow: 'hidden',
transition: 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)',
boxShadow: '0 12px 24px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.03)'
});
const dropdown = document.createElement('select');
dropdown.id = id;
dropdown.className = 'premium-select';
dropdown.innerHTML = `<option value="">${placeholder}</option>`;
Object.assign(dropdown.style, {
width: '100%',
padding: '20px 60px 20px 28px',
fontSize: '16px',
fontWeight: '500',
background: 'transparent',
color: 'rgba(200,30,30,0.95)',
border: 'none',
borderRadius: '16px',
appearance: 'none',
cursor: 'pointer',
transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
opacity: '0',
transform: 'translateY(-25px)',
letterSpacing: '0.4px',
fontFamily: "'Inter', sans-serif",
outline: 'none'
});
// Premium chevron with enhanced styling
const chevronContainer = document.createElement('div');
Object.assign(chevronContainer.style, {
position: 'absolute',
right: '20px',
top: '50%',
transform: 'translateY(-50%)',
pointerEvents: 'none',
transition: 'all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1)',
color: 'rgba(200,30,30,0.8)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '6px',
borderRadius: '8px',
background: 'rgba(200,30,30,0.1)',
border: '1px solid rgba(200,30,30,0.2)'
});
chevronContainer.innerHTML = createIcon('chevron');
// Enhanced dropdown interactions with premium effects
const addHoverEffect = () => {
dropdownContainer.style.background = 'linear-gradient(145deg, rgba(30,30,30,0.98), rgba(18,18,18,1))';
dropdownContainer.style.boxShadow = '0 20px 40px rgba(0,0,0,0.5), 0 0 0 2px rgba(200,30,30,0.3), inset 0 1px 0 rgba(255,255,255,0.05)';
dropdownContainer.style.border = '1px solid rgba(200,30,30,0.3)';
dropdownContainer.style.transform = 'translateY(-2px)';
label.style.color = 'rgba(200,30,30,0.95)';
labelIcon.style.color = 'rgba(200,30,30,1)';
chevronContainer.style.transform = 'translateY(-50%) rotate(180deg)';
chevronContainer.style.background = 'rgba(200,30,30,0.2)';
chevronContainer.style.border = '1px solid rgba(200,30,30,0.4)';
};
const removeHoverEffect = () => {
if (document.activeElement !== dropdown) {
dropdownContainer.style.background = 'linear-gradient(145deg, rgba(20,20,20,0.95), rgba(12,12,12,0.98))';
dropdownContainer.style.boxShadow = '0 12px 24px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.03)';
dropdownContainer.style.border = '1px solid rgba(200,30,30,0.15)';
dropdownContainer.style.transform = 'translateY(0)';
label.style.color = 'rgba(255,255,255,0.85)';
labelIcon.style.color = 'rgba(200,30,30,0.8)';
chevronContainer.style.transform = 'translateY(-50%) rotate(0deg)';
chevronContainer.style.background = 'rgba(200,30,30,0.1)';
chevronContainer.style.border = '1px solid rgba(200,30,30,0.2)';
}
};
dropdownContainer.addEventListener('mouseover', addHoverEffect);
dropdownContainer.addEventListener('mouseout', removeHoverEffect);
dropdown.addEventListener('focus', () => {
dropdownContainer.style.outline = 'none';
dropdownContainer.style.border = '1px solid rgba(200,30,30,0.5)';
dropdownContainer.style.boxShadow = '0 20px 40px rgba(0,0,0,0.5), 0 0 0 4px rgba(200,30,30,0.25), inset 0 1px 0 rgba(255,255,255,0.05)';
label.style.color = 'rgba(200,30,30,1)';
labelIcon.style.color = 'rgba(200,30,30,1)';
chevronContainer.style.transform = 'translateY(-50%) rotate(180deg)';
});
dropdown.addEventListener('blur', removeHoverEffect);
dropdown.addEventListener('change', () => {
// Premium selection animation
dropdownContainer.style.transform = 'translateY(-2px) scale(0.98)';
setTimeout(() => {
dropdownContainer.style.transform = 'translateY(-2px) scale(1)';
}, 150);
// Enhanced flash effect
const flash = document.createElement('div');
Object.assign(flash.style, {
position: 'absolute',
inset: '0',
borderRadius: '16px',
background: 'linear-gradient(145deg, rgba(200,30,30,0.2), rgba(200,30,30,0.1))',
pointerEvents: 'none',
opacity: '0',
transition: 'opacity 0.4s ease'
});
dropdownContainer.appendChild(flash);
flash.style.opacity = '1';
setTimeout(() => {
flash.style.opacity = '0';
setTimeout(() => dropdownContainer.removeChild(flash), 400);
}, 80);
});
// Staggered fade-in animation
setTimeout(() => {
labelContainer.style.opacity = '1';
labelContainer.style.transform = 'translateX(0)';
}, 400);
setTimeout(() => {
dropdown.style.opacity = '1';
dropdown.style.transform = 'translateY(0)';
}, 600);
dropdownContainer.appendChild(dropdown);
dropdownContainer.appendChild(chevronContainer);
wrapper.appendChild(dropdownContainer);
return wrapper;
};
// Create premium dropdowns with icons
const countryDropdown = createDropdown('countryFilter', 'All Countries', 'globe');
const cityDropdown = createDropdown('cityFilter', 'All Cities', 'city');
// Populate dropdowns with server data and flags
const countryCounts = {};
const countryServerMap = {}; // To store server info for each country
servers.forEach(server => {
const country = server.location.country.name;
countryCounts[country] = (countryCounts[country] || 0) + 1;
if (!countryServerMap[country]) {
countryServerMap[country] = server; // Store first server for country code reference
}
});
const sortedCountries = Object.keys(countryCounts).sort();
const countrySelect = countryDropdown.querySelector('select');
sortedCountries.forEach(country => {
const option = document.createElement('option');
option.value = country;
// Try to get country code from server data first, then fallback to mapping
let countryCode;
const server = countryServerMap[country];
if (server && server.location.country.code) {
countryCode = server.location.country.code;
} else {
countryCode = getCountryCode(country);
}
// Create flag element
try {
const flagImg = getFlagEmoji(countryCode);
if (flagImg) {
flagImg.className = 'flag-image';
// Since we can't directly add HTML to option text, we'll use a data attribute
// and handle the display with CSS or JavaScript
option.setAttribute('data-flag-src', flagImg.src);
option.setAttribute('data-country-code', countryCode);
option.textContent = `${country} (${countryCounts[country]})`;
}
} catch (error) {
ConsoleLogEnabled(`Could not load flag for ${country} (${countryCode}):`, error);
option.textContent = `${country} (${countryCounts[country]})`;
}
Object.assign(option.style, {
background: 'rgba(15,15,15,0.98)',
color: 'rgba(200,30,30,0.9)',
padding: '12px',
borderRadius: '8px',
margin: '2px'
});
countrySelect.appendChild(option);
});
// Create a custom dropdown display that shows flags
const createCustomDropdownDisplay = (selectElement) => {
const customDisplay = document.createElement('div');
Object.assign(customDisplay.style, {
position: 'absolute',
top: '0',
left: '0',
right: '0',
bottom: '0',
display: 'flex',
alignItems: 'center',
padding: '20px 60px 20px 28px',
pointerEvents: 'none',
zIndex: '1',
color: 'rgba(200,30,30,0.95)',
fontSize: '16px',
fontWeight: '500',
letterSpacing: '0.4px',
fontFamily: "'Inter', sans-serif"
});
const updateDisplay = () => {
const selectedOption = selectElement.options[selectElement.selectedIndex];
if (selectedOption && selectedOption.getAttribute('data-flag-src')) {
const flagSrc = selectedOption.getAttribute('data-flag-src');
const countryCode = selectedOption.getAttribute('data-country-code');
customDisplay.innerHTML = `
<img src="${flagSrc}"
alt="${countryCode}"
class="flag-image"
style="width: 24px; height: 18px; margin-right: 12px; border-radius: 3px; box-shadow: 0 2px 4px rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1);">
<span>${selectedOption.textContent}</span>
`;
} else {
customDisplay.textContent = selectedOption ? selectedOption.textContent : selectElement.options[0].textContent;
}
};
selectElement.addEventListener('change', updateDisplay);
updateDisplay(); // Initial display
return customDisplay;
};
// Add custom display to country dropdown
const countryDropdownContainer = countryDropdown.querySelector('.shimmer-effect');
const countryCustomDisplay = createCustomDropdownDisplay(countrySelect);
countryDropdownContainer.appendChild(countryCustomDisplay);
// Make the original select transparent when it has a selection
countrySelect.addEventListener('change', () => {
if (countrySelect.value) {
countrySelect.style.color = 'transparent';
} else {
countrySelect.style.color = 'rgba(200,30,30,0.95)';
}
});
// Premium separator with gradient
const separator = document.createElement('div');
Object.assign(separator.style, {
height: '80px',
width: '2px',
background: 'linear-gradient(to bottom, rgba(255,255,255,0), rgba(200,30,30,0.4) 20%, rgba(200,30,30,0.6) 50%, rgba(200,30,30,0.4) 80%, rgba(255,255,255,0))',
margin: '0 8px',
borderRadius: '2px',
position: 'relative',
overflow: 'hidden'
});
// Add subtle animation to separator
const separatorGlow = document.createElement('div');
Object.assign(separatorGlow.style, {
position: 'absolute',
inset: '0',
background: 'linear-gradient(to bottom, transparent, rgba(200,30,30,0.8), transparent)',
animation: 'shimmer 4s infinite',
opacity: '0.3'
});
separator.appendChild(separatorGlow);
// Enhanced country change handler with flag support
countrySelect.addEventListener('change', () => {
const selectedCountry = countrySelect.value;
const citySelect = cityDropdown.querySelector('select');
citySelect.innerHTML = '<option value="">All Cities</option>';
if (selectedCountry) {
const cityCounts = {};
servers
.filter(server => server.location.country.name === selectedCountry)
.forEach(server => {
const city = server.location.city;
const region = server.location.region?.name;
const cityKey = region ? `${city}, ${region}` : city;
cityCounts[cityKey] = (cityCounts[cityKey] || 0) + 1;
});
const sortedCities = Object.keys(cityCounts).sort();
sortedCities.forEach(city => {
const option = document.createElement('option');
option.value = city;
option.textContent = `${city} (${cityCounts[city]})`;
Object.assign(option.style, {
background: 'rgba(15,15,15,0.98)',
color: 'rgba(200,30,30,0.9)',
padding: '12px'
});
citySelect.appendChild(option);
});
// Premium update animation
const cityContainer = cityDropdown.querySelector('div');
cityContainer.style.opacity = '0.4';
cityContainer.style.transform = 'translateY(-15px)';
setTimeout(() => {
cityContainer.style.opacity = '1';
cityContainer.style.transform = 'translateY(0)';
}, 200);
// Visual update indicator
const updateRipple = document.createElement('div');
Object.assign(updateRipple.style, {
position: 'absolute',
inset: '0',
borderRadius: '16px',
background: 'radial-gradient(circle at center, rgba(200,30,30,0.3) 0%, rgba(200,30,30,0.1) 40%, transparent 70%)',
pointerEvents: 'none',
opacity: '1',
transition: 'all 1s ease',
transform: 'scale(0.8)'
});
cityDropdown.style.position = 'relative';
cityDropdown.appendChild(updateRipple);
setTimeout(() => {
updateRipple.style.opacity = '0';
updateRipple.style.transform = 'scale(1.2)';
setTimeout(() => cityDropdown.removeChild(updateRipple), 1000);
}, 100);
}
});
// Append elements to container
filterContainer.appendChild(countryDropdown);
filterContainer.appendChild(separator);
filterContainer.appendChild(cityDropdown);
// Premium container entrance animation
setTimeout(() => {
filterContainer.style.opacity = '1';
filterContainer.style.transform = 'translateY(0) scale(1)';
}, 200);
return filterContainer;
}
/*******************************************************
name of function: filterServers
description: Function to filter servers based on selected country and city cause im lazy
*******************************************************/
function filterServers(servers, country, city) {
return servers.filter(server => {
const matchesCountry = !country || server.location.country.name === country;
const matchesCity = !city || `${server.location.city}${server.location.region?.name ? `, ${server.location.region.name}` : ''}` === city;
return matchesCountry && matchesCity;
});
}
/*******************************************************
name of function: fetchPlayerThumbnails_servers
description: not really a function but idc. Finds player thumbnails (Server regions)
*******************************************************/
const fetchPlayerThumbnails_servers = (() => {
const queue = [];
let processing = false;
// Simple transparent 1x1 base64 PNG
const randomBase64Image = () => {
const placeholders = [
window.Base64Images.roblox_avatar,
window.Base64Images.builderman_avatar,
];
const index = Math.floor(Math.random() * placeholders.length);
return placeholders[index];
};
return async function(playerTokens) {
ConsoleLogEnabled("Function called with playerTokens:", playerTokens);
// Check if fast server mode is enabled
if (localStorage.getItem("ROLOCATE_fastservers") === "true") {
ConsoleLogEnabled("ROLOCATE_fastservers is enabled. Returning mock base64 images.");
const mockData = playerTokens.map(token => ({
requestId: `0:${token}:AvatarHeadshot:150x150:png:regular`,
targetId: 0,
state: "Completed",
imageUrl: randomBase64Image(),
}));
return mockData;
}
const waitHalfSecond = (ms = 250) => new Promise(res => setTimeout(res, ms));
return new Promise(resolve => {
ConsoleLogEnabled("Pushing to queue:", playerTokens);
queue.push({
playerTokens,
resolve
});
const processQueue = async () => {
if (processing) {
ConsoleLogEnabled("Already processing, exiting...");
return;
}
processing = true;
ConsoleLogEnabled("Started processing queue...");
while (queue.length > 0) {
const {
playerTokens,
resolve
} = queue.shift();
ConsoleLogEnabled("Processing batch:", playerTokens);
const body = playerTokens.map(token => ({
requestId: `0:${token}:AvatarHeadshot:150x150:png:regular`,
type: "AvatarHeadShot",
targetId: 0,
token,
format: "png",
size: "150x150",
}));
let success = false;
let data = [];
while (!success) {
ConsoleLogEnabled("Sending request to thumbnails.roblox.com...");
const response = await fetch("https://thumbnails.roblox.com/v1/batch", { // bruh i wanna use gmx but using fetch here is fine
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(body),
});
ConsoleLogEnabled("Response status:", response.status);
if (response.status === 429) {
ConsoleLogEnabled("Rate limited. Waiting...");
await waitHalfSecond();
} else {
const json = await response.json();
data = json.data || [];
success = true;
ConsoleLogEnabled("Received data:", data);
}
}
resolve(data);
ConsoleLogEnabled("Resolved promise with data");
}
processing = false;
ConsoleLogEnabled("Finished processing queue.");
};
processQueue();
});
};
})();
/*******************************************************
name of function: rebuildServerList
description: A thicc function to find server regions and create the server cards
*******************************************************/
async function rebuildServerList(gameId, totalLimit, best_connection, quick_join = false) {
const serverListContainer = document.getElementById("rbx-public-game-server-item-container");
const isJoinMode = best_connection || quick_join;
// If in any join mode (best connection or quick join)
if (isJoinMode) {
const originalInvert = localStorage.getItem('ROLOCATE_invertplayercount') === 'true';
let foundServer = false;
try {
// Only disable filter button for best_connection, not for quick_join
if (best_connection) {
disableFilterButton(true);
}
notifications("Retrieving Location...", "success", "🌎", '5000');
const userLocation = await getUserLocation(true); // finally bruh this too too long
if (!userLocation) {
notifications('Error: Unable to fetch your location. Please enable location access or set it to manual in settings.', 'error', '⚠️', '5000');
return;
}
// Attempt to find server (up to 2 attempts)
for (let attempt = 0; attempt < 2 && !foundServer; attempt++) {
const servers = await fetchPublicServers(gameId, 50);
if (servers.length === 0) {
notifications('No servers found for this game.', 'error', '⚠️', '3000');
continue;
}
const isFastServers = localStorage.getItem("ROLOCATE_fastservers") === "true";
let closestServer = null;
let minDistance = Infinity;
let closestServerLocation = null;
if (isFastServers) {
// Parallel processing for fast servers
const results = await Promise.allSettled(
servers.map(async server => {
const {
id: serverId,
maxPlayers,
playing
} = server;
if (playing >= maxPlayers) return null;
try {
const location = await fetchServerDetails(gameId, serverId);
const distance = calculateDistance(
userLocation.latitude,
userLocation.longitude,
location.latitude,
location.longitude
);
return {
server,
location,
distance
};
} catch (error) {
ConsoleLogEnabled(`Error fetching details for server ${serverId}:`, error);
return null;
}
})
);
for (const result of results) {
if (result.status === "fulfilled" && result.value) {
const {
server,
location,
distance
} = result.value;
if (distance < minDistance) {
minDistance = distance;
closestServer = server;
closestServerLocation = location;
}
}
}
} else {
// Sequential processing for regular servers
for (const server of servers) {
const {
id: serverId,
maxPlayers,
playing
} = server;
if (playing >= maxPlayers) continue;
try {
const location = await fetchServerDetails(gameId, serverId);
const distance = calculateDistance(
userLocation.latitude,
userLocation.longitude,
location.latitude,
location.longitude
);
if (distance < minDistance) {
minDistance = distance;
closestServer = server;
closestServerLocation = location;
}
} catch (error) {
ConsoleLogEnabled(`Error fetching details for server ${serverId}:`, error);
continue;
}
}
}
if (closestServer) {
Roblox.GameLauncher.joinGameInstance(gameId, closestServer.id);
notifications(`Joining nearest server! \nDistance: ${Math.round(minDistance / 1.609)} miles | ${Math.round(minDistance)} km`, 'success', '🚀', '5000');
foundServer = true;
} else if (attempt === 0) {
// First attempt failed - try inverted player count
localStorage.setItem('ROLOCATE_invertplayercount', 'true');
notifications('No available servers found. Trying smallest servers...', 'info', '🔄', '3000');
}
}
if (!foundServer) {
notifications('No valid servers found. This game might be popular right now. Try using \'Server Region\' or refresh the page and try again later.', 'error', '⚠️', '8000');
}
} catch (error) {
ConsoleLogEnabled("Error in join mode:", error);
notifications('Error during server search: ' + error.message, 'error', '⚠️', '5000');
} finally {
// Reset to original setting
localStorage.setItem('ROLOCATE_invertplayercount', originalInvert ? 'true' : 'false');
if (best_connection) {
disableFilterButton(false);
}
Loadingbar(false);
}
return;
}
// Rest of the function for normal server list display
if (!serverListContainer) {
ConsoleLogEnabled("Server list container not found!");
notifications('Error: No Servers found. There is nobody playing this game. Please refresh the page.', 'error', '⚠️', '8000');
Loadingbar(false);
return;
}
const messageElement = showMessage("Just a moment — to detect your location accurately, please stay on this page...");
const premium_message = messageElement.querySelector('.premium-message-text');
try {
// Retrieve user's location for distance calculations
const userLocation = await getUserLocation();
if (!userLocation) {
notifications('Error: Unable to fetch your location. Please enable location access.', 'error', '⚠️', '5000');
disableFilterButton(false);
return;
}
const servers = await fetchPublicServers(gameId, totalLimit);
const totalServers = servers.length;
let skippedServers = 0;
if (premium_message) {
premium_message.textContent = `Filtering servers... Please stay on this page to ensure a faster and more accurate search. ${totalServers} servers found, 0 loaded so far.`;
}
notifications(`Please do not leave this page as it slows down the search. \nFound a total of ${totalServers} servers.`, 'success', '👍', '3000');
const serverDetails = [];
const useBatching = localStorage.ROLOCATE_fastservers === "true";
if (useBatching) {
// Process servers in batches of 100
const batchSize = 100;
let processedCount = 0;
for (let i = 0; i < servers.length; i += batchSize) {
const batch = servers.slice(i, i + batchSize);
const batchPromises = batch.map(async (server) => {
const {
id: serverId,
maxPlayers,
playing,
ping,
fps,
playerTokens
} = server;
// Skip full servers early to avoid unnecessary API calls
if (playing >= maxPlayers) {
skippedServers++;
return null;
}
try {
const location = await fetchServerDetails(gameId, serverId);
if (location.city === "Unknown") {
ConsoleLogEnabled(`Skipping server ${serverId} because location is unknown.`);
skippedServers++;
return null;
}
// Fetch player thumbnails
const playerThumbnails = playerTokens && playerTokens.length > 0 ?
await fetchPlayerThumbnails_servers(playerTokens) : [];
return {
server,
location,
playerThumbnails
};
} catch (error) {
if (error === 'purchase_required') {
throw error;
} else if (error === 'subplace_join_restriction') {
throw error;
} else {
ConsoleLogEnabled(error);
skippedServers++;
return null;
}
}
});
// Smoothly update the processed count
function updateProcessedCountSmoothly(startCount, targetCount) {
const increment = 1;
let currentCount = startCount;
const interval = setInterval(() => {
if (currentCount < targetCount) {
currentCount += increment;
if (premium_message) {
premium_message.textContent = `Filtering servers, please do not leave this page...\n${totalServers} servers found, ${currentCount} server locations found`;
}
} else {
clearInterval(interval);
}
}, 0.5);
}
const batchResults = await Promise.all(batchPromises);
const previousProcessedCount = processedCount;
// Filter out null results and add valid ones to serverDetails
const validResults = batchResults.filter(result => result !== null);
serverDetails.push(...validResults);
// Gradually update processedCount after processing the batch
processedCount += batch.length;
updateProcessedCountSmoothly(previousProcessedCount, processedCount);
}
} else {
// Original sequential processing
for (let i = 0; i < servers.length; i++) {
const server = servers[i];
const {
id: serverId,
maxPlayers,
playing,
ping,
fps,
playerTokens
} = server;
let location;
try {
location = await fetchServerDetails(gameId, serverId);
} catch (error) {
if (error === 'purchase_required') {
if (premium_message) {
premium_message.textContent = "Error: Cannot access server regions because you have not purchased the game.";
}
notifications('Cannot access server regions because you have not purchased the game.', 'error', '⚠️', '15000');
Loadingbar(false);
return;
} else if (error === 'subplace_join_restriction') {
if (premium_message) {
premium_message.textContent = "Error: This game requires users to teleport to a subplace. As a result, server regions cannot be retrieved.";
}
notifications('Error: This game requires users to teleport to a subplace. As a result, server regions cannot be retrieved.', 'error', '⚠️', '15000');
Loadingbar(false);
return;
} else {
ConsoleLogEnabled(error);
location = {
city: "Unknown",
country: {
name: "Unknown",
code: "??"
}
};
}
}
if (location.city === "Unknown" || playing >= maxPlayers) {
ConsoleLogEnabled(`Skipping server ${serverId} because it is full or location is unknown.`);
skippedServers++;
continue;
}
// Fetch player thumbnails
const playerThumbnails = playerTokens && playerTokens.length > 0 ? await fetchPlayerThumbnails_servers(playerTokens) : [];
serverDetails.push({
server,
location,
playerThumbnails,
});
if (premium_message) {
premium_message.textContent = `Filtering servers, please do not leave this page...\n${totalServers} servers found, ${i + 1} server locations found`;
}
}
}
if (serverDetails.length === 0) {
showMessage("END");
if (servers.every(s => s.maxPlayers === 1)) {
notifications('All servers have a max player count of 1. These are likely solo servers and cannot be joined normally.', 'error', '⚠️', '8000');
} else {
notifications('Error: No servers found. Try increasing the search limit or enabling "Invert Player Count" in Settings > General.', 'error', '⚠️', '8000');
}
Loadingbar(false);
return;
}
const loadedServers = totalServers - skippedServers;
notifications(`Filtering complete!\n${totalServers} servers found, ${loadedServers} servers loaded, ${skippedServers} servers skipped (full).`, 'success', '👍', '2000');
if (localStorage.getItem('ROLOCATE_fastservers') === 'true') {
let secondsSaved = (loadedServers * 150) / 1000;
if (secondsSaved < 0.1 && secondsSaved > 0) {
secondsSaved = '0.1';
} else {
secondsSaved = secondsSaved.toFixed(1);
}
notifications(`FastServers: Thumbnails replaced with Builderman and Roblox. Saved ${secondsSaved} seconds`, 'info', '🚀', '2500');
}
// check stuff you know
if (typeof GM_info !== 'undefined') {
const handler = GM_info.scriptHandler?.toLowerCase();
const fastServers = localStorage.getItem('ROLOCATE_fastservers');
if (handler?.includes('violentmonkey') && fastServers === 'false') {
notifications(`You're using Violentmonkey supports Fast Servers. Turn on "Fast Server Search" in Settings → General → Fast Server Search, to search servers up to 100x faster!`, 'info', '🚀', '12000');
}
if (handler?.includes('scriptcat') && fastServers === 'false') {
notifications(`You're using ScriptCat supports Fast Servers. Turn on "Fast Server Search" in Settings → General → Fast Server Search, to search servers up to 100x faster!`, 'info', '🚀', '12000');
}
if (handler?.includes('tampermonkey')) {
notifications(`Server search is slow because of a bug in Tampermonkey that can make it 100x slower. Use Violentmonkey or Scriptcat to make it 100x faster!`, 'info', '🚀', '12000');
}
}
showMessage("END");
Loadingbar(false);
// Add filter dropdowns
const filterContainer = createFilterDropdowns(serverDetails);
serverListContainer.parentNode.insertBefore(filterContainer, serverListContainer);
// Style the server list container
serverListContainer.style.display = "grid";
serverListContainer.style.gridTemplateColumns = "repeat(4, 1fr)";
serverListContainer.style.gap = "0px";
const displayFilteredServers = (country, city) => {
serverListContainer.innerHTML = "";
const filteredServers = filterServers(serverDetails, country, city);
const sortedServers = filteredServers.sort((a, b) => {
const distanceA = calculateDistance(userLocation.latitude, userLocation.longitude, a.location.latitude, a.location.longitude);
const distanceB = calculateDistance(userLocation.latitude, userLocation.longitude, b.location.latitude, b.location.longitude);
return distanceA - distanceB;
});
sortedServers.forEach(({
server,
location,
playerThumbnails
}) => {
const serverCard = document.createElement("li");
serverCard.className = "rbx-game-server-item col-md-3 col-sm-4 col-xs-6";
serverCard.style.width = "100%";
serverCard.style.minHeight = "400px";
serverCard.style.display = "flex";
serverCard.style.flexDirection = "column";
serverCard.style.justifyContent = "space-between";
serverCard.style.boxSizing = "border-box";
serverCard.style.outline = 'none';
serverCard.style.padding = '6px';
serverCard.style.borderRadius = '8px';
// Create ping label
const pingLabel = document.createElement("div");
pingLabel.style.marginBottom = "5px";
pingLabel.style.padding = "5px 10px";
pingLabel.style.borderRadius = "8px";
pingLabel.style.fontWeight = "bold";
pingLabel.style.textAlign = "center";
// Calculate distance and ping
const distance = calculateDistance(
userLocation.latitude,
userLocation.longitude,
location.latitude,
location.longitude
);
const calculatedPing = 40 + 0.004 * distance + 1.2 * Math.sqrt(distance);
if (distance < 1250) {
pingLabel.textContent = "⚡ Fast";
pingLabel.style.backgroundColor = "#014737";
pingLabel.style.color = "#73e1bc";
} else if (distance < 5000) {
pingLabel.textContent = "⏳ OK";
pingLabel.style.backgroundColor = "#c75a00";
pingLabel.style.color = "#ffe8c2";
} else {
pingLabel.textContent = "🐌 Slow";
pingLabel.style.backgroundColor = "#771d1d";
pingLabel.style.color = "#fcc468";
}
// Create thumbnails container
const thumbnailsContainer = document.createElement("div");
thumbnailsContainer.className = "player-thumbnails-container";
thumbnailsContainer.style.display = "grid";
thumbnailsContainer.style.gridTemplateColumns = "repeat(3, 60px)";
thumbnailsContainer.style.gridTemplateRows = "repeat(2, 60px)";
thumbnailsContainer.style.gap = "5px";
thumbnailsContainer.style.marginBottom = "10px";
// Add player thumbnails
const maxThumbnails = 5;
const displayedThumbnails = playerThumbnails.slice(0, maxThumbnails);
displayedThumbnails.forEach(thumb => {
if (thumb && thumb.imageUrl) {
const img = document.createElement("img");
img.src = thumb.imageUrl;
img.className = "avatar-card-image";
img.style.width = "60px";
img.style.height = "60px";
img.style.borderRadius = "50%";
thumbnailsContainer.appendChild(img);
}
});
// Add placeholder for hidden players
const hiddenPlayers = server.playing - displayedThumbnails.length;
if (hiddenPlayers > 0) {
const placeholder = document.createElement("div");
placeholder.className = "avatar-card-image";
placeholder.style.width = "60px";
placeholder.style.height = "60px";
placeholder.style.borderRadius = "50%";
placeholder.style.backgroundColor = "#6a6f81";
placeholder.style.display = "flex";
placeholder.style.alignItems = "center";
placeholder.style.justifyContent = "center";
placeholder.style.color = "#fff";
placeholder.style.fontSize = "14px";
placeholder.textContent = `+${hiddenPlayers}`;
thumbnailsContainer.appendChild(placeholder);
}
// Server card content
const cardItem = document.createElement("div");
cardItem.className = "card-item";
cardItem.style.display = "flex";
cardItem.style.flexDirection = "column";
cardItem.style.justifyContent = "space-between";
cardItem.style.height = "100%";
cardItem.innerHTML = `
${thumbnailsContainer.outerHTML}
<div class="rbx-game-server-details game-server-details">
<div class="text-info rbx-game-status rbx-game-server-status text-overflow">
${server.playing} of ${server.maxPlayers} people max
</div>
<div class="server-player-count-gauge border">
<div class="gauge-inner-bar border" style="width: ${(server.playing / server.maxPlayers) * 100}%;"></div>
</div>
<span data-placeid="${gameId}">
<button type="button" class="btn-full-width btn-control-xs rbx-game-server-join game-server-join-btn btn-primary-md btn-min-width">Join</button>
</span>
</div>
<div style="margin-top: 10px; text-align: center;">
${pingLabel.outerHTML}
<div class="info-lines" style="margin-top: 8px;">
<div class="ping-info">Ping: ${calculatedPing.toFixed(2)} ms</div>
<hr style="margin: 6px 0;">
<div class="ping-info">Distance: ${distance.toFixed(2)} km</div>
<hr style="margin: 6px 0;">
<div class="location-info">${location.city}, ${location.country.name}</div>
<hr style="margin: 6px 0;">
<div class="fps-info">FPS: ${Math.round(server.fps)}</div>
</div>
</div>
`;
const joinButton = cardItem.querySelector(".rbx-game-server-join");
joinButton.addEventListener("click", () => {
ConsoleLogEnabled(`Roblox.GameLauncher.joinGameInstance(${gameId}, "${server.id}")`);
Roblox.GameLauncher.joinGameInstance(gameId, server.id);
});
const container = adjustJoinButtonContainer(joinButton);
const inviteButton = createInviteButton(gameId, server.id);
container.appendChild(inviteButton);
serverCard.appendChild(cardItem);
serverListContainer.appendChild(serverCard);
});
};
// Add event listeners to dropdowns
const countryFilter = document.getElementById('countryFilter');
const cityFilter = document.getElementById('cityFilter');
countryFilter.addEventListener('change', () => {
displayFilteredServers(countryFilter.value, cityFilter.value);
});
cityFilter.addEventListener('change', () => {
displayFilteredServers(countryFilter.value, cityFilter.value);
});
// Display all servers initially
displayFilteredServers("", "");
} catch (error) {
if (error === 'purchase_required') {
if (premium_message) {
premium_message.textContent = "Error: Cannot access server regions because you have not purchased the game.";
}
notifications('Cannot access server regions because you have not purchased the game.', 'error', '⚠️', '15000');
Loadingbar(false);
return;
} else if (error === 'subplace_join_restriction') {
if (premium_message) {
premium_message.textContent = "Error: This game requires users to teleport to a subplace. As a result, server regions cannot be retrieved.";
}
notifications('Error: This game requires users to teleport to a subplace. As a result, server regions cannot be retrieved.', 'error', '⚠️', '15000');
Loadingbar(false);
return;
} else {
ConsoleLogEnabled("Error rebuilding server list:", error);
notifications('Filtering Error: Failed to obtain permission to send API requests to the Roblox API. Please allow the script to enable request sending.', 'error', '⚠️ ', '8000');
if (premium_message) {
premium_message.textContent = "Filtering Error: Failed to obtain permission to send API requests to the Roblox API. Please allow the script to enable request sending.";
}
Loadingbar(false);
}
} finally {
Loadingbar(false);
disableFilterButton(false);
}
}
// this is used for best connection and server regions
const gameId = /^https:\/\/www\.roblox\.com(\/[a-z]{2})?\/games\//.test(window.location.href) ? (window.location.href.match(/\/games\/(\d+)/) || [])[1] || null : null;
/*******************************************************
name of function: createInviteButton
description: Creates the invite button (server region)
*******************************************************/
function createInviteButton(placeId, serverId) {
const inviteButton = document.createElement('button');
inviteButton.textContent = 'Invite';
inviteButton.className = 'btn-control-xs btn-primary-md btn-min-width btn-full-width';
inviteButton.style.width = '25%';
inviteButton.style.marginLeft = '5px';
inviteButton.style.padding = '4px 8px';
inviteButton.style.fontSize = '12px';
inviteButton.style.borderRadius = '8px';
inviteButton.style.backgroundColor = '#3b3e49';
inviteButton.style.borderColor = '#3b3e49';
inviteButton.style.color = '#ffffff';
inviteButton.style.cursor = 'pointer';
inviteButton.style.fontWeight = '500';
inviteButton.style.textAlign = 'center';
inviteButton.style.whiteSpace = 'nowrap';
inviteButton.style.verticalAlign = 'middle';
inviteButton.style.lineHeight = '100%';
inviteButton.style.fontFamily = 'Builder Sans, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif';
inviteButton.style.textRendering = 'auto';
inviteButton.style.webkitFontSmoothing = 'antialiased';
inviteButton.style.mozOsxFontSmoothing = 'grayscale';
let resetTextTimeout = null;
inviteButton.addEventListener('click', () => {
const inviteLink = `https://oqarshi.github.io/Invite/?placeid=${placeId}&serverid=${serverId}`;
navigator.clipboard.writeText(inviteLink).then(() => {
ConsoleLogEnabled(`Invite link copied to clipboard: ${inviteLink}`);
notifications('Success! Invite link copied to clipboard!', 'success', '🎉', '2000');
// Prevent spam clicking
inviteButton.disabled = true;
inviteButton.style.opacity = '0.6';
inviteButton.style.cursor = 'not-allowed';
// Reset any previous timeout
if (resetTextTimeout !== null) {
clearTimeout(resetTextTimeout);
}
inviteButton.textContent = 'Copied!';
resetTextTimeout = setTimeout(() => {
inviteButton.textContent = 'Invite';
inviteButton.disabled = false;
inviteButton.style.opacity = '1';
inviteButton.style.cursor = 'pointer';
resetTextTimeout = null;
}, 1000);
}).catch(() => {
ConsoleLogEnabled('Failed to copy invite link.');
notifications('Error: Failed to copy invite link', 'error', '😔', '2000');
});
});
return inviteButton;
}
/*******************************************************
name of function: adjustJoinButtonContainer
description: Function to adjust the Join button and its container
but it fails lmao and does 50/50 instead of 75/25
*******************************************************/
function adjustJoinButtonContainer(joinButton) {
const container = document.createElement('div');
container.style.display = 'flex';
container.style.width = '100%';
joinButton.style.width = '75%';
joinButton.parentNode.insertBefore(container, joinButton);
container.appendChild(joinButton);
return container;
}
/*********************************************************************************************************************************************************************************************************************************************
Functions for the 6th button.
*********************************************************************************************************************************************************************************************************************************************/
/*******************************************************
name of function: calculateDistance
description: finds the distance between two points on a sphere (Earth)
*******************************************************/
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371; // Radius of the Earth in kilometers as a perfect sphere but obv its not a perfect sphere
const dLat = (lat2 - lat1) * (Math.PI / 180);
const dLon = (lon2 - lon1) * (Math.PI / 180);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // Distance in kilometers
}
/*******************************************************
name of function: resolveOfflineFallbackLocation
description: estimate user location if user declines
*******************************************************/
// fallback location resolver with timezone-based estimation
function resolveOfflineFallbackLocation(resolve) {
ConsoleLogEnabled("Attempting offline location estimation...");
let guessedLocation = null;
let closestLocation = null;
let closestDistance = Infinity;
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "";
const timezoneMap = {
"America/Los_Angeles": {
lat: 34.0522,
lon: -118.2437
},
"America/Denver": {
lat: 39.7392,
lon: -104.9903
},
"America/Chicago": {
lat: 41.8781,
lon: -87.6298
},
"America/New_York": {
lat: 40.7128,
lon: -74.006
},
"Europe/London": {
lat: 51.5074,
lon: -0.1278
},
"Europe/Berlin": {
lat: 52.52,
lon: 13.405
},
"Europe/Paris": {
lat: 48.8566,
lon: 2.3522
},
"Asia/Tokyo": {
lat: 35.6895,
lon: 139.6917
},
"Asia/Kolkata": {
lat: 28.6139,
lon: 77.209
},
"Australia/Sydney": {
lat: -33.8688,
lon: 151.2093
},
"America/Argentina/Buenos_Aires": {
lat: -34.6037,
lon: -58.3816
},
"Africa/Nairobi": {
lat: -1.286389,
lon: 36.817223
},
"Asia/Singapore": {
lat: 1.3521,
lon: 103.8198
},
"America/Toronto": {
lat: 43.65107,
lon: -79.347015
},
"Europe/Moscow": {
lat: 55.7558,
lon: 37.6173
},
"Europe/Madrid": {
lat: 40.4168,
lon: -3.7038
},
"Asia/Shanghai": {
lat: 31.2304,
lon: 121.4737
},
"Africa/Cairo": {
lat: 30.0444,
lon: 31.2357
},
"Africa/Johannesburg": {
lat: -26.2041,
lon: 28.0473
},
"Europe/Amsterdam": {
lat: 52.3676,
lon: 4.9041
},
"Asia/Manila": {
lat: 14.5995,
lon: 120.9842
},
"Asia/Seoul": {
lat: 37.5665,
lon: 126.978
}
};
// If user's timezone is available in the map
if (timezoneMap[timezone]) {
guessedLocation = timezoneMap[timezone];
ConsoleLogEnabled("User's timezone found:", timezone);
}
// If the timezone is not found, find the closest match
if (!guessedLocation) {
ConsoleLogEnabled("User's timezone not found. Finding closest match...");
Object.keys(timezoneMap).forEach((tz) => {
const location = timezoneMap[tz];
const distance = calculateDistance(location.lat, location.lon, 0, 0); // Distance from the equator (0,0)
if (distance < closestDistance) {
closestDistance = distance;
closestLocation = location;
}
});
guessedLocation = closestLocation;
}
// If we found a location, return it, otherwise default to New York
if (guessedLocation) {
notifications("Estimated location based on timezone. Please allow location access to see what servers are closest to you or change to manual in settings.", "info", "🕒", "6000");
resolve({
latitude: guessedLocation.lat,
longitude: guessedLocation.lon
});
} else {
notifications("Error: Could not estimate location. Fatal error, please report on Greasyfork. Using default (New York).", "error", "⚠️", "6000");
resolve({
latitude: 40.7128,
longitude: -74.0060
}); // Default to NYC
}
}
/*******************************************************
name of function: getUserLocation
description: gets the user's location
@param {boolean} [quickJoin=false] – when true, operates in lightweight "quick join" mode
*******************************************************/
function getUserLocation(quickJoin = false) {
return new Promise((resolve, reject) => {
// Check priority location setting
const priorityLocation = localStorage.getItem("ROLOCATE_prioritylocation") || "automatic";
// If in manual mode, use stored coordinates
if (priorityLocation === "manual") {
try {
const coords = JSON.parse(GM_getValue("ROLOCATE_coordinates", '{"lat":"","lng":""}'));
if (coords.lat && coords.lng) {
ConsoleLogEnabled("Using manual location from storage");
notifications("We successfully detected your location.", "success", "🌎", "2000");
return resolve({
latitude: parseFloat(coords.lat), // Changed to match automatic mode
longitude: parseFloat(coords.lng), // Changed to match automatic mode
source: "manual",
accuracy: 0 // Manual coordinates have no accuracy metric
});
} else {
ConsoleLogEnabled("Manual mode selected but no coordinates set - falling back to automatic behavior");
notifications("Manual mode selected but no coordinates set. Fatal error: Report on greasyfork. Using Automatic Mode.", "error", "", "2000");
// Fall through to automatic behavior
}
} catch (e) {
ConsoleLogEnabled("Error reading manual coordinates:", e);
notifications("Error reading manual coordinates. Fatal error: Report on greasyfork. Using Automatic Mode.", "error", "", "2000");
// Fall through to automatic behavior
}
}
// Automatic mode behavior
if (!navigator.geolocation) {
ConsoleLogEnabled("Geolocation not supported.");
notifications("Geolocation is not supported by your browser.", "error", "⚠️", "15000");
return resolveOfflineFallbackLocation(resolve);
}
navigator.geolocation.getCurrentPosition(
(position) => resolveSuccess(position, resolve, quickJoin),
async (error) => {
ConsoleLogEnabled("Geolocation error:", error);
// Attempt to inspect geolocation permission state
try {
if (navigator.permissions && navigator.permissions.query) {
const permissionStatus = await navigator.permissions.query({
name: "geolocation"
});
ConsoleLogEnabled("Geolocation permission status:", permissionStatus.state);
if (permissionStatus.state === "denied") {
return resolveOfflineFallbackLocation(resolve);
}
}
} catch (permError) {
ConsoleLogEnabled("Permission check failed:", permError);
}
// Retry geolocation once with a slightly relaxed setting
navigator.geolocation.getCurrentPosition(
(position) => resolveSuccess(position, resolve, quickJoin),
(retryError) => {
ConsoleLogEnabled("Second geolocation attempt failed:", retryError);
notifications("Could not get your location. Using fallback.", "error", "⚠️", "15000");
resolveOfflineFallbackLocation(resolve);
}, {
maximumAge: 5000,
timeout: 10000,
}
);
}, {
timeout: 10000,
maximumAge: 0,
}
);
});
}
/*******************************************************
name of function: resolveSuccess
description: tells the user that location was detected
@param {GeolocationPosition} position – browser geolocation position
@param {Function} resolve – promise resolver
@param {boolean} [quickJoin=false] – when true, skips UI-disabling side‑effects
*******************************************************/
function resolveSuccess(position, resolve, quickJoin = false) {
notifications("We successfully detected your location.", "success", "🌎", "2000");
if (!quickJoin) {
disableLoadMoreButton(true);
disableFilterButton(true);
Loadingbar(true);
}
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
source: "geolocation",
accuracy: position.coords.accuracy
});
}
/*********************************************************************************************************************************************************************************************************************************************
Functions for the 7th button.
*********************************************************************************************************************************************************************************************************************************************/
/*******************************************************
name of function: auto_join_small_server
description: Automatically joins the smallest server
*******************************************************/
async function auto_join_small_server() {
// Disable the "Load More" button and show the loading bar
Loadingbar(true);
disableFilterButton(true);
disableLoadMoreButton();
// Get the game ID from the URL
const gameId = ((p = window.location.pathname.split('/')) => {
const i = p.indexOf('games');
return i !== -1 && p.length > i + 1 ? p[i + 1] : null;
})();
// Retry mechanism for 429 errors
let retries = 3; // Number of retries
let success = false;
while (retries > 0 && !success) {
try {
// Fetch server data using GM_xmlhttpRequest
const data = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://games.roblox.com/v1/games/${gameId}/servers/public?sortOrder=1&excludeFullGames=true&limit=100`,
onload: function(response) {
if (response.status === 429) {
reject('429: Too Many Requests');
} else if (response.status >= 200 && response.status < 300) {
resolve(JSON.parse(response.responseText));
} else {
reject(`HTTP error: ${response.status}`);
}
},
onerror: function(error) {
reject(error);
},
});
});
// find servers with low player count, prob doesnet work with bloxfruits cause bots
let minPlayers = Infinity;
let targetServer = null;
for (const server of data.data) {
if (server.playing < minPlayers) {
minPlayers = server.playing;
targetServer = server;
}
}
if (targetServer) {
// Join the server with the lowest player count
//showLoadingOverlay();
Roblox.GameLauncher.joinGameInstance(gameId, targetServer.id);
notifications(`Joining a server with ${targetServer.playing} player(s).`, 'success', '🚀');
success = true; // Mark as successful
} else {
notifications('No available servers found.', 'error', '⚠️');
break; // Exit the loop if no servers are found
}
} catch (error) {
if (error === '429: Too Many Requests' && retries > 0) {
ConsoleLogEnabled('Rate limited. Retrying in 10 seconds...');
notifications('Rate limited. Retrying in 10 seconds...', 'warning', '⏳', '10000');
await delay(10000); // Wait 10 seconds before retrying
retries--;
} 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
}
}
}
// Hide the loading bar and enable the filter button
Loadingbar(false);
disableFilterButton(false);
}
/*********************************************************************************************************************************************************************************************************************************************
Functions for the 8th button. roblox borke it lmao. basically fillter code, might remove it one day
*********************************************************************************************************************************************************************************************************************************************/
async function scanRobloxServers() {
const BRAND_LOGO = window.Base64Images.logo;
notifications('Note: This may not actually work and is just experimental.', 'info', '⚠️', '5000');
// Create popup UI
function createScannerPopup() {
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10000;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
`;
const popup = document.createElement('div');
popup.style.cssText = `
width: 400px;
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 50%, #2a2a2a 100%);
border: 2px solid #333;
border-radius: 16px;
padding: 24px;
color: #fff;
font-family: 'Segoe UI', sans-serif;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255, 255, 255, 0.1);
transform: scale(0.9);
transition: transform 0.3s ease;
position: relative;
overflow: hidden;
`;
// Animated background pattern
const bgPattern = document.createElement('div');
bgPattern.style.cssText = `
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(100, 100, 100, 0.05) 0%, transparent 70%);
animation: rotate 20s linear infinite;
pointer-events: none;
`;
const style = document.createElement('style');
style.textContent = `
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes pulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(10px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
`;
document.head.appendChild(style);
popup.appendChild(bgPattern);
// Header with logo
const header = document.createElement('div');
header.style.cssText = `
text-align: center;
margin-bottom: 20px;
position: relative;
z-index: 1;
`;
// Brand logo
const logoContainer = document.createElement('div');
logoContainer.style.cssText = `
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 12px;
`;
const logo = document.createElement('img');
logo.src = BRAND_LOGO;
logo.style.cssText = `
width: 32px;
height: 32px;
margin-right: 8px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
`;
const title = document.createElement('div');
title.textContent = 'RoLocate';
title.style.cssText = `
font-size: 24px;
font-weight: bold;
color: #e0e0e0;
text-shadow: 0 0 20px rgba(255, 255, 255, 0.2);
`;
logoContainer.appendChild(logo);
logoContainer.appendChild(title);
const subtitle = document.createElement('div');
subtitle.textContent = 'Trying to search for new servers...';
subtitle.style.cssText = `
color: #999;
font-size: 14px;
`;
header.appendChild(logoContainer);
header.appendChild(subtitle);
// Status section
const statusDiv = document.createElement('div');
statusDiv.style.cssText = `
margin-bottom: 24px;
position: relative;
z-index: 1;
`;
const statusText = document.createElement('div');
statusText.style.cssText = `
color: #fff;
margin-bottom: 12px;
font-size: 16px;
text-align: center;
min-height: 24px;
animation: slideUp 0.5s ease;
`;
statusText.textContent = 'Initializing...';
const progressContainer = document.createElement('div');
progressContainer.style.cssText = `
width: 100%;
height: 8px;
background: #1a1a1a;
border-radius: 4px;
overflow: hidden;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.5);
border: 1px solid #333;
`;
const progressFill = document.createElement('div');
progressFill.style.cssText = `
height: 100%;
background: linear-gradient(90deg, #4a4a4a, #666, #4a4a4a);
background-size: 200% 100%;
width: 0%;
transition: width 0.5s ease;
animation: shimmer 2s infinite;
`;
const shimmerStyle = document.createElement('style');
shimmerStyle.textContent = `
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
`;
document.head.appendChild(shimmerStyle);
progressContainer.appendChild(progressFill);
statusDiv.appendChild(statusText);
statusDiv.appendChild(progressContainer);
// Stats section
const statsDiv = document.createElement('div');
statsDiv.style.cssText = `
background: rgba(0, 0, 0, 0.4);
border: 1px solid #333;
border-radius: 12px;
padding: 16px;
margin-bottom: 24px;
position: relative;
z-index: 1;
`;
const statsGrid = document.createElement('div');
statsGrid.style.cssText = `
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
`;
const createStatItem = (label, id, color) => {
const item = document.createElement('div');
item.style.cssText = 'text-align: center;';
const value = document.createElement('div');
value.id = id;
value.style.cssText = `
font-size: 24px;
font-weight: bold;
color: ${color};
margin-bottom: 4px;
text-shadow: 0 0 10px ${color}40;
`;
value.textContent = '0';
const labelDiv = document.createElement('div');
labelDiv.style.cssText = 'color: #aaa; font-size: 12px;';
labelDiv.textContent = label;
item.appendChild(value);
item.appendChild(labelDiv);
return item;
};
statsGrid.appendChild(createStatItem('Total Scans', 'totalScans', '#e0e0e0'));
statsGrid.appendChild(createStatItem('Servers Tracked', 'serversFound', '#4CAF50'));
statsGrid.appendChild(createStatItem('New Servers', 'newServers', '#64B5F6'));
statsGrid.appendChild(createStatItem('Cards Created', 'cardsCreated', '#FFB74D'));
statsDiv.appendChild(statsGrid);
// Button container
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
justify-content: center;
position: relative;
z-index: 1;
`;
const cancelBtn = document.createElement('button');
cancelBtn.textContent = '✕ Stop';
cancelBtn.style.cssText = `
background: linear-gradient(135deg, #333, #444);
border: 1px solid #555;
color: white;
padding: 12px 24px;
border-radius: 25px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
`;
cancelBtn.onmouseover = () => {
cancelBtn.style.transform = 'translateY(-2px)';
cancelBtn.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.5)';
cancelBtn.style.background = 'linear-gradient(135deg, #444, #555)';
};
cancelBtn.onmouseout = () => {
cancelBtn.style.transform = 'translateY(0)';
cancelBtn.style.boxShadow = '0 4px 15px rgba(0, 0, 0, 0.3)';
cancelBtn.style.background = 'linear-gradient(135deg, #333, #444)';
};
let cancelled = false;
cancelBtn.onclick = () => {
Loadingbar(false);
disableFilterButton(false);
cancelled = true;
overlay.style.opacity = '0';
popup.style.transform = 'scale(0.9)';
notifications('Scan stopped', 'info', '🚫');
setTimeout(() => overlay.remove(), 300);
};
buttonContainer.appendChild(cancelBtn);
// Assemble popup
popup.appendChild(header);
popup.appendChild(statusDiv);
popup.appendChild(statsDiv);
popup.appendChild(buttonContainer);
overlay.appendChild(popup);
document.body.appendChild(overlay);
// Animate in
setTimeout(() => {
overlay.style.opacity = '1';
popup.style.transform = 'scale(1)';
}, 10);
return {
updateStatus: (text, progress = null) => {
statusText.textContent = text;
statusText.style.animation = 'slideUp 0.5s ease';
if (progress !== null) {
progressFill.style.width = `${Math.min(100, Math.max(0, progress))}%`;
}
},
updateStats: (stats) => {
const updateStat = (id, value) => {
const element = document.getElementById(id);
if (element && value !== undefined) {
element.textContent = value;
element.style.animation = 'pulse 0.5s ease';
}
};
updateStat('totalScans', stats.totalScans);
updateStat('serversFound', stats.serversFound);
updateStat('newServers', stats.newServers);
updateStat('cardsCreated', stats.cardsCreated);
},
isCancelled: () => cancelled,
close: () => {
overlay.style.opacity = '0';
popup.style.transform = 'scale(0.9)';
setTimeout(() => overlay.remove(), 300);
}
};
}
// Initialize popup
const ui = createScannerPopup();
ui.updateStatus('🚀 Starting continuous scanner...', 0);
ConsoleLogEnabled("Starting Roblox continuous server scanner...");
// Run startup functions once
ConsoleLogEnabled("Running startup functions...");
ui.updateStatus('⚙️ Setting up scanner...', 5);
try {
Loadingbar(true);
disableFilterButton(true);
disableLoadMoreButton();
ConsoleLogEnabled("Startup functions executed successfully");
} catch (error) {
ConsoleLogEnabled("❌ Error running startup functions:", error);
ui.updateStatus('❌ Setup failed', 100);
Loadingbar(false);
disableFilterButton(false);
notifications('An error occured. Please try again', 'error', '⚠️');
return;
}
// Check for cancellation
if (ui.isCancelled()) {
ConsoleLogEnabled("Scanner cancelled by user");
Loadingbar(false);
disableFilterButton(false);
notifications('Scanner cancelled', 'info', '🚫');
return;
}
// 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('/')));
if (!gameId) {
ConsoleLogEnabled("Could not extract game ID from URL");
ui.updateStatus('❌ Game ID not found', 100);
notifications('An error occured. Please try again', 'error', '⚠️');
return;
}
ConsoleLogEnabled(`Game ID extracted: ${gameId}`);
// Rate limiting variables
let requestDelay = 250;
const normalDelay = 250;
const rateLimitedDelay = 750;
// Store all known server IDs
let knownServerIds = new Set();
// Track totals
let totalScansPerformed = 0;
let totalNewServersFound = 0;
let totalCardsCreated = 0;
ConsoleLogEnabled(`🎯 Starting continuous scanning mode - will run until user cancels`);
// Function to fetch all servers from API with dynamic rate limiting
async function fetchAllServers() {
if (ui.isCancelled()) return [];
ConsoleLogEnabled("Starting to fetch all servers...");
ConsoleLogEnabled(`Current request delay: ${requestDelay}ms`);
const allServers = [];
let cursor = null;
let pageCount = 0;
do {
if (ui.isCancelled()) return [];
pageCount++;
ConsoleLogEnabled(`Fetching page ${pageCount}${cursor ? ` with cursor: ${cursor.substring(0, 50)}...` : ' (first page)'}`);
try {
const url = `https://games.roblox.com/v1/games/${gameId}/servers/0?sortOrder=2&excludeFullGames=false&limit=100${cursor ? `&cursor=${cursor}` : ''}`;
ConsoleLogEnabled(`Request URL: ${url}`);
const response = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
},
onload: function(response) {
resolve(response);
},
onerror: function(error) {
reject(error);
},
ontimeout: function() {
reject(new Error('Request timeout'));
},
timeout: 10000
});
});
// Check for rate limiting
if (response.status === 429) {
ConsoleLogEnabled(`Rate limited! Status: ${response.status}`);
if (requestDelay !== rateLimitedDelay) {
requestDelay = rateLimitedDelay;
ConsoleLogEnabled(`Switching to slower requests: ${requestDelay}ms delay`);
}
ConsoleLogEnabled(`Waiting ${requestDelay}ms before retry...`);
await new Promise(resolve => setTimeout(resolve, requestDelay));
continue;
}
// Check if we can go back to normal speed
if (response.status >= 200 && response.status < 300 && requestDelay === rateLimitedDelay) {
requestDelay = normalDelay;
ConsoleLogEnabled(`Rate limit cleared! Back to normal speed: ${requestDelay}ms delay`);
}
if (response.status < 200 || response.status >= 300) {
ConsoleLogEnabled(`API request failed with status ${response.status}: ${response.statusText}`);
notifications('An error occured. Please try again. Roblox API blocked.', 'error', '⚠️');
break;
}
const data = JSON.parse(response.responseText);
ConsoleLogEnabled(`Page ${pageCount} fetched successfully - Found ${data.data.length} servers`);
if (data.data && Array.isArray(data.data)) {
allServers.push(...data.data);
ConsoleLogEnabled(`Total servers collected so far: ${allServers.length}`);
} else {
ConsoleLogEnabled(`Unexpected data format on page ${pageCount}`);
}
cursor = data.nextPageCursor;
ConsoleLogEnabled(`Next page cursor: ${cursor ? cursor.substring(0, 50) + '...' : 'null (no more pages)'}`);
} catch (error) {
ConsoleLogEnabled(`Error fetching page ${pageCount}:`, error);
if (error.message && (error.message.includes('timeout') || error.message.includes('network'))) {
ConsoleLogEnabled(`Network/timeout error detected, might be rate limiting`);
if (requestDelay !== rateLimitedDelay) {
requestDelay = rateLimitedDelay;
ConsoleLogEnabled(`Switching to slower requests due to network error: ${requestDelay}ms delay`);
}
}
ConsoleLogEnabled(`Waiting ${requestDelay}ms before continuing...`);
await new Promise(resolve => setTimeout(resolve, requestDelay));
continue;
}
if (cursor) {
ConsoleLogEnabled(`Waiting ${requestDelay}ms before next request...`);
await new Promise(resolve => setTimeout(resolve, requestDelay));
}
} while (cursor && !ui.isCancelled());
ConsoleLogEnabled(`Finished fetching all servers. Total pages: ${pageCount}, Total servers: ${allServers.length}`);
return allServers;
}
// Function to create card for a new server
async function createCardForServer(server) {
ConsoleLogEnabled(`Creating card for new server: ${server.id}`);
ConsoleLogEnabled(` - Players: ${server.playing}/${server.maxPlayers}`);
ConsoleLogEnabled(` - Player tokens: ${server.playerTokens ? server.playerTokens.length : 0}`);
ConsoleLogEnabled(` - FPS: ${server.fps}`);
ConsoleLogEnabled(` - Ping: ${server.ping}`);
try {
await rbx_card(server.id, server.playerTokens, server.maxPlayers, server.playing, gameId);
ConsoleLogEnabled(`✅ Card created successfully for server: ${server.id}`);
totalCardsCreated++;
notifications(`New server found! Card created (${server.playing}/${server.maxPlayers} players)`, 'success', '🎉');
return true;
} catch (error) {
ConsoleLogEnabled(`❌ Error creating card for server ${server.id}:`, error);
return false;
}
}
// INITIAL SCAN: Build baseline of known servers
ConsoleLogEnabled("\n=== INITIAL SCAN - Building baseline ===");
ui.updateStatus('🔍 Scan 1 - Searching for new servers...', 15);
totalScansPerformed++;
ui.updateStats({
totalScans: totalScansPerformed
});
const initialServers = await fetchAllServers();
if (ui.isCancelled()) return;
initialServers.forEach(server => {
if (server && server.id) {
knownServerIds.add(server.id);
ConsoleLogEnabled(`Added baseline server ID: ${server.id}`);
}
});
ConsoleLogEnabled(`INITIAL SCAN COMPLETE: Stored ${knownServerIds.size} baseline server IDs`);
ui.updateStats({
serversFound: knownServerIds.size
});
// CONTINUOUS SCANNING LOOP
ConsoleLogEnabled("\n=== STARTING CONTINUOUS SCANNING ===");
let scanNumber = 2;
const scanDelay = 1000; // Wait between scans
while (!ui.isCancelled()) {
ConsoleLogEnabled(`\n=== SCAN ${scanNumber} - Looking for new servers ===`);
ui.updateStatus(`🔍 Scan ${scanNumber} - Searching for new servers...`, null);
totalScansPerformed++;
ui.updateStats({
totalScans: totalScansPerformed
});
const currentScanServers = await fetchAllServers();
if (ui.isCancelled()) break;
let newServersFoundThisScan = 0;
// Process each server
for (const server of currentScanServers) {
if (ui.isCancelled()) break;
if (server && server.id) {
if (!knownServerIds.has(server.id)) {
// This is a completely new server
ConsoleLogEnabled(`🆕 BRAND NEW SERVER DETECTED: ${server.id}`);
// Add to known servers immediately
knownServerIds.add(server.id);
ConsoleLogEnabled(`Added server ${server.id} to known servers list`);
function getAdaptivePlayerThreshold(maxPlayers) {
const curve = 1.18; // re-tuned for better match
const minRatio = 0.38; // minimum allowed ratio (~38% of maxPlayers)
const rawRatio = 1 - Math.exp(-curve * (maxPlayers / 25));
const adjustedRatio = Math.max(minRatio, rawRatio);
return Math.floor(maxPlayers * adjustedRatio);
}
const adaptiveThreshold = getAdaptivePlayerThreshold(server.maxPlayers);
if (server.playing < adaptiveThreshold) {
ConsoleLogEnabled(`✅ Server meets criteria (${server.playing}/${server.maxPlayers} players - under ${adaptiveThreshold})`);
// Create card immediately
const cardCreated = await createCardForServer(server);
if (cardCreated) {
newServersFoundThisScan++;
totalNewServersFound++;
}
} else {
ConsoleLogEnabled(`❌ Server rejected (too full): ${server.id} (${server.playing}/${server.maxPlayers} players - need less than ${adaptiveThreshold})`);
}
}
}
}
ConsoleLogEnabled(`\nSCAN ${scanNumber} RESULTS:`);
ConsoleLogEnabled(` - Total servers scanned: ${currentScanServers.length}`);
ConsoleLogEnabled(` - Total known server IDs: ${knownServerIds.size}`);
ConsoleLogEnabled(` - NEW SERVERS FOUND THIS SCAN: ${newServersFoundThisScan}`);
ConsoleLogEnabled(` - TOTAL NEW SERVERS FOUND: ${totalNewServersFound}`);
ConsoleLogEnabled(` - TOTAL CARDS CREATED: ${totalCardsCreated}`);
// Update UI stats
ui.updateStats({
totalScans: totalScansPerformed,
serversFound: knownServerIds.size,
newServers: totalNewServersFound,
cardsCreated: totalCardsCreated
});
if (newServersFoundThisScan > 0) {
ui.updateStatus(`🎉 Found ${newServersFoundThisScan} new server${newServersFoundThisScan > 1 ? 's' : ''}!`, null);
} else {
ui.updateStatus(`🔍 Scan ${scanNumber} - Searching for new servers...`, null);
}
// Wait before next scan
ConsoleLogEnabled(`Waiting ${scanDelay}ms before next scan...`);
await new Promise(resolve => setTimeout(resolve, scanDelay));
scanNumber++;
}
// Scanner was cancelled
ConsoleLogEnabled(`\n=== SCANNER STOPPED BY USER ===`);
ConsoleLogEnabled(`Total scans performed: ${totalScansPerformed}`);
ConsoleLogEnabled(`Total known server IDs: ${knownServerIds.size}`);
ConsoleLogEnabled(`New servers found: ${totalNewServersFound}`);
ConsoleLogEnabled(`Cards created: ${totalCardsCreated}`);
ui.updateStatus('🛑 Scanner stopped by user', null);
Loadingbar(false);
disableFilterButton(false);
}
/*********************************************************************************************************************************************************************************************************************************************
End of: This is all the functions for the 8 buttons
*********************************************************************************************************************************************************************************************************************************************/
/*******************************************************
name of function: disableLoadMoreButton
description: Disables the "Load More" button
*******************************************************/
function disableLoadMoreButton() {
const loadMoreButton = document.querySelector('.rbx-running-games-load-more');
if (loadMoreButton) {
loadMoreButton.disabled = true;
loadMoreButton.style.opacity = '0.5';
loadMoreButton.style.cursor = 'not-allowed';
// only add the label if it doesnt already exist
if (!loadMoreButton.textContent.includes('(Disabled by Rolocate)')) {
loadMoreButton.textContent += ' (Disabled by Rolocate)';
}
ConsoleLogEnabled('Load More button disabled with text change');
} else {
ConsoleLogEnabled('Load More button not found!');
}
}
/*******************************************************
name of function: Loadingbar
description: Shows or hides a loading bar (now using pulsing boxes)
*******************************************************/
function Loadingbar(disable) {
const serverListSection = document.querySelector('#rbx-public-running-games');
const serverCardsContainer = document.querySelector('#rbx-public-game-server-item-container');
const emptyGameInstancesContainer = document.querySelector('.section-content-off.empty-game-instances-container');
const noServersMessage = emptyGameInstancesContainer?.querySelector('.no-servers-message');
// check if the "Unable to load servers." message is visible
if (!serverCardsContainer && noServersMessage?.textContent.includes('Unable to load servers.')) {
notifications('Unable to load servers. Please refresh the page.', 'error', '⚠️', '8000');
return;
}
// reset
if (disable) {
if (serverCardsContainer) {
serverCardsContainer.innerHTML = ''; // Clear contents
serverCardsContainer.removeAttribute('style'); // Remove inline styles if present
}
// no duplicate ones
const existingLoadingBar = document.querySelector('#loading-bar');
if (existingLoadingBar) {
existingLoadingBar.remove(); // Remove the existing loading bar if it exists
}
// Create and display the loading boxes
const loadingContainer = document.createElement('div');
loadingContainer.id = 'loading-bar';
loadingContainer.style.cssText = `
display: flex;
justify-content: center;
align-items: center;
gap: 5px;
margin-top: 10px;
`;
const fragment = document.createDocumentFragment();
for (let i = 0; i < 3; i++) {
const box = document.createElement('div');
box.style.cssText = `
width: 10px;
height: 10px;
background-color: white;
margin: 0 5px;
border-radius: 2px;
animation: pulse 1.2s ${i * 0.2}s infinite;
`;
fragment.appendChild(box);
}
loadingContainer.appendChild(fragment);
if (serverListSection) {
serverListSection.appendChild(loadingContainer);
}
// make thing look good
const existingStyle = document.querySelector('#loading-style');
if (!existingStyle) {
const styleSheet = document.createElement('style');
styleSheet.id = 'loading-style';
styleSheet.textContent = `
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.5); }
}
`;
document.head.appendChild(styleSheet);
}
// target by the unique select IDs that are created in the component
const countryFilter = document.getElementById('countryFilter');
const cityFilter = document.getElementById('cityFilter');
// find the dumb container
let outerDiv = null;
if (countryFilter) {
outerDiv = countryFilter.closest('div[style*="display: flex"][style*="gap: 32px"]');
} else if (cityFilter) {
outerDiv = cityFilter.closest('div[style*="display: flex"][style*="gap: 32px"]');
}
// remove it
if (outerDiv) {
outerDiv.remove();
}
// ik this approach sucks but its the best i can do. it remove ths premium messages with this specific
// text so it doesnet remove the other stuff, you prob cant even understand what im sayin right now
const premiumMessageDiv = document.querySelector('.premium-message-text');
if (premiumMessageDiv) {
const messageText = premiumMessageDiv.textContent.trim();
const errorMessages = [
"Error: Cannot access server regions because you have not purchased the game.",
"Error: This game requires users to teleport to a subplace. As a result, server regions cannot be retrieved."
];
if (errorMessages.includes(messageText)) {
showMessage("END");
}
}
} else {
// If disable is false, remove the loading bar
const loadingBar = document.querySelector('#loading-bar');
if (loadingBar) {
loadingBar.remove();
}
// Reset any applied styles
const styleSheet = document.querySelector('#loading-style');
if (styleSheet) {
styleSheet.remove();
}
}
}
/*******************************************************
name of function: fetchPlayerThumbnails
description: Fetches player thumbnails for up to 5 players. Skips the batch if an error occurs.
*******************************************************/
async function fetchPlayerThumbnails(playerTokens) {
const limitedTokens = playerTokens.slice(0, 5);
const body = limitedTokens.map(token => ({
requestId: `0:${token}:AvatarHeadshot:150x150:png:regular`,
type: "AvatarHeadShot",
targetId: 0,
token,
format: "png",
size: "150x150",
}));
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "POST",
url: "https://thumbnails.roblox.com/v1/batch",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
data: JSON.stringify(body),
onload: function(response) {
try {
if (response.status >= 200 && response.status < 300) {
const data = JSON.parse(response.responseText);
resolve(data.data || []);
} else {
ConsoleLogEnabled(`HTTP error! Status: ${response.status}`);
resolve([]);
}
} catch (error) {
ConsoleLogEnabled('Error parsing batch thumbnail response:', error);
resolve([]);
}
},
onerror: function(err) {
ConsoleLogEnabled('Request error fetching batch thumbnails:', err);
resolve([]);
}
});
});
}
/*******************************************************
name of function: disableFilterButton
description: Disables or enables the filter button based on the input.
*******************************************************/
function disableFilterButton(disable) {
const filterButton = document.querySelector('.RL-filter-button');
const refreshButtons = document.querySelectorAll('.btn-more.rbx-refresh.refresh-link-icon.btn-control-xs.btn-min-width');
const filterOverlayId = 'filter-button-overlay';
const refreshOverlayClass = 'refresh-button-overlay';
if (filterButton) {
const parent = filterButton.parentElement;
if (disable) {
// kill the filter button so it cant be clicked
filterButton.disabled = true;
filterButton.style.opacity = '0.5';
filterButton.style.cursor = 'not-allowed';
// an invisible overlay on it so no sneaky clicks
let overlay = document.getElementById(filterOverlayId);
if (!overlay) {
overlay = document.createElement('div');
overlay.id = filterOverlayId;
overlay.style.position = 'absolute';
overlay.style.top = '-10px';
overlay.style.left = '-10px';
overlay.style.width = 'calc(100% + 20px)';
overlay.style.height = 'calc(100% + 20px)';
overlay.style.backgroundColor = 'transparent';
overlay.style.zIndex = '9999';
overlay.style.pointerEvents = 'all'; // block clicks like a boss
parent.style.position = 'relative';
parent.appendChild(overlay);
}
} else {
// bring the filter button back to life
filterButton.disabled = false;
filterButton.style.opacity = '1';
filterButton.style.cursor = 'pointer';
// remove that annoying overlay
const overlay = document.getElementById(filterOverlayId);
if (overlay) {
overlay.remove();
}
}
} else {
ConsoleLogEnabled('Filter button not found! Something is wrong!');
notifications("Something's wrong. Please report an issue on Greasyfork.", "error", "⚠️", "15000");
}
if (refreshButtons.length > 0) {
refreshButtons.forEach((refreshButton) => {
const refreshParent = refreshButton.parentElement;
if (disable) {
// same overlay trick but for refresh buttons
let refreshOverlay = refreshParent.querySelector(`.${refreshOverlayClass}`);
if (!refreshOverlay) {
refreshOverlay = document.createElement('div');
refreshOverlay.className = refreshOverlayClass;
refreshOverlay.style.position = 'absolute';
refreshOverlay.style.top = '-10px';
refreshOverlay.style.left = '-10px';
refreshOverlay.style.width = 'calc(100% + 20px)';
refreshOverlay.style.height = 'calc(100% + 20px)';
refreshOverlay.style.backgroundColor = 'transparent';
refreshOverlay.style.zIndex = '9999';
refreshOverlay.style.pointerEvents = 'all'; // no clicks allowed here either
refreshParent.style.position = 'relative';
refreshParent.appendChild(refreshOverlay);
}
} else {
// remove overlays and let buttons live again
const refreshOverlay = refreshParent.querySelector(`.${refreshOverlayClass}`);
if (refreshOverlay) {
refreshOverlay.remove();
}
}
});
} else {
ConsoleLogEnabled('Refresh button not found!');
notifications("Something's wrong. Please report an issue on Greasyfork.", "error", "⚠️", "15000");
}
}
/*******************************************************
name of function: rbx_card
description: Creates the roblox cards that are not from server regions
*******************************************************/
async function rbx_card(serverId, playerTokens, maxPlayers, playing, gameId) {
const thumbnails = await fetchPlayerThumbnails(playerTokens);
const cardItem = document.createElement('li');
cardItem.className = 'rbx-game-server-item col-md-3 col-sm-4 col-xs-6';
// Create player thumbnails container
const playerThumbnailsContainer = document.createElement('div');
playerThumbnailsContainer.className = 'player-thumbnails-container';
// Add player thumbnails (up to 5)
thumbnails.forEach(thumbnail => {
const playerAvatar = document.createElement('span');
playerAvatar.className = 'avatar avatar-headshot-md player-avatar';
const thumbnailImage = document.createElement('span');
thumbnailImage.className = 'thumbnail-2d-container avatar-card-image';
const img = document.createElement('img');
Object.assign(img, {
src: thumbnail.imageUrl,
alt: '',
title: ''
});
thumbnailImage.appendChild(img);
playerAvatar.appendChild(thumbnailImage);
playerThumbnailsContainer.appendChild(playerAvatar);
});
// Add placeholder for remaining players
if (playing > 5) {
const placeholder = document.createElement('span');
placeholder.className = 'avatar avatar-headshot-md player-avatar hidden-players-placeholder';
placeholder.textContent = `+${playing - 5}`;
placeholder.style.cssText = `
background-color: #6a6f81;
color: white;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: 16px;
width: 60px;
height: 60px;
`;
playerThumbnailsContainer.appendChild(placeholder);
}
// Create server details
const serverDetails = document.createElement('div');
serverDetails.className = 'rbx-game-server-details game-server-details';
// Server status
const serverStatus = document.createElement('div');
serverStatus.className = 'text-info rbx-game-status rbx-game-server-status text-overflow';
serverStatus.textContent = `${playing} of ${maxPlayers} people max`;
serverDetails.appendChild(serverStatus);
// Player count gauge
const gaugeContainer = document.createElement('div');
gaugeContainer.className = 'server-player-count-gauge border';
const gaugeInner = document.createElement('div');
gaugeInner.className = 'gauge-inner-bar border';
gaugeInner.style.width = `${(playing / maxPlayers) * 100}%`;
gaugeContainer.appendChild(gaugeInner);
serverDetails.appendChild(gaugeContainer);
// Button container
const buttonContainer = document.createElement('div');
buttonContainer.className = 'button-container';
buttonContainer.style.cssText = 'display: flex; gap: 8px;';
// Join button
const joinButton = document.createElement('button');
Object.assign(joinButton, {
type: 'button',
className: 'btn-full-width btn-control-xs rbx-game-server-join game-server-join-btn btn-primary-md btn-min-width',
textContent: 'Join'
});
joinButton.addEventListener('click', () => {
Roblox.GameLauncher.joinGameInstance(gameId, serverId);
});
// Invite button
const inviteButton = document.createElement('button');
Object.assign(inviteButton, {
type: 'button',
className: 'btn-full-width btn-control-xs rbx-game-server-invite game-server-invite-btn btn-secondary-md btn-min-width',
textContent: 'Invite'
});
inviteButton.addEventListener('click', () => {
const inviteLink = `https://oqarshi.github.io/Invite/?placeid=${gameId}&serverid=${serverId}`;
ConsoleLogEnabled('Copied invite link:', inviteLink);
navigator.clipboard.writeText(inviteLink).then(() => {
notifications('Success! Invite link copied to clipboard!', 'success', '🎉', '2000');
ConsoleLogEnabled('Invite link copied to clipboard');
const originalText = inviteButton.textContent;
inviteButton.textContent = 'Copied!';
inviteButton.disabled = true;
setTimeout(() => {
inviteButton.textContent = originalText;
inviteButton.disabled = false;
}, 1000);
}).catch(err => {
ConsoleLogEnabled('Failed to copy invite link:', err);
notifications('Failed! Invite link copied to clipboard!', 'error', '⚠️', '2000');
});
});
buttonContainer.append(joinButton, inviteButton);
serverDetails.appendChild(buttonContainer);
// Assemble the card
const cardContainer = document.createElement('div');
cardContainer.className = 'card-item';
cardContainer.append(playerThumbnailsContainer, serverDetails);
cardItem.appendChild(cardContainer);
// Add to server list
document.querySelector('#rbx-public-game-server-item-container').appendChild(cardItem);
}
/*******************************************************
name of function: showLoadingOverlay
description: Loading box when joining a server + Shows server location
*******************************************************/
// WARNING: Do not republish this script. Licensed for personal use only.
async function showLoadingOverlay(gameId, serverId) {
// Remove existing overlay if present
const existingOverlay = document.querySelector('[data-loading-overlay]');
if (existingOverlay) {
existingOverlay.style.opacity = '0';
existingOverlay.querySelector('div').style.transform = 'translate(-50%, -55%) scale(0.9)';
setTimeout(() => existingOverlay.remove(), 400);
}
// Remove existing styles
const existingStyle = document.querySelector('[data-loading-overlay-style]');
if (existingStyle) existingStyle.remove();
// Helper function to create elements with styles
const createElement = (tag, styles, content = '') => {
const el = document.createElement(tag);
Object.assign(el.style, styles);
if (content) el.innerHTML = content;
return el;
};
// Helper function for common gradient background
const gradientBg = (color1, color2) => `linear-gradient(145deg, ${color1}, ${color2})`;
// Create CSS animations
const style = createElement('style', {}, `
@keyframes loading-slide {
0% { transform: translateX(-100%); background-position: 0% 50%; }
100% { transform: translateX(250%); background-position: 100% 50%; }
}
@keyframes pulse-glow {
0%, 100% { opacity: 0.7; transform: scale(1); }
50% { opacity: 1; transform: scale(1.05); }
}
@keyframes dots {
0%, 20% { content: ''; }
40% { content: '.'; }
60% { content: '..'; }
80%, 100% { content: '...'; }
}
`);
style.setAttribute('data-loading-overlay-style', '');
document.head.appendChild(style);
// Main overlay
const overlay = createElement('div', {
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.12)',
zIndex: '999999',
opacity: '0',
transition: 'opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1)'
});
overlay.setAttribute('data-loading-overlay', '');
// Main container
const container = createElement('div', {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -55%) scale(0.9)',
width: '534px',
height: '380px',
background: gradientBg('#1e1e1e', '#161616'),
borderRadius: '22px',
boxShadow: '0 18px 55px rgba(0, 0, 0, 0.5), 0 7px 23px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1)',
border: '1px solid rgba(255, 255, 255, 0.12)',
display: 'flex',
flexDirection: 'column',
padding: '33px',
fontFamily: 'system-ui, -apple-system, sans-serif',
zIndex: '1000000',
opacity: '0',
transition: 'opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1), transform 0.5s cubic-bezier(0.4, 0, 0.2, 1)'
});
// Exit button with hover effects
const exitButton = createElement('button', {
position: 'absolute',
top: '15px',
right: '15px',
width: '33px',
height: '33px',
borderRadius: '11px',
border: '1px solid rgba(255, 255, 255, 0.15)',
background: gradientBg('#2a2a2a', '#1f1f1f'),
color: '#a0a0a0',
fontSize: '15px',
fontWeight: '600',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
boxShadow: '0 2px 7px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1)',
outline: 'none'
}, '✕');
// Exit button event handlers
const exitButtonHover = (enter) => {
exitButton.style.background = enter ? gradientBg('#333333', '#262626') : gradientBg('#2a2a2a', '#1f1f1f');
exitButton.style.color = enter ? '#ffffff' : '#a0a0a0';
exitButton.style.transform = enter ? 'scale(1.05)' : 'scale(1)';
exitButton.style.boxShadow = enter ?
'0 4px 11px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.15)' :
'0 2px 7px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1)';
};
exitButton.addEventListener('mouseenter', () => exitButtonHover(true));
exitButton.addEventListener('mouseleave', () => exitButtonHover(false));
exitButton.addEventListener('mousedown', () => exitButton.style.transform = 'scale(0.95)');
exitButton.addEventListener('mouseup', () => exitButton.style.transform = 'scale(1.05)');
// Top section
const topSection = createElement('div', {
display: 'flex',
alignItems: 'center',
marginBottom: '26px'
});
// Icon container
const iconContainer = createElement('div', {
width: '77px',
height: '77px',
borderRadius: '18px',
background: gradientBg('#2a2a2a', '#1f1f1f'),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginRight: '22px',
border: '1px solid rgba(255, 255, 255, 0.15)',
overflow: 'hidden',
flexShrink: '0',
boxShadow: '0 4px 15px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1)',
transition: 'transform 0.3s ease, box-shadow 0.3s ease'
});
// Default logo
const defaultLogo = createElement('div', {
width: '40px',
height: '40px',
borderRadius: '11px',
background: gradientBg('#404040', '#333333'),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '18px',
animation: 'pulse-glow 2s ease-in-out infinite'
}, `<img src="${window.Base64Images.logo}" alt="Logo" width="80" height="80">`);
// Game icon
const gameIcon = createElement('img', {
width: '100%',
height: '100%',
objectFit: 'cover',
borderRadius: '18px',
display: 'none',
transition: 'opacity 0.3s ease'
});
iconContainer.appendChild(defaultLogo);
iconContainer.appendChild(gameIcon);
// Text container
const textContainer = createElement('div', {
flex: '1',
display: 'flex',
flexDirection: 'column'
});
// Main loading text
const isServerHopping = !gameId || !serverId;
const loadingText = createElement('div', {
fontSize: '24px',
fontWeight: '700',
background: 'linear-gradient(135deg, #ffffff, #e5e5e5)',
webkitBackgroundClip: 'text',
webkitTextFillColor: 'transparent',
backgroundClip: 'text',
marginBottom: '6px',
letterSpacing: '-0.03em',
lineHeight: '1.2'
}, isServerHopping ? 'Server Hopping' : 'Joining Roblox Game');
// Animated dots
const dotsSpan = createElement('span', {
animation: 'dots 1.5s steps(4, end) infinite'
});
loadingText.appendChild(dotsSpan);
// Status text
const statusText = createElement('div', {
fontSize: '14px',
color: '#a0a0a0',
lineHeight: '1.4',
fontWeight: '500',
marginBottom: '12px'
}, isServerHopping ? 'Picking a random server...' : 'Please wait while we connect you');
textContainer.appendChild(loadingText);
textContainer.appendChild(statusText);
topSection.appendChild(iconContainer);
topSection.appendChild(textContainer);
// Location section
const locationSection = createElement('div', {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '16px',
padding: '18px',
background: gradientBg('#282828', '#202020'),
borderRadius: '15px',
border: '1px solid rgba(255, 255, 255, 0.1)',
minHeight: '60px',
boxShadow: 'inset 0 2px 4px rgba(0, 0, 0, 0.2)',
position: 'relative',
overflow: 'hidden'
});
// Background pattern
const pattern = createElement('div', {
position: 'absolute',
top: '0',
left: '0',
right: '0',
bottom: '0',
opacity: '0.03',
backgroundImage: 'radial-gradient(circle at 2px 2px, white 1px, transparent 0)',
backgroundSize: '18px 18px'
});
locationSection.appendChild(pattern);
// Location content
const locationContent = createElement('div', {
textAlign: 'center',
opacity: '0',
transition: 'opacity 0.4s ease, transform 0.4s ease',
transform: 'translateY(10px)',
zIndex: '1',
position: 'relative'
});
const locationDisplay = createElement('div', {
fontSize: '17px',
color: '#ffffff',
fontWeight: '600',
marginBottom: '4px',
letterSpacing: '-0.01em'
});
const locationSubtext = createElement('div', {
fontSize: '12px',
color: '#999999',
fontWeight: '500',
textTransform: 'uppercase',
letterSpacing: '0.5px'
});
locationContent.appendChild(locationDisplay);
locationContent.appendChild(locationSubtext);
locationSection.appendChild(locationContent);
// Server details container
const serverDetailsContainer = createElement('div', {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '12px',
marginBottom: '12px',
padding: '12px 16px',
background: gradientBg('#262626', '#1e1e1e'),
borderRadius: '12px',
border: '1px solid rgba(255, 255, 255, 0.08)',
opacity: '0',
transition: 'opacity 0.4s ease 0.2s',
boxShadow: 'inset 0 1px 3px rgba(0, 0, 0, 0.2)'
});
// Helper function to create ID displays
const createIdDisplay = (label, value, color) => {
const container = createElement('div', {
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '6px 10px',
background: 'rgba(255, 255, 255, 0.03)',
borderRadius: '8px',
border: '1px solid rgba(255, 255, 255, 0.06)',
flex: '1',
minWidth: '0'
});
const labelSpan = createElement('span', {
fontSize: '9px',
color: '#888888',
fontWeight: '600',
textTransform: 'uppercase',
letterSpacing: '0.5px',
flexShrink: '0'
}, label);
const valueSpan = createElement('span', {
fontSize: '11px',
color: color,
fontWeight: '600',
fontFamily: 'Monaco, Consolas, monospace',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
minWidth: '0',
flex: '1'
}, value || 'N/A');
container.appendChild(labelSpan);
container.appendChild(valueSpan);
return container;
};
serverDetailsContainer.appendChild(createIdDisplay('Game', gameId, '#60a5fa'));
serverDetailsContainer.appendChild(createIdDisplay('Server', serverId, '#34d399'));
// Loading bar container
const loadingBarContainer = createElement('div', {
width: '100%',
height: '7px',
backgroundColor: '#2a2a2a',
borderRadius: '4px',
overflow: 'hidden',
marginBottom: '12px',
boxShadow: 'inset 0 2px 4px rgba(0, 0, 0, 0.3)',
border: '1px solid rgba(255, 255, 255, 0.05)'
});
// Loading bar
const loadingBar = createElement('div', {
height: '100%',
background: 'linear-gradient(90deg, #3b82f6, #60a5fa, #93c5fd, #60a5fa, #3b82f6)',
backgroundSize: '300% 100%',
borderRadius: '4px',
animation: 'loading-slide 2s ease-in-out infinite',
width: '60%',
boxShadow: '0 0 11px rgba(96, 165, 250, 0.4)'
});
loadingBarContainer.appendChild(loadingBar);
// Branding section
const brandingSection = createElement('div', {
textAlign: 'center',
marginTop: 'auto',
paddingTop: '8px',
borderTop: '1px solid rgba(255, 255, 255, 0.06)'
});
const brandingText = createElement('div', {
fontSize: '11px',
color: '#666666',
fontWeight: '600',
letterSpacing: '0.8px',
textTransform: 'uppercase',
opacity: '0.7',
transition: 'opacity 0.2s ease, color 0.2s ease'
}, 'RoLocate by Oqarshi');
brandingText.addEventListener('mouseenter', () => {
brandingText.style.opacity = '1';
brandingText.style.color = '#888888';
});
brandingText.addEventListener('mouseleave', () => {
brandingText.style.opacity = '0.7';
brandingText.style.color = '#666666';
});
brandingSection.appendChild(brandingText);
// Assemble overlay
container.appendChild(exitButton);
container.appendChild(topSection);
container.appendChild(locationSection);
container.appendChild(serverDetailsContainer);
container.appendChild(loadingBarContainer);
container.appendChild(brandingSection);
overlay.appendChild(container);
document.body.appendChild(overlay);
// Fade in animation
setTimeout(() => {
overlay.style.opacity = '1';
container.style.opacity = '1';
container.style.transform = 'translate(-50%, -50%) scale(1)';
}, 50);
setTimeout(() => serverDetailsContainer.style.opacity = '1', 300);
// Icon hover effects
const iconHover = (enter) => {
iconContainer.style.transform = enter ? 'scale(1.05)' : 'scale(1)';
iconContainer.style.boxShadow = enter ?
'0 7px 23px rgba(0, 0, 0, 0.4), 0 0 18px rgba(96, 165, 250, 0.2)' :
'0 4px 15px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1)';
};
iconContainer.addEventListener('mouseenter', () => iconHover(true));
iconContainer.addEventListener('mouseleave', () => iconHover(false));
// Fetch game icon
if (gameId) {
getUniverseIdFromPlaceId(gameId)
.then(universeId => getGameIconFromUniverseId(universeId))
.then(iconUrl => {
gameIcon.src = iconUrl;
gameIcon.onload = () => {
defaultLogo.style.opacity = '0';
setTimeout(() => {
defaultLogo.style.display = 'none';
gameIcon.style.display = 'block';
gameIcon.style.opacity = '1';
}, 200);
};
gameIcon.onerror = () => ConsoleLogEnabled('Failed to load game icon, using default');
})
.catch(error => ConsoleLogEnabled('Error fetching game icon:', error));
}
// Server location detection
(async () => {
statusText.textContent = isServerHopping ? 'Finding available server...' : 'Locating server location...';
await new Promise(resolve => setTimeout(resolve, 1000));
try {
if (isServerHopping) {
locationDisplay.innerHTML = '🌍 Random Server';
locationSubtext.textContent = 'SERVER HOPPING';
statusText.textContent = 'Connecting to random server...';
} else {
const locationData = await fetchServerDetails(gameId, serverId);
const flagEmoji = getFlagEmoji(locationData.country.code);
locationDisplay.innerHTML = '';
locationDisplay.appendChild(flagEmoji);
locationDisplay.append(` ${locationData.city}, ${locationData.country.name}`);
locationSubtext.textContent = 'Server Located';
statusText.innerHTML = `Connecting to <strong style="color: #60a5fa; font-weight: 600;">${locationData.city}</strong> server`;
}
} catch (error) {
ConsoleLogEnabled('Error fetching server location:', error);
locationDisplay.innerHTML = isServerHopping ? '🌍 Random Server' : '🌍 Unknown Server Location';
locationSubtext.textContent = isServerHopping ? 'SERVER HOPPING' : 'JOINING FULL/RESTRICTED SERVER';
statusText.textContent = isServerHopping ? 'Connecting to random server...' : 'Joining Server...';
}
locationContent.style.opacity = '1';
locationContent.style.transform = 'translateY(0)';
})();
// Cleanup function
const cleanup = () => {
overlay.style.opacity = '0';
container.style.transform = 'translate(-50%, -55%) scale(0.9)';
setTimeout(() => {
overlay.remove();
style.remove();
}, 400);
};
// Auto-hide after 6 seconds
const fadeOutTimer = setTimeout(cleanup, 6000);
// Exit button handler
exitButton.addEventListener('click', () => {
clearTimeout(fadeOutTimer);
cleanup();
});
}
/**
* Fetch Universe ID from Place ID using GM_xmlhttpRequest (Tampermonkey/Greasemonkey)
* @param {number|string} placeId
* @returns {Promise<number>} resolves with universeId or rejects on error
*/
function getUniverseIdFromPlaceId(placeId) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://games.roblox.com/v1/games/multiget-place-details?placeIds=${placeId}`,
headers: {
"Accept": "application/json"
},
onload: function(response) {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
if (Array.isArray(data) && data.length > 0 && data[0].universeId) {
// Console log inside the function
ConsoleLogEnabled(`Universe ID for place ${placeId}: ${data[0].universeId}`);
resolve(data[0].universeId);
} else {
reject(new Error("Universe ID not found in response."));
}
} catch (e) {
reject(e);
}
} else {
reject(new Error(`HTTP error! Status: ${response.status}`));
}
},
onerror: function(err) {
reject(err);
}
});
});
}
/**
* Fetches the game icon thumbnail URL using universeId via GM_xmlhttpRequest
* @param {number|string} universeId - The Universe ID of the game
* @returns {Promise<string>} Resolves with the image URL of the game icon
*/
function getGameIconFromUniverseId(universeId) {
const apiUrl = `https://thumbnails.roblox.com/v1/games/icons?universeIds=${universeId}&size=512x512&format=Png&isCircular=false&returnPolicy=PlaceHolder`;
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: apiUrl,
headers: {
"Accept": "application/json"
},
onload: function(response) {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
if (Array.isArray(data.data) && data.data.length > 0 && data.data[0].imageUrl) {
ConsoleLogEnabled(`Game icon URL for universe ${universeId}: ${data.data[0].imageUrl}`);
resolve(data.data[0].imageUrl);
} else {
reject(new Error("Image URL not found in response."));
}
} catch (err) {
reject(err);
}
} else {
reject(new Error(`HTTP error! Status: ${response.status}`));
}
},
onerror: function(err) {
reject(err);
}
});
});
}
}
})();