// ==UserScript== // @name RoLocate // @namespace https://oqarshi.github.io/ // @version 40.0 // @description Adds filter options to roblox server page. Alternative to paid extensions like RoPro, RoGold (Ultimate), RoQol, and RoKit. // @author Oqarshi // @match https://www.roblox.com/* // @license Custom - Personal Use Only // @icon https://oqarshi.github.io/Invite/rolocate/assets/logo.svg // @homepageURL https://oqarshi.github.io/Invite/rolocate/ // @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/539427/1607754/Rolocate%20Server%20Region%20Data.js // @require https://update.greasyfork.icu/scripts/540553/1612919/Rolocate%20Flag%20Base64%20Data.js // @connect thumbnails.roblox.com // @connect games.roblox.com // @connect gamejoin.roblox.com // @connect presence.roblox.com // @connect www.roblox.com // @connect friends.roblox.com // @connect apis.roblox.com // @connect groups.roblox.com // @downloadURL none // ==/UserScript== /* * RoLocate userscript by Oqarshi * License: Custom - Personal Use Only * * Copyright (c) 2025 Oqarshi * * This license grants limited rights to end users and does not imply any transfer of copyright ownership. * By using this script, you agree to these license terms. * * You are permitted to use and modify this script **for personal, non-commercial use only**. * * You are **NOT permitted** to: * - Redistribute or reupload this script, in original or modified form * - Publish it on any website (e.g., GreasyFork, GitHub, UserScripts.org) * - Include it in any commercial, monetized, or donation-based tools * - Remove or alter this license or attribution * * Attribution to the original author (Oqarshi) must always be preserved. * * Violations may result in takedown notices under the DMCA or applicable copyright law. */ /*jshint esversion: 6 */ /*jshint esversion: 11 */ (function() { 'use strict'; // =============================== // TODO LIST // =============================== /* * NEXT UP: * - Fix Localstorage bugs not saving * - ui change stuff idk * - preferred region * - make smartsearch find items and other stuff */ /* * NICE TO HAVE / IDEAS / NOT IMPORTANT: * - Improve Server Amount pick UI * - Have a global function for GameID * - Move functions out of blocks * - Custom theme builder */ /******************************************************* name of function: ConsoleLogEnabled description: console.logs eveyrthing if settings is turned on *******************************************************/ function ConsoleLogEnabled(...args) { if (localStorage.getItem("ROLOCATE_enableLogs") === "true") { console.log("[ROLOCATE]", ...args); } } /******************************************************* name of function: notifications description: the notifications function *******************************************************/ function notifications(message, type = 'info', emoji = '', duration = 3000) { if (localStorage.getItem('ROLOCATE_enablenotifications') !== 'true') { return; } // Inject minimal CSS once if (!document.getElementById('sleek-toast-styles')) { const style = document.createElement('style'); style.id = 'sleek-toast-styles'; style.innerHTML = ` @keyframes slideIn { from { opacity: 0; transform: translateX(100%); } to { opacity: 1; transform: translateX(0); } } @keyframes slideOut { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(100%); } } @keyframes shrink { from { width: 100%; } to { width: 0%; } } #toast-container { position: fixed; top: 20px; right: 20px; z-index: 999999; display: flex; flex-direction: column; gap: 8px; pointer-events: none; } .toast { background: rgba(45, 45, 45, 0.95); color: #e8e8e8; padding: 12px 16px; border-radius: 8px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; font-weight: 500; min-width: 280px; max-width: 400px; backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.15); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); animation: slideIn 0.3s ease-out; pointer-events: auto; position: relative; overflow: hidden; } .toast.removing { animation: slideOut 0.3s ease-in forwards; } .toast:hover { background: rgba(55, 55, 55, 0.98); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3); } .toast-content { display: flex; align-items: center; gap: 10px; } .toast-icon { width: 16px; height: 16px; flex-shrink: 0; } .toast-emoji { font-size: 16px; flex-shrink: 0; } .toast-message { flex: 1; line-height: 1.4; white-space: normal; } .toast-close { position: absolute; top: 4px; right: 6px; width: 20px; height: 20px; cursor: pointer; opacity: 0.6; display: flex; align-items: center; justify-content: center; border-radius: 4px; transition: all 0.2s; } .toast-close:hover { opacity: 1; background: rgba(255, 255, 255, 0.1); } .toast-close::before, .toast-close::after { content: ''; position: absolute; width: 10px; height: 1px; background: #ccc; } .toast-close::before { transform: rotate(45deg); } .toast-close::after { transform: rotate(-45deg); } .progress-bar { position: absolute; bottom: 0; left: 0; height: 2px; background: rgba(255, 255, 255, 0.25); animation: shrink linear forwards; } .toast.success { border-left: 3px solid #4CAF50; } .toast.error { border-left: 3px solid #F44336; } .toast.warning { border-left: 3px solid #FF9800; } .toast.info { border-left: 3px solid #2196F3; } `; document.head.appendChild(style); } // Get or create container let container = document.getElementById('toast-container'); if (!container) { container = document.createElement('div'); container.id = 'toast-container'; document.body.appendChild(container); } // Create toast const toast = document.createElement('div'); toast.className = `toast ${type}`; // Create content const content = document.createElement('div'); content.className = 'toast-content'; // Add icon const iconMap = { success: '', error: '', warning: '', info: '' }; const icon = document.createElement('div'); icon.className = 'toast-icon'; icon.innerHTML = iconMap[type] || iconMap.info; content.appendChild(icon); // Add emoji if provided if (emoji) { const emojiSpan = document.createElement('span'); emojiSpan.className = 'toast-emoji'; emojiSpan.textContent = emoji; content.appendChild(emojiSpan); } // Add message - interpret \n as
const messageSpan = document.createElement('span'); messageSpan.className = 'toast-message'; messageSpan.innerHTML = message.replace(/\n/g, '
'); content.appendChild(messageSpan); toast.appendChild(content); // Add close button const closeBtn = document.createElement('div'); closeBtn.className = 'toast-close'; closeBtn.addEventListener('click', () => removeToast()); toast.appendChild(closeBtn); // Add progress bar const progressBar = document.createElement('div'); progressBar.className = 'progress-bar'; progressBar.style.animationDuration = `${duration}ms`; toast.appendChild(progressBar); container.appendChild(toast); // Auto remove let timeout = setTimeout(removeToast, duration); // Pause on hover toast.addEventListener('mouseenter', () => { progressBar.style.animationPlayState = 'paused'; clearTimeout(timeout); }); toast.addEventListener('mouseleave', () => { progressBar.style.animationPlayState = 'running'; const remaining = (progressBar.offsetWidth / toast.offsetWidth) * duration; timeout = setTimeout(removeToast, remaining); }); function removeToast() { clearTimeout(timeout); toast.classList.add('removing'); setTimeout(() => toast.remove(), 300); } // Return control object return { remove: removeToast, update: (newMessage) => { messageSpan.innerHTML = newMessage.replace(/\n/g, '
'); }, setType: (newType) => { toast.className = `toast ${newType}`; icon.innerHTML = iconMap[newType] || iconMap.info; }, setDuration: (newDuration) => { clearTimeout(timeout); progressBar.style.animation = `shrink ${newDuration}ms linear forwards`; timeout = setTimeout(removeToast, newDuration); }, updateEmoji: (newEmoji) => { const emojiEl = toast.querySelector('.toast-emoji'); if (emojiEl) emojiEl.textContent = newEmoji; } }; } /******************************************************* name of function: Update_Popup description: the update popup if an update is released *******************************************************/ function Update_Popup() { const VERSION = "V40.0"; const PREV_VERSION = "V38.4"; const currentVersion = localStorage.getItem('version') || "V0.0"; if (currentVersion === VERSION) return; localStorage.setItem('version', VERSION); if (localStorage.getItem(PREV_VERSION)) { localStorage.removeItem(PREV_VERSION); } const css = ` .first-time-popup { display: flex; position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); justify-content: center; align-items: center; z-index: 1000; opacity: 0; animation: fadeIn 0.5s cubic-bezier(0.22, 0.61, 0.36, 1) forwards; backdrop-filter: blur(6px); } .first-time-popup-content { background: #2a2a2a; border-radius: 20px; padding: 0; width: 900px; max-width: 95%; max-height: 85vh; overflow: hidden; box-shadow: 0 25px 50px rgba(0, 0, 0, 0.4); border: 1px solid #404040; color: #e8e8e8; transform: scale(0.95); animation: scaleUp 0.6s cubic-bezier(0.18, 0.89, 0.32, 1.28) forwards; position: relative; display: flex; flex-direction: column; will-change: transform; } .popup-header { padding: 24px 32px; border-bottom: 1px solid #404040; display: flex; align-items: center; gap: 16px; background: #1f1f1f; position: relative; } .popup-logo { width: 56px; height: 56px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); flex-shrink: 0; } .popup-header-content { flex: 1; } .popup-title { font-size: 24px; font-weight: 600; color: #ffffff; margin: 0 0 4px; letter-spacing: -0.5px; } .popup-version { display: inline-block; background: #1a1a1a; color: #ffffff; padding: 6px 12px; border-radius: 6px; font-size: 13px; font-weight: 500; border: 1px solid #404040; } .popup-main { display: flex; flex: 1; min-height: 0; } .popup-left { flex: 1; padding: 24px; border-right: 1px solid #404040; overflow-y: auto; background: #252525; } .popup-right { flex: 1; padding: 24px; overflow-y: auto; background: #2a2a2a; display: flex; flex-direction: column; } .features-title { font-size: 18px; font-weight: 600; color: #ffffff; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; } .feature-item { margin-bottom: 12px; border-radius: 10px; overflow: hidden; border: 1px solid #404040; transition: all 0.4s cubic-bezier(0.22, 0.61, 0.36, 1); cursor: pointer; } .feature-item:hover { border-color: #555555; background: #303030; transform: translateY(-2px); } .feature-item.active { border-color: #666666; background: #303030; } .feature-header { display: flex; align-items: center; padding: 16px; background: #1f1f1f; transition: all 0.4s cubic-bezier(0.22, 0.61, 0.36, 1); user-select: none; } .feature-item:hover .feature-header { background: #2a2a2a; } .feature-item.active .feature-header { background: #333333; } .feature-icon { font-size: 20px; margin-right: 12px; min-width: 24px; transition: transform 0.3s ease; } .feature-item:hover .feature-icon { transform: scale(1.1); } .feature-title { flex: 1; font-size: 15px; font-weight: 500; color: #ffffff; margin: 0; } .feature-badge { background: #404040; color: #cccccc; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; transition: all 0.3s ease; } .feature-item:hover .feature-badge { transform: translateX(3px); } .detail-panel { background: #1f1f1f; border-radius: 12px; padding: 24px; margin-bottom: 20px; border: 1px solid #404040; flex: 1; display: flex; flex-direction: column; opacity: 0; transform: translateY(15px); animation: fadeInUp 0.6s cubic-bezier(0.22, 0.61, 0.36, 1) forwards; will-change: transform, opacity; } .detail-title { font-size: 20px; font-weight: 600; color: #ffffff; margin: 0 0 8px; display: flex; align-items: center; gap: 10px; } .detail-subtitle { font-size: 13px; color: #999999; margin-bottom: 16px; text-transform: uppercase; letter-spacing: 0.5px; } .detail-description { font-size: 14px; color: #cccccc; line-height: 1.6; margin-bottom: 16px; flex: 1; } .detail-settings { padding: 16px; background: #252525; border-radius: 8px; border: 1px solid #404040; margin-top: auto; } .setting-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } .setting-row:last-child { margin-bottom: 0; } .setting-label { font-size: 13px; color: #cccccc; font-weight: 500; } .setting-value { font-size: 12px; color: #999999; padding: 4px 8px; background: #1a1a1a; border-radius: 4px; border: 1px solid #404040; } .welcome-panel { text-align: center; padding: 40px 20px; color: #999999; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; } .welcome-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.5; animation: float 4s ease-in-out infinite; } .welcome-text { font-size: 16px; margin-bottom: 8px; } .welcome-subtext { font-size: 13px; color: #666666; } .developer-message { background: #1a1a1a; border-radius: 8px; padding: 16px; margin-bottom: 20px; border-left: 3px solid #555555; transition: all 0.4s ease; } .developer-message:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } .developer-message-title { font-weight: 600; color: #ffffff; margin-bottom: 8px; font-size: 14px; } .developer-message-text { font-size: 13px; color: #cccccc; line-height: 1.5; } .help-section { background: #1f1f1f; border-radius: 8px; padding: 16px; border: 1px solid #404040; } .help-title { font-size: 14px; font-weight: 600; color: #ffffff; margin-bottom: 12px; } .help-link { color: #70a5ff; text-decoration: none; font-size: 13px; display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-radius: 6px; transition: all 0.4s cubic-bezier(0.22, 0.61, 0.36, 1); background: rgba(112, 165, 255, 0.1); border: 1px solid rgba(112, 165, 255, 0.2); } .help-link:hover { color: #ffffff; background: rgba(112, 165, 255, 0.2); border-color: rgba(112, 165, 255, 0.4); transform: translateY(-2px); } .help-link-icon { font-size: 16px; transition: transform 0.3s ease; } .help-link:hover .help-link-icon { transform: translateX(3px); } .first-time-popup-close { position: absolute; top: 16px; right: 16px; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: #888888; font-size: 18px; font-weight: 300; border-radius: 8px; transition: all 0.4s cubic-bezier(0.22, 0.61, 0.36, 1); background: rgba(255, 255, 255, 0.05); border: 1px solid transparent; z-index: 10; } .first-time-popup-close:hover { color: #ffffff; background: rgba(255, 255, 255, 0.1); border-color: #555555; transform: rotate(90deg); } .popup-footer { padding: 16px 32px; border-top: 1px solid #404040; background: #1f1f1f; text-align: center; } .popup-note { font-size: 12px; color: #999999; margin: 0; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } @keyframes scaleUp { 0% { transform: scale(0.95) translateY(10px); } 100% { transform: scale(1) translateY(0); } } @keyframes scaleDown { from { transform: scale(1); } to { transform: scale(0.9); opacity: 0; } } @keyframes fadeInUp { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } } @keyframes float { 0% { transform: translateY(0px); } 50% { transform: translateY(-5px); } 100% { transform: translateY(0px); } } /* Scrollbar styling */ .popup-left::-webkit-scrollbar, .popup-right::-webkit-scrollbar { width: 6px; } .popup-left::-webkit-scrollbar-track, .popup-right::-webkit-scrollbar-track { background: #1a1a1a; } .popup-left::-webkit-scrollbar-thumb, .popup-right::-webkit-scrollbar-thumb { background: #555555; border-radius: 3px; transition: background 0.3s ease; } .popup-left::-webkit-scrollbar-thumb:hover, .popup-right::-webkit-scrollbar-thumb:hover { background: #666666; } /* Responsive design */ @media (max-width: 768px) { .first-time-popup-content { width: 95%; flex-direction: column; } .popup-main { flex-direction: column; } .popup-left, .popup-right { flex: none; } .popup-left { border-right: none; border-bottom: 1px solid #404040; } } `; const style = document.createElement('style'); style.innerHTML = css; document.head.appendChild(style); const featureData = { smartsearch: { title: "SmartSearch", icon: "๐Ÿง ", subtitle: "Smarter, Faster Searches", description: "โšก Lightning fast search bar that shows results as you type with almost instant results. ๐Ÿ” See games, users, and groups appear as you type!", settings: [{ label: "Enabled by default", value: "True" }, { label: "Toggle Location", value: "General Tab" }, { label: "Scope", value: "Roblox.com/*" } ] }, mutualfriends: { title: "Mutual Friends", icon: "๐Ÿ‘ฅ", subtitle: "Find Who You Both Know", description: "๐Ÿค See if someone shares the same friend as you. ๐Ÿ‘€", settings: [{ label: "Enabled by default", value: "True" }, { label: "Toggle Location", value: "Extras Tab" }, { label: "Scope", value: "Roblox.com/users/*" } ] }, chatcontrols: { title: "Disable Chat", icon: "๐Ÿ’ฌ", subtitle: "Turn Off Chat Easily", description: "๐Ÿ› ๏ธ Have the ability to remove the chat bar on the website! ๐Ÿšซ", settings: [{ label: "Enabled by default", value: "False" }, { label: "Toggle Location", value: "Extras Tab" }, { label: "Scope", value: "Roblox.com/*" } ] }, quicklaunchgames_feature: { title: "Quick Launch Games", icon: "โšก", subtitle: "Quickly Play Your Games", description: "๐ŸŽฎ Launch your games directly from the homepage. It even connects you to the closest server for the best experience! ๐Ÿš€", settings: [{ label: "Enabled by default", value: "True" }, { label: "Toggle Location", value: "Extras Tab" }, { label: "Scope", value: "Roblox.com/home" } ] } }; const popupHTML = `
`; const popupContainer = document.createElement('div'); popupContainer.innerHTML = popupHTML; document.body.appendChild(popupContainer); const closeButton = popupContainer.querySelector('.first-time-popup-close'); const popup = popupContainer.querySelector('.first-time-popup'); const featureItems = popupContainer.querySelectorAll('.feature-item'); const welcomePanel = popupContainer.querySelector('#welcome-panel'); const detailPanel = popupContainer.querySelector('#detail-panel'); featureItems.forEach(item => { item.addEventListener('click', (e) => { featureItems.forEach(i => i.classList.remove('active')); item.classList.add('active'); const featureKey = item.dataset.feature; const feature = featureData[featureKey]; if (feature) { welcomePanel.style.display = 'none'; detailPanel.style.display = 'flex'; detailPanel.classList.remove('detail-panel'); void detailPanel.offsetWidth; detailPanel.classList.add('detail-panel'); detailPanel.innerHTML = `
${feature.icon} ${feature.title}
${feature.subtitle}
${feature.description}
${feature.settings.map(setting => `
${setting.label}: ${setting.value}
`).join('')}
`; } }); }); closeButton.addEventListener('click', (e) => { popup.style.animation = 'fadeOut 0.5s cubic-bezier(0.22, 0.61, 0.36, 1) forwards'; popup.querySelector('.first-time-popup-content').style.animation = 'scaleDown 0.5s cubic-bezier(0.22, 0.61, 0.36, 1) forwards'; setTimeout(() => { popup.parentNode.removeChild(popup); }, 500); }); } /******************************************************* name of function: initializeLocalStorage description: adds default settings *******************************************************/ function initializeLocalStorage() { // define default settings const defaultSettings = { enableLogs: false, // disabled by default removeads: true, // enabled by default togglefilterserversbutton: true, // enable by default toggleserverhopbutton: true, // enable by default AutoRunServerRegions: false, // disabled by default ShowOldGreeting: false, // disabled by default togglerecentserverbutton: true, // enable by default quicknav: false, // disabled by default prioritylocation: "automatic", // automatic by default fastservers: false, // disabled by default invertplayercount: false, // disabled by default enablenotifications: true, // enabled by default disabletrailer: true, // enabled by default gamequalityfilter: false, // disabled by default mutualfriends: true, // enabled by default disablechat: false, // disabled by default smartsearch: true, // enabled by default quicklaunchgames: true, // enabled by default // experiencedtime: false, // not needed anymore }; // Loop through default settings and set them in localStorage if they don't exist Object.entries(defaultSettings).forEach(([key, value]) => { const storageKey = `ROLOCATE_${key}`; if (localStorage.getItem(storageKey) === null) { localStorage.setItem(storageKey, value); } }); } /******************************************************* name of function: initializeCoordinatesStorage description: finds coordinates *******************************************************/ function initializeCoordinatesStorage() { // coors alredyt in there try { const storedCoords = GM_getValue("ROLOCATE_coordinates"); if (!storedCoords) { // make empty GM_setValue("ROLOCATE_coordinates", JSON.stringify({ lat: "", lng: "" })); } else { // yea const parsedCoords = JSON.parse(storedCoords); if ((!parsedCoords.lat || !parsedCoords.lng) && localStorage.getItem("ROLOCATE_prioritylocation") === "manual") { // if manual mode but no coordinates, revert to automatic localStorage.setItem("ROLOCATE_prioritylocation", "automatic"); } } } catch (e) { ConsoleLogEnabled("Error initializing coordinates storage:", e); // not commenting this cause im bored GM_setValue("ROLOCATE_coordinates", JSON.stringify({ lat: "", lng: "" })); } } /******************************************************* name of function: getSettingsContent description: adds section to settings page *******************************************************/ function getSettingsContent(section) { if (section === "home") { return `
Rolocate: Version 40.0

Rolocate by Oqarshi.

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

`; } if (section === "appearance") { return `
`; } 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 `
Small features that might be useful!
`; } if (section === "about") { return `

Credits

This project was created by:

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

โš™๏ธ General Tab

๐ŸŽจ Appearance Tab

๐Ÿš€ Advanced Tab

โœจ Extra Tab

Need more help?

  • You can visit the troubleshooting or create an issue on greasyfork for more assistance.
  • `; } // General tab (default) return `
    `; } /******************************************************* name of function: openSettingsMenu description: opens setting menu and makes it look good *******************************************************/ function openSettingsMenu() { if (document.getElementById("userscript-settings-menu")) return; // storage make go uyea initializeLocalStorage(); initializeCoordinatesStorage(); const overlay = document.createElement("div"); overlay.id = "userscript-settings-menu"; overlay.innerHTML = `

    RoLocate

    Home

    ${getSettingsContent("home")}
    `; document.body.appendChild(overlay); // put css in const style = document.createElement("style"); style.textContent = ` .grayish-center { color: white; font-weight: bold; text-align: center; position: relative; display: inline-block; font-size: 18px !important; /* idk whats overriding this but screw finding that */ } .grayish-center::after { content: ""; display: block; margin: 4px auto 0; width: 50%; border-bottom: 2px solid #888888; opacity: 0.6; border-radius: 2px; } li a.about-link { position: relative !important; font-weight: bold !important; color: #dc2626 !important; text-decoration: none !important; cursor: pointer !important; transition: color 0.2s ease !important; } li a.about-link::after { content: '' !important; position: absolute !important; left: 0 !important; bottom: -2px !important; height: 2px !important; width: 100% !important; background-color: #dc2626 !important; transform: scaleX(0) !important; transform-origin: left !important; transition: transform 0.3s ease !important; } li a.about-link:hover { color: #b91c1c !important; } li a.about-link:hover::after { transform: scaleX(1) !important; } .about-section ul li a { position: relative; font-weight: bold; color: #dc2626; /* red-600 */ text-decoration: none; cursor: pointer; transition: color 0.2s ease; } .about-section ul li a::after { content: ''; position: absolute; left: 0; bottom: -2px; height: 2px; width: 100%; background-color: #dc2626; /* red underline */ transform: scaleX(0); transform-origin: left; transition: transform 0.3s ease; } .about-section ul li a:hover { color: #b91c1c; /* darker red on hover */ } .about-section ul li a:hover::after { transform: scaleX(1); } .license-note { font-size: 0.65em; color: #999; margin-top: 12px; font-style: italic; text-align: center; } .edit-button { margin-left: auto; padding: 2px 8px; font-size: 12px; border: none; border-radius: 6px; background: linear-gradient(145deg, #3a3a3a, #2c2c2c); color: #f0f0f0; cursor: pointer; font-weight: 500; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 2px 4px rgba(0, 0, 0, 0.25); transition: all 0.2s ease; } .edit-button:hover { background: linear-gradient(145deg, #4a4a4a, #343434); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 3px 6px rgba(0, 0, 0, 0.35); transform: translateY(-0.5px); } .help-icon { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; background: rgba(220, 53, 69, 0.15); border-radius: 50%; font-size: 12px; font-weight: 600; color: #e02d3c; cursor: pointer; transition: all 0.2s ease; margin-left: auto; /* Pushes it to the right */ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); position: relative; border: 1px solid rgba(220, 53, 69, 0.2); } .help-icon:hover { background: rgba(220, 53, 69, 0.25); transform: translateY(-1px); box-shadow: 0 3px 5px rgba(0, 0, 0, 0.15); cursor: pointer; } /* Add tooltip on hover */ .help-icon::after { content: "Click for help"; position: absolute; bottom: -30px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.8); color: white; padding: 4px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap; opacity: 0; visibility: hidden; transition: all 0.2s ease; pointer-events: none; } .help-icon:hover::after { opacity: 1; visibility: visible; } .help-icon:active { transform: translateY(0); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.4); } 70% { box-shadow: 0 0 0 6px rgba(220, 53, 69, 0); } 100% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0); } } .help-icon.attention { animation: pulse 2s infinite; } .highlight-help-item { animation: highlight 1.5s ease; background: rgba(76, 175, 80, 0.1); /* Green highlight */ border-left: 3px solid #4CAF50; /* Green border */ } @keyframes highlight { 0% { background: rgba(76, 175, 80, 0.3); } /* Green start */ 100% { background: rgba(76, 175, 80, 0.1); } /* Green end */ } .new_label .new { margin-left: 8px; color: #32cd32; /* LimeGreen */ font-size: 12px; font-weight: bold; background-color: rgba(50, 205, 50, 0.1); /* soft green background */ padding: 2px 6px; border-radius: 3px; position: relative; z-index: 10001; } .new_label .tooltip { visibility: hidden; background-color: rgba(0, 0, 0, 0.75); color: #fff; font-size: 12px; padding: 6px; border-radius: 5px; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); white-space: nowrap; z-index: 10001; opacity: 0; transition: opacity 0.3s; } .new_label .new:hover .tooltip { visibility: visible; opacity: 1; z-index: 10001; } .experiment_label .experimental { margin-left: 8px; color: gold; font-size: 12px; font-weight: bold; background-color: rgba(255, 215, 0, 0.1); padding: 2px 6px; border-radius: 3px; position: relative; /* Needed for positioning the tooltip */ z-index: 10001; } .experiment_label .tooltip { visibility: hidden; background-color: rgba(0, 0, 0, 0.7); color: #fff; font-size: 12px; padding: 6px; border-radius: 5px; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); white-space: nowrap; z-index: 10001; opacity: 0; transition: opacity 0.3s; } .experiment_label .experimental:hover .tooltip { visibility: visible; opacity: 1; z-index: 10001; } @keyframes fadeIn { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } } @keyframes fadeOut { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.96); } } @keyframes sectionFade { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } } @keyframes slideIn { from { transform: translateX(-20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } #userscript-settings-menu { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 10000; animation: fadeIn 0.7s cubic-bezier(0.19, 1, 0.22, 1); } .settings-container { display: flex; position: relative; width: 580px; /* Reduced from 680px */ height: 440px; /* Reduced from 480px */ background: linear-gradient(145deg, #1a1a1a, #232323); border-radius: 12px; /* Slightly smaller radius */ overflow: hidden; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.7); font-family: 'Inter', 'Segoe UI', Arial, sans-serif; border: 1px solid rgba(255, 255, 255, 0.05); } #close-settings { position: absolute; top: 12px; /* Reduced from 16px */ right: 12px; /* Reduced from 16px */ background: transparent; border: none; color: #c0c0c0; font-size: 20px; /* Reduced from 22px */ cursor: pointer; z-index: 10001; transition: all 0.5s ease; width: 30px; /* Reduced from 34px */ height: 30px; /* Reduced from 34px */ border-radius: 50%; display: flex; align-items: center; justify-content: center; } #close-settings:hover { color: #ff3b47; background: rgba(255, 59, 71, 0.1); transform: rotate(90deg); } .settings-sidebar { width: 32%; /* Reduced from 35% */ background: #272727; padding: 18px 12px; /* Reduced from 24px 15px */ color: white; display: flex; flex-direction: column; align-items: center; box-shadow: 6px 0 12px -6px rgba(0,0,0,0.3); position: relative; overflow: hidden; } .settings-sidebar h2 { margin-bottom: 16px; /* Reduced from 20px */ font-weight: 600; font-size: 22px; /* Reduced from 24px */ text-shadow: 0 1px 3px rgba(0,0,0,0.5); text-decoration: none; position: relative; text-align: center; } .settings-sidebar h2::after { content: ""; position: absolute; left: 50%; transform: translateX(-50%); bottom: -6px; /* Reduced from -8px */ width: 36px; /* Reduced from 40px */ height: 3px; background: white; border-radius: 2px; } .settings-sidebar ul { list-style: none; padding: 0; width: 100%; margin-top: 5px; /* Reduced from 10px */ } .settings-sidebar li { padding: 10px 12px; /* Reduced from 14px */ margin: 6px 0; /* Reduced from 8px 0 */ text-align: left; cursor: pointer; transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1); border-radius: 8px; font-weight: 500; font-size: 17px; /* increased from 15px */ position: relative; animation: slideIn 0.5s cubic-bezier(0.19, 1, 0.22, 1); animation-fill-mode: both; display: flex; align-items: center; } .settings-sidebar li:hover { background: #444; transform: translateX(5px); } .settings-sidebar .active { background: #444; color: white; transform: translateX(0); } .settings-sidebar .active:hover { transform: translateX(0); } .settings-sidebar li:hover::before { height: 100%; } .settings-sidebar .active::before { background: #dc3545; } /* Custom Scrollbar */ .settings-content { flex: 1; padding: 24px; /* Reduced from 32px */ color: white; text-align: center; max-height: 100%; overflow-y: auto; scrollbar-width: thin; scrollbar-color: darkgreen black; background: #1e1e1e; position: relative; } /* Webkit (Chrome, Safari) Scrollbar */ .settings-content::-webkit-scrollbar { width: 6px; /* Reduced from 8px */ } .settings-content::-webkit-scrollbar-track { background: #333; border-radius: 3px; /* Reduced from 4px */ } .settings-content::-webkit-scrollbar-thumb { background: linear-gradient(180deg, #dc3545, #b02a37); border-radius: 3px; /* Reduced from 4px */ } .settings-content::-webkit-scrollbar-thumb:hover { background: linear-gradient(180deg, #ff3b47, #dc3545); } .settings-content h2 { margin-bottom: 24px; /* Reduced from 30px */ font-weight: 600; font-size: 22px; /* Reduced from 24px */ color: white; text-shadow: 0 1px 3px rgba(0,0,0,0.4); letter-spacing: 0.5px; position: relative; display: inline-block; padding-bottom: 6px; /* Reduced from 8px */ } .settings-content h2::after { content: ""; position: absolute; bottom: 0; left: 0; width: 100%; height: 2px; background: white; border-radius: 2px; } .settings-content div { animation: sectionFade 0.7s cubic-bezier(0.19, 1, 0.22, 1); } /* Toggle Slider Styles */ .toggle-slider { display: flex; align-items: center; margin: 12px 0; /* Reduced from 16px 0 */ cursor: pointer; padding: 8px 14px; /* Reduced from 10px 16px */ background: rgba(255, 255, 255, 0.03); border-radius: 6px; /* Reduced from 8px */ transition: all 0.5s ease; user-select: none; border: 1px solid rgba(255, 255, 255, 0.05); } .toggle-slider:hover { background: rgba(255, 255, 255, 0.05); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); transform: translateY(-2px); } .toggle-slider input { display: none; } .toggle-slider .slider { position: relative; display: inline-block; width: 42px; /* Reduced from 48px */ height: 22px; /* Reduced from 24px */ background-color: rgba(255, 255, 255, 0.2); border-radius: 22px; margin-right: 12px; /* Reduced from 14px */ transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1); box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2); } .toggle-slider .slider::before { content: ""; position: absolute; height: 16px; /* Reduced from 18px */ width: 16px; /* Reduced from 18px */ left: 3px; bottom: 3px; background-color: white; border-radius: 50%; transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); } .toggle-slider input:checked + .slider { background-color: #4CAF50; box-shadow: 0 0 0 1px rgba(220, 53, 69, 0.05), inset 0 1px 3px rgba(0, 0, 0, 0.2); } .toggle-slider input:checked + .slider::before { transform: translateX(20px); /* Reduced from 24px */ } .toggle-slider input:checked + .slider::after { opacity: 1; } .rolocate-logo { width: 90px !important; /* Reduced from 110px */ height: 90px !important; /* Reduced from 110px */ object-fit: contain; border-radius: 14px; /* Reduced from 16px */ display: block; margin: 0 auto 16px auto; /* Reduced from 20px */ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4); transition: all 0.5s ease; border: 2px solid rgba(220, 53, 69, 0.4); } .rolocate-logo:hover { transform: scale(1.05); } .version { font-size: 13px; /* Reduced from 14px */ color: #aaa; margin-bottom: 24px; /* Reduced from 30px */ display: inline-block; padding: 5px 14px; /* Reduced from 6px 16px */ background: rgba(220, 53, 69, 0.1); border-radius: 18px; /* Reduced from 20px */ border: 1px solid rgba(220, 53, 69, 0.2); } .settings-content ul { text-align: left; list-style-type: none; padding: 0; margin-top: 16px; /* Reduced from 20px */ } .settings-content ul li { margin: 12px 0; /* Reduced from 16px 0 */ padding: 10px 14px; /* Reduced from 12px 16px */ background: rgba(255, 255, 255, 0.03); border-radius: 6px; /* Reduced from 8px */ transition: all 0.4s ease; } .settings-content ul li:hover { background: rgba(255, 255, 255, 0.05); border-left: 3px solid #4CAF50; transform: translateX(5px); } .settings-content ul li strong { color: #4CAF50; } .warning_advanced { font-size: 14px; color: #ff3b47; font-weight: bold; padding: 8px 14px; background: rgba(220, 53, 69, 0.1); border-radius: 6px; margin-bottom: 16px; display: inline-block; border: 1px solid rgba(220, 53, 69, 0.2); box-shadow: 0 0 6px rgba(220, 53, 69, 0.3); transition: box-shadow 0.3s ease; } .warning_advanced:hover { box-shadow: 0 0 12px rgba(220, 53, 69, 0.6); } .extras_section { font-size: 14px; color: #0d6efd; /* vibrant blue */ font-weight: bold; padding: 8px 14px; background: rgba(13, 110, 253, 0.1); border-radius: 6px; margin-bottom: 16px; display: inline-block; border: 1px solid rgba(13, 110, 253, 0.3); box-shadow: 0 0 6px rgba(13, 110, 253, 0.3); transition: box-shadow 0.3s ease; } .extras_section:hover { box-shadow: 0 0 12px rgba(13, 110, 253, 0.6); } .average_text { font-size: 16px; /* Reduced from 18px */ color: #e0e0e0; font-weight: 500; margin-top: 12px; /* Reduced from 15px */ line-height: 1.5; letter-spacing: 0.3px; background: linear-gradient(90deg, #ff3b47, #ff6b74); -webkit-background-clip: text; -webkit-text-fill-color: transparent; display: inline-block; } .edit-nav-button { padding: 6px 14px; /* Reduced from 8px 16px */ background: #4CAF50; color: white; border: none; border-radius: 6px; /* Reduced from 8px */ cursor: pointer; font-family: 'Inter', 'Helvetica', sans-serif; font-size: 12px; /* Reduced from 13px */ font-weight: 600; letter-spacing: 0.5px; text-transform: uppercase; transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1); height: auto; line-height: 1.5; position: relative; overflow: hidden; } .edit-nav-button:hover { transform: translateY(-3px); background: linear-gradient(135deg, #1e8449 0%, #196f3d 100%); } .edit-nav-button:hover::before { left: 100%; } .edit-nav-button:active { background: linear-gradient(135deg, #1e8449 0%, #196f3d 100%); transform: translateY(1px); } /* Dropdown styling */ select { width: 100%; padding: 10px 14px; /* Reduced from 12px 16px */ border-radius: 6px; /* Reduced from 8px */ background: rgba(255, 255, 255, 0.05); color: #e0e0e0; font-size: 14px; /* Reduced from 14px */ appearance: none; background-image: url('data:image/svg+xml;utf8,'); background-repeat: no-repeat; background-position: right 14px center; background-size: 14px; transition: all 0.5s ease; cursor: pointer; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); border-color: rgba(255, 255, 255, 0.05); } /* Dropdown hint styling */ #location-hint { margin-top: 10px; /* Reduced from 12px */ font-size: 12px; /* Reduced from 13px */ color: #c0c0c0; background: rgba(255, 255, 255, 0.05); border-radius: 6px; /* Reduced from 8px */ padding: 10px 14px; /* Reduced from 12px 16px */ border: 1px solid rgba(255, 255, 255, 0.05); line-height: 1.6; transition: all 0.5s ease; } /* Section separator */ .section-separator { width: 100%; height: 1px; background: linear-gradient(90deg, transparent, #272727, transparent); margin: 24px 0; /* Reduced from 30px 0 */ } /* Help section styles */ .help-section h3, .about-section h3 { color: white; margin-top: 20px; /* Reduced from 25px */ margin-bottom: 12px; /* Reduced from 15px */ font-size: 16px; /* Reduced from 18px */ text-align: left; } /* Hint text styling */ .hint-text { font-size: 13px; /* Reduced from 14px */ color: #a0a0a0; margin-top: 6px; /* Reduced from 8px */ margin-left: 16px; /* Reduced from 20px */ text-align: left; } /* Location settings styling */ .location-settings { background: rgba(255, 255, 255, 0.03); border-radius: 6px; /* Reduced from 8px */ padding: 14px; /* Reduced from 16px */ margin-top: 16px; /* Reduced from 20px */ border: 1px solid rgba(255, 255, 255, 0.05); transition: all 0.5s ease; } .setting-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; /* Reduced from 12px */ } .setting-header span { font-size: 14px; /* Reduced from 15px */ font-weight: 500; } .help-icon { display: inline-flex; align-items: center; justify-content: center; width: 18px; /* Reduced from 20px */ height: 18px; /* Reduced from 20px */ background: rgba(220, 53, 69, 0.2); border-radius: 50%; font-size: 11px; /* Reduced from 12px */ color: #ff3b47; cursor: help; transition: all 0.5s ease; } /* Manual coordinates input styling */ #manual-coordinates { margin-top: 12px !important; /* Reduced from 15px */ } .coordinates-inputs { gap: 8px !important; /* Reduced from 10px */ margin-bottom: 10px !important; /* Reduced from 12px */ } #manual-coordinates input { padding: 8px 10px !important; /* Reduced from 10px 12px */ border-radius: 6px !important; /* Reduced from 8px */ font-size: 13px !important; /* Reduced from default */ } #manual-coordinates label { margin-bottom: 6px !important; /* Reduced from 8px */ font-size: 13px !important; /* Reduced from 14px */ } #save-coordinates { margin-top: 6px !important; /* Reduced from 8px */ } /* Animated content */ .animated-content { animation: sectionFade 0.7s cubic-bezier(0.19, 1, 0.22, 1); } `; document.head.appendChild(style); // hopefully this works document.querySelectorAll(".settings-sidebar li").forEach((li, index) => { // aniamtions stuff li.style.animationDelay = `${0.05 * (index + 1)}s`; li.addEventListener("click", function() { const currentActive = document.querySelector(".settings-sidebar .active"); if (currentActive) currentActive.classList.remove("active"); this.classList.add("active"); const section = this.getAttribute("data-section"); const settingsBody = document.getElementById("settings-body"); const settingsTitle = document.getElementById("settings-title"); // aniamtions stuff settingsBody.style.opacity = "0"; settingsBody.style.transform = "translateY(10px)"; settingsTitle.style.opacity = "0"; settingsTitle.style.transform = "translateY(10px)"; setTimeout(() => { // aniamtions stuff settingsTitle.textContent = section.charAt(0).toUpperCase() + section.slice(1); settingsBody.innerHTML = getSettingsContent(section); // quick nav stuff if (section === "appearance") { const quickNavCheckbox = document.getElementById("quicknav"); const editButton = document.getElementById("edit-quicknav-btn"); if (quickNavCheckbox && editButton) { // Set initial display based on localStorage editButton.style.display = localStorage.getItem("ROLOCATE_quicknav") === "true" ? "block" : "none"; // Update localStorage and edit button visibility when checkbox changes quickNavCheckbox.addEventListener("change", function() { const isEnabled = this.checked; localStorage.setItem("ROLOCATE_quicknav", isEnabled); editButton.style.display = isEnabled ? "block" : "none"; }); } } if (section === "extras") { const gameQualityCheckbox = document.getElementById("gamequalityfilter"); const editButton = document.getElementById("edit-gamequality-btn"); if (gameQualityCheckbox && editButton) { // Set visibility on load editButton.style.display = localStorage.getItem("ROLOCATE_gamequalityfilter") === "true" ? "block" : "none"; // Toggle visibility when the checkbox changes gameQualityCheckbox.addEventListener("change", function() { const isEnabled = this.checked; editButton.style.display = isEnabled ? "block" : "none"; }); } } settingsBody.style.transition = "all 0.4s cubic-bezier(0.19, 1, 0.22, 1)"; settingsTitle.style.transition = "all 0.4s cubic-bezier(0.19, 1, 0.22, 1)"; void settingsBody.offsetWidth; void settingsTitle.offsetWidth; settingsBody.style.opacity = "1"; settingsBody.style.transform = "translateY(0)"; settingsTitle.style.opacity = "1"; settingsTitle.style.transform = "translateY(0)"; applyStoredSettings(); }, 200); }); }); // Close button with enhanced animation document.getElementById("close-settings").addEventListener("click", function() { // Check if manual mode is selected with empty coordinates const priorityLocation = localStorage.getItem("ROLOCATE_prioritylocation"); if (priorityLocation === "manual") { try { const coords = JSON.parse(GM_getValue("ROLOCATE_coordinates", '{"lat":"","lng":""}')); if (!coords.lat || !coords.lng) { notifications('Please set the latitude and longitude values for the manual location, or set it to automatic.', 'error', 'โš ๏ธ', 8000); return; // Prevent closing } } catch (e) { ConsoleLogEnabled("Error checking coordinates:", e); notifications('Error checking location settings', 'error', 'โš ๏ธ', 8000); return; // Prevent closing } } // Proceed with closing if validation passes const menu = document.getElementById("userscript-settings-menu"); menu.style.animation = "fadeOut 0.4s cubic-bezier(0.19, 1, 0.22, 1) forwards"; // Add rotation to close button when closing this.style.transform = "rotate(90deg)"; setTimeout(() => menu.remove(), 400); }); // Apply stored settings immediately when opened applyStoredSettings(); // Add ripple effect to buttons const buttons = document.querySelectorAll(".edit-nav-button, .settings-button"); buttons.forEach(button => { button.addEventListener("mousedown", function(e) { const ripple = document.createElement("span"); const rect = this.getBoundingClientRect(); const size = Math.max(rect.width, rect.height); const x = e.clientX - rect.left - size / 2; const y = e.clientY - rect.top - size / 2; ripple.style.cssText = ` position: absolute; background: rgba(255,255,255,0.4); border-radius: 50%; pointer-events: none; width: ${size}px; height: ${size}px; top: ${y}px; left: ${x}px; transform: scale(0); transition: transform 0.6s, opacity 0.6s; `; this.appendChild(ripple); setTimeout(() => { ripple.style.transform = "scale(2)"; ripple.style.opacity = "0"; setTimeout(() => ripple.remove(), 600); }, 10); }); }); // Handle help icon clicks document.addEventListener('click', function(e) { if (e.target.classList.contains('help-icon')) { // Prevent the event from bubbling up to the toggle button e.stopPropagation(); e.preventDefault(); const helpItem = e.target.getAttribute('data-help'); if (helpItem) { // Switch to help tab const helpTab = document.querySelector('.settings-sidebar li[data-section="help"]'); if (helpTab) helpTab.click(); // Scroll to the corresponding help item after a short delay setTimeout(() => { const helpElement = document.getElementById(`help-${helpItem}`); if (helpElement) { helpElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); helpElement.classList.add('highlight-help-item'); setTimeout(() => { helpElement.classList.remove('highlight-help-item'); }, 1500); } }, 300); } } }); } /******************************************************* name of function: showQuickNavPopup description: quick nav popup menu *******************************************************/ function showQuickNavPopup() { // Remove existing quick nav if it exists const existingNav = document.getElementById("premium-quick-nav"); if (existingNav) existingNav.remove(); // POPUP CREATION // Create overlay const overlay = document.createElement("div"); overlay.id = "quicknav-overlay"; overlay.style.position = "fixed"; overlay.style.top = "0"; overlay.style.left = "0"; overlay.style.width = "100%"; overlay.style.height = "100%"; overlay.style.backgroundColor = "rgba(0,0,0,0)"; // Darker overlay for dark mode overlay.style.backdropFilter = "blur(1px)"; overlay.style.zIndex = "10000"; overlay.style.opacity = "0"; overlay.style.transition = "opacity 0.3s ease"; // Create popup const popup = document.createElement("div"); popup.id = "premium-quick-nav-popup"; popup.style.position = "fixed"; popup.style.top = "50%"; popup.style.left = "50%"; popup.style.transform = "translate(-50%, -50%) scale(0.95)"; popup.style.opacity = "0"; popup.style.background = "linear-gradient(145deg, #0a0a0a, #121212)"; // Darker background for dark mode popup.style.color = "white"; popup.style.padding = "32px"; popup.style.borderRadius = "16px"; popup.style.boxShadow = "0 20px 40px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.05)"; popup.style.zIndex = "10001"; popup.style.width = "600px"; popup.style.maxWidth = "90%"; popup.style.maxHeight = "85vh"; popup.style.overflowY = "auto"; popup.style.transition = "transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.4s ease"; // Get saved quick navs (if any) const saved = JSON.parse(localStorage.getItem("ROLOCATE_quicknav_settings") || "[]"); // Build header const header = `

    Quick Navigation

    Configure up to 9 custom navigation shortcuts

    `; // Build inputs for 9 links in a 3x3 grid const inputsGrid = `
    ${Array.from({length: 9}, (_, i) => `

    ${i + 1}

    `).join("")}
    `; // Build footer with buttons const footer = `
    `; // Combine all sections popup.innerHTML = header + inputsGrid + footer; // Add elements to DOM document.body.appendChild(overlay); document.body.appendChild(popup); // POPUP EVENTS // Add input hover and focus effects popup.querySelectorAll('input').forEach(input => { input.addEventListener('focus', () => { input.style.background = 'rgba(255,255,255,0.1)'; input.style.boxShadow = '0 0 0 2px rgba(76, 175, 80, 0.4)'; }); input.addEventListener('blur', () => { input.style.background = 'rgba(255,255,255,0.05)'; input.style.boxShadow = 'none'; }); input.addEventListener('mouseover', () => { if (document.activeElement !== input) { input.style.background = 'rgba(255,255,255,0.08)'; } }); input.addEventListener('mouseout', () => { if (document.activeElement !== input) { input.style.background = 'rgba(255,255,255,0.05)'; } }); }); // Add button hover effects const saveBtn = popup.querySelector('#save-quicknav'); saveBtn.addEventListener('mouseover', () => { saveBtn.style.background = 'linear-gradient(90deg, #66BB6A, #4CAF50)'; saveBtn.style.boxShadow = '0 4px 15px rgba(76, 175, 80, 0.4)'; saveBtn.style.transform = 'translateY(-1px)'; }); saveBtn.addEventListener('mouseout', () => { saveBtn.style.background = 'linear-gradient(90deg, #4CAF50, #388E3C)'; saveBtn.style.boxShadow = '0 4px 12px rgba(76, 175, 80, 0.3)'; saveBtn.style.transform = 'translateY(0)'; }); const cancelBtn = popup.querySelector('#cancel-quicknav'); cancelBtn.addEventListener('mouseover', () => { cancelBtn.style.background = 'rgba(255,255,255,0.05)'; }); cancelBtn.addEventListener('mouseout', () => { cancelBtn.style.background = 'transparent'; }); // Animate in setTimeout(() => { overlay.style.opacity = "1"; popup.style.opacity = "1"; popup.style.transform = "translate(-50%, -50%) scale(1)"; }, 10); // POPUP CLOSE FUNCTION function closePopup() { overlay.style.opacity = "0"; popup.style.opacity = "0"; popup.style.transform = "translate(-50%, -50%) scale(0.95)"; setTimeout(() => { overlay.remove(); popup.remove(); }, 300); } // Save on click popup.querySelector("#save-quicknav").addEventListener("click", () => { const quickNavSettings = []; for (let i = 0; i < 9; i++) { const name = document.getElementById(`quicknav-name-${i}`).value.trim(); const link = document.getElementById(`quicknav-link-${i}`).value.trim(); if (name && link) { quickNavSettings.push({ name, link }); } } localStorage.setItem("ROLOCATE_quicknav_settings", JSON.stringify(quickNavSettings)); closePopup(); }); // Cancel button popup.querySelector("#cancel-quicknav").addEventListener("click", closePopup); // Close when clicking overlay overlay.addEventListener("click", (e) => { if (e.target === overlay) { closePopup(); } }); // Close with ESC key document.addEventListener("keydown", function escClose(e) { if (e.key === "Escape") { closePopup(); document.removeEventListener("keydown", escClose); } }); // AUTO-INIT AND KEYBOARD SHORTCUT // Set up keyboard shortcut (Alt+Q) document.addEventListener("keydown", function keyboardShortcut(e) { if (e.altKey && e.key === "q") { showQuickNavPopup(); } }); } /******************************************************* name of function: applyStoredSettings description: makes sure local storage is stored in correctly *******************************************************/ function applyStoredSettings() { // Handle all checkboxes document.querySelectorAll("input[type='checkbox']").forEach(checkbox => { const storageKey = `ROLOCATE_${checkbox.id}`; const savedValue = localStorage.getItem(storageKey); checkbox.checked = savedValue === "true"; checkbox.addEventListener("change", () => { localStorage.setItem(storageKey, checkbox.checked); }); }); // Handle dropdown for prioritylocation-select const prioritySelect = document.getElementById("prioritylocation-select"); if (prioritySelect) { const storageKey = "ROLOCATE_prioritylocation"; const savedValue = localStorage.getItem(storageKey) || "automatic"; prioritySelect.value = savedValue; // Show/hide coordinates inputs based on selected value const manualCoordinates = document.getElementById("manual-coordinates"); if (manualCoordinates) { manualCoordinates.style.display = savedValue === "manual" ? "block" : "none"; // Set input values from stored coordinates if available if (savedValue === "manual") { try { const savedCoords = JSON.parse(GM_getValue("ROLOCATE_coordinates", '{"lat":"","lng":""}')); document.getElementById("latitude").value = savedCoords.lat || ""; document.getElementById("longitude").value = savedCoords.lng || ""; // If manual mode but no coordinates saved, revert to automatic if (!savedCoords.lat || !savedCoords.lng) { prioritySelect.value = "automatic"; localStorage.setItem(storageKey, "automatic"); manualCoordinates.style.display = "none"; } } catch (e) { ConsoleLogEnabled("Error loading saved coordinates:", e); } } } prioritySelect.addEventListener("change", () => { const newValue = prioritySelect.value; localStorage.setItem(storageKey, newValue); // Show/hide coordinates inputs based on new value if (manualCoordinates) { manualCoordinates.style.display = newValue === "manual" ? "block" : "none"; // When switching to manual mode, load any saved coordinates if (newValue === "manual") { try { const savedCoords = JSON.parse(GM_getValue("ROLOCATE_coordinates", '{"lat":"","lng":""}')); document.getElementById("latitude").value = savedCoords.lat || ""; document.getElementById("longitude").value = savedCoords.lng || ""; // If no coordinates exist, keep the inputs empty } catch (e) { ConsoleLogEnabled("Error loading saved coordinates:", e); } } } }); } // Button click handlers const editQuickNavBtn = document.getElementById("edit-quicknav-btn"); if (editQuickNavBtn) { editQuickNavBtn.addEventListener("click", () => { showQuickNavPopup(); }); } const editQualityGameBtn = document.getElementById("edit-gamequality-btn"); if (editQualityGameBtn) { editQualityGameBtn.addEventListener("click", () => { openGameQualitySettings(); }); } const fastServersToggle = document.getElementById("fastservers"); if (fastServersToggle) { fastServersToggle.addEventListener("change", () => { if (fastServersToggle.checked) { notifications('Fast Server Search: 100x faster on Violentmonkey, ~2x on Tampermonkey. Replaces thumbnails with builderman to bypass rate limits.', 'info', '๐Ÿงช', 2000); } }); } const AutoRunServerRegions = document.getElementById("AutoRunServerRegions"); if (AutoRunServerRegions) { AutoRunServerRegions.addEventListener("change", () => { if (AutoRunServerRegions.checked) { notifications('Auto Server Regions works best when paired with Fast Server Search in Advanced Settings.', 'info', '๐Ÿงช', 2000); } }); } // Save coordinates button handler const saveCoordinatesBtn = document.getElementById("save-coordinates"); if (saveCoordinatesBtn) { saveCoordinatesBtn.addEventListener("click", () => { const latInput = document.getElementById("latitude"); const lngInput = document.getElementById("longitude"); const lat = latInput.value.trim(); const lng = lngInput.value.trim(); // If manual mode but no coordinates provided, revert to automatic if (!lat || !lng) { const prioritySelect = document.getElementById("prioritylocation-select"); if (prioritySelect) { prioritySelect.value = "automatic"; localStorage.setItem("ROLOCATE_prioritylocation", "automatic"); document.getElementById("manual-coordinates").style.display = "none"; // show feedback to user even if they dont see it saveCoordinatesBtn.textContent = "Reverted to Automatic!"; saveCoordinatesBtn.style.background = "#4CAF50"; setTimeout(() => { saveCoordinatesBtn.textContent = "Save Coordinates"; saveCoordinatesBtn.style.background = "background: #4CAF50;"; }, 2000); } return; } // Validate coordinates const latNum = parseFloat(lat); const lngNum = parseFloat(lng); if (isNaN(latNum) || isNaN(lngNum) || latNum < -90 || latNum > 90 || lngNum < -180 || lngNum > 180) { notifications('Invalid coordinates! Latitude must be between -90 and 90, and longitude between -180 and 180.', 'error', 'โš ๏ธ', '8000'); return; } // Save valid coordinates const coordinates = { lat, lng }; GM_setValue("ROLOCATE_coordinates", JSON.stringify(coordinates)); // store coordinates in secure storage // Ensure we're in manual mode localStorage.setItem("ROLOCATE_prioritylocation", "manual"); if (prioritySelect) { prioritySelect.value = "manual"; } // Provide feedback saveCoordinatesBtn.textContent = "Saved!"; saveCoordinatesBtn.style.background = "linear-gradient(135deg, #1e8449 0%, #196f3d 100%);"; setTimeout(() => { saveCoordinatesBtn.textContent = "Save Coordinates"; saveCoordinatesBtn.style.background = "background: #4CAF50;"; }, 2000); }); } } /******************************************************* name of function: AddSettingsButton description: adds settings button *******************************************************/ function AddSettingsButton() { const base64Logo = window.Base64Images.logo; const navbarGroup = document.querySelector('.nav.navbar-right.rbx-navbar-icon-group'); if (!navbarGroup || document.getElementById('custom-logo')) return; const li = document.createElement('li'); li.id = 'custom-logo-container'; li.style.position = 'relative'; li.innerHTML = ` Settings `; const logo = li.querySelector('#custom-logo'); const tooltip = li.querySelector('#custom-tooltip'); logo.addEventListener('click', () => openSettingsMenu()); logo.addEventListener('mouseover', () => { logo.style.width = '30px'; logo.style.border = '2px solid white'; tooltip.style.visibility = 'visible'; tooltip.style.opacity = '1'; }); logo.addEventListener('mouseout', () => { logo.style.width = '26px'; logo.style.border = 'none'; tooltip.style.visibility = 'hidden'; tooltip.style.opacity = '0'; }); navbarGroup.appendChild(li); } /******************************************************* name of function: removeAds description: remove roblox ads. This one is for checking if settings are turned on. *******************************************************/ function removeAds() { if (localStorage.getItem("ROLOCATE_removeads") !== "true") { return; } const iframeSelector = `.ads-container iframe,.abp iframe,.abp-spacer iframe,.abp-container iframe,.top-abp-container iframe, #AdvertisingLeaderboard iframe,#AdvertisementRight iframe,#MessagesAdSkyscraper iframe,.Ads_WideSkyscraper iframe, .profile-ads-container iframe, #ad iframe, iframe[src*="roblox.com/user-sponsorship/"]`; const iframes = document.getElementsByTagName("iframe"); const scripts = document.getElementsByTagName("script"); const doneMap = new WeakMap(); /******************************************************* name of function: removeElements description: remove the roblox elements where ads are in *******************************************************/ function removeElements() { // Remove Iframes for (let i = iframes.length; i--;) { const iframe = iframes[i]; if (!doneMap.get(iframe) && iframe.matches(iframeSelector)) { iframe.remove(); doneMap.set(iframe, true); } } // Remove Scripts for (let i = scripts.length; i--;) { const script = scripts[i]; if (doneMap.get(script)) { continue; } doneMap.set(script, true); if (script.src && ( script.src.includes("imasdk.googleapis.com") || script.src.includes("googletagmanager.com") || script.src.includes("radar.cedexis.com") || script.src.includes("ns1p.net") )) { script.remove(); } else { const cont = script.textContent; if (!cont.includes("ContentJS") && ( cont.includes("scorecardresearch.com") || cont.includes("cedexis.com") || cont.includes("pingdom.net") || cont.includes("ns1p.net") || cont.includes("Roblox.Hashcash") || cont.includes("Roblox.VideoPreRollDFP") || cont.includes("Roblox.AdsHelper=") || cont.includes("googletag.enableServices()") || cont.includes("gtag('config'") )) { script.remove(); } else if (cont.includes("Roblox.EventStream.Init")) { script.textContent = cont.replace(/"[^"]*"/g, "\"\""); } } } // Hide Sponsored Game Cards (existing method) document.querySelectorAll(".game-card-native-ad").forEach(ad => { const gameCard = ad.closest(".game-card-container"); if (gameCard) { gameCard.style.display = "none"; } }); // Block Sponsored Ads Game Card document.querySelectorAll("div.gamecardcontainer").forEach(container => { if (container.querySelector("div.game-card-native-ad")) { container.style.display = "none"; } }); // Block Sponsored Section On HomePage document.querySelectorAll(".game-sort-carousel-wrapper").forEach(wrapper => { const sponsoredLink = wrapper.querySelector('a[href*="Sponsored"]'); if (sponsoredLink) { wrapper.style.display = "none"; } }); // NEW: Remove elements with class "sdui-feed-item-container" document.querySelectorAll(".sdui-feed-item-container").forEach(node => { node.remove(); }); } // Observe DOM for dynamically added elements new MutationObserver(removeElements).observe(document.body, { childList: true, subtree: true }); removeElements(); // Initial run } function openGameQualitySettings() { // Prevent multiple modals from opening if (document.getElementById('game-settings-modal')) { return; } // Create modal overlay const overlay = document.createElement('div'); overlay.id = 'game-settings-modal'; overlay.setAttribute('role', 'dialog'); overlay.setAttribute('aria-modal', 'true'); overlay.setAttribute('aria-labelledby', 'modal-title'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.6); display: flex; justify-content: center; align-items: center; z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; opacity: 0; transition: opacity 0.2s ease; `; // Create modal container const modal = document.createElement('div'); modal.style.cssText = ` background: #1a1a1a; border-radius: 16px; padding: 32px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); width: 480px; max-width: 90vw; max-height: 90vh; overflow-y: auto; transform: scale(0.95) translateY(20px); transition: all 0.2s ease; color: #ffffff; border: 1px solid #404040; `; // Create form element for better semantics const form = document.createElement('form'); form.setAttribute('novalidate', ''); // Create title const title = document.createElement('h2'); title.id = 'modal-title'; title.textContent = 'Game Quality Settings'; title.style.cssText = ` margin: 0 0 24px 0; font-size: 24px; font-weight: 600; color: #e0e0e0; text-align: center; line-height: 1.3; `; // Create game rating section const ratingSection = document.createElement('div'); ratingSection.style.cssText = ` margin-bottom: 32px; padding: 24px; background: #2a2a2a; border-radius: 10px; border: 1px solid #404040; `; const ratingFieldset = document.createElement('fieldset'); ratingFieldset.style.cssText = ` border: none; padding: 0; margin: 0; `; const ratingLegend = document.createElement('legend'); ratingLegend.textContent = 'Game Rating Threshold'; ratingLegend.style.cssText = ` font-weight: 600; color: #e0e0e0; font-size: 16px; margin-bottom: 16px; padding: 0; `; const ratingContainer = document.createElement('div'); ratingContainer.style.cssText = ` display: flex; align-items: center; gap: 16px; `; const ratingSlider = document.createElement('input'); ratingSlider.type = 'range'; ratingSlider.id = 'game-rating-slider'; ratingSlider.name = 'gameRating'; ratingSlider.min = '1'; ratingSlider.max = '100'; ratingSlider.step = '1'; ratingSlider.value = localStorage.getItem('ROLOCATE_gamerating') || '75'; ratingSlider.setAttribute('aria-label', 'Game rating threshold percentage'); ratingSlider.style.cssText = ` flex: 1; height: 6px; border-radius: 3px; background: #333333; outline: none; cursor: pointer; -webkit-appearance: none; appearance: none; `; // Add slider styling const sliderStyles = document.createElement('style'); sliderStyles.textContent = ` #game-rating-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 20px; height: 20px; border-radius: 50%; background: #166534; cursor: pointer; border: 2px solid #ffffff; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } #game-rating-slider::-moz-range-thumb { width: 20px; height: 20px; border-radius: 50%; background: #166534; cursor: pointer; border: 2px solid #ffffff; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } #game-rating-slider:focus::-webkit-slider-thumb { box-shadow: 0 0 0 3px rgba(22, 101, 52, 0.25); } #game-rating-slider:focus::-moz-range-thumb { box-shadow: 0 0 0 3px rgba(22, 101, 52, 0.25); } `; document.head.appendChild(sliderStyles); const ratingDisplay = document.createElement('div'); ratingDisplay.style.cssText = ` min-width: 60px; text-align: center; font-weight: 600; color: #cccccc; font-size: 16px; `; const ratingValue = document.createElement('span'); ratingValue.id = 'rating-value'; ratingValue.textContent = `${ratingSlider.value}%`; ratingValue.setAttribute('aria-live', 'polite'); const ratingDescription = document.createElement('p'); ratingDescription.style.cssText = ` margin: 12px 0 0 0; font-size: 14px; color: #b0b0b0; line-height: 1.4; `; ratingDescription.textContent = 'Show games with ratings at or above this threshold'; ratingSlider.addEventListener('input', function() { ratingValue.textContent = `${this.value}%`; }); ratingDisplay.appendChild(ratingValue); ratingContainer.appendChild(ratingSlider); ratingContainer.appendChild(ratingDisplay); ratingFieldset.appendChild(ratingLegend); ratingFieldset.appendChild(ratingContainer); ratingFieldset.appendChild(ratingDescription); ratingSection.appendChild(ratingFieldset); // Create player count section const playerSection = document.createElement('div'); playerSection.style.cssText = ` margin-bottom: 32px; padding: 24px; background: #2a2a2a; border-radius: 10px; border: 1px solid #404040; `; const playerFieldset = document.createElement('fieldset'); playerFieldset.style.cssText = ` border: none; padding: 0; margin: 0; `; const playerLegend = document.createElement('legend'); playerLegend.textContent = 'Player Count Range'; playerLegend.style.cssText = ` font-weight: 600; color: #e0e0e0; font-size: 16px; margin-bottom: 16px; padding: 0; `; const inputGrid = document.createElement('div'); inputGrid.style.cssText = ` display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 12px; `; // Minimum player count input const minContainer = document.createElement('div'); const minLabel = document.createElement('label'); minLabel.textContent = 'Minimum Players'; minLabel.setAttribute('for', 'min-players'); minLabel.style.cssText = ` display: block; margin-bottom: 6px; font-weight: 500; color: #e0e0e0; font-size: 14px; `; // Load existing player count settings const existingPlayerCount = localStorage.getItem('ROLOCATE_playercount'); let minPlayerValue = '2500'; let maxPlayerValue = 'unlimited'; if (existingPlayerCount) { try { const playerCountData = JSON.parse(existingPlayerCount); minPlayerValue = playerCountData.min || '2500'; maxPlayerValue = playerCountData.max || 'unlimited'; } catch (e) { // If parsing fails, use defaults ConsoleLogEnabled('Failed to parse player count data, using defaults'); } } const minInput = document.createElement('input'); minInput.type = 'number'; minInput.id = 'min-players'; minInput.name = 'minPlayers'; minInput.min = '0'; minInput.max = '1000000'; minInput.value = minPlayerValue; minInput.setAttribute('aria-describedby', 'player-count-desc'); minInput.style.cssText = ` width: 100%; padding: 12px; background: #333333; border: 2px solid #555555; border-radius: 8px; color: #ffffff; font-size: 14px; transition: border-color 0.15s ease; outline: none; box-sizing: border-box; `; // Maximum player count input const maxContainer = document.createElement('div'); const maxLabel = document.createElement('label'); maxLabel.textContent = 'Maximum Players'; maxLabel.setAttribute('for', 'max-players'); maxLabel.style.cssText = ` display: block; margin-bottom: 6px; font-weight: 500; color: #495057; font-size: 14px; `; const maxInput = document.createElement('input'); maxInput.type = 'text'; maxInput.id = 'max-players'; maxInput.name = 'maxPlayers'; maxInput.value = maxPlayerValue; maxInput.setAttribute('aria-describedby', 'player-count-desc'); maxInput.placeholder = 'Enter number or "unlimited"'; maxInput.style.cssText = ` width: 100%; padding: 12px; background: #333333; border: 2px solid #555555; border-radius: 8px; color: #ffffff; font-size: 14px; transition: border-color 0.15s ease; outline: none; box-sizing: border-box; `; const playerDescription = document.createElement('p'); playerDescription.id = 'player-count-desc'; playerDescription.style.cssText = ` margin: 0; font-size: 14px; color: #b0b0b0; line-height: 1.4; `; playerDescription.textContent = 'Filter games by active player count. Use "unlimited" for no upper limit.'; // Error message container const errorContainer = document.createElement('div'); errorContainer.style.cssText = ` margin-top: 12px; padding: 8px 12px; background: #2a2a2a; color: #ff4757; border: 1px solid #ff6b6b; border-radius: 8px; font-size: 14px; display: none; `; // Input validation and focus effects [minInput, maxInput].forEach(input => { input.addEventListener('focus', function() { this.style.borderColor = '#166534'; this.style.boxShadow = '0 0 0 3px rgba(22, 101, 52, 0.25)'; }); input.addEventListener('blur', function() { this.style.borderColor = '#555555'; this.style.boxShadow = 'none'; validateInputs(); }); input.addEventListener('input', validateInputs); }); function validateInputs() { errorContainer.style.display = 'none'; const minValue = parseInt(minInput.value); const maxValue = maxInput.value.toLowerCase() === 'unlimited' ? Infinity : parseInt(maxInput.value); let isValid = true; let errorMessage = ''; if (isNaN(minValue) || minValue < 0) { isValid = false; errorMessage = 'Minimum player count must be a valid number greater than or equal to 0.'; } else if (maxInput.value.toLowerCase() !== 'unlimited' && (isNaN(maxValue) || maxValue < 0)) { isValid = false; errorMessage = 'Maximum player count must be a valid number or "unlimited".'; } else if (maxValue !== Infinity && minValue > maxValue) { isValid = false; errorMessage = 'Minimum player count cannot be greater than maximum player count.'; } if (!isValid) { errorContainer.textContent = errorMessage; errorContainer.style.display = 'block'; } return isValid; } minContainer.appendChild(minLabel); // lmao this looks so bad minContainer.appendChild(minInput); maxContainer.appendChild(maxLabel); maxContainer.appendChild(maxInput); inputGrid.appendChild(minContainer); inputGrid.appendChild(maxContainer); playerFieldset.appendChild(playerLegend); playerFieldset.appendChild(inputGrid); playerFieldset.appendChild(playerDescription); playerFieldset.appendChild(errorContainer); playerSection.appendChild(playerFieldset); // Create button container const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; justify-content: flex-end; gap: 12px; margin-top: 32px; `; // Create cancel button const cancelButton = document.createElement('button'); cancelButton.type = 'button'; cancelButton.textContent = 'Cancel'; cancelButton.style.cssText = ` padding: 12px 24px; background: #333333; color: #cccccc; border: 2px solid #555555; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.15s ease; outline: none; `; cancelButton.addEventListener('mouseenter', function() { this.style.backgroundColor = '#404040'; this.style.borderColor = '#666666'; }); cancelButton.addEventListener('mouseleave', function() { this.style.backgroundColor = '#333333'; this.style.borderColor = '#555555'; }); cancelButton.addEventListener('focus', function() { this.style.boxShadow = '0 0 0 3px rgba(108, 117, 125, 0.25)'; }); cancelButton.addEventListener('blur', function() { this.style.boxShadow = 'none'; }); // Create save button const saveButton = document.createElement('button'); saveButton.type = 'submit'; saveButton.textContent = 'Save Settings'; saveButton.style.cssText = ` padding: 12px 24px; background: #166534; color: white; border: 2px solid #166534; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.15s ease; outline: none; `; saveButton.addEventListener('mouseenter', function() { this.style.backgroundColor = '#14532d'; this.style.borderColor = '#14532d'; }); saveButton.addEventListener('mouseleave', function() { this.style.backgroundColor = '#166534'; this.style.borderColor = '#166534'; }); saveButton.addEventListener('focus', function() { this.style.boxShadow = '0 0 0 3px rgba(22, 101, 52, 0.25)'; }); saveButton.addEventListener('blur', function() { this.style.boxShadow = 'none'; }); // Form submission handler form.addEventListener('submit', function(e) { e.preventDefault(); if (!validateInputs()) { return; } try { const playerCountData = { min: minInput.value, max: maxInput.value }; localStorage.setItem('ROLOCATE_gamerating', ratingSlider.value); localStorage.setItem('ROLOCATE_playercount', JSON.stringify(playerCountData)); closeModal(); } catch (error) { ConsoleLogEnabled('Failed to save settings:', error); errorContainer.textContent = 'Failed to save settings. Please try again.'; errorContainer.style.display = 'block'; } }); cancelButton.addEventListener('click', closeModal); function closeModal() { modal.style.transform = 'scale(0.95) translateY(20px)'; overlay.style.opacity = '0'; setTimeout(() => { if (document.body.contains(overlay)) { document.body.removeChild(overlay); } if (document.head.contains(sliderStyles)) { document.head.removeChild(sliderStyles); } }, 200); } buttonContainer.appendChild(cancelButton); buttonContainer.appendChild(saveButton); // Assemble form form.appendChild(title); form.appendChild(ratingSection); form.appendChild(playerSection); form.appendChild(buttonContainer); modal.appendChild(form); overlay.appendChild(modal); // Close modal on overlay click overlay.addEventListener('click', function(e) { if (e.target === overlay) { closeModal(); } }); // Keyboard navigation support overlay.addEventListener('keydown', function(e) { if (e.key === 'Escape') { closeModal(); } if (e.key === 'Tab') { const focusableElements = modal.querySelectorAll( 'input, button, [tabindex]:not([tabindex="-1"])' ); const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; if (e.shiftKey && document.activeElement === firstElement) { e.preventDefault(); lastElement.focus(); } else if (!e.shiftKey && document.activeElement === lastElement) { e.preventDefault(); firstElement.focus(); } } }); // Add modal to page and trigger animation document.body.appendChild(overlay); // Trigger entrance animation requestAnimationFrame(() => { overlay.style.opacity = '1'; modal.style.transform = 'scale(1) translateY(0)'; }); // Focus first input for accessibility setTimeout(() => { ratingSlider.focus(); }, 250); } function qualityfilterRobloxGames() { // Prevent multiple observers if (window.robloxGameFilterObserver) { window.robloxGameFilterObserver.disconnect(); } const seenCards = new WeakSet(); function parsePlayerCount(text) { if (!text) return 0; const clean = text.replace(/[,\s]/g, '').toLowerCase(); const multiplier = clean.includes('k') ? 1000 : clean.includes('m') ? 1000000 : 1; const number = parseFloat(clean.replace(/[km]/, '')); return isNaN(number) ? 0 : number * multiplier; } function getFilterSettings() { return { enabled: localStorage.getItem('ROLOCATE_gamequalityfilter') === 'true', rating: parseInt(localStorage.getItem('ROLOCATE_gamerating') || '80'), playerCount: (() => { const data = JSON.parse(localStorage.getItem('ROLOCATE_playercount') || '{"min":"5000","max":"unlimited"}'); return { min: parseInt(data.min), max: data.max === 'unlimited' ? Infinity : parseInt(data.max) }; })() }; } function filterCard(card, settings) { if (seenCards.has(card)) return; seenCards.add(card); let rating = 0; const ratingSelectors = [ '.vote-percentage-label', '[data-testid="game-tile-stats-rating"] .vote-percentage-label', '.game-card-info .vote-percentage-label', '.base-metadata .vote-percentage-label' ]; for (const sel of ratingSelectors) { const el = card.querySelector(sel); if (el) { const match = el.textContent.match(/(\d+)%/); if (match) { rating = parseInt(match[1]); break; } } } let playerCount = 0; let hasPlayerCount = false; const pcEl = card.querySelector('.playing-counts-label'); if (pcEl) { playerCount = parsePlayerCount(pcEl.textContent); hasPlayerCount = true; } const shouldShow = ( rating >= settings.rating && (!hasPlayerCount || (playerCount >= settings.playerCount.min && playerCount <= settings.playerCount.max)) ); card.style.display = shouldShow ? '' : 'none'; } function filterAllCards() { const settings = getFilterSettings(); if (!settings.enabled) return; const cards = document.querySelectorAll(` li.game-card, li[data-testid="wide-game-tile"], .grid-item-container.game-card-container `); cards.forEach(card => filterCard(card, settings)); } // Run filtering every second to pick up new cards and setting changes const intervalId = setInterval(() => { try { filterAllCards(); } catch (err) { ConsoleLogEnabled('[ROLOCATE] Filter error:', err); } }, 1000); // MutationObserver for extra responsiveness on new DOM nodes const observer = new MutationObserver(() => { filterAllCards(); }); observer.observe(document.body, { childList: true, subtree: true }); // Store refs for cleanup window.robloxGameFilterObserver = observer; window.robloxGameFilterInterval = intervalId; } /******************************************************* name of function: showOldRobloxGreeting description: shows old roblox greeting if setting is turned on *******************************************************/ async function showOldRobloxGreeting() { ConsoleLogEnabled("Function showOldRobloxGreeting() started."); // Check if the URL is roblox.com/home or roblox.com/{lang}/home if (!/^https?:\/\/(www\.)?roblox\.com(\/[a-z]{2})?\/home\/?$/i.test(window.location.href)) { ConsoleLogEnabled("Not on roblox.com/home. Exiting function."); return; // stops execution if not on the home page } // Check LocalStorage before proceeding if (localStorage.getItem("ROLOCATE_ShowOldGreeting") !== "true") { ConsoleLogEnabled("ShowOldGreeting is disabled. Exiting function."); return; // stops execution if setting is off } ConsoleLogEnabled("Waiting 500ms before proceeding."); await new Promise(r => setTimeout(r, 500)); /******************************************************* name of function: observeElement description: Finds specific element to place old greeting *******************************************************/ function observeElement(selector) { ConsoleLogEnabled(`Observing element: ${selector}`); return new Promise((resolve) => { const observer = new MutationObserver(() => { const element = document.querySelector(selector); if (element) { ConsoleLogEnabled(`Element found: ${selector}`); observer.disconnect(); resolve(element); } }); observer.observe(document.body, { childList: true, subtree: true }); }); } /******************************************************* name of function: fetchAvatar description: find user avatar from the page *******************************************************/ async function fetchAvatar(selector, fallbackImage) { ConsoleLogEnabled(`Fetching avatar from selector: ${selector}`); for (let attempt = 0; attempt < 3; attempt++) { ConsoleLogEnabled(`Attempt ${attempt + 1} to fetch avatar.`); const imgElement = document.querySelector(selector); if (imgElement && imgElement.src !== fallbackImage) { ConsoleLogEnabled(`Avatar found: ${imgElement.src}`); return imgElement.src; } await new Promise(r => setTimeout(r, 1500)); } ConsoleLogEnabled("Avatar not found, using fallback image."); return fallbackImage; } let homeContainer = await observeElement("#HomeContainer .section:first-child"); ConsoleLogEnabled("Home container located."); let userNameElement = document.querySelector("#navigation.rbx-left-col > ul > li > a .font-header-2"); ConsoleLogEnabled(`User name found: ${userNameElement ? userNameElement.innerText : "Unknown"}`); let user = { name: userNameElement ? `Hello, ${userNameElement.innerText}!` : "Hello, Roblox User!", avatar: await fetchAvatar("#navigation.rbx-left-col > ul > li > a img", window.Base64Images.image_place_holder) }; ConsoleLogEnabled(`Final user details: Name - ${user.name}, Avatar - ${user.avatar}`); let headerContainer = document.createElement("div"); headerContainer.classList.add("new-header"); // Removed fade-in opacity initialization here let profileFrame = document.createElement("div"); profileFrame.classList.add("profile-frame"); let profileImage = document.createElement("img"); profileImage.src = user.avatar; profileImage.classList.add("profile-img"); profileFrame.appendChild(profileImage); let userDetails = document.createElement("div"); userDetails.classList.add("user-details"); let userName = document.createElement("h1"); userName.classList.add("user-name"); userName.textContent = user.name; userDetails.appendChild(userName); headerContainer.appendChild(profileFrame); headerContainer.appendChild(userDetails); ConsoleLogEnabled("Replacing old home container with new header."); homeContainer.replaceWith(headerContainer); let styleTag = document.createElement("style"); styleTag.textContent = ` .new-header { display: flex; align-items: center; margin-bottom: 30px; /* Removed opacity transition */ } .profile-frame { width: 150px; height: 150px; border-radius: 50%; overflow: hidden; border: 3px solid #121215; display: flex; justify-content: center; align-items: center; } .profile-img { width: 100%; height: 100%; object-fit: cover; } .user-details { margin-left: 20px; display: flex; align-items: center; } .user-name { font-size: 1.2em; font-weight: bold; color: white; } `; document.head.appendChild(styleTag); ConsoleLogEnabled("Style tag added."); } /******************************************************* name of function: observeURLChanges description: observes url changes for the old old greeting and quality game filter *******************************************************/ let lastUrl = window.location.href.split("#")[0]; // Store only the base URL function observeURLChanges() { let lastUrl = window.location.href.split("#")[0]; const checkUrl = () => { const currentUrl = window.location.href.split("#")[0]; if (currentUrl !== lastUrl) { ConsoleLogEnabled(`URL changed from ${lastUrl} to ${currentUrl}`); lastUrl = currentUrl; // Safe cleanup for the game filter if (window.robloxGameFilterObserver) window.robloxGameFilterObserver.disconnect(); if (window.robloxGameFilterInterval) clearInterval(window.robloxGameFilterInterval); if (currentUrl.includes("roblox.com/home") || currentUrl.match(/roblox\.com\/[a-z]{2}\/home/)) { // uh just added language support lmao ConsoleLogEnabled("Detected return to home page."); quicklaunchgamesfunction(); showOldRobloxGreeting(); } } }; // Intercept pushState and replaceState const interceptHistoryMethod = (method) => { const original = history[method]; history[method] = function(...args) { original.apply(this, args); checkUrl(); }; }; interceptHistoryMethod('pushState'); interceptHistoryMethod('replaceState'); window.addEventListener('popstate', checkUrl); // For back/forward navigation } /******************************************************* name of function: quicknavbutton description: Adds the quick nav buttons to the side panel if it is turned on *******************************************************/ function quicknavbutton() { if (localStorage.getItem('ROLOCATE_quicknav') === 'true') { const settingsRaw = localStorage.getItem('ROLOCATE_quicknav_settings'); if (!settingsRaw) return; let settings; try { settings = JSON.parse(settingsRaw); } catch (e) { ConsoleLogEnabled('Failed to parse ROLOCATE_quicknav_settings:', e); return; } const sidebar = document.querySelector('.left-col-list'); if (!sidebar) return; const premiumButton = sidebar.querySelector('.rbx-upgrade-now'); const style = document.createElement('style'); style.textContent = ` .rolocate-icon-custom { display: inline-block; width: 24px; height: 24px; margin-left: 3px; background-image: url("${window.Base64Images.quicknav}"); background-size: contain; background-repeat: no-repeat; transition: filter 0.2s ease; } `; document.head.appendChild(style); settings.forEach(({ name, link }) => { const li = document.createElement('li'); const a = document.createElement('a'); a.className = 'dynamic-overflow-container text-nav'; a.href = link; a.target = '_self'; const divIcon = document.createElement('div'); const spanIcon = document.createElement('span'); spanIcon.className = 'rolocate-icon-custom'; divIcon.appendChild(spanIcon); const spanText = document.createElement('span'); spanText.className = 'font-header-2 dynamic-ellipsis-item'; spanText.title = name; spanText.textContent = name; a.appendChild(divIcon); a.appendChild(spanText); li.appendChild(a); if (premiumButton && premiumButton.parentElement === sidebar) { sidebar.insertBefore(li, premiumButton); } else { sidebar.appendChild(li); } }); } } /******************************************************* name of function: validateManualMode description: Check if user set their location manually or if it is still in automatic. Some error handling also *******************************************************/ function validateManualMode() { // Check if in manual mode if (localStorage.getItem("ROLOCATE_prioritylocation") === "manual") { ConsoleLogEnabled("Manual mode detected"); try { // Get stored coordinates const coords = JSON.parse(GM_getValue("ROLOCATE_coordinates", '{"lat":"","lng":""}')); ConsoleLogEnabled("Coordinates fetched:", coords); // If coordinates are empty, switch to automatic if (!coords.lat || !coords.lng) { localStorage.setItem("ROLOCATE_prioritylocation", "automatic"); ConsoleLogEnabled("No coordinates set. Switched to automatic mode."); return true; // Indicates that a switch occurred } } catch (e) { ConsoleLogEnabled("Error checking coordinates:", e); // If there's an error reading coordinates, switch to automatic localStorage.setItem("ROLOCATE_prioritylocation", "automatic"); ConsoleLogEnabled("Error encountered while fetching coordinates. Switched to automatic mode."); return true; } } ConsoleLogEnabled("No Errors detected."); return false; // No switch occurred } /******************************************************* name of function: loadBase64Library description: Loads base64 images *******************************************************/ function loadBase64Library(callback, timeout = 5000) { let elapsed = 0; (function waitForLibrary() { if (typeof window.Base64Images === "undefined") { if (elapsed < timeout) { elapsed += 50; setTimeout(waitForLibrary, 50); } else { ConsoleLogEnabled("Base64Images did not load within the timeout."); notifications('An error occurred! No icons will show. Please refresh the page.', 'error', 'โš ๏ธ', '8000'); } } else { if (callback) callback(); } })(); } /******************************************************* name of function: loadmutualfriends description: shows mutual friends. a huge function so its harder to copy. *******************************************************/ async function loadmutualfriends() { // check if mutualfriends is enabled in localStorage and double check if url is the correct one. if (localStorage.getItem("ROLOCATE_mutualfriends") !== "true" || !/^\/(?:[a-z]{2}\/)?users\/\d+\/profile$/.test(window.location.pathname)) return; // Local cache for storing avatar data per page visit let localAvatarCache = {}; // Helper function to get current user ID const getCurrentUserId = () => Roblox?.CurrentUser?.userId || null; // Helper function to fetch friends via GM_xmlhttpRequest const gmFetchFriends = (userId) => { const url = `https://friends.roblox.com/v1/users/${userId}/friends`; return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url, onload: function(response) { if (response.status >= 200 && response.status < 300) { try { const data = JSON.parse(response.responseText); resolve(data.data); } catch (e) { ConsoleLogEnabled(`[gmFetchFriends] Failed to parse response for user ${userId}`, e); resolve(null); } } else { ConsoleLogEnabled(`[gmFetchFriends] Request failed for user ${userId} with status ${response.status}`); resolve(null); } }, onerror: function(err) { ConsoleLogEnabled(`[gmFetchFriends] Network error for user ${userId}`, err); resolve(null); } }); }); }; // helper function to fetch user avatars const fetchUserAvatars = (userIds) => { return new Promise((resolve) => { const requests = userIds.map(userId => ({ requestId: userId.toString(), targetId: userId, type: "AvatarHeadShot", size: "48x48", format: "Png", isCircular: false })); GM_xmlhttpRequest({ method: "POST", url: "https://thumbnails.roblox.com/v1/batch", headers: { "Content-Type": "application/json" }, data: JSON.stringify(requests), onload: function(response) { if (response.status >= 200 && response.status < 300) { try { const data = JSON.parse(response.responseText); const avatarMap = {}; data.data.forEach(item => { if (item.state === "Completed" && item.imageUrl) { avatarMap[item.targetId] = item.imageUrl; } }); resolve(avatarMap); } catch (e) { ConsoleLogEnabled("[fetchUserAvatars] Failed to parse response", e); resolve({}); } } else { ConsoleLogEnabled(`[fetchUserAvatars] Request failed with status ${response.status}`); resolve({}); } }, onerror: function(err) { ConsoleLogEnabled("[fetchUserAvatars] Network error", err); resolve({}); } }); }); }; // function to fetch and cache all avatars at once const fetchAndCacheAllAvatars = async (mutualFriends) => { if (Object.keys(localAvatarCache).length > 0) { ConsoleLogEnabled('[fetchAndCacheAllAvatars] Using cached avatars'); return localAvatarCache; } ConsoleLogEnabled('[fetchAndCacheAllAvatars] Fetching avatars for the first time'); const avatarPromises = []; for (let i = 0; i < mutualFriends.length; i += 5) { const batch = mutualFriends.slice(i, i + 5); const userIds = batch.map(friend => friend.id); avatarPromises.push(fetchUserAvatars(userIds)); } const avatarResults = await Promise.all(avatarPromises); localAvatarCache = Object.assign({}, ...avatarResults); ConsoleLogEnabled(`[fetchAndCacheAllAvatars] Cached ${Object.keys(localAvatarCache).length} avatars`); return localAvatarCache; }; // function to create the mutual friends element with all styles const createMutualFriendsElement = () => { if (!document.querySelector('#mutual-friends-styles')) { // css stuff const style = document.createElement('style'); style.id = 'mutual-friends-styles'; style.textContent = ` .mutual-friends-container { background: linear-gradient(135deg, #111114 0%, #1a1a1d 100%); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; padding: 20px; margin: 20px 0; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); transition: all 0.2s ease; position: relative; overflow: hidden; animation: slideInUp 0.3s ease-out; } .mutual-friends-container:hover { background: linear-gradient(135deg, #1a1a1d 0%, #222226 100%); box-shadow: 0 12px 48px rgba(0, 0, 0, 0.4); transform: translateY(-1px); border-color: rgba(255, 255, 255, 0.2); } @keyframes slideInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .mutual-friends-header { display: flex; align-items: center; margin-bottom: 16px; color: #ffffff; font-size: 18px; font-weight: 700; font-family: "Source Sans Pro", Arial, sans-serif; position: relative; z-index: 1; } .mutual-friends-icon { width: 24px; height: 24px; margin-right: 12px; fill: url(#iconGradient); flex-shrink: 0; } .mutual-friends-count { background: linear-gradient(45deg, #4a90e2, #357abd); color: white; padding: 8px 14px; border-radius: 20px; font-size: 14px; font-weight: 800; margin-left: 12px; box-shadow: 0 4px 15px rgba(74, 144, 226, 0.3); animation: bounceIn 0.3s ease-out; min-width: 40px; text-align: center; border: 2px solid rgba(255, 255, 255, 0.2); } @keyframes bounceIn { 0% { transform: scale(0.5); opacity: 0; } 60% { transform: scale(1.05); } 100% { transform: scale(1); opacity: 1; } } .mutual-friends-list { display: flex; flex-wrap: wrap; gap: 12px; } .mutual-friend-tag { background: rgba(255, 255, 255, 0.08); color: #ffffff; padding: 8px 16px; border-radius: 25px; font-size: 14px; font-weight: 600; border: 1px solid rgba(255, 255, 255, 0.15); transition: all 0.15s ease; cursor: pointer; font-family: "Source Sans Pro", Arial, sans-serif; white-space: nowrap; position: relative; overflow: hidden; animation: fadeInScale 0.2s ease-out backwards; } .mutual-friend-tag:nth-child(1) { animation-delay: 0.05s; } .mutual-friend-tag:nth-child(2) { animation-delay: 0.1s; } .mutual-friend-tag:nth-child(3) { animation-delay: 0.15s; } .mutual-friend-tag:nth-child(4) { animation-delay: 0.2s; } .mutual-friend-tag:nth-child(5) { animation-delay: 0.25s; } .mutual-friend-tag:nth-child(6) { animation-delay: 0.3s; } @keyframes fadeInScale { from { opacity: 0; transform: scale(0.9) translateY(10px); } to { opacity: 1; transform: scale(1) translateY(0); } } .mutual-friend-tag:hover { background: linear-gradient( 45deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.12) ); border-color: rgba(255, 255, 255, 0.3); transform: translateY(-2px) scale(1.02); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); } .mutual-friends-more { background: linear-gradient(45deg, #ff6b35, #f7931e) !important; border-color: rgba(255, 255, 255, 0.3) !important; color: white !important; font-weight: 700 !important; box-shadow: 0 4px 15px rgba(255, 107, 53, 0.4) !important; } .mutual-friends-more:hover { background: linear-gradient(45deg, #ff5722, #e68900) !important; border-color: rgba(255, 255, 255, 0.5) !important; box-shadow: 0 6px 20px rgba(255, 107, 53, 0.6) !important; } .mutual-friends-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.3); display: flex; align-items: center; justify-content: center; z-index: 10000; backdrop-filter: blur(0.3px); animation: fadeIn 0.2s ease-out; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } .mutual-friends-popup { background: linear-gradient(135deg, #111114 0%, #1a1a1d 100%); border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 16px; width: 90%; max-width: 700px; max-height: 80vh; overflow: hidden; box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); animation: popupSlideIn 0.2s ease-out; } @keyframes popupSlideIn { from { opacity: 0; transform: scale(0.95) translateY(20px); } to { opacity: 1; transform: scale(1) translateY(0); } } .mutual-friends-popup-header { display: flex; justify-content: space-between; align-items: center; padding: 24px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); background: linear-gradient(90deg, rgba(255, 255, 255, 0.05), transparent); } .mutual-friends-popup-header h3 { color: #ffffff; margin: 0; font-family: "Source Sans Pro", Arial, sans-serif; font-size: 20px; font-weight: 700; } .mutual-friends-close { background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); color: #ffffff; font-size: 20px; cursor: pointer; padding: 8px; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: all 0.15s ease; } .mutual-friends-close:hover { background: rgba(255, 59, 59, 0.2); border-color: rgba(255, 59, 59, 0.4); transform: rotate(90deg); } .mutual-friends-popup-grid { padding: 24px; max-height: 60vh; overflow-y: auto; display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 16px; } .mutual-friends-popup-item { display: flex; align-items: center; padding: 16px; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; cursor: pointer; transition: all 0.15s ease; animation: itemSlideIn 0.2s ease-out backwards; } .mutual-friends-popup-item:nth-child(odd) { animation-delay: 0.05s; } .mutual-friends-popup-item:nth-child(even) { animation-delay: 0.1s; } @keyframes itemSlideIn { from { opacity: 0; transform: translateX(-20px); } to { opacity: 1; transform: translateX(0); } } .mutual-friends-popup-item:hover { background: linear-gradient( 45deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.08) ); border-color: rgba(255, 255, 255, 0.25); transform: translateY(-2px) scale(1.01); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3); } .mutual-friend-avatar { width: 48px; height: 48px; background: linear-gradient( 45deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.08) ); border: 2px solid rgba(255, 255, 255, 0.15); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-right: 16px; font-size: 20px; flex-shrink: 0; overflow: hidden; transition: all 0.15s ease; } .mutual-friend-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; } .mutual-friends-popup-item:hover .mutual-friend-avatar { transform: scale(1.05); border-color: rgba(255, 255, 255, 0.3); } .mutual-friend-name { color: #ffffff; font-family: "Source Sans Pro", Arial, sans-serif; font-size: 16px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .mutual-friends-loading { display: flex; align-items: center; color: rgba(255, 255, 255, 0.8); font-size: 16px; font-family: "Source Sans Pro", Arial, sans-serif; font-weight: 500; } .loading-spinner { width: 20px; height: 20px; border: 3px solid rgba(255, 255, 255, 0.2); border-top: 3px solid #ffffff; border-radius: 50%; animation: spin 0.8s linear infinite; margin-right: 12px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .no-mutual-friends { color: rgba(255, 255, 255, 0.6); font-style: italic; font-size: 16px; font-family: "Source Sans Pro", Arial, sans-serif; text-align: center; padding: 20px; } .mutual-friends-popup-grid::-webkit-scrollbar { width: 8px; } .mutual-friends-popup-grid::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.1); border-radius: 4px; } .mutual-friends-popup-grid::-webkit-scrollbar-thumb { background: linear-gradient(45deg, #555555, #666666); border-radius: 4px; } .mutual-friends-popup-grid::-webkit-scrollbar-thumb:hover { background: linear-gradient(45deg, #666666, #777777); } @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } `; document.head.appendChild(style); const svgDefs = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svgDefs.style.width = '0'; svgDefs.style.height = '0'; svgDefs.style.position = 'absolute'; svgDefs.innerHTML = ``; document.body.appendChild(svgDefs); } const container = document.createElement('div'); container.className = 'mutual-friends-container'; container.style.display = 'none'; const header = document.createElement('div'); header.className = 'mutual-friends-header'; header.innerHTML = `Mutual Friends`; const content = document.createElement('div'); content.className = 'mutual-friends-content'; container.appendChild(header); container.appendChild(content); return container; }; // Function to show loading state const showMutualFriendsLoading = (contentElement) => { contentElement.innerHTML = `
    Finding mutual friends...
    `; }; // Function to create mutual friends popup const createMutualFriendsPopup = async (mutualFriends) => { const overlay = document.createElement('div'); overlay.className = 'mutual-friends-overlay'; const popup = document.createElement('div'); popup.className = 'mutual-friends-popup'; const header = document.createElement('div'); header.className = 'mutual-friends-popup-header'; header.innerHTML = `

    All Mutual Friends (${mutualFriends.length})

    `; const grid = document.createElement('div'); grid.className = 'mutual-friends-popup-grid'; const avatarMap = localAvatarCache; mutualFriends.forEach(friend => { const friendItem = document.createElement('div'); friendItem.className = 'mutual-friends-popup-item'; const avatarUrl = avatarMap[friend.id]; const avatarContent = avatarUrl ? `${friend.name}` : '๐Ÿ‘ค'; friendItem.innerHTML = `
    ${avatarContent}
    ${friend.name}`; friendItem.onclick = () => { window.open(`https://www.roblox.com/users/${friend.id}/profile`, '_blank'); }; grid.appendChild(friendItem); }); popup.appendChild(header); popup.appendChild(grid); overlay.appendChild(popup); overlay.onclick = (e) => { if (e.target === overlay) { overlay.style.animation = 'fadeOut 0.2s ease-out forwards'; setTimeout(() => overlay.remove(), 200); } }; header.querySelector('.mutual-friends-close').onclick = () => { overlay.style.animation = 'fadeOut 0.2s ease-out forwards'; setTimeout(() => overlay.remove(), 200); }; return overlay; }; // Function to display mutual friends const displayMutualFriends = async (contentElement, mutualFriends) => { contentElement.innerHTML = ''; if (mutualFriends.length === 0) { contentElement.innerHTML = '
    No mutual friends found. RoLocate by Oqarshi
    '; return; } const header = contentElement.parentElement.querySelector('.mutual-friends-header'); const countBadge = document.createElement('span'); countBadge.className = 'mutual-friends-count'; countBadge.textContent = mutualFriends.length; header.appendChild(countBadge); const friendsList = document.createElement('div'); friendsList.className = 'mutual-friends-list'; const maxVisible = 6; const friendsToShow = mutualFriends.slice(0, maxVisible); friendsToShow.forEach(friend => { const friendTag = document.createElement('div'); friendTag.className = 'mutual-friend-tag'; friendTag.textContent = friend.name; friendTag.onclick = () => { window.open(`https://www.roblox.com/users/${friend.id}/profile`, '_blank'); }; friendsList.appendChild(friendTag); }); if (mutualFriends.length > maxVisible) { const moreButton = document.createElement('div'); moreButton.className = 'mutual-friend-tag mutual-friends-more'; moreButton.textContent = `+${mutualFriends.length - maxVisible} more`; moreButton.onclick = async () => { const popup = await createMutualFriendsPopup(mutualFriends); document.body.appendChild(popup); }; friendsList.appendChild(moreButton); } contentElement.appendChild(friendsList); }; // Function to find profile insertion point const findProfileInsertionPoint = () => { const profileHeader = document.querySelector('.profile-header-main'); if (profileHeader) return profileHeader.parentElement; return document.querySelector('[class*="profile"]'); }; // Main execution logic try { const currentUserId = getCurrentUserId(); if (!currentUserId) return; const urlMatch = window.location.pathname.match(/^\/(?:[a-z]{2}\/)?users\/(\d+)\/profile$/); // check if path name is right. if not then return if (!urlMatch) return; const otherUserId = urlMatch[1]; if (otherUserId === String(currentUserId)) return; // Clear local cache for new page visit localAvatarCache = {}; const mutualFriendsElement = createMutualFriendsElement(); const insertionPoint = findProfileInsertionPoint(); if (!insertionPoint) { ConsoleLogEnabled('[Mutual Friends] Could not find suitable insertion point'); return; } insertionPoint.appendChild(mutualFriendsElement); mutualFriendsElement.style.display = 'block'; const contentElement = mutualFriendsElement.querySelector('.mutual-friends-content'); showMutualFriendsLoading(contentElement); const [currentUserFriends, otherUserFriends] = await Promise.all([ gmFetchFriends(currentUserId), gmFetchFriends(otherUserId), ]); if (!currentUserFriends || !otherUserFriends) { contentElement.innerHTML = '
    Failed to load friend data
    '; return; } const mutualFriends = currentUserFriends.filter(currentFriend => otherUserFriends.some(otherFriend => otherFriend.id === currentFriend.id) ); await fetchAndCacheAllAvatars(mutualFriends); await displayMutualFriends(contentElement, mutualFriends); } catch (error) { ConsoleLogEnabled('[executeMutualFriendsFeature] Error occurred:', error); } } /******************************************************* name of function: manageRobloxChatBar description: Disables roblox chat *******************************************************/ function manageRobloxChatBar() { if (localStorage.ROLOCATE_disablechat !== "true") { return; } const CHAT_ID = 'chat-container'; let observer = null; function removeChatBar() { const chat = document.getElementById(CHAT_ID); if (chat) { chat.remove(); ConsoleLogEnabled('Roblox chat bar removed.'); // Disconnect observer to prevent memory leaks if (observer) { observer.disconnect(); ConsoleLogEnabled('Observer disconnected.'); } return true; } return false; } // Try removing immediately, then once after a delay if (!removeChatBar()) { setTimeout(removeChatBar, 1000); } observer = 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}`)) ) { removeChatBar(); } } } }); if (document.body) { observer.observe(document.body, { childList: true, subtree: true, }); } } /******************************************************* name of function: SmartSearch description: Smart Search function with play button *******************************************************/ // WARNING: Do not republish this script. Licensed for personal use only. function SmartSearch() { if (localStorage.ROLOCATE_smartsearch !== "true") { return; } // helper function to chunk arrays for batch processing function chunkArray(array, size) { const chunks = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } // yea i dont even know hoiw this works but it works. thx google function levenshteinDistance(a, b) { const matrix = Array(b.length + 1).fill().map(() => Array(a.length + 1).fill(0)); for (let i = 0; i <= a.length; i++) matrix[0][i] = i; for (let j = 0; j <= b.length; j++) matrix[j][0] = j; for (let j = 1; j <= b.length; j++) { for (let i = 1; i <= a.length; i++) { const indicator = a[i - 1] === b[j - 1] ? 0 : 1; matrix[j][i] = Math.min( matrix[j][i - 1] + 1, matrix[j - 1][i] + 1, matrix[j - 1][i - 1] + indicator ); } } return matrix[b.length][a.length]; } function getSimilarityScore(str1, str2) { ConsoleLogEnabled("Original strings:", { str1, str2 }); // Remove emojis using a general emoji regex and clean the string const removeEmojisAndClean = (str) => str .replace(/[\u{1F300}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu, '') .toLowerCase() .replace(/[^a-z0-9]/g, ''); const cleanStr1 = removeEmojisAndClean(str1); const cleanStr2 = removeEmojisAndClean(str2); ConsoleLogEnabled("Cleaned strings:", { cleanStr1, cleanStr2 }); if (cleanStr1.includes(cleanStr2) || cleanStr2.includes(cleanStr1)) { ConsoleLogEnabled("One string includes the other."); const longer = cleanStr1.length > cleanStr2.length ? cleanStr1 : cleanStr2; const shorter = cleanStr1.length > cleanStr2.length ? cleanStr2 : cleanStr1; ConsoleLogEnabled("Longer string:", longer); ConsoleLogEnabled("Shorter string:", shorter); let baseScore = 0.8 + (shorter.length / longer.length) * 0.15; ConsoleLogEnabled("Base score (inclusion case):", baseScore); if (cleanStr1 === cleanStr2) { ConsoleLogEnabled("Exact match."); return 1.0; } const result = Math.min(0.95, baseScore); ConsoleLogEnabled("Inclusion final score:", result); return result; } const maxLength = Math.max(cleanStr1.length, cleanStr2.length); if (maxLength === 0) { ConsoleLogEnabled("Both strings are empty after cleaning. Returning 1."); return 1; } const distance = levenshteinDistance(cleanStr1, cleanStr2); const levenshteinScore = 1 - (distance / maxLength); ConsoleLogEnabled("Levenshtein distance:", distance); ConsoleLogEnabled("Levenshtein score:", levenshteinScore); const minLength = Math.min(cleanStr1.length, cleanStr2.length); let substringBoost = 0; let longestMatch = 0; for (let i = 0; i < cleanStr1.length; i++) { for (let j = 0; j < cleanStr2.length; j++) { let k = 0; while ( i + k < cleanStr1.length && j + k < cleanStr2.length && cleanStr1[i + k] === cleanStr2[j + k] ) { k++; } if (k > longestMatch) { longestMatch = k; } } } ConsoleLogEnabled("Longest matching substring length:", longestMatch); if (longestMatch >= 3) { substringBoost = (longestMatch / minLength) * 0.5; ConsoleLogEnabled("Substring boost applied:", substringBoost); } else { ConsoleLogEnabled("No substring boost applied."); } const finalScore = Math.min(0.95, levenshteinScore + substringBoost); ConsoleLogEnabled("Final similarity score:", finalScore); return finalScore; } function formatNumberCount(num) { if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M+'; } else if (num >= 1000) { return (num / 1000).toFixed(1) + 'K+'; } else { return num.toString(); } } function formatDate(dateString) { const date = new Date(dateString); const options = { year: 'numeric', month: 'short', day: 'numeric' }; return date.toLocaleDateString('en-US', options); } /******************************************************* Optimized thumbnail fetching functions *******************************************************/ async function fetchGameIconsBatch(universeIds) { if (!universeIds.length) return []; const apiUrl = `https://thumbnails.roblox.com/v1/games/icons?universeIds=${universeIds.join(',')}&size=512x512&format=Png&isCircular=false`; return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url: apiUrl, headers: { "Accept": "application/json" }, onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); resolve(data.data || []); } catch (error) { resolve([]); } } else { resolve([]); } }, onerror: function() { resolve([]); } }); }); } async function fetchPlayerThumbnailsBatch(userIds) { if (!userIds.length) return []; const params = new URLSearchParams({ userIds: userIds.join(","), size: "150x150", format: "Png", isCircular: "false" }); const url = `https://thumbnails.roblox.com/v1/users/avatar-headshot?${params.toString()}`; return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url: url, headers: { "Accept": "application/json" }, onload: function(response) { try { if (response.status === 200) { const data = JSON.parse(response.responseText); resolve(data.data || []); } else { resolve([]); } } catch (error) { resolve([]); } }, onerror: function() { resolve([]); } }); }); } async function fetchGroupIconsBatch(groupIds) { if (!groupIds.length) return []; const params = new URLSearchParams({ groupIds: groupIds.join(","), size: "150x150", format: "Png", isCircular: "false" }); const url = `https://thumbnails.roblox.com/v1/groups/icons?${params.toString()}`; return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url: url, headers: { "Accept": "application/json" }, onload: function(response) { try { if (response.status === 200) { const data = JSON.parse(response.responseText); resolve(data.data || []); } else { resolve([]); } } catch (error) { resolve([]); } }, onerror: function() { resolve([]); } }); }); } /******************************************************* Search functions with dynamic loading *******************************************************/ async function fetchGameSearchResults(query) { const sessionId = Date.now(); const apiUrl = `https://apis.roblox.com/search-api/omni-search?searchQuery=${encodeURIComponent(query)}&pageToken=&sessionId=${sessionId}&pageType=all`; contentArea.innerHTML = '
    Loading games...
    '; try { const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: apiUrl, headers: { "Accept": "application/json" }, onload: resolve, onerror: reject }); }); if (response.status === 200) { const data = JSON.parse(response.responseText); const searchResults = data.searchResults || []; const allGames = searchResults.map(result => result.contents[0]); const gamesWithSimilarity = allGames.map(game => ({ ...game, similarity: getSimilarityScore(query, game.name) })); const sortedGames = gamesWithSimilarity.sort((a, b) => { const similarityA = a.similarity; const similarityB = b.similarity; if ((similarityA >= 0.80 && similarityB >= 0.80) || Math.abs(similarityA - similarityB) < 0.0001) { return b.playerCount - a.playerCount; } return similarityB - similarityA; }); const games = sortedGames.slice(0, 30); const activeTab = document.querySelector('.ROLOCATE_SMARTSEARCH_dropdown-tab.ROLOCATE_SMARTSEARCH_active')?.textContent; if (activeTab !== "Games") return; if (games.length === 0) { contentArea.innerHTML = '
    No results found
    '; return; } // Render cards with play button contentArea.innerHTML = games.map(game => `

    ${game.name}

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

    `).join(''); // Add event listeners to play buttons setTimeout(() => { document.querySelectorAll('.ROLOCATE_SMARTSEARCH_play-button').forEach(button => { button.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); const placeId = this.getAttribute('data-place-id'); window.location.href = `https://www.roblox.com/games/${placeId}#?ROLOCATE_QUICKJOIN`; }); }); }, 100); // Load thumbnails in batches const universeIds = games.map(game => game.universeId); const thumbnailBatches = chunkArray(universeIds, 10); for (const batch of thumbnailBatches) { try { const thumbnails = await fetchGameIconsBatch(batch); thumbnails.forEach(thumb => { const loadingElement = document.querySelector(`.ROLOCATE_SMARTSEARCH_thumbnail-loading[data-universe-id="${thumb.targetId}"]`); if (loadingElement) { loadingElement.outerHTML = ` ${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
    '; } } 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 users = userGroup?.contents || []; if (users.length === 0) { contentArea.innerHTML = '
    No users found
    '; return; } // Render cards immediately with loading state contentArea.innerHTML = users.map(user => `
    `).join(''); // Load thumbnails in batches 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); } } } else { contentArea.innerHTML = '
    Error loading user results
    '; } } catch (error) { ConsoleLogEnabled('Error in user search:', error); contentArea.innerHTML = '
    Error loading user results
    '; } } async function fetchGroupSearchResults(query) { const apiUrl = `https://groups.roblox.com/v1/groups/search?cursor=&keyword=${encodeURIComponent(query)}&limit=25&prioritizeExactMatch=true&sortOrder=Asc`; contentArea.innerHTML = '
    Loading groups...
    '; try { const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: apiUrl, headers: { "Accept": "application/json" }, onload: resolve, onerror: reject }); }); if (response.status === 200) { const data = JSON.parse(response.responseText); const groups = data.data || []; if (groups.length === 0) { contentArea.innerHTML = '
    No groups found
    '; return; } // Render cards immediately with loading state contentArea.innerHTML = groups.map(group => `

    ${group.name}

    Members: ${formatNumberCount(group.memberCount)}

    Created: ${formatDate(group.created)}

    `).join(''); // Load thumbnails in batches const groupIds = groups.map(group => group.id); const thumbnailBatches = chunkArray(groupIds, 10); for (const batch of thumbnailBatches) { try { const thumbnails = await fetchGroupIconsBatch(batch); thumbnails.forEach(thumb => { const loadingElement = document.querySelector(`.ROLOCATE_SMARTSEARCH_thumbnail-loading[data-group-id="${thumb.targetId}"]`); if (loadingElement) { loadingElement.outerHTML = ` ${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
    '; } } const originalSearchContainer = document.querySelector('[data-testid="navigation-search-input"]'); if (!originalSearchContainer) { ConsoleLogEnabled('Search container not found'); return false; } originalSearchContainer.remove(); const customSearchContainer = document.createElement('div'); customSearchContainer.className = 'navbar-left navbar-search col-xs-5 col-sm-6 col-md-2 col-lg-3 shown'; customSearchContainer.setAttribute('role', 'search'); customSearchContainer.style.marginTop = '4px'; customSearchContainer.style.position = 'relative'; const form = document.createElement('form'); form.name = 'custom-search-form'; form.addEventListener('submit', (e) => { e.preventDefault(); const query = searchInput.value.trim(); if (!query) return; const activeTab = document.querySelector('.ROLOCATE_SMARTSEARCH_dropdown-tab.ROLOCATE_SMARTSEARCH_active')?.dataset.tab; let url = ''; switch (activeTab) { case 'games': url = `https://www.roblox.com/discover/?Keyword=${encodeURIComponent(query)}`; break; case 'users': url = `https://www.roblox.com/search/users?keyword=${encodeURIComponent(query)}`; break; case 'groups': url = `https://www.roblox.com/search/communities?keyword=${encodeURIComponent(query)}`; break; default: url = `https://www.roblox.com/discover/?Keyword=${encodeURIComponent(query)}`; break; } window.location.href = url; }); const formWrapper = document.createElement('div'); formWrapper.className = 'ROLOCATE_SMARTSEARCH_form-has-feedback'; const searchInput = document.createElement('input'); let wasPreviouslyBlurred = true; let lastInputValue = ''; searchInput.addEventListener('focus', () => { if (wasPreviouslyBlurred) { const activeTab = document.querySelector('.ROLOCATE_SMARTSEARCH_dropdown-tab.ROLOCATE_SMARTSEARCH_active')?.textContent || 'Unknown'; const typedText = searchInput.value.trim(); ConsoleLogEnabled(`[SmartSearch] Search bar focused | Tab: ${activeTab} | Input: "${typedText}"`); wasPreviouslyBlurred = false; } }); searchInput.addEventListener('blur', () => { wasPreviouslyBlurred = true; }); searchInput.id = 'custom-navbar-search-input'; searchInput.type = 'search'; searchInput.className = 'form-control input-field ROLOCATE_SMARTSEARCH_custom-search-input'; searchInput.placeholder = 'SmartSearch | RoLocate by Oqarshi'; searchInput.maxLength = 120; searchInput.autocomplete = 'off'; const searchIcon = document.createElement('span'); searchIcon.className = 'icon-common-search-sm ROLOCATE_SMARTSEARCH_custom-search-icon'; const dropdownMenu = document.createElement('div'); dropdownMenu.className = 'ROLOCATE_SMARTSEARCH_search-dropdown-menu'; dropdownMenu.style.display = 'none'; const navTabs = document.createElement('div'); navTabs.className = 'ROLOCATE_SMARTSEARCH_dropdown-nav-tabs'; const tabs = ['Games', 'Users', 'Groups']; const tabButtons = []; tabs.forEach((tabName, index) => { const tabButton = document.createElement('button'); tabButton.className = `ROLOCATE_SMARTSEARCH_dropdown-tab ${index === 0 ? 'ROLOCATE_SMARTSEARCH_active' : ''}`; tabButton.textContent = tabName; tabButton.type = 'button'; tabButton.dataset.tab = tabName.toLowerCase(); tabButtons.push(tabButton); navTabs.appendChild(tabButton); }); const contentArea = document.createElement('div'); contentArea.className = 'ROLOCATE_SMARTSEARCH_dropdown-content'; contentArea.innerHTML = '
    Quickly search for games above!
    '; dropdownMenu.appendChild(navTabs); dropdownMenu.appendChild(contentArea); formWrapper.appendChild(searchInput); formWrapper.appendChild(searchIcon); form.appendChild(formWrapper); customSearchContainer.appendChild(form); customSearchContainer.appendChild(dropdownMenu); let isMenuOpen = false; searchInput.addEventListener('click', showDropdownMenu); searchInput.addEventListener('focus', showDropdownMenu); searchInput.addEventListener('input', function() { const currentValue = this.value.trim(); if (currentValue && currentValue !== lastInputValue && !isMenuOpen) { showDropdownMenu(); } lastInputValue = currentValue; }); tabButtons.forEach(button => { button.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); tabButtons.forEach(tab => tab.classList.remove('ROLOCATE_SMARTSEARCH_active')); button.classList.add('ROLOCATE_SMARTSEARCH_active'); const query = searchInput.value.trim(); if (query) { if (button.textContent === "Games") { fetchGameSearchResults(query); } else if (button.textContent === "Users") { fetchUserSearchResults(query); } else if (button.textContent === "Groups") { fetchGroupSearchResults(query); } } else { if (button.textContent === "Games") { contentArea.innerHTML = `
    Quickly search for games above!
    `; } else if (button.textContent === "Users") { contentArea.innerHTML = `
    Instantly find the user you're looking for!
    `; } else if (button.textContent === "Groups") { contentArea.innerHTML = `
    Search for groups rapidly.
    `; } } }); }); document.addEventListener('click', (e) => { if (!customSearchContainer.contains(e.target)) { hideDropdownMenu(); } }); dropdownMenu.addEventListener('click', (e) => { e.stopPropagation(); }); function showDropdownMenu() { isMenuOpen = true; dropdownMenu.style.display = 'block'; formWrapper.classList.add('ROLOCATE_SMARTSEARCH_menu-open'); setTimeout(() => { dropdownMenu.classList.add('ROLOCATE_SMARTSEARCH_show'); }, 10); const activeTab = document.querySelector('.ROLOCATE_SMARTSEARCH_dropdown-tab.ROLOCATE_SMARTSEARCH_active')?.textContent; const query = searchInput.value.trim(); if (query) { if (activeTab === "Games" && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_game-card') === null && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_no-results') === null) { fetchGameSearchResults(query); } else if (activeTab === "Users" && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_user-card') === null && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_no-results') === null) { fetchUserSearchResults(query); } else if (activeTab === "Groups" && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_group-card') === null && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_no-results') === null) { fetchGroupSearchResults(query); } } } function hideDropdownMenu() { isMenuOpen = false; dropdownMenu.classList.remove('ROLOCATE_SMARTSEARCH_show'); formWrapper.classList.remove('ROLOCATE_SMARTSEARCH_menu-open'); setTimeout(() => { if (!isMenuOpen) { dropdownMenu.style.display = 'none'; } }, 200); } const rightNavigation = document.getElementById('right-navigation-header'); if (rightNavigation) { rightNavigation.insertBefore(customSearchContainer, rightNavigation.firstChild); } let debounceTimeout; searchInput.addEventListener('input', () => { if (searchInput.value.trim() && !isMenuOpen) { showDropdownMenu(); } clearTimeout(debounceTimeout); debounceTimeout = setTimeout(() => { const query = searchInput.value.trim(); const activeTab = document.querySelector('.ROLOCATE_SMARTSEARCH_dropdown-tab.ROLOCATE_SMARTSEARCH_active')?.textContent; if (!query) { if (activeTab === "Games") { contentArea.innerHTML = '
    Quickly search for games above!
    '; } else if (activeTab === "Users") { contentArea.innerHTML = '
    Instantly find the user you\'re looking for!
    '; } else if (activeTab === "Groups") { contentArea.innerHTML = '
    Search for groups rapidly.
    '; } return; } if (activeTab === "Games") { fetchGameSearchResults(query); } else if (activeTab === "Users") { fetchUserSearchResults(query); } else if (activeTab === "Groups") { fetchGroupSearchResults(query); } }, 250); }); const style = document.createElement('style'); style.textContent = ` .ROLOCATE_SMARTSEARCH_form-has-feedback { position: relative !important; display: flex !important; align-items: center !important; border: 2px solid #2c2f36 !important; border-radius: 8px !important; background-color: #191a1f !important; transition: all 0.3s ease !important; z-index: 1000 !important; } .ROLOCATE_SMARTSEARCH_form-has-feedback:focus-within, .ROLOCATE_SMARTSEARCH_form-has-feedback.ROLOCATE_SMARTSEARCH_menu-open { border-color: #00b2ff !important; } .ROLOCATE_SMARTSEARCH_form-has-feedback.ROLOCATE_SMARTSEARCH_menu-open { border-bottom-left-radius: 0 !important; border-bottom-right-radius: 0 !important; border-bottom-color: transparent !important; position: relative !important; } .ROLOCATE_SMARTSEARCH_form-has-feedback.ROLOCATE_SMARTSEARCH_menu-open::after { content: '' !important; position: absolute !important; bottom: -12px !important; left: -2px !important; right: -2px !important; height: 12px !important; border-left: 2px solid #00b2ff !important; border-right: 2px solid #00b2ff !important; background-color: transparent !important; z-index: 1000 !important; } .ROLOCATE_SMARTSEARCH_custom-search-input { width: 100% !important; border: none !important; background-color: transparent !important; color: #ffffff !important; padding: 8px 36px 8px 12px !important; font-size: 16px !important; height: 27px !important; border-radius: 8px !important; } .ROLOCATE_SMARTSEARCH_custom-search-input:focus { outline: none !important; box-shadow: none !important; } .ROLOCATE_SMARTSEARCH_custom-search-input::placeholder { color: #8a8d93 !important; opacity: 1 !important; } .ROLOCATE_SMARTSEARCH_custom-search-icon { position: absolute !important; right: 10px !important; top: 50% !important; transform: translateY(-50%) !important; pointer-events: none !important; font-size: 14px !important; color: #8a8d93 !important; } .ROLOCATE_SMARTSEARCH_form-has-feedback:focus-within .ROLOCATE_SMARTSEARCH_custom-search-icon, .ROLOCATE_SMARTSEARCH_form-has-feedback.ROLOCATE_SMARTSEARCH_menu-open .ROLOCATE_SMARTSEARCH_custom-search-icon { color: #00b2ff !important; } .ROLOCATE_SMARTSEARCH_search-dropdown-menu { position: absolute !important; top: calc(100% - 2px) !important; left: 0 !important; width: 100% !important; background-color: #191a1f !important; border-left: 2px solid #00b2ff !important; border-right: 2px solid #00b2ff !important; border-bottom: 2px solid #00b2ff !important; border-top: none !important; border-radius: 0 0 8px 8px !important; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important; z-index: 999 !important; opacity: 0 !important; transform: translateY(-10px) !important; transition: all 0.2s ease !important; box-sizing: border-box !important; } .ROLOCATE_SMARTSEARCH_search-dropdown-menu.ROLOCATE_SMARTSEARCH_show { opacity: 1 !important; transform: translateY(0) !important; } .ROLOCATE_SMARTSEARCH_dropdown-nav-tabs { display: flex !important; background-color: #1e2025 !important; border-bottom: 1px solid #2c2f36 !important; } .ROLOCATE_SMARTSEARCH_dropdown-tab { flex: 1 !important; padding: 12px 16px !important; background: none !important; border: none !important; color: #8a8d93 !important; font-size: 14px !important; font-weight: 500 !important; cursor: pointer !important; transition: all 0.2s ease !important; border-bottom: 2px solid transparent !important; } .ROLOCATE_SMARTSEARCH_dropdown-tab:hover { color: #ffffff !important; background-color: rgba(255, 255, 255, 0.05) !important; } .ROLOCATE_SMARTSEARCH_dropdown-tab.ROLOCATE_SMARTSEARCH_active { color: #00b2ff !important; border-bottom-color: #00b2ff !important; background-color: rgba(0, 178, 255, 0.1) !important; } .ROLOCATE_SMARTSEARCH_dropdown-content { padding: 10px !important; max-height: 350px !important; overflow-y: auto !important; display: block !important; } .ROLOCATE_SMARTSEARCH_content-text { color: #ffffff !important; font-size: 16px !important; text-align: center !important; } .ROLOCATE_SMARTSEARCH_content-text strong { color: #00b2ff !important; } .navbar-left.navbar-search { z-index: 1001 !important; position: relative !important; } /* Game card styles with play button */ .ROLOCATE_SMARTSEARCH_game-card-container { position: relative; margin: 6px 0; } .ROLOCATE_SMARTSEARCH_game-card-link { display: block; text-decoration: none; color: inherit; } .ROLOCATE_SMARTSEARCH_game-card { display: flex; align-items: center; padding: 8px; background-color: #1e2025; border-radius: 8px; transition: background-color 0.2s ease; } .ROLOCATE_SMARTSEARCH_game-card:hover { background-color: #2c2f36; } .ROLOCATE_SMARTSEARCH_thumbnail-loading { width: 50px; height: 50px; border-radius: 4px; margin-right: 10px; background-color: #2c2f36; position: relative; overflow: hidden; } .ROLOCATE_SMARTSEARCH_thumbnail-loading::after { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent); animation: loading 1.5s infinite; } @keyframes loading { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } .ROLOCATE_SMARTSEARCH_game-thumbnail { width: 50px; height: 50px; border-radius: 4px; margin-right: 10px; object-fit: cover; } .ROLOCATE_SMARTSEARCH_game-info { flex: 1; overflow: hidden; padding-right: 40px !important; } .ROLOCATE_SMARTSEARCH_game-name { font-size: 14px; color: #ffffff; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: calc(100% - 40px); } .ROLOCATE_SMARTSEARCH_game-stats { font-size: 12px; color: #8a8d93; margin: 2px 0 0 0; } .ROLOCATE_SMARTSEARCH_thumbs-up { color: #4caf50; } .ROLOCATE_SMARTSEARCH_thumbs-down { color: #f44336; } /* Play button styles - square with rounded edges */ .ROLOCATE_SMARTSEARCH_play-button { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); width: 36px; height: 36px; border-radius: 6px; background: rgba(76, 175, 80, 0.2); border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; z-index: 2; } .ROLOCATE_SMARTSEARCH_play-button:hover { background: rgba(76, 175, 80, 0.3); transform: translateY(-50%) scale(1.05); } .ROLOCATE_SMARTSEARCH_play-button svg { width: 18px; height: 18px; } /* User card styles */ .ROLOCATE_SMARTSEARCH_user-card-link { display: block; text-decoration: none; color: inherit; } .ROLOCATE_SMARTSEARCH_user-card { display: flex; align-items: center; padding: 8px; margin: 6px 0; background-color: #1e2025; border-radius: 8px; transition: background-color 0.2s ease; } .ROLOCATE_SMARTSEARCH_user-card:hover { background-color: #2c2f36; } .ROLOCATE_SMARTSEARCH_user-thumbnail { width: 50px; height: 50px; border-radius: 50%; margin-right: 12px; object-fit: cover; } .ROLOCATE_SMARTSEARCH_user-info { flex: 1; overflow: hidden; } .ROLOCATE_SMARTSEARCH_user-display-name { font-size: 14px; font-weight: 500; color: #ffffff; margin: 0 0 2px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .ROLOCATE_SMARTSEARCH_user-username { font-size: 12px; color: #8a8d93; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* Group card styles */ .ROLOCATE_SMARTSEARCH_group-card-link { display: block; text-decoration: none; color: inherit; } .ROLOCATE_SMARTSEARCH_group-card { display: flex; align-items: center; padding: 8px; margin: 6px 0; background-color: #1e2025; border-radius: 8px; transition: background-color 0.2s ease; } .ROLOCATE_SMARTSEARCH_group-card:hover { background-color: #2c2f36; } .ROLOCATE_SMARTSEARCH_group-thumbnail { width: 50px; height: 50px; border-radius: 4px; margin-right: 12px; object-fit: cover; } .ROLOCATE_SMARTSEARCH_group-info { flex: 1; overflow: hidden; } .ROLOCATE_SMARTSEARCH_group-name { font-size: 14px; font-weight: 500; color: #ffffff; margin: 0 0 4px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .ROLOCATE_SMARTSEARCH_group-members { font-size: 12px; color: #8a8d93; margin: 0 0 2px 0; } .ROLOCATE_SMARTSEARCH_group-created { font-size: 11px; color: #6d717a; margin: 0; } /* Status messages */ .ROLOCATE_SMARTSEARCH_loading, .ROLOCATE_SMARTSEARCH_no-results, .ROLOCATE_SMARTSEARCH_error { text-align: center; color: #8a8d93; padding: 20px; font-size: 14px; } `; document.head.appendChild(style); ConsoleLogEnabled('Enhanced search bar with play buttons added successfully!'); const urlParams = new URLSearchParams(window.location.search); const keywordParam = urlParams.get('keyword') || urlParams.get('Keyword'); if (keywordParam) { searchInput.value = decodeURIComponent(keywordParam); if (window.location.href.includes('/search/users')) { setActiveTab('users'); } else if (window.location.href.includes('/search/communities')) { setActiveTab('groups'); } else { setActiveTab('games'); } } function setActiveTab(tabKey) { tabButtons.forEach(btn => { if (btn.dataset.tab === tabKey) { btn.classList.add('ROLOCATE_SMARTSEARCH_active'); if (btn.textContent === "Games") { contentArea.innerHTML = `
    Quickly search for games above!
    `; } else if (btn.textContent === "Users") { contentArea.innerHTML = `
    Instantly find the user you're looking for!
    `; } else if (btn.textContent === "Groups") { contentArea.innerHTML = `
    Search for groups rapidly.
    `; } } else { btn.classList.remove('ROLOCATE_SMARTSEARCH_active'); } }); } return true; } /******************************************************* name of function: quicklaunchgamesfunction description: adds quick launch *******************************************************/ function quicklaunchgamesfunction() { if (!/^https?:\/\/(www\.)?roblox\.com(\/[a-z]{2})?\/home\/?$/i.test(window.location.href)) return; if (localStorage.getItem('ROLOCATE_quicklaunchgames') === 'true') { const observer = new MutationObserver((mutations, obs) => { const friendsSection = document.querySelector('.friend-carousel-container'); const friendTiles = document.querySelectorAll('.friends-carousel-tile'); if (friendsSection && friendTiles.length > 1) { obs.disconnect(); // Create new games section with premium styling const newGamesContainer = document.createElement('div'); newGamesContainer.className = 'ROLOCATE_QUICKLAUNCHGAMES_new-games-container'; newGamesContainer.innerHTML = `
    Quick Launch Games
    Quickly play your games from here!
    Add Game
    `; // Premium CSS styles const style = document.createElement('style'); style.textContent = ` .ROLOCATE_QUICKLAUNCHGAMES_new-games-container { background: linear-gradient(135deg, #1a1c23, #1e2028); border-radius: 16px; padding: 20px; margin: 24px 0; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); border: 1px solid rgba(255, 255, 255, 0.05); } .container-header.people-list-header { margin-bottom: 18px; } .ROLOCATE_QUICKLAUNCHGAMES_header-content { display: flex; flex-direction: column; gap: 4px; } .ROLOCATE_QUICKLAUNCHGAMES_title { font-size: 22px !important; font-weight: 700 !important; color: #f7f8fa !important; margin: 0 !important; letter-spacing: -0.3px !important; background: linear-gradient(to right, #8a9cff, #5d78ff) !important; -webkit-background-clip: text !important; -webkit-text-fill-color: transparent !important; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; } .ROLOCATE_QUICKLAUNCHGAMES_subtitle { font-size: 12px !important; color: #a0a5b1 !important; font-weight: 500 !important; letter-spacing: 0.2px !important; } .ROLOCATE_QUICKLAUNCHGAMES_game-grid-container { margin-top: 16px; } .ROLOCATE_QUICKLAUNCHGAMES_game-grid { display: flex; gap: 20px; overflow-x: auto; padding-bottom: 12px; scrollbar-width: thin; scrollbar-color: #5d78ff #2d2f36; } .ROLOCATE_QUICKLAUNCHGAMES_game-grid::-webkit-scrollbar { height: 6px; } .ROLOCATE_QUICKLAUNCHGAMES_game-grid::-webkit-scrollbar-track { background: #23252d; border-radius: 3px; } .ROLOCATE_QUICKLAUNCHGAMES_game-grid::-webkit-scrollbar-thumb { background: linear-gradient(to right, #5d78ff, #8a9cff); border-radius: 3px; } .ROLOCATE_QUICKLAUNCHGAMES_game-grid::-webkit-scrollbar-thumb:hover { background: linear-gradient(to right, #6d85ff, #9aabff); } .ROLOCATE_QUICKLAUNCHGAMES_add-tile { flex: 0 0 auto; width: 170px; height: 230px; background: linear-gradient(145deg, #23252d, #1e2028); border-radius: 14px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25); position: relative; overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.05); } .ROLOCATE_QUICKLAUNCHGAMES_add-tile::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(135deg, rgba(93, 120, 255, 0.1), rgba(138, 156, 255, 0.05)); opacity: 0; transition: opacity 0.3s ease; } .ROLOCATE_QUICKLAUNCHGAMES_add-tile:hover { transform: translateY(4px) scale(1.03); box-shadow: 0 14px 28px rgba(0, 0, 0, 0.35); } .ROLOCATE_QUICKLAUNCHGAMES_add-tile:hover::before { opacity: 1; } .ROLOCATE_QUICKLAUNCHGAMES_add-content { text-align: center; color: #8b8d94; z-index: 1; display: flex; flex-direction: column; align-items: center; gap: 12px; } .ROLOCATE_QUICKLAUNCHGAMES_add-icon { width: 32px; height: 32px; stroke-width: 2; color: #5d78ff; transition: all 0.3s ease; } .ROLOCATE_QUICKLAUNCHGAMES_add-tile:hover .ROLOCATE_QUICKLAUNCHGAMES_add-icon { color: #8a9cff; transform: scale(1.2) rotate(90deg); } .ROLOCATE_QUICKLAUNCHGAMES_add-text { font-size: 15px; font-weight: 600; color: #d0d4e0; letter-spacing: 0.3px; } .ROLOCATE_QUICKLAUNCHGAMES_game-tile { flex: 0 0 auto; width: 170px; background: linear-gradient(145deg, #23252d, #1e2028); border-radius: 14px; overflow: hidden; transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.4s ease; cursor: pointer; position: relative; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25); border: 1px solid rgba(255, 255, 255, 0.05); } .ROLOCATE_QUICKLAUNCHGAMES_game-tile:hover { transform: translateY(-7px) scale(1.04); box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4); z-index: 10; } .ROLOCATE_QUICKLAUNCHGAMES_game-tile .thumbnail-container { width: 100%; height: 150px; display: block; position: relative; overflow: hidden; } .ROLOCATE_QUICKLAUNCHGAMES_game-tile img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.6s ease; } .ROLOCATE_QUICKLAUNCHGAMES_game-tile:hover img { transform: scale(1.12); } .ROLOCATE_QUICKLAUNCHGAMES_game-name { padding: 14px 16px; font-size: 14px; font-weight: 600; color: #f0f2f6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: transparent; position: relative; z-index: 1; } .ROLOCATE_QUICKLAUNCHGAMES_game-info { padding: 10px 16px; display: flex; justify-content: space-between; align-items: center; background: rgba(28, 30, 38, 0.85); position: relative; backdrop-filter: blur(6px); border-top: 1px solid rgba(255, 255, 255, 0.05); } .ROLOCATE_QUICKLAUNCHGAMES_game-stat { display: flex; align-items: center; font-size: 12px; color: #b8b9bf; gap: 4px; font-weight: 500; } .ROLOCATE_QUICKLAUNCHGAMES_player-count::before { content: "๐Ÿ‘ค"; margin-right: 4px; filter: drop-shadow(0 1px 1px rgba(0,0,0,0.3)); } .ROLOCATE_QUICKLAUNCHGAMES_like-ratio { display: flex; align-items: center; gap: 4px; } .ROLOCATE_QUICKLAUNCHGAMES_like-ratio .thumb { font-size: 12px; filter: drop-shadow(0 1px 1px rgba(0,0,0,0.3)); } /* Premium X Button Styles */ .ROLOCATE_QUICKLAUNCHGAMES_remove-button { position: absolute; top: 10px; right: 10px; width: 26px; height: 26px; background: rgba(20, 22, 30, 0.85); border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; opacity: 0; transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); z-index: 2; backdrop-filter: blur(4px); border: 1px solid rgba(255,255,255,0.1); box-shadow: 0 4px 10px rgba(0,0,0,0.3); } .ROLOCATE_QUICKLAUNCHGAMES_remove-button::before, .ROLOCATE_QUICKLAUNCHGAMES_remove-button::after { content: ''; position: absolute; width: 14px; height: 2px; background: #f0f2f6; border-radius: 1px; transition: all 0.2s ease; } .ROLOCATE_QUICKLAUNCHGAMES_remove-button::before { transform: rotate(45deg); } .ROLOCATE_QUICKLAUNCHGAMES_remove-button::after { transform: rotate(-45deg); } .ROLOCATE_QUICKLAUNCHGAMES_remove-button:hover { background: rgba(255, 75, 66, 0.95); transform: rotate(90deg) scale(1.1); } .ROLOCATE_QUICKLAUNCHGAMES_remove-button:hover::before, .ROLOCATE_QUICKLAUNCHGAMES_remove-button:hover::after { background: white; } .ROLOCATE_QUICKLAUNCHGAMES_game-tile:hover .ROLOCATE_QUICKLAUNCHGAMES_remove-button { opacity: 1; } /* Animations */ @keyframes fadeIn { to { opacity: 1; } } @keyframes popupIn { to { transform: scale(1); opacity: 1; } } @keyframes popupOut { to { transform: scale(0.9); opacity: 0; } } @keyframes tileAppear { 0% { transform: translateY(10px) scale(0.95); opacity: 0; } 100% { transform: translateY(0) scale(1); opacity: 1; } } @keyframes tileRemove { 0% { transform: translateY(0) scale(1); opacity: 1; } 50% { transform: translateY(-20px) scale(0.9); opacity: 0.5; } 100% { transform: translateY(40px) scale(0.8); opacity: 0; } } @keyframes buttonClick { 0% { transform: scale(1); } 50% { transform: scale(0.95); } 100% { transform: scale(1); } } @keyframes cancelButtonPulse { 0% { background: rgba(60, 64, 78, 0.5); } 50% { background: rgba(100, 104, 118, 0.7); } 100% { background: rgba(60, 64, 78, 0.5); } } @keyframes cancelButtonClick { 0% { transform: scale(1); } 50% { transform: scale(0.95); background: rgba(100, 104, 118, 0.8); } 100% { transform: scale(1); } } .ROLOCATE_QUICKLAUNCHGAMES_game-tile { animation: tileAppear 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; } .ROLOCATE_QUICKLAUNCHGAMES_game-tile.removing { animation: tileRemove 0.4s cubic-bezier(0.55, 0.085, 0.68, 0.53) forwards; pointer-events: none; } /* Popup Styles */ .ROLOCATE_QUICKLAUNCHGAMES_popup-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.3); display: flex; justify-content: center; align-items: center; z-index: 10000; opacity: 0; animation: fadeIn 0.3s ease forwards; } .ROLOCATE_QUICKLAUNCHGAMES_popup { background: linear-gradient(to bottom, #1f2128, #1a1c23); border-radius: 18px; padding: 32px; width: 440px; max-width: 90vw; box-shadow: 0 40px 70px rgba(0, 0, 0, 0.7); border: 1px solid rgba(255, 255, 255, 0.08); transform: scale(0.9); animation: popupIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; position: relative; overflow: hidden; } .ROLOCATE_QUICKLAUNCHGAMES_popup::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: linear-gradient(to right, #5d78ff, #8a9cff); } .ROLOCATE_QUICKLAUNCHGAMES_popup h3 { color: #f7f8fa; font-size: 22px; font-weight: 700; margin: 0 0 24px 0; text-align: center; letter-spacing: -0.3px; } .ROLOCATE_QUICKLAUNCHGAMES_popup label { color: #a0a5b1; font-size: 15px; font-weight: 500; display: block; margin-bottom: 10px; } .ROLOCATE_QUICKLAUNCHGAMES_popup input { width: 100%; padding: 15px; background: rgba(40, 42, 50, 0.6); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; color: #f7f8fa; font-size: 15px; margin-bottom: 28px; outline: none; transition: border-color 0.3s ease, box-shadow 0.3s ease; } .ROLOCATE_QUICKLAUNCHGAMES_popup input::placeholder { color: #6a6e7d; } .ROLOCATE_QUICKLAUNCHGAMES_popup input:focus { border-color: #5d78ff; box-shadow: 0 0 0 4px rgba(93, 120, 255, 0.25); } .ROLOCATE_QUICKLAUNCHGAMES_popup-buttons { display: flex; gap: 16px; justify-content: flex-end; } .ROLOCATE_QUICKLAUNCHGAMES_popup-button { padding: 14px 28px; border: none; border-radius: 12px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); letter-spacing: 0.3px; } .ROLOCATE_QUICKLAUNCHGAMES_popup-button.cancel { background: rgba(60, 64, 78, 0.5); color: #d0d4e0; border: 1px solid rgba(255, 255, 255, 0.1); } .ROLOCATE_QUICKLAUNCHGAMES_popup-button.cancel:hover { background: rgba(80, 84, 98, 0.7); transform: translateY(-3px); box-shadow: 0 6px 12px rgba(0,0,0,0.25); animation: cancelButtonPulse 1.5s infinite; } .ROLOCATE_QUICKLAUNCHGAMES_popup-button.confirm { background: linear-gradient(135deg, #5d78ff, #8a9cff); color: white; box-shadow: 0 6px 16px rgba(93, 120, 255, 0.4); } .ROLOCATE_QUICKLAUNCHGAMES_popup-button.confirm:hover { background: linear-gradient(135deg, #6d85ff, #9aabff); transform: translateY(-3px); box-shadow: 0 8px 20px rgba(93, 120, 255, 0.5); } .ROLOCATE_QUICKLAUNCHGAMES_popup-button:active { transform: translateY(1px); } .ROLOCATE_QUICKLAUNCHGAMES_popup-button.cancel:active { animation: cancelButtonClick 0.3s ease; background: rgba(80, 84, 98, 0.8) !important; } @keyframes popupFadeOut { 0% { transform: scale(1); opacity: 1; } 100% { transform: scale(0.95); opacity: 0; } } .ROLOCATE_QUICKLAUNCHGAMES_popup.fade-out { animation: popupFadeOut 0.3s ease forwards; } .ROLOCATE_QUICKLAUNCHGAMES_add-tile:active { transform: translateY(2px) scale(0.97) !important; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2) !important; } .ROLOCATE_QUICKLAUNCHGAMES_add-tile.clicked { animation: buttonClick 0.3s ease; } `; document.head.appendChild(style); // Insert after friends section friendsSection.parentNode.insertBefore(newGamesContainer, friendsSection.nextSibling); // Add game functions function getUniverseIdFromPlaceId_quicklaunch(placeId) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `https://games.roblox.com/v1/games/multiget-place-details?placeIds=${placeId}`, headers: { "Accept": "application/json" }, onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); if (Array.isArray(data) && data.length > 0 && data[0].universeId) { resolve(data[0].universeId); } else { reject(new Error("Universe ID not found")); } } catch (e) { reject(e); } } else { reject(new Error(`HTTP error: ${response.status}`)); } }, onerror: function(err) { reject(err); } }); }); } function getGameIconFromUniverseId_quicklaunch(universeId) { return new Promise((resolve, reject) => { const apiUrl = `https://thumbnails.roblox.com/v1/games/icons?universeIds=${universeId}&size=512x512&format=Png&isCircular=false`; GM_xmlhttpRequest({ method: "GET", url: apiUrl, headers: { "Accept": "application/json" }, onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); if (data.data && data.data.length > 0 && data.data[0].imageUrl) { resolve(data.data[0].imageUrl); } else { reject(new Error("Image URL not found")); } } catch (err) { reject(err); } } else { reject(new Error(`HTTP error: ${response.status}`)); } }, onerror: function(err) { reject(err); } }); }); } async function getGameDetails(universeId) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `https://games.roblox.com/v1/games?universeIds=${universeId}`, headers: { "Accept": "application/json" }, onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); if (data.data && data.data.length > 0) { resolve(data.data[0]); } else { reject(new Error("Game data not found")); } } catch (e) { reject(e); } } else { reject(new Error(`HTTP error: ${response.status}`)); } }, onerror: function(err) { reject(err); } }); }); } function formatNumber(num) { if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; if (num >= 1000) return (num / 1000).toFixed(1) + 'K'; return num; } // Show add game popup function showAddGamePopup() { const existingGames = document.querySelectorAll('.ROLOCATE_QUICKLAUNCHGAMES_game-tile').length; if (existingGames >= 10) { notifications('Maximum 10 games allowed', 'error', 'โš ๏ธ', '4000'); return; } // Add click animation to add button const addButton = document.getElementById('ROLOCATE_QUICKLAUNCHGAMES_add-button'); addButton.classList.add('clicked'); setTimeout(() => { addButton.classList.remove('clicked'); }, 300); const overlay = document.createElement('div'); overlay.className = 'ROLOCATE_QUICKLAUNCHGAMES_popup-overlay'; overlay.innerHTML = `

    Add New Game

    Example: roblox.com/games/17625359962/RIVALS
    `; document.body.appendChild(overlay); setTimeout(() => { document.getElementById('gameIdInput').focus(); }, 100); // Event listeners const cancelBtn = overlay.querySelector('.cancel'); const confirmBtn = overlay.querySelector('.confirm'); cancelBtn.addEventListener('click', () => { overlay.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_popup').classList.add('fade-out'); setTimeout(() => overlay.remove(), 300); }); confirmBtn.addEventListener('click', async () => { const gameId = document.getElementById('gameIdInput').value.trim(); if (!gameId) { notifications('Please enter a game ID', 'error', 'โš ๏ธ', '4000'); return; } if (!/^\d+$/.test(gameId)) { notifications('Game ID must be numeric', 'error', 'โš ๏ธ', '4000'); return; } const games = JSON.parse(localStorage.getItem('ROLOCATE_quicklaunch_games_storage') || '[]'); if (games.includes(gameId)) { notifications('Game already added!', 'error', 'โš ๏ธ', '4000'); return; } // Show loading state confirmBtn.textContent = 'Adding...'; confirmBtn.disabled = true; try { // Get game details const universeId = await getUniverseIdFromPlaceId_quicklaunch(gameId); const gameDetails = await getGameDetails(universeId); games.push(gameId); localStorage.setItem('ROLOCATE_quicklaunch_games_storage', JSON.stringify(games)); addGameTile(gameId, gameDetails); // Only fade out on success overlay.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_popup').classList.add('fade-out'); setTimeout(() => overlay.remove(), 300); } catch (error) { notifications('Error adding game: ' + error.message, 'error', 'โš ๏ธ', '4000'); confirmBtn.textContent = 'Add Game'; confirmBtn.disabled = false; } // Remove these two lines - they were causing the problem }); } // Add game tile with animations and API data function addGameTile(gameId, gameDetails = null) { const gameGrid = document.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_game-grid'); if (!gameGrid) return; const gameTile = document.createElement('div'); gameTile.className = 'ROLOCATE_QUICKLAUNCHGAMES_game-tile'; gameTile.dataset.gameId = gameId; // Create tile with placeholder content gameTile.innerHTML = `
    Loading...
    ๐Ÿ‘ -
    -
    `; gameGrid.insertBefore(gameTile, gameGrid.firstChild); // Add remove functionality const removeBtn = gameTile.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_remove-button'); removeBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); // Animated removal with bounce effect gameTile.classList.add('removing'); setTimeout(() => { const games = JSON.parse(localStorage.getItem('ROLOCATE_quicklaunch_games_storage') || '[]'); const updatedGames = games.filter(id => id !== gameId); localStorage.setItem('ROLOCATE_quicklaunch_games_storage', JSON.stringify(updatedGames)); gameTile.remove(); }, 400); }); // Load game details asynchronously const loadGameDetails = async () => { try { const universeId = await getUniverseIdFromPlaceId_quicklaunch(gameId); const [iconUrl, details] = await Promise.all([ getGameIconFromUniverseId_quicklaunch(universeId), gameDetails || getGameDetails(universeId) ]); // Update thumbnail const thumbContainer = gameTile.querySelector('.thumbnail-container'); thumbContainer.innerHTML = `${details.name}`; // Update game name const gameName = gameTile.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_game-name'); gameName.textContent = details.name || 'Unknown Game'; // Update stats const playerCount = gameTile.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_player-count'); const likeRatio = gameTile.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_like-ratio'); playerCount.textContent = formatNumber(details.playing); // Calculate like ratio (using favorites as proxy) const ratio = details.favoritedCount > 0 ? Math.round((details.favoritedCount / (details.favoritedCount + (details.favoritedCount * 0.1))) * 100) : 0; likeRatio.innerHTML = `๐Ÿ‘ ${ratio}%`; } catch (error) { console.error('Error loading game details:', error); const playerCount = gameTile.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_player-count'); playerCount.textContent = 'Error'; } }; loadGameDetails(); } // Add event to add button const addButton = document.getElementById('ROLOCATE_QUICKLAUNCHGAMES_add-button'); addButton.addEventListener('click', showAddGamePopup); addButton.addEventListener('mousedown', function() { this.classList.add('active'); }); addButton.addEventListener('mouseup', function() { this.classList.remove('active'); }); addButton.addEventListener('mouseleave', function() { this.classList.remove('active'); }); // Load saved games function loadSavedGames() { const savedGames = JSON.parse(localStorage.getItem('ROLOCATE_quicklaunch_games_storage') || '[]'); savedGames.forEach(gameId => { addGameTile(gameId); }); } // Initial load setTimeout(loadSavedGames, 100); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); if (!document.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_new-games-container')) { quicklaunchgamesfunction(); } }, 5000); } } /******************************************************* name of function: event listener description: Note a function but runs the initial setup for the script to actually start working. Very important *******************************************************/ window.addEventListener("load", () => { const startTime = performance.now(); loadBase64Library(() => { ConsoleLogEnabled("Loaded Base64Images. It is ready to use!"); }); AddSettingsButton(() => { ConsoleLogEnabled("Loaded Settings button!"); }); SmartSearch(); // love this function btw lmao quicklaunchgamesfunction(); manageRobloxChatBar(); loadmutualfriends(); Update_Popup(); initializeLocalStorage(); removeAds(); showOldRobloxGreeting(); quicknavbutton(); validateManualMode(); qualityfilterRobloxGames(); // Start observing URL changes observeURLChanges(); const endTime = performance.now(); const elapsed = Math.round(endTime - startTime); console.log(`%cRoLocate by Oqarshi - loaded in ${elapsed} ms. Personal use only.`, "color: #FFD700; font-size: 18px; font-weight: bold;"); }); /******************************************************* The code for the random hop button and the filter button on roblox.com/games/* *******************************************************/ if (window.location.href.includes("/games/") && (localStorage.getItem("ROLOCATE_togglefilterserversbutton") === "true" || localStorage.getItem("ROLOCATE_toggleserverhopbutton") === "true" || localStorage.getItem("ROLOCATE_togglerecentserverbutton") === "true")) { let Isongamespage = true; if (window.location.href.includes("/games/")) { // saftey check and lazy load data to save the 2mb of ram lmao loadServerRegions(); // lazy loads the server region data to save 2mb of ram lol if (window.serverRegionsByIp) { ConsoleLogEnabled("Server regions data loaded successfully."); } else { ConsoleLogEnabled("Failed to load server regions data."); } getFlagEmoji(); // lazy loads the flag emoji base64 to save some ram i guess InitRobloxLaunchHandler(); // listens for game join and if true shows popup. } /********************************************************************************************************************************************************************************************************************************************* This is all of the functions for the filter button and the popup for the 8 buttons *********************************************************************************************************************************************************************************************************************************************/ //Testing //HandleRecentServersAddGames("126884695634066", "853e79a5-1a2b-4178-94bf-a242de1aecd6"); //HandleRecentServersAddGames("126884695634066", "a08849f1-40e32-4b3215c-31231231a268-e948519caf39"); //HandleRecentServersAddGames("126884695634066", "a08849f1-40e32-4b5c-31236541231a268-e948519caf39"); //HandleRecentServersAddGames("126884695634066", "a08849f1-40e32-4b5c-31231287631a268-e948519caf39"); //HandleRecentServersAddGames("126884695634066", "a08849f1-40e32-4b5c-31231231a268-87e948519caf39"); //HandleRecentServersAddGames("126884695634066", "a08849f1-40e32-4b5c-31231231a268089-e948519caf39"); //document.querySelector('.recent-servers-section')?.remove(); // remove old list //HandleRecentServers(); // re-render with updated order /******************************************************* name of function: InitRobloxLaunchHandler description: Basically detects if the user joins a roblox server and then adds that to recent servers and shows the popup. *******************************************************/ // WARNING: Do not republish this script. Licensed for personal use only. function InitRobloxLaunchHandler() { if (!/^https:\/\/www\.roblox\.com(\/[a-z]{2})?\/games\//.test(window.location.href)) return; if (window._robloxJoinInterceptorInitialized) return; window._robloxJoinInterceptorInitialized = true; const originalJoin = Roblox.GameLauncher.joinGameInstance; Roblox.GameLauncher.joinGameInstance = async function(gameId, serverId) { ConsoleLogEnabled(`Intercepted join: Game ID = ${gameId}, Server ID = ${serverId}`); showLoadingOverlay(gameId, serverId); // pass to overlay if (localStorage.getItem("ROLOCATE_togglerecentserverbutton") === "true") { await HandleRecentServersAddGames(gameId, serverId); document.querySelector('.recent-servers-section')?.remove(); HandleRecentServers(); } else { // ima put something here oneday i guess } // add 1500ms artificial delay before joining await new Promise(resolve => setTimeout(resolve, 1500)); return originalJoin.apply(this, arguments); }; } /******************************************************* name of function: HandleRecentServersAddGames description: Adds recent servers to localstorage for safe keeping *******************************************************/ // WARNING: Do not republish this script. Licensed for personal use only. async function HandleRecentServersAddGames(gameId, serverId) { const storageKey = "ROLOCATE_recentservers_button"; const stored = JSON.parse(localStorage.getItem(storageKey) || "{}"); const key = `${gameId}_${serverId}`; // Check if we already have region data for this server if (!stored[key] || !stored[key].region) { try { // Fetch server region if not already stored const region = await fetchServerDetails(gameId, serverId); stored[key] = { timestamp: Date.now(), region: region }; } catch (error) { ConsoleLogEnabled("Failed to fetch server region:", error); // Store without region data if fetch fails stored[key] = { timestamp: Date.now(), region: null }; } } else { // Update timestamp but keep existing region data stored[key].timestamp = Date.now(); } localStorage.setItem(storageKey, JSON.stringify(stored)); } /******************************************************* name of function: HandleRecentServersURL description: Detects recent servers from the url if user joins server from invite url and cleans up the URL *******************************************************/ // WARNING: Do not republish this script. Licensed for personal use only. function HandleRecentServersURL() { // Static-like variable to remember if we've already found an invalid URL if (HandleRecentServersURL.alreadyInvalid) { return; // Skip if previously marked as invalid } const url = window.location.href; // Regex pattern to match ROLOCATE_GAMEID and SERVERID from the hash const match = url.match(/ROLOCATE_GAMEID=(\d+)_SERVERID=([a-f0-9-]+)/i); if (match && match.length === 3) { const gameId = match[1]; const serverId = match[2]; // Clean up the URL (remove the hash part) while preserving query parameters const cleanURL = window.location.pathname + window.location.search; history.replaceState(null, null, cleanURL); // Call the handler with extracted values HandleRecentServersAddGames(gameId, serverId); } else { ConsoleLogEnabled("No gameId and serverId found in URL."); HandleRecentServersURL.alreadyInvalid = true; // Set internal flag } } /******************************************************* name of function: getFlagEmoji description: Guves Flag Emoji *******************************************************/ function getFlagEmoji(countryCode) { // Static variables to maintain state without globals if (!getFlagEmoji.flagsData) { ConsoleLogEnabled("[getFlagEmoji] Initializing static variables."); getFlagEmoji.flagsData = null; getFlagEmoji.isLoaded = false; } // If no countryCode provided, lazy load all data if (!countryCode) { ConsoleLogEnabled("[getFlagEmoji] No country code provided."); if (!getFlagEmoji.isLoaded) { ConsoleLogEnabled("[getFlagEmoji] Loading flag data (no countryCode)."); getFlagEmoji.flagsData = loadFlagsData(); // This function comes from @require getFlagEmoji.isLoaded = true; ConsoleLogEnabled("[getFlagEmoji] Flag data loaded successfully."); } else { ConsoleLogEnabled("[getFlagEmoji] Flag data already loaded."); } return; } // If data not loaded yet, load it now if (!getFlagEmoji.isLoaded) { ConsoleLogEnabled(`[getFlagEmoji] Lazy loading flag data for country: ${countryCode}`); getFlagEmoji.flagsData = loadFlagsData(); getFlagEmoji.isLoaded = true; ConsoleLogEnabled("[getFlagEmoji] Flag data loaded successfully."); } const src = getFlagEmoji.flagsData[countryCode]; ConsoleLogEnabled(`[getFlagEmoji] Creating flag image for country code: ${countryCode}`); const img = document.createElement('img'); img.src = src; img.alt = countryCode; img.width = 24; img.height = 18; img.style.verticalAlign = 'middle'; img.style.marginRight = '4px'; return img; } /******************************************************* name of function: HandleRecentServers description: Detects if recent servers are in localstorage and then adds them to the page with css styles *******************************************************/ // WARNING: Do not republish this script. Licensed for personal use only. function HandleRecentServers() { const serverList = document.querySelector('.server-list-options'); if (!serverList || document.querySelector('.recent-servers-section')) return; const match = window.location.href.match(/\/games\/(\d+)\//); if (!match) return; const currentGameId = match[1]; const allHeaders = document.querySelectorAll('.server-list-header'); let friendsSectionHeader = null; allHeaders.forEach(header => { if (header.textContent.trim() === 'Servers My Friends Are In') { friendsSectionHeader = header.closest('.container-header'); } }); function formatLastPlayedWithRelative(lastPlayed, mode) { // yea ik a function in a function is abd but idrc 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 is "relativeOnly", just return the relative time if (mode === "relativeOnly") { return relativeTime; } // Otherwise, return full date + relative return `${lastPlayed} (${relativeTime})`; } if (!friendsSectionHeader) return; // Custom premium dark theme CSS variables const theme = { bgDark: '#14161a', bgCard: '#1c1f25', bgCardHover: '#22262e', bgGradient: 'linear-gradient(145deg, #1e2228, #18191e)', bgGradientHover: 'linear-gradient(145deg, #23272f, #1c1f25)', accentPrimary: '#4d85ee', accentSecondary: '#3464c9', accentGradient: 'linear-gradient(to bottom, #4d85ee, #3464c9)', accentGradientHover: 'linear-gradient(to bottom, #5990ff, #3b6fdd)', textPrimary: '#e8ecf3', textSecondary: '#a0a8b8', textMuted: '#6c7484', borderLight: 'rgba(255, 255, 255, 0.06)', borderLightHover: 'rgba(255, 255, 255, 0.12)', shadow: '0 5px 15px rgba(0, 0, 0, 0.25)', shadowHover: '0 8px 25px rgba(0, 0, 0, 0.3)', dangerColor: '#ff5b5b', dangerColorHover: '#ff7575', dangerGradient: 'linear-gradient(to bottom, #ff5b5b, #e04444)', dangerGradientHover: 'linear-gradient(to bottom, #ff7575, #f55)', popupBg: 'rgba(20, 22, 26, 0.95)', popupBorder: 'rgba(77, 133, 238, 0.2)' }; const recentSection = document.createElement('div'); recentSection.className = 'recent-servers-section premium-dark'; recentSection.style.marginBottom = '24px'; const headerContainer = document.createElement('div'); headerContainer.className = 'container-header'; const headerInner = document.createElement('div'); headerInner.className = 'server-list-container-header'; headerInner.style.padding = '0 4px'; const headerTitle = document.createElement('h2'); headerTitle.className = 'server-list-header'; headerTitle.textContent = 'Recent Servers'; headerTitle.style.cssText = ` font-weight: 600; color: ${theme.textPrimary}; letter-spacing: 0.5px; position: relative; display: inline-block; padding-bottom: 4px; `; // Add premium underline accent to header const headerAccent = document.createElement('span'); headerAccent.style.cssText = ` position: absolute; bottom: 0; left: 0; width: 40px; height: 2px; background: ${theme.accentGradient}; border-radius: 2px; `; headerTitle.appendChild(headerAccent); headerInner.appendChild(headerTitle); headerContainer.appendChild(headerInner); const contentContainer = document.createElement('div'); contentContainer.className = 'section-content-off empty-game-instances-container'; contentContainer.style.padding = '8px 4px'; const storageKey = "ROLOCATE_recentservers_button"; let stored = JSON.parse(localStorage.getItem(storageKey) || "{}"); // Auto-remove servers older than 3 days const currentTime = Date.now(); const threeDaysInMs = 3 * 24 * 60 * 60 * 1000; // 3days in milliseconds let storageUpdated = false; Object.keys(stored).forEach(key => { const serverData = stored[key]; // Handle both old format (timestamp only) and new format (object with timestamp and region) const serverTime = typeof serverData === 'object' ? serverData.timestamp : serverData; if (currentTime - serverTime > threeDaysInMs) { delete stored[key]; storageUpdated = true; } }); if (storageUpdated) { localStorage.setItem(storageKey, JSON.stringify(stored)); } const keys = Object.keys(stored).filter(key => key.startsWith(`${currentGameId}_`)); if (keys.length === 0) { const emptyMessage = document.createElement('div'); emptyMessage.className = 'no-servers-message'; emptyMessage.innerHTML = ` No Recent Servers Found`; emptyMessage.style.cssText = ` color: ${theme.textSecondary}; text-align: center; padding: 28px 0; font-size: 14px; letter-spacing: 0.3px; font-weight: 500; display: flex; align-items: center; justify-content: center; background: rgba(20, 22, 26, 0.4); backdrop-filter: blur(5px); border-radius: 12px; border: 1px solid rgba(77, 133, 238, 0.15); box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.2); `; contentContainer.appendChild(emptyMessage); } else { keys.sort((a, b) => { 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; }); // Create server cards wrapper const cardsWrapper = document.createElement('div'); cardsWrapper.style.cssText = ` display: flex; flex-direction: column; gap: 12px; margin: 2px 0; `; keys.forEach((key, index) => { const [gameId, serverId] = key.split("_"); const serverData = stored[key]; // Handle both old format (timestamp only) and new format (object with timestamp and region) 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' }); // Format region display let regionDisplay = ''; let flagElement = null; if (regionData && regionData !== null) { // Region data exists and is not null const city = regionData.city || 'Unknown'; const countryCode = (regionData.country && regionData.country.code) || ''; flagElement = getFlagEmoji(countryCode); } else { // Region is null or doesn't exist flagElement = getFlagEmoji(''); regionDisplay = 'Unknown'; } // Ensure flagElement is a valid DOM node before proceeding if (!flagElement) { // Create a fallback text node if getFlagEmoji returns null/undefined flagElement = document.createTextNode('๐ŸŒ'); regionDisplay = regionDisplay || 'Unknown'; } // Set regionDisplay if it wasn't set above if (!regionDisplay) { if (regionData && regionData !== null && regionData.city) { regionDisplay = regionData.city; } else { regionDisplay = 'Unknown'; } } // Force flag image alignment if it's an img element if (flagElement && flagElement.tagName === 'IMG') { flagElement.style.cssText = ` width: 24px; height: 18px; vertical-align: middle; margin-right: 4px; display: inline-block; `; } 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; `; // Add hover effect serverCard.onmouseover = function() { this.style.boxShadow = theme.shadowHover; this.style.transform = 'translateY(-2px)'; this.style.borderColor = theme.borderLightHover; this.style.background = theme.bgGradientHover; }; serverCard.onmouseout = function() { this.style.boxShadow = theme.shadow; this.style.transform = 'translateY(0)'; this.style.borderColor = theme.borderLight; this.style.background = theme.bgGradient; }; // Add glass effect overlay const glassOverlay = document.createElement('div'); glassOverlay.style.cssText = ` position: absolute; left: 0; top: 0; right: 0; height: 50%; background: linear-gradient(to bottom, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0)); border-radius: 14px 14px 0 0; pointer-events: none; `; serverCard.appendChild(glassOverlay); // Server icon with glow const serverIconWrapper = document.createElement('div'); serverIconWrapper.style.cssText = ` position: absolute; left: 14px; display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; `; const serverIcon = document.createElement('div'); serverIcon.innerHTML = ` `; serverIconWrapper.appendChild(serverIcon); // Add subtle glow to the server icon const iconGlow = document.createElement('div'); iconGlow.style.cssText = ` position: absolute; width: 24px; height: 24px; border-radius: 50%; background: ${theme.accentPrimary}; opacity: 0.15; filter: blur(8px); z-index: -1; `; serverIconWrapper.appendChild(iconGlow); const left = document.createElement('div'); left.style.cssText = ` display: flex; flex-direction: column; justify-content: center; margin-left: 12px; 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; `; // Create the region info element 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; `; // Create the content safely 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('๐ŸŒ')); } // Wrap regionDisplay in a styled span 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; `; // Create the smaller remove button to be positioned on the left const removeButton = document.createElement('button'); removeButton.innerHTML = ` `; removeButton.className = 'btn-control-xs remove-button'; removeButton.style.cssText = ` background: ${theme.dangerGradient}; color: white; border: none; padding: 6px; border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all 0.15s ease; letter-spacing: 0.4px; box-shadow: 0 2px 8px rgba(255, 91, 91, 0.3); display: flex; align-items: center; justify-content: center; width: 30px; height: 30px; `; // Add remove button hover effect removeButton.onmouseover = function() { this.style.background = theme.dangerGradientHover; this.style.boxShadow = '0 4px 10px rgba(255, 91, 91, 0.4)'; this.style.transform = 'translateY(-1px)'; }; removeButton.onmouseout = function() { this.style.background = theme.dangerGradient; this.style.boxShadow = '0 2px 8px rgba(255, 91, 91, 0.3)'; this.style.transform = 'translateY(0)'; }; // Add remove button functionality removeButton.addEventListener('click', function(e) { e.stopPropagation(); const serverKey = this.closest('.recent-server-card').dataset.serverKey; // Animate removal serverCard.style.transition = 'all 0.3s ease-out'; serverCard.style.opacity = '0'; serverCard.style.height = '0'; serverCard.style.margin = '0'; serverCard.style.padding = '0'; setTimeout(() => { serverCard.remove(); // Update localStorage const storedData = JSON.parse(localStorage.getItem(storageKey) || "{}"); delete storedData[serverKey]; localStorage.setItem(storageKey, JSON.stringify(storedData)); // If no servers left, show empty message if (document.querySelectorAll('.recent-server-card').length === 0) { const emptyMessage = document.createElement('div'); emptyMessage.className = 'no-servers-message'; emptyMessage.innerHTML = ` No Recent Servers Found`; emptyMessage.style.cssText = ` color: ${theme.textSecondary}; text-align: center; padding: 28px 0; font-size: 14px; letter-spacing: 0.3px; font-weight: 500; display: flex; align-items: center; justify-content: center; background: rgba(20, 22, 26, 0.4); backdrop-filter: blur(5px); border-radius: 12px; border: 1px solid rgba(77, 133, 238, 0.15); box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.2); `; cardsWrapper.appendChild(emptyMessage); } }, 300); }); // Create a separator element const separator = document.createElement('div'); separator.style.cssText = ` height: 24px; width: 1px; background-color: rgba(255, 255, 255, 0.15); margin: 0 2px; `; const joinButton = document.createElement('button'); joinButton.innerHTML = ` Join `; joinButton.className = 'btn-control-xs join-button'; joinButton.style.cssText = ` background: ${theme.accentGradient}; color: white; border: none; padding: 8px 18px; border-radius: 10px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all 0.15s ease; letter-spacing: 0.4px; box-shadow: 0 2px 10px rgba(52, 100, 201, 0.3); display: flex; align-items: center; justify-content: center; `; // Add join button functionality joinButton.addEventListener('click', function() { try { Roblox.GameLauncher.joinGameInstance(gameId, serverId); //showLoadingOverlay(); } catch (error) { ConsoleLogEnabled("Error joining game:", error); } }); // Add hover effect for join button joinButton.onmouseover = function() { this.style.background = theme.accentGradientHover; this.style.boxShadow = '0 4px 12px rgba(77, 133, 238, 0.4)'; this.style.transform = 'translateY(-1px)'; }; joinButton.onmouseout = function() { this.style.background = theme.accentGradient; this.style.boxShadow = '0 2px 10px rgba(52, 100, 201, 0.3)'; this.style.transform = 'translateY(0)'; }; const inviteButton = document.createElement('button'); inviteButton.innerHTML = ` Invite `; inviteButton.className = 'btn-control-xs invite-button'; inviteButton.style.cssText = ` background: rgba(28, 31, 37, 0.6); color: ${theme.textPrimary}; border: 1px solid rgba(255, 255, 255, 0.12); padding: 8px 18px; border-radius: 10px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.15s ease; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(4px); `; // Add invite button functionality inviteButton.addEventListener('click', function() { const inviteUrl = `https://oqarshi.github.io/Invite/?placeid=${gameId}&serverid=${serverId}`; // Disable the button temporarily inviteButton.disabled = true; // Copy to clipboard navigator.clipboard.writeText(inviteUrl).then( function() { // Show feedback that URL was copied const originalText = inviteButton.innerHTML; inviteButton.innerHTML = ` Copied! `; ConsoleLogEnabled(`Invite link copied to clipboard`); notifications('Success! Invite link copied to clipboard!', 'success', '๐ŸŽ‰', '2000'); // Reset button after 1 second setTimeout(() => { inviteButton.innerHTML = originalText; inviteButton.disabled = false; }, 1000); }, function(err) { ConsoleLogEnabled('Could not copy text: ', err); inviteButton.disabled = false; // re-enable in case of error } ); }); // Add hover effect for invite button inviteButton.onmouseover = function() { this.style.background = 'rgba(35, 39, 46, 0.8)'; this.style.borderColor = 'rgba(255, 255, 255, 0.18)'; this.style.transform = 'translateY(-1px)'; }; inviteButton.onmouseout = function() { this.style.background = 'rgba(28, 31, 37, 0.6)'; this.style.borderColor = 'rgba(255, 255, 255, 0.12)'; this.style.transform = 'translateY(0)'; }; // NEW: More Info Button 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; backdrop-filter: blur(4px); width: 34px; height: 34px; `; // Add hover effect for more info button 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; }; // Add click handler for more info button 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; // Remove any existing popup const existingPopup = document.querySelector('.server-info-popup'); if (existingPopup) existingPopup.remove(); // Create popup container 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.5); opacity: 0; transition: opacity 0.2s ease-out; `; // Create popup content const popupContent = document.createElement('div'); popupContent.style.cssText = ` background: ${theme.popupBg}; border-radius: 16px; width: 400px; 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); `; // Popup header const popupHeader = document.createElement('div'); popupHeader.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 12px; border-bottom: 1px solid rgba(255, 255, 255, 0.08); `; const popupTitle = document.createElement('h3'); popupTitle.textContent = 'Server Information'; popupTitle.style.cssText = ` color: ${theme.textPrimary}; font-size: 18px; font-weight: 600; margin: 0; display: flex; align-items: center; gap: 10px; `; const serverIconPopup = document.createElement('div'); serverIconPopup.innerHTML = ` `; popupTitle.prepend(serverIconPopup); popupHeader.appendChild(popupTitle); // Info items container const infoItems = document.createElement('div'); infoItems.style.cssText = ` display: flex; flex-direction: column; gap: 16px; `; // Info item template 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; } // Add info items infoItems.appendChild(createInfoItem('Game ID', gameId, ` `)); infoItems.appendChild(createInfoItem('Server ID', serverId, ` `)); infoItems.appendChild(createInfoItem('Region', region, ` `)); const formattedLastPlayed = formatLastPlayedWithRelative(lastPlayed); infoItems.appendChild(createInfoItem('Last Played', formattedLastPlayed, ` ` )); // Add footer with action buttons const popupFooter = document.createElement('div'); popupFooter.style.cssText = ` display: flex; justify-content: flex-end; gap: 10px; margin-top: 24px; padding-top: 16px; border-top: 1px solid rgba(255, 255, 255, 0.08); `; const copyButton = document.createElement('button'); copyButton.textContent = 'Copy Info'; copyButton.style.cssText = ` background: rgba(28, 31, 37, 0.6); color: ${theme.textPrimary}; border: 1px solid rgba(255, 255, 255, 0.12); padding: 8px 16px; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.15s ease; display: flex; align-items: center; gap: 6px; `; copyButton.innerHTML = ` Copy Info `; copyButton.addEventListener('click', function() { const infoText = `Game ID: ${gameId}\nServer ID: ${serverId}\nRegion: ${region}\nLast Played: ${lastPlayed}`; navigator.clipboard.writeText(infoText); copyButton.innerHTML = ` Copied! `; setTimeout(() => { copyButton.innerHTML = ` Copy Info `; }, 2000); }); const closeButton_serverinfo = document.createElement('button'); // yea idk what to name this lol closeButton_serverinfo.textContent = 'Close'; closeButton_serverinfo.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_serverinfo.addEventListener('click', function() { popup.style.opacity = '0'; setTimeout(() => { popup.remove(); }, 200); }); popupFooter.appendChild(copyButton); popupFooter.appendChild(closeButton_serverinfo); // Assemble popup content popupContent.appendChild(popupHeader); popupContent.appendChild(infoItems); popupContent.appendChild(popupFooter); popup.appendChild(popupContent); // Add to document document.body.appendChild(popup); // Animate in setTimeout(() => { popup.style.opacity = '1'; popupContent.style.opacity = '1'; popupContent.style.transform = 'translateY(0)'; }, 10); // Close when clicking outside popup.addEventListener('click', function(e) { if (e.target === popup) { popup.style.opacity = '0'; setTimeout(() => { popup.remove(); }, 200); } }); }); // Add buttons to button group buttonGroup.appendChild(removeButton); buttonGroup.appendChild(separator); buttonGroup.appendChild(joinButton); buttonGroup.appendChild(inviteButton); buttonGroup.appendChild(moreInfoButton); serverCard.appendChild(serverIconWrapper); serverCard.appendChild(left); serverCard.appendChild(buttonGroup); // Add subtle line accent const lineAccent = document.createElement('div'); lineAccent.style.cssText = ` position: absolute; left: 0; top: 16px; bottom: 16px; width: 3px; background: ${theme.accentGradient}; border-radius: 0 2px 2px 0; `; serverCard.appendChild(lineAccent); // Add subtle corner accent if (index === 0) { const cornerAccent = document.createElement('div'); cornerAccent.style.cssText = ` position: absolute; right: 0; top: 0; width: 40px; height: 40px; overflow: hidden; pointer-events: none; `; const cornerInner = document.createElement('div'); cornerInner.style.cssText = ` position: absolute; right: -20px; top: -20px; width: 40px; height: 40px; background: ${theme.accentPrimary}; transform: rotate(45deg); opacity: 0.15; `; cornerAccent.appendChild(cornerInner); serverCard.appendChild(cornerAccent); } cardsWrapper.appendChild(serverCard); }); contentContainer.appendChild(cardsWrapper); } recentSection.appendChild(headerContainer); recentSection.appendChild(contentContainer); friendsSectionHeader.parentNode.insertBefore(recentSection, friendsSectionHeader); } /******************************************************* name of function: disableYouTubeAutoplayInIframes Description: Disable autoplay in YouTube and youtube-nocookie iframes inside a container element. @param {HTMLElement|Document} rootElement - The root element to search for iframes (defaults to document). @param {boolean} [observeMutations=false] - Whether to watch for dynamically added iframes. @returns {MutationObserver|null} Returns the MutationObserver if observing, else null. *******************************************************/ function disableYouTubeAutoplayInIframes(rootElement = document, observeMutations = false) { const processedFlag = 'data-autoplay-blocked'; function disableAutoplay(iframe) { if (iframe.hasAttribute(processedFlag)) return; const src = iframe.src; if (!src) return; if (!src.includes('youtube.com') && !src.includes('youtube-nocookie.com')) return; iframe.removeAttribute('allow'); try { const url = new URL(src); url.searchParams.delete('autoplay'); url.searchParams.set('enablejsapi', '0'); const newSrc = url.toString(); if (src !== newSrc) { iframe.src = newSrc; } iframe.setAttribute(processedFlag, 'true'); } catch (e) { // If URL parsing fails, just skip safely ConsoleLogEnabled('Failed to parse iframe src URL', e); } } function processAll() { const selector = 'iframe[src*="youtube.com"], iframe[src*="youtube-nocookie.com"]'; const iframes = (rootElement.querySelectorAll) ? rootElement.querySelectorAll(selector) : []; iframes.forEach(disableAutoplay); } processAll(); if (!observeMutations) return null; // Setup mutation observer if requested const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (!(node instanceof HTMLElement)) return; if (node.tagName === 'IFRAME') { disableAutoplay(node); } else if (node.querySelectorAll) { node.querySelectorAll('iframe[src*="youtube.com"], iframe[src*="youtube-nocookie.com"]') .forEach(disableAutoplay); } }); }); }); observer.observe(rootElement.body || rootElement, { childList: true, subtree: true }); return observer; } /******************************************************* name of function: createPopup description: Creates a popup with server filtering options and interactive buttons. *******************************************************/ function createPopup() { const popup = document.createElement('div'); popup.className = 'server-filters-dropdown-box'; // Unique class name popup.style.cssText = ` position: absolute; width: 210px; height: 382px; right: 0px; top: 30px; z-index: 1000; border-radius: 5px; background-color: rgb(30, 32, 34); display: flex; flex-direction: column; padding: 5px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); `; // Create the header section const header = document.createElement('div'); header.style.cssText = ` display: flex; align-items: center; padding: 10px; border-bottom: 1px solid #444; margin-bottom: 5px; `; // Add the logo (base64 image) const logo = document.createElement('img'); logo.src = window.Base64Images.logo; logo.style.cssText = ` width: 24px; height: 24px; margin-right: 10px; `; // Add the title const title = document.createElement('span'); title.textContent = 'RoLocate'; title.style.cssText = ` color: white; font-size: 18px; font-weight: bold; `; // Append logo and title to the header header.appendChild(logo); header.appendChild(title); // Append the header to the popup popup.appendChild(header); // Define unique names, tooltips, experimental status, and explanations for each button const buttonData = [{ name: "Smallest Servers", tooltip: "**Reverses the order of the server list.** The emptiest servers will be displayed first.", experimental: false, new: false, popular: false, }, { name: "Available Space", tooltip: "**Filters out servers which are full.** Servers with space will only be shown.", experimental: false, new: false, popular: false, }, { name: "Player Count", tooltip: "**Rolocate will find servers with your specified player count or fewer.** Searching for up to 3 minutes. If no exact match is found, it shows servers closest to the target.", experimental: false, new: false, popular: false, }, { name: "Random Shuffle", tooltip: "**Display servers in a completely random order.** Shows servers with space and servers with low player counts in a randomized order.", experimental: false, new: false, popular: false, }, { name: "Server Region", tooltip: "**Filters servers by region.** Offering more accuracy than 'Best Connection' in areas with fewer Roblox servers, like India, or in games with high player counts.", experimental: true, experimentalExplanation: "**Experimental**: Still in development and testing. Sometimes user location cannot be detected.", new: false, popular: false, }, { name: "Best Connection", tooltip: "**Automatically joins the fastest servers for you.** However, it may be less accurate in regions with fewer Roblox servers, like India, or in games with large player counts.", experimental: true, experimentalExplanation: "**Experimental**: Still in development and testing. it may be less accurate in regions with fewer Roblox servers", new: false, popular: false, }, { name: "Join Small Server", tooltip: "**Automatically tries to join a server with a very low population.** On popular games servers may fill up very fast so you might not always get in alone.", experimental: false, new: false, popular: false, }, { name: "Newest Server", tooltip: "**Tries to find Roblox servers that are less than 5 minute old.** This may take longer for very popular games or games with few players.", experimental: false, new: true, popular: false, }, ]; // Create buttons with unique names, tooltips, experimental status, and explanations buttonData.forEach((data, index) => { const buttonContainer = document.createElement('div'); buttonContainer.className = 'server-filter-option'; buttonContainer.classList.add(data.disabled ? "disabled" : "enabled"); // Create a wrapper for the button content that can have opacity applied const buttonContentWrapper = document.createElement('div'); buttonContentWrapper.style.cssText = ` width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; ${data.disabled ? 'opacity: 0.7;' : ''} `; buttonContainer.style.cssText = ` width: 190px; height: 30px; background-color: ${data.disabled ? '#2c2c2c' : '#393B3D'}; margin: 5px; border-radius: 5px; padding: 3.5px; position: relative; cursor: ${data.disabled ? 'not-allowed' : 'pointer'}; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; transform: translateY(-30px); opacity: 0; `; const tooltip = document.createElement('div'); tooltip.className = 'filter-tooltip'; tooltip.style.cssText = ` display: none; position: absolute; top: -10px; left: 200px; width: auto; inline-size: 200px; height: auto; background-color: #191B1D; color: white; padding: 5px; border-radius: 5px; white-space: pre-wrap; font-size: 14px; opacity: 1; z-index: 1001; `; // Parse tooltip text and replace **...** with bold HTML tags tooltip.innerHTML = data.tooltip.replace(/\*\*(.*?)\*\*/g, "$1"); const buttonText = document.createElement('p'); buttonText.style.cssText = ` margin: 0; color: white; font-size: 16px; `; buttonText.textContent = data.name; // Add "DISABLED" style if the button is disabled if (data.disabled) { // Show explanation tooltip (left side like experimental) const disabledTooltip = document.createElement('div'); disabledTooltip.className = 'disabled-tooltip'; disabledTooltip.style.cssText = ` display: none; position: absolute; top: 0; right: 200px; width: 200px; background-color: #191B1D; color: white; padding: 5px; border-radius: 5px; font-size: 14px; white-space: pre-wrap; z-index: 1001; opacity: 1; `; disabledTooltip.innerHTML = data.disabledExplanation.replace(/\*\*(.*?)\*\*/g, '$1'); buttonContainer.appendChild(disabledTooltip); // Add disabled indicator const disabledIndicator = document.createElement('span'); disabledIndicator.textContent = 'DISABLED'; disabledIndicator.style.cssText = ` margin-left: 8px; color: #ff5555; font-size: 10px; font-weight: bold; background-color: rgba(255, 85, 85, 0.1); padding: 1px 4px; border-radius: 3px; `; buttonText.appendChild(disabledIndicator); // Show on hover buttonContainer.addEventListener('mouseenter', () => { disabledTooltip.style.display = 'block'; }); buttonContainer.addEventListener('mouseleave', () => { disabledTooltip.style.display = 'none'; }); } // Add "EXP" label if the button is experimental if (data.experimental) { const expLabel = document.createElement('span'); expLabel.textContent = 'EXP'; expLabel.style.cssText = ` margin-left: 8px; color: gold; font-size: 12px; font-weight: bold; background-color: rgba(255, 215, 0, 0.1); padding: 2px 6px; border-radius: 3px; `; buttonText.appendChild(expLabel); } // Add "POPULAR" label if the button is popular if (data.popular) { const popularLabel = document.createElement('span'); popularLabel.textContent = 'Popular'; popularLabel.style.cssText = ` margin-left: 8px; color: #4CAF50; font-size: 10px; font-weight: bold; background-color: rgba(76, 175, 80, 0.1); padding: 2px 6px; border-radius: 3px; `; buttonText.appendChild(popularLabel); } // add new tooltip let newTooltip = null; if (data.new) { const newLabel = document.createElement('span'); newLabel.textContent = 'NEW'; newLabel.style.cssText = ` margin-left: 8px; color: #2196F3; font-size: 12px; font-weight: bold; background-color: rgba(33, 150, 243, 0.1); padding: 2px 6px; border-radius: 3px; `; buttonText.appendChild(newLabel); // Add NEW explanation tooltip (left side) const newTooltip = document.createElement('div'); newTooltip.className = 'new-tooltip'; newTooltip.style.cssText = ` display: none; position: absolute; top: 0; right: 200px; width: 200px; background-color: #191B1D; color: white; padding: 5px; border-radius: 5px; font-size: 14px; white-space: pre-wrap; z-index: 1001; opacity: 1; `; newTooltip.innerHTML = "New Feature: This feature was recently added. There is no guarantee it will work."; buttonContainer.appendChild(newTooltip); // Show on hover buttonContainer.addEventListener('mouseenter', () => { newTooltip.style.display = 'block'; }); buttonContainer.addEventListener('mouseleave', () => { newTooltip.style.display = 'none'; }); } // Add experimental explanation tooltip (left side) let experimentalTooltip = null; if (data.experimental) { experimentalTooltip = document.createElement('div'); experimentalTooltip.className = 'experimental-tooltip'; experimentalTooltip.style.cssText = ` display: none; position: absolute; top: 0; right: 200px; width: 200px; background-color: #191B1D; color: white; padding: 5px; border-radius: 5px; font-size: 14px; white-space: pre-wrap; z-index: 1001; opacity: 1; `; // Function to replace **text** with bold and gold styled text const formatText = (text) => { return text.replace(/\*\*(.*?)\*\*/g, '$1'); }; // Apply the formatting to the experimental explanation experimentalTooltip.innerHTML = formatText(data.experimentalExplanation); buttonContainer.appendChild(experimentalTooltip); } // Append tooltip directly to button container so it won't inherit opacity buttonContainer.appendChild(tooltip); // Append button text to content wrapper buttonContentWrapper.appendChild(buttonText); // Append content wrapper to button container buttonContainer.appendChild(buttonContentWrapper); // In the event listeners: buttonContainer.addEventListener('mouseover', () => { tooltip.style.display = 'block'; if (data.experimental && experimentalTooltip) { experimentalTooltip.style.display = 'block'; } if (data.new && newTooltip) { // <-- Only show if it exists newTooltip.style.display = 'block'; } if (!data.disabled) { buttonContainer.style.backgroundColor = '#4A4C4E'; buttonContainer.style.transform = 'translateY(0px) scale(1.02)'; } }); buttonContainer.addEventListener('mouseout', () => { tooltip.style.display = 'none'; if (data.experimental && experimentalTooltip) { experimentalTooltip.style.display = 'none'; } if (data.new && newTooltip) { // <-- Only hide if it exists newTooltip.style.display = 'none'; } if (!data.disabled) { buttonContainer.style.backgroundColor = '#393B3D'; buttonContainer.style.transform = 'translateY(0px) scale(1)'; } }); buttonContainer.addEventListener('click', () => { // Prevent click functionality for disabled buttons if (data.disabled) { return; } // Add click animation buttonContainer.style.transform = 'translateY(0px) scale(0.95)'; setTimeout(() => { buttonContainer.style.transform = 'translateY(0px) scale(1)'; }, 150); switch (index) { case 0: smallest_servers(); break; case 1: available_space_servers(); break; case 2: player_count_tab(); break; case 3: random_servers(); break; case 4: createServerCountPopup((totalLimit) => { rebuildServerList(gameId, totalLimit); }); break; case 5: rebuildServerList(gameId, 50, true); break; case 6: auto_join_small_server(); break; case 7: scanRobloxServers(); break; } }); popup.appendChild(buttonContainer); }); // trigger the button animations after DOM insertion // this should be called after the popup is added to the DOM setTimeout(() => { // animate buttons in sequence from top to bottom const buttons = popup.querySelectorAll('.server-filter-option'); buttons.forEach((button, index) => { setTimeout(() => { button.style.transform = 'translateY(0px)'; button.style.opacity = '1'; }, index * 30); // 30 ms from each button }); }, 20); return popup; } /******************************************************* name of function: ServerHop description: Handles server hopping by fetching and joining a random server, excluding recently joined servers. *******************************************************/ function ServerHop() { ConsoleLogEnabled("Starting server hop..."); // Extract the game ID from the URL const url = window.location.href; const gameId = (url.split("/").indexOf("games") !== -1) ? url.split("/")[url.split("/").indexOf("games") + 1] : null; ConsoleLogEnabled(`Game ID: ${gameId}`); // Array to store server IDs let serverIds = []; let nextPageCursor = null; let pagesRequested = 0; // Get the list of all recently joined servers in localStorage const allStoredServers = Object.keys(localStorage) .filter(key => key.startsWith("ROLOCATE_recentServers_")) // server go after! .map(key => JSON.parse(localStorage.getItem(key))); // Remove any expired servers for all games (older than 15 minutes) const currentTime = new Date().getTime(); allStoredServers.forEach(storedServers => { const validServers = storedServers.filter(server => { const lastJoinedTime = new Date(server.timestamp).getTime(); return (currentTime - lastJoinedTime) <= 15 * 60 * 1000; // 15 minutes }); // Update localStorage with the valid (non-expired) servers localStorage.setItem(`ROLOCATE_recentServers_${gameId}`, JSON.stringify(validServers)); }); // Get the list of recently joined servers for the current game const storedServers = JSON.parse(localStorage.getItem(`ROLOCATE_recentServers_${gameId}`)) || []; // Check if there are any recently joined servers and exclude them from selection const validServers = storedServers.filter(server => { const lastJoinedTime = new Date(server.timestamp).getTime(); return (currentTime - lastJoinedTime) <= 15 * 60 * 1000; // 15 minutes }); if (validServers.length > 0) { ConsoleLogEnabled(`Excluding servers joined in the last 15 minutes: ${validServers.map(s => s.serverId).join(', ')}`); } else { ConsoleLogEnabled("No recently joined servers within the last 15 minutes. Proceeding to pick a new server."); } let currentDelay = 150; // Start with 0.15 seconds let isRateLimited = false; /******************************************************* name of function: fetchServers description: Function to fetch servers *******************************************************/ function fetchServers(cursor) { // Randomly choose between sortOrder=1 and sortOrder=2 (50% chance each) const sortOrder = Math.random() < 0.5 ? 1 : 2; const url = `https://games.roblox.com/v1/games/${gameId}/servers/0?sortOrder=${sortOrder}&excludeFullGames=true&limit=100${cursor ? `&cursor=${cursor}` : ""}`; ConsoleLogEnabled(`Using sortOrder: ${sortOrder}`); GM_xmlhttpRequest({ method: "GET", url: url, onload: function(response) { ConsoleLogEnabled("API Response:", response.responseText); if (response.status === 429) { ConsoleLogEnabled("Rate limited! Slowing down requests."); isRateLimited = true; currentDelay = 750; // Switch to 0.75 seconds setTimeout(() => fetchServers(cursor), currentDelay); return; } else if (isRateLimited && response.status === 200) { ConsoleLogEnabled("Recovered from rate limiting. Restoring normal delay."); isRateLimited = false; currentDelay = 150; // Back to normal } try { const data = JSON.parse(response.responseText); if (data.errors) { ConsoleLogEnabled("Skipping unreadable response:", data.errors[0].message); return; } setTimeout(() => { if (!data || !data.data) { ConsoleLogEnabled("Invalid response structure: 'data' is missing or undefined", data); return; } data.data.forEach(server => { if (validServers.some(vs => vs.serverId === server.id)) { ConsoleLogEnabled(`Skipping previously joined server ${server.id}.`); } else { serverIds.push(server.id); } }); if (data.nextPageCursor && pagesRequested < 4) { pagesRequested++; ConsoleLogEnabled(`Fetching page ${pagesRequested}...`); fetchServers(data.nextPageCursor); } else { pickRandomServer(); } }, currentDelay); } catch (error) { ConsoleLogEnabled("Error parsing response:", error); } }, onerror: function(error) { ConsoleLogEnabled("Error fetching server data:", error); } }); } /******************************************************* name of function: pickRandomServer description: Function to pick a random server and join it *******************************************************/ function pickRandomServer() { if (serverIds.length > 0) { const randomServerId = serverIds[Math.floor(Math.random() * serverIds.length)]; ConsoleLogEnabled(`Joining server: ${randomServerId}`); // Join the game instance with the selected server ID Roblox.GameLauncher.joinGameInstance(gameId, randomServerId); // Store the selected server ID with the time and date in localStorage const timestamp = new Date().toISOString(); const newServer = { serverId: randomServerId, timestamp }; validServers.push(newServer); // Save the updated list of recently joined servers to localStorage localStorage.setItem(`ROLOCATE_recentServers_${gameId}`, JSON.stringify(validServers)); ConsoleLogEnabled(`Server ${randomServerId} stored with timestamp ${timestamp}`); } else { ConsoleLogEnabled("No servers found to join."); notifications("You have joined all the servers recently. No servers found to join.", "error", "โš ๏ธ", "5000"); } } // Start the fetching process fetchServers(); } /******************************************************* name of function: Bulk of functions for observer stuff description: adds lots of stuff like autoserver regions and stuff *******************************************************/ if (/^https:\/\/www\.roblox\.com(\/[a-z]{2})?\/games\//.test(window.location.href)) { if (localStorage.ROLOCATE_AutoRunServerRegions === "true") { (() => { /******************************************************* name of function: waitForElement description: waits for a specific element to load onto the page *******************************************************/ function waitForElement(selector, timeout = 5000) { return new Promise((resolve, reject) => { const intervalTime = 100; let elapsed = 0; const interval = setInterval(() => { const el = document.querySelector(selector); if (el) { clearInterval(interval); resolve(el); } else if (elapsed >= timeout) { clearInterval(interval); reject(new Error(`Element "${selector}" not found after ${timeout}ms`)); } elapsed += intervalTime; }, intervalTime); }); } /******************************************************* name of function: waitForAnyElement description: waits for any element on the page to load *******************************************************/ function waitForAnyElement(selector, timeout = 5000) { return new Promise((resolve, reject) => { const intervalTime = 100; let elapsed = 0; const interval = setInterval(() => { const elements = document.querySelectorAll(selector); if (elements.length > 0) { clearInterval(interval); resolve(elements); } else if (elapsed >= timeout) { clearInterval(interval); reject(new Error(`No elements matching "${selector}" found after ${timeout}ms`)); } elapsed += intervalTime; }, intervalTime); }); } /******************************************************* name of function: waitForDivWithStyleSubstring description: waits for server tab to show up, if this doesent happen then it just spits out an error *******************************************************/ function waitForDivWithStyleSubstring(substring, timeout = 5000) { return new Promise((resolve, reject) => { const intervalTime = 100; let elapsed = 0; const interval = setInterval(() => { const divs = Array.from(document.querySelectorAll("div[style]")); const found = divs.find(div => div.style && div.style.background && div.style.background.includes(substring)); if (found) { clearInterval(interval); resolve(found); } else if (elapsed >= timeout) { clearInterval(interval); reject(new Error(`No div with style containing "${substring}" found after ${timeout}ms`)); } elapsed += intervalTime; }, intervalTime); }); } /******************************************************* name of function: clickServersTab description: clicks server tab on game page *******************************************************/ async function clickServersTab() { try { const serversTab = await waitForElement("#tab-game-instances a"); serversTab.click(); ConsoleLogEnabled("[Auto] Servers tab clicked."); return true; } catch (err) { ConsoleLogEnabled("[Auto] Servers tab not found:", err.message); return false; } } /******************************************************* name of function: waitForServerListContainer description: Waits for server list container to load onto the page *******************************************************/ async function waitForServerListContainer() { try { const container = await waitForElement("#rbx-public-running-games"); ConsoleLogEnabled("[Auto] Server list container (#rbx-public-running-games) detected."); return container; } catch (err) { ConsoleLogEnabled("[Auto] Server list container not found:", err.message); return null; } } /******************************************************* name of function: waitForServerItems description: Detects the server item for the functions to start *******************************************************/ async function waitForServerItems() { try { const items = await waitForAnyElement(".rbx-public-game-server-item"); ConsoleLogEnabled(`[Auto] Detected ${items.length} server item(s) (.rbx-public-game-server-item)`); return items; } catch (err) { ConsoleLogEnabled("[Auto] Server items not found:", err.message); return null; } } /******************************************************* name of function: runServerRegions description: Runs auto server regions *******************************************************/ async function runServerRegions() { // Store the original state at the beginning using getItem/setItem const originalNotifFlag = window.localStorage.getItem('ROLOCATE_enablenotifications'); ConsoleLogEnabled("[DEBUG] Original state:", originalNotifFlag); if (originalNotifFlag === "true") { window.localStorage.setItem('ROLOCATE_enablenotifications', 'false'); ConsoleLogEnabled("[Auto] Notifications disabled."); } else { ConsoleLogEnabled("[Auto] Notifications already disabled; leaving flag untouched."); } const gameId = /^https:\/\/www\.roblox\.com(\/[a-z]{2})?\/games\//.test(window.location.href) ? (window.location.href.match(/\/games\/(\d+)/) || [])[1] || null : null; if (!gameId) { ConsoleLogEnabled("[Auto] Game ID not found, aborting runServerRegions."); // Restore original state before early return if (originalNotifFlag !== null) { window.localStorage.setItem('ROLOCATE_enablenotifications', originalNotifFlag); } ConsoleLogEnabled("[DEBUG] Restored to:", window.localStorage.getItem('ROLOCATE_enablenotifications')); ConsoleLogEnabled("[Auto] Notifications restored to original state (early abort)."); return; } if (typeof Loadingbar === "function") Loadingbar(true); if (typeof disableFilterButton === "function") disableFilterButton(true); if (typeof disableLoadMoreButton === "function") disableLoadMoreButton(); if (typeof rebuildServerList === "function") { rebuildServerList(gameId, 16); ConsoleLogEnabled(`[Auto] Server list rebuilt for game ID: ${gameId}`); } else { ConsoleLogEnabled("[Auto] rebuildServerList function not found."); } if (originalNotifFlag === "true") { try { await waitForDivWithStyleSubstring( "radial-gradient(circle, rgba(255, 40, 40, 0.4)", 5000 ); // Restore original state window.localStorage.setItem('ROLOCATE_enablenotifications', originalNotifFlag); ConsoleLogEnabled("[DEBUG] Restored to:", window.localStorage.getItem('ROLOCATE_enablenotifications')); ConsoleLogEnabled("[Auto] Notifications restored to original state (style div detected)."); } catch (err) { ConsoleLogEnabled("[Auto] Style div not detected in time:", err.message); // Restore original state even if there's an error window.localStorage.setItem('ROLOCATE_enablenotifications', originalNotifFlag); ConsoleLogEnabled("[DEBUG] Restored to:", window.localStorage.getItem('ROLOCATE_enablenotifications')); ConsoleLogEnabled("[Auto] Notifications restored to original state (error occurred)."); } } // Final restoration to ensure it's always restored if (originalNotifFlag !== null) { window.localStorage.setItem('ROLOCATE_enablenotifications', originalNotifFlag); } ConsoleLogEnabled("[DEBUG] Final restore to:", window.localStorage.getItem('ROLOCATE_enablenotifications')); ConsoleLogEnabled("[Auto] Function completed - notifications restored to original state."); } window.addEventListener("load", async () => { const clicked = await clickServersTab(); if (!clicked) return; const container = await waitForServerListContainer(); if (!container) return; const items = await waitForServerItems(); if (!items) return; await runServerRegions(); }); })(); } else { ConsoleLogEnabled("[Auto] ROLOCATE_AutoRunServerRegions is not true. Script skipped."); } /******************************************************* name of function: An observer description: Not a function, but an observer which ads the filter button, server hop button, recent servers, and disables trailer autoplay if settings are true *******************************************************/ const observer = new MutationObserver((mutations, obs) => { const serverListOptions = document.querySelector('.server-list-options'); const playButton = document.querySelector('.btn-common-play-game-lg.btn-primary-md'); // debug //ConsoleLogEnabled("Checking Filter Button Insertion:"); //ConsoleLogEnabled("serverListOptions:", serverListOptions); //ConsoleLogEnabled("RL-filter-button exists:", !!document.querySelector('.RL-filter-button')); //ConsoleLogEnabled("Filter button enabled?", localStorage.getItem("ROLOCATE_togglefilterserversbutton")); if (serverListOptions && !document.querySelector('.RL-filter-button') && localStorage.getItem("ROLOCATE_togglefilterserversbutton") === "true") { ConsoleLogEnabled("Added Filter Button"); const filterButton = document.createElement('a'); // yes lmao filterButton.className = 'RL-filter-button'; filterButton.style.cssText = ` color: white; font-weight: bold; text-decoration: none; cursor: pointer; margin-left: 10px; padding: 5px 10px; display: flex; align-items: center; gap: 5px; position: relative; margin-top: 4px; `; filterButton.addEventListener('mouseover', () => { filterButton.style.textDecoration = 'underline'; }); filterButton.addEventListener('mouseout', () => { filterButton.style.textDecoration = 'none'; }); const buttonText = document.createElement('span'); buttonText.className = 'RL-filter-text'; buttonText.textContent = 'Filters'; filterButton.appendChild(buttonText); const icon = document.createElement('span'); icon.className = 'RL-filter-icon'; icon.textContent = 'โ‰ก'; icon.style.cssText = `font-size: 18px;`; filterButton.appendChild(icon); serverListOptions.appendChild(filterButton); let popup = null; filterButton.addEventListener('click', (event) => { event.stopPropagation(); if (popup) { popup.remove(); popup = null; } else { popup = createPopup(); popup.style.top = `${filterButton.offsetHeight}px`; popup.style.left = '0'; filterButton.appendChild(popup); } }); document.addEventListener('click', (event) => { if (popup && !filterButton.contains(event.target)) { popup.remove(); popup = null; } }); } // new condition to trigger recent server logic if (localStorage.getItem("ROLOCATE_togglerecentserverbutton") === "true") { HandleRecentServers(); HandleRecentServersURL(); } // new condition to trigger recent server logic if (localStorage.getItem("ROLOCATE_disabletrailer") === "true") { disableYouTubeAutoplayInIframes(); } if (playButton && !document.querySelector('.custom-play-button') && localStorage.getItem("ROLOCATE_toggleserverhopbutton") === "true") { ConsoleLogEnabled("Added Server Hop Button"); const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; gap: 10px; align-items: center; width: 100%; `; playButton.style.cssText += ` flex: 3; padding: 10px 12px; text-align: center; `; const serverHopButton = document.createElement('button'); serverHopButton.className = 'custom-play-button'; serverHopButton.style.cssText = ` background-color: #335fff; color: white; border: none; padding: 7.5px 12px; cursor: pointer; font-weight: bold; border-radius: 8px; flex: 1; text-align: center; display: flex; align-items: center; justify-content: center; position: relative; `; const tooltip = document.createElement('div'); tooltip.textContent = 'Join Random Server / Server Hop'; tooltip.style.cssText = ` position: absolute; background: rgba(51, 95, 255, 0.9); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); color: white; padding: 6px 10px; border-radius: 8px; font-size: 12px; font-weight: 500; letter-spacing: 0.025em; visibility: hidden; opacity: 0; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%) translateY(4px); white-space: nowrap; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04), 0 0 0 1px rgba(255, 255, 255, 0.05); border: 1px solid rgba(148, 163, 184, 0.1); z-index: 1000; /* Arrow */ &::after { content: ''; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid rgba(51, 95, 255, 0.9); filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1)); } `; serverHopButton.appendChild(tooltip); serverHopButton.addEventListener('mouseover', () => { tooltip.style.visibility = 'visible'; tooltip.style.opacity = '1'; }); serverHopButton.addEventListener('mouseout', () => { tooltip.style.visibility = 'hidden'; tooltip.style.opacity = '0'; }); const logo = document.createElement('img'); logo.src = window.Base64Images.icon_serverhop; logo.style.cssText = ` width: 45px; height: 45px; `; serverHopButton.appendChild(logo); playButton.parentNode.insertBefore(buttonContainer, playButton); buttonContainer.appendChild(playButton); buttonContainer.appendChild(serverHopButton); serverHopButton.addEventListener('click', () => { ServerHop(); }); } const filterEnabled = localStorage.getItem("ROLOCATE_togglefilterserversbutton") === "true"; const hopEnabled = localStorage.getItem("ROLOCATE_toggleserverhopbutton") === "true"; const recentEnabled = localStorage.getItem("ROLOCATE_togglerecentserverbutton") === "true"; const filterPresent = !filterEnabled || document.querySelector('.RL-filter-button'); const hopPresent = !hopEnabled || document.querySelector('.custom-play-button'); const recentPresent = !recentEnabled || document.querySelector('.recent-servers-section'); if (filterPresent && hopPresent && recentPresent) { obs.disconnect(); ConsoleLogEnabled("Disconnected Observer"); } }); observer.observe(document.body, { childList: true, subtree: true }); } /********************************************************************************************************************************************************************************************************************************************* The End of: This is all of the functions for the filter button and the popup for the 8 buttons does not include the functions for the 8 buttons *********************************************************************************************************************************************************************************************************************************************/ // Quick join handler for smartsearch if (window.location.hash === '#?ROLOCATE_QUICKJOIN') { if (localStorage.ROLOCATE_smartsearch === 'true') { // Extract gameId from URL path (assuming format: /games/gameId) const gameIdMatch = window.location.pathname.match(/\/games\/(\d+)/); if (gameIdMatch && gameIdMatch[1]) { const gameId = gameIdMatch[1]; rebuildServerList(gameId, 50, false, true); // Quick join mode } else { console.log('[RoLocate] Could not extract gameId from URL'); notifications('Error: Failed to extract gameid. Please try again later.', 'error', 'โš ๏ธ', '5000'); } // Clean up the URL history.replaceState(null, null, window.location.pathname + window.location.search); } else { console.log('[RoLocate] Quick Join detected but smartsearch is disabled'); } } /********************************************************************************************************************************************************************************************************************************************* Functions for the 1st button *********************************************************************************************************************************************************************************************************************************************/ /******************************************************* name of function: smallest_servers description: Fetches the smallest servers, disables the "Load More" button, shows a loading bar, and recreates the server cards. *******************************************************/ async function smallest_servers() { // Disable the "Load More" button and show the loading bar Loadingbar(true); disableFilterButton(true); disableLoadMoreButton(); notifications("Finding small servers...", "success", "๐Ÿง"); // Get the game ID from the URL const gameId = ((p => { const i = p.indexOf('games'); return i !== -1 && p.length > i + 1 ? p[i + 1] : null; })(window.location.pathname.split('/'))); // Retry mechanism let retries = 3; let success = false; while (retries > 0 && !success) { try { // Use GM_xmlhttpRequest to fetch server data from the Roblox API const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `https://games.roblox.com/v1/games/${gameId}/servers/0?sortOrder=1&excludeFullGames=true&limit=100`, onload: function(response) { if (response.status === 429) { reject(new Error('429: Too Many Requests')); } else if (response.status >= 200 && response.status < 300) { resolve(response); } else { reject(new Error(`HTTP error! status: ${response.status}`)); } }, onerror: function(error) { reject(error); } }); }); const data = JSON.parse(response.responseText); // Process each server for (const server of data.data) { const { id: serverId, playerTokens, maxPlayers, playing } = server; // Pass the server data to the card creation function await rbx_card(serverId, playerTokens, maxPlayers, playing, gameId); } success = true; // Mark as successful if no errors occurred } catch (error) { retries--; // Decrement the retry count if (error.message === '429: Too Many Requests' && retries > 0) { ConsoleLogEnabled('Encountered a 429 error. Retrying in 5 seconds...'); await new Promise(resolve => setTimeout(resolve, 5000)); // Wait for 5 seconds } else { ConsoleLogEnabled('Error fetching server data:', error); notifications('Error: Failed to fetch server data. Please try again later.', 'error', 'โš ๏ธ', '5000'); Loadingbar(false); break; // Exit the loop if it's not a 429 error or no retries left } } finally { if (success || retries === 0) { // Hide the loading bar and enable the filter button Loadingbar(false); disableFilterButton(false); } } } } /********************************************************************************************************************************************************************************************************************************************* Functions for the 2nd button *********************************************************************************************************************************************************************************************************************************************/ /******************************************************* name of function: available_space_servers description: Fetches servers with available space, disables the "Load More" button, shows a loading bar, and recreates the server cards. *******************************************************/ async function available_space_servers() { // Disable the "Load More" button and show the loading bar Loadingbar(true); disableLoadMoreButton(); disableFilterButton(true); notifications("Finding servers with space...", "success", "๐Ÿง"); // Get the game ID from the URL const gameId = ((p => { const i = p.indexOf('games'); return i !== -1 && p.length > i + 1 ? p[i + 1] : null; })(window.location.pathname.split('/'))); // Retry mechanism let retries = 3; let success = false; while (retries > 0 && !success) { try { // Use GM_xmlhttpRequest to fetch server data from the Roblox API const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `https://games.roblox.com/v1/games/${gameId}/servers/0?sortOrder=2&excludeFullGames=true&limit=100`, onload: function(response) { if (response.status === 429) { reject(new Error('429: Too Many Requests')); } else if (response.status >= 200 && response.status < 300) { resolve(response); } else { reject(new Error(`HTTP error! status: ${response.status}`)); } }, onerror: function(error) { reject(error); } }); }); const data = JSON.parse(response.responseText); // Process each server for (const server of data.data) { const { id: serverId, playerTokens, maxPlayers, playing } = server; // Pass the server data to the card creation function await rbx_card(serverId, playerTokens, maxPlayers, playing, gameId); } success = true; // Mark as successful if no errors occurred } catch (error) { retries--; // Decrement the retry count if (error.message === '429: Too Many Requests' && retries > 0) { ConsoleLogEnabled('Encountered a 429 error. Retrying in 10 seconds...'); await new Promise(resolve => setTimeout(resolve, 10000)); // Wait for 10 seconds } else { ConsoleLogEnabled('Error fetching server data:', error); break; // Exit the loop if it's not a 429 error or no retries left } } finally { if (success || retries === 0) { // Hide the loading bar and enable the filter button Loadingbar(false); disableFilterButton(false); } } } } /********************************************************************************************************************************************************************************************************************************************* Functions for the 3rd button *********************************************************************************************************************************************************************************************************************************************/ /******************************************************* name of function: player_count_tab description: Opens a popup for the user to select the max player count using a slider and filters servers accordingly. Maybe one of my best functions lowkey. *******************************************************/ function player_count_tab() { // Check if the max player count has already been determined if (!player_count_tab.maxPlayers) { // Try to find the element containing the player count information const playerCountElement = document.querySelector('.text-info.rbx-game-status.rbx-game-server-status.text-overflow'); if (playerCountElement) { const playerCountText = playerCountElement.textContent.trim(); const match = playerCountText.match(/(\d+) of (\d+) people max/); if (match) { const maxPlayers = parseInt(match[2], 10); if (!isNaN(maxPlayers) && maxPlayers > 1) { player_count_tab.maxPlayers = maxPlayers; ConsoleLogEnabled("Found text element with max playercount"); } } } else { // If the element is not found, extract the gameId from the URL const gameIdMatch = window.location.href.match(/\/(?:[a-z]{2}\/)?games\/(\d+)/); if (gameIdMatch && gameIdMatch[1]) { const gameId = gameIdMatch[1]; // Send a request to the Roblox API to get server information GM_xmlhttpRequest({ method: 'GET', url: `https://games.roblox.com/v1/games/${gameId}/servers/public?sortOrder=1&excludeFullGames=true&limit=100`, onload: function(response) { try { if (response.status === 429) { // Rate limit error, default to 100 ConsoleLogEnabled("Rate limited defaulting to 100."); player_count_tab.maxPlayers = 100; } else { ConsoleLogEnabled("Valid api response"); const data = JSON.parse(response.responseText); if (data.data && data.data.length > 0) { const maxPlayers = data.data[0].maxPlayers; if (!isNaN(maxPlayers) && maxPlayers > 1) { player_count_tab.maxPlayers = maxPlayers; } } } // Update the slider range if the popup is already created const slider = document.querySelector('.player-count-popup input[type="range"]'); if (slider) { slider.max = player_count_tab.maxPlayers ? (player_count_tab.maxPlayers - 1).toString() : '100'; slider.style.background = ` linear-gradient( to right, #00A2FF 0%, #00A2FF ${slider.value}%, #444 ${slider.value}%, #444 100% ); `; } } catch (error) { ConsoleLogEnabled('Failed to parse API response:', error); // Default to 100 if parsing fails player_count_tab.maxPlayers = 100; const slider = document.querySelector('.player-count-popup input[type="range"]'); if (slider) { slider.max = '100'; slider.style.background = ` linear-gradient( to right, #00A2FF 0%, #00A2FF ${slider.value}%, #444 ${slider.value}%, #444 100% ); `; } } }, onerror: function(error) { ConsoleLogEnabled('Failed to fetch server information:', error); ConsoleLogEnabled('Fallback to 100 players.'); // Default to 100 if the request fails player_count_tab.maxPlayers = 100; const slider = document.querySelector('.player-count-popup input[type="range"]'); if (slider) { slider.max = '100'; slider.style.background = ` linear-gradient( to right, #00A2FF 0%, #00A2FF ${slider.value}%, #444 ${slider.value}%, #444 100% ); `; } } }); } } } // Create the overlay (backdrop) const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 9999; opacity: 0; transition: opacity 0.3s ease; `; document.body.appendChild(overlay); // Create the popup container const popup = document.createElement('div'); popup.className = 'player-count-popup'; popup.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: rgb(30, 32, 34); padding: 20px; border-radius: 10px; z-index: 10000; box-shadow: 0 0 15px rgba(0, 0, 0, 0.7); display: flex; flex-direction: column; align-items: center; gap: 15px; width: 300px; opacity: 0; transition: opacity 0.3s ease, transform 0.3s ease; `; // Add a close button in the top-right corner (bigger size) const closeButton = document.createElement('button'); closeButton.innerHTML = '×'; // Using 'ร—' for the close icon closeButton.style.cssText = ` position: absolute; top: 10px; right: 10px; background: transparent; border: none; color: #ffffff; font-size: 24px; /* Increased font size */ cursor: pointer; width: 36px; /* Increased size */ height: 36px; /* Increased size */ border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: background-color 0.3s ease, color 0.3s ease; `; closeButton.addEventListener('mouseenter', () => { closeButton.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'; closeButton.style.color = '#ff4444'; }); closeButton.addEventListener('mouseleave', () => { closeButton.style.backgroundColor = 'transparent'; closeButton.style.color = '#ffffff'; }); // Add a title const title = document.createElement('h3'); title.textContent = 'Select Max Player Count'; title.style.cssText = ` color: white; margin: 0; font-size: 18px; font-weight: 500; `; popup.appendChild(title); // Add a slider with improved functionality and styling const slider = document.createElement('input'); slider.type = 'range'; slider.min = '1'; slider.max = player_count_tab.maxPlayers ? (player_count_tab.maxPlayers - 1).toString() : '100'; slider.value = '1'; // Default value slider.step = '1'; // Step for better accuracy slider.style.cssText = ` width: 80%; cursor: pointer; margin: 10px 0; -webkit-appearance: none; /* Remove default styling */ background: transparent; `; // Custom slider track slider.style.background = ` linear-gradient( to right, #00A2FF 0%, #00A2FF ${slider.value}%, #444 ${slider.value}%, #444 100% ); border-radius: 5px; height: 6px; `; // Custom slider thumb slider.style.setProperty('--thumb-size', '20px'); /* Larger thumb */ slider.style.setProperty('--thumb-color', '#00A2FF'); slider.style.setProperty('--thumb-hover-color', '#0088cc'); slider.style.setProperty('--thumb-border', '2px solid #fff'); slider.style.setProperty('--thumb-shadow', '0 0 5px rgba(0, 0, 0, 0.5)'); slider.addEventListener('input', () => { slider.style.background = ` linear-gradient( to right, #00A2FF 0%, #00A2FF ${slider.value}%, #444 ${slider.value}%, #444 100% ); `; sliderValue.textContent = slider.value; // Update the displayed value }); // Keyboard support for better accuracy (fixed to increment/decrement by 1) slider.addEventListener('keydown', (e) => { e.preventDefault(); // Prevent default behavior (which might cause jumps) let newValue = parseInt(slider.value, 10); if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') { newValue = Math.max(1, newValue - 1); // Decrease by 1 } else if (e.key === 'ArrowRight' || e.key === 'ArrowUp') { newValue = Math.min(100, newValue + 1); // Increase by 1 } slider.value = newValue; slider.dispatchEvent(new Event('input')); // Trigger input event to update UI }); popup.appendChild(slider); // Add a display for the slider value const sliderValue = document.createElement('span'); sliderValue.textContent = slider.value; sliderValue.style.cssText = ` color: white; font-size: 16px; font-weight: bold; `; popup.appendChild(sliderValue); // Add a submit button with dark, blackish style const submitButton = document.createElement('button'); submitButton.textContent = 'Search'; submitButton.style.cssText = ` padding: 8px 20px; font-size: 16px; background-color: #1a1a1a; /* Dark blackish color */ color: white; border: none; border-radius: 5px; cursor: pointer; transition: background-color 0.3s ease, transform 0.2s ease; `; submitButton.addEventListener('mouseenter', () => { submitButton.style.backgroundColor = '#333'; /* Slightly lighter on hover */ submitButton.style.transform = 'scale(1.05)'; }); submitButton.addEventListener('mouseleave', () => { submitButton.style.backgroundColor = '#1a1a1a'; submitButton.style.transform = 'scale(1)'; }); // Add a yellow box with a tip under the submit button const tipBox = document.createElement('div'); tipBox.style.cssText = ` width: 100%; padding: 10px; background-color: rgba(255, 204, 0, 0.15); border-radius: 5px; text-align: center; font-size: 14px; color: #ffcc00; transition: background-color 0.3s ease; `; tipBox.textContent = 'Tip: Click the slider and use the arrow keys for more accuracy.'; tipBox.addEventListener('mouseenter', () => { tipBox.style.backgroundColor = 'rgba(255, 204, 0, 0.25)'; }); tipBox.addEventListener('mouseleave', () => { tipBox.style.backgroundColor = 'rgba(255, 204, 0, 0.15)'; }); popup.appendChild(tipBox); // Append the popup to the body document.body.appendChild(popup); // Fade in the overlay and popup setTimeout(() => { overlay.style.opacity = '1'; popup.style.opacity = '1'; popup.style.transform = 'translate(-50%, -50%) scale(1)'; }, 10); /******************************************************* name of function: fadeOutAndRemove description: Fades out and removes the popup and overlay. *******************************************************/ function fadeOutAndRemove(popup, overlay) { popup.style.opacity = '0'; popup.style.transform = 'translate(-50%, -50%) scale(0.9)'; overlay.style.opacity = '0'; setTimeout(() => { popup.remove(); overlay.remove(); }, 300); // Match the duration of the transition } // Close the popup when clicking outside overlay.addEventListener('click', () => { fadeOutAndRemove(popup, overlay); }); // Close the popup when the close button is clicked closeButton.addEventListener('click', () => { fadeOutAndRemove(popup, overlay); }); // Handle submit button click submitButton.addEventListener('click', () => { const maxPlayers = parseInt(slider.value, 10); if (!isNaN(maxPlayers) && maxPlayers > 0) { filterServersByPlayerCount(maxPlayers); fadeOutAndRemove(popup, overlay); } else { notifications('Error: Please enter a number greater than 0', 'error', 'โš ๏ธ', '5000'); } }); popup.appendChild(submitButton); popup.appendChild(closeButton); } /******************************************************* name of function: fetchServersWithRetry description: Fetches server data with retry logic and a delay between requests to avoid rate-limiting. Uses GM_xmlhttpRequest instead of fetch. *******************************************************/ async function fetchServersWithRetry(url, retries = 15, currentDelay = 750) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) { // Check for 429 Rate Limit error if (response.status === 429) { if (retries > 0) { const newDelay = currentDelay * 1; // Exponential backoff ConsoleLogEnabled(`[DEBUG] Rate limited. Waiting ${newDelay / 1000} seconds before retrying...`); setTimeout(() => { resolve(fetchServersWithRetry(url, retries - 1, newDelay)); // Retry with increased delay }, newDelay); } else { ConsoleLogEnabled('[DEBUG] Rate limit retries exhausted.'); notifications('Error: Rate limited please try again later.', 'error', 'โš ๏ธ', '5000'); reject(new Error('RateLimit')); } return; } // Handle other HTTP errors if (response.status < 200 || response.status >= 300) { ConsoleLogEnabled('[DEBUG] HTTP error:', response.status, response.statusText); reject(new Error(`HTTP error: ${response.status}`)); return; } // Parse and return the JSON data try { const data = JSON.parse(response.responseText); ConsoleLogEnabled('[DEBUG] Fetched data successfully:', data); resolve(data); } catch (error) { ConsoleLogEnabled('[DEBUG] Error parsing JSON:', error); reject(error); } }, onerror: function(error) { ConsoleLogEnabled('[DEBUG] Error in GM_xmlhttpRequest:', error); reject(error); } }); }); } /******************************************************* name of function: filterServersByPlayerCount description: Filters servers to show only those with a player count equal to or below the specified max. If no exact matches are found, prioritizes servers with player counts lower than the input. Keeps fetching until at least 8 servers are found, with a dynamic delay between requests. *******************************************************/ async function filterServersByPlayerCount(maxPlayers) { // Validate maxPlayers before proceeding if (isNaN(maxPlayers) || maxPlayers < 1 || !Number.isInteger(maxPlayers)) { ConsoleLogEnabled('[DEBUG] Invalid input for maxPlayers.'); notifications('Error: Please input a valid whole number greater than or equal to 1.', 'error', 'โš ๏ธ', '5000'); return; } // Disable UI elements and clear the server list Loadingbar(true); disableLoadMoreButton(); disableFilterButton(true); const serverList = document.querySelector('#rbx-public-game-server-item-container'); serverList.innerHTML = ''; const gameId = ((p = window.location.pathname.split('/')) => { const i = p.indexOf('games'); return i !== -1 && p.length > i + 1 ? p[i + 1] : null; })(); let cursor = null; let serversFound = 0; let serverMaxPlayers = null; let isCloserToOne = null; let topDownServers = []; // Servers collected during top-down search let bottomUpServers = []; // Servers collected during bottom-up search let currentDelay = 500; // Initial delay of 0.5 seconds const timeLimit = 3 * 60 * 1000; // 3 minutes in milliseconds const startTime = Date.now(); // Record the start time notifications('Will search for a maximum of 3 minutes to find a server.', 'success', '๐Ÿ”Ž', '5000'); try { while (serversFound < 16) { // Check if the time limit has been exceeded if (Date.now() - startTime > timeLimit) { ConsoleLogEnabled('[DEBUG] Time limit reached. Proceeding to fallback servers.'); notifications('Warning: Time limit reached. Proceeding to fallback servers.', 'warning', 'โ—', '5000'); break; } // Fetch initial data to determine serverMaxPlayers and isCloserToOne if (!serverMaxPlayers) { const initialUrl = cursor ? `https://games.roblox.com/v1/games/${gameId}/servers/public?excludeFullGames=true&limit=100&cursor=${cursor}` : `https://games.roblox.com/v1/games/${gameId}/servers/public?excludeFullGames=true&limit=100`; const initialData = await fetchServersWithRetry(initialUrl); if (initialData.data.length > 0) { serverMaxPlayers = initialData.data[0].maxPlayers; isCloserToOne = maxPlayers <= (serverMaxPlayers / 2); } else { notifications("No servers found in initial fetch.", "error", "โš ๏ธ", "5000"); ConsoleLogEnabled('[DEBUG] No servers found in initial fetch.', 'warning', 'โ—'); break; } } // Validate maxPlayers against serverMaxPlayers if (maxPlayers >= serverMaxPlayers) { ConsoleLogEnabled('[DEBUG] Invalid input: maxPlayers is greater than or equal to serverMaxPlayers.'); notifications(`Error: Please input a number between 1 through ${serverMaxPlayers - 1}`, 'error', 'โš ๏ธ', '5000'); return; } // Adjust the URL based on isCloserToOne const baseUrl = isCloserToOne ? `https://games.roblox.com/v1/games/${gameId}/servers/public?sortOrder=1&excludeFullGames=true&limit=100` : `https://games.roblox.com/v1/games/${gameId}/servers/public?excludeFullGames=true&limit=100`; // why does this work lmao const url = cursor ? `${baseUrl}&cursor=${cursor}` : baseUrl; const data = await fetchServersWithRetry(url); // Safety check: Ensure the server list is valid and iterable if (!Array.isArray(data.data)) { ConsoleLogEnabled('[DEBUG] Invalid server list received. Waiting 1 second before retrying...'); await delay(1000); // Wait 1 second before retrying continue; // Skip the rest of the loop and retry } // Filter and process servers for (const server of data.data) { if (server.playing === maxPlayers) { await rbx_card(server.id, server.playerTokens, server.maxPlayers, server.playing, gameId); serversFound++; if (serversFound >= 16) { break; } } else if (!isCloserToOne && server.playing > maxPlayers) { topDownServers.push(server); // Add to top-down fallback list } else if (isCloserToOne && server.playing < maxPlayers) { bottomUpServers.push(server); // Add to bottom-up fallback list } } // Exit if no more servers are available if (!data.nextPageCursor) { break; } cursor = data.nextPageCursor; // Adjust delay dynamically if (currentDelay > 150) { currentDelay = Math.max(150, currentDelay / 2); // Gradually reduce delay } ConsoleLogEnabled(`[DEBUG] Waiting ${currentDelay / 1000} seconds before next request...`); await delay(currentDelay); } // If no exact matches were found or time limit reached, use fallback servers if (serversFound === 0 && (topDownServers.length > 0 || bottomUpServers.length > 0)) { notifications(`There are no servers with ${maxPlayers} players. Showing servers closest to ${maxPlayers} players.`, 'warning', '๐Ÿ˜”', '8000'); // Sort top-down servers by player count (ascending) topDownServers.sort((a, b) => a.playing - b.playing); // Sort bottom-up servers by player count (descending) bottomUpServers.sort((a, b) => b.playing - a.playing); // Combine both fallback lists (prioritize top-down servers first) const combinedFallback = [...topDownServers, ...bottomUpServers]; for (const server of combinedFallback) { await rbx_card(server.id, server.playerTokens, server.maxPlayers, server.playing, gameId); serversFound++; if (serversFound >= 16) { break; } } } if (serversFound <= 0) { notifications('No Servers Found Within The Provided Criteria', 'info', '๐Ÿ”Ž', '5000'); } } catch (error) { ConsoleLogEnabled('[DEBUG] Error in filterServersByPlayerCount:', error); } finally { Loadingbar(false); disableFilterButton(false); } } /********************************************************************************************************************************************************************************************************************************************* Functions for the 4th button *********************************************************************************************************************************************************************************************************************************************/ /******************************************************* name of function: random_servers description: Fetches servers from two different URLs, combines the results, ensures no duplicates, shuffles the list, and passes the server information to the rbx_card function in a random order. Handles 429 errors with retries. *******************************************************/ async function random_servers() { notifications('Finding Random Servers. Please wait 2-5 seconds', 'success', '๐Ÿ”Ž', '5000'); // Disable the "Load More" button and show the loading bar Loadingbar(true); disableFilterButton(true); disableLoadMoreButton(); // Get the game ID from the URL ik reduent function const gameId = ((p = window.location.pathname.split('/')) => { const i = p.indexOf('games'); return i !== -1 && p.length > i + 1 ? p[i + 1] : null; })(); try { // Fetch servers from the first URL with retry logic const firstUrl = `https://games.roblox.com/v1/games/${gameId}/servers/public?excludeFullGames=true&limit=10`; const firstData = await fetchWithRetry(firstUrl, 10); // Retry up to 3 times // Wait for 5 seconds await delay(1500); // Fetch servers from the second URL with retry logic const secondUrl = `https://games.roblox.com/v1/games/${gameId}/servers/public?sortOrder=1&excludeFullGames=true&limit=10`; const secondData = await fetchWithRetry(secondUrl, 10); // Retry up to 3 times // Combine the servers from both URLs. Yea im kinda proud of this lmao const combinedServers = [...firstData.data, ...secondData.data]; // Remove duplicates by server ID const uniqueServers = []; const seenServerIds = new Set(); for (const server of combinedServers) { if (!seenServerIds.has(server.id)) { seenServerIds.add(server.id); uniqueServers.push(server); } } // Shuffle the unique servers array const shuffledServers = shuffleArray(uniqueServers); // Get the first 16 shuffled servers const selectedServers = shuffledServers.slice(0, 16); // Process each server in random order for (const server of selectedServers) { const { id: serverId, playerTokens, maxPlayers, playing } = server; // Pass the server data to the card creation function await rbx_card(serverId, playerTokens, maxPlayers, playing, gameId); } } catch (error) { ConsoleLogEnabled('Error fetching server data:', error); notifications('Error: Failed to fetch server data. Please try again later.', 'error', 'โš ๏ธ', '5000'); } finally { // Hide the loading bar and enable the filter button Loadingbar(false); disableFilterButton(false); } } /******************************************************* name of function: fetchWithRetry description: Fetches data from a URL with retry logic for 429 errors using GM_xmlhttpRequest. *******************************************************/ function fetchWithRetry(url, retries) { return new Promise((resolve, reject) => { const attemptFetch = (attempt = 0) => { GM_xmlhttpRequest({ method: "GET", url: url, onload: function(response) { if (response.status === 429) { if (attempt < retries) { ConsoleLogEnabled(`Rate limited. Retrying in 2.5 seconds... (Attempt ${attempt + 1}/${retries})`); setTimeout(() => attemptFetch(attempt + 1), 1500); // Wait 1.5 seconds and retry } else { reject(new Error('Rate limit exceeded after retries')); } } else if (response.status >= 200 && response.status < 300) { try { const data = JSON.parse(response.responseText); resolve(data); } catch (error) { reject(new Error('Failed to parse JSON response')); } } else { reject(new Error(`HTTP error: ${response.status}`)); } }, onerror: function(error) { if (attempt < retries) { ConsoleLogEnabled(`Error occurred. Retrying in 10 seconds... (Attempt ${attempt + 1}/${retries})`); setTimeout(() => attemptFetch(attempt + 1), 10000); // Wait 10 seconds and retry } else { reject(error); } } }); }; attemptFetch(); }); } /******************************************************* name of function: shuffleArray description: Shuffles an array using the Fisher-Yates algorithm. This ronald fisher guy was kinda smart *******************************************************/ function shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); // random index from 0 to i [array[i], array[j]] = [array[j], array[i]]; // swap elements } return array; } /********************************************************************************************************************************************************************************************************************************************* Functions for the 5th button. taken from my other project *********************************************************************************************************************************************************************************************************************************************/ /******************************************************* name of function: Isongamespage description: not a function but if on game page inject styles *******************************************************/ if (Isongamespage) { // Create a