// ==UserScript== // @name RoLocate // @namespace https://oqarshi.github.io/ // @version 45.5 // @description Adds filter options to roblox server page. Alternative to paid extensions like RoPro, RoGoldยฎ, 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/1586769/Rolocate%20Base64%20Image%20Library%2020.js // @require https://update.greasyfork.icu/scripts/547134/1754967/Rolocate%20Server%20Region%20Data%20%28Data%20Saving%29.js // @require https://update.greasyfork.icu/scripts/540553/1648593/Rolocate%20Flag%20Base64%20Data.js // @require https://update.greasyfork.icu/scripts/544437/1642116/Rolocate%20Restore%20Classic%20Terms%20All%20Languages.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 // @connect users.roblox.com // @connect catalog.roblox.com // @downloadURL none // ==/UserScript== /** * -- RoLocate Userscript -------------------------------- * Author: Oqarshi * License: Custom - Personal Use Only * Copyright (c) 2026 Oqarshi * * This license grants limited rights to end users and does not imply * any transfer of copyright ownership. * * You MAY: * * Use and modify this script for personal, non-commercial use only. * * You MAY NOT: * * Redistribute or reupload this script (original or modified) * * Publish it on any website (GreasyFork, GitHub, UserScripts.org, etc.) * * Include it in 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 DMCA or applicable law. * * --- Dependencies -------------------------------------- * * Base64 Images & Icons: * https://update.greasyfork.icu/scripts/535590/1586769/Rolocate%20Base64%20Image%20Library%2020.js * * * Server Regions Data: * https://update.greasyfork.icu/scripts/547134/1652105/Rolocate%20Server%20Region%20Data%20%28Data%20Saving%29.js * * * Flag Icons (Base64): * https://update.greasyfork.icu/scripts/540553/1648593/Rolocate%20Flag%20Base64%20Data.js * * * Classic Terms Replacements: * https://update.greasyfork.icu/scripts/544437/1642116/Rolocate%20Restore%20Classic%20Terms%20All%20Languages.js * * ------------------------------------------------------- */ /*jshint esversion: 6 */ /*jshint esversion: 11 */ (function() { 'use strict'; //---------------XSS Attack Vectors Protection-------------------- // ik this should be fixed, but roblox engineers are not the brightest... // so extra protection i guess. // escape the hmtl. Used in consolog function, notifications function, etc. const escapeHtmlnoxssattackvectors = (text) => { const temp = document.createElement('div'); temp.textContent = text; return temp.innerHTML; }; // for numbers. currently used in mutualfriends function const sanitizeUserId = (id) => { const numId = parseInt(id, 10); return (!isNaN(numId) && numId > 0) ? numId : 0; // return 0 install of null yea }; // for attributes. currenltyy used in custombackgrounds function const sanitizeAttribute = (str) => { return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); }; // for hex value colors. currenltyy used in custombackgrounds function const sanitizeColor = (color) => { return /^#[0-9A-Fa-f]{6}$/.test(color) ? color : '#ffffff'; }; // for css values like rgb and rgba. currenltyy used in custombackgrounds function const sanitizeCssValue = (value) => { const rgbaPattern = /^rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(?:,\s*[\d.]+\s*)?\)$/; const hexPattern = /^#[0-9A-Fa-f]{3,6}$/; return (rgbaPattern.test(value) || hexPattern.test(value)) ? value : 'rgba(40,40,40,0.85)'; }; //---------------------------------------------------------- /******************************************************* name of function: ConsoleLogEnabled description: console.logs everything if settings is turned on *******************************************************/ const MAX_LOG_BYTES = 3 * 1024 * 1024; // 3 mb let rolocateLogSize = 0; function ConsoleLogEnabled(...args) { if (localStorage.getItem("ROLOCATE_enableLogs") !== "true") return; window.rolocateLogBuffer ??= []; const msg = args.map(a => typeof a === 'object' ? JSON.stringify(a) : a).join(' '); const entry = { time: new Date().toLocaleTimeString(), msg }; const size = JSON.stringify(entry).length; window.rolocateLogBuffer.push(entry); rolocateLogSize += size; while (rolocateLogSize > MAX_LOG_BYTES) { const removed = window.rolocateLogBuffer.shift(); rolocateLogSize -= JSON.stringify(removed).length; } console.log("[ROLOCATE]", ...args); } /******************************************************* name of function: isDarkMode description: tells if user is using dark mode on roblox *******************************************************/ function isDarkMode(bypass = false) { if (!bypass && localStorage.getItem("ROLOCATE_forcedarkmode") === "true") { // exception return true; } const bg = getComputedStyle(document.body).backgroundColor; const rgb = bg.match(/\d+/g).map(Number); const brightness = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000; // funny vision formula from google return brightness < 128; // true = dark, false = light } /******************************************************* name of function: getCurrentGameId description: uh gets gameid *******************************************************/ const getCurrentGameId = () => { const urlMatch = window.location.href.match(/games\/(\d+)/); const gameId = urlMatch ? urlMatch[1] : null; ConsoleLogEnabled(`getCurrentGameId: ${gameId}`); return sanitizeUserId(gameId); // protection }; /******************************************************* name of function: getCurrentUserId description: gets user's id from roblox page. *******************************************************/ function getCurrentUserId() { // 1st: try to grab the userId directly from Roblox's JS object const primaryUserId = sanitizeUserId(Roblox?.CurrentUser?.userId); // some extensions like roseal break this and set the userId to 0 // so if user id is not 0, return if it is then try another method if (primaryUserId && primaryUserId !== 0) { return primaryUserId; } // 2nd: check in dom for user id instead const Userid2ndmethodcauserosealbreakyeayeay = document.querySelector('meta[name="user-data"]'); if (Userid2ndmethodcauserosealbreakyeayeay) { // get it then yea const fallbackUserId = parseInt( Userid2ndmethodcauserosealbreakyeayeay.getAttribute('data-userid'), 10 ); // xtikhgiasgd protedtion what ever if (fallbackUserId > 0) { return sanitizeUserId(fallbackUserId); } } // lowkey just give up, im too lazy to find an api that exposes the userid return 0; } /******************************************************* name of function: notifications description: notifications function (XSS-safe) *******************************************************/ function notifications(message, type = 'info', emoji = '', duration = 3000) { if (localStorage.getItem('ROLOCATE_enablenotifications') !== 'true') return; 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: 999999999999999999; 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; white-space: pre-wrap; } .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); } let container = document.getElementById('toast-container'); if (!container) { container = document.createElement('div'); container.id = 'toast-container'; document.body.appendChild(container); } const toast = document.createElement('div'); const validTypes = ['success', 'error', 'warning', 'info']; const safeType = validTypes.includes(type) ? type : 'info'; toast.className = `toast ${safeType}`; const icons = { success: '', error: '', warning: '', info: '' }; // escape the user input sduyhgads const safeMessage = escapeHtmlnoxssattackvectors(message); const safeEmoji = escapeHtmlnoxssattackvectors(emoji); toast.innerHTML = `
${icons[safeType]}
${emoji ? `${safeEmoji}` : ''} ${safeMessage.replace(/\n/g, '
')}
`; container.appendChild(toast); let timeout = setTimeout(removeToast, duration); const progressBar = toast.querySelector('.progress-bar'); 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); }); toast.querySelector('.toast-close').addEventListener('click', removeToast); function removeToast() { clearTimeout(timeout); toast.classList.add('removing'); setTimeout(() => toast.remove(), 300); } return { remove: removeToast, update: (newMessage) => { const escaped = escapeHtmlnoxssattackvectors(newMessage); toast.querySelector('.toast-message').innerHTML = escaped.replace(/\n/g, '
'); }, setType: (newType) => { const validType = validTypes.includes(newType) ? newType : 'info'; toast.className = `toast ${validType}`; toast.querySelector('.toast-icon').innerHTML = icons[validType]; }, setDuration: (newDuration) => { clearTimeout(timeout); const safeDuration = parseInt(newDuration); progressBar.style.animation = `shrink ${safeDuration}ms linear forwards`; timeout = setTimeout(removeToast, safeDuration); }, updateEmoji: (newEmoji) => { const emojiEl = toast.querySelector('.toast-emoji'); if (emojiEl) emojiEl.textContent = escapeHtmlnoxssattackvectors(newEmoji); } }; } /******************************************************* name of function: Update_Popup description: notifications for updates! *******************************************************/ function Update_Popup() { localStorage.removeItem('ROLOCATE_compactprivateservers'); // remove this cause its called better private servers now const VERSION = "V45.5"; const PREV_VERSION = "V44.5"; const CHANGELOG = { EasterEggs: { title: "Easter Eggs", icon: "๐Ÿฅš", description: "Hidden Easter Eggs have been added across the script. Have fun finding them!", badge: "New" }, SettingsUpgrade: { title: "Settings Overhaul", icon: "โš™๏ธ", description: "Added a Technical tab with storage info, live console, reset button, searchable settings, contributor list, and more preset options.", badge: "Updated" }, UIUpdates: { title: "UI & Appearance Updates", icon: "๐ŸŽจ", description: "Light Mode support added, responsive game cards introduced, SVG icons applied, and Smart Join popup UI slightly improved.", badge: "Updated" }, PerformanceBoost: { title: "Performance Improvements", icon: "๐Ÿš€", description: "Script size reduced by ~59%, faster Smart Join, and Fast Server Search is now the default.", badge: "Improved" }, ServerRegions: { title: "Server Region Accuracy", icon: "๐ŸŒ", description: "Moved from IP detection to datacenters for better accuracy and smaller script size. Fixed incorrect Dallas region flags.", badge: "Fixed" }, SmartSearch: { title: "Smart Search Enhancements", icon: "๐Ÿ”", description: "Smart Search now shows friend & follower counts, verified badges, and supports adding games directly to Quick Launch.", badge: "Updated" }, QuickLaunch: { title: "Quick Launch Improvements", icon: "๐Ÿ“Œ", description: "Games can now be reordered via drag & drop instead of arrow buttons.", badge: "Updated" }, PrivateServers: { title: "Better Private Servers", icon: "๐Ÿ”’", description: "Compact Private Servers renamed to Better Private Servers. Now shows only your private servers with more improvements planned.", badge: "Updated" }, SecurityAndFixes: { title: "Bug Fixes & Stability", icon: "๐Ÿ› ๏ธ", description: "Fixed Recent Servers issues, SmartSearch UI bugs, and added join confirmation when already in a game.", badge: "Fixed" }, AndMore: { title: "And More!", icon: "โœจ", description: "This update includes many more improvements, fixes, and changes. Visit the full changelog to see everything.", badge: "Info", link: "https://oqarshi.github.io/Invite/rolocate/changelog/" } }; const currentVersion = localStorage.getItem('version') || "V0.0"; if (currentVersion === VERSION) return; localStorage.setItem('version', VERSION); if (localStorage.getItem(PREV_VERSION)) localStorage.removeItem(PREV_VERSION); const style = document.createElement('style'); style.innerHTML = ` .rup-popup { display: flex; position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); justify-content: center; align-items: center; z-index: 1000; opacity: 0; animation: rup-fadeIn 0.5s cubic-bezier(0.22, 0.61, 0.36, 1) forwards; } .rup-content { background: #2a2a2a; border-radius: 20px; padding: 0; width: 650px; max-width: 95%; max-height: 85vh; overflow: hidden; box-shadow: 0 25px 50px rgba(0, 0, 0, 0.4); border: 1px solid #404040; color: #e8e8e8; transform: scale(0.95); animation: rup-scaleUp 0.6s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards; display: flex; flex-direction: column; } .rup-header { padding: 24px 32px; border-bottom: 1px solid #404040; display: flex; align-items: flex-start; gap: 16px; background: #1f1f1f; position: relative; } .rup-logo { width: 56px; height: 56px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); flex-shrink: 0; } .rup-header-content { flex: 1; } .rup-title { font-size: 24px; font-weight: 600; color: #ffffff; margin: 0 0 4px; letter-spacing: -0.5px; } .rup-version { display: inline-block; background: #1a1a1a; color: #ffffff; padding: 6px 12px; border-radius: 6px; font-size: 13px; font-weight: 500; border: 1px solid #404040; } .rup-refresh-btn { background: #3a3a3a; color: #e8e8e8; border: 1px solid #505050; padding: 8px 16px; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; gap: 8px; position: absolute; top: 16px; right: 16px; } .rup-refresh-btn:hover { background: #454545; transform: translateY(-1px); } .rup-refresh-btn:active { transform: translateY(0); } .rup-main { padding: 24px 32px; overflow-y: auto; background: #252525; flex: 1; } .rup-top-section { display: flex; gap: 12px; margin-bottom: 20px; } .rup-developer-message { background: #1a1a1a; border-radius: 8px; padding: 16px; border-left: 3px solid #555555; flex: 1; } .rup-developer-message-title { font-weight: 600; color: #ffffff; margin-bottom: 8px; font-size: 14px; } .rup-developer-message-text { font-size: 13px; color: #cccccc; line-height: 1.5; } .rup-help-section { background: #1a1a1a; border-radius: 8px; padding: 16px; border: 1px solid #404040; min-width: 200px; } .rup-help-title { font-size: 14px; font-weight: 600; color: #ffffff; margin-bottom: 12px; } .rup-help-link { color: #70a5ff; text-decoration: none; font-size: 13px; display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: 6px; transition: all 0.3s ease; background: rgba(112, 165, 255, 0.1); border: 1px solid rgba(112, 165, 255, 0.2); margin-bottom: 8px; } .rup-help-link:last-child { margin-bottom: 0; } .rup-help-link:hover { color: #ffffff; background: rgba(112, 165, 255, 0.2); border-color: rgba(112, 165, 255, 0.4); transform: translateX(2px); } .rup-help-link-icon { font-size: 16px; } .rup-features-title { font-size: 18px; font-weight: 600; color: #ffffff; margin-bottom: 16px; } .rup-feature-item { margin-bottom: 12px; border-radius: 10px; padding: 16px; background: #1f1f1f; border: 1px solid #404040; transition: all 0.3s ease; } .rup-feature-item:hover { border-color: #555555; background: #2a2a2a; transform: translateY(-2px); } .rup-feature-header { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; } .rup-feature-icon { font-size: 20px; min-width: 24px; } .rup-feature-title { flex: 1; font-size: 15px; font-weight: 500; color: #ffffff; margin: 0; } .rup-feature-badge { background: #404040; color: #cccccc; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; } .rup-feature-description { font-size: 14px; color: #aaaaaa; line-height: 1.5; margin: 0; } .rup-feature-link { display: inline-block; margin-top: 8px; color: #70a5ff; text-decoration: none; font-size: 13px; transition: all 0.3s ease; } .rup-feature-link:hover { color: #90b5ff; transform: translateX(2px); } .rup-footer { padding: 20px 32px; border-top: 1px solid #404040; background: #1f1f1f; display: flex; align-items: center; justify-content: center; } .rup-note { font-size: 12px; color: #999999; margin: 0; } .rup-main::-webkit-scrollbar { width: 6px; } .rup-main::-webkit-scrollbar-track { background: #1a1a1a; } .rup-main::-webkit-scrollbar-thumb { background: #555555; border-radius: 3px; } .rup-main::-webkit-scrollbar-thumb:hover { background: #666666; } @keyframes rup-fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes rup-fadeOut { from { opacity: 1; } to { opacity: 0; } } @keyframes rup-scaleUp { 0% { transform: scale(0.95) translateY(10px); } 100% { transform: scale(1) translateY(0); } } @keyframes rup-scaleDown { from { transform: scale(1); } to { transform: scale(0.9); opacity: 0; } } @media (max-width: 768px) { .rup-content { width: 95%; } .rup-footer { flex-direction: column; text-align: center; } .rup-top-section { flex-direction: column; } .rup-help-section { min-width: auto; } } `; document.head.appendChild(style); const featuresHTML = Object.entries(CHANGELOG).map(([key, feat]) => `
${feat.icon}
${feat.title}
${feat.badge}

${feat.description}

${feat.link ? `https://oqarshi.github.io/Invite/rolocate/changelog/` : ''}
`).join(''); const popupHTML = `

Rolocate Update

${VERSION}
From Oqarshi:
Please report any issues on GreasyFork if something breaks! Thank you! RoLocate is designed to be used with Roblox's dark mode or dark theme.
โœจ What's New in ${VERSION}
${featuresHTML}
`; const popupContainer = document.createElement('div'); popupContainer.innerHTML = popupHTML; document.body.appendChild(popupContainer); } 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 prioritylocation: "automatic", // automatic by default fastservers: true, // enabled by default invertplayercount: false, // disabled by default enablenotifications: true, // enabled by default disabletrailer: true, // enabled by default gamequalityfilter: false, // disabled by default mutualfriends: true, // enabled by default disablechat: false, // disabled by default smartsearch: true, // enabled by default quicklaunchgames: true, // enabled by default smartjoinpopup: true, // enabled by default betterfriends: true, // enabled by default restoreclassicterms: true, // enabled by default betterprivateservers: true, // enabled by default custombackgrounds: false, // disabled by default btrobloxfix: false, // disabled by default mobilemode: false, // disabled by default joinconfirmation: true, // enabled by default forcedarkmode: false, // disabled by default responsivegamecards: true // enabled by default }; const presetConfigurations = { default: { name: "Default", settings: { enableLogs: false, removeads: true, togglefilterserversbutton: true, toggleserverhopbutton: true, AutoRunServerRegions: false, ShowOldGreeting: true, togglerecentserverbutton: true, prioritylocation: "automatic", fastservers: true, invertplayercount: false, enablenotifications: true, disabletrailer: true, gamequalityfilter: false, mutualfriends: true, disablechat: false, smartsearch: true, quicklaunchgames: true, smartjoinpopup: true, betterfriends: true, restoreclassicterms: true, betterprivateservers: true, custombackgrounds: false, btrobloxfix: false, mobilemode: false, joinconfirmation: true, forcedarkmode: false, responsivegamecards: true } }, mobilesettings: { name: "Mobile Settings", settings: { enableLogs: false, removeads: true, togglefilterserversbutton: true, toggleserverhopbutton: true, AutoRunServerRegions: false, ShowOldGreeting: true, togglerecentserverbutton: true, prioritylocation: "automatic", fastservers: true, invertplayercount: false, enablenotifications: true, disabletrailer: true, gamequalityfilter: false, mutualfriends: false, disablechat: true, smartsearch: true, quicklaunchgames: true, smartjoinpopup: false, betterfriends: true, restoreclassicterms: true, betterprivateservers: true, custombackgrounds: false, btrobloxfix: false, mobilemode: true, joinconfirmation: true, forcedarkmode: false, responsivegamecards: false } }, developerpref: { name: "Developer Preference", settings: { enableLogs: true, removeads: true, togglefilterserversbutton: true, toggleserverhopbutton: true, AutoRunServerRegions: false, ShowOldGreeting: true, togglerecentserverbutton: true, prioritylocation: "automatic", fastservers: true, invertplayercount: false, enablenotifications: true, disabletrailer: true, gamequalityfilter: false, mutualfriends: true, disablechat: true, smartsearch: true, quicklaunchgames: true, smartjoinpopup: true, betterfriends: true, restoreclassicterms: true, betterprivateservers: true, custombackgrounds: false, btrobloxfix: false, mobilemode: false, joinconfirmation: true, forcedarkmode: false, responsivegamecards: true } }, serverfiltersonly: { name: "Server Filters Only", settings: { enableLogs: false, removeads: false, togglefilterserversbutton: true, toggleserverhopbutton: false, AutoRunServerRegions: false, ShowOldGreeting: false, togglerecentserverbutton: false, prioritylocation: "automatic", fastservers: true, invertplayercount: false, enablenotifications: true, disabletrailer: false, gamequalityfilter: false, mutualfriends: false, disablechat: false, smartsearch: false, quicklaunchgames: false, smartjoinpopup: true, betterfriends: false, restoreclassicterms: false, betterprivateservers: false, custombackgrounds: false, btrobloxfix: false, mobilemode: false, joinconfirmation: true, forcedarkmode: false, responsivegamecards: false } }, smartsearchonly: { name: "Smart Search Only", settings: { enableLogs: false, removeads: false, togglefilterserversbutton: false, toggleserverhopbutton: false, AutoRunServerRegions: false, ShowOldGreeting: false, togglerecentserverbutton: false, prioritylocation: "automatic", fastservers: false, invertplayercount: false, enablenotifications: true, disabletrailer: false, gamequalityfilter: false, mutualfriends: false, disablechat: false, smartsearch: true, quicklaunchgames: false, smartjoinpopup: false, betterfriends: false, restoreclassicterms: false, betterprivateservers: false, custombackgrounds: false, btrobloxfix: false, mobilemode: false, joinconfirmation: false, forcedarkmode: false, responsivegamecards: false } }, disablerolocate: { name: "Disable RoLocate", settings: { enableLogs: false, removeads: false, togglefilterserversbutton: false, toggleserverhopbutton: false, AutoRunServerRegions: false, ShowOldGreeting: false, togglerecentserverbutton: false, prioritylocation: "automatic", fastservers: false, invertplayercount: false, enablenotifications: true, // ik its suppose to turn off evyerhitng but its for confirmation disabletrailer: false, gamequalityfilter: false, mutualfriends: false, disablechat: false, smartsearch: false, quicklaunchgames: false, smartjoinpopup: false, betterfriends: false, restoreclassicterms: false, betterprivateservers: false, custombackgrounds: false, btrobloxfix: false, mobilemode: false, joinconfirmation: false, forcedarkmode: false, responsivegamecards: false } } }; function initializeLocalStorage() { // this loops through the settings and if they dont exist then add them 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 (error) { ConsoleLogEnabled("Error initializing coordinates storage:", error); // used like the userscript manager storage (cannot be accessed by other extensions) to store coordinates. 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 45.5

Rolocate by Oqarshi.

Licensed under a Custom License โ€“ Personal Use Only. No redistribution.

`; } if (section === "presets") { return `

Overwhelmed by the number of features? Pick a preset right here!

Built-in Presets

๐Ÿ› ๏ธ Default

Default settings that RoLocate comes with.

๐Ÿ“ฑ Mobile Settings

Optimized for Mobile Users.

๐Ÿ‘‘ Dev Settings

Settings used by the developer Oqarshi.

๐ŸŒ Server Filters

Only Enables Server Filters.

๐Ÿง  Smart Search

Only Enables Smart Seach.

๐Ÿšซ RoLocate Off

Turns off all settings.

`; } if (section === "appearance") { return `
Visual settings ๐ŸŽจ๐Ÿ–Œ๏ธ
`; } if (section === "advanced") { return `
For Experienced Users Only๐Ÿง ๐Ÿ™ƒ
Set Default Location Mode ?
Manual: Set your location manually below
Automatic: Auto detect your device's location
`; } if (section === "extras") { return `
Features that might be useful! ๐Ÿ’กโœจ
`; } if (section === "about") { return `

Contributors

Special thanks to everyone who has contributed to this project:

Resources & Links:

`; } if (section === "technical") { // stop it from updating if nothing changed let lastRenderedHash = ''; let storageUpdateInterval; const MB = 1024 * 1024; // helper to make the storage math const getStats = (size, limit) => { const mb = (size / MB).toFixed(2); const percent = (size / limit) * 100; const display = percent >= 0.01 ? `${mb} MB` : `${size.toLocaleString()} bytes`; return { size, mb, percent, display }; }; function calculateStorages() { // calc localstorag size let lsSize = 0; for (let key in localStorage) { if (localStorage.hasOwnProperty(key)) lsSize += key.length + (localStorage[key]?.length || 0); } // do the same for gm storage let gmSize = 0; GM_listValues().forEach(key => { const val = GM_getValue(key, ''); gmSize += key.length + (typeof val === 'string' ? val.length : JSON.stringify(val).length); }); return { ls: getStats(lsSize, 5 * MB), gm: getStats(gmSize, 50 * MB) }; } // shortcut to update the bars so we dont write this 3 times // save some code storage to keep it under 1mb const updateBar = (id, percent, colors) => { const bar = document.getElementById(`rolocate-${id}-bar`); // this is the bar for the storage const txt = document.getElementById(`rolocate-${id}-display`); // this is the display if (bar) { bar.style.width = `${Math.min(percent, 100)}%`; bar.style.background = percent > 90 ? colors[0] : percent > 80 ? colors[1] : colors[2]; } return txt; }; function updateAllDisplays() { const { ls, gm } = calculateStorages(); const lsTxt = updateBar('localstorage', ls.percent, ['#f44336', '#ff9800', '#4CAF50']); if (lsTxt) lsTxt.textContent = `${ls.display} / 5 MB`; const gmTxt = updateBar('gmstorage', gm.percent, ['#f44336', '#ff9800', '#2196F3']); if (gmTxt) gmTxt.textContent = `${gm.display} / 50 MB`; const logMB = rolocateLogSize / MB; const logPercent = (rolocateLogSize / (3 * MB)) * 100; const logTxt = updateBar('logstorage', logPercent, ['#f44336', '#ff9800', '#C8A2C8']); if (logTxt) logTxt.textContent = `${logMB.toFixed(2)} MB / 3 MB`; updateLogDisplay(); } // it updates the log display function updateLogDisplay() { const logEl = document.getElementById('rolocate-logs'); if (!logEl) return; const allLogs = window.rolocateLogBuffer || []; const displayLogs = allLogs.slice(-50); const currentHash = displayLogs.length ? `${displayLogs[0].time}-${displayLogs.length}` : 'empty'; if (currentHash === lastRenderedHash) return; lastRenderedHash = currentHash; if (!displayLogs.length) { logEl.innerHTML = '
No Logs. Enable in Advanced Tab.
'; } else { const atBottom = logEl.scrollHeight - logEl.scrollTop <= logEl.clientHeight + 50; logEl.innerHTML = displayLogs.map(l => `
[${l.time}] ${l.msg}
`).join(''); if (atBottom) logEl.scrollTop = logEl.scrollHeight; } const countEl = document.getElementById('rolocate-log-count'); if (countEl) { countEl.style.display = allLogs.length > 50 ? 'block' : 'none'; countEl.textContent = `Showing last 50 of ${allLogs.length} logs`; } } setTimeout(() => { updateAllDisplays(); // buttons logic document.getElementById('rolocate-copy-logs-btn')?.addEventListener('click', () => { const text = (window.rolocateLogBuffer || []).map(l => `[${l.time}] ${l.msg}`).join('\n'); navigator.clipboard.writeText(text).then(() => notifications(`Copied logs!`, 'success', '', 3000)); }); document.getElementById('rolocate-clear-logs-btn')?.addEventListener('click', () => { window.rolocateLogBuffer = []; rolocateLogSize = 0; lastRenderedHash = ''; updateAllDisplays(); notifications('Logs Cleared!', 'success', '', 3000); }); // the big reset popup document.getElementById('rolocate-factory-reset-btn')?.addEventListener('click', () => { const modal = document.createElement('div'); modal.style.cssText = `position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:10000;display:flex;justify-content:center;align-items:center;transition: opacity 0.2s ease-out;`; modal.innerHTML = `
โš ๏ธ

Factory Reset?

This will delete all data and reset RoLocate to a fresh install.

`; document.body.appendChild(modal); // fade in aniamtion const close = () => { document.getElementById('rolocate-modal-box').style.animation = 'rolocateFadeOut 0.2s forwards'; modal.style.opacity = '0'; setTimeout(() => modal.remove(), 200); }; // reset stuff document.getElementById('cancel-res').onclick = close; document.getElementById('confirm-res').onclick = () => { localStorage.removeItem("version"); // remove localstorage version GM_listValues().forEach(localstoragevaluesreset => GM_deleteValue(localstoragevaluesreset)); // delete all gm values in storage notifications('Reset Complete. Refreshing in 2 seconds...', 'warning', '', '2000'); // we do this before resetting settings becasue notifications won't work if we delete settings first Object.keys(localStorage).forEach(localstoragevaluesreset => localstoragevaluesreset.startsWith("ROLOCATE_") && localStorage.removeItem(localstoragevaluesreset)); // delete all localostorage setting keys close(); // close the popup setTimeout(() => location.reload(), 2000); // refresh page in 2 seconds }; }); storageUpdateInterval = setInterval(updateAllDisplays, 3000); }, 100); window.rolocateStorageInterval = storageUpdateInterval; return `
Used For Development/Support๐Ÿ”ง๐Ÿง ๐Ÿ™ƒ

Script Storage & Console

${['localstorage', 'gmstorage', 'logstorage'].map(id => `
${id === 'logstorage' ? 'Console RAM' : id === 'gmstorage' ? 'GM Storage' : 'LocalStorage'}
${id === 'logstorage' ? '
Resets after page refresh
' : ''}
`).join('')}
Live Console
`; } if (section === "help") { return `

โš™๏ธ General Tab

๐ŸŽจ Appearance Tab

๐Ÿš€ Advanced Tab

โœจ Extra Tab

Need more help?

  • For help, see the troubleshooting page or report an issue on GreasyFork.
  • `; } // general tab which is the default return `
    Common settings in most extensions! โš™๏ธ๐Ÿ”ง
    `; } /******************************************************* name of function: openSettingsMenu description: opens setting menu and makes it look good *******************************************************/ function openSettingsMenu() { if (isDarkMode() === false) { notifications('Youโ€™re using light mode on Roblox. While RoLocate may work, itโ€™s not fully optimized for light mode. For the best experience, please switch to dark mode.', 'info', '๐Ÿ“Œ', 16000); } 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 = `

    RoLocate

    Home

    ${getSettingsContent("home")}
    `; document.body.appendChild(overlay); // put css in const style = document.createElement("style"); style.textContent = ` .highlight-setting { animation: highlightPulse 2s ease; background: rgba(76, 175, 80, 0.2) !important; border-left: 4px solid #4CAF50 !important; border-radius: 8px !important; box-shadow: 0 0 20px rgba(76, 175, 80, 0.4) !important; } @keyframes highlightPulse { 0% { background: rgba(76, 175, 80, 0.3); box-shadow: 0 0 30px rgba(76, 175, 80, 0.6); } 50% { background: rgba(76, 175, 80, 0.25); box-shadow: 0 0 25px rgba(76, 175, 80, 0.5); } 100% { background: rgba(76, 175, 80, 0.15); box-shadow: 0 0 15px rgba(76, 175, 80, 0.3); } } .search-container { width: 100%; position: relative; } #settings-search { width: 100%; padding: 10px 12px; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; color: #e0e0e0; font-size: 14px; transition: all 0.4s cubic-bezier(0.19, 1, 0.22, 1); box-sizing: border-box; } #settings-search:focus { outline: none; background: rgba(255, 255, 255, 0.08); border-color: #4CAF50; box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.15); } #search-suggestions { position: absolute; top: 100%; left: 0; right: 0; background: #2a2a2a; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; margin-top: 4px; max-height: 300px; overflow-y: auto; display: none; z-index: 10002; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); opacity: 0; transform: translateY(-10px); transition: opacity 0.3s cubic-bezier(0.19, 1, 0.22, 1), transform 0.3s cubic-bezier(0.19, 1, 0.22, 1); } #search-suggestions.show { display: block; } @keyframes fadeInItem { from { opacity: 0; } to { opacity: 1; } } .search-suggestion-item { padding: 10px 12px; cursor: pointer; transition: all 0.6s cubic-bezier(0.19, 1, 0.22, 1); border-bottom: 1px solid rgba(255, 255, 255, 0.05); font-size: 13px; opacity: 0; animation: fadeInItem 0.3s ease forwards; position: relative; overflow: hidden; } .search-suggestion-item::before { content: ''; position: absolute; left: 0; top: 0; width: 3px; height: 100%; background: transparent; transition: background 0.3s cubic-bezier(0.19, 1, 0.22, 1); } .search-suggestion-item.matched { background: rgba(76, 175, 80, 0.08); border-left: 3px solid #4CAF50; } .search-suggestion-item.matched::before { background: #4CAF50; } .search-suggestion-item.unmatched { background: rgba(255, 255, 255, 0.02); opacity: 0.7; } .search-suggestion-item:last-child { border-bottom: none; } .search-suggestion-item:hover { background: rgba(76, 175, 80, 0.15); transform: translateX(2px); padding-left: 16px; } .search-suggestion-item.matched:hover { background: rgba(76, 175, 80, 0.2); } .search-suggestion-item:active { transform: translateX(5px) scale(0.98); } .suggestion-title { color: #4CAF50; font-weight: 600; display: block; margin-bottom: 2px; transition: color 0.3s ease; } .search-suggestion-item.unmatched .suggestion-title { color: #999; } .search-suggestion-item:hover .suggestion-title { color: #5fd663; } .suggestion-section { color: #999; font-size: 11px; transition: color 0.3s ease; } .search-suggestion-item:hover .suggestion-section { color: #b0b0b0; } #search-suggestions::-webkit-scrollbar { width: 6px; } #search-suggestions::-webkit-scrollbar-track { background: #1a1a1a; border-radius: 3px; } #search-suggestions::-webkit-scrollbar-thumb { background: #4CAF50; border-radius: 3px; } #search-suggestions::-webkit-scrollbar-thumb:hover { background: #5fd663; } .presets-section { text-align: center; } .presets-actions { display: flex; gap: 12px; justify-content: center; margin-bottom: 20px; } .preset-btn { padding: 10px 20px; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; font-size: 14px; } .export-btn { background: #4CAF50; color: white; } .export-btn:hover { background: #45a049; transform: translateY(-2px); } .import-btn { background: #dc3545; color: white; } .import-btn:hover { background: #c82333; transform: translateY(-2px); } .presets-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 16px; } .preset-card { background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; padding: 16px; cursor: pointer; transition: all 0.3s ease; text-align: left; } .preset-card:hover { background: rgba(255, 255, 255, 0.08); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } .preset-card h4 { margin: 0 0 8px 0; color: #4CAF50; font-size: 14px; } .preset-card p { margin: 0; font-size: 12px; color: #c0c0c0; line-height: 1.4; } .confirmation-popup { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center; z-index: 10002; opacity: 0; animation-fill-mode: forwards; } .confirmation-content { background: #1a1a1a; border-radius: 12px; padding: 24px; width: 400px; text-align: center; border: 1px solid rgba(255, 255, 255, 0.1); } .confirmation-content h3 { margin-top: 0; color: #4CAF50; } .confirmation-buttons { display: flex; gap: 12px; justify-content: center; margin-top: 20px; } .confirm-btn, .cancel-btn { padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; transition: transform 0.1s ease, background 0.2s ease; } .confirm-btn { background: #4CAF50; color: white; } .cancel-btn { background: #666; color: white; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } .fade-in { animation: fadeIn 0.25s ease-out forwards; } .fade-out { animation: fadeOut 0.2s ease-in forwards; } .confirm-btn:active, .cancel-btn:active { transform: scale(0.96); filter: brightness(0.95); } .grayish-center { color: white; font-weight: bold; text-align: center; position: relative; display: inline-block; font-size: 18px !important; } .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: #60a5fa !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: #60a5fa !important; transform: scaleX(0) !important; transform-origin: left !important; transition: transform 0.3s ease !important; } li a.about-link:hover { color: #3b82f6 !important; } li a.about-link:hover::after { transform: scaleX(1) !important; } .about-section ul li a { position: relative; font-weight: bold; color: #60a5fa; 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: #60a5fa; transform: scaleX(0); transform-origin: left; transition: transform 0.3s ease; } .about-section ul li a:hover { color: #3b82f6; } .about-section ul li a:hover::after { transform: scaleX(1); } .license-note { font-size: 0.8em; color: #999; margin-top: 12px; font-style: italic; text-align: center; } .edit-button { margin-left: auto; padding: 2px 8px; font-size: 12px; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 6px; background: #2a2a2a; color: #f0f0f0; cursor: pointer; font-weight: 500; transition: all 0.2s ease; } .edit-button:hover { background: #323232; border-color: rgba(255, 255, 255, 0.15); color: #ffffff; } .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; 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; } .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); border-left: 3px solid #4CAF50; } @keyframes highlight { 0% { background: rgba(76, 175, 80, 0.3); } 100% { background: rgba(76, 175, 80, 0.1); } } .new_label .new { margin-left: 8px; color: #32cd32; font-size: 12px; font-weight: bold; background-color: rgba(50, 205, 50, 0.1); 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; 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; height: 480px; background: linear-gradient(145deg, #1a1a1a, #232323); border-radius: 12px; 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; right: 12px; background: transparent; border: none; color: #c0c0c0; font-size: 20px; cursor: pointer; z-index: 10001; transition: all 0.5s ease; width: 30px; height: 30px; 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%; background: #272727; padding: 18px 12px; 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-y: auto; } .settings-sidebar h2 { margin-bottom: 16px; font-weight: 600; font-size: 22px; 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; width: 36px; height: 3px; background: white; border-radius: 2px; } .settings-sidebar ul { list-style: none; padding: 0; width: 100%; margin-top: 5px; } .settings-sidebar li { padding: 10px 12px; margin: 6px 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; 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; } .settings-sidebar::-webkit-scrollbar { width: 6px; } .settings-sidebar::-webkit-scrollbar-track { background: black; border-radius: 3px; } .settings-sidebar::-webkit-scrollbar-thumb { background: darkgreen; border-radius: 3px; } .settings-sidebar::-webkit-scrollbar-thumb:hover { background: #006400; } .settings-sidebar { scrollbar-width: thin; scrollbar-color: darkgreen black; } .settings-content { flex: 1; padding: 24px; color: white; text-align: center; max-height: 100%; overflow-y: auto; scrollbar-width: thin; scrollbar-color: darkgreen black; background: #1e1e1e; position: relative; } .settings-content::-webkit-scrollbar { width: 6px; } .settings-content::-webkit-scrollbar-track { background: #333; border-radius: 3px; } .settings-content::-webkit-scrollbar-thumb { background: linear-gradient(180deg, #dc3545, #b02a37); border-radius: 3px; } .settings-content::-webkit-scrollbar-thumb:hover { background: linear-gradient(180deg, #ff3b47, #dc3545); } .settings-content h2 { margin-bottom: 24px; font-weight: 600; font-size: 22px; 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; } .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 { display: flex; align-items: center; margin: 12px 0; cursor: pointer; padding: 8px 14px; background: rgba(255, 255, 255, 0.03); border-radius: 6px; 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; height: 22px; background-color: rgba(255, 255, 255, 0.2); border-radius: 22px; margin-right: 12px; 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; width: 16px; 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); } .toggle-slider input:checked + .slider::after { opacity: 1; } .rolocate-logo { width: 90px !important; height: 90px !important; object-fit: contain; border-radius: 14px; display: block; margin: 0 auto 16px auto; 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; color: #aaa; margin-bottom: 24px; display: inline-block; padding: 5px 14px; background: rgba(220, 53, 69, 0.1); border-radius: 18px; border: 1px solid rgba(220, 53, 69, 0.2); } .settings-content ul { text-align: left; list-style-type: none; padding: 0; margin-top: 16px; } .settings-content ul li { margin: 12px 0; padding: 10px 14px; background: rgba(255, 255, 255, 0.03); border-radius: 6px; 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); } .general_section { font-size: 14px; color: #858585; font-weight: bold; padding: 8px 14px; background: rgba(140, 140, 140, 0.12); border-radius: 6px; margin-bottom: 16px; display: inline-block; border: 1px solid rgba(120, 120, 120, 0.2); box-shadow: 0 0 6px rgba(120, 120, 120, 0.2); transition: box-shadow 0.3s ease; } .general_section:hover { box-shadow: 0 0 10px rgba(120, 120, 120, 0.35); } .appearance_section { font-size: 14px; color: #6b5cff; font-weight: bold; padding: 8px 14px; background: rgba(107, 92, 255, 0.1); border-radius: 6px; margin-bottom: 16px; display: inline-block; border: 1px solid rgba(107, 92, 255, 0.25); box-shadow: 0 0 6px rgba(107, 92, 255, 0.25); transition: box-shadow 0.3s ease; } .appearance_section:hover { box-shadow: 0 0 12px rgba(107, 92, 255, 0.5); } .extras_section { font-size: 14px; color: #0d6efd; 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); } .edit-nav-button { padding: 6px 14px; background: #4CAF50; color: white; border: none; border-radius: 6px; cursor: pointer; font-family: 'Inter', 'Helvetica', sans-serif; font-size: 12px; 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); } #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); } #location-hint { margin-top: 10px; font-size: 12px; color: #c0c0c0; background: rgba(255, 255, 255, 0.05); border-radius: 6px; padding: 10px 14px; border: 1px solid rgba(255, 255, 255, 0.05); line-height: 1.6; transition: all 0.5s ease; } .section-separator { width: 100%; height: 1px; background: linear-gradient(90deg, transparent, #272727, transparent); margin: 24px 0; } .help-section h3, .about-section h3 { color: white; margin-top: 20px; margin-bottom: 12px; font-size: 16px; text-align: left; } .hint-text { font-size: 13px; color: #a0a0a0; margin-top: 6px; margin-left: 16px; text-align: left; } .location-settings { background: rgba(255, 255, 255, 0.03); border-radius: 6px; padding: 14px; margin-top: 16px; 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; } .setting-header span { font-size: 14px; font-weight: 500; } .help-icon { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; background: rgba(220, 53, 69, 0.2); border-radius: 50%; font-size: 11px; color: #ff3b47; cursor: help; transition: all 0.5s ease; } #manual-coordinates { margin-top: 12px !important; } .coordinates-inputs { gap: 8px !important; margin-bottom: 10px !important; } #manual-coordinates input { padding: 8px 10px !important; border-radius: 6px !important; font-size: 13px !important; } #manual-coordinates label { margin-bottom: 6px !important; font-size: 13px !important; } #save-coordinates { margin-top: 6px !important; } .animated-content { animation: sectionFade 0.7s cubic-bezier(0.19, 1, 0.22, 1); } .section-divider { height: 1px !important; background: linear-gradient(90deg, transparent, #444, transparent); margin: 8px 12px !important; padding: 0 !important; cursor: default !important; pointer-events: none; } .section-divider:hover { background: linear-gradient(90deg, transparent, #444, transparent) !important; transform: none !important; } `; 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); if (section === "general") { const autoserverregionscheckbox = document.getElementById("AutoRunServerRegions"); const editButton_autoserverregionsbtn = document.getElementById("edit-autoserverregionsbutton-btn"); if (autoserverregionscheckbox && editButton_autoserverregionsbtn) { // show edit button editButton_autoserverregionsbtn.style.display = localStorage.getItem("ROLOCATE_AutoRunServerRegions") === "true" ? "block" : "none"; // uhh on and off for edit buttoin autoserverregionscheckbox.addEventListener("change", function() { const isEnabled = this.checked; editButton_autoserverregionsbtn.style.display = isEnabled ? "block" : "none"; }); } } // quick nav and removeads stuff if (section === "appearance") { // remove the ads const removeAdsCheckbox = document.getElementById("removeads"); const editRemoveAdsButton = document.getElementById("edit-removeads-btn"); if (removeAdsCheckbox && editRemoveAdsButton) { editRemoveAdsButton.style.display = localStorage.getItem("ROLOCATE_removeads") === "true" ? "block" : "none"; removeAdsCheckbox.addEventListener("change", function() { const isEnabled = this.checked; localStorage.setItem("ROLOCATE_removeads", isEnabled); editRemoveAdsButton.style.display = isEnabled ? "block" : "none"; }); } const betterprivateserversCheckbox = document.getElementById("betterprivateservers"); const editBetterPrivateServersButton = document.getElementById("edit-betterprivateservers-btn"); if (betterprivateserversCheckbox && editBetterPrivateServersButton) { editBetterPrivateServersButton.style.display = localStorage.getItem("ROLOCATE_betterprivateservers") === "true" ? "block" : "none"; betterprivateserversCheckbox.addEventListener("change", function() { const isEnabled = this.checked; localStorage.setItem("ROLOCATE_betterprivateservers", isEnabled); editBetterPrivateServersButton.style.display = isEnabled ? "block" : "none"; }); } // custom backtgrounds const customBackgroundsCheckbox = document.getElementById("custombackgrounds"); const editBackgroundsButton = document.getElementById("edit-backgrounds-btn"); if (customBackgroundsCheckbox && editBackgroundsButton) { editBackgroundsButton.style.display = localStorage.getItem("ROLOCATE_custombackgrounds") === "true" ? "block" : "none"; // update localstorage and show edit button if button has any customBackgroundsCheckbox.addEventListener("change", function() { const isEnabled = this.checked; localStorage.setItem("ROLOCATE_custombackgrounds", isEnabled); editBackgroundsButton.style.display = isEnabled ? "block" : "none"; }); } } if (section === "advanced") { const serverfilterscheckbox = document.getElementById("togglefilterserversbutton"); const editButton_serverfiltersbtn = document.getElementById("edit-serverfilters-btn"); if (serverfilterscheckbox && editButton_serverfiltersbtn) { // show edit button editButton_serverfiltersbtn.style.display = localStorage.getItem("ROLOCATE_togglefilterserversbutton") === "true" ? "block" : "none"; // uhh on and off for edit buttoin serverfilterscheckbox.addEventListener("change", function() { const isEnabled = this.checked; editButton_serverfiltersbtn.style.display = isEnabled ? "block" : "none"; }); } } if (section === "extras") { const gameQualityCheckbox = document.getElementById("gamequalityfilter"); const editButton = document.getElementById("edit-gamequality-btn"); if (gameQualityCheckbox && editButton) { // show edit button editButton.style.display = localStorage.getItem("ROLOCATE_gamequalityfilter") === "true" ? "block" : "none"; // uhh on and off for edit buttoin 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 document.getElementById("close-settings").addEventListener("click", function() { 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 if no coordiantes in manual mode } } catch (error) { ConsoleLogEnabled("Error checking coordinates:", error); notifications('Error checking location settings', 'error', 'โš ๏ธ', 8000); return; // prevent closing if there is an error } } // uh close if all is good const menu = document.getElementById("userscript-settings-menu"); menu.style.animation = "fadeOut 0.4s cubic-bezier(0.19, 1, 0.22, 1) forwards"; // cool aniamtion for the close button this.style.transform = "rotate(90deg)"; setTimeout(() => menu.remove(), 400); }); // uh does whats in the fucntion name applyStoredSettings(); // oooo a ripple animation cool :) 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); }); }); // uh look at help icon clicky document.addEventListener('click', function(e) { if (e.target.classList.contains('help-icon')) { // no glitches no bubble up e.stopPropagation(); e.preventDefault(); const helpItem = e.target.getAttribute('data-help'); if (helpItem) { // go to help tab const helpTab = document.querySelector('.settings-sidebar li[data-section="help"]'); if (helpTab) helpTab.click(); // cool animtion to scroll down on help tab 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); } } }); // all settings that are shown in search in settings // js for the search funcitonality const searchableSettings = [ ["SmartSearch", "general", "smartsearch", "search smart instant"], ["Auto Server Regions", "general", "AutoRunServerRegions", "server region auto location"], ["Fast Server Search", "general", "fastservers", "fast server speed quick"], ["Invert Player Count", "general", "invertplayercount", "invert player count"], ["Recent Servers", "general", "togglerecentserverbutton", "recent server history"], ["Join Confirmation", "general", "joinconfirmation", "join confirm popup"], ["Disable Trailer Autoplay", "appearance", "disabletrailer", "trailer autoplay video"], ["Smart Join Popup", "appearance", "smartjoinpopup", "join popup smart"], ["Remove All Roblox Ads", "appearance", "removeads", "ads remove block"], ["Restore Classic Terms", "appearance", "restoreclassicterms", "classic terms restore"], ["Responsive Game Cards", "appearance", "responsivegamecards", "game cards responsive"], ["Better Private Servers", "appearance", "betterprivateservers", "private server compact"], ["Custom Backgrounds", "appearance", "custombackgrounds", "background custom theme"], ["Enable Console Logs", "advanced", "enableLogs", "console log debug"], ["Enable Server Filters", "advanced", "togglefilterserversbutton", "server filter"], ["Enable Server Hop Button", "advanced", "toggleserverhopbutton", "server hop button"], ["Enable Notifications", "advanced", "enablenotifications", "notification alert"], ["Fix BTRoblox Compatability", "advanced", "btrobloxfix", "btroblox fix compatible"], ["Mobile Mode", "advanced", "mobilemode", "mobile mode phone"], ["Force Dark Mode Styles", "advanced", "forcedarkmode", "dark mode force theme"], ["Set Default Location Mode", "advanced", "prioritylocation-select", "location gps coordinates"], ["Game Quality Filter", "extras", "gamequalityfilter", "game quality filter"], ["Mutual Friends", "extras", "mutualfriends", "mutual friends shared"], ["Disable Chat", "extras", "disablechat", "chat disable hide"], ["Quick Launch Games", "extras", "quicklaunchgames", "quick launch favorite"], ["Show Old Greeting", "extras", "ShowOldGreeting", "old greeting classic"], ["Better Friends", "extras", "betterfriends", "friends better best"] ].map(([name, section, id, keywords]) => ({ name, section, id, keywords: keywords.split(' ') })); const searchInput = document.getElementById('settings-search'); const suggestionsBox = document.getElementById('search-suggestions'); if (searchInput && suggestionsBox) { let debounceTimer; // holy crap this was annoying af searchInput.addEventListener('input', function() { clearTimeout(debounceTimer); const q = this.value.toLowerCase().trim(); debounceTimer = setTimeout(() => { const results = searchableSettings.map(s => ({ ...s, match: !q || s.name.toLowerCase().includes(q) || s.section.toLowerCase().includes(q) || s.keywords.some(k => k.includes(q)) })).sort((a, b) => b.match - a.match); suggestionsBox.innerHTML = results.map((r, i) => `
    ${r.name} ${r.section[0].toUpperCase() + r.section.slice(1)}
    `).join(''); suggestionsBox.classList.add('show'); // Add a small delay to let display: block render first requestAnimationFrame(() => { requestAnimationFrame(() => { suggestionsBox.style.opacity = '1'; suggestionsBox.style.transform = 'translateY(0)'; }); }); }, 100); }); // also show all suggestions when search box is focused & nothing is shown searchInput.addEventListener('focus', function() { // show suggestions if there is text too this.dispatchEvent(new Event('input')); }); // hide suggestions when clicking outside document.addEventListener('click', function(e) { if (!searchInput.contains(e.target) && !suggestionsBox.contains(e.target)) { suggestionsBox.style.opacity = '0'; suggestionsBox.style.transform = 'translateY(-10px)'; setTimeout(() => { suggestionsBox.classList.remove('show'); }, 200); } }); // hde suggestions when search box loses focus unless is clisnign the suggestions searchInput.addEventListener('blur', function() { setTimeout(() => { if (!suggestionsBox.matches(':hover')) { suggestionsBox.style.opacity = '0'; suggestionsBox.style.transform = 'translateY(-10px)'; setTimeout(() => { suggestionsBox.classList.remove('show'); }, 200); } }, 150); }); // click on suggestion suggestionsBox.addEventListener('click', function(e) { const item = e.target.closest('.search-suggestion-item'); if (item) { const section = item.getAttribute('data-section'); const settingName = item.getAttribute('data-setting'); // add click animation item.style.transform = 'scale(0.95)'; setTimeout(() => item.style.transform = '', 100); // go to section const sectionTab = document.querySelector(`.settings-sidebar li[data-section="${section}"]`); if (sectionTab) { sectionTab.click(); // highlight and scroll to the actual setting toggle setTimeout(() => { // try to find the setting by its ID or text content const settingElement = findSettingElement(settingName, section); if (settingElement) { settingElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); // add highlight class settingElement.classList.add('highlight-setting'); // remove highlight after 2 seconds setTimeout(() => { settingElement.classList.remove('highlight-setting'); }, 2000); } }, 400); // wait for section transition } searchInput.value = ''; suggestionsBox.style.opacity = '0'; suggestionsBox.style.transform = 'translateY(-10px)'; setTimeout(() => { suggestionsBox.classList.remove('show'); suggestionsBox.style.transform = 'translateY(0)'; }, 200); } }); // helper function to find the setting element and stufdf function findSettingElement(settingName) { const setting = searchableSettings.find(s => s.name === settingName); if (!setting?.id) return null; const el = document.getElementById(setting.id); return el?.closest('.toggle-slider') || el?.closest('.location-settings'); } } } /******************************************************* name of function: applyStoredSettings description: makes sure local storage is stored in correctly *******************************************************/ function applyStoredSettings() { // checkbox stuff 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); }); }); // location stuff const prioritySelect = document.getElementById("prioritylocation-select"); if (prioritySelect) { const storageKey = "ROLOCATE_prioritylocation"; const savedValue = localStorage.getItem(storageKey) || "automatic"; prioritySelect.value = savedValue; // hide coordinate box if in automica vice versa const manualCoordinates = document.getElementById("manual-coordinates"); if (manualCoordinates) { manualCoordinates.style.display = savedValue === "manual" ? "block" : "none"; // manual set input stuff 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 go back to automatic if (!savedCoords.lat || !savedCoords.lng) { prioritySelect.value = "automatic"; localStorage.setItem(storageKey, "automatic"); manualCoordinates.style.display = "none"; } } catch (error) { ConsoleLogEnabled("Error loading saved coordinates:", error); } } } prioritySelect.addEventListener("change", () => { const newValue = prioritySelect.value; localStorage.setItem(storageKey, newValue); // show coordinate input if thereq 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 input then keep it empty } catch (error) { ConsoleLogEnabled("Error loading saved coordinates:", error); } } } }); } // uh buttons that need special treatment const editRemoveads = document.getElementById("edit-removeads-btn"); if (editRemoveads) { editRemoveads.addEventListener("click", () => { editremoveads(); }); } const editBackgrounds = document.getElementById("edit-backgrounds-btn"); if (editBackgrounds) { editBackgrounds.addEventListener("click", () => { showSettingsPopup_background(); }); } 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.', 'info', '๐Ÿงช', 2000); } }); } const AutoRunServerRegions = document.getElementById("AutoRunServerRegions"); const AutoRunServerRegionsbutton = document.getElementById("edit-autoserverregionsbutton-btn") 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); } }); AutoRunServerRegionsbutton.addEventListener("click", () => { ChangeAutoServerRegionCount(); }); } const editServerfilters = document.getElementById("edit-serverfilters-btn"); if (editServerfilters) { editServerfilters.addEventListener("click", () => { editserverregions(); }); } const editBetterPrivateServers = document.getElementById("edit-betterprivateservers-btn"); if (editBetterPrivateServers) { editBetterPrivateServers.addEventListener("click", () => { editprivateserversettings(); }); } // save coordinates button duh 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(); // doubole check for stuff 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"; // if user sees this then something went wrong. saveCoordinatesBtn.textContent = "Reverted to Automatic!"; saveCoordinatesBtn.style.background = "#4CAF50"; setTimeout(() => { saveCoordinatesBtn.textContent = "Save Coordinates"; saveCoordinatesBtn.style.background = "background: #4CAF50;"; }, 2000); } return; } // make sure they are actually real coordiantes // wont check if ur in a middle of the ocean lmao 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 the coordinates const coordinates = { lat, lng }; GM_setValue("ROLOCATE_coordinates", JSON.stringify(coordinates)); // store coordinates in secure storage // make sure in manaul mode. triple check localStorage.setItem("ROLOCATE_prioritylocation", "manual"); if (prioritySelect) { prioritySelect.value = "manual"; } // tell user it saved saveCoordinatesBtn.textContent = "Saved!"; saveCoordinatesBtn.style.background = "linear-gradient(135deg, #1e8449 0%, #196f3d 100%);"; setTimeout(() => { saveCoordinatesBtn.textContent = "Save Coordinates"; saveCoordinatesBtn.style.background = "background: #4CAF50;"; }, 2000); }); } const exportBtn = document.getElementById("export-settings"); const importBtn = document.getElementById("import-settings"); const importFile = document.getElementById("import-file"); if (exportBtn) { exportBtn.addEventListener("click", exportSettings); } if (importBtn && importFile) { importBtn.addEventListener("click", () => importFile.click()); importFile.addEventListener("change", (e) => { if (e.target.files[0]) { showConfirmation( "Import Settings", "This will overwrite your current settings. Continue?", () => importSettings(e.target.files[0]) ); } }); } // preset cards document.querySelectorAll(".preset-card").forEach(card => { card.addEventListener("click", () => { const preset = card.dataset.preset; const config = presetConfigurations[preset]; if (config) { showConfirmation( `Apply ${config.name} Preset`, `This will change your current settings to the ${config.name} configuration. Continue?`, () => applyPreset(preset) ); } }); }); } function exportSettings() { const settings = {}; Object.keys(defaultSettings).forEach(key => { settings[key] = localStorage.getItem(`ROLOCATE_${key}`); }); const blob = new Blob([JSON.stringify(settings, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'rolocate-settings.json'; a.click(); URL.revokeObjectURL(url); notifications('Settings exported successfully!', 'success', '๐Ÿ“ค', 3000); } function importSettings(file) { const reader = new FileReader(); reader.onload = function(e) { try { const settings = JSON.parse(e.target.result); Object.entries(settings).forEach(([key, value]) => { if (Object.prototype.hasOwnProperty.call(defaultSettings, key) && value !== null) { localStorage.setItem(`ROLOCATE_${key}`, value); } }); notifications('Settings imported successfully! Refresh the page to see changes.', 'success', '๐Ÿ“ฅ', 5000); } catch (error) { notifications('Invalid settings file!', 'error', 'โŒ', 3000); } }; reader.readAsText(file); } function applyPreset(presetKey) { const preset = presetConfigurations[presetKey]; if (!preset) return; Object.entries(preset.settings).forEach(([key, value]) => { localStorage.setItem(`ROLOCATE_${key}`, value); }); notifications(`${preset.name} preset applied! Refreshing the page in 3 seconds...`, 'success', 'โšก', 3000); setTimeout(() => { location.reload(); }, 3000); // refresh after 5 seconds } function showConfirmation(title, message, onConfirm) { const popup = document.createElement('div'); popup.className = 'confirmation-popup fade-in'; popup.innerHTML = `

    ${title}

    ${message}

    `; document.body.appendChild(popup); const removePopup = () => { popup.classList.remove('fade-in'); popup.classList.add('fade-out'); popup.addEventListener('animationend', () => popup.remove(), { once: true }); }; popup.querySelector('.confirm-btn').addEventListener('click', () => { removePopup(); onConfirm(); }); popup.querySelector('.cancel-btn').addEventListener('click', () => { removePopup(); }); } /******************************************************* 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: editremoveads description: popup for customizing the ads *******************************************************/ function editremoveads () { // donโ€™t open it twice if (document.getElementById('rolocate-ad-settings-modal')) return; // default toggle values const defaultSettings = { adIframes: true, sponsoredGames: true, sponsoredSections: true, todaysPicks: true, recommendedForYou: true, feedItems: true }; // load saved settings and fall back to defaults const savedSettings = JSON.parse( localStorage.getItem('ROLOCATE_editremoveads') || '{}' ); const settings = { ...defaultSettings, ...savedSettings }; // dark background overlay const overlay = document.createElement('div'); overlay.id = 'rolocate-ad-settings-modal'; overlay.style.cssText = ` position:fixed;inset:0;display:flex;justify-content:center;align-items:center; background:rgba(0,0,0,.45);z-index:10000;opacity:0;transition:.2s; `; // main modal box const modal = document.createElement('div'); modal.style.cssText = ` background:#181818;border-radius:14px;padding:18px;width:340px;max-width:92vw; color:#fff;border:1px solid #2f2f2f;box-shadow:0 10px 30px rgba(0,0,0,.6); transform:scale(.96) translateY(12px);transition:.2s; `; // title + subtitle modal.innerHTML = `

    Ad Settings

    Choose what you want hidden

    `; // toggle definitions const toggleOptions = [ ['adIframes', 'Ad Iframes'], ['sponsoredGames', 'Sponsored Games'], ['sponsoredSections', 'Sponsored Sections'], ['todaysPicks', "Today's Picks"], ['recommendedForYou', 'Recommended For You'], ['feedItems', 'Feed Posts'] ]; // container for all toggles const togglesContainer = document.createElement('div'); togglesContainer.style.cssText = ` background:#222;padding:10px;border-radius:10px;display:grid;gap:8px; `; // build each toggle row toggleOptions.forEach(([key, label]) => { const row = document.createElement('label'); row.style.cssText = ` display:flex;justify-content:space-between;align-items:center; padding:8px 10px;border-radius:8px;cursor:pointer; transition:.15s;background:#262626; `; // hover effect row.onmouseenter = () => (row.style.background = '#2d2d2d'); row.onmouseleave = () => (row.style.background = '#262626'); row.innerHTML = ` ${label}
    `; const checkbox = row.querySelector('input'); const toggle = row.querySelector('.tgl'); const knob = toggle.querySelector('div'); // handle toggle click row.onclick = (e) => { e.preventDefault(); checkbox.checked = !checkbox.checked; toggle.style.background = checkbox.checked ? '#16a34a' : '#444'; knob.style.left = checkbox.checked ? '18px' : '2px'; }; togglesContainer.appendChild(row); }); // buttons container const buttonRow = document.createElement('div'); buttonRow.style.cssText = ` display:flex;justify-content:flex-end;gap:8px;margin-top:14px; `; // reusable button factory const createButton = (text, bgColor, onClick) => { const button = document.createElement('button'); button.textContent = text; button.style.cssText = ` padding:8px 14px;border-radius:8px;border:1px solid ${bgColor}; background:${bgColor};color:#fff;font-size:13px;cursor:pointer; transition:.15s; `; button.onmouseenter = () => (button.style.opacity = 0.85); button.onmouseleave = () => (button.style.opacity = 1); button.onclick = onClick; return button; }; // close animation + cleanup const closeModal = () => { modal.style.transform = 'scale(.96) translateY(12px)'; overlay.style.opacity = '0'; setTimeout(() => overlay.remove(), 200); }; // add buttons buttonRow.append( createButton('Cancel', '#333', closeModal), createButton('Save', '#16a34a', () => { const newSettings = {}; toggleOptions.forEach(([key]) => { newSettings[key] = document.getElementById(key).checked; }); localStorage.setItem( 'ROLOCATE_editremoveads', JSON.stringify(newSettings) ); // feedback stuff ConsoleLogEnabled('Ad settings saved:', newSettings); notifications('Settings saved', 'success', '๐Ÿ‘', '5000'); closeModal(); }) ); // assemble modal modal.append(togglesContainer, buttonRow); overlay.append(modal); document.body.append(overlay); // animate in requestAnimationFrame(() => { overlay.style.opacity = '1'; modal.style.transform = 'scale(1) translateY(0)'; }); } /******************************************************* name of function: removeAds description: remove roblox ads *******************************************************/ function removeAds() { if (localStorage.getItem("ROLOCATE_removeads") !== "true") { return; } const userSettings = JSON.parse(localStorage.getItem("ROLOCATE_editremoveads") || '{}'); const defaultSettings = { adIframes: true, sponsoredGames: true, sponsoredSections: true, todaysPicks: true, recommendedForYou: true, feedItems: true };// if no settings use default settings const settings = { ...defaultSettings, ...userSettings }; 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 Updated to filter based on user settings *******************************************************/ function removeElements() { // prevent multiple runs at same time if (isRunning) return; isRunning = true; try { // block ad iframes if enabled by roblox for some reason if (settings.adIframes) { 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 cause no want page break iframe.style.display = "none"; iframe.style.visibility = "hidden"; doneMap.set(iframe, true); } }); } // block sponsored game cards if enabled if (settings.sponsoredGames) { 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 if enabled if (settings.sponsoredSections) { document.querySelectorAll(".game-sort-carousel-wrapper").forEach(wrapper => { if (doneMap.get(wrapper)) return; const headerText = wrapper.querySelector('[data-testid="text-icon-row-text"]')?.textContent.trim(); const linkElement = wrapper.querySelector('a[href*="/sortName/v2/"]'); // Check if it's specifically a sponsored section const isSponsored = headerText === "Sponsored" || (linkElement && linkElement.href.includes("/sortName/v2/Sponsored")); if (isSponsored) { wrapper.style.display = "none"; doneMap.set(wrapper, true); } }); } // block "today's picks" section if enabled if (settings.todaysPicks) { 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); } }); } // block "recommended for you" section if enabled if (settings.recommendedForYou) { document.querySelectorAll('[data-testid="home-page-game-grid"]').forEach(grid => { if (!doneMap.get(grid)) { grid.style.display = "none"; doneMap.set(grid, true); } }); } // block feed items if enabled if (settings.feedItems) { document.querySelectorAll(".sdui-feed-item-container").forEach(node => { if (!doneMap.get(node)) { node.style.display = "none"; doneMap.set(node, true); } }); } } finally { isRunning = false; } } // no comment 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 orgin do its thing first if its installed // im a master at glitch fixing ikr setTimeout(removeElements, 100); } /******************************************************* name of function: changeServerCount description: gui to cyhange autoservergion count *******************************************************/ function ChangeAutoServerRegionCount () { const currentCount = localStorage.getItem('ROLOCATE_AutoRunServerRegionsnumber') || '16'; // create dark overlay const overlay = document.createElement('div'); overlay.id = 'rolocate-server-count-modal'; overlay.style.cssText = ` position:fixed;inset:0;display:flex;justify-content:center;align-items:center; background:rgba(0,0,0,.45);z-index:10000;opacity:0;transition:.2s; `; // main modal box const modal = document.createElement('div'); modal.style.cssText = ` background:#181818;border-radius:14px;padding:18px;width:340px;max-width:92vw; color:#fff;border:1px solid #2f2f2f;box-shadow:0 10px 30px rgba(0,0,0,.6); transform:scale(.96) translateY(12px);transition:.2s; `; // title + input + buttons modal.innerHTML = `

    # of Servers to Search

    Default is 16 (Range: 1โ€“700)

    `; const input = modal.querySelector('input'); // buttons container const btnRow = document.createElement('div'); btnRow.style.cssText = 'display:flex;justify-content:flex-end;gap:8px;'; // helper to make buttons const makeBtn = (text, bg, fn) => { const b = document.createElement('button'); b.textContent = text; b.style.cssText = ` padding:8px 14px;border-radius:8px;border:1px solid ${bg}; background:${bg};color:#fff;font-size:13px;cursor:pointer;transition:.15s; `; b.onmouseenter = () => b.style.opacity = .85; b.onmouseleave = () => b.style.opacity = 1; b.onclick = fn; return b; }; // fade out modal const closeModal = () => { modal.style.transform = 'scale(.96) translateY(12px)'; overlay.style.opacity = '0'; setTimeout(() => overlay.remove(), 200); }; // add cancel + save buttons const cancelBtn = makeBtn('Cancel', '#333', closeModal); const saveBtn = makeBtn('Save', '#16a34a', () => { const val = parseInt(input.value); if (val >= 1 && val <= 700) { localStorage.setItem('ROLOCATE_AutoRunServerRegionsnumber', val.toString()); saveBtn.textContent = 'โœ“ Saved!'; saveBtn.style.background = '#10b981'; setTimeout(() => closeModal(), 1000); } else { notifications("Please enter a valid number from 1-700", "warning", "", 4000); } }); btnRow.append(cancelBtn, saveBtn); modal.append(btnRow); overlay.append(modal); document.body.append(overlay); // animate in like editRemoveAds requestAnimationFrame(() => { overlay.style.opacity = '1'; modal.style.transform = 'scale(1) translateY(0)'; }); // focus input input.focus(); // click outside closes overlay.onclick = (e) => { if (e.target === overlay) closeModal(); }; } /******************************************************* name of function: editserverregions description: popup for customizing allowed/banned server regions *******************************************************/ function editserverregions() { // don't open it twice if (document.getElementById('rolocate-region-settings-modal')) return; // ensure server regions are loaded if (typeof window.loadServerRegions === 'function') { window.loadServerRegions(); } // preload flag data getFlagEmoji(); // get all unique locations from the library const locations = window.serverRegionsByIp?._locations || {}; // load saved settings or create defaults (all allowed) const savedSettings = JSON.parse( localStorage.getItem('ROLOCATE_serverRegions') || '{}' ); // create default settings with all regions allowed const defaultSettings = {}; Object.keys(locations).forEach(locationId => { const location = locations[locationId]; const key = `${location.city}_${location.country.code}`; defaultSettings[key] = savedSettings[key] !== undefined ? savedSettings[key] : 'allowed'; }); const settings = { ...defaultSettings }; // dark background overlay const overlay = document.createElement('div'); overlay.id = 'rolocate-region-settings-modal'; overlay.style.cssText = ` position:fixed;inset:0;display:flex;justify-content:center;align-items:center; background:rgba(0,0,0,.45);z-index:10000;opacity:0;transition:.2s; `; // main modal box const modal = document.createElement('div'); modal.className = 'dummy-class-for-server-region-edit-so-restoreclassicterms-can-target-this'; // yea ik im the best at naming stuff modal.style.cssText = ` background:#181818;border-radius:14px;padding:18px;width:420px;max-width:92vw; max-height:85vh;color:#fff;border:1px solid #2f2f2f; box-shadow:0 10px 30px rgba(0,0,0,.6); transform:scale(.96) translateY(12px);transition:.2s; display:flex;flex-direction:column; `; // title + subtitle const header = document.createElement('div'); header.innerHTML = `

    Server Region Settings

    Only join servers from enabled regions

    Affects ServerHop, Server Regions, and Best Connection

    `; // scrollable container for toggles const scrollContainer = document.createElement('div'); scrollContainer.style.cssText = ` overflow-y:auto;max-height:50vh; `; // container for all toggles const togglesContainer = document.createElement('div'); togglesContainer.style.cssText = ` background:#222;padding:10px;border-radius:10px;display:grid;gap:8px; `; // build toggle for each unique region const uniqueRegions = {}; Object.keys(locations).forEach(locationId => { const location = locations[locationId]; const key = `${location.city}_${location.country.code}`; if (!uniqueRegions[key]) { uniqueRegions[key] = location; } }); // sort regions alphabetically by city const sortedKeys = Object.keys(uniqueRegions).sort((a, b) => { return uniqueRegions[a].city.localeCompare(uniqueRegions[b].city); }); sortedKeys.forEach(key => { const location = uniqueRegions[key]; const isAllowed = settings[key] === 'allowed'; const row = document.createElement('label'); row.style.cssText = ` display:flex;justify-content:space-between;align-items:center; padding:8px 10px;border-radius:8px;cursor:pointer; transition:.15s;background:#262626; `; // hover effect row.onmouseenter = () => (row.style.background = '#2d2d2d'); row.onmouseleave = () => (row.style.background = '#262626'); // create flag element const flagImg = getFlagEmoji(location.country.code); if (flagImg) { flagImg.style.borderRadius = '3px'; flagImg.style.objectFit = 'cover'; flagImg.style.marginRight = '10px'; } // create left side container const leftContainer = document.createElement('div'); leftContainer.style.cssText = 'display:flex;align-items:center;gap:10px;flex:1;'; if (flagImg) { leftContainer.appendChild(flagImg); } const textContainer = document.createElement('div'); textContainer.style.cssText = 'display:flex;flex-direction:column;gap:2px;'; textContainer.innerHTML = ` ${location.city}, ${location.country.code} ${location.region.name} `; leftContainer.appendChild(textContainer); // create checkbox (hidden) const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = key; checkbox.checked = isAllowed; checkbox.style.display = 'none'; // create toggle switch const toggle = document.createElement('div'); toggle.className = 'tgl'; toggle.style.cssText = ` width:36px;height:20px;border-radius:20px; background:${isAllowed ? '#16a34a' : '#dc2626'}; position:relative;transition:.15s;flex-shrink:0; `; const knob = document.createElement('div'); knob.style.cssText = ` width:16px;height:16px;border-radius:50%;background:#fff; position:absolute;top:2px;left:${isAllowed ? '18px' : '2px'}; transition:.15s; `; toggle.appendChild(knob); // assemble row row.appendChild(leftContainer); row.appendChild(checkbox); row.appendChild(toggle); // handle toggle click row.onclick = (e) => { e.preventDefault(); checkbox.checked = !checkbox.checked; toggle.style.background = checkbox.checked ? '#16a34a' : '#dc2626'; knob.style.left = checkbox.checked ? '18px' : '2px'; }; togglesContainer.appendChild(row); }); scrollContainer.appendChild(togglesContainer); // buttons container const buttonRow = document.createElement('div'); buttonRow.style.cssText = ` display:flex;justify-content:space-between;gap:8px;margin-top:14px; `; // reusable button factory const createButton = (text, bgColor, onClick) => { const button = document.createElement('button'); button.textContent = text; button.style.cssText = ` padding:8px 14px;border-radius:8px;border:1px solid ${bgColor}; background:${bgColor};color:#fff;font-size:13px;cursor:pointer; transition:.15s;flex:1; `; button.onmouseenter = () => (button.style.opacity = 0.85); button.onmouseleave = () => (button.style.opacity = 1); button.onclick = onClick; return button; }; // close animation & cleanup const closeModal = () => { modal.style.transform = 'scale(.96) translateY(12px)'; overlay.style.opacity = '0'; setTimeout(() => overlay.remove(), 200); }; // add buttons const leftButtons = document.createElement('div'); leftButtons.style.cssText = 'display:flex;gap:8px;'; const rightButtons = document.createElement('div'); rightButtons.style.cssText = 'display:flex;gap:8px;'; leftButtons.append( createButton('Reset', '#0ea5e9', () => { sortedKeys.forEach(key => { const checkbox = document.getElementById(key); const row = checkbox.closest('label'); const toggle = row.querySelector('.tgl'); const knob = toggle.querySelector('div'); checkbox.checked = true; toggle.style.background = '#16a34a'; knob.style.left = '18px'; }); }, true), createButton('Disable All', '#bf7c0a', () => { sortedKeys.forEach(key => { const checkbox = document.getElementById(key); const row = checkbox.closest('label'); const toggle = row.querySelector('.tgl'); const knob = toggle.querySelector('div'); checkbox.checked = false; toggle.style.background = '#dc2626'; knob.style.left = '2px'; }); }, true) ); rightButtons.append( createButton('Cancel', '#333', closeModal), createButton('Save', '#16a34a', () => { const newSettings = {}; sortedKeys.forEach(key => { const isChecked = document.getElementById(key).checked; newSettings[key] = isChecked ? 'allowed' : 'banned'; }); localStorage.setItem( 'ROLOCATE_serverRegions', JSON.stringify(newSettings) ); // feedback if (typeof ConsoleLogEnabled === 'function') { ConsoleLogEnabled('Server region settings saved:', newSettings); } if (typeof notifications === 'function') { notifications('Region settings saved', 'success', '๐ŸŒ', '5000'); } closeModal(); }) ); buttonRow.append(leftButtons, rightButtons); // assemble modal modal.append(header, scrollContainer, buttonRow); overlay.append(modal); document.body.append(overlay); // animate in requestAnimationFrame(() => { overlay.style.opacity = '1'; modal.style.transform = 'scale(1) translateY(0)'; }); } /******************************************************* name of function: editprivateserversettings description: popup for customizing better private server settings *******************************************************/ function editprivateserversettings() { // check if Better Private Servers is enabled const bpsEnabled = localStorage.getItem('ROLOCATE_betterprivateservers'); if (bpsEnabled !== 'true') return; // don't open it twice if (document.getElementById('rolocate-ps-settings-modal')) return; notifications('โ€œCompact Private Serversโ€ and โ€œPrivate Server Searchโ€ are temporarily locked due to stability issues and will be available to be changed in the next update. Sorry about this!', 'info', '๐Ÿ˜ข', '15000'); // default toggle values const defaultSettings = { compactPrivateServers: true, onlyYourPrivateServers: false, privateServerSearch: false }; // load saved settings and fall back to defaults const savedSettings = JSON.parse( localStorage.getItem('ROLOCATE_editprivateserversettings') || '{}' ); const settings = { ...defaultSettings, ...savedSettings }; // Force compact to always be true settings.compactPrivateServers = true; // Force privateServerSearch to always be false settings.privateServerSearch = false; // dark background overlay const overlay = document.createElement('div'); overlay.id = 'rolocate-ps-settings-modal'; overlay.style.cssText = ` position:fixed;inset:0;display:flex;justify-content:center;align-items:center; background:rgba(0,0,0,.45);z-index:10000;opacity:0;transition:.2s; `; // main modal box const modal = document.createElement('div'); modal.style.cssText = ` background:#181818;border-radius:14px;padding:18px;width:340px;max-width:92vw; color:#fff;border:1px solid #2f2f2f;box-shadow:0 10px 30px rgba(0,0,0,.6); transform:scale(.96) translateY(12px);transition:.2s; `; // title + subtitle modal.innerHTML = `

    Private Server Settings

    Change Settings for Private Servers

    `; // toggle definitions const toggleOptions = [ ['compactPrivateServers', 'Compact Private Servers'], ['onlyYourPrivateServers', 'Only Your Private Servers'], ['privateServerSearch', 'Private Server Search'] ]; // container for all toggles const togglesContainer = document.createElement('div'); togglesContainer.style.cssText = ` background:#222;padding:10px;border-radius:10px;display:grid;gap:8px; `; // store toggle elements for mutual exclusion logic const toggleElements = {}; // build each toggle row toggleOptions.forEach(([key, label]) => { const isDisabled = key === 'compactPrivateServers' || key === 'privateServerSearch'; const row = document.createElement('label'); row.style.cssText = ` display:flex;justify-content:space-between;align-items:center; padding:8px 10px;border-radius:8px; cursor:${isDisabled ? 'not-allowed' : 'pointer'}; transition:.15s;background:${isDisabled ? '#1a1a1a' : '#262626'}; opacity:${isDisabled ? '0.6' : '1'}; `; // hover effect (only for enabled toggles) if (!isDisabled) { row.onmouseenter = () => (row.style.background = '#2d2d2d'); row.onmouseleave = () => (row.style.background = '#262626'); } row.innerHTML = ` ${label}
    `; const checkbox = row.querySelector('input'); const toggle = row.querySelector('.tgl'); const knob = toggle.querySelector('div'); // store references toggleElements[key] = { checkbox, toggle, knob }; // handle toggle click (skip if disabled) if (!isDisabled) { row.onclick = (e) => { e.preventDefault(); const willBeChecked = !checkbox.checked; // update current toggle checkbox.checked = willBeChecked; toggle.style.background = checkbox.checked ? '#16a34a' : '#444'; knob.style.left = checkbox.checked ? '18px' : '2px'; }; } togglesContainer.appendChild(row); }); // buttons container const buttonRow = document.createElement('div'); buttonRow.style.cssText = ` display:flex;justify-content:flex-end;gap:8px;margin-top:14px; `; // reusable button factory const createButton = (text, bgColor, onClick) => { const button = document.createElement('button'); button.textContent = text; button.style.cssText = ` padding:8px 14px;border-radius:8px;border:1px solid ${bgColor}; background:${bgColor};color:#fff;font-size:13px;cursor:pointer; transition:.15s; `; button.onmouseenter = () => (button.style.opacity = 0.85); button.onmouseleave = () => (button.style.opacity = 1); button.onclick = onClick; return button; }; // close animation + cleanup const closeModal = () => { modal.style.transform = 'scale(.96) translateY(12px)'; overlay.style.opacity = '0'; setTimeout(() => overlay.remove(), 200); }; // add buttons buttonRow.append( createButton('Cancel', '#333', closeModal), createButton('Save', '#16a34a', () => { const newSettings = {}; toggleOptions.forEach(([key]) => { // Force compact to always be true and privateServerSearch to always be false if (key === 'compactPrivateServers') { newSettings[key] = true; } else if (key === 'privateServerSearch') { newSettings[key] = false; } else { newSettings[key] = document.getElementById(key).checked; } }); localStorage.setItem( 'ROLOCATE_editprivateserversettings', JSON.stringify(newSettings) ); // feedback stuff ConsoleLogEnabled('Private server settings saved:', newSettings); notifications('Settings saved', 'success', '๐Ÿ‘', '5000'); closeModal(); }) ); // assemble modal modal.append(togglesContainer, buttonRow); overlay.append(modal); document.body.append(overlay); // animate in requestAnimationFrame(() => { overlay.style.opacity = '1'; modal.style.transform = 'scale(1) translateY(0)'; }); } /******************************************************* applycustombackgrounds(): applies user background and handles transparency *******************************************************/ async function applycustombackgrounds() { // stupid storage helper const getFile = k => { let d = (typeof GM_getValue != 'undefined') ? GM_getValue(`ROLOCATE_FILE_${k}`) : localStorage.getItem(`ROLOCATE_FILE_${k}`); return d ? JSON.parse(d) : null; }; if (localStorage.getItem("ROLOCATE_custombackgrounds") !== "true") return; // this was so painful const useVid = localStorage.getItem('ROLOCATE_CUSTOMBACKGROUND_use_animated') === 'true'; const vidURL = localStorage.getItem('ROLOCATE_CUSTOMBACKGROUND_video_url') || ''; const txtColor = localStorage.getItem('ROLOCATE_CUSTOMBACKGROUND_text_color') || ''; const overrideTxt = localStorage.getItem('ROLOCATE_CUSTOMBACKGROUND_override_text_color') === 'true'; const adv = localStorage.getItem('ROLOCATE_CUSTOMBACKGROUND_use_advanced') === 'true'; // clear leftovers document.querySelectorAll('video[custom-bg], img[custom-bg], #custom-ui-style').forEach(e => e.remove()); let hasBG = false; const el = document.createElement(useVid ? 'video' : 'img'); el.setAttribute('custom-bg', ''); el.style.cssText = `position:fixed;top:0;left:0;width:100vw;height:100vh;object-fit:cover;z-index:-9999;pointer-events:none;`; if (useVid) Object.assign(el, { muted: true, loop: true, playsInline: true }); const file = getFile(useVid ? 'video' : 'image'); if (file?.data) { el.src = file.data; hasBG = true; } else if (useVid && vidURL) { el.src = vidURL; hasBG = true; } if (hasBG) { if (useVid) el.play().catch(() => {}); document.documentElement.appendChild(el); } // using the variable css isnt a good idea but idc let css = hasBG ? `html,body,.content{background:transparent!important}` : ''; // advanced panel if (adv) { const def = 'rgba(45,45,45,0.85)'; const map = [ ['.profile-avatar-mask', 'avatar-mask'], ['.chat-body', 'chat-body'], ['.dropdown-menu', 'dropdown-menu'], ['.container-footer', 'footer'] ]; map.forEach(([sel, key]) => { const c = localStorage.getItem(`ROLOCATE_CUSTOMBACKGROUND_style_${key}`) || def; css += `${sel}{background-color:${c}!important}`; }); } if (overrideTxt && txtColor) css += `body,body *{color:${txtColor}!important}`; const style = document.createElement('style'); style.id = 'custom-ui-style'; style.textContent = css; document.head.appendChild(style); // trasnaprent backgroudn const targets = [ '.rolocate-greeting-header', '.best-friends-section', '.friend-carousel-container', '.ROLOCATE_QUICKLAUNCHGAMES_new-games-container' ]; const tStyle = document.createElement('style'); tStyle.id = 'rolocate-transparency-style'; document.head.appendChild(tStyle); const applyTransparent = s => { if (!tStyle.textContent.includes(s)) tStyle.textContent += `${s}{background:transparent!important;box-shadow:none!important;border-color:transparent!important}`; document.querySelectorAll(s).forEach(e => { Object.assign(e.style, { background: 'transparent', backgroundColor: 'transparent', boxShadow: 'none', borderColor: 'transparent' }); }); }; const maybeTransparent = () => { const bodyBg = getComputedStyle(document.body).backgroundColor; if (bodyBg === 'rgba(0, 0, 0, 0)' || bodyBg === 'transparent') targets.forEach(applyTransparent); else tStyle.textContent = ''; }; maybeTransparent(); // initial check // wont even comment new MutationObserver(maybeTransparent).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); } /******************************************************* name of function: showSettingsPopup_background description: menu to manage the custom backgrounds *******************************************************/ function showSettingsPopup_background() { notifications('Uh maybe i will work on this later but its kinda hard to update this', 'info', 'โ„น๏ธ', 4000); // uh the file helper const fileToBase64 = (file) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(file); }); const saveFile = async (key, file) => { // validate file type const validImageTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/bmp']; // yea gif is considered an image. const validVideoTypes = ['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime']; const isImage = validImageTypes.includes(file.type); const isVideo = validVideoTypes.includes(file.type); if ((key === 'image' && !isImage) || (key === 'video' && !isVideo)) { notifications(`Invalid file type: ${file.type}. Please upload a valid ${key === 'image' ? 'image (JPG, PNG, GIF, WebP)' : 'video (MP4, WebM, OGG)'} file.`, 'error', 'โš ๏ธ', 8000); return; } const SOFT_LIMIT = 5 * 1024 * 1024; // 5MB - warning const HARD_LIMIT = 20 * 1024 * 1024; // 20MB - blocked if (file.size > HARD_LIMIT) { notifications(`File exceeds hard limit of ${(HARD_LIMIT / 1024 / 1024).toFixed(0)}MB (${(file.size / 1024 / 1024).toFixed(2)}MB). Please use a direct URL for larger files.`, 'error', 'โš ๏ธ', 8000); return; } if (file.size > SOFT_LIMIT) { notifications(`Warning: File size is ${(file.size / 1024 / 1024).toFixed(2)}MB. Files over ${(SOFT_LIMIT / 1024 / 1024).toFixed(0)}MB may cause lag or slow page loads. Consider using a direct URL instead.`, 'warning', 'โš ๏ธ', 10000); } if (typeof GM_setValue === 'undefined') { notifications('Userscript storage (GM_setValue) not available. Cannot save file.', 'error', 'โš ๏ธ', 8000); return; } const base64Data = await fileToBase64(file); const fileData = { name: file.name, size: file.size, type: file.type, data: base64Data }; GM_setValue(`ROLOCATE_FILE_${key}`, JSON.stringify(fileData)); }; const getFile = (key) => { const storage = typeof GM_getValue !== 'undefined' ? GM_getValue : (k) => localStorage.getItem(k); const data = storage(`ROLOCATE_FILE_${key}`, null); if (!data) return null; try { return JSON.parse(data); } catch { return null; } }; const deleteFile = (key) => { const del = typeof GM_deleteValue !== 'undefined' ? GM_deleteValue : (k) => localStorage.removeItem(k); del(`ROLOCATE_FILE_${key}`); }; // cleanup stuff document.getElementById('rolocate-settings-popup')?.remove(); const style = document.createElement('style'); // css smaller to save space style.textContent = ` @keyframes rFadeIn{from{opacity:0}to{opacity:1}} @keyframes rSlideIn{from{opacity:0;transform:translate(-50%,-48%)scale(.96)}to{opacity:1;transform:translate(-50%,-50%)scale(1)}} @keyframes rSlideTab{from{opacity:0;transform:translateX(-10px)}to{opacity:1;transform:translateX(0)}} .r-toggle{position:relative;display:inline-block;width:44px;height:24px;vertical-align:middle} .r-toggle input{opacity:0;width:0;height:0} .r-slider{position:absolute;cursor:pointer;inset:0;background:#3d3d3d;transition:.25s;border-radius:24px} .r-slider:before{content:"";position:absolute;height:18px;width:18px;left:3px;bottom:3px;background:#8a8a8a;transition:.25s;border-radius:50%} input:checked+.r-slider{background:#2f4f3f} input:checked+.r-slider:before{background:#5fb589;transform:translateX(20px)} .r-input{width:100%;padding:10px;background:#2a2a2a;border:1px solid #3d3d3d;border-radius:6px;color:#d0d0d0;font-size:13px;transition:.2s;box-sizing:border-box} .r-input:focus{outline:none;border-color:#5fb589;background:#2f2f2f} .r-card{background:#242424;border-radius:10px;padding:16px;margin:0 0 12px;border:1px solid #323232;transition:border-color .2s} .r-card:hover{border-color:#3d3d3d} .r-card-title{color:#e0e0e0;margin:0 0 12px;font-size:14px;font-weight:600;display:flex;align-items:center;gap:8px} .r-label{display:flex;align-items:center;justify-content:space-between;margin:0;padding:10px 0} .r-label-text{font-size:13px;color:#c0c0c0;line-height:1.4} .r-helper{font-size:11px;color:#808080;margin:6px 0 0;line-height:1.5} .r-upload-zone{margin-top:10px;padding:20px;background:#1e1e1e;border:2px dashed #3d3d3d;border-radius:8px;text-align:center;cursor:pointer;transition:.2s} .r-upload-zone:hover{border-color:#5fb589;background:#232323} .r-file-preview{margin-top:12px;padding:12px;background:#1e1e1e;border:1px solid #3d3d3d;border-radius:8px;display:flex;align-items:center;justify-content:space-between} .r-file-info{display:flex;align-items:center;gap:10px;color:#c0c0c0;font-size:12px} .r-remove-btn{padding:6px 12px;background:#3d2a2a;color:#ff9999;border:1px solid #4d3535;border-radius:5px;cursor:pointer;font-size:11px;font-weight:500;transition:.15s} .r-remove-btn:hover{background:#4d3535;color:#ffb0b0} .r-tabs{display:flex;gap:6px;margin-bottom:12px;background:#1e1e1e;padding:5px;border-radius:8px} .r-tab{flex:1;padding:8px 12px;background:transparent;color:#808080;border:none;border-radius:5px;cursor:pointer;font-size:12px;font-weight:500;transition:.2s} .r-tab:hover{background:#242424;color:#b5b5b5} .r-tab.active{background:#2f4f3f;color:#5fb589} .r-tab-content{display:none} .r-tab-content.active{display:block;animation:rSlideTab .3s ease-out} .r-adv-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px;margin-top:10px} .r-adv-item{padding:10px;background:#1e1e1e;border:1px solid #2a2a2a;border-radius:6px} .r-adv-label{display:block;color:#999;font-size:10px;margin-bottom:5px;font-weight:500;text-transform:uppercase;letter-spacing:.5px} `; document.head.appendChild(style); // overlay const overlay = Object.assign(document.createElement('div'), { id: 'rolocate-settings-overlay', style: 'position:fixed;inset:0;background:rgba(0,0,0,.3);z-index:9999998;animation:rFadeIn .2s' }); // popup const popup = Object.assign(document.createElement('div'), { id: 'rolocate-settings-popup', style: 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#1a1a1a;color:#d0d0d0;border:1px solid #2a2a2a;border-radius:14px;width:94%;max-width:520px;max-height:85vh;overflow:hidden;z-index:9999999;box-shadow:0 24px 48px rgba(0,0,0,.8);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;font-size:13px;animation:rSlideIn .3s cubic-bezier(.16,1,.3,1)' }); // the header const header = Object.assign(document.createElement('div'), { innerHTML: `
    ๐ŸŽจ

    Custom Backgrounds

    `, style: 'background:linear-gradient(135deg,#2a2a2a,#1e1e1e);padding:18px 20px;border-bottom:1px solid #2a2a2a;display:flex;align-items:center;justify-content:space-between' }); const closeBtn = Object.assign(document.createElement('button'), { innerHTML: 'โœ•', style: 'background:#2a2a2a;color:#999;border:none;width:32px;height:32px;font-size:16px;cursor:pointer;border-radius:7px;transition:.15s', onmouseover() { this.style.cssText += 'background:#3d3d3d;color:#fff' }, onmouseout() { this.style.cssText = this.style.cssText.replace(/background:#3d3d3d;color:#fff/, 'background:#2a2a2a;color:#999') }, onclick() { popup.style.animation = 'rSlideIn .2s reverse'; overlay.style.animation = 'rFadeIn .2s reverse'; setTimeout(() => { overlay.remove(); popup.remove(); }, 200); } }); header.appendChild(closeBtn); popup.appendChild(header); // content const content = Object.assign(document.createElement('div'), { style: 'padding:18px 20px;overflow-y:auto;max-height:calc(85vh - 160px)' }); // the tabs ooooo const tabs = Object.assign(document.createElement('div'), { className: 'r-tabs', innerHTML: ` ` }); content.appendChild(tabs); // get localStorage helper const localstoragegetternator = (key, def = '') => localStorage.getItem(`ROLOCATE_CUSTOMBACKGROUND_${key}`) || def; // basictav const basicTab = Object.assign(document.createElement('div'), { className: 'r-tab-content active', innerHTML: `

    ๐ŸŽฌ Background Type

    Choose between a video or an image

    ๐Ÿ“น Video Background

    Enter a direct URL to an MP4 video file, or upload your own below

    ๐Ÿ“ค
    Upload Video File
    Click to browse โ€ข Max 5MB โ€ข MP4, WebM

    ๐Ÿ–ผ๏ธ Image Background

    Upload a static image to use as your background

    ๐Ÿ“ค
    Upload Image File
    Click to browse โ€ข Max 5MB โ€ข JPG, PNG, GIF
    ` }); content.appendChild(basicTab); // appearance tab const appearanceTab = Object.assign(document.createElement('div'), { className: 'r-tab-content', innerHTML: `

    ๐ŸŽจ Text Color

    Preview

    This will change all text on the page to your selected color

    ` }); content.appendChild(appearanceTab); // the advanced tav const advancedTab = Object.assign(document.createElement('div'), { className: 'r-tab-content', innerHTML: `

    โš™๏ธ Advanced Settings

    These styles are not documented correctly yet. So yea advanced users only.

    ` }); content.appendChild(advancedTab); popup.appendChild(content); // the footor of the popup const footer = Object.assign(document.createElement('div'), { style: 'padding:16px 20px;background:#1e1e1e;border-top:1px solid #2a2a2a;display:flex;justify-content:space-between;gap:10px' }); const resetBtn = Object.assign(document.createElement('button'), { textContent: '๐Ÿ”„ Reset All', style: 'padding:10px 18px;background:#2a2a2a;color:#b5b5b5;border:1px solid #3d3d3d;border-radius:7px;cursor:pointer;font-weight:500;font-size:13px;transition:.15s', onmouseover() { this.style.cssText += ';background:#3d3d3d;color:#fff' }, onmouseout() { this.style.cssText = this.style.cssText.replace(/;background:#3d3d3d;color:#fff/, '') } }); const saveBtn = Object.assign(document.createElement('button'), { textContent: 'โœ“ Save Changes', style: 'padding:10px 26px;background:linear-gradient(135deg,#5fb589,#2f4f3f);color:#fff;border:none;border-radius:7px;cursor:pointer;font-weight:600;font-size:13px;transition:.15s;box-shadow:0 3px 10px rgba(95,181,137,.3)', onmouseover() { this.style.transform = 'translateY(-2px)'; this.style.boxShadow = '0 5px 14px rgba(95,181,137,.4)' }, onmouseout() { this.style.transform = 'translateY(0)'; this.style.boxShadow = '0 3px 10px rgba(95,181,137,.3)' } }); footer.append(resetBtn, saveBtn); popup.appendChild(footer); // uhhhhhh the elemnt refreenrencessss const $ = (sel) => popup.querySelector(sel); const useAnimated = $('#use-animated'); const videoSection = $('#video-section'); const imageSection = $('#image-section'); const videoUrl = $('#video-url'); const overrideTextColor = $('#override-text-color'); const textColorGroup = $('#text-color-group'); const textColorInput = $('#text-color'); const textColorHex = $('#text-color-hex'); const textColorPreview = $('#text-color-preview'); const useAdvanced = $('#use-advanced'); const advancedGrid = $('#advanced-grid'); // the file upload const updateFilePreview = (type) => { const fileData = getFile(type); const preview = document.querySelector(`#${type}-file-preview`); if (fileData) { preview.innerHTML = `
    ${type === 'video' ? '๐Ÿ“น' : '๐Ÿ–ผ๏ธ'}
    ${(fileData.size / 1024 / 1024).toFixed(2)} MB
    `; // no attakcs here preview.querySelector('.r-filename').textContent = fileData.name; preview.style.display = 'block'; preview.querySelector('.r-remove-btn').onclick = () => { deleteFile(type); preview.style.display = 'none'; preview.innerHTML = ''; document.querySelector(`#${type}-file-input`).value = ''; }; } }; const setupFileUpload = (type) => { const input = $(`#${type}-file-input`); const zone = $(`#${type}-upload-zone`); zone.onclick = () => input.click(); input.onchange = async (e) => { const file = e.target.files[0]; if (file) { try { await saveFile(type, file); updateFilePreview(type); } catch (err) { alert(err.message); input.value = ''; } } }; }; setupFileUpload('video'); setupFileUpload('image'); // this does tab switching stuff popup.querySelectorAll('.r-tab').forEach(tab => { tab.onclick = () => { popup.querySelectorAll('.r-tab').forEach(t => t.classList.remove('active')); popup.querySelectorAll('.r-tab-content').forEach(tc => tc.classList.remove('active')); tab.classList.add('active'); $(`.r-tab-content:nth-child(${Array.from(tab.parentNode.children).indexOf(tab) + 2})`).classList.add('active'); }; }); // update the visibiltiyiedsadasdasdjkahgdiakhgdikagsdJ const updateVisibility = () => { videoSection.style.display = useAnimated.checked ? 'block' : 'none'; imageSection.style.display = useAnimated.checked ? 'none' : 'block'; textColorGroup.style.opacity = overrideTextColor.checked ? '1' : '.3'; textColorGroup.style.pointerEvents = overrideTextColor.checked ? 'auto' : 'none'; textColorPreview.style.color = textColorInput.value; advancedGrid.style.display = useAdvanced.checked ? 'grid' : 'none'; }; useAnimated.addEventListener('change', updateVisibility); overrideTextColor.addEventListener('change', updateVisibility); useAdvanced.addEventListener('change', updateVisibility); textColorInput.addEventListener('input', () => { textColorHex.value = textColorInput.value; textColorPreview.style.color = textColorInput.value; }); textColorHex.addEventListener('input', () => { if (/^#[0-9A-F]{6}$/i.test(textColorHex.value)) { textColorInput.value = textColorHex.value; textColorPreview.style.color = textColorHex.value; } }); updateVisibility(); // bro finding all of these stiles on the roblox website was a pain const styleMap = { 'profile-header': 'Profile Header', 'tabs-nav': 'Navigation Tabs', 'avatar-mask': 'Avatar Container', 'collections': 'Collections', 'switcher': 'Switcher', 'stats-panel': 'Statistics', 'search-input': 'Search Input', 'item-details': 'Item Details', 'comment-section': 'Comments', 'charts-container': 'Charts', 'vertical-menu': 'Vertical Menu', 'store-card-footer': 'Store Footer', 'submenu': 'Submenu', 'chat-body': 'Chat Body', 'dropdown-menu': 'Dropdown', 'select-group': 'Select Group', 'game-stats': 'Game Stats', 'server-banner': 'Server Banner', 'badge-rows': 'Badge Rows', 'security-container': 'Security Settings', 'security-desc': 'Security Desc', 'footer': 'Footer' }; // random defaults const defaultStyles = { 'profile-header': 'rgba(40,40,40,0.85)', 'tabs-nav': 'rgba(50,50,50,0.85)', 'avatar-mask': 'rgba(45,45,45,0.85)', 'collections': 'rgba(40,40,40,0.85)', 'switcher': 'rgba(50,50,50,0.85)', 'stats-panel': 'rgba(45,45,45,0.85)', 'search-input': 'rgba(40,40,40,0.85)', 'item-details': 'rgba(50,50,50,0.85)', 'comment-section': 'rgba(45,45,45,0.85)', 'charts-container': 'rgba(40,40,40,0.85)', 'vertical-menu': 'rgba(50,50,50,0.85)', 'store-card-footer': 'rgba(45,45,45,0.85)', 'submenu': 'rgba(40,40,40,0.85)', 'chat-body': 'rgba(50,50,50,0.85)', 'dropdown-menu': 'rgba(45,45,45,0.85)', 'select-group': 'rgba(40,40,40,0.85)', 'game-stats': 'rgba(50,50,50,0.85)', 'server-banner': 'rgba(45,45,45,0.85)', 'badge-rows': 'rgba(40,40,40,0.85)', 'security-container': 'rgba(50,50,50,0.85)', 'security-desc': 'rgba(45,45,45,0.85)', 'footer': 'rgba(40,40,40,0.85)' }; Object.entries(styleMap).forEach(([key, label]) => { const saved = sanitizeCssValue(localstoragegetternator(`style_${key}`, defaultStyles[key])); advancedGrid.innerHTML += `
    `; }); // save button saveBtn.onclick = () => { localStorage.setItem('ROLOCATE_CUSTOMBACKGROUND_use_animated', useAnimated.checked); localStorage.setItem('ROLOCATE_CUSTOMBACKGROUND_video_url', videoUrl.value.trim()); localStorage.setItem('ROLOCATE_CUSTOMBACKGROUND_override_text_color', overrideTextColor.checked); localStorage.setItem('ROLOCATE_CUSTOMBACKGROUND_text_color', textColorInput.value); localStorage.setItem('ROLOCATE_CUSTOMBACKGROUND_use_advanced', useAdvanced.checked); if (useAdvanced.checked) { advancedGrid.querySelectorAll('input[data-key]').forEach(input => { localStorage.setItem(`ROLOCATE_CUSTOMBACKGROUND_style_${input.dataset.key}`, input.value.trim()); }); } applycustombackgrounds(); popup.style.animation = 'rSlideIn .2s reverse'; overlay.style.animation = 'rFadeIn .2s reverse'; setTimeout(() => { overlay.remove(); popup.remove(); }, 200); }; // reset button resetBtn.onclick = () => { const confirm = Object.assign(document.createElement('div'), { innerHTML: `

    โš ๏ธ Reset Everything?

    This will restore all settings to defaults and delete all uploaded files. This cannot be undone.

    `, style: 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#1e1e1e;border:1px solid #3d3d3d;border-radius:10px;padding:24px;z-index:99999999;box-shadow:0 20px 50px rgba(0,0,0,.9);animation:rSlideIn .2s;min-width:320px' }); document.body.appendChild(confirm); const cancel = confirm.querySelector('#cancel-reset'); const confirmBtn = confirm.querySelector('#confirm-reset'); cancel.onmouseover = () => { cancel.style.background = '#3d3d3d'; cancel.style.color = '#fff'; }; cancel.onmouseout = () => { cancel.style.background = '#2a2a2a'; cancel.style.color = '#b5b5b5'; }; confirmBtn.onmouseover = () => confirmBtn.style.background = '#6d4545'; confirmBtn.onmouseout = () => confirmBtn.style.background = '#5a3838'; cancel.onclick = () => confirm.remove(); confirmBtn.onclick = () => { Object.keys(localStorage).forEach(k => { if (k.startsWith('ROLOCATE_CUSTOMBACKGROUND_')) localStorage.removeItem(k); }); deleteFile('video'); deleteFile('image'); confirm.remove(); location.reload(); }; }; document.body.append(overlay, popup); updateFilePreview('video'); updateFilePreview('image'); } /******************************************************* name of function: openGameQualitySettings description: opens game quality settings *******************************************************/ 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'); } } // 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() { // exit if on home page or filter disabled if (/^https?:\/\/(www\.)?roblox\.com(\/[a-z]{2})?\/home\/?$/i.test(window.location.href)) { ConsoleLogEnabled("On roblox.com/home. Gamequalityfilter Exiting function."); return; } if (localStorage.ROLOCATE_gamequalityfilter !== "true") return; 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 { 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(); 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 // plz no memoryt leak const intervalId = setInterval(() => { try { filterAllCards(); } catch (err) { ConsoleLogEnabled('[ROLOCATE] Filter error:', err); } }, 1000); const observer = new MutationObserver(() => { filterAllCards(); }); observer.observe(document.body, { childList: true, subtree: true }); } /*************************************************************** * name of function: showOldRobloxGreeting * description: shows old roblox greeting if setting is turned on ****************************************************************/ async function showOldRobloxGreeting() { const implementation = async () => { ConsoleLogEnabled("Function showOldRobloxGreeting() started."); // if we are on homepage if (!/^https?:\/\/(www\.)?roblox\.com(\/[a-z]{2})?\/home\/?$/i.test(window.location.href)) { ConsoleLogEnabled("Not on roblox.com/home. Exiting function."); return; } if (localStorage.getItem("ROLOCATE_ShowOldGreeting") !== "true") { ConsoleLogEnabled("ShowOldGreeting is disabled. Exiting function."); return; } // wait for apge to laod await new Promise(r => setTimeout(r, 500)); // 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 { // elements needed 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}`); const styleId = 'rolocate-greeting-styles'; if (!document.getElementById(styleId)) { const styleTag = document.createElement("style"); styleTag.id = styleId; styleTag.textContent = ` .rolocate-greeting-header { display: flex; align-items: center; margin-bottom: 16px; padding: 30px; background: ${isDarkMode() ? '#1a1c23' : '#E0D8CC'}; border-radius: 12px; border: 1px solid ${isDarkMode() ? '#2a2a30' : '#C1B19A'}; min-height: 180px; } .rolocate-profile-frame { width: 140px; height: 140px; border-radius: 50%; overflow: hidden; border: 3px solid ${isDarkMode() ? '#2a2a30' : '#C1B19A'}; } .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: ${isDarkMode() ? '#ffffff' : 'black'}; margin: 0; font-family: 'Segoe UI', Roboto, sans-serif; } `; document.head.appendChild(styleTag); } // header creation const headerContainer = document.createElement("div"); headerContainer.className = "rolocate-greeting-header"; // make profile 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); // make greeting 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); // mix them headerContainer.appendChild(profileFrame); headerContainer.appendChild(userDetails); homeContainer.replaceWith(headerContainer); ConsoleLogEnabled("Greeting header created successfully."); } catch (error) { ConsoleLogEnabled(`Error creating greeting: ${error.message}`); } }; // add them 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; // 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(); } // if on games or discover pages do gamequalityfilter if (/roblox\.com(\/[a-z]{2})?\/(games(\/.*)?|discover(\/.*)?)\/?$/.test(currentUrl)) { ConsoleLogEnabled("on games or discover page"); qualityfilterRobloxGames(); } } }; // 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: validateManualMode description: Check if user set their location manually or if it is still in automatic. Some error handling also *******************************************************/ // why tf did i put this all the way down here function validateManualMode() { // if manual mode if (localStorage.getItem("ROLOCATE_prioritylocation") === "manual") { ConsoleLogEnabled("Manual mode detected"); try { // get cooridnates const coords = JSON.parse(GM_getValue("ROLOCATE_coordinates", '{"lat":"","lng":""}')); ConsoleLogEnabled("Coordinates fetched:", coords); // if coordiates are missing switch to auatomcait if (!coords.lat || !coords.lng) { localStorage.setItem("ROLOCATE_prioritylocation", "automatic"); ConsoleLogEnabled("No coordinates set. Switched to automatic mode."); return true; } } catch (error) { ConsoleLogEnabled("Error checking coordinates:", error); // if error swithc to automatic localStorage.setItem("ROLOCATE_prioritylocation", "automatic"); ConsoleLogEnabled("Error encountered while fetching coordinates. Switched to automatic mode."); return true; } } ConsoleLogEnabled("No Errors detected with manual mode."); return false; } /******************************************************* 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. optimized version with minimal API calls. *******************************************************/ 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; // store for spedup let localAvatarCache = {}; // function to fetch user details in batch (up to 100 at once) const fetchUserDetailsBatch = (userIds) => { if (userIds.length === 0) return Promise.resolve([]); const url = `https://users.roblox.com/v1/users`; return new Promise((resolve) => { GM_xmlhttpRequest({ method: "POST", url, headers: { "Content-Type": "application/json" }, data: JSON.stringify({ userIds: userIds.slice(0, 100) }), onload: function(response) { if (response.status === 429) { ConsoleLogEnabled(`[fetchUserDetailsBatch] Rate limited - stopping requests`); resolve(null); return; } if (response.status >= 200 && response.status < 300) { try { const data = JSON.parse(response.responseText); resolve(data.data || []); } catch (e) { ConsoleLogEnabled(`[fetchUserDetailsBatch] Failed to parse response`, e); resolve([]); } } else { ConsoleLogEnabled(`[fetchUserDetailsBatch] Request failed with status ${response.status}`); resolve([]); } }, onerror: function(err) { ConsoleLogEnabled(`[fetchUserDetailsBatch] Network error`, err); resolve([]); } }); }); }; // function to fetch friends const gmFetchFriends = async (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 === 429) { ConsoleLogEnabled(`[gmFetchFriends] Rate limited for user ${userId}`); resolve(null); return; } 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); } }); }); }; // function to fetch user avatars in batches const fetchUserAvatars = (userIds) => { return new Promise((resolve) => { const requests = userIds.slice(0, 100).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 fetchAllAvatars = async (mutualFriends) => { if (mutualFriends.length === 0) return {}; ConsoleLogEnabled(`[fetchAllAvatars] Fetching avatars for ${mutualFriends.length} mutual friends`); const allIds = mutualFriends.map(f => f.id); const batches = []; for (let i = 0; i < allIds.length; i += 100) { batches.push(allIds.slice(i, i + 100)); } const avatarResults = await Promise.all(batches.map(batch => fetchUserAvatars(batch))); const combinedAvatars = Object.assign({}, ...avatarResults); ConsoleLogEnabled(`[fetchAllAvatars] Cached ${Object.keys(combinedAvatars).length} avatars`); return combinedAvatars; }; // 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: ${isDarkMode() ? 'linear-gradient(135deg, #111114 0%, #1a1a1d 100%)' : '#E0D8CC'}; 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: ${isDarkMode() ? 'linear-gradient(135deg, #1a1a1d 0%, #222226 100%);' : '#E0D8CC'}; 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: ${isDarkMode() ? 'white' : 'black'}; 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: ${isDarkMode() ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.1)'}; 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; padding-top: 13.5px; box-shadow: 0 4px 15px rgba(255, 107, 53, 0.2) !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.4) !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...
    `; }; // make popup for mutual frineds 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 ? `${friend.displayName || friend.name}` : '๐Ÿ‘ค'; const displayName = friend.displayName || friend.name || `User${friend.id}`; friendItem.innerHTML = `
    ${avatarUrl ? `` : '๐Ÿ‘ค'}
    `; friendItem.lastElementChild.textContent = displayName; friendItem.onclick = () => { // i dont even know how hackers and inject js in a url but whatever window.open(`https://www.roblox.com/users/${sanitizeUserId(friend.id)}/profile`, '_blank'); // yo ik this isnt really neccessary but better safe than sorry. }; 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); }; overlay.onclick = (randomvariableineed) => { if (randomvariableineed.target === overlay) { overlay.style.animation = 'fadeOut 0.2s ease-out forwards'; setTimeout(() => overlay.remove(), 200); } }; return overlay; }; // show mutal frinds 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 = 4; const friendsToShow = mutualFriends.slice(0, maxVisible); // get avatar fromcm cache const avatarMap = localAvatarCache; friendsToShow.forEach(friend => { const friendTag = document.createElement('div'); friendTag.className = 'mutual-friend-tag'; // add it to tag const avatarUrl = avatarMap[friend.id]; const displayName = friend.displayName || friend.name || `User${friend.id}`; // no attacks if (avatarUrl) { const avatarDiv = document.createElement('div'); avatarDiv.className = 'mutual-friend-avatar'; avatarDiv.style.cssText = 'width: 32px; height: 32px; margin-right: 10px; display: inline-block; vertical-align: middle;'; const img = document.createElement('img'); img.src = avatarUrl; img.alt = ''; img.style.cssText = 'width: 100%; height: 100%; border-radius: 50%; object-fit: cover;'; avatarDiv.appendChild(img); const nameSpan = document.createElement('span'); nameSpan.style.verticalAlign = 'middle'; nameSpan.textContent = escapeHtmlnoxssattackvectors(displayName); friendTag.appendChild(avatarDiv); friendTag.appendChild(nameSpan); } else { friendTag.textContent = escapeHtmlnoxssattackvectors(displayName); // no attacks } 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 = () => { return document.querySelector('ul.profile-tabs.flex'); }; // main code try { const currentUserId = getCurrentUserId(); if (!currentUserId) return; const urlMatch = window.location.pathname.match(/^\/(?:[a-z]{2}\/)?users\/(\d+)\/profile$/); 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.insertAdjacentElement('afterend', mutualFriendsElement); mutualFriendsElement.style.display = 'block'; const contentElement = mutualFriendsElement.querySelector('.mutual-friends-content'); showMutualFriendsLoading(contentElement); // Step 1: fetch both friend lists const [currentUserFriends, otherUserFriends] = await Promise.all([ gmFetchFriends(currentUserId), gmFetchFriends(otherUserId), ]); if (!currentUserFriends || !otherUserFriends) { contentElement.innerHTML = '
    Failed to load friend data
    '; return; } ConsoleLogEnabled(`[Mutual Friends] Current user has ${currentUserFriends.length} friends`); ConsoleLogEnabled(`[Mutual Friends] Other user has ${otherUserFriends.length} friends`); // Step 2: find mutual friends via comparison of id const otherFriendIds = new Set(otherUserFriends.map(f => f.id)); let mutualFriends = currentUserFriends.filter(f => otherFriendIds.has(f.id)); ConsoleLogEnabled(`[Mutual Friends] Found ${mutualFriends.length} mutual friends`); if (mutualFriends.length === 0) { await displayMutualFriends(contentElement, mutualFriends); return; } // Step 3: check if they need display anmes const friendsNeedingData = mutualFriends.filter(f => !f.name || f.name.trim() === '' || !f.displayName || f.displayName.trim() === '' ); ConsoleLogEnabled(`[Mutual Friends] ${friendsNeedingData.length} mutual friends need data fixes`); // Step 4: fetch details of the friends that are mutual friends. find dispolayname if (friendsNeedingData.length > 0) { const userIds = friendsNeedingData.map(f => f.id); const userDetails = await fetchUserDetailsBatch(userIds); if (userDetails && userDetails.length > 0) { const detailsMap = new Map(userDetails.map(u => [u.id, u])); mutualFriends = mutualFriends.map(friend => { if (friend.name && friend.displayName) return friend; const details = detailsMap.get(friend.id); return { ...friend, name: details?.name || `User${friend.id}`, displayName: details?.displayName || details?.name || `User${friend.id}` }; }); ConsoleLogEnabled(`[Mutual Friends] Successfully enriched ${userDetails.length} friend details`); } } // Step 5: fetch the avatars localAvatarCache = await fetchAllAvatars(mutualFriends); // Step 6: show it to the user await displayMutualFriends(contentElement, mutualFriends); ConsoleLogEnabled('[Mutual Friends] Feature loaded successfully'); } catch (error) { ConsoleLogEnabled('[loadmutualfriends] Error occurred:', error); } } /******************************************************* name of function: manageRobloxChatBar description: Disables 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: SmartSearch desc: smartsearch like better search with games, users, catalog, and groups *******************************************************/ function SmartSearch() { if (localStorage.ROLOCATE_smartsearch !== "true") return; // set friend list so later on in the user tab if a friend is found add friend label let friendList = [], friendIdSet = new Set(), friendListFetched = false, friendListFetching = false; // quick l;aunch on like the smart search yeeeeeee function triggerQuickLaunchUpdate(placeId, action) { // basicalyl if on homepage uopdates it liike isntanyl const event = new CustomEvent('quicklaunch-update', { detail: { placeId: placeId, action: action } }); window.dispatchEvent(event); } function isGameInQuickLaunch(placeId) { const games = JSON.parse(localStorage.getItem('ROLOCATE_quicklaunch_games_storage') || '[]'); return games.includes(placeId.toString()); } function isQuickLaunchEnabled() { return localStorage.getItem('ROLOCATE_quicklaunchgames') === 'true'; } function addToQuickLaunch(placeId) { const games = JSON.parse(localStorage.getItem('ROLOCATE_quicklaunch_games_storage') || '[]'); if (games.length >= 10) { notifications('Maximum 10 games allowed in Quick Launch', 'error', 'โš ๏ธ', '4000'); return false; } if (!games.includes(placeId.toString())) { games.push(placeId.toString()); localStorage.setItem('ROLOCATE_quicklaunch_games_storage', JSON.stringify(games)); return true; } return false; } function removeFromQuickLaunch(placeId) { const games = JSON.parse(localStorage.getItem('ROLOCATE_quicklaunch_games_storage') || '[]'); const updatedGames = games.filter(id => id !== placeId.toString()); localStorage.setItem('ROLOCATE_quicklaunch_games_storage', JSON.stringify(updatedGames)); } 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 { resolve(JSON.parse(response.responseText).data || []); } catch (e) { resolve([]); } } else resolve([]); }, onerror: function() { resolve([]); } }); }); } function hasSubstringMatch(str, query) { if (query.length < 3) return false; return str.toLowerCase().includes(query.toLowerCase()); } function chunkArray(array, size) { const chunks = []; for (let i = 0; i < array.length; i += size) chunks.push(array.slice(i, i + size)); return chunks; } // levenshteinDistance functiuon. leetcode 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]; } // custom similarity function to determine similarity function getSimilarityScore(str1, str2) { ConsoleLogEnabled("Original strings:", {str1, str2}); // no emojis yea 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 searchQuery = removeEmojisAndClean(str1); const targetText = removeEmojisAndClean(str2); ConsoleLogEnabled("Cleaned strings:", {searchQuery, targetText}); // if something includes the samething then its prob a match if (searchQuery.includes(targetText) || targetText.includes(searchQuery)) { ConsoleLogEnabled("One string includes the other."); const longerText = searchQuery.length > targetText.length ? searchQuery : targetText; const shorterText = searchQuery.length > targetText.length ? targetText : searchQuery; ConsoleLogEnabled("Longer string:", longerText); ConsoleLogEnabled("Shorter string:", shorterText); // uh increase score if it has like lengths let matchScore = 0.8 + (shorterText.length / longerText.length) * 0.15; ConsoleLogEnabled("Base score (inclusion case):", matchScore); if (searchQuery === targetText) { ConsoleLogEnabled("Exact match."); return 1.0; } const result = Math.min(0.95, matchScore); ConsoleLogEnabled("Inclusion final score:", result); return result; } // if no direct match do distance claucaltion instead const maxLen = Math.max(searchQuery.length, targetText.length); if (maxLen === 0) { ConsoleLogEnabled("uh maybe all emojis returning 1"); return 1; } const editDistance = levenshteinDistance(searchQuery, targetText); const distanceScore = 1 - (editDistance / maxLen); ConsoleLogEnabled("Levenshtein distance:", editDistance); ConsoleLogEnabled("Levenshtein score:", distanceScore); // comon chunks then yeasss const minLen = Math.min(searchQuery.length, targetText.length); let bonusPoints = 0; let bestMatch = 0; for (let i = 0; i < searchQuery.length; i++) { for (let j = 0; j < targetText.length; j++) { let matchLen = 0; while (i + matchLen < searchQuery.length && j + matchLen < targetText.length && searchQuery[i + matchLen] === targetText[j + matchLen]) { matchLen++; } if (matchLen > bestMatch) bestMatch = matchLen; } } ConsoleLogEnabled("longest matching substring length:", bestMatch); // boost if its decent ig if (bestMatch >= 3) { bonusPoints = (bestMatch / minLen) * 0.5; ConsoleLogEnabled("boosting subtring:", bonusPoints); } else { ConsoleLogEnabled("no substring boost applied"); } const finalScore = Math.min(0.95, distanceScore + bonusPoints); ConsoleLogEnabled("final similarity score:", finalScore); return finalScore; // this is the final score to rank the items } // uh like u know shorten numbers 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(); } // format dates function formatDate(dateString) { const date = new Date(dateString); const options = {year: 'numeric', month: 'short', day: 'numeric'}; return date.toLocaleDateString('en-US', options); } // find game icons in a batch 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 { resolve(JSON.parse(response.responseText).data || []); } catch (error) { resolve([]); } } else resolve([]); }, onerror: function() { resolve([]); } }); }); } // find the playerth8mbnbs in a batch 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) resolve(JSON.parse(response.responseText).data || []); else resolve([]); } catch (error) { resolve([]); } }, onerror: function() { resolve([]); } }); }); } // find group icons in a batch 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) resolve(JSON.parse(response.responseText).data || []); else resolve([]); } catch (error) { resolve([]); } }, onerror: function() { resolve([]); } }); }); } // fubd yser fruiebd for the likie smtartseafch async function fetchUserFriendCount(userId) { return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url: `https://friends.roblox.com/v1/users/${userId}/friends/count`, headers: {"Accept": "application/json"}, onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); resolve(data.count || 0); } catch (e) { resolve(0); } } else resolve(0); }, onerror: function() { resolve(0); } }); }); } // find user follower count in a batch ig async function fetchUserFollowerCount(userId) { return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url: `https://friends.roblox.com/v1/users/${userId}/followers/count`, headers: {"Accept": "application/json"}, onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); resolve(data.count || 0); } catch (e) { resolve(0); } } else resolve(0); }, onerror: function() { resolve(0); } }); }); } // get user stats like friends and followers in a bathc. calls 2 functuions async function fetchUserStatsBatch(userIds) { const statsPromises = userIds.map(async (userId) => { const [friendCount, followerCount] = await Promise.all([ fetchUserFriendCount(userId), fetchUserFollowerCount(userId) ]); return { userId, friendCount, followerCount }; }); return Promise.all(statsPromises); } // find the liike stats for the items async function fetchCatalogItemDetails(assetId) { return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url: `https://catalog.roblox.com/v1/catalog/items/${assetId}/details?itemType=Asset`, headers: {"Accept": "application/json"}, onload: function(response) { if (response.status === 200) { try { resolve(JSON.parse(response.responseText)); } catch (e) { resolve(null); } } else resolve(null); }, onerror: function() { resolve(null); } }); }); } // find thumbnail for items in batch async function fetchCatalogThumbnailsBatch(assetIds) { if (!assetIds.length) return []; const params = new URLSearchParams({assetIds: assetIds.join(","), size: "150x150", format: "png", isCircular: "false"}); const url = `https://thumbnails.roblox.com/v1/assets?${params.toString()}`; return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url: url, headers: {"Accept": "application/json"}, onload: function(response) { try { if (response.status === 200) resolve(JSON.parse(response.responseText).data || []); else resolve([]); } catch (error) { resolve([]); } }, onerror: function() { resolve([]); } }); }); } // find thumbnails in a batch async function fetchBundleThumbnailsBatch(bundleIds) { if (!bundleIds.length) return []; const params = new URLSearchParams({bundleIds: bundleIds.join(","), size: "150x150", format: "png", isCircular: "false"}); const url = `https://thumbnails.roblox.com/v1/bundles/thumbnails?${params.toString()}`; return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url: url, headers: {"Accept": "application/json"}, onload: function(response) { try { if (response.status === 200) resolve(JSON.parse(response.responseText).data || []); else resolve([]); } catch (error) { resolve([]); } }, onerror: function() { resolve([]); } }); }); } /******************************************************* search fucntionssnsnsn *******************************************************/ 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; } // the game cards in smart search yea crap thgis took ma long time contentArea.innerHTML = games.map(game => { const isInQuickLaunch = isGameInQuickLaunch(game.rootPlaceId); const quickLaunchEnabled = isQuickLaunchEnabled(); // yea im cool for making a variable to calla function return `

    ${game.name}

    Players: ${formatNumberCount(game.playerCount)} | ๐Ÿ‘ ${formatNumberCount(game.totalUpVotes)} | ๐Ÿ‘Ž ${formatNumberCount(game.totalDownVotes)}

    `}).join(''); setTimeout(() => { // Play button listeners 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`; }); }); // quick Launch button listeners document.querySelectorAll('.ROLOCATE_SMARTSEARCH_quicklaunch-button').forEach(button => { button.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); // a check to do nothing if something breaks and button isnt grayed out for some reason if (!isQuickLaunchEnabled()) { notifications('Button disabled because Quick Launch Games is Disabled.', 'error', 'โš ๏ธ', '6000'); return; // do ntohign if disabled } const placeId = this.getAttribute('data-place-id'); const isAdded = this.classList.contains('added'); // ok so basically this like shows the buttons for quicklaunchn. check amrk, x mark, the plus mark, and gray out stuff if (isAdded) { removeFromQuickLaunch(placeId); this.classList.remove('added'); this.title = 'Add to Quick Launch'; this.innerHTML = ` `; triggerQuickLaunchUpdate(placeId, 'remove'); notifications('Removed from Quick Launch Games!', 'success', '', '3000'); } else { const success = addToQuickLaunch(placeId); if (success) { this.classList.add('added'); this.title = 'Remove from Quick Launch'; this.innerHTML = ` `; triggerQuickLaunchUpdate(placeId, 'add'); notifications('Added to Quick Launch Games!', 'success', '', '3000'); } } }); }); }, 100); 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 = `${games.find(g => g.universeId == thumb.targetId)?.name || 'Game'}`; } }); } 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
    '; } } // Uhh user results for the suer thing in smartsearch 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 || []; const currentUserId = getCurrentUserId(); if (currentUserId && !friendListFetched && !friendListFetching) { friendListFetching = true; friendList = await fetchFriendList(currentUserId); friendIdSet = new Set(friendList.map(friend => friend.id)); friendListFetched = true; friendListFetching = false; } 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, hasVerifiedBadge: false, }); } }); } // wow cool programming donehere let combinedResults = [ ...apiUsers.map(user => ({...user, isFriend: friendIdSet.has(user.contentId)})), ...matchedFriends.filter(friend => !apiUsers.some(u => u.contentId === friend.contentId)) ]; combinedResults.sort((a, b) => { if (a.isFriend && !b.isFriend) return -1; if (!a.isFriend && b.isFriend) return 1; return 0; }); const users = combinedResults.slice(0, 30); if (users.length === 0) { contentArea.innerHTML = '
    No users found
    '; return; } // whats in the user cards like name, follors, verify badge, friends, and the loading stats text for stats contentArea.innerHTML = users.map(user => `
    `).join(''); // this gets the user thumbnails for smartsearch 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 = `${users.find(u => u.contentId == thumb.targetId)?.username || 'User'}`; } }); } catch (error) { ConsoleLogEnabled('Error fetching user thumbnails:', error); } } const statsBatches = chunkArray(userIds, 10); for (const batch of statsBatches) { try { const stats = await fetchUserStatsBatch(batch); stats.forEach(stat => { const statsElement = document.querySelector(`.ROLOCATE_SMARTSEARCH_user-stats[data-user-id="${stat.userId}"]`); if (statsElement) { // ok so basically this is the place for thge svg for the users friends and folowoers yea statsElement.innerHTML = ` ${formatNumberCount(stat.friendCount)} Friends  |  ${formatNumberCount(stat.followerCount)} Followers `; } }); } catch (error) { ConsoleLogEnabled('Error fetching user stats:', error); } } } else contentArea.innerHTML = '
    Error loading user results
    '; } catch (error) { ConsoleLogEnabled('Error in user search:', error); contentArea.innerHTML = '
    Error loading user results
    '; } } // reading the furnciton name should tell u what this is // ok so basically it shows members created and verified and logo and yea more stuff 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; } contentArea.innerHTML = groups.map(group => `

    ${group.name} ${group.hasVerifiedBadge ? '' : ''}

    Members: ${formatNumberCount(group.memberCount)}

    Created: ${formatDate(group.created)}

    `).join(''); const groupIds = groups.map(group => group.id); // ten thumbnail batchs at a time 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 = `${groups.find(g => g.id == thumb.targetId)?.name || 'Group'}`; } }); } 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
    '; } } async function fetchCatalogSearchResults(query) { const apiUrl = `https://catalog.roblox.com/v1/search/items?keyword=${encodeURIComponent(query)}&category=All&salesTypeFilter=1&limit=30`; contentArea.innerHTML = '
    Loading catalog items...
    '; 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 catalogItems = data.data || []; if (catalogItems.length === 0) { contentArea.innerHTML = '
    No catalog items found
    '; return; } ConsoleLogEnabled('Creating fetch promises for Asset and Bundle types...'); const detailPromises_Asset = catalogItems.slice(0, 120).map(item => fetchCatalogItemDetails(item.id)); ConsoleLogEnabled('Waiting for Asset fetches to complete...'); const detailedResults_Asset = await Promise.all(detailPromises_Asset); const itemsNeedingBundleRetry = catalogItems.slice(0, 120).filter((item, index) => detailedResults_Asset[index] === null); ConsoleLogEnabled(`Failed as Asset, retrying as Bundle for ${itemsNeedingBundleRetry.length} items:`, itemsNeedingBundleRetry.map(i => i.id)); const detailPromises_Bundle = itemsNeedingBundleRetry.map(item => new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url: `https://catalog.roblox.com/v1/catalog/items/${item.id}/details?itemType=Bundle`, headers: {"Accept": "application/json"}, onload: function(response) { if (response.status === 200) { try { resolve(JSON.parse(response.responseText)); } catch (e) { resolve(null); } } else resolve(null); }, onerror: function() { resolve(null); } }); }) ); ConsoleLogEnabled('Waiting for Bundle fetches to complete...'); const detailedResults_Bundle = await Promise.all(detailPromises_Bundle); const combinedResults = detailedResults_Asset.map((assetResult, index) => { if (assetResult !== null) return { ...assetResult, __itemType: 'Asset' }; else { const bundleIndex = itemsNeedingBundleRetry.findIndex(item => item.id === catalogItems[index].id); return bundleIndex >= 0 ? { ...detailedResults_Bundle[bundleIndex], __itemType: 'Bundle' } : null; } }); const failedIds = catalogItems.slice(0, 100).filter((item, index) => combinedResults[index] === null).map(item => item.id); ConsoleLogEnabled(`รข Failed to fetch details (Asset & Bundle) for ${failedIds.length} items:`, failedIds); ConsoleLogEnabled('Filtering out completely failed requests...'); const detailedItems = combinedResults.filter(details => details !== null); ConsoleLogEnabled(`รข Got ${detailedItems.length} valid items.`); if (detailedItems.length === 0) { contentArea.innerHTML = '
    No catalog items found
    '; return; } contentArea.innerHTML = detailedItems.map(item => `

    ${item.name}

    ${item.priceStatus === "Free" ? 'Free' : `${item.price} โฃ`} ${item.favoriteCount > 0 ? ` | ๐Ÿ‘ ${formatNumberCount(item.favoriteCount)}` : ''}

    by ${item.creatorName}

    `).join(''); const assetIds = detailedItems.filter(item => item.__itemType === 'Asset').map(item => item.id); const bundleIds = detailedItems.filter(item => item.__itemType === 'Bundle').map(item => item.id); if (assetIds.length > 0) { const assetThumbnailBatches = chunkArray(assetIds, 10); for (const batch of assetThumbnailBatches) { try { const thumbnails = await fetchCatalogThumbnailsBatch(batch); thumbnails.forEach(thumb => { const loadingElement = document.querySelector(`.ROLOCATE_SMARTSEARCH_thumbnail-loading[data-asset-id="${thumb.targetId}"]`); if (loadingElement) { loadingElement.outerHTML = `${detailedItems.find(i => i.id == thumb.targetId)?.name || 'Item'}`; } }); } catch (error) { ConsoleLogEnabled('Error fetching catalog thumbnails:', error); } } } if (bundleIds.length > 0) { const bundleThumbnailBatches = chunkArray(bundleIds, 10); for (const batch of bundleThumbnailBatches) { try { const thumbnails = await fetchBundleThumbnailsBatch(batch); thumbnails.forEach(thumb => { const loadingElement = document.querySelector(`.ROLOCATE_SMARTSEARCH_thumbnail-loading[data-asset-id="${thumb.targetId}"]`); if (loadingElement) { loadingElement.outerHTML = `${detailedItems.find(i => i.id == thumb.targetId)?.name || 'Bundle'}`; } }); } catch (error) { ConsoleLogEnabled('Error fetching bundle thumbnails:', error); } } } } else contentArea.innerHTML = '
    Error loading catalog results
    '; } catch (error) { ConsoleLogEnabled('Error in catalog search:', error); contentArea.innerHTML = '
    Error loading catalog 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; case 'catalog': url = `https://www.roblox.com/catalog?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', 'Catalog']; 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 === "Catalog") fetchCatalogSearchResults(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.
    `; else if (button.textContent === "Catalog") contentArea.innerHTML = `
    Browse the catalog for items!
    `; } }); }); 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); else if (activeTab === "Catalog" && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_catalog-card') === null && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_no-results') === null) fetchCatalogSearchResults(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.
    '; else if (activeTab === "Catalog") contentArea.innerHTML = '
    Browse the catalog for items!
    '; return; } if (activeTab === "Games") fetchGameSearchResults(query); else if (activeTab === "Users") fetchUserSearchResults(query); else if (activeTab === "Groups") fetchGroupSearchResults(query); else if (activeTab === "Catalog") fetchCatalogSearchResults(query); }, 250); }); const style = document.createElement('style'); // one day i gotta clean this up cause ik some of these styles arnt needed 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: ${isDarkMode() ? '#191a1f' : '#C1B19A'} !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: ${isDarkMode() ? '#00b2ff' : '#2c2f36'} !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 ${isDarkMode() ? '#00b2ff' : '#2c2f36'} !important; border-right: 2px solid ${isDarkMode() ? '#00b2ff' : '#2c2f36'} !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: ${isDarkMode() ? 'white' : 'black'} !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: ${isDarkMode() ? '#8a8d93' : '#75726C'} !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: 16px !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: ${isDarkMode() ? '#00b2ff' : '#C1B19A'} !important; } .ROLOCATE_SMARTSEARCH_search-dropdown-menu { position: absolute !important; top: calc(100% - 2px) !important; left: 0 !important; width: 100% !important; background-color: ${isDarkMode() ? '#191a1f' : '#E0D8CC'} !important; border-left: 2px solid ${isDarkMode() ? '#00b2ff' : '#2c2f36'} !important; border-right: 2px solid ${isDarkMode() ? '#00b2ff' : '#2c2f36'} !important; border-bottom: 2px solid ${isDarkMode() ? '#00b2ff' : '#2c2f36'} !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: ${isDarkMode() ? '#1e2025' : '#C1B19A'} !important; border-bottom: 1px solid ${isDarkMode() ? '#2c2f36' : '#2c2f36'} !important; } .ROLOCATE_SMARTSEARCH_dropdown-tab { flex: 1 !important; padding: 12px 16px !important; background: none !important; border: none !important; color: ${isDarkMode() ? '#8a8d93' : 'white'} !important; font-size: 16px !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: ${isDarkMode() ? 'white' : '#f7eddf'} !important; background-color: rgba(255, 255, 255, 0.05) !important; } .ROLOCATE_SMARTSEARCH_dropdown-tab.ROLOCATE_SMARTSEARCH_active { color: ${isDarkMode() ? '#00b2ff' : '#f7eddf'} !important; border-bottom-color: ${isDarkMode() ? '#00b2ff' : '#2c2f36'} !important; background-color: ${isDarkMode() ? 'rgba(0, 178, 255, 0.1)' : 'rgba(224, 216, 204, 0.25)'} !important; } .ROLOCATE_SMARTSEARCH_dropdown-content { padding: 10px !important; max-height: 350px !important; overflow-y: auto !important; display: block !important; } .ROLOCATE_SMARTSEARCH_content-text { color: ${isDarkMode() ? 'white' : 'black'} !important; font-size: 16px !important; text-align: center !important; } .ROLOCATE_SMARTSEARCH_content-text strong { color: ${isDarkMode() ? '#00b2ff' : '#8a7e6d'} !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: ${isDarkMode() ? '#1e2025' : '#C1B19A'} !important; border-radius: 8px; transition: background-color 0.2s ease; } .ROLOCATE_SMARTSEARCH_game-card:hover { background-color: ${isDarkMode() ? '#2c2f36' : '#b3a694'} !important; } .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: 90px !important; } /*QuickLaunchGames styles on smart search*/ .ROLOCATE_SMARTSEARCH_quicklaunch-button { position: absolute; right: 54px; top: 50%; transform: translateY(-50%); width: 36px; height: 36px; border-radius: 6px; background: rgba(93, 120, 255, 0.2); border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; z-index: 2; color: #5d78ff; } .ROLOCATE_SMARTSEARCH_quicklaunch-button:hover { background: rgba(93, 120, 255, 0.3); transform: translateY(-50%) scale(1.05); } .ROLOCATE_SMARTSEARCH_quicklaunch-button.added { background: rgba(76, 175, 80, 0.2); color: #4CAF50; } .ROLOCATE_SMARTSEARCH_quicklaunch-button.added:hover { background: rgba(244, 67, 54, 0.3); color: #f44336; } .ROLOCATE_SMARTSEARCH_quicklaunch-button svg { width: 20px; height: 20px; } /* svg switching on smartsearch quicklaunch wow ik so cool */ .ROLOCATE_SMARTSEARCH_quicklaunch-button .x-mark { display: none; position: absolute; } .ROLOCATE_SMARTSEARCH_quicklaunch-button .checkmark { display: block; } .ROLOCATE_SMARTSEARCH_quicklaunch-button.added:hover .checkmark { display: none; } .ROLOCATE_SMARTSEARCH_quicklaunch-button.added:hover .x-mark { display: block; } /* sisabled state oif thew quicklauncha dd buttons */ .ROLOCATE_SMARTSEARCH_quicklaunch-button.disabled, .ROLOCATE_SMARTSEARCH_quicklaunch-button:disabled { background: rgba(128, 128, 128, 0.2); color: #6a6e7d; opacity: 0.5; } .ROLOCATE_SMARTSEARCH_quicklaunch-button.disabled:hover, .ROLOCATE_SMARTSEARCH_quicklaunch-button:disabled:hover { background: rgba(128, 128, 128, 0.2); transform: translateY(-50%) scale(1.05); color: #6a6e7d; } .ROLOCATE_SMARTSEARCH_quicklaunch-button.disabled .checkmark, .ROLOCATE_SMARTSEARCH_quicklaunch-button:disabled .checkmark { display: block; } .ROLOCATE_SMARTSEARCH_quicklaunch-button.disabled .x-mark, .ROLOCATE_SMARTSEARCH_quicklaunch-button:disabled .x-mark { display: none; } /* game name stuff */ .ROLOCATE_SMARTSEARCH_game-name { font-size: 16px; color: #ffffff; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: calc(100% - 40px); } .ROLOCATE_SMARTSEARCH_game-stats { font-size: 16px; color: #8a8d93; margin: 2px 0 0 0; } .ROLOCATE_SMARTSEARCH_thumbs-up { color: #4caf50; } .ROLOCATE_SMARTSEARCH_thumbs-down { color: #f44336; } .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: ${isDarkMode() ? '#1e2025' : '#C1B19A'} !important; border-radius: 8px; transition: background-color 0.2s ease; } .ROLOCATE_SMARTSEARCH_user-card:hover { background-color: ${isDarkMode() ? '#2c2f36' : '#b3a694'} !important; } .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: 16px; font-weight: 500; color: #ffffff; margin: 0 0 2px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .ROLOCATE_SMARTSEARCH_user-username { font-size: 16px; color: #8a8d93; margin: 0; white-space: nowrap; overflow: visible; text-overflow: ellipsis; display: flex; align-items: center; } .ROLOCATE_SMARTSEARCH_user-stats { font-size: 14px; color: #6d717a; margin: 4px 0 0 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .ROLOCATE_SMARTSEARCH_stat-item { color: #8a8d93; } .ROLOCATE_SMARTSEARCH_stats-loading { color: #6d717a; font-style: italic; font-size: 13px; } /* 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: ${isDarkMode() ? '#1e2025' : '#C1B19A'} !important; border-radius: 8px; transition: background-color 0.2s ease; } .ROLOCATE_SMARTSEARCH_group-card:hover { background-color: ${isDarkMode() ? '#2c2f36' : '#b3a694'} !important; } .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: 16px; font-weight: 500; color: #ffffff; margin: 0 0 4px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .ROLOCATE_SMARTSEARCH_group-members { font-size: 16px; color: #8a8d93; margin: 0 0 2px 0; } .ROLOCATE_SMARTSEARCH_group-created { font-size: 16px; color: #6d717a; margin: 0; } /* Catalog card styles */ .ROLOCATE_SMARTSEARCH_catalog-card-link { display: block; text-decoration: none; color: inherit; } .ROLOCATE_SMARTSEARCH_catalog-card { display: flex; align-items: center; padding: 8px; margin: 6px 0; background-color: ${isDarkMode() ? '#1e2025' : '#C1B19A'} !important; border-radius: 8px; transition: background-color 0.2s ease; } .ROLOCATE_SMARTSEARCH_catalog-card:hover { background-color: ${isDarkMode() ? '#2c2f36' : '#b3a694'} !important; } .ROLOCATE_SMARTSEARCH_catalog-thumbnail { width: 50px; height: 50px; border-radius: 4px; margin-right: 12px; object-fit: cover; } .ROLOCATE_SMARTSEARCH_catalog-info { flex: 1; overflow: hidden; } .ROLOCATE_SMARTSEARCH_catalog-name { font-size: 16px; font-weight: 500; color: #ffffff; margin: 0 0 4px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .ROLOCATE_SMARTSEARCH_catalog-price { font-size: 16px; margin: 0 0 2px 0; } .ROLOCATE_SMARTSEARCH_catalog-creator { font-size: 16px; 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: 16px; } /* Friend badge styles */ .ROLOCATE_SMARTSEARCH_friend-badge { display: inline-block; background-color: #6b7280; color: #ffffff; font-size: 14px; font-weight: 500; padding: 2px 6px; border-radius: 4px; margin-left: 8px; vertical-align: middle; line-height: 1.2; letter-spacing: 0.025em; transform: translateY(-1px); 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 if (window.location.href.includes('/catalog')) setActiveTab('catalog'); 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 if (btn.textContent === "Catalog") contentArea.innerHTML = `
    Browse the catalog for items!
    `; } else btn.classList.remove('ROLOCATE_SMARTSEARCH_active'); }); } return true; } //fetch Universe ID from Place ID using GM_xmlhttpRequest function getUniverseIdFromPlaceId(placeId) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `https://games.roblox.com/v1/games/multiget-place-details?placeIds=${placeId}`, headers: { "Accept": "application/json" }, onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); if (Array.isArray(data) && data.length > 0 && data[0].universeId) { // Console log inside the function ConsoleLogEnabled(`Universe ID for place ${placeId}: ${data[0].universeId}`); resolve(data[0].universeId); } else { reject(new Error("Universe ID not found in response.")); } } catch (e) { reject(e); } } else { reject(new Error(`HTTP error! Status: ${response.status}`)); } }, onerror: function(err) { reject(err); } }); }); } // Fetches the game icon thumbnail URL using universeId via GM_xmlhttpRequest function getGameIconFromUniverseId(universeId) { const apiUrl = `https://thumbnails.roblox.com/v1/games/icons?universeIds=${universeId}&size=512x512&format=Png&isCircular=false&returnPolicy=PlaceHolder`; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: apiUrl, headers: { "Accept": "application/json" }, onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); if (Array.isArray(data.data) && data.data.length > 0 && data.data[0].imageUrl) { ConsoleLogEnabled(`Game icon URL for universe ${universeId}: ${data.data[0].imageUrl}`); resolve(data.data[0].imageUrl); } else { reject(new Error("Image URL not found in response.")); } } catch (err) { reject(err); } } else { reject(new Error(`HTTP error! Status: ${response.status}`)); } }, onerror: function(err) { reject(err); } }); }); } /******************************************************* 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') return; 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(); const newGamesContainer = document.createElement('div'); newGamesContainer.className = 'ROLOCATE_QUICKLAUNCHGAMES_new-games-container'; newGamesContainer.innerHTML = `
    Quick Launch Games
    Drag to reorder โ€ข Click to play
    Add Game
    `; const style = document.createElement('style'); style.textContent = ` .ROLOCATE_QUICKLAUNCHGAMES_new-games-container { background: ${isDarkMode() ? '#1a1c23' : '#E0D8CC'}; 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: 240px; background: ${isDarkMode() ? 'linear-gradient(145deg, #23252d, #1e2028)' : '#C1B19A'}; 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); 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); } .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: ${isDarkMode() ? '#d0d4e0' : 'black'}; letter-spacing: 0.3px; } .ROLOCATE_QUICKLAUNCHGAMES_game-tile { flex: 0 0 auto; width: 170px; background: ${isDarkMode() ? 'linear-gradient(145deg, #23252d, #1e2028)' : '#C1B19A'}; 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; border: 1px solid rgba(255, 255, 255, 0.05); } .ROLOCATE_QUICKLAUNCHGAMES_game-tile:hover { transform: translateY(-7px) scale(1.04); 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: ${isDarkMode() ? '#f0f2f6' : 'black'}; 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: ${isDarkMode() ? 'rgba(28, 30, 38, 0.85);' : '#C1B19A'}; 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: ${isDarkMode() ? '#b8b9bf' : 'black'}; 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)); } .ROLOCATE_QUICKLAUNCHGAMES_game-tile { cursor: grab; } .ROLOCATE_QUICKLAUNCHGAMES_game-tile.dragging { border: 2px dashed #5d78ff !important; background: rgba(93, 120, 255, 0.1) !important; transform: scale(0.95); cursor: grabbing; } .ROLOCATE_QUICKLAUNCHGAMES_game-tile.drag-over { border: 1px solid rgba(255, 255, 255, 0.05); } .ROLOCATE_QUICKLAUNCHGAMES_remove-button { position: absolute; top: 10px; right: 10px; width: 25px; height: 25px; background: ${isDarkMode() ? 'rgba(20, 22, 30, 0.85);' : '#C1B19A'}; border-radius: 8px; 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; } @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 moveTile { 0% { transform: translateY(0); } 50% { transform: translateY(-8px); } 100% { transform: translateY(0); } } .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; } .ROLOCATE_QUICKLAUNCHGAMES_game-tile.moving { animation: moveTile 0.4s ease; } .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; 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); } .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); } @keyframes fadeIn { to { opacity: 1; } } @keyframes popupIn { to { transform: scale(1); opacity: 1; } } @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; } .ROLOCATE_QUICKLAUNCHGAMES_add-tile.clicked { animation: buttonClick 0.3s ease; } @keyframes buttonClick { 0% { transform: scale(1); } 50% { transform: scale(0.95); } 100% { transform: scale(1); } } `; document.head.appendChild(style); friendsSection.parentNode.insertBefore(newGamesContainer, friendsSection.nextSibling); // dumb functions 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); resolve(data.data && data.data.length > 0 ? data.data[0] : null); } catch (e) { reject(e); } } else { reject(new Error(`HTTP ${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.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } function saveCurrentOrder() { const tiles = document.querySelectorAll('.ROLOCATE_QUICKLAUNCHGAMES_game-tile'); const order = Array.from(tiles).map(tile => tile.dataset.gameId); localStorage.setItem('ROLOCATE_quicklaunch_games_storage', JSON.stringify(order)); } 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; gameTile.innerHTML = `
    Loading...
    ๐Ÿ‘ -
    -
    `; gameGrid.insertBefore(gameTile, gameGrid.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_add-tile')); // remove button for tyhe quaklcjahcgyhin gmasjerhbvsajmn const removeBtn = gameTile.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_remove-button'); removeBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); 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); }); // the drag and drop stuff gameTile.draggable = true; gameTile.addEventListener('dragstart', (e) => { gameTile.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setDragImage(gameTile, 85, 120); // tells browser to use the actual cards as a dragging thingy e.dataTransfer.setData('text/html', gameTile.innerHTML); }); gameTile.addEventListener('dragend', (e) => { gameTile.classList.remove('dragging'); document.querySelectorAll('.ROLOCATE_QUICKLAUNCHGAMES_game-tile').forEach(tile => { tile.classList.remove('drag-over'); }); }); gameTile.addEventListener('dragover', (e) => { e.preventDefault(); const draggingTile = document.querySelector('.dragging'); if (draggingTile && draggingTile !== gameTile) { const gameGrid = document.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_game-grid'); const bounding = gameTile.getBoundingClientRect(); // lets go geometry finally becoming useful. // calcualtes the midpoint const offset = e.clientX - bounding.left; const isPastMidpoint = offset > bounding.width / 2; const allTiles = [...gameGrid.querySelectorAll('.ROLOCATE_QUICKLAUNCHGAMES_game-tile')]; const draggingIndex = allTiles.indexOf(draggingTile); const targetIndex = allTiles.indexOf(gameTile); // Ooly swap if we've actually moved past the center to prevent like a dumb flicker. this took too long to figure out a solution to if (draggingIndex < targetIndex && isPastMidpoint) { gameGrid.insertBefore(draggingTile, gameTile.nextSibling); } else if (draggingIndex > targetIndex && !isPastMidpoint) { gameGrid.insertBefore(draggingTile, gameTile); } } }); gameTile.addEventListener('dragleave', (e) => { gameTile.classList.remove('drag-over'); }); gameTile.addEventListener('drop', (e) => { e.preventDefault(); e.stopPropagation(); gameTile.classList.remove('drag-over'); saveCurrentOrder(); }); // load detials of the gmae (async () => { try { const universeId = await getUniverseIdFromPlaceId(gameId); const [iconUrl, details] = await Promise.all([ getGameIconFromUniverseId(universeId), gameDetails || getGameDetails(universeId) ]); const thumbContainer = gameTile.querySelector('.thumbnail-container'); thumbContainer.innerHTML = `${details?.name || 'Game'}`; const gameName = gameTile.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_game-name'); gameName.textContent = details?.name || 'Unknown Game'; const playerCount = gameTile.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_player-count'); const likeRatio = gameTile.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_like-ratio'); playerCount.textContent = formatNumber(details?.playing || 0); const ratio = details?.favoritedCount > 0 ? Math.min(100, Math.round((details.favoritedCount / (details.favoritedCount + details.favoritedCount * 0.1)) * 100)) : 0; likeRatio.innerHTML = `๐Ÿ‘ ${ratio}%`; } catch (err) { ConsoleLogEnabled('Game load err:', err); const gameName = gameTile.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_game-name'); gameName.textContent = 'Load Failed'; } })(); } function showAddGamePopup() { const existingGames = document.querySelectorAll('.ROLOCATE_QUICKLAUNCHGAMES_game-tile').length; if (existingGames >= 10) { notifications('Maximum 10 games allowed', 'error', 'โš ๏ธ', '4000'); return; } 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 = `

    Add New Game

    Example: roblox.com/games/17625359962/RIVALS
    OR
    Use SmartSearch to add a new game
    `; document.body.appendChild(overlay); setTimeout(() => document.getElementById('gameIdInput').focus(), 100); const cancelBtn = overlay.querySelector('.cancel'); const confirmBtn = overlay.querySelector('.confirm'); const input = document.getElementById('gameIdInput'); cancelBtn.onclick = () => { overlay.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_popup').classList.add('fade-out'); setTimeout(() => overlay.remove(), 300); }; confirmBtn.onclick = async () => { const gameId = input.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; } confirmBtn.textContent = 'Adding...'; confirmBtn.disabled = true; try { const universeId = await getUniverseIdFromPlaceId(gameId); const gameDetails = await getGameDetails(universeId); games.push(gameId); localStorage.setItem('ROLOCATE_quicklaunch_games_storage', JSON.stringify(games)); addGameTile(gameId, gameDetails); overlay.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_popup').classList.add('fade-out'); setTimeout(() => overlay.remove(), 300); } catch (error) { notifications('Error: ' + (error.message || 'Failed to add game'), 'error', 'โš ๏ธ', '4000'); confirmBtn.textContent = 'Add Game'; confirmBtn.disabled = false; } }; } function loadSavedGames() { const savedGames = JSON.parse(localStorage.getItem('ROLOCATE_quicklaunch_games_storage') || '[]'); savedGames.forEach(gameId => { addGameTile(gameId); }); } // add button const addButton = document.getElementById('ROLOCATE_QUICKLAUNCHGAMES_add-button'); addButton.addEventListener('click', showAddGamePopup); setTimeout(loadSavedGames, 100); // listen for updates from SmartSearch and then uopdate quicklaunch window.addEventListener('quicklaunch-update', function(e) { const { placeId, action } = e.detail; if (action === 'add') { // Check if already exists const existingTile = document.querySelector(`.ROLOCATE_QUICKLAUNCHGAMES_game-tile[data-game-id="${placeId}"]`); if (!existingTile) { addGameTile(placeId); } } else if (action === 'remove') { // Find and remove the tile const tileToRemove = document.querySelector(`.ROLOCATE_QUICKLAUNCHGAMES_game-tile[data-game-id="${placeId}"]`); if (tileToRemove) { tileToRemove.classList.add('removing'); setTimeout(() => { tileToRemove.remove(); }, 400); } } }); } }); 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; } // variables 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: ${isDarkMode() ? '#1a1c23' : '#C1B19A'} !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: ${isDarkMode() ? '#1a1c23' : '#E0D8CC'}; border-radius: 12px; border: 1px solid ${isDarkMode() ? '#2a2a30' : '#C1B19A'}; 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: ${isDarkMode() ? 'white' : 'black'}; font-size: 18px; font-weight: 600; margin: 0; font-family: "Source Sans Pro", Arial, sans-serif; `; headerDiv.appendChild(headerTitle); // caroskulecontioner for the frinds const carouselContainer = document.createElement('div'); carouselContainer.className = 'friends-carousel-container'; carouselContainer.style.cssText = ` background: transparent; border: none; padding: 0; margin: 0; `; // create another 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); // add before friends section so ontop of the frineds section friendsContainer.parentNode.insertBefore(bestFriendsSection, friendsContainer); // populat populateBestFriendsSection(); }; //add best frinds 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'; } }); // friends ingame are frist const bestFriendsList = allFriends .filter(friend => bestFriends.has(friend.id)) .sort((a, b) => { const aStatus = onlineStatusMap[a.id] || 'offline'; const bStatus = onlineStatusMap[b.id] || 'offline'; // priority: game > online > other (studio) > offline const priority = { 'game': 3, 'other': 2, 'online': 1, 'offline': 0 }; return priority[bStatus] - priority[aStatus]; }); if (bestFriendsList.length === 0) return; const friendIds = bestFriendsList.map(friend => friend.id); const avatarMap = await fetchUserAvatars(friendIds); bestFriendsList.forEach(friend => { const tile = createBestFriendTile(friend, avatarMap[friend.id]); const status = onlineStatusMap[friend.id] || 'offline'; // Add hover functionality only if online/offline (not ingame) if (status === 'online' || status === 'offline') { tile.classList.add('ROLOCATE_hover-enabled'); } const statusIcon = tile.querySelector('[data-testid="presence-icon"]'); if (statusIcon) { statusIcon.className = ''; statusIcon.classList.add(`icon-${status}`); const statusTitles = { 'online': 'Online', 'other': 'In Studio', // other is studio 'game': 'In Game', 'offline': 'Offline' }; statusIcon.setAttribute('title', statusTitles[status]); const statusColors = { 'online': '#00a2ff', 'other': '#f68802', 'game': '#02b757', 'offline': '#6b7280' }; statusIcon.style.background = statusColors[status]; } bestFriendsCarousel.appendChild(tile); }); setTimeout(() => applyFriendStatusStyling(), 100); } catch (error) { ConsoleLogEnabled('[populateBestFriendsSection] Error:', error); } }; // remove best friends from regular friends section const removeBestFriendsFromRegularSection = () => { const bestFriends = getBestFriends(); if (bestFriends.size === 0) return; const regularFriendsTiles = document.querySelectorAll('.friend-carousel-container:not(.best-friends-section .friends-carousel-container) .friends-carousel-tile'); regularFriendsTiles.forEach(tile => { const nameElement = tile.querySelector('.friend-name'); if (!nameElement) return; // try to find friend id from the firned ssection 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; `; // status circle thing 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 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: ${isDarkMode() ? 'white' : 'black'}; 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') || ''; // status detection stuff for friends 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 stuff 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'); // 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'; }); }); }; // function to fetch friends with fallback for missing names const gmFetchFriends = async (userId) => { const url = `https://friends.roblox.com/v1/users/${userId}/friends`; return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url, onload: async function(response) { if (response.status >= 200 && response.status < 300) { try { const data = JSON.parse(response.responseText); let friends = data.data; // check if any friends have missing names/displayNames const friendsWithMissingData = friends.filter(friend => !friend.name || !friend.displayName || friend.name === "" || friend.displayName === "" ); if (friendsWithMissingData.length > 0) { ConsoleLogEnabled(`[gmFetchFriends] Found ${friendsWithMissingData.length} friends with missing name data, fetching individual user data...`); // fetchj user data const userDataResults = await fetchUserDataWithRateLimit(friendsWithMissingData); try { // create a map for the user data const userDataMap = {}; userDataResults.forEach((userData, index) => { if (userData) { userDataMap[friendsWithMissingData[index].id] = userData; } }); // update the friends array with suer data friends = friends.map(friend => { if (userDataMap[friend.id]) { return { ...friend, name: userDataMap[friend.id].name, displayName: userDataMap[friend.id].displayName }; } return friend; }); ConsoleLogEnabled(`[gmFetchFriends] Successfully updated ${Object.keys(userDataMap).length} friends with user data`); } catch (fallbackError) { ConsoleLogEnabled(`[gmFetchFriends] Failed to fetch some individual user data:`, fallbackError); // continue with user data } } resolve(friends); } 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); } }); }); }; // function to fetch user data with rate limiting const fetchUserDataWithRateLimit = async (friendsWithMissingData) => { const results = []; const DELAY_MS = 100; // 100ms delay between requests const BATCH_SIZE = 5; // do 5 requests at a time for (let i = 0; i < friendsWithMissingData.length; i += BATCH_SIZE) { const batch = friendsWithMissingData.slice(i, i + BATCH_SIZE); // batch concurrently const batchPromises = batch.map(friend => fetchIndividualUserData(friend.id)); const batchResults = await Promise.all(batchPromises); results.push(...batchResults); // add the delay between batches except for the last batch cause like, its the end lol if (i + BATCH_SIZE < friendsWithMissingData.length) { await new Promise(resolve => setTimeout(resolve, DELAY_MS)); } } return results; }; // function to fetch individual user data const fetchIndividualUserData = (userId) => { const url = `https://users.roblox.com/v1/users/${userId}`; return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url, onload: function(response) { if (response.status >= 200 && response.status < 300) { try { const userData = JSON.parse(response.responseText); resolve({ id: userData.id, name: userData.name, displayName: userData.displayName }); } catch (e) { ConsoleLogEnabled(`[fetchIndividualUserData] Failed to parse response for user ${userId}`, e); resolve(null); } } else if (response.status === 429) { ConsoleLogEnabled(`[fetchIndividualUserData] Rate limited for user ${userId}, retrying after delay...`); // retry after a longer delay for rate limiting setTimeout(() => { fetchIndividualUserData(userId).then(resolve); }, 1000); } else { ConsoleLogEnabled(`[fetchIndividualUserData] Request failed for user ${userId} with status ${response.status}`); resolve(null); } }, onerror: function(err) { ConsoleLogEnabled(`[fetchIndividualUserData] Network error for user ${userId}`, err); resolve(null); } }); }); }; // function to fetch user avatars const fetchUserAvatars = (userIds) => { return new Promise((resolve) => { const requests = userIds.map(userId => ({ requestId: userId.toString(), targetId: userId, type: "AvatarHeadShot", size: "150x150", format: "Png", isCircular: false })); GM_xmlhttpRequest({ method: "POST", url: "https://thumbnails.roblox.com/v1/batch", headers: { "Content-Type": "application/json" }, data: JSON.stringify(requests), onload: function(response) { if (response.status >= 200 && response.status < 300) { try { const data = JSON.parse(response.responseText); const avatarMap = {}; data.data.forEach(item => { if (item.state === "Completed" && item.imageUrl) { avatarMap[item.targetId] = item.imageUrl; } }); resolve(avatarMap); } catch (e) { ConsoleLogEnabled("[fetchUserAvatars] Failed to parse response", e); resolve({}); } } else { ConsoleLogEnabled(`[fetchUserAvatars] Request failed with status ${response.status}`); resolve({}); } }, onerror: function(err) { ConsoleLogEnabled("[fetchUserAvatars] Network error", err); resolve({}); } }); }); }; // create star icon for best friends const createStarIcon = () => { const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('class', CLASSES.BEST_FRIEND_STAR); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('fill', 'currentColor'); svg.setAttribute('stroke', 'none'); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', 'M12 .587l3.668 7.568 8.332 1.151-6.064 5.828 1.48 8.279-7.416-3.967-7.417 3.967 1.481-8.279-6.064-5.828 8.332-1.151z'); svg.appendChild(path); // fade in animation setTimeout(() => { svg.classList.add('star-visible'); }, 50); return svg; }; // get best friends from localStorage const getBestFriends = () => { try { const stored = localStorage.getItem('ROLOCATE_BEST_FRIENDS_IDS'); return stored ? new Set(JSON.parse(stored)) : new Set(); } catch (e) { return new Set(); } }; // save best friends to localStorage const saveBestFriends = (bestFriends) => { localStorage.setItem('ROLOCATE_BEST_FRIENDS_IDS', JSON.stringify([...bestFriends])); }; // fetch online friends status from API const ROLOCATE_fetchOnlineFriends = async (userId) => { try { const url = `https://friends.roblox.com/v1/users/${userId}/friends/online`; const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, onload: resolve, onerror: reject }); }); if (response.status >= 200 && response.status < 300) { return JSON.parse(response.responseText).data || []; } ConsoleLogEnabled(`ROLOCATE: Online friends API error: ${response.status}`); return []; } catch (error) { ConsoleLogEnabled('ROLOCATE: Failed to fetch online friends:', error); return []; } }; // check best friends online status const ROLOCATE_checkBestFriendsStatus = async () => { const currentUserId = Roblox?.CurrentUser?.userId; if (!currentUserId) { ConsoleLogEnabled('ROLOCATE: Current user ID not available'); return; } const bestFriends = getBestFriends(); if (bestFriends.size === 0) { ConsoleLogEnabled('ROLOCATE: No best friends set'); return; } const onlineFriends = await ROLOCATE_fetchOnlineFriends(currentUserId); const onlineIds = new Set(onlineFriends.map(friend => friend.id)); bestFriends.forEach(bfId => { const friend = onlineFriends.find(f => f.id === bfId); if (friend) { const presence = friend.userPresence; if (presence.UserPresenceType === 'Online') { ConsoleLogEnabled(`ROLOCATE: Best friend ${bfId} is online (Website)`); } else if (presence.UserPresenceType === 'InGame') { ConsoleLogEnabled(`ROLOCATE: Best friend ${bfId} is in-game: ${presence.lastLocation}`); } else { // else user is in studio ConsoleLogEnabled(`ROLOCATE: Best friend ${bfId} is in-studio: ${presence.UserPresenceType}`); } } else { ConsoleLogEnabled(`ROLOCATE: Best friend ${bfId} is offline`); } }); }; const showBestFriendsPopup = async () => { const overlay = document.createElement('div'); overlay.className = 'best-friends-overlay'; const popup = document.createElement('div'); popup.className = 'best-friends-popup'; const header = document.createElement('div'); header.className = 'best-friends-popup-header'; header.innerHTML = `

    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 stuff for the ui best friewnds 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 id 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 = `${isDarkMode() ? '#1a1c23' : '#E0D8CC'}`; friendsContainerElement.style.borderRadius = '12px'; friendsContainerElement.style.border = `1px solid ${isDarkMode() ? '#1a1c23' : '#C1B19A'}`; 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(); // check if dom is ready const checkWhenReady = () => { if (Roblox?.CurrentUser?.userId) { ROLOCATE_checkBestFriendsStatus(); } else { requestAnimationFrame(checkWhenReady); } }; checkWhenReady(); return true; }; // cleanup function for observers so no memory leaks const cleanupAllObservers = () => { if (dropdownObserver) dropdownObserver.disconnect(); if (avatarObserver) avatarObserver.disconnect(); if (mainObserver) mainObserver.disconnect(); if (bestFriendsButtonObserver) bestFriendsButtonObserver.disconnect(); if (observerTimeout) clearTimeout(observerTimeout); }; // check if friends section exists const checkForFriendsSectionExistence = () => { return document.querySelector('.friend-carousel-container') || document.querySelector('.add-friends-icon-container'); }; // main execution logic if (checkForFriendsSectionExistence()) { initializeBetterFriendsFeatures(); return cleanupAllObservers; } // timeout for cleanup if friends section doesnt appear observerTimeout = setTimeout(cleanupAllObservers, 15000); // main observer for waiting for friends section mainObserver = new MutationObserver(() => { if (checkForFriendsSectionExistence()) { if (initializeBetterFriendsFeatures()) { mainObserver.disconnect(); if (observerTimeout) clearTimeout(observerTimeout); } } }); mainObserver.observe(document.body, { childList: true, subtree: true }); return cleanupAllObservers; } /******************************************************* name of function: restoreclassicterms description: restores the classic terms that roblox removed *******************************************************/ function restoreclassicterms() { // bug report fix #308650 if (window.location.pathname.toLowerCase() === '/login' || window.location.pathname.toLowerCase().match(/^\/[a-z]{2}\/login$/)) { return; } if (localStorage.getItem("ROLOCATE_restoreclassicterms") !== "true") return; // language from the page const htmlElement = document.querySelector('html'); const robloxLang = (htmlElement.getAttribute('lang') || htmlElement.getAttribute('xml:lang') || 'en').split('-')[0].toLowerCase(); const currentLang = Object.prototype.hasOwnProperty.call(classicTerms, robloxLang) ? robloxLang : 'en'; const classicTermReplacementsList = classicTerms[currentLang]; const attributesToCheckForTextContent = ["placeholder", "title", "aria-label", "alt"]; const htmlTagsToTargetForReplacement = [ "span", "div", "a", "button", "label", "input", "textarea", "h1", "h2", "h3", "li", "p" ]; function isElementInOverrideContainer(element) { // stuff that the script did not catch return !!element.closest(` .container-header.people-list-header, .server-list-container-header, .profile-header-social-count, .create-server-banner-text, .play-with-others-text, .announcement-display-body-content, .profile-header-buttons, .friends-in-server-label, .friends-carousel-display-name, .actions-btn-container, .games-list-header, .catalog-header, .chat-search-input, .select-friends-input, .content-action-utility, #user-profile-header-AddFriend `.replace(/\s+/g, '')); } function isElementInBlockedGameContext(element) { if (isElementInOverrideContainer(element)) return false; const experienceTerms = { // tf did i do here en: 'experience', fr: 'expรฉrience', es: 'experiencia' }; const currentExperienceTerm = experienceTerms[currentLang] || 'experience'; const isExperienceTerm = element.textContent && new RegExp(currentExperienceTerm, 'i').test(element.textContent); let currentElement = element; while (currentElement) { const elementIdLower = (currentElement.id || "").toLowerCase(); if (!isExperienceTerm && elementIdLower.includes("game")) return true; const classList = currentElement.classList; if (classList) { for (const className of classList) { const lowerClassName = className.toLowerCase(); // to keep safe if ( lowerClassName.includes("shopping-cart") || lowerClassName.includes("catalog-item-container") || lowerClassName.includes("catalog") || lowerClassName.includes("profile-header-details") || lowerClassName.includes("rolocate_smartsearch_") || lowerClassName.includes("avatar-card-container") || lowerClassName.includes("dialog-container") || lowerClassName.includes("friends-carousel-tile-label") || lowerClassName.includes("chat-container") || lowerClassName.includes("profile") || lowerClassName.includes("mutual-friends-container") || lowerClassName.includes("game-name") || lowerClassName.includes("settings-container") || lowerClassName.includes("text-overflow") || lowerClassName.includes("profile-about-content-text") || lowerClassName.includes("toast-message") || lowerClassName.includes("dummy-class-for-server-region-edit-so-restoreclassicterms-can-target-this") // yea im the best ikr ) { return true; } } } currentElement = currentElement.parentElement; } return false; } function replaceTextContentWithClassicTerms(textNode) { if (!textNode || textNode.nodeType !== Node.TEXT_NODE) return; let originalText = textNode.textContent; let modifiedText = originalText; for (const { from, to } of classicTermReplacementsList) { modifiedText = modifiedText.replace(from, to); } if (modifiedText !== originalText) { textNode.textContent = modifiedText; } } function processElementForTermReplacement(element) { if (!element || (!isElementInOverrideContainer(element) && isElementInBlockedGameContext(element))) return; element.childNodes.forEach(childNode => { if (childNode.nodeType === Node.TEXT_NODE) { replaceTextContentWithClassicTerms(childNode); } }); attributesToCheckForTextContent.forEach(attribute => { const attributeValue = element.getAttribute(attribute); if (attributeValue && typeof attributeValue === "string") { let updatedValue = attributeValue; for (const { from, to } of classicTermReplacementsList) { updatedValue = updatedValue.replace(from, to); } if (updatedValue !== attributeValue) { element.setAttribute(attribute, updatedValue); } } }); } function scanAndReplaceInitialPageContent() { htmlTagsToTargetForReplacement.forEach(tag => { document.querySelectorAll(tag).forEach(processElementForTermReplacement); }); } scanAndReplaceInitialPageContent(); const domChangeObserver = new MutationObserver(mutationRecords => { for (const mutation of mutationRecords) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach(addedNode => { if (addedNode.nodeType === Node.ELEMENT_NODE) { processElementForTermReplacement(addedNode); htmlTagsToTargetForReplacement.forEach(tag => { addedNode.querySelectorAll(tag).forEach(processElementForTermReplacement); }); } else if (addedNode.nodeType === Node.TEXT_NODE && addedNode.parentElement) { const parent = addedNode.parentElement; if (isElementInOverrideContainer(parent) || !isElementInBlockedGameContext(parent)) { replaceTextContentWithClassicTerms(addedNode); } } }); } else if (mutation.type === 'characterData') { const textNode = mutation.target; if (textNode.nodeType === Node.TEXT_NODE) { const parent = textNode.parentElement; if (parent && (isElementInOverrideContainer(parent) || !isElementInBlockedGameContext(parent))) { replaceTextContentWithClassicTerms(textNode); } } } else if (mutation.type === 'attributes') { const element = mutation.target; const attrName = mutation.attributeName; if (attributesToCheckForTextContent.includes(attrName)) { if (isElementInOverrideContainer(element) || !isElementInBlockedGameContext(element)) { const value = element.getAttribute(attrName); let newValue = value; for (const { from, to } of classicTermReplacementsList) { newValue = newValue.replace(from, to); } if (newValue !== value) { element.setAttribute(attrName, newValue); } } } } } }); domChangeObserver.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: attributesToCheckForTextContent }); } /******************************************************* name of function: fetchServerDetails description: Function to fetch server details so game id and job id. yea! *******************************************************/ // WARNING: Do not republish this script. Licensed for personal use only. // oneday I will change the variable names from ip to datacenters async function fetchServerDetails(gameId, jobId) { //here! const useBatching = localStorage.ROLOCATE_fastservers === "true"; if (!useBatching) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: "https://gamejoin.roblox.com/v1/join-game-instance", headers: { "Content-Type": "application/json", "User-Agent": "Roblox/WinInet", }, data: JSON.stringify({ placeId: gameId, gameId: jobId }), onload: function(response) { const json = JSON.parse(response.responseText); ConsoleLogEnabled("API Response:", json); if (json.status === 12 && json.message === 'You need to purchase access to this game before you can play.') { reject('purchase_required'); return; } if (json.status === 12 && json.message === 'Cannot join this non-root place due to join restrictions') { reject('subplace_join_restriction'); return; } if (json.status === 23 && json.message.includes('You have been banned from this experience by its creators.')) { reject('banned_by_creator'); return; } const datacenterId = json?.joinScript?.DataCenterId; if (!datacenterId) { ConsoleLogEnabled("API Response (No DataCenterId) Which means Full Server!:", json); reject(`Unable to fetch server location: Status ${json.status}`); return; } // function to get location data for a datacenter ID function getLocationData(datacenterId) { const locationId = serverRegionsByIp[datacenterId]; if (locationId && serverRegionsByIp._locations && serverRegionsByIp._locations[locationId]) { return serverRegionsByIp._locations[locationId]; } return null; } const location = getLocationData(String(datacenterId)); if (!location) { ConsoleLogEnabled("API Response (Unknown Location):", json); reject(`Unknown datacenter ID ${datacenterId}`); return; } location.placeVersion = json.joinScript.PlaceVersion; // not wrapping this in an object cause it breaks stuff. so this should do resolve(location); }, onerror: function(error) { ConsoleLogEnabled("API Request Failed:", error); reject(`Failed to fetch server details: ${error}`); }, }); }); } // Batching logic with rate limit handling const queue = fetchServerDetails._queue || []; const concurrencyLimit = 100; // this can be any value from 1 to 2000 (integer) if (!fetchServerDetails._queue) { fetchServerDetails._queue = queue; fetchServerDetails._activeCount = 0; fetchServerDetails._rateLimited = false; } return new Promise((resolve, reject) => { const makeRequest = async (gameId, jobId) => { return new Promise((innerResolve, innerReject) => { GM_xmlhttpRequest({ method: "POST", url: "https://gamejoin.roblox.com/v1/join-game-instance", headers: { "Content-Type": "application/json", "User-Agent": "Roblox/WinInet", }, data: JSON.stringify({ placeId: gameId, gameId: jobId }), onload: function(response) { const json = JSON.parse(response.responseText); ConsoleLogEnabled("API Response:", json); // Check if we got rate limited (status undefined) if (json.status === undefined) { ConsoleLogEnabled("Rate limited detected - status undefined"); innerReject('rate_limited'); return; } if (json.status === 12 && json.message === 'You need to purchase access to this game before you can play.') { innerReject('purchase_required'); return; } if (json.status === 12 && json.message === 'Cannot join this non-root place due to join restrictions') { innerReject('subplace_join_restriction'); return; } if (json.status === 23 && json.message.includes('You have been banned from this experience by its creators.')) { reject('banned_by_creator'); return; } const datacenterId = json?.joinScript?.DataCenterId; if (!datacenterId) { ConsoleLogEnabled("API Response (No DataCenterId) Which means Full Server!:", json); innerReject(`Unable to fetch server location: Status ${json.status}`); return; } // function to get location data for a datacenter ID function getLocationData(datacenterId) { const locationId = serverRegionsByIp[datacenterId]; if (locationId && serverRegionsByIp._locations && serverRegionsByIp._locations[locationId]) { return serverRegionsByIp._locations[locationId]; } return null; } const location = getLocationData(String(datacenterId)); if (!location) { ConsoleLogEnabled("API Response (Unknown Location):", json); //// this is for finding server regions //// Store unknown datacenter ID in localStorage //let unknownDatacenters = JSON.parse(localStorage.getItem("unknownDatacenters") || "[]"); //if (!unknownDatacenters.includes(datacenterId)) { // unknownDatacenters.push(datacenterId); // localStorage.setItem("unknownDatacenters", JSON.stringify(unknownDatacenters)); //} innerReject(`Unknown datacenter ID ${datacenterId}`); return; } location.placeVersion = json.joinScript.PlaceVersion; // samething innerResolve(location); }, onerror: function(error) { ConsoleLogEnabled("API Request Failed:", error); innerReject(`Failed to fetch server details: ${error}`); }, }); }); }; const task = async () => { try { fetchServerDetails._activeCount++; let result; let attempts = 0; const maxAttempts = 100; // prevent infinite loops while (attempts < maxAttempts) { try { result = await makeRequest(gameId, jobId); // if we get here, request was successful if (fetchServerDetails._rateLimited) { ConsoleLogEnabled("Rate limit cleared, resuming normal operation"); fetchServerDetails._rateLimited = false; } break; } catch (err) { if (err === 'rate_limited') { if (!fetchServerDetails._rateLimited) { ConsoleLogEnabled("Rate limited - retrying every second until cleared"); fetchServerDetails._rateLimited = true; } ConsoleLogEnabled(`Rate limit retry attempt ${attempts + 1}`); await delay(1000); // wait 1 second before retry attempts++; } else { // for other errors, don't retry throw err; } } } if (attempts >= maxAttempts) { throw new Error(`Rate limited for too long, exceeded ${maxAttempts} attempts`); } resolve(result); } catch (err) { reject(err); } finally { fetchServerDetails._activeCount--; if (queue.length > 0) { const next = queue.shift(); next(); } } }; if (fetchServerDetails._activeCount < concurrencyLimit) { task(); } else { queue.push(task); } }); } /******************************************************* name of function: 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"; // don't mind me spamming this around 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 if (error?.toString().includes("Unable to fetch server location: Status 12")) { ConsoleLogEnabled("Private Server Detected. Not adding to recentservers"); return; // exit the function early } 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: fetchUserPresence description: Fetches the current presence data for a user from the Roblox presence API. Returns: Promise that resolves to presence data or null *******************************************************/ async function fetchUserPresence(userId) { ConsoleLogEnabled(`Fetching presence for userId: ${userId}`); if (!userId) { ConsoleLogEnabled("No userId provided to fetchUserPresence"); return null; } return new Promise((resolve) => { GM_xmlhttpRequest({ method: "POST", url: "https://presence.roblox.com/v1/presence/users", headers: { "Content-Type": "application/json" }, data: JSON.stringify({ userIds: [parseInt(userId)] }), onload: function(response) { ConsoleLogEnabled(`Presence API response status: ${response.status}`); if (response.status !== 200) { ConsoleLogEnabled(`Presence API error: ${response.status}`); resolve(null); return; } try { const data = JSON.parse(response.responseText); ConsoleLogEnabled(`Presence API response data: ${JSON.stringify(data)}`); if (data.userPresences && data.userPresences.length > 0) { const presence = data.userPresences[0]; ConsoleLogEnabled(`Presence data - placeId: ${presence.placeId}, gameId: ${presence.gameId}, userPresenceType: ${presence.userPresenceType}`); resolve(presence); } else { ConsoleLogEnabled("No user presence data in response"); resolve(null); } } catch (parseError) { ConsoleLogEnabled(`Error parsing presence API response: ${parseError.message}`); resolve(null); } }, onerror: function(error) { ConsoleLogEnabled(`Presence API request error: ${error}`); resolve(null); } }); }); } /******************************************************* name of function: HandleRecentServersURLandTrackPresence description: Detects recent servers from the url if user joins server from invite url and cleans up the URL. Also tracks user presence every 30 seconds. *******************************************************/ // WARNING: Do not republish this script. Licensed for personal use only. async function HandleRecentServersURLandTrackPresence() { const currentPath = window.location.pathname; const isGamesPage = /^\/(([a-z]{2}(-[a-z]{2})?)\/)?(games\/.+)/.test(currentPath); ConsoleLogEnabled("HandleRecentServersURLandTrackPresence called"); // static like variable to remember if we've already found an invalid URL if (HandleRecentServersURLandTrackPresence.alreadyInvalid) { ConsoleLogEnabled("Already checked URL and found invalid, returning early"); return; } const url = window.location.href; ConsoleLogEnabled(`Current URL: ${url}`); // in url 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]; ConsoleLogEnabled(`Found gameId: ${gameId}, serverId: ${serverId} in URL`); // clean up the URL from invite const cleanURL = window.location.pathname + window.location.search; history.replaceState(null, null, cleanURL); ConsoleLogEnabled(`URL cleaned to: ${cleanURL}`); // call handler stuff await HandleRecentServersAddGames(gameId, serverId); document.querySelector(".recent-servers-section")?.remove(); HandleRecentServers(); // update list visually } else { ConsoleLogEnabled("No gameId and serverId found in URL. (From invite link)"); HandleRecentServersURLandTrackPresence.alreadyInvalid = true; // Set internal flag } // start presence tracking if not already started if (!HandleRecentServersURLandTrackPresence.presenceTracking) { ConsoleLogEnabled("Starting presence tracking..."); HandleRecentServersURLandTrackPresence.presenceTracking = true; const checkPresence = async () => { ConsoleLogEnabled("--- Checking presence ---"); try { const userId = getCurrentUserId(); ConsoleLogEnabled(`userId: ${userId}`); if (!userId) { ConsoleLogEnabled("Could not get userId for presence tracking"); return; } // Use the new fetchUserPresence function const presence = await fetchUserPresence(userId); if (!presence) { ConsoleLogEnabled("No presence data returned"); return; } const placeId = presence.placeId?.toString(); const gameId = presence.gameId; if (!placeId || !gameId) { ConsoleLogEnabled("User not in game or presence data incomplete"); return; } ConsoleLogEnabled(`User is in game - placeId: ${placeId}, gameId: ${gameId}`); // get recent servers from localStorage const recentServersData = localStorage.getItem("ROLOCATE_recentservers_button"); ConsoleLogEnabled(`Recent servers data from localStorage: ${recentServersData}`); if (!recentServersData) { // no recent servers stored, add this one ConsoleLogEnabled("No recent servers found, adding current server"); await HandleRecentServersAddGames(placeId, gameId); if (isGamesPage) { document.querySelector(".recent-servers-section")?.remove(); HandleRecentServers(); // update visually } return; } const recentServers = JSON.parse(recentServersData); const serverKey = `${placeId}_${gameId}`; ConsoleLogEnabled(`Checking for serverKey: ${serverKey}`); ConsoleLogEnabled(`Recent servers keys: ${Object.keys(recentServers).join(", ")}`); // check if this server is already in recent servers if (!recentServers[serverKey]) { ConsoleLogEnabled(`New server detected: ${serverKey} - Adding to recent servers`); await HandleRecentServersAddGames(placeId, gameId); if (isGamesPage) { document.querySelector(".recent-servers-section")?.remove(); HandleRecentServers(); // to update the list visually } } else { ConsoleLogEnabled(`User still in known server: ${serverKey}`); } } catch (error) { ConsoleLogEnabled(`Presence tracking error: ${error.message}`); } }; // check immediately on start ConsoleLogEnabled("initial presence check"); checkPresence(); // then check every 8 seconds setInterval(() => { ConsoleLogEnabled("every 8 seconds seeing if joined server"); checkPresence(); }, 8000); ConsoleLogEnabled("Presence tracking started - will check every 8 seconds"); } else { ConsoleLogEnabled("Presence tracking already running"); } } /******************************************************* 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 currentPath = window.location.pathname; const isGamesPage = /^\/(([a-z]{2}(-[a-z]{2})?)\/)?(games\/.+)/.test(currentPath); // 2nd saftey check only if on gamepage if (!isGamesPage) { ConsoleLogEnabled("If you see this, then somethings wrong"); return; } const serverList = document.querySelector('.server-list-options'); if (!serverList || document.querySelector('.recent-servers-section')) return; const currentGameId = getCurrentGameId(); const allHeaders = document.querySelectorAll('.server-list-header'); let friendsSectionHeader = null; allHeaders.forEach(header => { // fix so restore classic terms would not interfere const text = header.textContent.trim(); const match = ['Servers My Connections Are In', 'Servers My Friends Are In'].some( label => text === label ); if (match) { friendsSectionHeader = header.closest('.container-header'); } }); function formatLastPlayedWithRelative(lastPlayed, mode) { const lastPlayedDate = new Date(lastPlayed); const now = new Date(); const diffMs = now - lastPlayedDate; const diffSeconds = Math.floor(diffMs / 1000); const diffMinutes = Math.floor(diffSeconds / 60); const diffHours = Math.floor(diffMinutes / 60); const diffDays = Math.floor(diffHours / 24); let relativeTime = ''; if (diffDays > 0) { relativeTime = diffDays === 1 ? '1 day ago' : `${diffDays} days ago`; } else if (diffHours > 0) { relativeTime = diffHours === 1 ? '1 hour ago' : `${diffHours} hours ago`; } else if (diffMinutes > 0) { relativeTime = diffMinutes === 1 ? '1 minute ago' : `${diffMinutes} minutes ago`; } else { relativeTime = diffSeconds <= 1 ? 'just now' : `${diffSeconds} seconds ago`; } if (mode === "relativeOnly") { return relativeTime; } return `${lastPlayed} (${relativeTime})`; } if (!friendsSectionHeader) return; const theme = { bgGradient: 'linear-gradient(145deg, #1e2228, #18191e)', bgGradientHover: 'linear-gradient(145deg, #23272f, #1c1f25)', accentPrimary: '#4d85ee', accentGradient: 'linear-gradient(to bottom, #4d85ee, #3464c9)', accentGradientHover: 'linear-gradient(to bottom, #5990ff, #3b6fdd)', textPrimary: '#e8ecf3', textSecondary: '#a0a8b8', borderLight: 'rgba(255, 255, 255, 0.06)', borderLightHover: 'rgba(255, 255, 255, 0.12)', shadow: '0 5px 15px rgba(0, 0, 0, 0.25)', shadowHover: '0 8px 25px rgba(0, 0, 0, 0.3)', dangerGradient: 'linear-gradient(to bottom, #ff5b5b, #e04444)', dangerGradientHover: 'linear-gradient(to bottom, #ff7575, #f55)', popupBg: 'rgba(20, 22, 26, 0.95)', popupBorder: 'rgba(77, 133, 238, 0.2)' }; // svgs to save space so no repeats in code const emptyServerSVG = ` `; const serverstacklogo = ` `; const checkmarksvggeneraluse = ` `; const checkmarkwithoutcircle = ` `; const thelikecopysymbol = ` `; 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 buttonGroupHeader = document.createElement('div'); buttonGroupHeader.style.cssText = ` display: flex; gap: 8px; align-items: center; `; const checkStatusButton = document.createElement('button'); checkStatusButton.textContent = 'Check Status'; checkStatusButton.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; `; checkStatusButton.innerHTML = ` ${checkmarksvggeneraluse} Check Status `; checkStatusButton.onmouseover = function() { this.style.background = 'rgba(77, 133, 238, 0.15)'; this.style.color = theme.accentPrimary; this.style.borderColor = theme.accentPrimary; this.style.transform = 'scale(1.02)'; }; checkStatusButton.onmouseout = function() { this.style.background = 'transparent'; this.style.color = theme.textSecondary; this.style.borderColor = theme.borderLight; this.style.transform = 'scale(1)'; }; checkStatusButton.addEventListener('click', async function() { const storageKey = "ROLOCATE_recentservers_button"; // again lol let stored = JSON.parse(localStorage.getItem(storageKey) || "{}"); const keys = Object.keys(stored).filter(key => key.startsWith(`${currentGameId}_`)); if (keys.length === 0) { // if 0 servers notifications('No servers to check!', 'info', '', '2000'); return; } checkStatusButton.disabled = true; const originalText = checkStatusButton.innerHTML; checkStatusButton.innerHTML = ` Checking... `; let removedCount = 0; const cardsWrapper = document.querySelector('.recent-servers-section .section-content-off'); for (const key of keys) { const [gameId, serverId] = key.split("_"); try { await fetchServerDetails(gameId, serverId); } catch (error) { if (!error.toString().includes("Status 22")) { delete stored[key]; removedCount++; const card = document.querySelector(`[data-server-key="${key}"]`); if (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); } } } } localStorage.setItem(storageKey, JSON.stringify(stored)); if (removedCount > 0) { notifications(`Removed ${removedCount} inactive server${removedCount > 1 ? 's' : ''}`, 'success', '๐Ÿ—‘๏ธ', '2000'); if (Object.keys(stored).filter(k => k.startsWith(`${currentGameId}_`)).length === 0) { const emptyMessage = document.createElement('div'); emptyMessage.className = 'no-servers-message'; emptyMessage.innerHTML = `${emptyServerSVG} 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); } } } else { notifications('All servers are active!', 'success', '๐Ÿ˜Š', '2000'); } checkStatusButton.innerHTML = originalText; checkStatusButton.disabled = false; }); const clearAllButton = document.createElement('button'); clearAllButton.textContent = 'Clear All'; 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; `; clearAllButton.innerHTML = ` Clear All `; clearAllButton.onmouseover = function() { this.style.background = 'rgba(100, 0, 0, 0.85)'; this.style.color = 'white'; this.style.borderColor = 'rgba(100, 0, 0, 0.85)'; 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'; confirmButton.style.cssText = ` background: rgba(100, 0, 0, 0.85); 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)'; 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)'; 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"; //yep again localStorage.setItem(storageKey, JSON.stringify({})); const emptyMessage = document.createElement('div'); emptyMessage.className = 'no-servers-message'; emptyMessage.innerHTML = `${emptyServerSVG} 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'; setTimeout(() => { 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); } }); }); buttonGroupHeader.appendChild(checkStatusButton); buttonGroupHeader.appendChild(clearAllButton); headerInner.appendChild(headerTitleContainer); headerInner.appendChild(buttonGroupHeader); 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"; // again ik 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 = `${emptyServerSVG} 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 = ` ${serverstacklogo} `; 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; 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 = `${emptyServerSVG} 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 { JoinServer(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 = ` ${checkmarkwithoutcircle} 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 = ` ${serverstacklogo} `; 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, ` `)); // a cube cause n!nt*ndo gamecube came up in mind infoItems.appendChild(createInfoItem('Server ID', serverId, ` ${checkmarksvggeneraluse} `)); // yea idk what represents this lmao infoItems.appendChild(createInfoItem('Region', region, ` `)); // pointy thing on google maps const formattedLastPlayed = formatLastPlayedWithRelative(lastPlayed); infoItems.appendChild(createInfoItem('Last Played', formattedLastPlayed, ` `)); // a clock 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 = ` ${thelikecopysymbol} 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 = ` ${checkmarkwithoutcircle} Copied! `; setTimeout(() => { copyButton.innerHTML = ` ${thelikecopysymbol} Copy Info `; }, 1500); }); 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) { 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: showAlreadyInGamePopup description: Shows a styled popup when user is already in a game *******************************************************/ async function showAlreadyInGamePopup(currentGameData) { return new Promise(async (resolve) => { try { // Create overlay const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.4); display: flex; justify-content: center; align-items: center; z-index: 100000000; opacity: 0; transition: opacity 0.3s ease; `; // Create popup const popup = document.createElement('div'); popup.style.cssText = ` background: linear-gradient(135deg, #1a1c1e 0%, #232527 100%); border-radius: 16px; padding: 32px; max-width: 420px; width: 90%; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.7), 0 0 1px rgba(255, 255, 255, 0.1) inset; color: white; font-family: 'HCo Gotham SSm', 'Helvetica Neue', Helvetica, Arial, sans-serif; transform: scale(0.9); transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); border: 1px solid rgba(255, 255, 255, 0.05); `; popup.innerHTML = `

    Already in a Game

    You are currently playing ${currentGameData.lastLocation}. Would you still like to join this server?

    RoLocate by Oqarshi
    `; // Add keyframe animation for spinner const style = document.createElement('style'); style.textContent = ` @keyframes spin { to { transform: rotate(360deg); } } `; document.head.appendChild(style); overlay.appendChild(popup); document.body.appendChild(overlay); // Trigger fade in animation setTimeout(() => { overlay.style.opacity = '1'; popup.style.transform = 'scale(1)'; }, 10); // Load game icon const loadingSpinner = popup.querySelector('#loadingSpinner'); const gameIconImg = popup.querySelector('#gameIcon'); try { const universeId = await getUniverseIdFromPlaceId(currentGameData.rootPlaceId); const gameIcon = await getGameIconFromUniverseId(universeId); gameIconImg.src = gameIcon; gameIconImg.onload = () => { loadingSpinner.style.display = 'none'; gameIconImg.style.display = 'block'; }; } catch (error) { loadingSpinner.style.display = 'none'; // Show placeholder on error popup.querySelector('#gameIconContainer').innerHTML = `
    ๐ŸŽฎ
    `; } // Add hover effects const cancelBtn = popup.querySelector('#cancelJoin'); const confirmBtn = popup.querySelector('#confirmJoin'); cancelBtn.onmouseover = () => { cancelBtn.style.background = 'rgba(255, 255, 255, 0.12)'; cancelBtn.style.transform = 'translateY(-1px)'; }; cancelBtn.onmouseout = () => { cancelBtn.style.background = 'rgba(255, 255, 255, 0.08)'; cancelBtn.style.transform = 'translateY(0)'; }; confirmBtn.onmouseover = () => { confirmBtn.style.background = 'linear-gradient(135deg, #0088dd 0%, #0077cc 100%)'; confirmBtn.style.transform = 'translateY(-1px)'; confirmBtn.style.boxShadow = '0 6px 16px rgba(0, 162, 255, 0.4)'; }; confirmBtn.onmouseout = () => { confirmBtn.style.background = 'linear-gradient(135deg, #00a2ff 0%, #0088dd 100%)'; confirmBtn.style.transform = 'translateY(0)'; confirmBtn.style.boxShadow = '0 4px 12px rgba(0, 162, 255, 0.3)'; }; // Fade out animation const fadeOut = () => { overlay.style.opacity = '0'; popup.style.transform = 'scale(0.9)'; setTimeout(() => overlay.remove(), 300); }; // Handle button clicks cancelBtn.onclick = () => { fadeOut(); setTimeout(() => resolve(false), 300); }; confirmBtn.onclick = () => { fadeOut(); setTimeout(() => resolve(true), 300); }; } catch (error) { ConsoleLogEnabled(`Error creating popup: ${error}`); // Fallback to simple confirm resolve(confirm("You are already in a game. Would you like to continue joining this server?")); } }); } /******************************************************* name of function: JoinServer description: a function to join servers. has btroblox comptabaility. also join private servers *******************************************************/ async function JoinServer(placeId, serverId, serverType) { if (!/^https:\/\/www\.roblox\.com(\/[a-z]{2})?\/games\//.test(window.location.href)) return; if (localStorage.getItem("ROLOCATE_joinconfirmation") === "true") { // checkj if in game try { const userId = getCurrentUserId(); const presence = await fetchUserPresence(userId); if (presence && presence.gameId) { // show custom pouppu const shouldContinue = await showAlreadyInGamePopup(presence); if (!shouldContinue) { return; // no jioin } } } catch (error) { ConsoleLogEnabled(`Error checking user presence: ${error}`); // continue if fails } } // join private server if (serverType === "private server") { ConsoleLogEnabled(`Joining PRIVATE SERVER`); // bypass roblox interceptier window._skipRobloxJoinInterceptor = true; // join Roblox.GameLauncher.joinPrivateGame(placeId, serverId); return; } // mobile mode exception if (localStorage.getItem("ROLOCATE_mobilemode") === "true") { window.open( `https://oqarshi.github.io/Invite/?placeid=${placeId}&serverid=${serverId}&mobilemode=true`, "_blank" ); return; } if (localStorage.getItem("ROLOCATE_btrobloxfix") === "true") { /* ---------- recentโ€‘servers handling (always runs) ---------- */ if (localStorage.getItem("ROLOCATE_togglerecentserverbutton") === "true") { await HandleRecentServersAddGames(placeId, serverId); document.querySelector(".recent-servers-section")?.remove(); HandleRecentServers(); } /* ---------- smartserver join---------- */ if (localStorage.getItem("ROLOCATE_smartjoinpopup") === "true") { showLoadingOverlay(placeId, serverId); await new Promise(res => setTimeout(res, 1500)); } //join via deeplink ConsoleLogEnabled(`Joining via deeplink: placeId=${placeId}, serverId=${serverId}`); window.location.href = `roblox://experiences/start?placeId=${placeId}&gameInstanceId=${serverId}`; } else { // join via roblox launcher ConsoleLogEnabled(`Joining via Roblox launcher: placeId=${placeId}, serverId=${serverId}`); /* ---------- recentโ€‘servers handling (always runs) ---------- */ if (localStorage.getItem("ROLOCATE_togglerecentserverbutton") === "true") { await HandleRecentServersAddGames(placeId, serverId); document.querySelector(".recent-servers-section")?.remove(); HandleRecentServers(); } /* ---------- smartserver join---------- */ if (localStorage.getItem("ROLOCATE_smartjoinpopup") === "true") { showLoadingOverlay(placeId, serverId); await new Promise(res => setTimeout(res, 1500)); } // set flag to bypass interceptor window._skipRobloxJoinInterceptor = true; Roblox.GameLauncher.joinGameInstance(placeId, serverId); } } /******************************************************* name of function: showLoadingOverlay description: Loading box when joining a server + Shows server location *******************************************************/ // WARNING: Do not republish this script. Licensed for personal use only. async function showLoadingOverlay(gameId, serverId, mainMessage = "", statusMessage = "") { // remove existing overlay if present const existingOverlay = document.querySelector('[data-loading-overlay]'); if (existingOverlay) { existingOverlay.style.opacity = '0'; setTimeout(() => existingOverlay.remove(), 200); } // remove existing styles const existingStyle = document.querySelector('[data-loading-overlay-style]'); if (existingStyle) existingStyle.remove(); // function to create elements with styles const createElement = (tag, styles, content = '') => { const el = document.createElement(tag); Object.assign(el.style, styles); if (content) el.innerHTML = content; return el; }; const style = createElement('style', {}, ` @keyframes progress-slide { 0% { left: -30%; } 100% { left: 100%; } } @keyframes progress-glow { 0%, 100% { box-shadow: 0 0 10px rgba(59, 130, 246, 0.3); } 50% { box-shadow: 0 0 20px rgba(59, 130, 246, 0.6); } } @keyframes dots { 0%, 20% { content: ''; } 40% { content: '.'; } 60% { content: '..'; } 80%, 100% { content: '...'; } } @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } @keyframes slide-up { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } @keyframes scale-in { from { opacity: 0; transform: translate(-50%, -50%) scale(0.92); } to { opacity: 1; transform: translate(-50%, -50%) scale(1); } } `); style.setAttribute('data-loading-overlay-style', ''); document.head.appendChild(style); // main overlay - lighter and no blur const overlay = createElement('div', { position: 'fixed', top: '0', left: '0', width: '100%', height: '100%', backgroundColor: 'rgba(0, 0, 0, 0.3)', zIndex: '999999', opacity: '0', transition: 'opacity 0.5s ease' }); overlay.setAttribute('data-loading-overlay', ''); // main container - bigger and lighter const container = createElement('div', { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%) scale(0.92)', width: '540px', background: '#1a1a1a', borderRadius: '18px', boxShadow: '0 12px 48px rgba(0, 0, 0, 0.6)', border: '1px solid #2a2a2a', padding: '0', fontFamily: 'system-ui, -apple-system, sans-serif', zIndex: '1000000', overflow: 'hidden', opacity: '0', animation: 'scale-in 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards' }); // close button const closeButton = createElement('button', { position: 'absolute', top: '16px', right: '16px', width: '36px', height: '36px', borderRadius: '8px', border: 'none', background: '#252525', color: '#888', fontSize: '20px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'all 0.2s ease', outline: 'none', zIndex: '10' }, 'ร—'); closeButton.addEventListener('mouseenter', () => { closeButton.style.background = '#303030'; closeButton.style.color = '#fff'; }); closeButton.addEventListener('mouseleave', () => { closeButton.style.background = '#252525'; closeButton.style.color = '#888'; }); // header section with game icon const headerSection = createElement('div', { display: 'flex', alignItems: 'center', padding: '32px 32px 24px 32px', gap: '20px', animation: 'fade-in 0.4s ease 0.2s backwards' }); // game icon - much larger const iconContainer = createElement('div', { width: '96px', height: '96px', borderRadius: '16px', background: '#252525', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid #333', overflow: 'hidden', flexShrink: '0' }); const defaultLogo = createElement('div', { width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'opacity 0.2s ease' }, `Logo`); const gameIcon = createElement('img', { width: '100%', height: '100%', objectFit: 'cover', display: 'none', opacity: '0', transition: 'opacity 0.2s ease' }); iconContainer.appendChild(defaultLogo); iconContainer.appendChild(gameIcon); // text section const textSection = createElement('div', { flex: '1', minWidth: '0' }); const isServerHopping = !gameId || !serverId; const titleText = createElement('div', { fontSize: '24px', fontWeight: '600', color: '#fff', marginBottom: '6px', letterSpacing: '-0.02em' }, mainMessage || (isServerHopping ? 'Server Hopping' : 'Joining Game')); const dotsSpan = createElement('span', { animation: 'dots 1.4s steps(4, end) infinite' }); titleText.appendChild(dotsSpan); const subtitleText = createElement('div', { fontSize: '14px', color: '#aaa', fontWeight: '500' }, statusMessage || (isServerHopping ? 'Finding available server' : 'Connecting to server')); textSection.appendChild(titleText); textSection.appendChild(subtitleText); headerSection.appendChild(iconContainer); headerSection.appendChild(textSection); // divider const divider = createElement('div', { height: '1px', background: '#2a2a2a', margin: '0 32px' }); // location section const locationSection = createElement('div', { padding: '24px 32px', textAlign: 'center', animation: 'slide-up 0.4s ease 0.3s backwards', minHeight: '80px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }); const locationLabel = createElement('div', { fontSize: '11px', color: '#888', fontWeight: '600', textTransform: 'uppercase', letterSpacing: '1px', marginBottom: '10px' }, 'Server Location'); const locationValue = createElement('div', { fontSize: '20px', color: '#fff', fontWeight: '600', minHeight: '28px', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'opacity 0.3s ease' }, '๐ŸŒ Detecting...'); locationSection.appendChild(locationLabel); locationSection.appendChild(locationValue); // server details section const detailsSection = createElement('div', { padding: '0 32px 24px 32px', display: 'flex', gap: '12px', animation: 'slide-up 0.4s ease 0.4s backwards' }); const createDetail = (label, value, color) => { const detail = createElement('div', { flex: '1', background: '#222', border: '1px solid #2a2a2a', borderRadius: '10px', padding: '14px 16px', minWidth: '0' }); const labelEl = createElement('div', { fontSize: '11px', color: '#888', fontWeight: '600', textTransform: 'uppercase', letterSpacing: '0.5px', marginBottom: '6px' }, label); const valueEl = createElement('div', { fontSize: '13px', color: color, fontWeight: '600', fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, value || 'N/A'); detail.appendChild(labelEl); detail.appendChild(valueEl); return detail; }; detailsSection.appendChild(createDetail('Game ID', gameId, '#60a5fa')); detailsSection.appendChild(createDetail('Server ID', serverId, '#34d399')); // progress bar section const progressSection = createElement('div', { padding: '0 32px 24px 32px', animation: 'slide-up 0.4s ease 0.5s backwards' }); const progressBar = createElement('div', { height: '4px', background: '#252525', borderRadius: '2px', overflow: 'hidden', position: 'relative', boxShadow: 'inset 0 1px 2px rgba(0, 0, 0, 0.5)' }); const progressFill = createElement('div', { position: 'absolute', top: '0', left: '-30%', height: '100%', width: '30%', background: 'linear-gradient(90deg, rgba(59, 130, 246, 0) 0%, rgba(59, 130, 246, 0.8) 40%, #3b82f6 50%, rgba(59, 130, 246, 0.8) 60%, rgba(59, 130, 246, 0) 100%)', animation: 'progress-slide 2s ease-in-out infinite, progress-glow 2s ease-in-out infinite', borderRadius: '2px' }); progressBar.appendChild(progressFill); progressSection.appendChild(progressBar); // footer const footer = createElement('div', { padding: '20px 32px', borderTop: '1px solid #2a2a2a', textAlign: 'center', animation: 'fade-in 0.4s ease 0.6s backwards' }); const footerText = createElement('div', { fontSize: '11px', color: '#666', fontWeight: '600', letterSpacing: '0.5px', textTransform: 'uppercase' }, 'RoLocate by Oqarshi'); footer.appendChild(footerText); // assemble everything container.appendChild(closeButton); container.appendChild(headerSection); container.appendChild(divider); container.appendChild(locationSection); container.appendChild(detailsSection); container.appendChild(progressSection); container.appendChild(footer); overlay.appendChild(container); document.body.appendChild(overlay); // animate in requestAnimationFrame(() => { overlay.style.opacity = '1'; }); // fetch game icon if (gameId) { getUniverseIdFromPlaceId(gameId) .then(universeId => getGameIconFromUniverseId(universeId)) .then(iconUrl => { gameIcon.src = iconUrl; gameIcon.onload = () => { defaultLogo.style.opacity = '0'; setTimeout(() => { defaultLogo.style.display = 'none'; gameIcon.style.display = 'block'; requestAnimationFrame(() => { gameIcon.style.opacity = '1'; }); }, 200); }; gameIcon.onerror = () => ConsoleLogEnabled('Failed to load game icon'); }) .catch(error => ConsoleLogEnabled('Error fetching game icon:', error)); } // fetch server location (async () => { subtitleText.textContent = statusMessage || (isServerHopping ? 'Finding server...' : 'Locating server...'); await new Promise(resolve => setTimeout(resolve, 500)); try { if (isServerHopping) { locationValue.innerHTML = '๐ŸŒ Random Server'; subtitleText.textContent = statusMessage || 'Connecting...'; } else { const locationData = await fetchServerDetails(gameId, serverId); const flagEmoji = getFlagEmoji(locationData.country.code); locationValue.innerHTML = ''; locationValue.appendChild(flagEmoji); locationValue.append(` ${locationData.city}, ${locationData.country.name}`); subtitleText.innerHTML = statusMessage || `Connecting to ${locationData.city}`; } } catch (error) { ConsoleLogEnabled('Error fetching location:', error); locationValue.innerHTML = isServerHopping ? '๐ŸŒ Random Server' : '๐ŸŒ Unknown Location'; subtitleText.textContent = statusMessage || 'Connecting...'; } })(); // cleanup function const cleanup = () => { overlay.style.opacity = '0'; setTimeout(() => { overlay.remove(); style.remove(); }, 200); }; // auto hide after 20 seconds for server hopping, 6 seconds for normal join const fadeOutDuration = isServerHopping ? 20000 : 6000; const fadeOutTimer = setTimeout(cleanup, fadeOutDuration); // close button handler closeButton.addEventListener('click', () => { clearTimeout(fadeOutTimer); cleanup(); }); } function Responsivegamecards() { if (localStorage.getItem("ROLOCATE_responsivegamecards") === "false") { return; } // Add styles const style = document.createElement('style'); style.id = 'game-card-hover-styles'; style.textContent = ` .game-card-container { transition: all 0.25s ease-out !important; } .game-card-container:hover { transform: translateY(-6px) !important; box-shadow: 0 8px 20px rgba(0,0,0,0.2) !important; } .game-card-thumb-container { overflow: hidden !important; } .game-card-thumb-container img { transition: transform 0.3s ease !important; } .game-card-container:hover .game-card-thumb-container img { transform: scale(1.08) !important; } `; document.head.appendChild(style); // when game cards show up new MutationObserver(() => {}).observe(document.body, { childList: true, subtree: true }); } /******************************************************* name of function: event listener description: Not 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!"); }); loadServerRegions(() => { ConsoleLogEnabled("Loaded Server Regions!"); }); AddSettingsButton(() => { ConsoleLogEnabled("Loaded Settings button!"); }); if (localStorage.getItem("ROLOCATE_togglerecentserverbutton") === "true") { HandleRecentServersURLandTrackPresence(); // starts presence tracking for recent servers } betterfriends(); // shows better friends Responsivegamecards(); // uh the repsonsive game cards SmartSearch(); // smartsearch bar ontop cool function :) applycustombackgrounds(); // applies custom backgrounds restoreclassicterms(); // restores classic terms quicklaunchgamesfunction(); // shows quick launch games manageRobloxChatBar(); // removes chatbar if enabled loadmutualfriends(); // shows mutualfriends Update_Popup(); // shows update message initializeLocalStorage(); // sets up localstorage removeAds(); // removes ads showOldRobloxGreeting(); // shows old greeting validateManualMode(); // checks for manual mode qualityfilterRobloxGames(); // filters roblox game if it is on // start observing URL changes cuase its cool observeURLChanges(); const endTime = performance.now(); const elapsed = Math.round(endTime - startTime); // add small delay setTimeout(() => { 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;"); }, 10); }); /******************************************************* name of function: mobile stuff #1 description: mobile mode thing. if mobile mode true and not in game link then show notification. *******************************************************/ if (localStorage.ROLOCATE_mobilemode === "true" && !location.href.match(/^https:\/\/www\.roblox\.com(\/[a-z]{2})?\/games\//)) { const observer = new MutationObserver(() => { document.querySelectorAll('a[href*="/games/"]').forEach(link => { if (link.dataset.mobileModeAttached) return; link.dataset.mobileModeAttached = "true"; link.addEventListener("click", () => notifications('Tap the "Cancel" button', 'info', 'โ—', '60000')); }); }); observer.observe(document.body, { childList: true, subtree: true }); } /******************************************************* name of function: mobile stuff #2 description: mobile mode thingy so that servers can show on mobile devices. this is so scuffed lmao *******************************************************/ if (localStorage.ROLOCATE_mobilemode === "true" && /^https:\/\/www\.roblox\.com(\/[a-z]{2})?\/games\//.test(location.href)) { if (!location.href.includes("#!/game-instances")) { // not yet on game-instance notifications('Mobile Mode is Enabled. Some features may be disabled.', 'info', 'โ„น๏ธ', '6000'); setTimeout(() => { location.replace(location.href + "#!/game-instances"); }, 1000); } else { // after on game-instance notifications('Mobile Mode is Enabled. Some features may be disabled.', 'info', 'โ„น๏ธ', '6000'); } } /******************************************************* name of function: mobile stuff #3 description: so like if on roblox.com where says go to app, tell user to not do that *******************************************************/ if (localStorage.ROLOCATE_mobilemode === "true" && location.href.match(/^https:\/\/www\.roblox\.com\/?$/)) { notifications('Tap "Continue in browser"', 'info', 'โ—', '30000'); } /******************************************************* name of function: mobile stuff experimental #5 description: so like if on roblox.com where says go to app, tell user to not do that *******************************************************/ // RoLocate Loading Screen - Run immediately, before page loads if (localStorage.ROLOCATE_mobilemode === "true" && location.href.match(/^https:\/\/www\.roblox\.com(\/[a-z]{2})?\/home/)) { // Inject styles immediately in head const style = document.createElement('style'); style.textContent = ` #rolocate-loading-screen { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #000; display: flex; align-items: center; justify-content: center; z-index: 999999; opacity: 1; transition: opacity 0.6s ease; } #rolocate-loading-screen.fade-out { opacity: 0; } .rolocate-loading-content { text-align: center; } .rolocate-logo-text-container { display: flex; align-items: center; justify-content: center; gap: 15px; margin-bottom: 15px; } .rolocate-logo { width: 50px; height: 50px; border-radius: 8px; transform: translate(10px, -5px); opacity: 0; animation: rolocate-logo-fade 0.5s ease-in-out 0.3s forwards; } .rolocate-svg { width: 250px; height: auto; } .rolocate-text { font-size: 48px; font-weight: 700; font-family: Arial, sans-serif; fill: #ff4757; opacity: 0; animation: rolocate-text-fade 0.5s ease-in-out 0.5s forwards; } .rolocate-subtitle { font-size: 20px; font-weight: 400; font-family: Arial, sans-serif; color: #888; opacity: 0; margin: 0; animation: rolocate-fade-in 0.8s ease-in-out 1.2s forwards; } @keyframes rolocate-logo-fade { to { opacity: 1; } } @keyframes rolocate-text-fade { to { opacity: 1; } } @keyframes rolocate-fade-in { to { opacity: 0.6; } } `; document.head.appendChild(style); // Create loading screen HTML const loadingScreen = document.createElement('div'); loadingScreen.id = 'rolocate-loading-screen'; loadingScreen.innerHTML = `
    RoLocate

    For Mobile

    `; // Add to body immediately document.body.insertBefore(loadingScreen, document.body.firstChild); // Fade out after 3 seconds setTimeout(() => { loadingScreen.classList.add('fade-out'); setTimeout(() => { loadingScreen.remove(); style.remove(); }, 600); }, 3000); } /******************************************************* 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" || localStorage.getItem("ROLOCATE_betterprivateservers") == "true" ) ) { let Isongamespage = true; if (window.location.href.includes("/games/")) { // saftey check and lazy load data to save the 2mb of ram lmao InitRobloxLaunchHandler(); if (window.serverRegionsByIp) { ConsoleLogEnabled("enabled roblox launch handler"); } else { ConsoleLogEnabled("failed to enable roblox launch handler"); } getFlagEmoji(); // lazy loads the flag emoji base64 to save some ram i guess } /******************************************************* name of function:monitorPlayButton description: for join confimation and like future updates note that it is called if the jopinconfimation in localstop4arghe is true so not check needed here *******************************************************/ function monitorPlayButton() { const button = document.querySelector('[data-testid="play-button"]'); if (!button) return; async function onPlayClick(event) { //intercept real clicks if (!event.isTrusted) { return; // let program clicks } event.preventDefault(); event.stopImmediatePropagation(); try { const userId = getCurrentUserId(); const presence = await fetchUserPresence(userId); if (presence && presence.gameId) { const shouldContinue = await showAlreadyInGamePopup(presence); if (!shouldContinue) return; } } catch (error) { ConsoleLogEnabled('Error checking user presence:', error); } // Uok so rel click button.click(); } button.addEventListener('click', onPlayClick, true); } /******************************************************* 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. *******************************************************/ function InitRobloxLaunchHandler() { if (localStorage.getItem("ROLOCATE_btrobloxfix") === "true" || localStorage.getItem("ROLOCATE_mobilemode") === "true") { return; } 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) { // check if we should skip interception (called from JoinServer) if (window._skipRobloxJoinInterceptor) { window._skipRobloxJoinInterceptor = false; // reset flag return originalJoin.apply(this, arguments); } if (localStorage.getItem("ROLOCATE_joinconfirmation") === "true") { // check if in game try { const userId = getCurrentUserId(); const presence = await fetchUserPresence(userId); if (presence && presence.gameId) { // show the popup const shouldContinue = await showAlreadyInGamePopup(presence); if (!shouldContinue) { return; } } } catch (error) { ConsoleLogEnabled(`Error checking user presence: ${error}`); // contineu if presense fails } } 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); await new Promise(res => setTimeout(res, 1500)); } /* ---------- finally join the game ---------- */ return originalJoin.apply(this, arguments); }; } /******************************************************* name of function: disableYouTubeAutoplayInIframes Description: disable autoplay in YouTube iframes on game page *******************************************************/ // currently bug where if u play the video it like keeps playing when scrolling through 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 (error) { // url parsing failed, just skip it ConsoleLogEnabled('Failed to parse iframe src URL', error); } } 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: cleanupPrivateServerCards Description: compacts private servers so they don't take up so much space *******************************************************/ function cleanupPrivateServerCards() { if (localStorage.ROLOCATE_betterprivateservers !== "true") return; // prevent multiple observers or other runs if (cleanupPrivateServerCards._initialized) return; cleanupPrivateServerCards._initialized = true; let isRunning = false; let searchBar = null; let currentSearchQuery = ''; // load settings const getSettings = () => { const defaultSettings = { compactPrivateServers: true, onlyYourPrivateServers: false, privateServerSearch: false }; const saved = JSON.parse( localStorage.getItem('ROLOCATE_editprivateserversettings') || '{}' ); return { ...defaultSettings, ...saved }; }; // apply search filter const applySearchFilter = (query) => { currentSearchQuery = query; const cards = document.querySelectorAll('.card-item-private-server'); cards.forEach(card => { const parentLi = card.closest('li'); const serverNameEl = card.querySelector('.section-header .font-bold'); const serverName = serverNameEl ? serverNameEl.textContent.toLowerCase() : ''; const ownerNameEl = card.querySelector('.rbx-private-owner .text-name'); const ownerName = ownerNameEl ? ownerNameEl.textContent.toLowerCase() : ''; const matches = serverName.includes(query) || ownerName.includes(query); if (query === '' || matches) { if (parentLi) parentLi.style.display = ''; else card.style.display = ''; } else { if (parentLi) parentLi.style.display = 'none'; else card.style.display = 'none'; } }); updateFilterBadge(); }; // update filter badge const updateFilterBadge = () => { const container = document.getElementById('rolocate-ps-search-container'); if (!container) return; let badge = document.getElementById('rolocate-ps-filter-badge'); if (currentSearchQuery) { if (!badge) { badge = document.createElement('div'); badge.id = 'rolocate-ps-filter-badge'; badge.style.cssText = ` display: inline-flex; align-items: center; gap: 6px; padding: 8px 12px; background: rgba(77, 133, 238, 0.15); border: 1px solid rgba(77, 133, 238, 0.3); border-radius: 8px; color: #4d85ee; font-size: 13px; font-weight: 600; margin-left: 8px; `; const text = document.createElement('span'); text.id = 'rolocate-ps-filter-text'; text.textContent = `Filter: "${currentSearchQuery}"`; const closeBtn = document.createElement('span'); closeBtn.textContent = 'ร—'; closeBtn.style.cssText = ` cursor: pointer; font-size: 18px; font-weight: 700; line-height: 1; opacity: 0.7; transition: opacity 0.2s; `; closeBtn.onmouseenter = () => closeBtn.style.opacity = '1'; closeBtn.onmouseleave = () => closeBtn.style.opacity = '0.7'; closeBtn.onclick = () => { currentSearchQuery = ''; applySearchFilter(''); }; badge.appendChild(text); badge.appendChild(closeBtn); container.appendChild(badge); } else { const text = document.getElementById('rolocate-ps-filter-text'); if (text) text.textContent = `Filter: "${currentSearchQuery}"`; } } else { if (badge) badge.remove(); } }; // create search button const createSearchButton = () => { if (searchBar) return searchBar; const container = document.createElement('div'); container.id = 'rolocate-ps-search-container'; container.style.cssText = ` display: inline-flex; align-items: center; margin-bottom: 15px; margin-left: 9px; `; const button = document.createElement('button'); button.id = 'rolocate-ps-search-button'; button.innerHTML = '๐Ÿ” Search Private Servers'; button.className = 'btn-secondary-md'; button.style.cssText = ` padding: 10px 18px; background: transparent; border: 1px solid rgba(150, 150, 150, 0.3); border-radius: 8px; color: #a0a8b8; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; display: inline-flex; align-items: center; gap: 6px; `; button.onmouseenter = () => { button.style.background = 'rgba(77, 133, 238, 0.15)'; button.style.borderColor = 'rgba(77, 133, 238, 0.3)'; button.style.color = '#4d85ee'; }; button.onmouseleave = () => { button.style.background = 'transparent'; button.style.borderColor = 'rgba(150, 150, 150, 0.3)'; button.style.color = '#a0a8b8'; }; button.onclick = () => showSearchPopup(); container.appendChild(button); searchBar = container; return container; }; // show search popup const showSearchPopup = () => { const overlay = document.createElement('div'); overlay.className = 'search-popup-overlay'; const box = document.createElement('div'); box.className = 'search-popup-content'; const title = document.createElement('h3'); title.textContent = 'Search Private Servers'; title.style.cssText = ` margin: 0 0 20px 0; color: #e8ecf3; font-size: 20px; font-weight: 700; text-align: center; `; const input = document.createElement('input'); input.type = 'text'; input.placeholder = 'Search by server name or owner...'; input.id = 'rolocate-ps-search-input-popup'; input.value = currentSearchQuery; input.style.cssText = ` width: 100%; padding: 14px 16px; background: rgba(28, 31, 37, 0.6); border: 1px solid rgba(77, 133, 238, 0.3); border-radius: 8px; color: #e8ecf3; font-size: 15px; font-weight: 600; outline: none; transition: border-color 0.2s; box-sizing: border-box; `; input.onfocus = () => { input.style.borderColor = 'rgba(77, 133, 238, 0.6)'; }; input.onblur = () => { input.style.borderColor = 'rgba(77, 133, 238, 0.3)'; }; // search functionality input.oninput = (e) => { const query = e.target.value.toLowerCase().trim(); applySearchFilter(query); }; const close = document.createElement('button'); close.className = 'search-popup-close btn-secondary-md'; close.textContent = 'Close'; const closeOverlay = () => { overlay.classList.add('fade-out'); overlay.addEventListener('animationend', () => overlay.remove(), { once: true }); }; close.onclick = closeOverlay; overlay.onclick = e => e.target === overlay && closeOverlay(); box.addEventListener('click', (e) => { e.stopPropagation(); }); box.appendChild(title); box.appendChild(input); box.appendChild(close); overlay.appendChild(box); document.body.appendChild(overlay); // auto-focus the input setTimeout(() => input.focus(), 100); }; // insert search button into DOM const insertSearchBar = () => { const settings = getSettings(); if (!settings.privateServerSearch) { // remove search button if it exists const existing = document.getElementById('rolocate-ps-search-container'); if (existing) existing.remove(); searchBar = null; return; } // find the container const serverList = document.querySelector('#rbx-private-running-games'); if (!serverList) return; const existing = document.getElementById('rolocate-ps-search-container'); if (!existing) { const container = createSearchButton(); serverList.insertBefore(container, serverList.firstChild); updateFilterBadge(); } }; // popup stuff const showPlayersPopup = (thumbs, card) => { const overlay = document.createElement('div'); overlay.className = 'players-popup-overlay'; const box = document.createElement('div'); box.className = 'players-popup-content'; box.innerHTML = '

    Players in Server

    '; if (thumbs && thumbs.querySelector('img')) { Object.assign(thumbs.style, { display: 'flex', justifyContent: 'center', flexWrap: 'wrap' }); // open in new tab thumbs.querySelectorAll('a').forEach(link => { link.setAttribute('target', '_blank'); link.setAttribute('rel', 'noopener noreferrer'); }); // stop propagation on thumbnail clicks so links work thumbs.addEventListener('click', (e) => { e.stopPropagation(); }); box.appendChild(thumbs); } else { const noP = document.createElement('p'); noP.innerHTML = 'No players currently in this server.
    RoLocate: To disable: Settings -> Appearance -> Better Private Servers.'; box.appendChild(noP); } const close = document.createElement('button'); close.className = 'players-popup-close btn-secondary-md'; close.textContent = 'Close'; const closeOverlay = () => { overlay.classList.add('fade-out'); overlay.addEventListener('animationend', () => overlay.remove(), { once: true }); }; close.onclick = closeOverlay; overlay.onclick = e => e.target === overlay && closeOverlay(); box.addEventListener('click', (e) => { e.stopPropagation(); }); box.appendChild(close); overlay.appendChild(box); document.body.appendChild(overlay); }; // cleanup logic const performCleanup = () => { if (isRunning) return; isRunning = true; const settings = getSettings(); const currentUserId = getCurrentUserId(); // insert or remove search bar based on settings insertSearchBar(); const cards = document.querySelectorAll('.card-item-private-server'); for (const card of cards) { const parentLi = card.closest('li'); // Only Your Private Servers feature if (settings.onlyYourPrivateServers) { const ownerLink = card.querySelector('.rbx-private-owner a[href*="/users/"]'); if (ownerLink) { const href = ownerLink.getAttribute('href'); const match = href.match(/\/users\/(\d+)\//); if (match) { const ownerId = match[1]; if (ownerId !== currentUserId.toString()) { // Hide this server since it's not yours if (parentLi) { parentLi.style.display = 'none'; } else { card.style.display = 'none'; } continue; // Skip processing this card further } } } } else { // Make sure cards are visible if setting is off (unless search is hiding them) if (!currentSearchQuery) { if (parentLi) { parentLi.style.display = ''; } else { card.style.display = ''; } } } // Compact Private Servers feature if (settings.compactPrivateServers) { const thumbs = card.querySelector('.player-thumbnails-container'); if (thumbs) thumbs.remove(); const details = card.querySelector('.rbx-private-game-server-details'); details?.classList.remove('game-server-details', 'border-right'); const joinBtn = card.querySelector('.rbx-private-game-server-join'); if (joinBtn && !card.querySelector('.rolocate-view-players-btn')) { const btn = document.createElement('button'); btn.textContent = 'View Players'; btn.className = 'btn-full-width btn-control-xs rolocate-view-players-btn btn-secondary-md btn-min-width'; btn.style.marginTop = '6px'; joinBtn.after(btn); // no "once" here allows multiple uses without leaking memory btn.addEventListener('click', () => { const clonedThumbs = thumbs ? thumbs.cloneNode(true) : null; showPlayersPopup(clonedThumbs, card); }); } } } // Reapply search filter if one exists if (currentSearchQuery) { applySearchFilter(currentSearchQuery); } // Only apply compact styles if compact mode is enabled if (settings.compactPrivateServers) { document.querySelectorAll('.rbx-private-game-server-item') .forEach(i => i.classList.remove('rbx-private-game-server-item')); } if (!document.getElementById('private-server-cleanup-styles')) { const s = document.createElement('style'); s.id = 'private-server-cleanup-styles'; s.textContent = ` .card-item-private-server{display:inline-block;width:auto;max-width:250px;min-width:200px} #rbx-private-game-server-item-container li{display:inline-block;width:auto!important;float:none} .rbx-private-game-server-item-container{display:flex;flex-wrap:wrap;gap:10px} .players-popup-overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);display:flex;align-items:center;justify-content:center;z-index:9999;animation:fadeIn .2s ease-out;opacity:1} .players-popup-content{background:rgba(20, 22, 26, 0.95);color:#e8ecf3;border-radius:12px;padding:20px;max-width:400px;width:90%;box-shadow:0 10px 25px rgba(0,0,0,.3);border:1px solid rgba(77, 133, 238, 0.2);text-align:center;transform:scale(.95);animation:popIn .2s ease-out forwards} .players-popup-content h3{margin-top:0;color:#e8ecf3;font-size:16px;font-weight:600;margin-bottom:16px} .players-popup-content p{color:#a0a8b8;font-size:13px;line-height:1.5;margin-bottom:24px} .players-popup-content .player-thumbnails-container{display:flex;flex-wrap:wrap;justify-content:center;gap:10px;margin-top:10px} .players-popup-close{margin-top:15px;padding:8px 20px;cursor:pointer;background:rgba(28, 31, 37, 0.6);color:#e8ecf3;border:1px solid rgba(255, 255, 255, 0.12);border-radius:6px;font-size:13px;font-weight:500;transition:0.2s} .search-popup-overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);display:flex;align-items:center;justify-content:center;z-index:9999;animation:fadeIn .2s ease-out;opacity:1} .search-popup-content{background:rgba(20, 22, 26, 0.95);color:#e8ecf3;border-radius:12px;padding:30px;max-width:400px;width:20%;box-shadow:0 10px 25px rgba(0,0,0,.3);border:1px solid rgba(77, 133, 238, 0.2);transform:scale(.95);animation:popIn .2s ease-out forwards} .search-popup-close{margin-top:20px;width:100%;padding:10px 20px;cursor:pointer;background:rgba(28, 31, 37, 0.6);color:#e8ecf3;border:1px solid rgba(255, 255, 255, 0.12);border-radius:8px;font-size:14px;font-weight:600;transition:0.2s} .search-popup-close:hover{background:rgba(28, 31, 37, 0.8);border-color:rgba(255, 255, 255, 0.2)} @keyframes fadeIn{from{opacity:0}to{opacity:1}} @keyframes popIn{to{transform:scale(1)}} @keyframes fadeOut{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}} .fade-out{animation:fadeOut .2s ease-out forwards} `; document.head.appendChild(s); } isRunning = false; }; // observer inside same scope const observer = new MutationObserver(() => { observer.disconnect(); performCleanup(); observer.observe(document.body, { childList: true, subtree: true }); }); // run performCleanup(); observer.observe(document.body, { childList: true, subtree: true }); } /******************************************************* 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'; 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); `; // 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 base64 logo const logo = document.createElement('img'); logo.src = window.Base64Images.logo; logo.style.cssText = ` width: 24px; height: 24px; margin-right: 10px; `; // add title const title = document.createElement('span'); title.textContent = 'RoLocate'; title.style.cssText = ` color: white; font-size: 18px; font-weight: bold; `; // add logo and title header.appendChild(logo); header.appendChild(title); // add header popup.appendChild(header); // stuff for 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, disabled: false, }, { name: "Available Space", tooltip: "**Filters out servers which are full.** Servers with space will only be shown.", experimental: false, disabled: 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, disabled: 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, disabled: 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.", disabled: 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", disabled: 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, disabled: 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.", disabledExplanation: "Does not work anymore.", experimental: false, disabled: true, }, ]; // 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; `; // tooltip on the right side 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 experimental explanation tooltip (left side) const 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); // show on hover buttonContainer.addEventListener('mouseenter', () => { experimentalTooltip.style.display = 'block'; }); buttonContainer.addEventListener('mouseleave', () => { experimentalTooltip.style.display = 'none'; }); } // appent main tooltip buttonContainer.appendChild(tooltip); // button text next top cointyainer buttonContentWrapper.appendChild(buttonText); // content wrapper to button contadiner buttonContainer.appendChild(buttonContentWrapper); // event listerners: buttonContainer.addEventListener('mouseover', () => { tooltip.style.display = 'block'; if (data.experimental) { const expTooltip = buttonContainer.querySelector('.experimental-tooltip'); if (expTooltip) expTooltip.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) { const expTooltip = buttonContainer.querySelector('.experimental-tooltip'); if (expTooltip) expTooltip.style.display = 'none'; } if (!data.disabled) { buttonContainer.style.backgroundColor = '#393B3D'; buttonContainer.style.transform = 'translateY(0px) scale(1)'; } }); buttonContainer.addEventListener('click', () => { // no clciks on 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, 100, true); // finds 100 servers but this is for safety break; case 6: auto_join_small_server(); break; case 7: auto_join_small_server(); // for now break; } }); popup.appendChild(buttonContainer); }); // trigger the button animations after DOM insertion setTimeout(() => { const buttons = popup.querySelectorAll('.server-filter-option'); buttons.forEach((button, index) => { setTimeout(() => { button.style.transform = 'translateY(0px)'; button.style.opacity = '1'; }, index * 30); }); }, 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 = getCurrentGameId(); 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, index) => { const key = Object.keys(localStorage).filter(k => k.startsWith("ROLOCATE_recentServers_"))[index]; 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 servers delete iof empytu if (validServers.length > 0) { localStorage.setItem(key, JSON.stringify(validServers)); } else { localStorage.removeItem(key); ConsoleLogEnabled(`Deleted empty key: ${key}`); } }); // 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 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 0.15 seconds } 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 *******************************************************/ async function pickRandomServer() { if (serverIds.length > 0) { const serverRegionsPrefs = JSON.parse(localStorage.getItem('ROLOCATE_serverRegions') || '{}'); let attempts = 0; while (attempts < 100 && serverIds.length > 0) { // 100 genrous attempts const idx = Math.floor(Math.random() * serverIds.length); const randomServerId = serverIds[idx]; ConsoleLogEnabled(`Considering server: ${randomServerId}`); try { const location = await fetchServerDetails(gameId, randomServerId); const regionKey = `${location.city}_${location.country?.code}`; if (serverRegionsPrefs[regionKey] === 'banned') { ConsoleLogEnabled(`Skipping server ${randomServerId} due to banned region ${regionKey}.`); notifications(`Skipping server ${randomServerId} due to banned region ${regionKey}.`, "info", "", "1000"); // remove this one so we don't pick it again serverIds.splice(idx, 1); attempts++; continue; } } catch (e) { ConsoleLogEnabled(`Error fetching details for server ${randomServerId} during ServerHop:`, e); // remove and continue serverIds.splice(idx, 1); attempts++; continue; } // join the game instance with the selected server ID JoinServer(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}`); return; } ConsoleLogEnabled("No unbanned servers found to join. Try to enable more regions in settings!"); notifications("No unbanned servers found to join. Try to enable more regions in settings!", "error", "โš ๏ธ", "10000"); } 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 // i did some magic here now i don't know why this disabled notificatioons 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 = getCurrentGameId(); 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") { const serverCount = parseInt(window.localStorage.getItem('ROLOCATE_AutoRunServerRegionsnumber')) || 16; // fallback to 16 rebuildServerList(gameId, serverCount); // search 100 servers 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 adds the filter button, server hop button, recent servers, disables trailer autoplay, and adds monitor server button if settings are true *******************************************************/ const observer = new MutationObserver((mutations, obs) => { let trailerDisableInitialized = false; // for the dumb trailer thing const serverListOptions = document.querySelector('.server-list-options'); const playButton = document.querySelector('.btn-common-play-game-lg.btn-primary-md'); 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: ${isDarkMode(true) ? 'white' : 'black'}; 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" && !document.querySelector('.recent-servers-section')) { HandleRecentServers(); } // new condition to trigger disable trailer logic if (localStorage.getItem("ROLOCATE_disabletrailer") === "true" && !trailerDisableInitialized) { disableYouTubeAutoplayInIframes(); trailerDisableInitialized = true; } // new condition to trigger compact private server logic if (localStorage.getItem("ROLOCATE_betterprivateservers") === "true" && !document.querySelector('.rolocate-view-players-btn')) { cleanupPrivateServerCards(); } if (playButton && !document.querySelector('.rolocate-serverhop-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 = 'rolocate-serverhop-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(); }); } // for the like join confimatrion if (playButton && localStorage.getItem("ROLOCATE_joinconfirmation") === "true") { monitorPlayButton(); } 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('.rolocate-serverhop-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 const gameId = getCurrentGameId(); if (!gameId) { ConsoleLogEnabled('Could not extract gameId from URL'); notifications('Error: Failed to extract gameid. Please try again later.', 'error', 'โš ๏ธ', 5000); return; } rebuildServerList(gameId, 50, false, true); // 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 = getCurrentGameId(); // retry thing let retries = 3; let success = false; while (retries > 0 && !success) { try { // get server data 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=25`, 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); // find info on each server for (const server of data.data) { const { id: serverId, playerTokens, maxPlayers, playing } = server; // give to rbx_card function await rbx_card(serverId, playerTokens, maxPlayers, playing, gameId); } success = true; // mark as successful if no errors occurred } catch (error) { retries--; // remove 1 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 = getCurrentGameId(); // retry thing let retries = 3; let success = false; while (retries > 0 && !success) { try { // get server data 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=25`, 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); // get server info for (const server of data.data) { const { id: serverId, playerTokens, maxPlayers, playing } = server; // give to function for card creation await rbx_card(serverId, playerTokens, maxPlayers, playing, gameId); } success = true; // mark successful if no errors } catch (error) { retries--; // remove 1 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 gameId = getCurrentGameId(); if (/^\d{1,10}$/.test(gameId)) { // check if numeric // 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 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', (arrowkeybutton) => { arrowkeybutton.preventDefault(); // orevent default behavior (which might cause jumps) let newValue = parseInt(slider.value, 10); if (arrowkeybutton.key === 'ArrowLeft' || arrowkeybutton.key === 'ArrowDown') { newValue = Math.max(1, newValue - 1); // decrease by 1 } else if (arrowkeybutton.key === 'ArrowRight' || arrowkeybutton.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'; submitButton.style.transform = 'scale(1.05)'; }); submitButton.addEventListener('mouseleave', () => { submitButton.style.backgroundColor = '#1a1a1a'; submitButton.style.transform = 'scale(1)'; }); // add yeelow stuff 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 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; } // random errors handle it if (response.status < 200 || response.status >= 300) { ConsoleLogEnabled('[DEBUG] HTTP error:', response.status, response.statusText); reject(new Error(`HTTP error: ${response.status}`)); return; } // give json data af5ter parsing 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) { // make sure it actually good if (isNaN(maxPlayers) || maxPlayers < 1 || !Number.isInteger(maxPlayers)) { ConsoleLogEnabled('[DEBUG] Invalid input for maxPlayers.'); notifications('Error: Please input a valid whole number greater than or equal to 1.', 'error', 'โš ๏ธ', '5000'); return; } // disable UI elements and clear the server list Loadingbar(true); disableLoadMoreButton(); disableFilterButton(true); document.querySelector('#rbx-public-game-server-item-container').innerHTML = ''; const gameId = getCurrentGameId(); let cursor = null, serversFound = 0, serverMaxPlayers = null, isCloserToOne = null; let topDownServers = [], bottomUpServers = []; // servers collected during searches let currentDelay = 500; // initial delay of 0.5 seconds const timeLimit = 3 * 60 * 1000, startTime = Date.now(); // 3 minutes limit notifications('Will search for a maximum of 3 minutes to find a server.', 'success', '๐Ÿ”Ž', '5000'); try { while (serversFound < 16) { // check if the time limit has been exceeded if (Date.now() - startTime > timeLimit) { ConsoleLogEnabled('[DEBUG] Time limit reached. Proceeding to fallback servers.'); notifications('Warning: Time limit reached. Proceeding to fallback servers.', 'warning', 'โ—', '5000'); break; } // fetch initial data to determine serverMaxPlayers and isCloserToOne if (!serverMaxPlayers) { const initialUrl = cursor ? `https://games.roblox.com/v1/games/${gameId}/servers/public?excludeFullGames=true&limit=100&cursor=${cursor}` : `https://games.roblox.com/v1/games/${gameId}/servers/public?excludeFullGames=true&limit=100`; const initialData = await fetchServersWithRetry(initialUrl); if (initialData.data.length > 0) { serverMaxPlayers = initialData.data[0].maxPlayers; isCloserToOne = maxPlayers <= (serverMaxPlayers / 2); } else { notifications("No servers found in initial fetch.", "error", "โš ๏ธ", "5000"); ConsoleLogEnabled('[DEBUG] No servers found in initial fetch.', 'warning', 'โ—'); break; } } // vaklidate maxplayers if (maxPlayers >= serverMaxPlayers) { ConsoleLogEnabled('[DEBUG] Invalid input: maxPlayers is greater than or equal to serverMaxPlayers.'); notifications(`Error: Please input a number between 1 through ${serverMaxPlayers - 1}`, 'error', 'โš ๏ธ', '5000'); return; } // adjust the URL based on isCloserToOne const baseUrl = isCloserToOne ? `https://games.roblox.com/v1/games/${gameId}/servers/public?sortOrder=1&excludeFullGames=true&limit=100` : `https://games.roblox.com/v1/games/${gameId}/servers/public?excludeFullGames=true&limit=100`; const url = cursor ? `${baseUrl}&cursor=${cursor}` : baseUrl; const data = await fetchServersWithRetry(url); // servber lsit good? if (!Array.isArray(data.data)) { ConsoleLogEnabled('[DEBUG] Invalid server list received. Waiting 1 second before retrying...'); await delay(1000); continue; } // filter sevrers for (const server of data.data) { if (server.playing === maxPlayers) { await rbx_card(server.id, server.playerTokens, server.maxPlayers, server.playing, gameId); serversFound++; if (serversFound >= 16) break; } else if (!isCloserToOne && server.playing > maxPlayers) { topDownServers.push(server); } else if (isCloserToOne && server.playing < maxPlayers) { bottomUpServers.push(server); } } if (!data.nextPageCursor) break; cursor = data.nextPageCursor; // dynamicaic delay if (currentDelay > 150) { currentDelay = Math.max(150, currentDelay / 2); } ConsoleLogEnabled(`[DEBUG] Waiting ${currentDelay / 1000} seconds before next request...`); await delay(currentDelay); } // if no exact matches were found or time limit reached, use fallback servers if (serversFound === 0 && (topDownServers.length > 0 || bottomUpServers.length > 0)) { notifications(`There are no servers with ${maxPlayers} players. Showing servers closest to ${maxPlayers} players.`, 'warning', '๐Ÿ˜”', '8000'); topDownServers.sort((a, b) => a.playing - b.playing); bottomUpServers.sort((a, b) => b.playing - a.playing); const combinedFallback = [...topDownServers, ...bottomUpServers]; for (const server of combinedFallback) { await rbx_card(server.id, server.playerTokens, server.maxPlayers, server.playing, gameId); serversFound++; if (serversFound >= 16) break; } } if (serversFound <= 0) { notifications('No Servers Found Within The Provided Criteria', 'info', '๐Ÿ”Ž', '5000'); } } catch (error) { ConsoleLogEnabled('[DEBUG] Error in filterServersByPlayerCount:', error); } finally { Loadingbar(false); disableFilterButton(false); } } /********************************************************************************************************************************************************************************************************************************************* Functions for the 4th button *********************************************************************************************************************************************************************************************************************************************/ /******************************************************* name of function: random_servers description: Fetches servers from two different URLs, combines the results, ensures no duplicates, shuffles the list, and passes the server information to the rbx_card function in a random order. Handles 429 errors with retries. *******************************************************/ async function random_servers() { notifications('Finding Random Servers. Please wait 2-5 seconds', 'success', '๐Ÿ”Ž', '5000'); // disable the "Load More" button and show the loading bar Loadingbar(true); disableFilterButton(true); disableLoadMoreButton(); // get the game ID from the URL ik reduent function const gameId = getCurrentGameId(); 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 1.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); } } // shuffl;y it const shuffledServers = shuffleArray(uniqueServers); // get first 16 shuffled const selectedServers = shuffledServers.slice(0, 16); // random order for (const server of selectedServers) { const { id: serverId, playerTokens, maxPlayers, playing } = server; // give it to this 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) { // global styles for serverfilters to use const style = document.createElement('style'); style.textContent = ` /* Overlay for the modal background */ .overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.85); /* Solid black overlay */ z-index: 1000; /* Ensure overlay is below the popup */ opacity: 0; /* Start invisible */ animation: fadeIn 0.3s ease forwards; /* Fade-in animation */ } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } /* Popup Container for the server region */ .filter-popup { background-color: #1e1e1e; /* Darker background */ color: #ffffff; /* White text */ padding: 25px; border-radius: 12px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.5); width: 320px; max-width: 90%; position: fixed; /* Fixed positioning */ top: 50%; /* Center vertically */ left: 50%; /* Center horizontally */ transform: translate(-50%, -50%); /* Offset to truly center */ text-align: center; z-index: 1001; /* Ensure popup is above the overlay */ border: 1px solid #444; /* Subtle border */ opacity: 0; /* Start invisible */ animation: fadeInPopup 0.3s ease 0.1s forwards; /* Fade-in animation with delay */ } @keyframes fadeInPopup { from { opacity: 0; transform: translate(-50%, -55%); /* Slight upward offset */ } to { opacity: 1; transform: translate(-50%, -50%); /* Center position */ } } /* Fade-out animation for overlay and popup */ .overlay.fade-out { animation: fadeOut 0.3s ease forwards; } .filter-popup.fade-out { animation: fadeOutPopup 0.3s ease forwards; } @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } @keyframes fadeOutPopup { from { opacity: 1; transform: translate(-50%, -50%); /* Center position */ } to { opacity: 0; transform: translate(-50%, -55%); /* Slight upward offset */ } } /* Label */ .filter-popup label { display: block; margin-bottom: 12px; font-size: 16px; color: #ffffff; font-weight: 500; /* Slightly bolder text */ } /* Dropdown */ .filter-popup select { background-color: #333; /* Darker gray background */ color: #ffffff; /* White text */ padding: 10px; border-radius: 6px; border: 1px solid #555; /* Darker border */ width: 100%; margin-bottom: 12px; font-size: 14px; transition: border-color 0.3s ease; } .filter-popup select:focus { border-color: #888; /* Lighter border on focus */ outline: none; } /* Custom Input */ .filter-popup input[type="number"] { background-color: #333; /* Darker gray background */ color: #ffffff; /* White text */ padding: 10px; border-radius: 6px; border: 1px solid #555; /* Darker border */ width: 100%; margin-bottom: 12px; font-size: 14px; transition: border-color 0.3s ease; } .filter-popup input[type="number"]:focus { border-color: #888; /* Lighter border on focus */ outline: none; } /* Confirm Button */ #confirmServerCount { background-color: #444; /* Dark gray background */ color: #ffffff; /* White text */ padding: 10px 20px; border: 1px solid #666; /* Gray border */ border-radius: 6px; cursor: pointer; font-size: 14px; width: 100%; transition: background-color 0.3s ease, transform 0.2s ease; } #confirmServerCount:hover { background-color: #555; /* Lighter gray on hover */ transform: translateY(-1px); /* Slight lift effect */ } #confirmServerCount:active { transform: translateY(0); /* Reset lift effect on click */ } /* Highlighted server item */ .rbx-game-server-item.highlighted { border: 2px solid #4caf50; /* Green border */ border-radius: 8px; background-color: rgba(76, 175, 80, 0.1); /* Subtle green background */ } /* Disabled fetch button */ .fetch-button:disabled { opacity: 0.5; cursor: not-allowed; } /* Popup Header for server coutnodwn */ .popup-header { margin-bottom: 24px; text-align: left; padding: 16px; background-color: rgba(255, 255, 255, 0.05); /* Subtle background for contrast */ border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.1); /* Subtle border */ transition: background-color 0.3s ease, border-color 0.3s ease; } .popup-header:hover { background-color: rgba(255, 255, 255, 0.08); /* Slightly brighter on hover */ border-color: rgba(255, 255, 255, 0.2); } .popup-header h3 { margin: 0 0 12px 0; font-size: 22px; color: #ffffff; font-weight: 700; /* Bolder for emphasis */ letter-spacing: -0.5px; /* Tighter letter spacing for modern look */ } .popup-header p { margin: 0; font-size: 14px; color: #cccccc; line-height: 1.6; opacity: 0.9; } /* Popup Footer */ .popup-footer { margin-top: 20px; text-align: left; font-size: 14px; color: #ffcc00; /* Yellow color for warnings */ background-color: rgba(255, 204, 0, 0.15); /* Lighter yellow background */ padding: 12px; border-radius: 8px; border: 1px solid rgba(255, 204, 0, 0.15); /* Subtle border */ transition: background-color 0.3s ease, border-color 0.3s ease; } .popup-footer:hover { background-color: rgba(255, 204, 0, 0.25); /* Slightly brighter on hover */ border-color: rgba(255, 204, 0, 0.25); } .popup-footer p { margin: 0; line-height: 1.5; font-weight: 500; /* Slightly bolder for emphasis */ } /* Label */ .filter-popup label { display: block; margin-bottom: 12px; font-size: 15px; color: #ffffff; font-weight: 500; text-align: left; opacity: 0.9; /* Slightly transparent for a softer look */ transition: opacity 0.3s ease; } .filter-popup label:hover { opacity: 1; /* Fully opaque on hover */ } select:hover, select:focus { border-color: #ffffff; outline: none; } `; // add element to the document head document.head.appendChild(style); } /******************************************************* name of function: showMessage description: Shows the good looking messages on the bottom of server region search *******************************************************/ function showMessage(message) { const loadMoreButtonContainer = document.querySelector('.rbx-public-running-games-footer'); if (!loadMoreButtonContainer) { ConsoleLogEnabled("Error: 'Load More' button container not found! Ensure the element exists in the DOM."); return; } const existingMessage = loadMoreButtonContainer.querySelector('.premium-message-container'); // If message is "END", remove any existing message and exit if (message === "END") { if (existingMessage) { existingMessage.remove(); ConsoleLogEnabled("Message container removed."); } else { ConsoleLogEnabled("No message container found to remove."); } return; } // Remove existing message if present before showing a new one if (existingMessage) { existingMessage.remove(); ConsoleLogEnabled("Warning: An existing message was found and replaced."); } // Inject CSS only once if (!document.getElementById('premium-message-styles')) { const style = document.createElement('style'); style.id = 'premium-message-styles'; style.textContent = ` .premium-message-container { margin-top: 20px; padding: 18px 26px; background: linear-gradient(145deg, #2b0000, #1a0000); border-radius: 14px; box-shadow: 0 6px 20px rgba(255, 0, 0, 0.2); font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 16px; color: #ffdddd; transition: all 0.3s ease-in-out, transform 0.3s ease, box-shadow 0.3s ease; opacity: 0; animation: fadeIn 0.6s ease forwards; border: 1px solid #440000; display: flex; align-items: center; gap: 16px; cursor: default; user-select: none; } .premium-message-container:hover { transform: scale(1.015); box-shadow: 0 8px 24px rgba(255, 0, 0, 0.25); background: linear-gradient(145deg, #330000, #220000); color: #ffe5e5; } .premium-message-logo { width: 28px; height: 28px; border-radius: 6px; object-fit: contain; box-shadow: 0 0 8px rgba(255, 0, 0, 0.2); background-color: #000; } .premium-message-text { flex: 1; text-align: left; font-weight: 500; letter-spacing: 0.3px; } @keyframes fadeIn { to { opacity: 1; } } `; document.head.appendChild(style); } // Create the message container const container = document.createElement('div'); container.className = 'premium-message-container'; // Create and insert the logo const logo = document.createElement('img'); logo.className = 'premium-message-logo'; logo.src = window.Base64Images.logo; // Create and insert the message text const messageText = document.createElement('div'); messageText.className = 'premium-message-text'; messageText.textContent = message; // Build the full component container.appendChild(logo); container.appendChild(messageText); loadMoreButtonContainer.appendChild(container); ConsoleLogEnabled("Message displayed successfully:", message); return container; } /******************************************************* name of function: delay description: custom delay also known as sleep function in js cause this language sucks and doesent have a default built-in sleep. *******************************************************/ function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /******************************************************* name of function: createServerCountPopup description: Creates the first time popup and allows user to pick the amount of servers they want. *******************************************************/ // WARNING: Do not republish this script. Licensed for personal use only. function createServerCountPopup(callback) { const overlay = document.createElement('div'); overlay.className = 'overlay'; const popup = document.createElement('div'); popup.className = 'filter-popup'; // get current player count preference from localStorage const currentPlayerCountPreference = localStorage.getItem('ROLOCATE_invertplayercount'); const isLowPlayerCount = currentPlayerCountPreference === 'true'; // inject styles for dropdown icon and mobile responsiveness const style = document.createElement('style'); style.textContent = ` .overlay { z-index: 10000; } .filter-popup { width: 90%; max-width: 460px; max-height: 90vh; margin: 0 auto; box-sizing: border-box; overflow-y: auto; z-index: 10001; } .filter-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 15px; } @media (max-width: 600px) { .filter-grid { grid-template-columns: 1fr; gap: 15px; } .filter-popup { width: 95%; padding: 20px 15px; } .popup-header h3 { font-size: 18px; } .popup-header p { font-size: 13px; } .popup-footer p { font-size: 12px; } } /* Very small screens */ @media (max-width: 400px) { .filter-popup { width: 98%; padding: 15px 10px; } .popup-header h3 { font-size: 16px; } .filter-section label { font-size: 13px; } select, input, button { font-size: 13px; } } .dropdown-wrapper { position: relative; display: inline-block; width: 100%; } .dropdown-wrapper select { width: 100%; padding-right: 30px; appearance: none; -webkit-appearance: none; -moz-appearance: none; box-sizing: border-box; } .dropdown-wrapper .dropdown-icon { position: absolute; right: 10px; top: 40%; transform: translateY(-50%); pointer-events: none; font-size: 12px; color: #fff; } .filter-section label { display: block; margin-bottom: 5px; font-weight: 600; } #cancelServerCount { background-color: #2a1f1f; border: 1px solid #3d2626; border-radius: 6px; font-size: 14px; cursor: pointer; transition: background-color 0.3s ease, transform 0.2s ease; } #cancelServerCount:hover { background-color: #332222; transform: translateY(-1px); } #cancelServerCount:active { transform: translateY(0); } /* Ensure buttons are touch-friendly on mobile */ @media (max-width: 600px) { button { padding: 12px; min-height: 44px; } } `; document.head.appendChild(style); popup.innerHTML = `
    `; document.body.appendChild(overlay); document.body.appendChild(popup); const serverCountDropdown = popup.querySelector('#serverCount'); const customServerCountInput = popup.querySelector('#customServerCount'); const playerCountFilter = popup.querySelector('#playerCountFilter'); const confirmButton = popup.querySelector('#confirmServerCount'); const cancelButton = popup.querySelector('#cancelServerCount'); serverCountDropdown.addEventListener('change', () => { if (serverCountDropdown.value === 'custom') { customServerCountInput.style.display = 'block'; } else { customServerCountInput.style.display = 'none'; } }); confirmButton.addEventListener('click', () => { let serverCount; if (serverCountDropdown.value === 'custom') { serverCount = parseInt(customServerCountInput.value); if (isNaN(serverCount) || serverCount < 1 || serverCount > 2000) { notifications('Error: Please enter a valid number between 1 and 2000.', 'error', 'โš ๏ธ', '5000'); return; } } else { serverCount = parseInt(serverCountDropdown.value); } const playerCountPreference = playerCountFilter.value; localStorage.setItem('ROLOCATE_invertplayercount', playerCountPreference === 'low' ? 'true' : 'false'); callback(serverCount); disableFilterButton(true); disableLoadMoreButton(true); hidePopup(); Loadingbar(true); }); cancelButton.addEventListener('click', () => { hidePopup(); }); function hidePopup() { const overlay = document.querySelector('.overlay'); const popup = document.querySelector('.filter-popup'); overlay.classList.add('fade-out'); popup.classList.add('fade-out'); setTimeout(() => { overlay.remove(); popup.remove(); }, 300); } } /******************************************************* name of function: fetchPublicServers description: Function to fetch public servers with rate limtiing and stuff (Server regions) *******************************************************/ // WARNING: Do not republish this script. Licensed for personal use only. async function fetchPublicServers(gameId, totalLimit) { let servers = []; let cursor = null; let delayTime = 250; // Start with 0.25 seconds let retryingDueToRateLimit = false; let pageCount = 0; const invertPlayerCount = localStorage.getItem("ROLOCATE_invertplayercount") === "true"; ConsoleLogEnabled(`Starting to fetch up to ${totalLimit} public servers for game ${gameId}...`); ConsoleLogEnabled(`Invert player count: ${invertPlayerCount}`); while (servers.length < totalLimit) { const url = `https://games.roblox.com/v1/games/${gameId}/servers/public?excludeFullGames=true&limit=100${invertPlayerCount ? '&sortOrder=1' : ''}${cursor ? `&cursor=${cursor}` : ''}`; pageCount++; ConsoleLogEnabled(`Fetching page ${pageCount}... (Current delay: ${delayTime}ms)`); let responseData; try { responseData = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, onload: function(response) { if (response.status === 429 || !response.responseText) { reject({ rateLimited: true }); } else { try { const json = JSON.parse(response.responseText); resolve(json); } catch (err) { reject({ rateLimited: true }); } } }, onerror: function(error) { reject({ rateLimited: false, error }); }, }); }); if (retryingDueToRateLimit) { delayTime = 250; retryingDueToRateLimit = false; ConsoleLogEnabled(`Rate limit cleared. Resuming normal delay (${delayTime}ms).`); } const newServers = responseData.data || []; servers = servers.concat(newServers); ConsoleLogEnabled(`Fetched ${newServers.length} servers (Total: ${servers.length}/${totalLimit})`); if (!responseData.nextPageCursor || servers.length >= totalLimit) { ConsoleLogEnabled("No more pages or reached limit."); break; } cursor = responseData.nextPageCursor; } catch (err) { if (err.rateLimited) { delayTime = 750; retryingDueToRateLimit = true; ConsoleLogEnabled("โš ๏ธ Rate limited. Increasing delay to 0.75s..."); } else { ConsoleLogEnabled("โŒ Failed to fetch due to error:", err.error); break; } } await delay(delayTime); } ConsoleLogEnabled(`โœ… Done. Fetched ${servers.length} servers in total.`); return servers.slice(0, totalLimit); } /******************************************************* name of function: createFilterDropdowns description: Creates the server selecting dropdown with country flags. *******************************************************/ function createFilterDropdowns(servers) { // get flag data getFlagEmoji(); // load flag data without country code const serverRegionsPrefs = JSON.parse(localStorage.getItem('ROLOCATE_serverRegions') || '{}'); // create the main filter container with premium styling const filterContainer = document.createElement('div'); Object.assign(filterContainer.style, { display: 'flex', gap: '16px', alignItems: 'center', padding: '20px 24px', background: 'linear-gradient(145deg, rgba(12,12,12,0.98) 0%, rgba(8,8,8,0.98) 25%, rgba(15,10,10,0.98) 75%, rgba(10,8,8,0.98) 100%)', borderRadius: '28px', boxShadow: '0 32px 64px rgba(0,0,0,0.6), 0 0 0 1px rgba(200,30,30,0.15), inset 0 1px 0 rgba(255,255,255,0.02)', opacity: '0', transform: 'translateY(-50px) scale(0.94)', transition: 'all 1.2s cubic-bezier(0.16, 1, 0.3, 1)', position: 'relative', border: '1px solid rgba(200,30,30,0.12)', margin: '40px', fontFamily: "'Inter', 'SF Pro Display', system-ui, -apple-system, sans-serif", fontSize: '16px', overflow: 'hidden' }); // premium animated border with subtle red glow const borderGlow = document.createElement('div'); Object.assign(borderGlow.style, { position: 'absolute', inset: '-2px', borderRadius: '30px', pointerEvents: 'none', background: 'linear-gradient(60deg, rgba(200,25,25,0.25), rgba(50,50,50,0.1), rgba(200,25,25,0.15), rgba(30,30,30,0.1), rgba(200,25,25,0.2))', backgroundSize: '300% 300%', zIndex: '-1', animation: 'premiumFlow 20s ease infinite', opacity: '0.7' }); filterContainer.appendChild(borderGlow); // add premium CSS animations and styling const style = document.createElement('style'); style.textContent = ` @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); @keyframes premiumFlow { 0% { background-position: 0% 50%; transform: rotate(0deg); } 25% { background-position: 100% 25%; } 50% { background-position: 100% 100%; transform: rotate(0.5deg); } 75% { background-position: 0% 75%; } 100% { background-position: 0% 50%; transform: rotate(0deg); } } @keyframes premiumPulse { 0% { box-shadow: 0 0 0 0 rgba(200, 30, 30, 0.4); } 50% { box-shadow: 0 0 0 20px rgba(200, 30, 30, 0); } 100% { box-shadow: 0 0 0 0 rgba(200, 30, 30, 0); } } @keyframes shimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } @keyframes iconFloat { 0%, 100% { transform: translateY(0px); } 50% { transform: translateY(-2px); } } .premium-select { scrollbar-width: thin; scrollbar-color: rgba(200,30,30,0.6) rgba(20,20,20,0.4); } .premium-select::-webkit-scrollbar { width: 6px; } .premium-select::-webkit-scrollbar-track { background: rgba(15,15,15,0.8); border-radius: 10px; } .premium-select::-webkit-scrollbar-thumb { background: linear-gradient(180deg, rgba(200,30,30,0.8), rgba(150,25,25,0.6)); border-radius: 10px; border: 1px solid rgba(0,0,0,0.2); } .premium-select::-webkit-scrollbar-thumb:hover { background: linear-gradient(180deg, rgba(220,35,35,0.9), rgba(170,30,30,0.7)); } .logo-premium-pulse { animation: premiumPulse 3s infinite; } .shimmer-effect { position: relative; overflow: hidden; } .shimmer-effect::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.08), transparent); animation: shimmer 3s infinite; } .premium-icon { animation: iconFloat 3s ease-in-out infinite; } /* for the confetti easter egg */ @keyframes fall { to { transform: translateY(100vh) rotate(360deg); opacity: 0; } } .flag-image { width: 26px !important; /* Slightly larger */ height: 20px !important; /* Slightly larger */ object-fit: cover; object-position: center; overflow: hidden; border-radius: 3px; box-shadow: 0 2px 4px rgba(0,0,0,0.3); flex-shrink: 0; /* Crop the edges to hide outline */ clip-path: inset(1px 1px 1px 1px); } /* Custom select styling for flags */ .premium-select option { padding: 12px 16px; background: rgba(15,15,15,0.98) !important; color: rgba(200,30,30,0.9) !important; border-radius: 8px; margin: 2px; display: flex; align-items: center; } `; document.head.appendChild(style); // easter egg :) const createConfetti = () => { const c = document.createElement('div'); Object.assign(c.style, {position: 'fixed', top: '0', left: '0', width: '100%', height: '100%', pointerEvents: 'none', zIndex: '9999'}); document.body.appendChild(c); for (let i = 0; i < 20; i++) { const p = document.createElement('div'); Object.assign(p.style, {position: 'absolute', width: '8px', height: '8px', backgroundColor: ['#c81e1e','#ff3333','#fff'][i%3], top: '-20px', left: Math.random()*100+'%', animation: `fall ${2+Math.random()*2}s linear ${Math.random()*0.3}s forwards`}); c.appendChild(p); } setTimeout(() => document.body.removeChild(c), 4500); }; // colors const logoWrapper = document.createElement('div'); Object.assign(logoWrapper.style, { position: 'relative', marginRight: '36px', display: 'flex', alignItems: 'center', cursor: 'pointer' }); const logoContainer = document.createElement('div'); Object.assign(logoContainer.style, { position: 'relative', padding: '8px', borderRadius: '20px', background: 'linear-gradient(145deg, rgba(25,25,25,0.8), rgba(15,15,15,0.9))', border: '1px solid rgba(200,30,30,0.2)', transition: 'all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1)' }); const logo = document.createElement('img'); logo.src = window.Base64Images.logo; Object.assign(logo.style, { width: '64px', height: '64px', borderRadius: '14px', transition: 'all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1)', filter: 'drop-shadow(0 12px 24px rgba(200,30,30,0.4))', border: '2px solid rgba(200,30,30,0.3)', }); const logoGlow = document.createElement('div'); Object.assign(logoGlow.style, { position: 'absolute', inset: '-6px', borderRadius: '24px', background: 'radial-gradient(circle at center, rgba(200,30,30,0.5) 0%, rgba(200,30,30,0.1) 50%, transparent 70%)', opacity: '0', transition: 'all 0.6s ease', pointerEvents: 'none', zIndex: '-1', }); // Premium logo interactions logoContainer.addEventListener('mouseover', () => { logo.style.transform = 'rotate(-6deg) scale(1.12)'; logo.style.filter = 'drop-shadow(0 16px 32px rgba(200,30,30,0.6))'; logo.style.border = '2px solid rgba(200,30,30,0.7)'; logoContainer.style.background = 'linear-gradient(145deg, rgba(35,35,35,0.9), rgba(20,20,20,0.95))'; logoContainer.style.border = '1px solid rgba(200,30,30,0.4)'; logoGlow.style.opacity = '1'; logo.classList.add('logo-premium-pulse'); }); logoContainer.addEventListener('mouseout', () => { logo.style.transform = 'rotate(0) scale(1)'; logo.style.filter = 'drop-shadow(0 12px 24px rgba(200,30,30,0.4))'; logo.style.border = '2px solid rgba(200,30,30,0.3)'; logoContainer.style.background = 'linear-gradient(145deg, rgba(25,25,25,0.8), rgba(15,15,15,0.9))'; logoContainer.style.border = '1px solid rgba(200,30,30,0.2)'; logoGlow.style.opacity = '0'; logo.classList.remove('logo-premium-pulse'); logoContainer.addEventListener('click', () => { createConfetti(); }); }); logoContainer.appendChild(logoGlow); logoContainer.appendChild(logo); logoWrapper.appendChild(logoContainer); filterContainer.appendChild(logoWrapper); // create icons const createIcon = (type) => { const iconMap = { globe: ``, city: ``, version: ``, chevron: `` }; return iconMap[type] || ''; }; // function to get country code from country name const getCountryCode = (countryName) => { // name to code name 2 letter name yea daskdha const countryCodeMap = { 'Australia': 'AU', 'Brazil': 'BR', 'Germany': 'DE', 'France': 'FR', 'United Kingdom': 'GB', 'Hong Kong': 'HK', 'India': 'IN', 'Japan': 'JP', 'Netherlands': 'NL', 'Poland': 'PL', 'Singapore': 'SG', 'United States': 'US', 'Ireland': 'IE' }; // Return the country code or the first two letters of the country name as fallback return countryCodeMap[countryName] || countryName.substring(0, 2).toUpperCase(); }; // Function to create a premium dropdown with enhanced styling and icons const createDropdown = (id, placeholder, iconType) => { const wrapper = document.createElement('div'); Object.assign(wrapper.style, { position: 'relative', minWidth: '200px', flex: '1' }); // Premium label with icon const labelContainer = document.createElement('div'); Object.assign(labelContainer.style, { display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '14px', opacity: '0', transform: 'translateX(-10px)', transition: 'all 0.6s ease' }); const labelIcon = document.createElement('span'); labelIcon.innerHTML = createIcon(iconType); labelIcon.className = 'premium-icon'; Object.assign(labelIcon.style, { color: 'rgba(200,30,30,0.8)', display: 'flex', alignItems: 'center', filter: 'drop-shadow(0 2px 4px rgba(200,30,30,0.3))' }); const label = document.createElement('div'); label.textContent = placeholder.replace('All ', '').toUpperCase(); Object.assign(label.style, { color: 'rgba(255,255,255,0.85)', fontSize: '13px', fontWeight: '600', letterSpacing: '1px', transition: 'all 0.4s ease', fontFamily: "'Inter', sans-serif" }); labelContainer.appendChild(labelIcon); labelContainer.appendChild(label); wrapper.appendChild(labelContainer); // Premium dropdown with enhanced design const dropdownContainer = document.createElement('div'); dropdownContainer.className = 'shimmer-effect'; Object.assign(dropdownContainer.style, { position: 'relative', borderRadius: '16px', background: 'linear-gradient(145deg, rgba(20,20,20,0.95), rgba(12,12,12,0.98))', border: '1px solid rgba(200,30,30,0.15)', overflow: 'hidden', transition: 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)', boxShadow: '0 12px 24px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.03)' }); const dropdown = document.createElement('select'); dropdown.id = id; dropdown.className = 'premium-select'; dropdown.innerHTML = ``; Object.assign(dropdown.style, { width: '100%', padding: '20px 60px 20px 28px', fontSize: '16px', fontWeight: '500', background: 'transparent', color: 'rgba(200,30,30,0.95)', border: 'none', borderRadius: '16px', appearance: 'none', cursor: 'pointer', transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)', opacity: '0', transform: 'translateY(-25px)', letterSpacing: '0.4px', fontFamily: "'Inter', sans-serif", outline: 'none' }); // Premium chevron with enhanced styling const chevronContainer = document.createElement('div'); Object.assign(chevronContainer.style, { position: 'absolute', right: '20px', top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none', transition: 'all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1)', color: 'rgba(200,30,30,0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '6px', borderRadius: '8px', background: 'rgba(200,30,30,0.1)', border: '1px solid rgba(200,30,30,0.2)' }); chevronContainer.innerHTML = createIcon('chevron'); // Enhanced dropdown interactions with premium effects const addHoverEffect = () => { dropdownContainer.style.background = 'linear-gradient(145deg, rgba(30,30,30,0.98), rgba(18,18,18,1))'; dropdownContainer.style.boxShadow = '0 20px 40px rgba(0,0,0,0.5), 0 0 0 2px rgba(200,30,30,0.3), inset 0 1px 0 rgba(255,255,255,0.05)'; dropdownContainer.style.border = '1px solid rgba(200,30,30,0.3)'; dropdownContainer.style.transform = 'translateY(-2px)'; label.style.color = 'rgba(200,30,30,0.95)'; labelIcon.style.color = 'rgba(200,30,30,1)'; chevronContainer.style.transform = 'translateY(-50%) rotate(180deg)'; chevronContainer.style.background = 'rgba(200,30,30,0.2)'; chevronContainer.style.border = '1px solid rgba(200,30,30,0.4)'; }; const removeHoverEffect = () => { if (document.activeElement !== dropdown) { dropdownContainer.style.background = 'linear-gradient(145deg, rgba(20,20,20,0.95), rgba(12,12,12,0.98))'; dropdownContainer.style.boxShadow = '0 12px 24px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.03)'; dropdownContainer.style.border = '1px solid rgba(200,30,30,0.15)'; dropdownContainer.style.transform = 'translateY(0)'; label.style.color = 'rgba(255,255,255,0.85)'; labelIcon.style.color = 'rgba(200,30,30,0.8)'; chevronContainer.style.transform = 'translateY(-50%) rotate(0deg)'; chevronContainer.style.background = 'rgba(200,30,30,0.1)'; chevronContainer.style.border = '1px solid rgba(200,30,30,0.2)'; } }; dropdownContainer.addEventListener('mouseover', addHoverEffect); dropdownContainer.addEventListener('mouseout', removeHoverEffect); dropdown.addEventListener('focus', () => { dropdownContainer.style.outline = 'none'; dropdownContainer.style.border = '1px solid rgba(200,30,30,0.5)'; dropdownContainer.style.boxShadow = '0 20px 40px rgba(0,0,0,0.5), 0 0 0 4px rgba(200,30,30,0.25), inset 0 1px 0 rgba(255,255,255,0.05)'; label.style.color = 'rgba(200,30,30,1)'; labelIcon.style.color = 'rgba(200,30,30,1)'; chevronContainer.style.transform = 'translateY(-50%) rotate(180deg)'; }); dropdown.addEventListener('blur', removeHoverEffect); dropdown.addEventListener('change', () => { // Premium selection animation dropdownContainer.style.transform = 'translateY(-2px) scale(0.98)'; setTimeout(() => { dropdownContainer.style.transform = 'translateY(-2px) scale(1)'; }, 150); // Enhanced flash effect const flash = document.createElement('div'); Object.assign(flash.style, { position: 'absolute', inset: '0', borderRadius: '16px', background: 'linear-gradient(145deg, rgba(200,30,30,0.2), rgba(200,30,30,0.1))', pointerEvents: 'none', opacity: '0', transition: 'opacity 0.4s ease' }); dropdownContainer.appendChild(flash); flash.style.opacity = '1'; setTimeout(() => { flash.style.opacity = '0'; setTimeout(() => dropdownContainer.removeChild(flash), 400); }, 80); }); // Staggered fade-in animation setTimeout(() => { labelContainer.style.opacity = '1'; labelContainer.style.transform = 'translateX(0)'; }, 400); setTimeout(() => { dropdown.style.opacity = '1'; dropdown.style.transform = 'translateY(0)'; }, 600); dropdownContainer.appendChild(dropdown); dropdownContainer.appendChild(chevronContainer); wrapper.appendChild(dropdownContainer); return wrapper; }; // Create premium dropdowns with icons const countryDropdown = createDropdown('countryFilter', 'All Countries', 'globe'); const cityDropdown = createDropdown('cityFilter', 'All Cities', 'city'); const versionDropdown = createDropdown('versionFilter', 'Server Versions', 'version'); // glitch somehwre ein the code but idc // server data and flasgs const countryCounts = {}; const countryServerMap = {}; // store server ifno for each one servers.forEach(server => { const country = server.location.country.name; countryCounts[country] = (countryCounts[country] || 0) + 1; if (!countryServerMap[country]) { countryServerMap[country] = server; // store first server for country code reference } }); const sortedCountries = Object.keys(countryCounts).sort(); const countrySelect = countryDropdown.querySelector('select'); sortedCountries.forEach(country => { const option = document.createElement('option'); option.value = country; // Try to get country code from server data first, then fallback to mapping let countryCode; const server = countryServerMap[country]; if (server && server.location.country.code) { countryCode = server.location.country.code; } else { countryCode = getCountryCode(country); } // Create flag element try { const flagImg = getFlagEmoji(countryCode); if (flagImg) { flagImg.className = 'flag-image'; // Since we can't directly add HTML to option text, we'll use a data attribute // and handle the display with CSS or JavaScript option.setAttribute('data-flag-src', flagImg.src); option.setAttribute('data-country-code', countryCode); option.textContent = `${country} (${countryCounts[country]})`; // mark country if all or some regions inside are banned try { const serversForCountry = servers.filter(s => s.location.country.name === country); const allBanned = serversForCountry.length > 0 && serversForCountry.every(s => serverRegionsPrefs[`${s.location.city}_${s.location.country?.code}`] === 'banned'); const someBanned = serversForCountry.some(s => serverRegionsPrefs[`${s.location.city}_${s.location.country?.code}`] === 'banned'); if (allBanned) { option.textContent += ' โ€” BANNED'; option.style.color = '#ff6b6b'; } else if (someBanned) { option.textContent += ' โ€” PARTIAL BANS'; option.style.color = '#f59e0b'; } } catch (e) { ConsoleLogEnabled('Error checking banned status for country dropdown:', e); } } } catch (error) { ConsoleLogEnabled(`Could not load flag for ${country} (${countryCode}):`, error); option.textContent = `${country} (${countryCounts[country]})`; } Object.assign(option.style, { background: 'rgba(15,15,15,0.98)', color: 'rgba(200,30,30,0.9)', padding: '12px', borderRadius: '8px', margin: '2px' }); countrySelect.appendChild(option); }); // Create a custom dropdown display that shows flags const createCustomDropdownDisplay = (selectElement) => { const customDisplay = document.createElement('div'); Object.assign(customDisplay.style, { position: 'absolute', top: '0', left: '0', right: '0', bottom: '0', display: 'flex', alignItems: 'center', padding: '20px 60px 20px 28px', pointerEvents: 'none', zIndex: '1', color: 'rgba(200,30,30,0.95)', fontSize: '16px', fontWeight: '500', letterSpacing: '0.4px', fontFamily: "'Inter', sans-serif" }); const updateDisplay = () => { const selectedOption = selectElement.options[selectElement.selectedIndex]; if (selectedOption && selectedOption.getAttribute('data-flag-src')) { const flagSrc = selectedOption.getAttribute('data-flag-src'); const countryCode = selectedOption.getAttribute('data-country-code'); customDisplay.innerHTML = ` ${countryCode} ${selectedOption.textContent} `; } else { customDisplay.textContent = selectedOption ? selectedOption.textContent : selectElement.options[0].textContent; } }; selectElement.addEventListener('change', updateDisplay); updateDisplay(); // Initial display return customDisplay; }; // add custom display to country dropdown const countryDropdownContainer = countryDropdown.querySelector('.shimmer-effect'); const countryCustomDisplay = createCustomDropdownDisplay(countrySelect); countryDropdownContainer.appendChild(countryCustomDisplay); // make it transparent countrySelect.addEventListener('change', () => { if (countrySelect.value) { countrySelect.style.color = 'transparent'; } else { countrySelect.style.color = 'rgba(200,30,30,0.95)'; } }); // premium separator with gradient const separator = document.createElement('div'); Object.assign(separator.style, { height: '80px', width: '2px', background: 'linear-gradient(to bottom, rgba(255,255,255,0), rgba(200,30,30,0.4) 20%, rgba(200,30,30,0.6) 50%, rgba(200,30,30,0.4) 80%, rgba(255,255,255,0))', margin: '0 8px', borderRadius: '2px', position: 'relative', overflow: 'hidden' }); // Add subtle animation to separator const separatorGlow = document.createElement('div'); Object.assign(separatorGlow.style, { position: 'absolute', inset: '0', background: 'linear-gradient(to bottom, transparent, rgba(200,30,30,0.8), transparent)', animation: 'shimmer 4s infinite', opacity: '0.3' }); separator.appendChild(separatorGlow); // Enhanced country change handler with flag support countrySelect.addEventListener('change', () => { const selectedCountry = countrySelect.value; const citySelect = cityDropdown.querySelector('select'); citySelect.innerHTML = ''; if (selectedCountry) { const cityCounts = {}; const cityCountryMap = {}; servers .filter(server => server.location.country.name === selectedCountry) .forEach(server => { const city = server.location.city; const region = server.location.region?.name; const cityKey = region ? `${city}, ${region}` : city; cityCounts[cityKey] = (cityCounts[cityKey] || 0) + 1; cityCountryMap[cityKey] = server.location.country.code; }); const sortedCities = Object.keys(cityCounts).sort(); sortedCities.forEach(city => { const option = document.createElement('option'); option.value = city; // annotate banned cities const countryCodeForCity = cityCountryMap[city]; const cityNameOnly = city.split(',')[0].trim(); const regionKey = `${cityNameOnly}_${countryCodeForCity}`; option.textContent = `${city} (${cityCounts[city]})` + (serverRegionsPrefs[regionKey] === 'banned' ? ' โ€” BANNED' : ''); if (serverRegionsPrefs[regionKey] === 'banned') { option.style.color = '#ff6b6b'; } Object.assign(option.style, { background: 'rgba(15,15,15,0.98)', color: 'rgba(200,30,30,0.9)', padding: '12px' }); citySelect.appendChild(option); }); // Premium update animation const cityContainer = cityDropdown.querySelector('div'); cityContainer.style.opacity = '0.4'; cityContainer.style.transform = 'translateY(-15px)'; setTimeout(() => { cityContainer.style.opacity = '1'; cityContainer.style.transform = 'translateY(0)'; }, 200); // Visual update indicator const updateRipple = document.createElement('div'); Object.assign(updateRipple.style, { position: 'absolute', inset: '0', borderRadius: '16px', background: 'radial-gradient(circle at center, rgba(200,30,30,0.3) 0%, rgba(200,30,30,0.1) 40%, transparent 70%)', pointerEvents: 'none', opacity: '1', transition: 'all 1s ease', transform: 'scale(0.8)' }); cityDropdown.style.position = 'relative'; cityDropdown.appendChild(updateRipple); setTimeout(() => { updateRipple.style.opacity = '0'; updateRipple.style.transform = 'scale(1.2)'; setTimeout(() => cityDropdown.removeChild(updateRipple), 1000); }, 100); } // update theis populateVersionOptions(countrySelect.value, citySelect.value); }); // function to do stuff like picmversioin // hopefulyl works no buggy wuggys const versionSelect = versionDropdown.querySelector('select'); function populateVersionOptions(countryValue, cityValue) { const prevValue = versionSelect.value; versionSelect.innerHTML = ''; let candidateServers = servers; if (countryValue) candidateServers = candidateServers.filter(s => s.location.country.name === countryValue); if (cityValue) { candidateServers = candidateServers.filter(s => `${s.location.city}${s.location.region?.name ? `, ${s.location.region.name}` : ''}` === cityValue); } const versionsSet = new Set(); candidateServers.forEach(s => { const v = s.location.placeVersion; if (v != null && v !== 'N/A') versionsSet.add(Number(v)); }); const versions = Array.from(versionsSet).sort((a,b) => b - a); // newest on top to oldest if (versions.length === 0) return; // safety const min = versions[versions.length - 1]; const max = versions[0]; versions.forEach(v => { const option = document.createElement('option'); option.value = String(v); // label newest and oldest if (v === max) { option.textContent = `Newest (${v})`; } else if (v === min) { option.textContent = `Oldest (${v})`; } else { option.textContent = String(v); } Object.assign(option.style, { background: 'rgba(15,15,15,0.98)', color: 'rgba(200,30,30,0.9)', padding: '12px' }); versionSelect.appendChild(option); }); // try to restore previous value if still available if (prevValue && Array.from(versionSelect.options).some(o => o.value === prevValue)) { versionSelect.value = prevValue; } }; // on city change update versions so no bugs const citySelect = cityDropdown.querySelector('select'); citySelect.addEventListener('change', () => { populateVersionOptions(countrySelect.value, citySelect.value); }); // elemtns inside so country city and version filterContainer.appendChild(countryDropdown); filterContainer.appendChild(cityDropdown); filterContainer.appendChild(versionDropdown); // add versions in populateVersionOptions('', ''); // Premium container entrance animation setTimeout(() => { filterContainer.style.opacity = '1'; filterContainer.style.transform = 'translateY(0) scale(1)'; }, 200); return filterContainer; } /******************************************************* name of function: filterServers description: Function to filter servers based on selected country and city cause im lazy *******************************************************/ function filterServers(servers, country, city, version) { if (!filterServers.index || filterServers.lastServers !== servers) { filterServers.index = new Map(); filterServers.lastServers = servers; for (const s of servers) { const countryName = s.location.country.name; const cityName = `${s.location.city}${s.location.region?.name ? `, ${s.location.region.name}` : ''}`; // hopefully i can remember later on what this does const cKey = `country:${countryName}`; const cityKey = `city:${countryName}:${cityName}`; const placeVersion = s.location.placeVersion; const hasValidVersion = placeVersion != null && placeVersion !== 'N/A'; const versionStr = hasValidVersion ? String(placeVersion) : null; const vKey = versionStr ? `version:${versionStr}` : null; const cVKey = versionStr ? `version:${countryName}:${versionStr}` : null; const cityVKey = versionStr ? `version:${countryName}:${cityName}:${versionStr}` : null; // holy if spam if (!filterServers.index.has(cKey)) { filterServers.index.set(cKey, []); } if (!filterServers.index.has(cityKey)) { filterServers.index.set(cityKey, []); } if (vKey && !filterServers.index.has(vKey)) { filterServers.index.set(vKey, []); } if (cVKey && !filterServers.index.has(cVKey)) { filterServers.index.set(cVKey, []); } if (cityVKey && !filterServers.index.has(cityVKey)) { filterServers.index.set(cityVKey, []); } filterServers.index.get(cKey).push(s); filterServers.index.get(cityKey).push(s); if (vKey) { filterServers.index.get(vKey).push(s); } if (cVKey) { filterServers.index.get(cVKey).push(s); } if (cityVKey) { filterServers.index.get(cityVKey).push(s); } } } if (country && city && version) { return filterServers.index.get(`version:${country}:${city}:${version}`) || []; } if (country && city) { return filterServers.index.get(`city:${country}:${city}`) || []; } if (country && version) { return filterServers.index.get(`version:${country}:${version}`) || []; } if (version) { return filterServers.index.get(`version:${version}`) || []; } if (country) { return filterServers.index.get(`country:${country}`) || []; } return servers; } /******************************************************* name of function: fetchPlayerThumbnails_servers description: finds player thumbnails (Server regions) - now returns promise *******************************************************/ const fetchPlayerThumbnails_servers = (() => { const queue = []; let processing = false; return async function(playerTokens) { ConsoleLogEnabled("Function called with playerTokens:", playerTokens); const waitHalfSecond = (ms = 250) => new Promise(res => setTimeout(res, ms)); return new Promise(resolve => { ConsoleLogEnabled("Pushing to queue:", playerTokens); queue.push({ playerTokens, resolve }); const processQueue = async () => { if (processing) { ConsoleLogEnabled("Already processing, exiting..."); return; } processing = true; ConsoleLogEnabled("Started processing queue..."); while (queue.length > 0) { const { playerTokens, resolve } = queue.shift(); ConsoleLogEnabled("Processing batch:", playerTokens); const body = playerTokens.map(token => ({ requestId: `0:${token}:AvatarHeadshot:150x150:png:regular`, type: "AvatarHeadShot", targetId: 0, token, format: "png", size: "150x150", })); let success = false; let data = []; while (!success) { ConsoleLogEnabled("Sending request to thumbnails.roblox.com..."); const response = await fetch("https://thumbnails.roblox.com/v1/batch", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify(body), }); ConsoleLogEnabled("Response status:", response.status); if (response.status === 429) { ConsoleLogEnabled("Rate limited. Waiting..."); await waitHalfSecond(); } else { const json = await response.json(); data = json.data || []; success = true; ConsoleLogEnabled("Received data:", data); } } resolve(data); ConsoleLogEnabled("Resolved promise with data"); } processing = false; ConsoleLogEnabled("Finished processing queue."); }; processQueue(); }); }; })(); /******************************************************* name of function: updateServerCardThumbnails description: updates thumbnails for a specific server card after they're loaded *******************************************************/ function updateServerCardThumbnails(serverId, playerThumbnails, maxPlayers, currentPlayers) { const serverCard = document.querySelector(`[data-server-id="${serverId}"]`); if (!serverCard) return; const thumbnailsContainer = serverCard.querySelector('.player-thumbnails-container'); if (!thumbnailsContainer) return; // Clear existing content (mock thumbnails) thumbnailsContainer.innerHTML = ''; // Add real player thumbnails const maxThumbnails = 5; const displayedThumbnails = playerThumbnails.slice(0, maxThumbnails); displayedThumbnails.forEach(thumb => { if (thumb && thumb.imageUrl) { const img = document.createElement("img"); img.src = thumb.imageUrl; img.className = "avatar-card-image"; img.style.width = "60px"; img.style.height = "60px"; img.style.borderRadius = "50%"; img.style.transition = "opacity 0.3s ease"; img.style.opacity = "0"; thumbnailsContainer.appendChild(img); img.onload = () => { img.style.opacity = "1"; }; } }); // Add placeholder for hidden players const hiddenPlayers = currentPlayers - displayedThumbnails.length; if (hiddenPlayers > 0) { const placeholder = document.createElement("div"); placeholder.className = "avatar-card-image"; placeholder.style.width = "60px"; placeholder.style.height = "60px"; placeholder.style.borderRadius = "50%"; placeholder.style.backgroundColor = "#6a6f81"; placeholder.style.display = "flex"; placeholder.style.alignItems = "center"; placeholder.style.justifyContent = "center"; placeholder.style.color = "#fff"; placeholder.style.fontSize = "14px"; placeholder.textContent = `+${hiddenPlayers}`; thumbnailsContainer.appendChild(placeholder); } } /******************************************************* name of function: createThumbnailContainer description: Creates thumbnail container - now works for both real and mock thumbnails *******************************************************/ function createThumbnailContainer(playerThumbnails, maxPlayers, currentPlayers, isMock = false) { const thumbnailsContainer = document.createElement("div"); thumbnailsContainer.className = "player-thumbnails-container"; thumbnailsContainer.style.display = "grid"; thumbnailsContainer.style.gridTemplateColumns = "repeat(3, 60px)"; thumbnailsContainer.style.gridTemplateRows = "repeat(2, 60px)"; thumbnailsContainer.style.gap = "5px"; thumbnailsContainer.style.marginBottom = "10px"; // function for mock thumbnails const randomBase64Image = () => { const placeholders = [ window.Base64Images.roblox_avatar, window.Base64Images.builderman_avatar, ]; return placeholders[Math.floor(Math.random() * placeholders.length)]; }; const maxThumbnails = 5; const displayedCount = Math.min(currentPlayers, maxThumbnails); // Create thumbnails (mock or real) for (let i = 0; i < displayedCount; i++) { const img = document.createElement("img"); if (isMock) { img.src = randomBase64Image(); } else if (playerThumbnails[i] && playerThumbnails[i].imageUrl) { img.src = playerThumbnails[i].imageUrl; } else { continue; // Skip if no thumbnail data } img.className = "avatar-card-image"; img.style.width = "60px"; img.style.height = "60px"; img.style.borderRadius = "50%"; img.style.opacity = "1"; thumbnailsContainer.appendChild(img); } // add +x placeholder for hidden players const hiddenPlayers = currentPlayers - displayedCount; if (hiddenPlayers > 0) { const placeholder = document.createElement("div"); placeholder.className = "avatar-card-image"; placeholder.style.width = "60px"; placeholder.style.height = "60px"; placeholder.style.borderRadius = "50%"; placeholder.style.backgroundColor = "#6a6f81"; placeholder.style.display = "flex"; placeholder.style.alignItems = "center"; placeholder.style.justifyContent = "center"; placeholder.style.color = "#fff"; placeholder.style.fontSize = "14px"; placeholder.textContent = `+${hiddenPlayers}`; thumbnailsContainer.appendChild(placeholder); } return thumbnailsContainer; } /******************************************************* name of function: getCsrfToken description: get crsf token *******************************************************/ async function getCsrfToken() { // look ik this function may get called like a million times. but shouldnt be an issue. :) real men test in production >:) return new Promise((resolve) => { GM_xmlhttpRequest({ // low risk endpoint. hopefully no ratelimit url: "https://catalog.roblox.com/v1/catalog/items/details", method: "POST", withCredentials: true, onload: function(response) { const token = response.responseHeaders .split("\n") .find(h => h.toLowerCase().startsWith("x-csrf-token")); if (!token) { ConsoleLogEnabled("Error: Something went wrong getting csrf token!"); resolve(null); return; } const value = token.split(":")[1].trim(); resolve(value); }, onerror: function() { ConsoleLogEnabled("Error: Request failed while getting csrf token!"); resolve(null); } }); }); } /******************************************************* name of function: getLatestPlaceVersion description: get the latest published version of a place *******************************************************/ async function getLatestPlaceVersion(gameId) { // yea may get called 1 billion times try { const token = await getCsrfToken(); if (!token) { ConsoleLogEnabled("Error: Could not get CSRF token for version check"); return null; } return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: "https://develop.roblox.com/v1/assets/latest-versions", headers: { "Content-Type": "application/json", "X-CSRF-TOKEN": token }, data: JSON.stringify({ assetIds: [parseInt(gameId)], versionStatus: "Published" }), onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.results && data.results.length > 0 && data.results[0].status === "Success") { const versionNumber = data.results[0].versionNumber; ConsoleLogEnabled(`Latest Published Place Version: ${versionNumber}`); resolve(versionNumber); } else { ConsoleLogEnabled("Error: Could not retrieve version number from response"); resolve(null); } } catch (err) { ConsoleLogEnabled("Error parsing latest version response:", err); resolve(null); } }, onerror: function(err) { ConsoleLogEnabled("Error fetching latest place version:", err); resolve(null); }, withCredentials: true }); }); } catch (error) { ConsoleLogEnabled("Error fetching latest place version:", error); return null; } } /******************************************************* name of function: rebuildServerList description: function to create server cards immediately and load thumbnails *******************************************************/ async function rebuildServerList(gameId, totalLimit, best_connection, quick_join = false) { const latestPublishedVersion = await getLatestPlaceVersion(gameId); const serverListContainer = document.getElementById("rbx-public-game-server-item-container"); const isJoinMode = best_connection || quick_join; // If in any join mode (best connection or quick join) if (isJoinMode) { const originalInvert = localStorage.getItem('ROLOCATE_invertplayercount'); let foundServer = false; try { // only disable filter button for best_connection, not for quick_join if (best_connection) { disableFilterButton(true); } notifications("Retrieving Location...", "success", "๐ŸŒŽ", '5000'); const userLocation = await getUserLocation(true); const serverRegionsPrefs = JSON.parse(localStorage.getItem('ROLOCATE_serverRegions') || '{}'); if (!userLocation) { notifications('Error: Unable to fetch your location. Please enable location access or set it to manual in settings.', 'error', 'โš ๏ธ', '5000'); return; } for (let attempt = 0; attempt < 2 && !foundServer; attempt++) { if (attempt === 0) { if (originalInvert === 'true') { localStorage.setItem('ROLOCATE_invertplayercount', 'false'); } // the fix } else { localStorage.setItem('ROLOCATE_invertplayercount', 'true'); notifications('No available servers found. Trying smallest servers...', 'info', '๐Ÿ”„', '3000'); } const servers = await fetchPublicServers(gameId, 50); if (servers.length === 0) { notifications('No servers found for this game.', 'error', 'โš ๏ธ', '3000'); continue; } const isFastServers = localStorage.getItem("ROLOCATE_fastservers") === "true"; let closestServer = null; let minDistance = Infinity; let closestServerLocation = null; // used for best connection (ik it says not used but its used) if (isFastServers) { const results = await Promise.allSettled( servers.map(async server => { const { id: serverId, maxPlayers, playing } = server; if (playing >= maxPlayers) return null; try { const location = await fetchServerDetails(gameId, serverId); // respect banned server regions const regionKey = `${location.city}_${location.country?.code}`; if (serverRegionsPrefs[regionKey] === 'banned') { ConsoleLogEnabled(`Skipping server ${serverId} due to banned region ${regionKey}.`); return null; } const distance = calculateDistance( userLocation.latitude, userLocation.longitude, location.latitude, location.longitude ); return { server, location, distance }; } catch (error) { ConsoleLogEnabled(`Error fetching details for server ${serverId}:`, error); return null; } }) ); for (const result of results) { if (result.status === "fulfilled" && result.value) { const { server, location, distance } = result.value; if (distance < minDistance) { minDistance = distance; closestServer = server; closestServerLocation = location; // used for best connection } } } } else { for (const server of servers) { const { id: serverId, maxPlayers, playing } = server; if (playing >= maxPlayers) continue; try { const location = await fetchServerDetails(gameId, serverId); // respect banned regions const regionKey = `${location.city}_${location.country?.code}`; if (serverRegionsPrefs[regionKey] === 'banned') { ConsoleLogEnabled(`Skipping server ${serverId} due to banned region ${regionKey}.`); continue; } const distance = calculateDistance( userLocation.latitude, userLocation.longitude, location.latitude, location.longitude ); if (distance < minDistance) { minDistance = distance; closestServer = server; closestServerLocation = location; // used for best connection } } catch (error) { ConsoleLogEnabled(`Error fetching details for server ${serverId}:`, error); continue; } } } if (closestServer) { JoinServer(gameId, closestServer.id); notifications(`Joining nearest server! \nDistance: ${Math.round(minDistance / 1.609)} miles | ${Math.round(minDistance)} km`, 'success', '๐Ÿš€', '5000'); foundServer = true; } } if (!foundServer) { notifications('No valid servers found. This game might be popular right now. Try using \'Server Region\' or refresh the page and try again later.', 'error', 'โš ๏ธ', '8000'); notifications('Or no unbanned servers found. Try to enable more regions in settings!', 'error', 'โš ๏ธ', '8000'); } } catch (error) { ConsoleLogEnabled("Error in join mode:", error); notifications('Error during server search: ' + error.message, 'error', 'โš ๏ธ', '5000'); } finally { if (originalInvert !== null) { localStorage.setItem('ROLOCATE_invertplayercount', originalInvert); } else { localStorage.setItem('ROLOCATE_invertplayercount', 'false'); } if (best_connection) { disableFilterButton(false); } Loadingbar(false); } return; } // Rest of the function for normal server list display if (!serverListContainer) { ConsoleLogEnabled("Server list container not found!"); notifications('Error: No Servers found. There is nobody playing this game. Please refresh the page.', 'error', 'โš ๏ธ', '8000'); Loadingbar(false); return; } const messageElement = showMessage("Just a moment โ€” to detect your location accurately, please stay on this page..."); const premium_message = messageElement.querySelector('.premium-message-text'); try { // Retrieve user's location for distance calculations const userLocation = await getUserLocation(); if (!userLocation) { notifications('Error: Unable to fetch your location. Please enable location access.', 'error', 'โš ๏ธ', '5000'); disableFilterButton(false); return; } // update message after location is retrieved if (premium_message) { premium_message.textContent = "Location detected! Discovering servers..."; } // thx Waivy let servers = await fetchPublicServers(gameId, totalLimit); if (servers.length === 0) { ConsoleLogEnabled("No servers returned on first attempt, Retrying after delay"); if (premium_message) { premium_message.textContent = "Waiting for server data to load"; } const retrycap = 3; for (let retry = 1; retry <= retrycap && servers.length === 0; retry++) { await new Promise(resolve => setTimeout(resolve, 1500)); if (premium_message) { premium_message.textContent = `Retrying server fetch (attempt ${retry}/${retrycap})`; } ConsoleLogEnabled(`Retry attempt ${retry}/${retrycap}...`); servers = await fetchPublicServers(gameId, totalLimit); } } const totalServers = servers.length; let skippedServers = 0; if (totalServers === 0) { if (premium_message) { premium_message.textContent = "No servers found. The game may not have active public servers right now or is a solo game. Try refreshing the page."; } notifications('No servers found. The game may not have active public servers right now or is a solo game. Try refreshing the page.', 'error', 'โš ๏ธ', '5000'); Loadingbar(false); disableFilterButton(false); return; } if (premium_message) { premium_message.textContent = `Filtering servers... Please stay on this page to ensure a faster and more accurate search. ${totalServers} servers found, 0 loaded so far.`; } notifications(`Please do not leave this page as it slows down the search. \nFound a total of ${totalServers} servers.`, 'success', '๐Ÿ‘', '3000'); let serverDetails = []; const thumbnailCache = new Map(); // cache for thumbnails const useBatching = localStorage.ROLOCATE_fastservers === "true"; // process servers to get location data (WITHOUT waiting for thumbnails) if (useBatching) { const batchSize = 100; let processedCount = 0; for (let i = 0; i < servers.length; i += batchSize) { const batch = servers.slice(i, i + batchSize); const batchPromises = batch.map(async (server) => { const { id: serverId, maxPlayers, playing } = server; if (playing >= maxPlayers) { skippedServers++; return null; } try { const location = await fetchServerDetails(gameId, serverId); if (location.city === "Unknown") { ConsoleLogEnabled(`Skipping server ${serverId} because location is unknown.`); skippedServers++; return null; } return { server, location }; } catch (error) { if (error === 'purchase_required') { throw error; } else if (error === 'subplace_join_restriction') { throw error; } else if (error === 'banned_by_creator') { throw error; } else { ConsoleLogEnabled(error); skippedServers++; return null; } } }); const batchResults = await Promise.all(batchPromises); const previousProcessedCount = processedCount; const validResults = batchResults.filter(result => result !== null); serverDetails.push(...validResults); processedCount += batch.length; // smoothly update the processed count function updateProcessedCountSmoothly(startCount, targetCount) { const increment = 1; let currentCount = startCount; const interval = setInterval(() => { if (currentCount < targetCount) { currentCount += increment; if (premium_message) { premium_message.textContent = `Filtering servers, please do not leave this page...\n${totalServers} servers found, ${currentCount} server locations found`; } } else { clearInterval(interval); } }, 0.5); } updateProcessedCountSmoothly(previousProcessedCount, processedCount); } } else { // sequential processing for (let i = 0; i < servers.length; i++) { const server = servers[i]; const { id: serverId, maxPlayers, playing } = server; let location; try { location = await fetchServerDetails(gameId, serverId); } catch (error) { if (error === 'purchase_required') { if (premium_message) { premium_message.textContent = "Error: Cannot access server regions because you have not purchased the game."; } notifications('Error: Cannot access server regions because you have not purchased the game.', 'error', 'โš ๏ธ', '15000'); Loadingbar(false); return; } else if (error === 'subplace_join_restriction') { if (premium_message) { premium_message.textContent = "Error: This game requires users to teleport to a subplace. As a result, server regions cannot be retrieved."; } notifications('Error: This game requires users to teleport to a subplace. As a result, server regions cannot be retrieved.', 'error', 'โš ๏ธ', '15000'); Loadingbar(false); return; } else if (error === 'banned_by_creator') { if (premium_message) { premium_message.textContent = "Error: Cannot access server regions because the creator has banned you from the game."; } notifications('Error: Cannot access server regions because the creator has banned you from the game.', 'error', 'โš ๏ธ', '15000'); Loadingbar(false); return; } else { ConsoleLogEnabled(error); location = { city: "Unknown", country: { name: "Unknown", code: "??" } }; } } if (location.city === "Unknown" || playing >= maxPlayers) { ConsoleLogEnabled(`Skipping server ${serverId} because it is full or location is unknown.`); skippedServers++; continue; } serverDetails.push({ server, location }); if (premium_message) { premium_message.textContent = `Filtering servers, please do not leave this page...\n${totalServers} servers found, ${i + 1} server locations found`; } } } // filter out servers from banned regions try { const serverRegionsPrefs = JSON.parse(localStorage.getItem('ROLOCATE_serverRegions') || '{}'); const beforeFilterCount = serverDetails.length; const allowed = serverDetails.filter(d => { const key = `${d.location.city}_${d.location.country?.code}`; return serverRegionsPrefs[key] !== 'banned'; }); const bannedExcluded = beforeFilterCount - allowed.length; if (bannedExcluded > 0) { skippedServers += bannedExcluded; notifications(`${bannedExcluded} servers excluded due to banned regions.`, 'info', '๐ŸŒ', '3000'); } serverDetails = allowed; } catch (e) { ConsoleLogEnabled('Error parsing server region preferences:', e); } if (serverDetails.length === 0) { showMessage("END"); if (servers.every(s => s.maxPlayers === 1)) { notifications('All servers have a max player count of 1. These are likely solo servers and cannot be joined normally.', 'error', 'โš ๏ธ', '8000'); } else { notifications('No servers were found. Possible reasons include: full servers, no servers in your enabled regions, or a temporary glitch. Try adjusting your settings or searching again with lowest player count.', 'error', 'โš ๏ธ', '25000'); } Loadingbar(false); return; } const loadedServers = totalServers - skippedServers; notifications(`Filtering complete!\n${totalServers} servers found, ${loadedServers} servers loaded, ${skippedServers} servers skipped (full) or were banned by user.`, 'success', '๐Ÿ‘', '5000'); // Check script handler notifications if (typeof GM_info !== 'undefined') { const handler = GM_info.scriptHandler?.toLowerCase(); const fastServers = localStorage.getItem('ROLOCATE_fastservers'); if (handler?.includes('violentmonkey') && fastServers === 'false') { notifications(`You're using Violentmonkey, it supports Fast Servers. Turn on "Fast Server Search" in Settings โ†’ General โ†’ Fast Server Search, to search servers up to 100x faster!`, 'info', '๐Ÿš€', '12000'); } if (handler?.includes('scriptcat') && fastServers === 'false') { notifications(`You're using ScriptCat, it supports Fast Servers. Turn on "Fast Server Search" in Settings โ†’ General โ†’ Fast Server Search, to search servers up to 100x faster!`, 'info', '๐Ÿš€', '12000'); } if (handler?.includes('tampermonkey')) { notifications(`Server search is slow because of a bug in Tampermonkey. Use Violentmonkey or Scriptcat to make it 100x faster!`, 'info', '๐Ÿš€', '12000'); } } showMessage("END"); Loadingbar(false); // Add filter dropdowns const filterContainer = createFilterDropdowns(serverDetails); serverListContainer.parentNode.insertBefore(filterContainer, serverListContainer); // Style the server list container serverListContainer.style.display = "grid"; serverListContainer.style.gridTemplateColumns = "repeat(4, 1fr)"; serverListContainer.style.gap = "0px"; // compacted code to save some space. still readable tho. for server version labels (New) and (Old) const serverVersions = serverDetails.map(d => d.location.placeVersion).filter(v => v != null && v !== 'N/A').map(Number); const minVersion = serverVersions.length > 0 ? Math.min(...serverVersions) : null; const maxVersion = serverVersions.length > 0 ? Math.max(...serverVersions) : null; const displayFilteredServers = (country, city, version) => { serverListContainer.innerHTML = ""; const filteredServers = filterServers(serverDetails, country, city, version); const sortedServers = filteredServers.sort((a, b) => { const distanceA = calculateDistance(userLocation.latitude, userLocation.longitude, a.location.latitude, a.location.longitude); const distanceB = calculateDistance(userLocation.latitude, userLocation.longitude, b.location.latitude, b.location.longitude); return distanceA - distanceB; }); //create server cards immediately sortedServers.forEach(({ server, location }) => { const serverCard = document.createElement("li"); serverCard.className = "rbx-game-server-item col-md-3 col-sm-4 col-xs-6"; serverCard.style.width = "100%"; serverCard.style.minHeight = "400px"; serverCard.style.display = "flex"; serverCard.style.flexDirection = "column"; serverCard.style.justifyContent = "space-between"; serverCard.style.boxSizing = "border-box"; serverCard.style.outline = 'none'; serverCard.setAttribute('data-server-id', server.id); // Create ping label const pingLabel = document.createElement("div"); pingLabel.style.marginBottom = "8px"; pingLabel.style.padding = "8px 12px"; pingLabel.style.borderRadius = "8px"; pingLabel.style.fontWeight = "bold"; pingLabel.style.textAlign = "center"; pingLabel.style.fontSize = "13px !important"; pingLabel.style.textTransform = "uppercase !important"; pingLabel.style.letterSpacing = "0.5px !important"; // calculate distance and ping const distance = calculateDistance( userLocation.latitude, userLocation.longitude, location.latitude, location.longitude ); // formula derived from regression of 382 data points. restricted domain of distance >= 0 // distance is km const calculatedPing = 2.05816 * Math.sqrt((1/0.700042) * (Math.max(distance,0) + 2479.47383)) - 72.29266; if (Math.max(distance, 0) < 1250) { pingLabel.textContent = "โšก Fast"; pingLabel.style.backgroundColor = "#1a4a3a"; pingLabel.style.color = "#4ade80"; pingLabel.style.border = "1px solid #22c55e"; } else if (Math.max(distance, 0) < 5000) { pingLabel.textContent = "โณ OK"; pingLabel.style.backgroundColor = "#4a3a1a"; pingLabel.style.color = "#fbbf24"; pingLabel.style.border = "1px solid #f59e0b"; } else { pingLabel.textContent = "๐ŸŒ Slow"; pingLabel.style.backgroundColor = "#4a1a1a"; pingLabel.style.color = "#f87171"; pingLabel.style.border = "1px solid #ef4444"; } // create thumbnails let thumbnailsContainer; const cachedThumbnails = thumbnailCache.get(server.id); if (cachedThumbnails && cachedThumbnails !== 'loading') { thumbnailsContainer = createThumbnailContainer(cachedThumbnails, server.maxPlayers, server.playing, false); } else { thumbnailsContainer = createThumbnailContainer(null, server.maxPlayers, server.playing, true); } // calculate server health const healthPercentage = Math.min(100, Math.round((server.fps / 60) * 100)); let healthBg, healthIcon; if (healthPercentage >= 90) { healthBg = '#1a4a3a'; healthIcon = '๐ŸŸข'; } else if (healthPercentage >= 80) { healthBg = '#4a3a1a'; healthIcon = '๐ŸŸก'; } else if (healthPercentage >= 70) { healthBg = '#4a2a1a'; healthIcon = '๐ŸŸ '; } else { healthBg = '#4a1a1a'; // yea bad server healthIcon = '๐Ÿ”ด'; } const cardItem = document.createElement("div"); cardItem.className = "card-item"; cardItem.style.display = "flex"; cardItem.style.flexDirection = "column"; cardItem.style.justifyContent = "space-between"; cardItem.style.height = "100%"; cardItem.style.color = "#e5e5e5"; const versionDisplay = location.placeVersion && location.placeVersion !== 'N/A' ? (location.placeVersion === latestPublishedVersion ? `${location.placeVersion} NEW` : location.placeVersion === minVersion ? `${location.placeVersion} OLD` : location.placeVersion) : 'N/A'; cardItem.innerHTML = ` ${thumbnailsContainer.outerHTML}
    ${server.playing} of ${server.maxPlayers} people max
    ${pingLabel.outerHTML}
    Estimated Ping: ${calculatedPing.toFixed(1)}ms

    Distance: ${distance.toFixed(1)}km

    Location: ${location.city}, ${location.country.name}

    Server Health:
    ${healthIcon} ${healthPercentage}%

    Version: ${versionDisplay}
    `; const joinButton = cardItem.querySelector(".rbx-game-server-join"); joinButton.addEventListener('mouseenter', () => { joinButton.style.background = '#4a4a4a'; joinButton.style.borderColor = '#666666'; joinButton.style.transform = 'translateY(-1px)'; }); joinButton.addEventListener('mouseleave', () => { joinButton.style.background = '#404040'; joinButton.style.borderColor = '#555555'; joinButton.style.transform = 'translateY(0)'; }); joinButton.addEventListener("click", () => { ConsoleLogEnabled(`Roblox.GameLauncher.joinGameInstance(${gameId}, "${server.id}")`); // not anymore JoinServer(gameId, server.id); }); const container = adjustJoinButtonContainer(joinButton); const inviteButton = createInviteButton(gameId, server.id); inviteButton.style.background = '#404040'; inviteButton.style.border = '1px solid #555555'; inviteButton.style.color = '#e5e5e5'; inviteButton.style.borderRadius = '8px'; inviteButton.style.fontWeight = '600'; inviteButton.style.transition = 'all 0.2s ease'; inviteButton.style.textTransform = 'uppercase'; inviteButton.style.letterSpacing = '0.5px'; inviteButton.style.fontSize = '12px'; inviteButton.addEventListener('mouseenter', () => { inviteButton.style.background = '#4a4a4a'; inviteButton.style.borderColor = '#666666'; inviteButton.style.transform = 'translateY(-1px)'; }); inviteButton.addEventListener('mouseleave', () => { inviteButton.style.background = '#404040'; inviteButton.style.borderColor = '#555555'; inviteButton.style.transform = 'translateY(0)'; }); container.appendChild(inviteButton); serverCard.appendChild(cardItem); serverListContainer.appendChild(serverCard); // load real thumbnails in background if not cached and not already being fetched // added another condition so that finding thumbnails only occurs if mobilemode = false. This is a performance upgrade for mobile users. if (server.playerTokens && server.playerTokens.length > 0 && !thumbnailCache.has(server.id) && localStorage.ROLOCATE_mobilemode === "false") { // mark as being fetched to prevent duplicate requests thumbnailCache.set(server.id, 'loading'); fetchPlayerThumbnails_servers(server.playerTokens) .then(thumbnails => { thumbnailCache.set(server.id, thumbnails); updateServerCardThumbnails(server.id, thumbnails, server.maxPlayers, server.playing); }) .catch(error => { ConsoleLogEnabled(`Failed to load thumbnails for server ${server.id}:`, error); // remove the 'loading' marker on error so it can be retried thumbnailCache.delete(server.id); }); } }); }; // Add event listeners to dropdowns const countryFilter = document.getElementById('countryFilter'); const cityFilter = document.getElementById('cityFilter'); const versionFilter = document.getElementById('versionFilter'); countryFilter.addEventListener('change', () => { displayFilteredServers(countryFilter.value, cityFilter.value, versionFilter.value); }); cityFilter.addEventListener('change', () => { displayFilteredServers(countryFilter.value, cityFilter.value, versionFilter.value); }); versionFilter.addEventListener('change', () => { displayFilteredServers(countryFilter.value, cityFilter.value, versionFilter.value); }); // countries cities and versions displayFilteredServers("", "", ""); } catch (error) { if (error === 'purchase_required') { if (premium_message) { premium_message.textContent = "Error: Cannot access server regions because you have not purchased the game."; } notifications('Error: Cannot access server regions because you have not purchased the game.', 'error', 'โš ๏ธ', '15000'); Loadingbar(false); return; } else if (error === 'subplace_join_restriction') { if (premium_message) { premium_message.textContent = "Error: This game requires users to teleport to a subplace. As a result, server regions cannot be retrieved."; } notifications('Error: This game requires users to teleport to a subplace. As a result, server regions cannot be retrieved.', 'error', 'โš ๏ธ', '15000'); Loadingbar(false); return; } else if (error === 'banned_by_creator') { if (premium_message) { premium_message.textContent = "Error: Cannot access server regions because the creator has banned you from the game."; } notifications('Error: Cannot access server regions because the creator has banned you from the game.', 'error', 'โš ๏ธ', '15000'); Loadingbar(false); return; } else { ConsoleLogEnabled("Error rebuilding server list:", error); notifications('Filtering Error: Failed to obtain permission to send API requests to the Roblox API. Please allow the script to enable request sending.', 'error', 'โš ๏ธ ', '8000'); if (premium_message) { premium_message.textContent = "Filtering Error: Failed to obtain permission to send API requests to the Roblox API. Please allow the script to enable request sending."; } Loadingbar(false); } } finally { Loadingbar(false); disableFilterButton(false); } } // script breaks if i remove this so its staying const gameId = getCurrentGameId(); /******************************************************* name of function: createInviteButton description: Creates the invite button (server region) *******************************************************/ function createInviteButton(placeId, serverId) { const inviteButton = document.createElement('button'); inviteButton.textContent = 'Invite'; inviteButton.className = 'btn-control-xs btn-primary-md btn-min-width btn-full-width'; inviteButton.style.width = '25%'; inviteButton.style.marginLeft = '5px'; inviteButton.style.padding = '4px 8px'; inviteButton.style.fontSize = '12px'; inviteButton.style.borderRadius = '8px'; inviteButton.style.backgroundColor = '#3b3e49'; inviteButton.style.borderColor = '#3b3e49'; inviteButton.style.color = '#ffffff'; inviteButton.style.cursor = 'pointer'; inviteButton.style.fontWeight = '500'; inviteButton.style.textAlign = 'center'; inviteButton.style.whiteSpace = 'nowrap'; inviteButton.style.verticalAlign = 'middle'; inviteButton.style.lineHeight = '100%'; inviteButton.style.fontFamily = 'Builder Sans, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif'; inviteButton.style.textRendering = 'auto'; inviteButton.style.webkitFontSmoothing = 'antialiased'; inviteButton.style.mozOsxFontSmoothing = 'grayscale'; let resetTextTimeout = null; inviteButton.addEventListener('click', () => { const inviteLink = `https://oqarshi.github.io/Invite/?placeid=${placeId}&serverid=${serverId}`; navigator.clipboard.writeText(inviteLink).then(() => { ConsoleLogEnabled(`Invite link copied to clipboard: ${inviteLink}`); notifications('Success! Invite link copied to clipboard!', 'success', '๐ŸŽ‰', '2000'); // no spam clicks inviteButton.disabled = true; inviteButton.style.opacity = '0.6'; inviteButton.style.cursor = 'not-allowed'; // reset the timeout if (resetTextTimeout !== null) { clearTimeout(resetTextTimeout); } inviteButton.textContent = 'Copied!'; resetTextTimeout = setTimeout(() => { inviteButton.textContent = 'Invite'; inviteButton.disabled = false; inviteButton.style.opacity = '1'; inviteButton.style.cursor = 'pointer'; resetTextTimeout = null; }, 1000); }).catch(() => { ConsoleLogEnabled('Failed to copy invite link.'); notifications('Error: Failed to copy invite link', 'error', '๐Ÿ˜”', '2000'); }); }); return inviteButton; } /******************************************************* name of function: adjustJoinButtonContainer description: Function to adjust the Join button and its container but it fails lmao and does 50/50 instead of 75/25 *******************************************************/ function adjustJoinButtonContainer(joinButton) { const container = document.createElement('div'); container.style.display = 'flex'; container.style.width = '100%'; joinButton.style.width = '75%'; joinButton.parentNode.insertBefore(container, joinButton); container.appendChild(joinButton); return container; } /********************************************************************************************************************************************************************************************************************************************* Functions for the 6th button. *********************************************************************************************************************************************************************************************************************************************/ /******************************************************* name of function: calculateDistance description: finds the distance between two points on a sphere (Earth) *******************************************************/ function calculateDistance(lat1, lon1, lat2, lon2) { const R = 6371; // radius of the Earth in kilometers as a perfect sphere but obv its not a perfect sphere but close enough const dLat = (lat2 - lat1) * (Math.PI / 180); const dLon = (lon2 - lon1) * (Math.PI / 180); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; // distance in kilometers } /******************************************************* name of function: resolveOfflineFallbackLocation description: estimate user location if user declines *******************************************************/ // fallback location resolver with timezone-based estimation function resolveOfflineFallbackLocation(resolve) { ConsoleLogEnabled("Attempting offline location estimation..."); let guessedLocation = null; let closestLocation = null; let closestDistance = Infinity; const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || ""; const timezoneMap = { "America/Los_Angeles": { lat: 34.0522, lon: -118.2437 }, "America/Denver": { lat: 39.7392, lon: -104.9903 }, "America/Chicago": { lat: 41.8781, lon: -87.6298 }, "America/New_York": { lat: 40.7128, lon: -74.006 }, "Europe/London": { lat: 51.5074, lon: -0.1278 }, "Europe/Berlin": { lat: 52.52, lon: 13.405 }, "Europe/Paris": { lat: 48.8566, lon: 2.3522 }, "Asia/Tokyo": { lat: 35.6895, lon: 139.6917 }, "Asia/Kolkata": { lat: 28.6139, lon: 77.209 }, "Australia/Sydney": { lat: -33.8688, lon: 151.2093 }, "America/Argentina/Buenos_Aires": { lat: -34.6037, lon: -58.3816 }, "Africa/Nairobi": { lat: -1.286389, lon: 36.817223 }, "Asia/Singapore": { lat: 1.3521, lon: 103.8198 }, "America/Toronto": { lat: 43.65107, lon: -79.347015 }, "Europe/Moscow": { lat: 55.7558, lon: 37.6173 }, "Europe/Madrid": { lat: 40.4168, lon: -3.7038 }, "Asia/Shanghai": { lat: 31.2304, lon: 121.4737 }, "Africa/Cairo": { lat: 30.0444, lon: 31.2357 }, "Africa/Johannesburg": { lat: -26.2041, lon: 28.0473 }, "Europe/Amsterdam": { lat: 52.3676, lon: 4.9041 }, "Asia/Manila": { lat: 14.5995, lon: 120.9842 }, "Asia/Seoul": { lat: 37.5665, lon: 126.978 } }; // check if user's timezone in map if (timezoneMap[timezone]) { guessedLocation = timezoneMap[timezone]; ConsoleLogEnabled("User's timezone found:", timezone); } // if no timezone find closest one if (!guessedLocation) { ConsoleLogEnabled("User's timezone not found. Finding closest match..."); Object.keys(timezoneMap).forEach((tz) => { const location = timezoneMap[tz]; const distance = calculateDistance(location.lat, location.lon, 0, 0); // distance from equator if (distance < closestDistance) { closestDistance = distance; closestLocation = location; } }); guessedLocation = closestLocation; } //if location then good, if not then newyork if (guessedLocation) { notifications("Estimated location based on timezone. Please allow location access to see what servers are closest to you or change to manual in settings.", "info", "๐Ÿ•’", "6000"); resolve({ latitude: guessedLocation.lat, longitude: guessedLocation.lon }); } else { notifications("Error: Could not estimate location. Fatal error, please report on Greasyfork. Using default (New York).", "error", "โš ๏ธ", "6000"); resolve({ latitude: 40.7128, longitude: -74.0060 }); // nyc } } /******************************************************* name of function: getUserLocation description: gets the user's location @param {boolean} [quickJoin=false] โ€“ when true, operates in lightweight "quick join" mode *******************************************************/ function getUserLocation(quickJoin = false) { return new Promise((resolve, reject) => { // check priority location setting const priorityLocation = localStorage.getItem("ROLOCATE_prioritylocation") || "automatic"; // if in manual mode, use stored coordinates if (priorityLocation === "manual") { try { const coords = JSON.parse(GM_getValue("ROLOCATE_coordinates", '{"lat":"","lng":""}')); if (coords.lat && coords.lng) { ConsoleLogEnabled("Using manual location from storage"); notifications("We successfully detected your location.", "success", "๐ŸŒŽ", "2000"); return resolve({ latitude: parseFloat(coords.lat), // changed to match automatic mode longitude: parseFloat(coords.lng), // changed to match automatic mode source: "manual", accuracy: 0 // manual coordinates have no accuracy metric }); } else { ConsoleLogEnabled("Manual mode selected but no coordinates set - falling back to automatic behavior"); notifications("Manual mode selected but no coordinates set. Fatal error: Report on greasyfork. Using Automatic Mode.", "error", "", "2000"); // fall through to automatic behavior } } catch (e) { ConsoleLogEnabled("Error reading manual coordinates:", e); notifications("Error reading manual coordinates. Fatal error: Report on greasyfork. Using Automatic Mode.", "error", "", "2000"); // fall through to automatic behavior } } // automatic mode behavior if (!navigator.geolocation) { ConsoleLogEnabled("Geolocation not supported."); notifications("Geolocation is not supported by your browser.", "error", "โš ๏ธ", "15000"); return resolveOfflineFallbackLocation(resolve); } navigator.geolocation.getCurrentPosition( (position) => resolveSuccess(position, resolve, quickJoin), async (error) => { ConsoleLogEnabled("Geolocation error:", error); // attempt to inspect geolocation permission state try { if (navigator.permissions && navigator.permissions.query) { const permissionStatus = await navigator.permissions.query({ name: "geolocation" }); ConsoleLogEnabled("Geolocation permission status:", permissionStatus.state); if (permissionStatus.state === "denied") { return resolveOfflineFallbackLocation(resolve); } } } catch (permError) { ConsoleLogEnabled("Permission check failed:", permError); } // retry geolocation once navigator.geolocation.getCurrentPosition( (position) => resolveSuccess(position, resolve, quickJoin), (retryError) => { ConsoleLogEnabled("Second geolocation attempt failed:", retryError); notifications("Could not get your location. Using fallback.", "error", "โš ๏ธ", "15000"); resolveOfflineFallbackLocation(resolve); }, { maximumAge: 5000, timeout: 10000, } ); }, { timeout: 10000, maximumAge: 0, } ); }); } /******************************************************* name of function: resolveSuccess description: tells the user that location was detected @param {GeolocationPosition} position โ€“ browser geolocation position @param {Function} resolve โ€“ promise resolver @param {boolean} [quickJoin=false] โ€“ when true, skips UI-disabling sideโ€‘effects *******************************************************/ function resolveSuccess(position, resolve, quickJoin = false) { notifications("We successfully detected your location.", "success", "๐ŸŒŽ", "2000"); if (!quickJoin) { disableLoadMoreButton(true); disableFilterButton(true); Loadingbar(true); } resolve({ latitude: position.coords.latitude, longitude: position.coords.longitude, source: "geolocation", accuracy: position.coords.accuracy }); } /********************************************************************************************************************************************************************************************************************************************* Functions for the 7th button. *********************************************************************************************************************************************************************************************************************************************/ /******************************************************* name of function: auto_join_small_server description: Automatically joins the smallest server *******************************************************/ async function auto_join_small_server() { // disable the "Load More" button and show the loading bar Loadingbar(true); disableFilterButton(true); disableLoadMoreButton(); // get the game ID from the URL const gameId = getCurrentGameId(); // retry mechanism for 429 errors let retries = 3; // number of retries let success = false; while (retries > 0 && !success) { try { // fetch server data const data = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `https://games.roblox.com/v1/games/${gameId}/servers/public?sortOrder=1&excludeFullGames=true&limit=25`, onload: function(response) { if (response.status === 429) { reject('429: Too Many Requests'); } else if (response.status >= 200 && response.status < 300) { resolve(JSON.parse(response.responseText)); } else { reject(`HTTP error: ${response.status}`); } }, onerror: function(error) { reject(error); }, }); }); // find servers with low player count, prob doesnet work with bloxfruits cause bots let minPlayers = Infinity; let targetServer = null; for (const server of data.data) { if (server.playing < minPlayers) { minPlayers = server.playing; targetServer = server; } } if (targetServer) { // join the server with the lowest player count JoinServer(gameId, targetServer.id); notifications(`Joining a server with ${targetServer.playing} player(s).`, 'success', '๐Ÿš€'); success = true; } else { notifications('No available servers found.', 'error', 'โš ๏ธ'); break; } } catch (error) { if (error === '429: Too Many Requests' && retries > 0) { ConsoleLogEnabled('Rate limited. Retrying in 10 seconds...'); notifications('Rate limited. Retrying in 10 seconds...', 'warning', 'โณ', '10000'); await delay(10000); retries--; } else { ConsoleLogEnabled('Error fetching server data:', error); notifications('Error: Failed to fetch server data. Please try again later.', 'error', 'โš ๏ธ', '5000'); Loadingbar(false); break; } } } Loadingbar(false); disableFilterButton(false); } /********************************************************************************************************************************************************************************************************************************************* Functions for the 8th button. roblox borke it lmao. basically fillter code, might remove it one day *********************************************************************************************************************************************************************************************************************************************/ /********************************************************************************************************************************************************************************************************************************************* End of: This is all the functions for the 8 buttons *********************************************************************************************************************************************************************************************************************************************/ /******************************************************* name of function: disableLoadMoreButton description: Disables the "Load More" button *******************************************************/ function disableLoadMoreButton() { const loadMoreButton = document.querySelector('.rbx-running-games-load-more'); if (loadMoreButton) { loadMoreButton.disabled = true; loadMoreButton.style.opacity = '0.5'; loadMoreButton.style.cursor = 'not-allowed'; // only add the label if it doesnt already exist if (!loadMoreButton.textContent.includes('(Disabled by RoLocate)')) { loadMoreButton.textContent += ' (Disabled by RoLocate)'; } ConsoleLogEnabled('Load More button disabled with text change'); } else { ConsoleLogEnabled('Load More button not found!'); } } /******************************************************* name of function: Loadingbar description: Shows or hides a loading bar (now using pulsing boxes) *******************************************************/ function Loadingbar(disable) { const serverListSection = document.querySelector('#rbx-public-running-games'); const serverCardsContainer = document.querySelector('#rbx-public-game-server-item-container'); const emptyGameInstancesContainer = document.querySelector('.section-content-off.empty-game-instances-container'); const noServersMessage = emptyGameInstancesContainer?.querySelector('.no-servers-message'); // check if the "Unable to load servers." message is visible if (!serverCardsContainer && noServersMessage?.textContent.includes('Unable to load servers.')) { notifications('Unable to load servers. Please refresh the page.', 'error', 'โš ๏ธ', '8000'); return; } // reset if (disable) { if (serverCardsContainer) { serverCardsContainer.innerHTML = ''; serverCardsContainer.removeAttribute('style'); } // no duplicate ones const existingLoadingBar = document.querySelector('#loading-bar'); if (existingLoadingBar) { existingLoadingBar.remove(); } // create the laoding boxes const loadingContainer = document.createElement('div'); loadingContainer.id = 'loading-bar'; loadingContainer.style.cssText = ` display: flex; justify-content: center; align-items: center; gap: 5px; margin-top: 10px; `; const fragment = document.createDocumentFragment(); for (let i = 0; i < 3; i++) { const box = document.createElement('div'); box.style.cssText = ` width: 10px; height: 10px; background-color: white; margin: 0 5px; border-radius: 2px; animation: pulse 1.2s ${i * 0.2}s infinite; `; fragment.appendChild(box); } loadingContainer.appendChild(fragment); if (serverListSection) { serverListSection.appendChild(loadingContainer); } // make thing look good const existingStyle = document.querySelector('#loading-style'); if (!existingStyle) { const styleSheet = document.createElement('style'); styleSheet.id = 'loading-style'; styleSheet.textContent = ` @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.5); } } `; document.head.appendChild(styleSheet); } // target by the unique select IDs that are created in the component const countryFilter_remove = document.getElementById('countryFilter'); const cityFilter_remove = document.getElementById('cityFilter'); const versionFilter_remove = document.getElementById('versionFilter'); // find the dumb container let outerDiv = null; if (countryFilter_remove) { outerDiv = countryFilter_remove.closest('div[style*="display: flex"][style*="gap: 16px"]'); } else if (cityFilter_remove) { outerDiv = cityFilter_remove.closest('div[style*="display: flex"][style*="gap: 16px"]'); } else if (versionFilter_remove) { outerDiv = versionFilter_remove.closest('div[style*="display: flex"][style*="gap: 16px"]'); } // remove it if (outerDiv) { outerDiv.remove(); } // ik this approach sucks but its the best i can do. it remove ths premium messages with this specific // text so it doesnet remove the other stuff, you prob cant even understand what im sayin right now const premiumMessageDiv = document.querySelector('.premium-message-text'); if (premiumMessageDiv) { const messageText = premiumMessageDiv.textContent.trim(); const errorMessages = [ "Error: Cannot access server regions because you have not purchased the game.", "Error: This game requires users to teleport to a subplace. As a result, server regions cannot be retrieved.", "Error: Cannot access server regions because the creator has banned you from the game.", "No servers found. The game may not have active public servers right now or is a solo game. Try refreshing the page." ]; if (errorMessages.includes(messageText)) { showMessage("END"); } } } else { // if disable is false, remove the loading bar const loadingBar = document.querySelector('#loading-bar'); if (loadingBar) { loadingBar.remove(); } // reset any applied styles const styleSheet = document.querySelector('#loading-style'); if (styleSheet) { styleSheet.remove(); } } } /******************************************************* name of function: fetchPlayerThumbnails description: Fetches player thumbnails for up to 5 players. Skips the batch if an error occurs. *******************************************************/ async function fetchPlayerThumbnails(playerTokens) { const limitedTokens = playerTokens.slice(0, 5); const body = limitedTokens.map(token => ({ requestId: `0:${token}:AvatarHeadshot:150x150:png:regular`, type: "AvatarHeadShot", targetId: 0, token, format: "png", size: "150x150", })); return new Promise((resolve) => { GM_xmlhttpRequest({ method: "POST", url: "https://thumbnails.roblox.com/v1/batch", headers: { "Content-Type": "application/json", "Accept": "application/json" }, data: JSON.stringify(body), onload: function(response) { try { if (response.status >= 200 && response.status < 300) { const data = JSON.parse(response.responseText); resolve(data.data || []); } else { ConsoleLogEnabled(`HTTP error! Status: ${response.status}`); resolve([]); } } catch (error) { ConsoleLogEnabled('Error parsing batch thumbnail response:', error); resolve([]); } }, onerror: function(err) { ConsoleLogEnabled('Request error fetching batch thumbnails:', err); resolve([]); } }); }); } /******************************************************* name of function: disableFilterButton description: Disables or enables the filter button based on the input. *******************************************************/ function disableFilterButton(disable) { const filterButton = document.querySelector('.RL-filter-button'); const refreshButtons = document.querySelectorAll('.btn-more.rbx-refresh.refresh-link-icon.btn-control-xs.btn-min-width'); const filterOverlayId = 'filter-button-overlay'; const refreshOverlayClass = 'refresh-button-overlay'; if (filterButton) { const parent = filterButton.parentElement; if (disable) { // kill the filter button so it cant be clicked filterButton.disabled = true; filterButton.style.opacity = '0.5'; filterButton.style.cursor = 'not-allowed'; // an invisible overlay on it so no sneaky clicks let overlay = document.getElementById(filterOverlayId); if (!overlay) { overlay = document.createElement('div'); overlay.id = filterOverlayId; overlay.style.position = 'absolute'; overlay.style.top = '-10px'; overlay.style.left = '-10px'; overlay.style.width = 'calc(100% + 20px)'; overlay.style.height = 'calc(100% + 20px)'; overlay.style.backgroundColor = 'transparent'; overlay.style.zIndex = '9999'; overlay.style.pointerEvents = 'all'; // block clicks like a boss parent.style.position = 'relative'; parent.appendChild(overlay); } } else { // bring the filter button back to life filterButton.disabled = false; filterButton.style.opacity = '1'; filterButton.style.cursor = 'pointer'; // remove that annoying overlay const overlay = document.getElementById(filterOverlayId); if (overlay) { overlay.remove(); } } } else { ConsoleLogEnabled('Filter button not found! Something is wrong!'); notifications("Something's wrong. Please report an issue on Greasyfork.", "error", "โš ๏ธ", "15000"); } if (refreshButtons.length > 0) { refreshButtons.forEach((refreshButton) => { const refreshParent = refreshButton.parentElement; if (disable) { // same overlay trick but for refresh buttons let refreshOverlay = refreshParent.querySelector(`.${refreshOverlayClass}`); if (!refreshOverlay) { refreshOverlay = document.createElement('div'); refreshOverlay.className = refreshOverlayClass; refreshOverlay.style.position = 'absolute'; refreshOverlay.style.top = '-10px'; refreshOverlay.style.left = '-10px'; refreshOverlay.style.width = 'calc(100% + 20px)'; refreshOverlay.style.height = 'calc(100% + 20px)'; refreshOverlay.style.backgroundColor = 'transparent'; refreshOverlay.style.zIndex = '9999'; refreshOverlay.style.pointerEvents = 'all'; // no clicks allowed here either refreshParent.style.position = 'relative'; refreshParent.appendChild(refreshOverlay); } } else { // remove overlays and let buttons live again const refreshOverlay = refreshParent.querySelector(`.${refreshOverlayClass}`); if (refreshOverlay) { refreshOverlay.remove(); } } }); } else { ConsoleLogEnabled('Refresh button not found!'); notifications("Something's wrong. Please report an issue on Greasyfork.", "error", "โš ๏ธ", "15000"); } } /******************************************************* name of function: rbx_card description: Creates the roblox cards that are not from server regions *******************************************************/ async function rbx_card(serverId, playerTokens, maxPlayers, playing, gameId) { const thumbnails = await fetchPlayerThumbnails(playerTokens); // helper function to create elements with properties const createElement = (tag, props = {}, styles = {}) => { const el = document.createElement(tag); Object.assign(el, props); Object.assign(el.style, styles); return el; }; const cardItem = createElement('li', { className: 'rbx-game-server-item col-md-3 col-sm-4 col-xs-6' }); const playerThumbnailsContainer = createElement('div', { className: 'player-thumbnails-container' }); // add player thumbnails thumbnails.forEach(thumbnail => { const playerAvatar = createElement('span', { className: 'avatar avatar-headshot-md player-avatar' }); const thumbnailImage = createElement('span', { className: 'thumbnail-2d-container avatar-card-image' }); const img = createElement('img', { src: thumbnail.imageUrl, alt: '', title: '' }); thumbnailImage.appendChild(img); playerAvatar.appendChild(thumbnailImage); playerThumbnailsContainer.appendChild(playerAvatar); }); // add placeholder for remaining players if (playing > 5) { const placeholder = createElement('span', { className: 'avatar avatar-headshot-md player-avatar hidden-players-placeholder', textContent: `+${playing - 5}` }, { backgroundColor: '#6a6f81', color: 'white', display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: '50%', fontSize: '16px', width: '60px', height: '60px' }); playerThumbnailsContainer.appendChild(placeholder); } // server details const serverDetails = createElement('div', { className: 'rbx-game-server-details game-server-details' }); const serverStatus = createElement('div', { className: 'text-info rbx-game-status rbx-game-server-status text-overflow', textContent: `${playing} of ${maxPlayers} people max` }); // player count gauge const gaugeContainer = createElement('div', { className: 'server-player-count-gauge border' }); const gaugeInner = createElement('div', { className: 'gauge-inner-bar border' }, { width: `${(playing / maxPlayers) * 100}%` }); gaugeContainer.appendChild(gaugeInner); // button container with buttons const buttonContainer = createElement('div', { className: 'button-container' }, { display: 'flex', gap: '8px' }); // join button const joinButton = createElement('button', { type: 'button', className: 'btn-full-width btn-control-xs rbx-game-server-join game-server-join-btn btn-primary-md btn-min-width', textContent: 'Join', onclick: () => JoinServer(gameId, serverId) }); // invite button const inviteButton = createElement('button', { type: 'button', className: 'btn-full-width btn-control-xs rbx-game-server-invite game-server-invite-btn btn-secondary-md btn-min-width', textContent: 'Invite' }); inviteButton.onclick = async () => { const inviteLink = `https://oqarshi.github.io/Invite/?placeid=${gameId}&serverid=${serverId}`; ConsoleLogEnabled('Copied invite link:', inviteLink); try { await navigator.clipboard.writeText(inviteLink); notifications('Success! Invite link copied to clipboard!', 'success', '๐ŸŽ‰', '2000'); ConsoleLogEnabled('Invite link copied to clipboard'); const originalText = inviteButton.textContent; inviteButton.textContent = 'Copied!'; inviteButton.disabled = true; setTimeout(() => { inviteButton.textContent = originalText; inviteButton.disabled = false; }, 1000); } catch (error) { ConsoleLogEnabled('Failed to copy invite link:', error); notifications('Failed! Invite link copied to clipboard!', 'error', 'โš ๏ธ', '2000'); } }; // uh create the stuff buttonContainer.append(joinButton, inviteButton); serverDetails.append(serverStatus, gaugeContainer, buttonContainer); const cardContainer = createElement('div', { className: 'card-item' }); cardContainer.append(playerThumbnailsContainer, serverDetails); cardItem.appendChild(cardContainer); document.querySelector('#rbx-public-game-server-item-container').appendChild(cardItem); } } })();