// ==UserScript==
// @name RoLocate
// @namespace https://oqarshi.github.io/
// @version 41.0
// @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.icu/scripts/535590/Rolocate%20Base64%20Image%20Library%2020.js
// @require https://update.greasyfork.icu/scripts/539427/Rolocate%20Server%20Region%20Data.js
// @require https://update.greasyfork.icu/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
// @downloadURL none
// ==/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: ' ',
error: ' ',
warning: ' ',
info: ' '
};
// Build content
toast.innerHTML = `
${icons[type] || icons.info}
${emoji ? `
${emoji} ` : ''}
${message.replace(/\n/g, ' ')}
`;
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, ' ');
},
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;
}
};
}
/*******************************************************
name of function: Update_Popup
description: update function poup
*******************************************************/
function Update_Popup() {
const VERSION = "V41.0";
const PREV_VERSION = "V40.0";
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 css = `
.ROLOCATE_UPDATE_POPUP_first-time-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: ROLOCATE_UPDATE_POPUP_fadeIn 0.5s cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
}
.ROLOCATE_UPDATE_POPUP_first-time-popup-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: ROLOCATE_UPDATE_POPUP_scaleUp 0.6s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards;
position: relative;
display: flex;
flex-direction: column;
will-change: transform;
}
.ROLOCATE_UPDATE_POPUP_popup-header {
padding: 24px 32px;
border-bottom: 1px solid #404040;
display: flex;
align-items: center;
gap: 16px;
background: #1f1f1f;
position: relative;
}
.ROLOCATE_UPDATE_POPUP_popup-logo {
width: 56px;
height: 56px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
flex-shrink: 0;
}
.ROLOCATE_UPDATE_POPUP_popup-header-content {
flex: 1;
}
.ROLOCATE_UPDATE_POPUP_popup-title {
font-size: 24px;
font-weight: 600;
color: #ffffff;
margin: 0 0 4px;
letter-spacing: -0.5px;
}
.ROLOCATE_UPDATE_POPUP_popup-version {
display: inline-block;
background: #1a1a1a;
color: #ffffff;
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
border: 1px solid #404040;
}
.ROLOCATE_UPDATE_POPUP_popup-main {
display: flex;
flex: 1;
min-height: 0;
}
.ROLOCATE_UPDATE_POPUP_popup-left {
flex: 1;
padding: 24px;
border-right: 1px solid #404040;
overflow-y: auto;
background: #252525;
}
.ROLOCATE_UPDATE_POPUP_popup-right {
flex: 1;
padding: 24px;
overflow-y: auto;
background: #2a2a2a;
display: flex;
flex-direction: column;
}
.ROLOCATE_UPDATE_POPUP_features-title {
font-size: 18px;
font-weight: 600;
color: #ffffff;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.ROLOCATE_UPDATE_POPUP_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;
}
.ROLOCATE_UPDATE_POPUP_feature-item:hover {
border-color: #555555;
background: #303030;
transform: translateY(-2px);
}
.ROLOCATE_UPDATE_POPUP_feature-item.ROLOCATE_UPDATE_POPUP_active {
border-color: #666666;
background: #303030;
}
.ROLOCATE_UPDATE_POPUP_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;
}
.ROLOCATE_UPDATE_POPUP_feature-item:hover .ROLOCATE_UPDATE_POPUP_feature-header {
background: #2a2a2a;
}
.ROLOCATE_UPDATE_POPUP_feature-item.ROLOCATE_UPDATE_POPUP_active .ROLOCATE_UPDATE_POPUP_feature-header {
background: #333333;
}
.ROLOCATE_UPDATE_POPUP_feature-icon {
font-size: 20px;
margin-right: 12px;
min-width: 24px;
transition: transform 0.3s ease;
}
.ROLOCATE_UPDATE_POPUP_feature-item:hover .ROLOCATE_UPDATE_POPUP_feature-icon {
transform: scale(1.1);
}
.ROLOCATE_UPDATE_POPUP_feature-title {
flex: 1;
font-size: 15px;
font-weight: 500;
color: #ffffff;
margin: 0;
}
.ROLOCATE_UPDATE_POPUP_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;
}
.ROLOCATE_UPDATE_POPUP_feature-item:hover .ROLOCATE_UPDATE_POPUP_feature-badge {
transform: translateX(3px);
}
.ROLOCATE_UPDATE_POPUP_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: ROLOCATE_UPDATE_POPUP_fadeInUp 0.6s cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
will-change: transform, opacity;
}
.ROLOCATE_UPDATE_POPUP_detail-title {
font-size: 20px;
font-weight: 600;
color: #ffffff;
margin: 0 0 8px;
display: flex;
align-items: center;
gap: 10px;
}
.ROLOCATE_UPDATE_POPUP_detail-subtitle {
font-size: 13px;
color: #999999;
margin-bottom: 16px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.ROLOCATE_UPDATE_POPUP_detail-description {
font-size: 14px;
color: #cccccc;
line-height: 1.6;
margin-bottom: 16px;
flex: 1;
}
.ROLOCATE_UPDATE_POPUP_detail-settings {
padding: 16px;
background: #252525;
border-radius: 8px;
border: 1px solid #404040;
margin-top: auto;
}
.ROLOCATE_UPDATE_POPUP_setting-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.ROLOCATE_UPDATE_POPUP_setting-row:last-child {
margin-bottom: 0;
}
.ROLOCATE_UPDATE_POPUP_setting-label {
font-size: 13px;
color: #cccccc;
font-weight: 500;
}
.ROLOCATE_UPDATE_POPUP_setting-value {
font-size: 12px;
color: #999999;
padding: 4px 8px;
background: #1a1a1a;
border-radius: 4px;
border: 1px solid #404040;
}
.ROLOCATE_UPDATE_POPUP_welcome-panel {
text-align: center;
padding: 40px 20px;
color: #999999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
.ROLOCATE_UPDATE_POPUP_welcome-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
animation: ROLOCATE_UPDATE_POPUP_float 4s ease-in-out infinite;
}
.ROLOCATE_UPDATE_POPUP_welcome-text {
font-size: 16px;
margin-bottom: 8px;
}
.ROLOCATE_UPDATE_POPUP_welcome-subtext {
font-size: 13px;
color: #666666;
}
.ROLOCATE_UPDATE_POPUP_developer-message {
background: #1a1a1a;
border-radius: 8px;
padding: 16px;
margin-bottom: 20px;
border-left: 3px solid #555555;
transition: all 0.4s ease;
}
.ROLOCATE_UPDATE_POPUP_developer-message:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.ROLOCATE_UPDATE_POPUP_developer-message-title {
font-weight: 600;
color: #ffffff;
margin-bottom: 8px;
font-size: 14px;
}
.ROLOCATE_UPDATE_POPUP_developer-message-text {
font-size: 13px;
color: #cccccc;
line-height: 1.5;
}
.ROLOCATE_UPDATE_POPUP_help-section {
background: #1f1f1f;
border-radius: 8px;
padding: 16px;
border: 1px solid #404040;
}
.ROLOCATE_UPDATE_POPUP_help-title {
font-size: 14px;
font-weight: 600;
color: #ffffff;
margin-bottom: 12px;
}
.ROLOCATE_UPDATE_POPUP_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);
}
.ROLOCATE_UPDATE_POPUP_help-link:hover {
color: #ffffff;
background: rgba(112, 165, 255, 0.2);
border-color: rgba(112, 165, 255, 0.4);
transform: translateY(-2px);
}
.ROLOCATE_UPDATE_POPUP_help-link-icon {
font-size: 16px;
transition: transform 0.3s ease;
}
.ROLOCATE_UPDATE_POPUP_help-link:hover .ROLOCATE_UPDATE_POPUP_help-link-icon {
transform: translateY(-2px);
}
.ROLOCATE_UPDATE_POPUP_first-time-popup-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;
}
.ROLOCATE_UPDATE_POPUP_first-time-popup-close:hover {
color: #ffffff;
background: rgba(255, 255, 255, 0.1);
border-color: #555555;
transform: rotate(90deg);
}
.ROLOCATE_UPDATE_POPUP_popup-footer {
padding: 16px 32px;
border-top: 1px solid #404040;
background: #1f1f1f;
text-align: center;
}
.ROLOCATE_UPDATE_POPUP_popup-note {
font-size: 12px;
color: #999999;
margin: 0;
}
@keyframes ROLOCATE_UPDATE_POPUP_fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes ROLOCATE_UPDATE_POPUP_fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes ROLOCATE_UPDATE_POPUP_scaleUp {
0% { transform: scale(0.95) translateY(10px); }
100% { transform: scale(1) translateY(0); }
}
@keyframes ROLOCATE_UPDATE_POPUP_scaleDown {
from { transform: scale(1); }
to { transform: scale(0.9); opacity: 0; }
}
@keyframes ROLOCATE_UPDATE_POPUP_fadeInUp {
from {
opacity: 0;
transform: translateY(15px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes ROLOCATE_UPDATE_POPUP_float {
0% { transform: translateY(0px); }
50% { transform: translateY(-5px); }
100% { transform: translateY(0px); }
}
/* Scrollbar styling */
.ROLOCATE_UPDATE_POPUP_popup-left::-webkit-scrollbar,
.ROLOCATE_UPDATE_POPUP_popup-right::-webkit-scrollbar {
width: 6px;
}
.ROLOCATE_UPDATE_POPUP_popup-left::-webkit-scrollbar-track,
.ROLOCATE_UPDATE_POPUP_popup-right::-webkit-scrollbar-track {
background: #1a1a1a;
}
.ROLOCATE_UPDATE_POPUP_popup-left::-webkit-scrollbar-thumb,
.ROLOCATE_UPDATE_POPUP_popup-right::-webkit-scrollbar-thumb {
background: #555555;
border-radius: 3px;
transition: background 0.3s ease;
}
.ROLOCATE_UPDATE_POPUP_popup-left::-webkit-scrollbar-thumb:hover,
.ROLOCATE_UPDATE_POPUP_popup-right::-webkit-scrollbar-thumb:hover {
background: #666666;
}
/* Responsive design */
@media (max-width: 768px) {
.ROLOCATE_UPDATE_POPUP_first-time-popup-content {
width: 95%;
flex-direction: column;
}
.ROLOCATE_UPDATE_POPUP_popup-main {
flex-direction: column;
}
.ROLOCATE_UPDATE_POPUP_popup-left, .ROLOCATE_UPDATE_POPUP_popup-right {
flex: none;
}
.ROLOCATE_UPDATE_POPUP_popup-left {
border-right: none;
border-bottom: 1px solid #404040;
}
}
`;
const style = document.createElement('style');
style.innerHTML = css;
document.head.appendChild(style);
const featureData = {
betterfriends: {
title: "Better Friends & Best Friends",
icon: "π€",
subtitle: "Best Friends appear first",
description: "This new feature improves the look of the friend bar on the roblox website. It also introduces best friends which show the status of those friends first Very buggy so beware.",
settings: [{
label: "Enabled by default",
value: "True"
},
{
label: "Toggle Location",
value: "Extras Tab"
},
{
label: "Scope",
value: "Roblox.com/home"
}
]
},
smartsearch: {
title: "SmartSearch",
icon: "π§ ",
subtitle: "Smarter, Faster Searches",
description: "SmartSearch now prioritizes friends.",
settings: [{
label: "Enabled by default",
value: "True"
},
{
label: "Toggle Location",
value: "General Tab"
},
{
label: "Scope",
value: "Roblox.com/*"
}
]
},
serverhop: {
title: "Serverhop",
icon: "π°",
subtitle: "Quickly Hop Servers",
description: "Fixed Serverhop button not showing a popup.",
settings: [{
label: "Enabled by default",
value: "True"
},
{
label: "Toggle Location",
value: "Advanced Tab"
},
{
label: "Scope",
value: "Roblox.com/games/*"
}
]
},
quicklaunch: {
title: "Quick Launch",
icon: "β‘",
subtitle: "Quickly Play Your Games",
description: "Fixed Quick Launch not working without SmartSearch.",
settings: [{
label: "Enabled by default",
value: "True"
},
{
label: "Toggle Location",
value: "Extras Tab"
},
{
label: "Scope",
value: "Roblox.com/home"
}
]
},
serverfilterregion: {
title: "Server Region Filter",
icon: "π",
subtitle: "Find Server Regions",
description: "Finding server regions is now ~3x faster with fast search on.\nAlso improved the fallback logic.",
settings: [{
label: "Enabled by default",
value: "True"
},
{
label: "Toggle Location",
value: "Advanced Tab & General Tab"
},
{
label: "Scope",
value: "Roblox.com/games/*"
}
]
},
recentservers: {
title: "Recent Servers",
icon: "π",
subtitle: "See Your Recent Servers",
description: "Added 'Clear All' button for recent servers.",
settings: [{
label: "Enabled by default",
value: "True"
},
{
label: "Toggle Location",
value: "General Tab"
},
{
label: "Scope",
value: "Roblox.com/games/*"
}
]
},
bugfix_feature: {
title: "Small Bug Fixes & Improvements",
icon: "π·οΈ",
subtitle: "More Bug Fixes & Improvements",
description: "π Bug Fixes:\n- Fixed duplicate arrow in server popup. \n- Fixed a bug where the setting remove all roblox ads would crash roblox if the extension ublock origin is installed. \n- Added toggle for server location popup in Settings. \n- Fixed margin error on homepage.",
settings: []
}
};
const popupHTML = `
`;
const popupContainer = document.createElement('div');
popupContainer.innerHTML = popupHTML;
document.body.appendChild(popupContainer);
const closeButton = popupContainer.querySelector('.ROLOCATE_UPDATE_POPUP_first-time-popup-close');
const popup = popupContainer.querySelector('.ROLOCATE_UPDATE_POPUP_first-time-popup');
const featureItems = popupContainer.querySelectorAll('.ROLOCATE_UPDATE_POPUP_feature-item');
const welcomePanel = popupContainer.querySelector('#ROLOCATE_UPDATE_POPUP_welcome-panel');
const detailPanel = popupContainer.querySelector('#ROLOCATE_UPDATE_POPUP_detail-panel');
featureItems.forEach(item => {
item.addEventListener('click', (e) => {
featureItems.forEach(i => i.classList.remove('ROLOCATE_UPDATE_POPUP_active'));
item.classList.add('ROLOCATE_UPDATE_POPUP_active');
const featureKey = item.dataset.feature;
const feature = featureData[featureKey];
if (feature) {
welcomePanel.style.display = 'none';
detailPanel.style.display = 'flex';
detailPanel.classList.remove('ROLOCATE_UPDATE_POPUP_detail-panel');
void detailPanel.offsetWidth;
detailPanel.classList.add('ROLOCATE_UPDATE_POPUP_detail-panel');
detailPanel.innerHTML = `
`;
}
});
});
closeButton.addEventListener('click', (e) => {
popup.style.animation = 'ROLOCATE_UPDATE_POPUP_fadeOut 0.5s cubic-bezier(0.22, 0.61, 0.36, 1) forwards';
popup.querySelector('.ROLOCATE_UPDATE_POPUP_first-time-popup-content').style.animation = 'ROLOCATE_UPDATE_POPUP_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 = `
RoLocate
RoLocate needs to refresh the page to enable some features.
Refresh Now
`;
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
// experiencedtime: false, // not needed anymore
};
// 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 `
Rolocate: Version 41.0
Rolocate by Oqarshi.
Licensed under a Custom License β Personal Use Only . No redistribution.
`;
}
if (section === "appearance") {
return `
Disable Trailer Autoplay
?
Smart Join Popup
New
Just Released/Updated
?
Remove All Roblox Ads
?
Quick Navigation
Edit
?
`;
}
if (section === "advanced") {
return `
`;
}
if (section === "extras") {
return `
`;
}
if (section === "about") {
return `
Credits
This project was created by:
`;
}
if (section === "help") {
return `
βοΈ General Tab
SmartSearch: Improves the Roblox websiteβs search bar by enabling instant searches for games, users, and groups.
Auto Server Regions: Replaces Roblox's 8 default servers with at least 8 servers, providing detailed info such as location and ping.
Fast Server Search: Boosts server search speed up to 100x (experimental). Replaces player thumbnails with Builderman/Roblox icons to bypass rate limits.
Invert Player Count: For server regions: shows low-player servers when enabled, high-player servers when disabled. You can also control this on the Roblox server popup.
Recent Servers: Shows the most recent servers you have joined in the past 3 days.
π¨ Appearance Tab
Disable Trailer Autoplay: Prevents trailers from autoplaying on Roblox game pages.
Remove All Roblox Ads: Blocks most ads on the Roblox site.
Quick Nav: Ability to add quick navigations to the leftside panel of the Roblox page.
π Advanced Tab
Enable Console Logs: Enables console.log messages from the script.
Enable Server Filters: Enables server filter features on the game page.
Enable Server Hop Button: Enables server hop feature on the game page.
Enable Notifications: Enables helpful notifications from the script.
Set default location: Enables the user to set a default location for Roblox server regions. Turn this on if the script cannot automatically detect your location.
β¨ Extra Tab
Game Quality Filter: Removes games from the charts/discover page based on your settings.
Mutual Friends: Displays friends you share with a certain person on their profile page.
Disable Chat: Disables the chat feature on the roblox website.
Quick Launch Games: Adds the ability to quickly launch your favorite games from the homepage.
Show Old Greeting: Shows the old greeting Roblox had on their home page.
Better Friends: Improves the look of the friends section on the homepage.
Need more help?
You can visit the
troubleshooting
or create an issue on
greasyfork
for more assistance.
`;
}
// General tab (default)
return `
SmartSearch
New
Just Released/Updated
Experimental
Still being tested
?
Auto Server Regions
?
Fast Server Search
Experimental
Still being tested
?
Invert Player Count
?
Recent Servers
?
`;
}
/*******************************************************
name of function: openSettingsMenu
description: opens setting menu and makes it look good
*******************************************************/
function openSettingsMenu() {
if (document.getElementById("userscript-settings-menu")) return;
// storage make go uyea
initializeLocalStorage();
initializeCoordinatesStorage();
const overlay = document.createElement("div");
overlay.id = "userscript-settings-menu";
overlay.innerHTML = `
β
Home
${getSettingsContent("home")}
`;
document.body.appendChild(overlay);
// put css in
const style = document.createElement("style");
style.textContent = `
.grayish-center {
color: white;
font-weight: bold;
text-align: center;
position: relative;
display: inline-block;
font-size: 18px !important; /* idk whats overriding this but screw finding that */
}
.grayish-center::after {
content: "";
display: block;
margin: 4px auto 0;
width: 50%;
border-bottom: 2px solid #888888;
opacity: 0.6;
border-radius: 2px;
}
li a.about-link {
position: relative !important;
font-weight: bold !important;
color: #dc2626 !important;
text-decoration: none !important;
cursor: pointer !important;
transition: color 0.2s ease !important;
}
li a.about-link::after {
content: '' !important;
position: absolute !important;
left: 0 !important;
bottom: -2px !important;
height: 2px !important;
width: 100% !important;
background-color: #dc2626 !important;
transform: scaleX(0) !important;
transform-origin: left !important;
transition: transform 0.3s ease !important;
}
li a.about-link:hover {
color: #b91c1c !important;
}
li a.about-link:hover::after {
transform: scaleX(1) !important;
}
.about-section ul li a {
position: relative;
font-weight: bold;
color: #dc2626; /* red-600 */
text-decoration: none;
cursor: pointer;
transition: color 0.2s ease;
}
.about-section ul li a::after {
content: '';
position: absolute;
left: 0;
bottom: -2px;
height: 2px;
width: 100%;
background-color: #dc2626; /* red underline */
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s ease;
}
.about-section ul li a:hover {
color: #b91c1c; /* darker red on hover */
}
.about-section ul li a:hover::after {
transform: scaleX(1);
}
.license-note {
font-size: 0.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, ');
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 = `
Quick Navigation
Configure up to 9 custom navigation shortcuts
`;
// Build inputs for 9 links in a 3x3 grid
const inputsGrid = `
`;
// Build footer with buttons
const footer = `
Cancel
Save Changes
`;
// Combine all sections
popup.innerHTML = header + inputsGrid + footer;
// Add elements to DOM
document.body.appendChild(overlay);
document.body.appendChild(popup);
// POPUP EVENTS
// Add input hover and focus effects
popup.querySelectorAll('input').forEach(input => {
input.addEventListener('focus', () => {
input.style.background = 'rgba(255,255,255,0.1)';
input.style.boxShadow = '0 0 0 2px rgba(76, 175, 80, 0.4)';
});
input.addEventListener('blur', () => {
input.style.background = 'rgba(255,255,255,0.05)';
input.style.boxShadow = 'none';
});
input.addEventListener('mouseover', () => {
if (document.activeElement !== input) {
input.style.background = 'rgba(255,255,255,0.08)';
}
});
input.addEventListener('mouseout', () => {
if (document.activeElement !== input) {
input.style.background = 'rgba(255,255,255,0.05)';
}
});
});
// Add button hover effects
const saveBtn = popup.querySelector('#save-quicknav');
saveBtn.addEventListener('mouseover', () => {
saveBtn.style.background = 'linear-gradient(90deg, #66BB6A, #4CAF50)';
saveBtn.style.boxShadow = '0 4px 15px rgba(76, 175, 80, 0.4)';
saveBtn.style.transform = 'translateY(-1px)';
});
saveBtn.addEventListener('mouseout', () => {
saveBtn.style.background = 'linear-gradient(90deg, #4CAF50, #388E3C)';
saveBtn.style.boxShadow = '0 4px 12px rgba(76, 175, 80, 0.3)';
saveBtn.style.transform = 'translateY(0)';
});
const cancelBtn = popup.querySelector('#cancel-quicknav');
cancelBtn.addEventListener('mouseover', () => {
cancelBtn.style.background = 'rgba(255,255,255,0.05)';
});
cancelBtn.addEventListener('mouseout', () => {
cancelBtn.style.background = 'transparent';
});
// Animate in
setTimeout(() => {
overlay.style.opacity = "1";
popup.style.opacity = "1";
popup.style.transform = "translate(-50%, -50%) scale(1)";
}, 10);
// POPUP CLOSE FUNCTION
function closePopup() {
overlay.style.opacity = "0";
popup.style.opacity = "0";
popup.style.transform = "translate(-50%, -50%) scale(0.95)";
setTimeout(() => {
overlay.remove();
popup.remove();
}, 300);
}
// Save on click
popup.querySelector("#save-quicknav").addEventListener("click", () => {
const quickNavSettings = [];
for (let i = 0; i < 9; i++) {
const name = document.getElementById(`quicknav-name-${i}`).value.trim();
const link = document.getElementById(`quicknav-link-${i}`).value.trim();
if (name && link) {
quickNavSettings.push({
name,
link
});
}
}
localStorage.setItem("ROLOCATE_quicknav_settings", JSON.stringify(quickNavSettings));
closePopup();
});
// Cancel button
popup.querySelector("#cancel-quicknav").addEventListener("click", closePopup);
// Close when clicking overlay
overlay.addEventListener("click", (e) => {
if (e.target === overlay) {
closePopup();
}
});
// Close with ESC key
document.addEventListener("keydown", function escClose(e) {
if (e.key === "Escape") {
closePopup();
document.removeEventListener("keydown", escClose);
}
});
// AUTO-INIT AND KEYBOARD SHORTCUT
// Set up keyboard shortcut (Alt+Q)
document.addEventListener("keydown", function keyboardShortcut(e) {
if (e.altKey && e.key === "q") {
showQuickNavPopup();
}
});
}
/*******************************************************
name of function: applyStoredSettings
description: makes sure local storage is stored in correctly
*******************************************************/
function applyStoredSettings() {
// Handle all checkboxes
document.querySelectorAll("input[type='checkbox']").forEach(checkbox => {
const storageKey = `ROLOCATE_${checkbox.id}`;
const savedValue = localStorage.getItem(storageKey);
checkbox.checked = savedValue === "true";
checkbox.addEventListener("change", () => {
localStorage.setItem(storageKey, checkbox.checked);
});
});
// Handle dropdown for prioritylocation-select
const prioritySelect = document.getElementById("prioritylocation-select");
if (prioritySelect) {
const storageKey = "ROLOCATE_prioritylocation";
const savedValue = localStorage.getItem(storageKey) || "automatic";
prioritySelect.value = savedValue;
// Show/hide coordinates inputs based on selected value
const manualCoordinates = document.getElementById("manual-coordinates");
if (manualCoordinates) {
manualCoordinates.style.display = savedValue === "manual" ? "block" : "none";
// Set input values from stored coordinates if available
if (savedValue === "manual") {
try {
const savedCoords = JSON.parse(GM_getValue("ROLOCATE_coordinates", '{"lat":"","lng":""}'));
document.getElementById("latitude").value = savedCoords.lat || "";
document.getElementById("longitude").value = savedCoords.lng || "";
// If manual mode but no coordinates saved, revert to automatic
if (!savedCoords.lat || !savedCoords.lng) {
prioritySelect.value = "automatic";
localStorage.setItem(storageKey, "automatic");
manualCoordinates.style.display = "none";
}
} catch (e) {
ConsoleLogEnabled("Error loading saved coordinates:", e);
}
}
}
prioritySelect.addEventListener("change", () => {
const newValue = prioritySelect.value;
localStorage.setItem(storageKey, newValue);
// Show/hide coordinates inputs based on new value
if (manualCoordinates) {
manualCoordinates.style.display = newValue === "manual" ? "block" : "none";
// When switching to manual mode, load any saved coordinates
if (newValue === "manual") {
try {
const savedCoords = JSON.parse(GM_getValue("ROLOCATE_coordinates", '{"lat":"","lng":""}'));
document.getElementById("latitude").value = savedCoords.lat || "";
document.getElementById("longitude").value = savedCoords.lng || "";
// If no coordinates exist, keep the inputs empty
} catch (e) {
ConsoleLogEnabled("Error loading saved coordinates:", e);
}
}
}
});
}
// Button click handlers
const editQuickNavBtn = document.getElementById("edit-quicknav-btn");
if (editQuickNavBtn) {
editQuickNavBtn.addEventListener("click", () => {
showQuickNavPopup();
});
}
const editQualityGameBtn = document.getElementById("edit-gamequality-btn");
if (editQualityGameBtn) {
editQualityGameBtn.addEventListener("click", () => {
openGameQualitySettings();
});
}
const fastServersToggle = document.getElementById("fastservers");
if (fastServersToggle) {
fastServersToggle.addEventListener("change", () => {
if (fastServersToggle.checked) {
notifications('Fast Server Search: 100x faster on Violentmonkey, ~2x on Tampermonkey. Replaces thumbnails with builderman to bypass rate limits.', 'info', 'π§ͺ', 2000);
}
});
}
const AutoRunServerRegions = document.getElementById("AutoRunServerRegions");
if (AutoRunServerRegions) {
AutoRunServerRegions.addEventListener("change", () => {
if (AutoRunServerRegions.checked) {
notifications('Auto Server Regions works best when paired with Fast Server Search in Advanced Settings.', 'info', 'π§ͺ', 2000);
}
});
}
// Save coordinates button handler
const saveCoordinatesBtn = document.getElementById("save-coordinates");
if (saveCoordinatesBtn) {
saveCoordinatesBtn.addEventListener("click", () => {
const latInput = document.getElementById("latitude");
const lngInput = document.getElementById("longitude");
const lat = latInput.value.trim();
const lng = lngInput.value.trim();
// If manual mode but no coordinates provided, revert to automatic
if (!lat || !lng) {
const prioritySelect = document.getElementById("prioritylocation-select");
if (prioritySelect) {
prioritySelect.value = "automatic";
localStorage.setItem("ROLOCATE_prioritylocation", "automatic");
document.getElementById("manual-coordinates").style.display = "none";
// show feedback to user even if they dont see it
saveCoordinatesBtn.textContent = "Reverted to Automatic!";
saveCoordinatesBtn.style.background = "#4CAF50";
setTimeout(() => {
saveCoordinatesBtn.textContent = "Save Coordinates";
saveCoordinatesBtn.style.background = "background: #4CAF50;";
}, 2000);
}
return;
}
// Validate coordinates
const latNum = parseFloat(lat);
const lngNum = parseFloat(lng);
if (isNaN(latNum) || isNaN(lngNum) || latNum < -90 || latNum > 90 || lngNum < -180 || lngNum > 180) {
notifications('Invalid coordinates! Latitude must be between -90 and 90, and longitude between -180 and 180.', 'error', 'β οΈ', '8000');
return;
}
// Save valid coordinates
const coordinates = {
lat,
lng
};
GM_setValue("ROLOCATE_coordinates", JSON.stringify(coordinates)); // 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 = `
Settings
`;
const logo = li.querySelector('#custom-logo');
const tooltip = li.querySelector('#custom-tooltip');
logo.addEventListener('click', () => openSettingsMenu());
logo.addEventListener('mouseover', () => {
logo.style.width = '30px';
logo.style.border = '2px solid white';
tooltip.style.visibility = 'visible';
tooltip.style.opacity = '1';
});
logo.addEventListener('mouseout', () => {
logo.style.width = '26px';
logo.style.border = 'none';
tooltip.style.visibility = 'hidden';
tooltip.style.opacity = '0';
});
navbarGroup.appendChild(li);
}
/*******************************************************
name of function: removeAds
description: remove roblox ads 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 = ` `;
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 = ` 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 = `
Finding mutual friends...
`;
};
// 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 = `All Mutual Friends (${mutualFriends.length}) Γ `;
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 ? ` ` : 'π€';
friendItem.innerHTML = `${avatarContent}
${friend.name} `;
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 = 'No mutual friends found. RoLocate by Oqarshi
';
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 = 'Failed to load friend data
';
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 = 'Loading games...
';
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 = 'No results found
';
return;
}
// Render cards with play button
contentArea.innerHTML = games.map(game => `
`).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 = `
`;
}
});
} catch (error) {
ConsoleLogEnabled('Error fetching game thumbnails:', error);
}
}
} else {
contentArea.innerHTML = 'Error loading results
';
}
} catch (error) {
ConsoleLogEnabled('Error in game search:', error);
contentArea.innerHTML = 'Error loading results
';
}
}
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 = 'Loading users...
';
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 = 'No users found
';
return;
}
// Render user cards
contentArea.innerHTML = users.map(user => `
${user.displayName || user.username}
@${user.username}
${user.isFriend ? 'Friend ' : ''}
`).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 = `
`;
}
});
} catch (error) {
ConsoleLogEnabled('Error fetching user thumbnails:', error);
}
}
} else {
contentArea.innerHTML = 'Error loading user results
';
}
} catch (error) {
ConsoleLogEnabled('Error in user search:', error);
contentArea.innerHTML = 'Error loading user results
';
}
}
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 = 'Loading groups...
';
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 = 'No groups found
';
return;
}
// Render cards immediately with loading state
contentArea.innerHTML = groups.map(group => `
${group.name}
Members: ${formatNumberCount(group.memberCount)}
Created: ${formatDate(group.created)}
`).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 = `
`;
}
});
} catch (error) {
ConsoleLogEnabled('Error fetching group thumbnails:', error);
}
}
} else {
contentArea.innerHTML = 'Error loading group results
';
}
} catch (error) {
ConsoleLogEnabled('Error in group search:', error);
contentArea.innerHTML = 'Error loading group results
';
}
}
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 = 'Quickly search for games above!
';
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 = `
Quickly search for games above!
`;
} else if (button.textContent === "Users") {
contentArea.innerHTML = `
Instantly find the user you're looking for!
`;
} else if (button.textContent === "Groups") {
contentArea.innerHTML = `
Search for groups rapidly.
`;
}
}
});
});
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 = 'Quickly search for games above!
';
} else if (activeTab === "Users") {
contentArea.innerHTML = 'Instantly find the user you\'re looking for!
';
} else if (activeTab === "Groups") {
contentArea.innerHTML = 'Search for groups rapidly.
';
}
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 = `
Quickly search for games above!
`;
} else if (btn.textContent === "Users") {
contentArea.innerHTML = `
Instantly find the user you're looking for!
`;
} else if (btn.textContent === "Groups") {
contentArea.innerHTML = `
Search for groups rapidly.
`;
}
} 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 = `
`;
// 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 = `
`;
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 = `
Loading...
`;
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 = ` `;
// 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 = `π ${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 > 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]));
};
// ======= INSERT NEW CODE START =======
// 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 = `Pick Your Best Friends `;
// 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 = `
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 = 'No friends match your search
';
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 = 'Failed to load friends
';
}
// 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: 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
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 => {
if (header.textContent.trim() === 'Servers My Friends Are In') {
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 = `
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 = `
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 = `
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 = `
`;
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 = `Region: `;
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 = `
`;
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 = `
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 = `
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 = `
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 = `
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 = `
`;
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 = `
`;
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 || `
`;
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, `
`));
infoItems.appendChild(createInfoItem('Server ID', serverId, `
`));
infoItems.appendChild(createInfoItem('Region', region, `
`));
const formattedLastPlayed = formatLastPlayedWithRelative(lastPlayed);
infoItems.appendChild(createInfoItem('Last Played', formattedLastPlayed, `
`));
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 = `
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 = `
Copied!
`;
setTimeout(() => {
copyButton.innerHTML = `
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, "$1 ");
const buttonText = document.createElement('p');
buttonText.style.cssText = `
margin: 0;
color: white;
font-size: 16px;
`;
buttonText.textContent = data.name;
// Add "DISABLED" style if the button is disabled
if (data.disabled) {
// Show explanation tooltip (left side like experimental)
const disabledTooltip = document.createElement('div');
disabledTooltip.className = 'disabled-tooltip';
disabledTooltip.style.cssText = `
display: none;
position: absolute;
top: 0;
right: 200px;
width: 200px;
background-color: #191B1D;
color: white;
padding: 5px;
border-radius: 5px;
font-size: 14px;
white-space: pre-wrap;
z-index: 1001;
opacity: 1;
`;
disabledTooltip.innerHTML = data.disabledExplanation.replace(/\*\*(.*?)\*\*/g, '$1 ');
buttonContainer.appendChild(disabledTooltip);
// Add disabled indicator
const disabledIndicator = document.createElement('span');
disabledIndicator.textContent = 'DISABLED';
disabledIndicator.style.cssText = `
margin-left: 8px;
color: #ff5555;
font-size: 10px;
font-weight: bold;
background-color: rgba(255, 85, 85, 0.1);
padding: 1px 4px;
border-radius: 3px;
`;
buttonText.appendChild(disabledIndicator);
// Show on hover
buttonContainer.addEventListener('mouseenter', () => {
disabledTooltip.style.display = 'block';
});
buttonContainer.addEventListener('mouseleave', () => {
disabledTooltip.style.display = 'none';
});
}
// Add "EXP" label if the button is experimental
if (data.experimental) {
const expLabel = document.createElement('span');
expLabel.textContent = 'EXP';
expLabel.style.cssText = `
margin-left: 8px;
color: gold;
font-size: 12px;
font-weight: bold;
background-color: rgba(255, 215, 0, 0.1);
padding: 2px 6px;
border-radius: 3px;
`;
buttonText.appendChild(expLabel);
}
// Add "POPULAR" label if the button is popular
if (data.popular) {
const popularLabel = document.createElement('span');
popularLabel.textContent = 'Popular';
popularLabel.style.cssText = `
margin-left: 8px;
color: #4CAF50;
font-size: 10px;
font-weight: bold;
background-color: rgba(76, 175, 80, 0.1);
padding: 2px 6px;
border-radius: 3px;
`;
buttonText.appendChild(popularLabel);
}
// add new tooltip
let newTooltip = null;
if (data.new) {
const newLabel = document.createElement('span');
newLabel.textContent = 'NEW';
newLabel.style.cssText = `
margin-left: 8px;
color: #2196F3;
font-size: 12px;
font-weight: bold;
background-color: rgba(33, 150, 243, 0.1);
padding: 2px 6px;
border-radius: 3px;
`;
buttonText.appendChild(newLabel);
// Add NEW explanation tooltip (left side)
const newTooltip = document.createElement('div');
newTooltip.className = 'new-tooltip';
newTooltip.style.cssText = `
display: none;
position: absolute;
top: 0;
right: 200px;
width: 200px;
background-color: #191B1D;
color: white;
padding: 5px;
border-radius: 5px;
font-size: 14px;
white-space: pre-wrap;
z-index: 1001;
opacity: 1;
`;
newTooltip.innerHTML = "New Feature : This feature was recently added. There is no guarantee it will work.";
buttonContainer.appendChild(newTooltip);
// Show on hover
buttonContainer.addEventListener('mouseenter', () => {
newTooltip.style.display = 'block';
});
buttonContainer.addEventListener('mouseleave', () => {
newTooltip.style.display = 'none';
});
}
// Add experimental explanation tooltip (left side)
let experimentalTooltip = null;
if (data.experimental) {
experimentalTooltip = document.createElement('div');
experimentalTooltip.className = 'experimental-tooltip';
experimentalTooltip.style.cssText = `
display: none;
position: absolute;
top: 0;
right: 200px;
width: 200px;
background-color: #191B1D;
color: white;
padding: 5px;
border-radius: 5px;
font-size: 14px;
white-space: pre-wrap;
z-index: 1001;
opacity: 1;
`;
// Function to replace **text** with bold and gold styled text
const formatText = (text) => {
return text.replace(/\*\*(.*?)\*\*/g, '$1 ');
};
// Apply the formatting to the experimental explanation
experimentalTooltip.innerHTML = formatText(data.experimentalExplanation);
buttonContainer.appendChild(experimentalTooltip);
}
// Append tooltip directly to button container so it won't inherit opacity
buttonContainer.appendChild(tooltip);
// Append button text to content wrapper
buttonContentWrapper.appendChild(buttonText);
// Append content wrapper to button container
buttonContainer.appendChild(buttonContentWrapper);
// In the event listeners:
buttonContainer.addEventListener('mouseover', () => {
tooltip.style.display = 'block';
if (data.experimental && experimentalTooltip) {
experimentalTooltip.style.display = 'block';
}
if (data.new && newTooltip) { // <-- Only show if it exists
newTooltip.style.display = 'block';
}
if (!data.disabled) {
buttonContainer.style.backgroundColor = '#4A4C4E';
buttonContainer.style.transform = 'translateY(0px) scale(1.02)';
}
});
buttonContainer.addEventListener('mouseout', () => {
tooltip.style.display = 'none';
if (data.experimental && experimentalTooltip) {
experimentalTooltip.style.display = 'none';
}
if (data.new && newTooltip) { // <-- Only hide if it exists
newTooltip.style.display = 'none';
}
if (!data.disabled) {
buttonContainer.style.backgroundColor = '#393B3D';
buttonContainer.style.transform = 'translateY(0px) scale(1)';
}
});
buttonContainer.addEventListener('click', () => {
// Prevent click functionality for disabled buttons
if (data.disabled) {
return;
}
// Add click animation
buttonContainer.style.transform = 'translateY(0px) scale(0.95)';
setTimeout(() => {
buttonContainer.style.transform = 'translateY(0px) scale(1)';
}, 150);
switch (index) {
case 0:
smallest_servers();
break;
case 1:
available_space_servers();
break;
case 2:
player_count_tab();
break;
case 3:
random_servers();
break;
case 4:
createServerCountPopup((totalLimit) => {
rebuildServerList(gameId, totalLimit);
});
break;
case 5:
rebuildServerList(gameId, 50, true);
break;
case 6:
auto_join_small_server();
break;
case 7:
scanRobloxServers();
break;
}
});
popup.appendChild(buttonContainer);
});
// trigger the button animations after DOM insertion
// this should be called after the popup is added to the DOM
setTimeout(() => {
// animate buttons in sequence from top to bottom
const buttons = popup.querySelectorAll('.server-filter-option');
buttons.forEach((button, index) => {
setTimeout(() => {
button.style.transform = 'translateY(0px)';
button.style.opacity = '1';
}, index * 30); // 30 ms from each button
});
}, 20);
return popup;
}
/*******************************************************
name of function: ServerHop
description: Handles server hopping by fetching and joining a random server, excluding recently joined servers.
*******************************************************/
function ServerHop() {
ConsoleLogEnabled("Starting server hop...");
showLoadingOverlay();
// Extract the game ID from the URL
const url = window.location.href;
const gameId = (url.split("/").indexOf("games") !== -1) ? url.split("/")[url.split("/").indexOf("games") + 1] : null;
ConsoleLogEnabled(`Game ID: ${gameId}`);
// Array to store server IDs
let serverIds = [];
let nextPageCursor = null;
let pagesRequested = 0;
// Get the list of all recently joined servers in localStorage
const allStoredServers = Object.keys(localStorage)
.filter(key => key.startsWith("ROLOCATE_recentServers_")) // 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);
const serverList = document.querySelector('#rbx-public-game-server-item-container');
serverList.innerHTML = '';
const gameId = ((p = window.location.pathname.split('/')) => {
const i = p.indexOf('games');
return i !== -1 && p.length > i + 1 ? p[i + 1] : null;
})();
let cursor = null;
let serversFound = 0;
let serverMaxPlayers = null;
let isCloserToOne = null;
let topDownServers = []; // Servers collected during top-down search
let bottomUpServers = []; // Servers collected during bottom-up search
let currentDelay = 500; // Initial delay of 0.5 seconds
const timeLimit = 3 * 60 * 1000; // 3 minutes in milliseconds
const startTime = Date.now(); // Record the start time
notifications('Will search for a maximum of 3 minutes to find a server.', 'success', 'π', '5000');
try {
while (serversFound < 16) {
// Check if the time limit has been exceeded
if (Date.now() - startTime > timeLimit) {
ConsoleLogEnabled('[DEBUG] Time limit reached. Proceeding to fallback servers.');
notifications('Warning: Time limit reached. Proceeding to fallback servers.', 'warning', 'β', '5000');
break;
}
// Fetch initial data to determine serverMaxPlayers and isCloserToOne
if (!serverMaxPlayers) {
const initialUrl = cursor ?
`https://games.roblox.com/v1/games/${gameId}/servers/public?excludeFullGames=true&limit=100&cursor=${cursor}` :
`https://games.roblox.com/v1/games/${gameId}/servers/public?excludeFullGames=true&limit=100`;
const initialData = await fetchServersWithRetry(initialUrl);
if (initialData.data.length > 0) {
serverMaxPlayers = initialData.data[0].maxPlayers;
isCloserToOne = maxPlayers <= (serverMaxPlayers / 2);
} else {
notifications("No servers found in initial fetch.", "error", "β οΈ", "5000");
ConsoleLogEnabled('[DEBUG] No servers found in initial fetch.', 'warning', 'β');
break;
}
}
// Validate maxPlayers against serverMaxPlayers
if (maxPlayers >= serverMaxPlayers) {
ConsoleLogEnabled('[DEBUG] Invalid input: maxPlayers is greater than or equal to serverMaxPlayers.');
notifications(`Error: Please input a number between 1 through ${serverMaxPlayers - 1}`, 'error', 'β οΈ', '5000');
return;
}
// Adjust the URL based on isCloserToOne
const baseUrl = isCloserToOne ?
`https://games.roblox.com/v1/games/${gameId}/servers/public?sortOrder=1&excludeFullGames=true&limit=100` :
`https://games.roblox.com/v1/games/${gameId}/servers/public?excludeFullGames=true&limit=100`; // why does this work lmao
const url = cursor ? `${baseUrl}&cursor=${cursor}` : baseUrl;
const data = await fetchServersWithRetry(url);
// Safety check: Ensure the server list is valid and iterable
if (!Array.isArray(data.data)) {
ConsoleLogEnabled('[DEBUG] Invalid server list received. Waiting 1 second before retrying...');
await delay(1000); // Wait 1 second before retrying
continue; // Skip the rest of the loop and retry
}
// Filter and process servers
for (const server of data.data) {
if (server.playing === maxPlayers) {
await rbx_card(server.id, server.playerTokens, server.maxPlayers, server.playing, gameId);
serversFound++;
if (serversFound >= 16) {
break;
}
} else if (!isCloserToOne && server.playing > maxPlayers) {
topDownServers.push(server); // Add to top-down fallback list
} else if (isCloserToOne && server.playing < maxPlayers) {
bottomUpServers.push(server); // Add to bottom-up fallback list
}
}
// Exit if no more servers are available
if (!data.nextPageCursor) {
break;
}
cursor = data.nextPageCursor;
// Adjust delay dynamically
if (currentDelay > 150) {
currentDelay = Math.max(150, currentDelay / 2); // Gradually reduce delay
}
ConsoleLogEnabled(`[DEBUG] Waiting ${currentDelay / 1000} seconds before next request...`);
await delay(currentDelay);
}
// If no exact matches were found or time limit reached, use fallback servers
if (serversFound === 0 && (topDownServers.length > 0 || bottomUpServers.length > 0)) {
notifications(`There are no servers with ${maxPlayers} players. Showing servers closest to ${maxPlayers} players.`, 'warning', 'π', '8000');
// Sort top-down servers by player count (ascending)
topDownServers.sort((a, b) => a.playing - b.playing);
// Sort bottom-up servers by player count (descending)
bottomUpServers.sort((a, b) => b.playing - a.playing);
// Combine both fallback lists (prioritize top-down servers first)
const combinedFallback = [...topDownServers, ...bottomUpServers];
for (const server of combinedFallback) {
await rbx_card(server.id, server.playerTokens, server.maxPlayers, server.playing, gameId);
serversFound++;
if (serversFound >= 16) {
break;
}
}
}
if (serversFound <= 0) {
notifications('No Servers Found Within The Provided Criteria', 'info', 'π', '5000');
}
} catch (error) {
ConsoleLogEnabled('[DEBUG] Error in filterServersByPlayerCount:', error);
} finally {
Loadingbar(false);
disableFilterButton(false);
}
}
/*********************************************************************************************************************************************************************************************************************************************
Functions for the 4th button
*********************************************************************************************************************************************************************************************************************************************/
/*******************************************************
name of function: random_servers
description: Fetches servers from two different URLs, combines the results, ensures no duplicates, shuffles the list, and passes the server information to the rbx_card function in a random order. Handles 429 errors with retries.
*******************************************************/
async function random_servers() {
notifications('Finding Random Servers. Please wait 2-5 seconds', 'success', 'π', '5000');
// Disable the "Load More" button and show the loading bar
Loadingbar(true);
disableFilterButton(true);
disableLoadMoreButton();
// Get the game ID from the URL ik reduent function
const gameId = ((p = window.location.pathname.split('/')) => {
const i = p.indexOf('games');
return i !== -1 && p.length > i + 1 ? p[i + 1] : null;
})();
try {
// Fetch servers from the first URL with retry logic
const firstUrl = `https://games.roblox.com/v1/games/${gameId}/servers/public?excludeFullGames=true&limit=10`;
const firstData = await fetchWithRetry(firstUrl, 10); // Retry up to 3 times
// Wait for 5 seconds
await delay(1500);
// Fetch servers from the second URL with retry logic
const secondUrl = `https://games.roblox.com/v1/games/${gameId}/servers/public?sortOrder=1&excludeFullGames=true&limit=10`;
const secondData = await fetchWithRetry(secondUrl, 10); // Retry up to 3 times
// Combine the servers from both URLs. Yea im kinda proud of this lmao
const combinedServers = [...firstData.data, ...secondData.data];
// Remove duplicates by server ID
const uniqueServers = [];
const seenServerIds = new Set();
for (const server of combinedServers) {
if (!seenServerIds.has(server.id)) {
seenServerIds.add(server.id);
uniqueServers.push(server);
}
}
// Shuffle the unique servers array
const shuffledServers = shuffleArray(uniqueServers);
// Get the first 16 shuffled servers
const selectedServers = shuffledServers.slice(0, 16);
// Process each server in random order
for (const server of selectedServers) {
const {
id: serverId,
playerTokens,
maxPlayers,
playing
} = server;
// Pass the server data to the card creation function
await rbx_card(serverId, playerTokens, maxPlayers, playing, gameId);
}
} catch (error) {
ConsoleLogEnabled('Error fetching server data:', error);
notifications('Error: Failed to fetch server data. Please try again later.', 'error', 'β οΈ', '5000');
} finally {
// Hide the loading bar and enable the filter button
Loadingbar(false);
disableFilterButton(false);
}
}
/*******************************************************
name of function: fetchWithRetry
description: Fetches data from a URL with retry logic for 429 errors using GM_xmlhttpRequest.
*******************************************************/
function fetchWithRetry(url, retries) {
return new Promise((resolve, reject) => {
const attemptFetch = (attempt = 0) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: function(response) {
if (response.status === 429) {
if (attempt < retries) {
ConsoleLogEnabled(`Rate limited. Retrying in 2.5 seconds... (Attempt ${attempt + 1}/${retries})`);
setTimeout(() => attemptFetch(attempt + 1), 1500); // Wait 1.5 seconds and retry
} else {
reject(new Error('Rate limit exceeded after retries'));
}
} else if (response.status >= 200 && response.status < 300) {
try {
const data = JSON.parse(response.responseText);
resolve(data);
} catch (error) {
reject(new Error('Failed to parse JSON response'));
}
} else {
reject(new Error(`HTTP error: ${response.status}`));
}
},
onerror: function(error) {
if (attempt < retries) {
ConsoleLogEnabled(`Error occurred. Retrying in 10 seconds... (Attempt ${attempt + 1}/${retries})`);
setTimeout(() => attemptFetch(attempt + 1), 10000); // Wait 10 seconds and retry
} else {
reject(error);
}
}
});
};
attemptFetch();
});
}
/*******************************************************
name of function: shuffleArray
description: Shuffles an array using the Fisher-Yates algorithm. This ronald fisher guy was kinda smart
*******************************************************/
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1)); // random index from 0 to i
[array[i], array[j]] = [array[j], array[i]]; // swap elements
}
return array;
}
/*********************************************************************************************************************************************************************************************************************************************
Functions for the 5th button. taken from my other project
*********************************************************************************************************************************************************************************************************************************************/
/*******************************************************
name of function: Isongamespage
description: not a function but if on game page inject styles
*******************************************************/
if (Isongamespage) {
// Create a