您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds filter options to roblox server page. Alternative to paid extensions like RoPro, RoGold (Ultimate), RoQol, and RoKit.
// ==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.org/scripts/535590/1586769/Rolocate%20Base64%20Image%20Library%2020.js // @require https://update.greasyfork.org/scripts/539427/1607754/Rolocate%20Server%20Region%20Data.js // @require https://update.greasyfork.org/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 // ==/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: '<svg width="16" height="16" fill="none" stroke="#4CAF50" stroke-width="2" viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg>', error: '<svg width="16" height="16" fill="none" stroke="#F44336" stroke-width="2" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></svg>', warning: '<svg width="16" height="16" fill="none" stroke="#FF9800" stroke-width="2" viewBox="0 0 24 24"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0zM12 9v4M12 17h.01"/></svg>', info: '<svg width="16" height="16" fill="none" stroke="#2196F3" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>' }; 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 <br> const messageSpan = document.createElement('span'); messageSpan.className = 'toast-message'; messageSpan.innerHTML = message.replace(/\n/g, '<br>'); 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, '<br>'); }, 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 = ` <div class="first-time-popup"> <div class="first-time-popup-content"> <div class="popup-header"> <img class="popup-logo" src="${window.Base64Images.logo}" alt="Rolocate Logo"> <div class="popup-header-content"> <h1 class="popup-title">Rolocate Update</h1> <div class="popup-version">${VERSION}</div> </div> <span class="first-time-popup-close">×</span> </div> <div class="popup-main"> <div class="popup-left"> <div class="developer-message"> <div class="developer-message-title">From Oqarshi:</div> <div class="developer-message-text">Please report any issues on GreasyFork if something breaks! Thank you!</div> </div> <div class="features-title">✨ What's New</div> <div class="feature-item" data-feature="smartsearch"> <div class="feature-header"> <span class="feature-icon">🧠</span> <div class="feature-title">SmartSearch</div> <span class="feature-badge">New</span> </div> </div> <div class="feature-item" data-feature="mutualfriends"> <div class="feature-header"> <span class="feature-icon">👥</span> <div class="feature-title">Mutual Friends</div> <span class="feature-badge">New</span> </div> </div> <div class="feature-item" data-feature="chatcontrols"> <div class="feature-header"> <span class="feature-icon">💬</span> <div class="feature-title">Disable Chat</div> <span class="feature-badge">New</span> </div> </div> <div class="feature-item" data-feature="quicklaunchgames_feature"> <div class="feature-header"> <span class="feature-icon">⚡</span> <div class="feature-title">Quick Launch Games</div> <span class="feature-badge">New</span> </div> </div> </div> <div class="popup-right"> <div class="welcome-panel" id="welcome-panel"> <div class="welcome-icon">🚀</div> <div class="welcome-text">Select a feature to learn more</div> <div class="welcome-subtext">Click on any feature from the left to see detailed information</div> </div> <div class="detail-panel" id="detail-panel" style="display: none;"></div> <div class="help-section"> <div class="help-title">Need Help?</div> <a href="https://oqarshi.github.io/Invite/rolocate/docs/" target="_blank" class="help-link"> <span class="help-link-icon">📖</span> <span>Documentation</span> </a> </div> </div> </div> <div class="popup-footer"> <p class="popup-note">This notification will not appear again until the next version release.</p> </div> </div> </div> `; const popupContainer = document.createElement('div'); popupContainer.innerHTML = popupHTML; document.body.appendChild(popupContainer); const closeButton = popupContainer.querySelector('.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 = ` <div class="detail-title"> <span>${feature.icon}</span> <span>${feature.title}</span> </div> <div class="detail-subtitle">${feature.subtitle}</div> <div class="detail-description">${feature.description}</div> <div class="detail-settings"> ${feature.settings.map(setting => ` <div class="setting-row"> <span class="setting-label">${setting.label}:</span> <span class="setting-value">${setting.value}</span> </div> `).join('')} </div> `; } }); }); 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 ` <div class="home-section"> <img class="rolocate-logo" src="${window.Base64Images.logo}" alt="ROLOCATE Logo"> <div class="version">Rolocate: Version 40.0</div> <div class="section-separator"></div> <p>Rolocate by Oqarshi.</p> <p class="license-note"> Licensed under a <strong>Custom License – Personal Use Only</strong>. No redistribution. </p> </div> `; } if (section === "appearance") { return ` <div class="appearance-section"> <label class="toggle-slider section-hover"> <input type="checkbox" id="ShowOldGreeting"> <span class="slider"></span> Show Old Greeting <span class="help-icon" data-help="Show Old Greeting">?</span> </label> <label class="toggle-slider section-hover new_label"> <input type="checkbox" id="disabletrailer"> <span class="slider"></span> Disable Trailer Autoplay <span class="new">New <span class="tooltip">Just Released/Updated</span> </span> <span class="help-icon" data-help="Disable Trailer Autoplay">?</span> </label> <label class="toggle-slider section-hover"> <input type="checkbox" id="removeads"> <span class="slider"></span> Remove All Roblox Ads <span class="help-icon" data-help="Remove All Roblox Ads">?</span> </label> <label class="toggle-slider section-hover"> <input type="checkbox" id="quicknav"> <span class="slider"></span> <!-- lmao dont question it --> Quick Navigation <button id="edit-quicknav-btn" class="edit-button" type="button" style="display: none;">Edit</button> <span class="help-icon" data-help="Quick Navigation">?</span> </label> </div> `; } if (section === "advanced") { return ` <div class="advanced-section"> <span class="warning_advanced">For Experienced Users Only🧠🙃</span> <div class="section-separator"></div> <label class="toggle-slider section-hover"> <input type="checkbox" id="enableLogs"> <span class="slider"></span> Enable Console Logs <span class="help-icon" data-help="Enable Console Logs">?</span> </label> <label class="toggle-slider section-hover"> <input type="checkbox" id="togglefilterserversbutton"> <span class="slider"></span> Enable Server Filters <span class="help-icon" data-help="Enable Server Filters">?</span> </label> <label class="toggle-slider section-hover"> <input type="checkbox" id="toggleserverhopbutton"> <span class="slider"></span> Enable Server Hop Button <span class="help-icon" data-help="Enable Server Hop Button">?</span> </label> <label class="toggle-slider section-hover new_label"> <input type="checkbox" id="enablenotifications"> <span class="slider"></span> Enable Notifications <span class="new">New <span class="tooltip">Just Released/Updated</span> </span> <span class="help-icon" data-help="Enable Notifications">?</span> </label> <div class="location-settings section-hover"> <div class="setting-header"> <span>Set Default Location Mode</span> <span class="help-icon" data-help="Set default location">?</span> </div> <select id="prioritylocation-select"> <option value="manual" style="color: rgb(255, 40, 40);">Manual</option> <option value="automatic" style="color: rgb(255, 40, 40);">Automatic</option> </select> <div id="location-hint"> <strong>Manual:</strong> Set your location manually below <strong>Automatic:</strong> Auto detect your device's location </div> <div id="manual-coordinates" style="margin-top: 15px; display: none;"> <div class="coordinates-inputs" style="display: flex; gap: 10px; margin-bottom: 12px;"> <div style="flex: 1;"> <label for="latitude" style="display: block; margin-bottom: 8px; font-size: 14px;">Latitude</label> <input type="text" id="latitude" placeholder="e.g. 34.0549" style="width: 100%; padding: 10px 12px; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.05);; background: rgba(255,255,255,0.05); color: #e0e0e0;"> </div> <div style="flex: 1;"> <label for="longitude" style="display: block; margin-bottom: 8px; font-size: 14px;">Longitude</label> <input type="text" id="longitude" placeholder="e.g. -118.2426" style="width: 100%; padding: 10px 12px; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.05);; background: rgba(255,255,255,0.05); color: #e0e0e0;"> </div> </div> <button id="save-coordinates" class="edit-nav-button" style="width: 100%; margin-top: 8px;"> Save Coordinates </button> <div class="hint-text" style="margin-top: 12px; font-size: 13px; color: #a0a0a0;"> Enter your location's decimal coordinates, or if you're not comfortable sharing them, use the nearest Roblox server coordinates (e.g., Los Angeles: 34.0549, -118.2426). </div> </div> </div> </div> `; } if (section === "extras") { return ` <div class="extras-section"> <span class="extras_section">Small features that might be useful!</span> <label class="toggle-slider section-hover"> <input type="checkbox" id="gamequalityfilter"> <span class="slider"></span> Game Quality Filter <button id="edit-gamequality-btn" class="edit-button" type="button" style="display: none;">Edit</button> <span class="help-icon" data-help="Game Quality Filter">?</span> </label> <label class="toggle-slider section-hover new_label"> <input type="checkbox" id="mutualfriends"> <span class="slider"></span> Mutual Friends <span class="new">New <span class="tooltip">Just Released/Updated</span> </span> <span class="help-icon" data-help="Enable Mutual Friends">?</span> </label> <label class="toggle-slider section-hover new_label"> <input type="checkbox" id="disablechat"> <span class="slider"></span> Disable Chat <span class="new">New <span class="tooltip">Just Released/Updated</span> </span> <span class="help-icon" data-help="Disable Chat">?</span> </label> <label class="toggle-slider section-hover new_label"> <input type="checkbox" id="quicklaunchgames"> <span class="slider"></span> Quick Launch Games <span class="new">New <span class="tooltip">Just Released/Updated</span> </span> <span class="help-icon" data-help="Quick Launch Games">?</span> </label> </div> `; } if (section === "about") { return ` <div class="about-section"> <h3 class="grayish-center">Credits</h3> <p>This project was created by:</p> <ul> <li><strong>Developer:</strong> <a href="https://www.roblox.com/users/545334824/profile" target="_blank">Oqarshi</a></li> <li><strong>Rolocate Source Code:</strong> <a href="https://greasyfork.org/en/scripts/523727-rolocate/code" target="_blank">GreasyFork</a></li> <li><strong>Invite & FAQ Source Code:</strong> <a href="https://github.com/Oqarshi/Invite" target="_blank">GitHub</a></li> <li><strong>Official Website:</strong> <a href="https://oqarshi.github.io/Invite/rolocate/index.html" target="_blank">RoLocate Website</a></li> <li><strong>Suggest or Report Issues:</strong> <a href="https://greasyfork.org/en/scripts/523727-rolocate/feedback" target="_blank">Submit Feedback</a></li> <li><strong>Inspiration:</strong> <a href="https://chromewebstore.google.com/detail/btroblox-making-roblox-be/hbkpclpemjeibhioopcebchdmohaieln" target="_blank">Btroblox Team</a></li> </ul> </div> `; } if (section === "help") { return ` <div class="help-section"> <div class="section-separator"></div> <h3 class="grayish-center">⚙️ General Tab</h3> <ul> <li id="help-Smart Search"><strong>SmartSearch:</strong> <span>Improves the Roblox website’s search bar by enabling instant searches for games, users, and groups.</span></li> <li id="help-Auto Server Regions"><strong>Auto Server Regions:</strong> <span>Replaces Roblox's 8 default servers with at least 8 servers, providing detailed info such as location and ping.</span></li> <li id="help-Fast Server Search"><strong>Fast Server Search:</strong> <span>Boosts server search speed up to 100x (experimental). Replaces player thumbnails with Builderman/Roblox icons to bypass rate limits.</span></li> <li id="help-Invert Player Count"><strong>Invert Player Count:</strong> <span>For server regions: shows low-player servers when enabled, high-player servers when disabled. You can also control this on the Roblox server popup.</span></li> <li id="help-Recent Servers"><strong>Recent Servers:</strong> <span>Shows the most recent servers you have joined in the past 3 days.</span></li> </ul> <div class="section-separator"></div> <h3 class="grayish-center">🎨 Appearance Tab</h3> <ul> <li id="help-Show Old Greeting"><strong>Show Old Greeting:</strong> <span>Shows the old greeting Roblox had on their home page.</span></li> <li id="help-Disable Trailer Autoplay"><strong>Disable Trailer Autoplay:</strong> <span>Prevents trailers from autoplaying on Roblox game pages.</span></li> <li id="help-Remove All Roblox Ads"><strong>Remove All Roblox Ads:</strong> <span>Blocks most ads on the Roblox site.</span></li> <li id="help-Quick Navigation"><strong>Quick Nav:</strong> <span>Ability to add quick navigations to the leftside panel of the Roblox page.</span></li> </ul> <div class="section-separator"></div> <h3 class="grayish-center">🚀 Advanced Tab</h3> <ul> <li id="help-Enable Console Logs"><strong>Enable Console Logs:</strong> <span>Enables console.log messages from the script.</span></li> <li id="help-Enable Server Filters"><strong>Enable Server Filters:</strong> <span>Enables server filter features on the game page.</span></li> <li id="help-Enable Server Hop Button"><strong>Enable Server Hop Button:</strong> <span>Enables server hop feature on the game page.</span></li> <li id="help-Enable Notifications"><strong>Enable Notifications:</strong> <span>Enables helpful notifications from the script.</span></li> <li id="help-Set default location"><strong>Set default location:</strong> <span>Enables the user to set a default location for Roblox server regions. Turn this on if the script cannot automatically detect your location.</span></li> </ul> <h3 class="grayish-center">✨ Extra Tab</h3> <ul> <li id="help-Game Quality Filter"><strong>Game Quality Filter:</strong> <span>Removes games from the charts/discover page based on your settings.</span></li> <li id="help-Enable Mutual Friends"><strong>Mutual Friends:</strong> <span>Displays friends you share with a certain person on their profile page.</span></li> <li id="help-Disable Chat"><strong>Disable Chat:</strong> <span>Disables the chat feature on the roblox website.</span></li> <li id="help-Quick Launch Games"><strong>Quick Launch Games:</strong> <span>Adds the ability to quickly launch your favorite games from the homepage.</span></li> </ul> <div class="section-separator"></div> <h3 class="grayish-center">Need more help?</h3> <li> You can visit the <a href="https://oqarshi.github.io/Invite/rolocate/docs/#troubleshooting" target="_blank" class="about-link"> troubleshooting </a> or create an issue on <a href="https://greasyfork.org/en/scripts/523727-rolocate/feedback" target="_blank" class="about-link"> greasyfork </a> for more assistance. </li> </div> `; } // General tab (default) return ` <div class="general-section"> <label class="toggle-slider section-hover new_label experiment_label"> <input type="checkbox" id="smartsearch"> <span class="slider"></span> SmartSearch <span class="new">New <span class="tooltip">Just Released/Updated</span> </span> <span class="experimental">Experimental <span class="tooltip">Still being tested</span> </span> <span class="help-icon" data-help="Smart Search">?</span> </label> <label class="toggle-slider section-hover"> <input type="checkbox" id="AutoRunServerRegions"> <span class="slider"></span> Auto Server Regions <span class="help-icon" data-help="Auto Server Regions">?</span> </label> <label class="toggle-slider section-hover experiment_label"> <input type="checkbox" id="fastservers"> <span class="slider"></span> Fast Server Search <span class="experimental">Experimental <span class="tooltip">Still being tested</span> </span> <span class="help-icon" data-help="Fast Server Search">?</span> </label> <label class="toggle-slider section-hover"> <input type="checkbox" id="invertplayercount"> <span class="slider"></span> Invert Player Count <span class="help-icon" data-help="Invert Player Count">?</span> </label> <label class="toggle-slider section-hover"> <input type="checkbox" id="togglerecentserverbutton"> <span class="slider"></span> Recent Servers <span class="help-icon" data-help="Recent Servers">?</span> </label> </div> `; } /******************************************************* name of function: openSettingsMenu description: opens setting menu and makes it look good *******************************************************/ function openSettingsMenu() { if (document.getElementById("userscript-settings-menu")) return; // storage make go uyea initializeLocalStorage(); initializeCoordinatesStorage(); const overlay = document.createElement("div"); overlay.id = "userscript-settings-menu"; overlay.innerHTML = ` <div class="settings-container"> <button id="close-settings">✖</button> <div class="settings-sidebar"> <h2>RoLocate</h2> <ul> <li class="active" data-section="home">🏠 Home</li> <li data-section="general">⚙️ General</li> <li data-section="appearance">🎨 Appearance</li> <li data-section="advanced">🚀 Advanced</li> <li data-section="extras">✨ Extras</li> <li data-section="help">📙 Help</li> <li data-section="about">ℹ️ About</li> </ul> </div> <div class="settings-content"> <h2 id="settings-title">Home</h2> <div id="settings-body" class="animated-content">${getSettingsContent("home")}</div> </div> </div> `; document.body.appendChild(overlay); // put css in const style = document.createElement("style"); style.textContent = ` .grayish-center { color: white; font-weight: bold; text-align: center; position: relative; display: inline-block; font-size: 18px !important; /* idk whats overriding this but screw finding that */ } .grayish-center::after { content: ""; display: block; margin: 4px auto 0; width: 50%; border-bottom: 2px solid #888888; opacity: 0.6; border-radius: 2px; } li a.about-link { position: relative !important; font-weight: bold !important; color: #dc2626 !important; text-decoration: none !important; cursor: pointer !important; transition: color 0.2s ease !important; } li a.about-link::after { content: '' !important; position: absolute !important; left: 0 !important; bottom: -2px !important; height: 2px !important; width: 100% !important; background-color: #dc2626 !important; transform: scaleX(0) !important; transform-origin: left !important; transition: transform 0.3s ease !important; } li a.about-link:hover { color: #b91c1c !important; } li a.about-link:hover::after { transform: scaleX(1) !important; } .about-section ul li a { position: relative; font-weight: bold; color: #dc2626; /* red-600 */ text-decoration: none; cursor: pointer; transition: color 0.2s ease; } .about-section ul li a::after { content: ''; position: absolute; left: 0; bottom: -2px; height: 2px; width: 100%; background-color: #dc2626; /* red underline */ transform: scaleX(0); transform-origin: left; transition: transform 0.3s ease; } .about-section ul li a:hover { color: #b91c1c; /* darker red on hover */ } .about-section ul li a:hover::after { transform: scaleX(1); } .license-note { font-size: 0.65em; color: #999; margin-top: 12px; font-style: italic; text-align: center; } .edit-button { margin-left: auto; padding: 2px 8px; font-size: 12px; border: none; border-radius: 6px; background: linear-gradient(145deg, #3a3a3a, #2c2c2c); color: #f0f0f0; cursor: pointer; font-weight: 500; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 2px 4px rgba(0, 0, 0, 0.25); transition: all 0.2s ease; } .edit-button:hover { background: linear-gradient(145deg, #4a4a4a, #343434); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 3px 6px rgba(0, 0, 0, 0.35); transform: translateY(-0.5px); } .help-icon { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; background: rgba(220, 53, 69, 0.15); border-radius: 50%; font-size: 12px; font-weight: 600; color: #e02d3c; cursor: pointer; transition: all 0.2s ease; margin-left: auto; /* Pushes it to the right */ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); position: relative; border: 1px solid rgba(220, 53, 69, 0.2); } .help-icon:hover { background: rgba(220, 53, 69, 0.25); transform: translateY(-1px); box-shadow: 0 3px 5px rgba(0, 0, 0, 0.15); cursor: pointer; } /* Add tooltip on hover */ .help-icon::after { content: "Click for help"; position: absolute; bottom: -30px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.8); color: white; padding: 4px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap; opacity: 0; visibility: hidden; transition: all 0.2s ease; pointer-events: none; } .help-icon:hover::after { opacity: 1; visibility: visible; } .help-icon:active { transform: translateY(0); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.4); } 70% { box-shadow: 0 0 0 6px rgba(220, 53, 69, 0); } 100% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0); } } .help-icon.attention { animation: pulse 2s infinite; } .highlight-help-item { animation: highlight 1.5s ease; background: rgba(76, 175, 80, 0.1); /* Green highlight */ border-left: 3px solid #4CAF50; /* Green border */ } @keyframes highlight { 0% { background: rgba(76, 175, 80, 0.3); } /* Green start */ 100% { background: rgba(76, 175, 80, 0.1); } /* Green end */ } .new_label .new { margin-left: 8px; color: #32cd32; /* LimeGreen */ font-size: 12px; font-weight: bold; background-color: rgba(50, 205, 50, 0.1); /* soft green background */ padding: 2px 6px; border-radius: 3px; position: relative; z-index: 10001; } .new_label .tooltip { visibility: hidden; background-color: rgba(0, 0, 0, 0.75); color: #fff; font-size: 12px; padding: 6px; border-radius: 5px; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); white-space: nowrap; z-index: 10001; opacity: 0; transition: opacity 0.3s; } .new_label .new:hover .tooltip { visibility: visible; opacity: 1; z-index: 10001; } .experiment_label .experimental { margin-left: 8px; color: gold; font-size: 12px; font-weight: bold; background-color: rgba(255, 215, 0, 0.1); padding: 2px 6px; border-radius: 3px; position: relative; /* Needed for positioning the tooltip */ z-index: 10001; } .experiment_label .tooltip { visibility: hidden; background-color: rgba(0, 0, 0, 0.7); color: #fff; font-size: 12px; padding: 6px; border-radius: 5px; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); white-space: nowrap; z-index: 10001; opacity: 0; transition: opacity 0.3s; } .experiment_label .experimental:hover .tooltip { visibility: visible; opacity: 1; z-index: 10001; } @keyframes fadeIn { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } } @keyframes fadeOut { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.96); } } @keyframes sectionFade { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } } @keyframes slideIn { from { transform: translateX(-20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } #userscript-settings-menu { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 10000; animation: fadeIn 0.7s cubic-bezier(0.19, 1, 0.22, 1); } .settings-container { display: flex; position: relative; width: 580px; /* Reduced from 680px */ height: 440px; /* Reduced from 480px */ background: linear-gradient(145deg, #1a1a1a, #232323); border-radius: 12px; /* Slightly smaller radius */ overflow: hidden; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.7); font-family: 'Inter', 'Segoe UI', Arial, sans-serif; border: 1px solid rgba(255, 255, 255, 0.05); } #close-settings { position: absolute; top: 12px; /* Reduced from 16px */ right: 12px; /* Reduced from 16px */ background: transparent; border: none; color: #c0c0c0; font-size: 20px; /* Reduced from 22px */ cursor: pointer; z-index: 10001; transition: all 0.5s ease; width: 30px; /* Reduced from 34px */ height: 30px; /* Reduced from 34px */ border-radius: 50%; display: flex; align-items: center; justify-content: center; } #close-settings:hover { color: #ff3b47; background: rgba(255, 59, 71, 0.1); transform: rotate(90deg); } .settings-sidebar { width: 32%; /* Reduced from 35% */ background: #272727; padding: 18px 12px; /* Reduced from 24px 15px */ color: white; display: flex; flex-direction: column; align-items: center; box-shadow: 6px 0 12px -6px rgba(0,0,0,0.3); position: relative; overflow: hidden; } .settings-sidebar h2 { margin-bottom: 16px; /* Reduced from 20px */ font-weight: 600; font-size: 22px; /* Reduced from 24px */ text-shadow: 0 1px 3px rgba(0,0,0,0.5); text-decoration: none; position: relative; text-align: center; } .settings-sidebar h2::after { content: ""; position: absolute; left: 50%; transform: translateX(-50%); bottom: -6px; /* Reduced from -8px */ width: 36px; /* Reduced from 40px */ height: 3px; background: white; border-radius: 2px; } .settings-sidebar ul { list-style: none; padding: 0; width: 100%; margin-top: 5px; /* Reduced from 10px */ } .settings-sidebar li { padding: 10px 12px; /* Reduced from 14px */ margin: 6px 0; /* Reduced from 8px 0 */ text-align: left; cursor: pointer; transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1); border-radius: 8px; font-weight: 500; font-size: 17px; /* increased from 15px */ position: relative; animation: slideIn 0.5s cubic-bezier(0.19, 1, 0.22, 1); animation-fill-mode: both; display: flex; align-items: center; } .settings-sidebar li:hover { background: #444; transform: translateX(5px); } .settings-sidebar .active { background: #444; color: white; transform: translateX(0); } .settings-sidebar .active:hover { transform: translateX(0); } .settings-sidebar li:hover::before { height: 100%; } .settings-sidebar .active::before { background: #dc3545; } /* Custom Scrollbar */ .settings-content { flex: 1; padding: 24px; /* Reduced from 32px */ color: white; text-align: center; max-height: 100%; overflow-y: auto; scrollbar-width: thin; scrollbar-color: darkgreen black; background: #1e1e1e; position: relative; } /* Webkit (Chrome, Safari) Scrollbar */ .settings-content::-webkit-scrollbar { width: 6px; /* Reduced from 8px */ } .settings-content::-webkit-scrollbar-track { background: #333; border-radius: 3px; /* Reduced from 4px */ } .settings-content::-webkit-scrollbar-thumb { background: linear-gradient(180deg, #dc3545, #b02a37); border-radius: 3px; /* Reduced from 4px */ } .settings-content::-webkit-scrollbar-thumb:hover { background: linear-gradient(180deg, #ff3b47, #dc3545); } .settings-content h2 { margin-bottom: 24px; /* Reduced from 30px */ font-weight: 600; font-size: 22px; /* Reduced from 24px */ color: white; text-shadow: 0 1px 3px rgba(0,0,0,0.4); letter-spacing: 0.5px; position: relative; display: inline-block; padding-bottom: 6px; /* Reduced from 8px */ } .settings-content h2::after { content: ""; position: absolute; bottom: 0; left: 0; width: 100%; height: 2px; background: white; border-radius: 2px; } .settings-content div { animation: sectionFade 0.7s cubic-bezier(0.19, 1, 0.22, 1); } /* Toggle Slider Styles */ .toggle-slider { display: flex; align-items: center; margin: 12px 0; /* Reduced from 16px 0 */ cursor: pointer; padding: 8px 14px; /* Reduced from 10px 16px */ background: rgba(255, 255, 255, 0.03); border-radius: 6px; /* Reduced from 8px */ transition: all 0.5s ease; user-select: none; border: 1px solid rgba(255, 255, 255, 0.05); } .toggle-slider:hover { background: rgba(255, 255, 255, 0.05); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); transform: translateY(-2px); } .toggle-slider input { display: none; } .toggle-slider .slider { position: relative; display: inline-block; width: 42px; /* Reduced from 48px */ height: 22px; /* Reduced from 24px */ background-color: rgba(255, 255, 255, 0.2); border-radius: 22px; margin-right: 12px; /* Reduced from 14px */ transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1); box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2); } .toggle-slider .slider::before { content: ""; position: absolute; height: 16px; /* Reduced from 18px */ width: 16px; /* Reduced from 18px */ left: 3px; bottom: 3px; background-color: white; border-radius: 50%; transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); } .toggle-slider input:checked + .slider { background-color: #4CAF50; box-shadow: 0 0 0 1px rgba(220, 53, 69, 0.05), inset 0 1px 3px rgba(0, 0, 0, 0.2); } .toggle-slider input:checked + .slider::before { transform: translateX(20px); /* Reduced from 24px */ } .toggle-slider input:checked + .slider::after { opacity: 1; } .rolocate-logo { width: 90px !important; /* Reduced from 110px */ height: 90px !important; /* Reduced from 110px */ object-fit: contain; border-radius: 14px; /* Reduced from 16px */ display: block; margin: 0 auto 16px auto; /* Reduced from 20px */ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4); transition: all 0.5s ease; border: 2px solid rgba(220, 53, 69, 0.4); } .rolocate-logo:hover { transform: scale(1.05); } .version { font-size: 13px; /* Reduced from 14px */ color: #aaa; margin-bottom: 24px; /* Reduced from 30px */ display: inline-block; padding: 5px 14px; /* Reduced from 6px 16px */ background: rgba(220, 53, 69, 0.1); border-radius: 18px; /* Reduced from 20px */ border: 1px solid rgba(220, 53, 69, 0.2); } .settings-content ul { text-align: left; list-style-type: none; padding: 0; margin-top: 16px; /* Reduced from 20px */ } .settings-content ul li { margin: 12px 0; /* Reduced from 16px 0 */ padding: 10px 14px; /* Reduced from 12px 16px */ background: rgba(255, 255, 255, 0.03); border-radius: 6px; /* Reduced from 8px */ transition: all 0.4s ease; } .settings-content ul li:hover { background: rgba(255, 255, 255, 0.05); border-left: 3px solid #4CAF50; transform: translateX(5px); } .settings-content ul li strong { color: #4CAF50; } .warning_advanced { font-size: 14px; color: #ff3b47; font-weight: bold; padding: 8px 14px; background: rgba(220, 53, 69, 0.1); border-radius: 6px; margin-bottom: 16px; display: inline-block; border: 1px solid rgba(220, 53, 69, 0.2); box-shadow: 0 0 6px rgba(220, 53, 69, 0.3); transition: box-shadow 0.3s ease; } .warning_advanced:hover { box-shadow: 0 0 12px rgba(220, 53, 69, 0.6); } .extras_section { font-size: 14px; color: #0d6efd; /* vibrant blue */ font-weight: bold; padding: 8px 14px; background: rgba(13, 110, 253, 0.1); border-radius: 6px; margin-bottom: 16px; display: inline-block; border: 1px solid rgba(13, 110, 253, 0.3); box-shadow: 0 0 6px rgba(13, 110, 253, 0.3); transition: box-shadow 0.3s ease; } .extras_section:hover { box-shadow: 0 0 12px rgba(13, 110, 253, 0.6); } .average_text { font-size: 16px; /* Reduced from 18px */ color: #e0e0e0; font-weight: 500; margin-top: 12px; /* Reduced from 15px */ line-height: 1.5; letter-spacing: 0.3px; background: linear-gradient(90deg, #ff3b47, #ff6b74); -webkit-background-clip: text; -webkit-text-fill-color: transparent; display: inline-block; } .edit-nav-button { padding: 6px 14px; /* Reduced from 8px 16px */ background: #4CAF50; color: white; border: none; border-radius: 6px; /* Reduced from 8px */ cursor: pointer; font-family: 'Inter', 'Helvetica', sans-serif; font-size: 12px; /* Reduced from 13px */ font-weight: 600; letter-spacing: 0.5px; text-transform: uppercase; transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1); height: auto; line-height: 1.5; position: relative; overflow: hidden; } .edit-nav-button:hover { transform: translateY(-3px); background: linear-gradient(135deg, #1e8449 0%, #196f3d 100%); } .edit-nav-button:hover::before { left: 100%; } .edit-nav-button:active { background: linear-gradient(135deg, #1e8449 0%, #196f3d 100%); transform: translateY(1px); } /* Dropdown styling */ 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,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="%23dc3545" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>'); background-repeat: no-repeat; background-position: right 14px center; background-size: 14px; transition: all 0.5s ease; cursor: pointer; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); border-color: rgba(255, 255, 255, 0.05); } /* Dropdown hint styling */ #location-hint { margin-top: 10px; /* Reduced from 12px */ font-size: 12px; /* Reduced from 13px */ color: #c0c0c0; background: rgba(255, 255, 255, 0.05); border-radius: 6px; /* Reduced from 8px */ padding: 10px 14px; /* Reduced from 12px 16px */ border: 1px solid rgba(255, 255, 255, 0.05); line-height: 1.6; transition: all 0.5s ease; } /* Section separator */ .section-separator { width: 100%; height: 1px; background: linear-gradient(90deg, transparent, #272727, transparent); margin: 24px 0; /* Reduced from 30px 0 */ } /* Help section styles */ .help-section h3, .about-section h3 { color: white; margin-top: 20px; /* Reduced from 25px */ margin-bottom: 12px; /* Reduced from 15px */ font-size: 16px; /* Reduced from 18px */ text-align: left; } /* Hint text styling */ .hint-text { font-size: 13px; /* Reduced from 14px */ color: #a0a0a0; margin-top: 6px; /* Reduced from 8px */ margin-left: 16px; /* Reduced from 20px */ text-align: left; } /* Location settings styling */ .location-settings { background: rgba(255, 255, 255, 0.03); border-radius: 6px; /* Reduced from 8px */ padding: 14px; /* Reduced from 16px */ margin-top: 16px; /* Reduced from 20px */ border: 1px solid rgba(255, 255, 255, 0.05); transition: all 0.5s ease; } .setting-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; /* Reduced from 12px */ } .setting-header span { font-size: 14px; /* Reduced from 15px */ font-weight: 500; } .help-icon { display: inline-flex; align-items: center; justify-content: center; width: 18px; /* Reduced from 20px */ height: 18px; /* Reduced from 20px */ background: rgba(220, 53, 69, 0.2); border-radius: 50%; font-size: 11px; /* Reduced from 12px */ color: #ff3b47; cursor: help; transition: all 0.5s ease; } /* Manual coordinates input styling */ #manual-coordinates { margin-top: 12px !important; /* Reduced from 15px */ } .coordinates-inputs { gap: 8px !important; /* Reduced from 10px */ margin-bottom: 10px !important; /* Reduced from 12px */ } #manual-coordinates input { padding: 8px 10px !important; /* Reduced from 10px 12px */ border-radius: 6px !important; /* Reduced from 8px */ font-size: 13px !important; /* Reduced from default */ } #manual-coordinates label { margin-bottom: 6px !important; /* Reduced from 8px */ font-size: 13px !important; /* Reduced from 14px */ } #save-coordinates { margin-top: 6px !important; /* Reduced from 8px */ } /* Animated content */ .animated-content { animation: sectionFade 0.7s cubic-bezier(0.19, 1, 0.22, 1); } `; document.head.appendChild(style); // hopefully this works document.querySelectorAll(".settings-sidebar li").forEach((li, index) => { // aniamtions stuff li.style.animationDelay = `${0.05 * (index + 1)}s`; li.addEventListener("click", function() { const currentActive = document.querySelector(".settings-sidebar .active"); if (currentActive) currentActive.classList.remove("active"); this.classList.add("active"); const section = this.getAttribute("data-section"); const settingsBody = document.getElementById("settings-body"); const settingsTitle = document.getElementById("settings-title"); // aniamtions stuff settingsBody.style.opacity = "0"; settingsBody.style.transform = "translateY(10px)"; settingsTitle.style.opacity = "0"; settingsTitle.style.transform = "translateY(10px)"; setTimeout(() => { // aniamtions stuff settingsTitle.textContent = section.charAt(0).toUpperCase() + section.slice(1); settingsBody.innerHTML = getSettingsContent(section); // quick nav stuff if (section === "appearance") { const quickNavCheckbox = document.getElementById("quicknav"); const editButton = document.getElementById("edit-quicknav-btn"); if (quickNavCheckbox && editButton) { // Set initial display based on localStorage editButton.style.display = localStorage.getItem("ROLOCATE_quicknav") === "true" ? "block" : "none"; // Update localStorage and edit button visibility when checkbox changes quickNavCheckbox.addEventListener("change", function() { const isEnabled = this.checked; localStorage.setItem("ROLOCATE_quicknav", isEnabled); editButton.style.display = isEnabled ? "block" : "none"; }); } } if (section === "extras") { const gameQualityCheckbox = document.getElementById("gamequalityfilter"); const editButton = document.getElementById("edit-gamequality-btn"); if (gameQualityCheckbox && editButton) { // Set visibility on load editButton.style.display = localStorage.getItem("ROLOCATE_gamequalityfilter") === "true" ? "block" : "none"; // Toggle visibility when the checkbox changes gameQualityCheckbox.addEventListener("change", function() { const isEnabled = this.checked; editButton.style.display = isEnabled ? "block" : "none"; }); } } settingsBody.style.transition = "all 0.4s cubic-bezier(0.19, 1, 0.22, 1)"; settingsTitle.style.transition = "all 0.4s cubic-bezier(0.19, 1, 0.22, 1)"; void settingsBody.offsetWidth; void settingsTitle.offsetWidth; settingsBody.style.opacity = "1"; settingsBody.style.transform = "translateY(0)"; settingsTitle.style.opacity = "1"; settingsTitle.style.transform = "translateY(0)"; applyStoredSettings(); }, 200); }); }); // Close button with enhanced animation document.getElementById("close-settings").addEventListener("click", function() { // Check if manual mode is selected with empty coordinates const priorityLocation = localStorage.getItem("ROLOCATE_prioritylocation"); if (priorityLocation === "manual") { try { const coords = JSON.parse(GM_getValue("ROLOCATE_coordinates", '{"lat":"","lng":""}')); if (!coords.lat || !coords.lng) { notifications('Please set the latitude and longitude values for the manual location, or set it to automatic.', 'error', '⚠️', 8000); return; // Prevent closing } } catch (e) { ConsoleLogEnabled("Error checking coordinates:", e); notifications('Error checking location settings', 'error', '⚠️', 8000); return; // Prevent closing } } // Proceed with closing if validation passes const menu = document.getElementById("userscript-settings-menu"); menu.style.animation = "fadeOut 0.4s cubic-bezier(0.19, 1, 0.22, 1) forwards"; // Add rotation to close button when closing this.style.transform = "rotate(90deg)"; setTimeout(() => menu.remove(), 400); }); // Apply stored settings immediately when opened applyStoredSettings(); // Add ripple effect to buttons const buttons = document.querySelectorAll(".edit-nav-button, .settings-button"); buttons.forEach(button => { button.addEventListener("mousedown", function(e) { const ripple = document.createElement("span"); const rect = this.getBoundingClientRect(); const size = Math.max(rect.width, rect.height); const x = e.clientX - rect.left - size / 2; const y = e.clientY - rect.top - size / 2; ripple.style.cssText = ` position: absolute; background: rgba(255,255,255,0.4); border-radius: 50%; pointer-events: none; width: ${size}px; height: ${size}px; top: ${y}px; left: ${x}px; transform: scale(0); transition: transform 0.6s, opacity 0.6s; `; this.appendChild(ripple); setTimeout(() => { ripple.style.transform = "scale(2)"; ripple.style.opacity = "0"; setTimeout(() => ripple.remove(), 600); }, 10); }); }); // Handle help icon clicks document.addEventListener('click', function(e) { if (e.target.classList.contains('help-icon')) { // Prevent the event from bubbling up to the toggle button e.stopPropagation(); e.preventDefault(); const helpItem = e.target.getAttribute('data-help'); if (helpItem) { // Switch to help tab const helpTab = document.querySelector('.settings-sidebar li[data-section="help"]'); if (helpTab) helpTab.click(); // Scroll to the corresponding help item after a short delay setTimeout(() => { const helpElement = document.getElementById(`help-${helpItem}`); if (helpElement) { helpElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); helpElement.classList.add('highlight-help-item'); setTimeout(() => { helpElement.classList.remove('highlight-help-item'); }, 1500); } }, 300); } } }); } /******************************************************* name of function: showQuickNavPopup description: quick nav popup menu *******************************************************/ function showQuickNavPopup() { // Remove existing quick nav if it exists const existingNav = document.getElementById("premium-quick-nav"); if (existingNav) existingNav.remove(); // POPUP CREATION // Create overlay const overlay = document.createElement("div"); overlay.id = "quicknav-overlay"; overlay.style.position = "fixed"; overlay.style.top = "0"; overlay.style.left = "0"; overlay.style.width = "100%"; overlay.style.height = "100%"; overlay.style.backgroundColor = "rgba(0,0,0,0)"; // Darker overlay for dark mode overlay.style.backdropFilter = "blur(1px)"; overlay.style.zIndex = "10000"; overlay.style.opacity = "0"; overlay.style.transition = "opacity 0.3s ease"; // Create popup const popup = document.createElement("div"); popup.id = "premium-quick-nav-popup"; popup.style.position = "fixed"; popup.style.top = "50%"; popup.style.left = "50%"; popup.style.transform = "translate(-50%, -50%) scale(0.95)"; popup.style.opacity = "0"; popup.style.background = "linear-gradient(145deg, #0a0a0a, #121212)"; // Darker background for dark mode popup.style.color = "white"; popup.style.padding = "32px"; popup.style.borderRadius = "16px"; popup.style.boxShadow = "0 20px 40px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.05)"; popup.style.zIndex = "10001"; popup.style.width = "600px"; popup.style.maxWidth = "90%"; popup.style.maxHeight = "85vh"; popup.style.overflowY = "auto"; popup.style.transition = "transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.4s ease"; // Get saved quick navs (if any) const saved = JSON.parse(localStorage.getItem("ROLOCATE_quicknav_settings") || "[]"); // Build header const header = ` <div style="position: relative; margin-bottom: 24px; text-align: center;"> <h2 style="margin: 0 0 8px; font-size: 28px; font-weight: 600; background: linear-gradient(90deg, #4CAF50, #8BC34A); -webkit-background-clip: text; background-clip: text; color: transparent;">Quick Navigation</h2> <p style="margin: 0; font-size: 16px; color: #a0a0a0; font-weight: 300;">Configure up to 9 custom navigation shortcuts</p> <div style="width: 60px; height: 4px; background: linear-gradient(90deg, #4CAF50, #8BC34A); margin: 16px auto; border-radius: 2px;"></div> </div> `; // Build inputs for 9 links in a 3x3 grid const inputsGrid = ` <div class="quicknav-inputs-grid" style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px;"> ${Array.from({length: 9}, (_, i) => ` <div class="quicknav-input-group" style="background: rgba(255,255,255,0.03); padding: 16px; border-radius: 12px; border: 1px solid rgba(255,255,255,0.05);"> <p style="font-weight: 500; font-size: 14px; margin: 0 0 8px; color: #A5D6A7;">${i + 1}</p> <input type="text" id="quicknav-name-${i}" placeholder="Name" value="${saved[i]?.name || ""}" style="width: 100%; padding: 10px 12px; margin-bottom: 8px; border-radius: 8px; border: none; background: rgba(255,255,255,0.05); color: white; font-size: 14px; transition: all 0.2s;"> <input type="text" id="quicknav-link-${i}" placeholder="URL" value="${saved[i]?.link || ""}" style="width: 100%; padding: 10px 12px; border-radius: 8px; border: none; background: rgba(255,255,255,0.05); color: white; font-size: 14px; transition: all 0.2s;"> </div> `).join("")} </div> `; // Build footer with buttons const footer = ` <div style="display: flex; justify-content: flex-end; gap: 12px;"> <button id="cancel-quicknav" style="background: transparent; color: #a0a0a0; border: 1px solid rgba(255,255,255,0.1); padding: 12px 20px; border-radius: 8px; cursor: pointer; font-weight: 500; transition: all 0.2s;"> Cancel </button> <button id="save-quicknav" style="background: linear-gradient(90deg, #4CAF50, #388E3C); color: white; border: none; padding: 12px 24px; border-radius: 8px; cursor: pointer; font-weight: 500; box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3); transition: all 0.2s;"> Save Changes </button> </div> `; // Combine all sections popup.innerHTML = header + inputsGrid + footer; // Add elements to DOM document.body.appendChild(overlay); document.body.appendChild(popup); // POPUP EVENTS // Add input hover and focus effects popup.querySelectorAll('input').forEach(input => { input.addEventListener('focus', () => { input.style.background = 'rgba(255,255,255,0.1)'; input.style.boxShadow = '0 0 0 2px rgba(76, 175, 80, 0.4)'; }); input.addEventListener('blur', () => { input.style.background = 'rgba(255,255,255,0.05)'; input.style.boxShadow = 'none'; }); input.addEventListener('mouseover', () => { if (document.activeElement !== input) { input.style.background = 'rgba(255,255,255,0.08)'; } }); input.addEventListener('mouseout', () => { if (document.activeElement !== input) { input.style.background = 'rgba(255,255,255,0.05)'; } }); }); // Add button hover effects const saveBtn = popup.querySelector('#save-quicknav'); saveBtn.addEventListener('mouseover', () => { saveBtn.style.background = 'linear-gradient(90deg, #66BB6A, #4CAF50)'; saveBtn.style.boxShadow = '0 4px 15px rgba(76, 175, 80, 0.4)'; saveBtn.style.transform = 'translateY(-1px)'; }); saveBtn.addEventListener('mouseout', () => { saveBtn.style.background = 'linear-gradient(90deg, #4CAF50, #388E3C)'; saveBtn.style.boxShadow = '0 4px 12px rgba(76, 175, 80, 0.3)'; saveBtn.style.transform = 'translateY(0)'; }); const cancelBtn = popup.querySelector('#cancel-quicknav'); cancelBtn.addEventListener('mouseover', () => { cancelBtn.style.background = 'rgba(255,255,255,0.05)'; }); cancelBtn.addEventListener('mouseout', () => { cancelBtn.style.background = 'transparent'; }); // Animate in setTimeout(() => { overlay.style.opacity = "1"; popup.style.opacity = "1"; popup.style.transform = "translate(-50%, -50%) scale(1)"; }, 10); // POPUP CLOSE FUNCTION function closePopup() { overlay.style.opacity = "0"; popup.style.opacity = "0"; popup.style.transform = "translate(-50%, -50%) scale(0.95)"; setTimeout(() => { overlay.remove(); popup.remove(); }, 300); } // Save on click popup.querySelector("#save-quicknav").addEventListener("click", () => { const quickNavSettings = []; for (let i = 0; i < 9; i++) { const name = document.getElementById(`quicknav-name-${i}`).value.trim(); const link = document.getElementById(`quicknav-link-${i}`).value.trim(); if (name && link) { quickNavSettings.push({ name, link }); } } localStorage.setItem("ROLOCATE_quicknav_settings", JSON.stringify(quickNavSettings)); closePopup(); }); // Cancel button popup.querySelector("#cancel-quicknav").addEventListener("click", closePopup); // Close when clicking overlay overlay.addEventListener("click", (e) => { if (e.target === overlay) { closePopup(); } }); // Close with ESC key document.addEventListener("keydown", function escClose(e) { if (e.key === "Escape") { closePopup(); document.removeEventListener("keydown", escClose); } }); // AUTO-INIT AND KEYBOARD SHORTCUT // Set up keyboard shortcut (Alt+Q) document.addEventListener("keydown", function keyboardShortcut(e) { if (e.altKey && e.key === "q") { showQuickNavPopup(); } }); } /******************************************************* name of function: applyStoredSettings description: makes sure local storage is stored in correctly *******************************************************/ function applyStoredSettings() { // Handle all checkboxes document.querySelectorAll("input[type='checkbox']").forEach(checkbox => { const storageKey = `ROLOCATE_${checkbox.id}`; const savedValue = localStorage.getItem(storageKey); checkbox.checked = savedValue === "true"; checkbox.addEventListener("change", () => { localStorage.setItem(storageKey, checkbox.checked); }); }); // Handle dropdown for prioritylocation-select const prioritySelect = document.getElementById("prioritylocation-select"); if (prioritySelect) { const storageKey = "ROLOCATE_prioritylocation"; const savedValue = localStorage.getItem(storageKey) || "automatic"; prioritySelect.value = savedValue; // Show/hide coordinates inputs based on selected value const manualCoordinates = document.getElementById("manual-coordinates"); if (manualCoordinates) { manualCoordinates.style.display = savedValue === "manual" ? "block" : "none"; // Set input values from stored coordinates if available if (savedValue === "manual") { try { const savedCoords = JSON.parse(GM_getValue("ROLOCATE_coordinates", '{"lat":"","lng":""}')); document.getElementById("latitude").value = savedCoords.lat || ""; document.getElementById("longitude").value = savedCoords.lng || ""; // If manual mode but no coordinates saved, revert to automatic if (!savedCoords.lat || !savedCoords.lng) { prioritySelect.value = "automatic"; localStorage.setItem(storageKey, "automatic"); manualCoordinates.style.display = "none"; } } catch (e) { ConsoleLogEnabled("Error loading saved coordinates:", e); } } } prioritySelect.addEventListener("change", () => { const newValue = prioritySelect.value; localStorage.setItem(storageKey, newValue); // Show/hide coordinates inputs based on new value if (manualCoordinates) { manualCoordinates.style.display = newValue === "manual" ? "block" : "none"; // When switching to manual mode, load any saved coordinates if (newValue === "manual") { try { const savedCoords = JSON.parse(GM_getValue("ROLOCATE_coordinates", '{"lat":"","lng":""}')); document.getElementById("latitude").value = savedCoords.lat || ""; document.getElementById("longitude").value = savedCoords.lng || ""; // If no coordinates exist, keep the inputs empty } catch (e) { ConsoleLogEnabled("Error loading saved coordinates:", e); } } } }); } // Button click handlers const editQuickNavBtn = document.getElementById("edit-quicknav-btn"); if (editQuickNavBtn) { editQuickNavBtn.addEventListener("click", () => { showQuickNavPopup(); }); } const editQualityGameBtn = document.getElementById("edit-gamequality-btn"); if (editQualityGameBtn) { editQualityGameBtn.addEventListener("click", () => { openGameQualitySettings(); }); } const fastServersToggle = document.getElementById("fastservers"); if (fastServersToggle) { fastServersToggle.addEventListener("change", () => { if (fastServersToggle.checked) { notifications('Fast Server Search: 100x faster on Violentmonkey, ~2x on Tampermonkey. Replaces thumbnails with builderman to bypass rate limits.', 'info', '🧪', 2000); } }); } const AutoRunServerRegions = document.getElementById("AutoRunServerRegions"); if (AutoRunServerRegions) { AutoRunServerRegions.addEventListener("change", () => { if (AutoRunServerRegions.checked) { notifications('Auto Server Regions works best when paired with Fast Server Search in Advanced Settings.', 'info', '🧪', 2000); } }); } // Save coordinates button handler const saveCoordinatesBtn = document.getElementById("save-coordinates"); if (saveCoordinatesBtn) { saveCoordinatesBtn.addEventListener("click", () => { const latInput = document.getElementById("latitude"); const lngInput = document.getElementById("longitude"); const lat = latInput.value.trim(); const lng = lngInput.value.trim(); // If manual mode but no coordinates provided, revert to automatic if (!lat || !lng) { const prioritySelect = document.getElementById("prioritylocation-select"); if (prioritySelect) { prioritySelect.value = "automatic"; localStorage.setItem("ROLOCATE_prioritylocation", "automatic"); document.getElementById("manual-coordinates").style.display = "none"; // show feedback to user even if they dont see it saveCoordinatesBtn.textContent = "Reverted to Automatic!"; saveCoordinatesBtn.style.background = "#4CAF50"; setTimeout(() => { saveCoordinatesBtn.textContent = "Save Coordinates"; saveCoordinatesBtn.style.background = "background: #4CAF50;"; }, 2000); } return; } // Validate coordinates const latNum = parseFloat(lat); const lngNum = parseFloat(lng); if (isNaN(latNum) || isNaN(lngNum) || latNum < -90 || latNum > 90 || lngNum < -180 || lngNum > 180) { notifications('Invalid coordinates! Latitude must be between -90 and 90, and longitude between -180 and 180.', 'error', '⚠️', '8000'); return; } // Save valid coordinates const coordinates = { lat, lng }; GM_setValue("ROLOCATE_coordinates", JSON.stringify(coordinates)); // store coordinates in secure storage // Ensure we're in manual mode localStorage.setItem("ROLOCATE_prioritylocation", "manual"); if (prioritySelect) { prioritySelect.value = "manual"; } // Provide feedback saveCoordinatesBtn.textContent = "Saved!"; saveCoordinatesBtn.style.background = "linear-gradient(135deg, #1e8449 0%, #196f3d 100%);"; setTimeout(() => { saveCoordinatesBtn.textContent = "Save Coordinates"; saveCoordinatesBtn.style.background = "background: #4CAF50;"; }, 2000); }); } } /******************************************************* name of function: AddSettingsButton description: adds settings button *******************************************************/ function AddSettingsButton() { const base64Logo = window.Base64Images.logo; const navbarGroup = document.querySelector('.nav.navbar-right.rbx-navbar-icon-group'); if (!navbarGroup || document.getElementById('custom-logo')) return; const li = document.createElement('li'); li.id = 'custom-logo-container'; li.style.position = 'relative'; li.innerHTML = ` <img id="custom-logo" style=" margin-top: 6px; margin-left: 6px; width: 26px; cursor: pointer; border-radius: 4px; transition: all 0.2s ease-in-out; " src="${base64Logo}"> <span id="custom-tooltip" style=" visibility: hidden; background-color: black; color: white; text-align: center; padding: 5px; border-radius: 5px; position: absolute; top: 35px; left: 50%; transform: translateX(-50%); white-space: nowrap; font-size: 12px; opacity: 0; transition: opacity 0.2s ease-in-out; "> Settings </span> `; const logo = li.querySelector('#custom-logo'); const tooltip = li.querySelector('#custom-tooltip'); logo.addEventListener('click', () => openSettingsMenu()); logo.addEventListener('mouseover', () => { logo.style.width = '30px'; logo.style.border = '2px solid white'; tooltip.style.visibility = 'visible'; tooltip.style.opacity = '1'; }); logo.addEventListener('mouseout', () => { logo.style.width = '26px'; logo.style.border = 'none'; tooltip.style.visibility = 'hidden'; tooltip.style.opacity = '0'; }); navbarGroup.appendChild(li); } /******************************************************* name of function: removeAds description: remove roblox ads. 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 = `<defs><linearGradient id="iconGradient" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#cccccc;stop-opacity:1" /><stop offset="100%" style="stop-color:#999999;stop-opacity:1" /></linearGradient></defs>`; document.body.appendChild(svgDefs); } const container = document.createElement('div'); container.className = 'mutual-friends-container'; container.style.display = 'none'; const header = document.createElement('div'); header.className = 'mutual-friends-header'; header.innerHTML = `<svg class="mutual-friends-icon" viewBox="0 0 24 24"><path transform="translate(0,-5)" d="M17.25 20.5c1.281 0.719 2 1.906 1.875 3.125-0.063 0.75-0.031 0.75-1 0.875-0.594 0.063-4.375 0.094-8.219 0.094-4.375 0-8.938-0.031-9.281-0.125-1.281-0.344-0.531-2.719 1.156-3.844 1.344-0.844 4.063-2.156 4.813-2.313 1.031-0.219 1.156-0.875 0-2.844-0.25-0.469-0.531-1.813-0.563-3.25-0.031-2.313 0.375-3.875 2.406-4.656 0.375-0.125 0.813-0.188 1.219-0.188 1.344 0 2.594 0.75 3.125 1.844 0.719 1.469 0.375 5.313-0.375 6.719-0.906 1.594-0.813 2.094 0.188 2.344 0.625 0.156 2.688 1.125 4.656 2.219zM24.094 18.531c1 0.531 1.563 1.5 1.469 2.438-0.031 0.563-0.031 0.594-0.781 0.688-0.375 0.063-2.344 0.094-4.656 0.094-0.406-0.969-1.188-1.844-2.25-2.406-1.219-0.688-2.656-1.406-3.75-1.875 0.719-0.344 1.344-0.625 1.625-0.688 0.781-0.188 0.875-0.625 0-2.188-0.219-0.375-0.469-1.438-0.5-2.563-0.031-1.813 0.375-3.063 1.938-3.656 0.313-0.094 0.656-0.156 0.969-0.156 1.031 0 2 0.563 2.406 1.438 0.531 1.156 0.281 4.156-0.281 5.281-0.688 1.25-0.625 1.625 0.156 1.813 0.5 0.125 2.094 0.906 3.656 1.781z"/></svg>Mutual Friends`; const content = document.createElement('div'); content.className = 'mutual-friends-content'; container.appendChild(header); container.appendChild(content); return container; }; // Function to show loading state const showMutualFriendsLoading = (contentElement) => { contentElement.innerHTML = `<div class="mutual-friends-loading"><div class="loading-spinner"></div>Finding mutual friends...</div>`; }; // Function to create mutual friends popup const createMutualFriendsPopup = async (mutualFriends) => { const overlay = document.createElement('div'); overlay.className = 'mutual-friends-overlay'; const popup = document.createElement('div'); popup.className = 'mutual-friends-popup'; const header = document.createElement('div'); header.className = 'mutual-friends-popup-header'; header.innerHTML = `<h3>All Mutual Friends (${mutualFriends.length})</h3><button class="mutual-friends-close">×</button>`; const grid = document.createElement('div'); grid.className = 'mutual-friends-popup-grid'; const avatarMap = localAvatarCache; mutualFriends.forEach(friend => { const friendItem = document.createElement('div'); friendItem.className = 'mutual-friends-popup-item'; const avatarUrl = avatarMap[friend.id]; const avatarContent = avatarUrl ? `<img src="${avatarUrl}" alt="${friend.name}">` : '👤'; friendItem.innerHTML = `<div class="mutual-friend-avatar">${avatarContent}</div><span class="mutual-friend-name">${friend.name}</span>`; friendItem.onclick = () => { window.open(`https://www.roblox.com/users/${friend.id}/profile`, '_blank'); }; grid.appendChild(friendItem); }); popup.appendChild(header); popup.appendChild(grid); overlay.appendChild(popup); 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 = '<div class="no-mutual-friends">No mutual friends found. RoLocate by Oqarshi</div>'; return; } const header = contentElement.parentElement.querySelector('.mutual-friends-header'); const countBadge = document.createElement('span'); countBadge.className = 'mutual-friends-count'; countBadge.textContent = mutualFriends.length; header.appendChild(countBadge); const friendsList = document.createElement('div'); friendsList.className = 'mutual-friends-list'; const maxVisible = 6; const friendsToShow = mutualFriends.slice(0, maxVisible); friendsToShow.forEach(friend => { const friendTag = document.createElement('div'); friendTag.className = 'mutual-friend-tag'; friendTag.textContent = friend.name; friendTag.onclick = () => { window.open(`https://www.roblox.com/users/${friend.id}/profile`, '_blank'); }; friendsList.appendChild(friendTag); }); if (mutualFriends.length > maxVisible) { const moreButton = document.createElement('div'); moreButton.className = 'mutual-friend-tag mutual-friends-more'; moreButton.textContent = `+${mutualFriends.length - maxVisible} more`; moreButton.onclick = async () => { const popup = await createMutualFriendsPopup(mutualFriends); document.body.appendChild(popup); }; friendsList.appendChild(moreButton); } contentElement.appendChild(friendsList); }; // Function to find profile insertion point const findProfileInsertionPoint = () => { const profileHeader = document.querySelector('.profile-header-main'); if (profileHeader) return profileHeader.parentElement; return document.querySelector('[class*="profile"]'); }; // Main execution logic try { const currentUserId = getCurrentUserId(); if (!currentUserId) return; const urlMatch = window.location.pathname.match(/^\/(?:[a-z]{2}\/)?users\/(\d+)\/profile$/); // check if path name is right. if not then return if (!urlMatch) return; const otherUserId = urlMatch[1]; if (otherUserId === String(currentUserId)) return; // Clear local cache for new page visit localAvatarCache = {}; const mutualFriendsElement = createMutualFriendsElement(); const insertionPoint = findProfileInsertionPoint(); if (!insertionPoint) { ConsoleLogEnabled('[Mutual Friends] Could not find suitable insertion point'); return; } insertionPoint.appendChild(mutualFriendsElement); mutualFriendsElement.style.display = 'block'; const contentElement = mutualFriendsElement.querySelector('.mutual-friends-content'); showMutualFriendsLoading(contentElement); const [currentUserFriends, otherUserFriends] = await Promise.all([ gmFetchFriends(currentUserId), gmFetchFriends(otherUserId), ]); if (!currentUserFriends || !otherUserFriends) { contentElement.innerHTML = '<div class="no-mutual-friends">Failed to load friend data</div>'; return; } const mutualFriends = currentUserFriends.filter(currentFriend => otherUserFriends.some(otherFriend => otherFriend.id === currentFriend.id) ); await fetchAndCacheAllAvatars(mutualFriends); await displayMutualFriends(contentElement, mutualFriends); } catch (error) { ConsoleLogEnabled('[executeMutualFriendsFeature] Error occurred:', error); } } /******************************************************* name of function: manageRobloxChatBar description: Disables roblox chat *******************************************************/ 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 = '<div class="ROLOCATE_SMARTSEARCH_loading">Loading games...</div>'; try { const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: apiUrl, headers: { "Accept": "application/json" }, onload: resolve, onerror: reject }); }); if (response.status === 200) { const data = JSON.parse(response.responseText); const searchResults = data.searchResults || []; const allGames = searchResults.map(result => result.contents[0]); const gamesWithSimilarity = allGames.map(game => ({ ...game, similarity: getSimilarityScore(query, game.name) })); const sortedGames = gamesWithSimilarity.sort((a, b) => { const similarityA = a.similarity; const similarityB = b.similarity; if ((similarityA >= 0.80 && similarityB >= 0.80) || Math.abs(similarityA - similarityB) < 0.0001) { return b.playerCount - a.playerCount; } return similarityB - similarityA; }); const games = sortedGames.slice(0, 30); const activeTab = document.querySelector('.ROLOCATE_SMARTSEARCH_dropdown-tab.ROLOCATE_SMARTSEARCH_active')?.textContent; if (activeTab !== "Games") return; if (games.length === 0) { contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_no-results">No results found</div>'; return; } // Render cards with play button contentArea.innerHTML = games.map(game => ` <div class="ROLOCATE_SMARTSEARCH_game-card-container"> <a href="https://www.roblox.com/games/${game.rootPlaceId}" class="ROLOCATE_SMARTSEARCH_game-card-link" target="_self"> <div class="ROLOCATE_SMARTSEARCH_game-card"> <div class="ROLOCATE_SMARTSEARCH_thumbnail-loading" data-universe-id="${game.universeId}"></div> <div class="ROLOCATE_SMARTSEARCH_game-info"> <h3 class="ROLOCATE_SMARTSEARCH_game-name">${game.name}</h3> <p class="ROLOCATE_SMARTSEARCH_game-stats"> Players: ${formatNumberCount(game.playerCount)} | <span class="ROLOCATE_SMARTSEARCH_thumbs-up">👍 ${formatNumberCount(game.totalUpVotes)}</span> | <span class="ROLOCATE_SMARTSEARCH_thumbs-down">👎 ${formatNumberCount(game.totalDownVotes)}</span> </p> </div> </div> </a> <button class="ROLOCATE_SMARTSEARCH_play-button" data-place-id="${game.rootPlaceId}" title="Quick Join"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M8 5V19L19 12L8 5Z" fill="#4CAF50"/> </svg> </button> </div> `).join(''); // Add event listeners to play buttons setTimeout(() => { document.querySelectorAll('.ROLOCATE_SMARTSEARCH_play-button').forEach(button => { button.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); const placeId = this.getAttribute('data-place-id'); window.location.href = `https://www.roblox.com/games/${placeId}#?ROLOCATE_QUICKJOIN`; }); }); }, 100); // Load thumbnails in batches const universeIds = games.map(game => game.universeId); const thumbnailBatches = chunkArray(universeIds, 10); for (const batch of thumbnailBatches) { try { const thumbnails = await fetchGameIconsBatch(batch); thumbnails.forEach(thumb => { const loadingElement = document.querySelector(`.ROLOCATE_SMARTSEARCH_thumbnail-loading[data-universe-id="${thumb.targetId}"]`); if (loadingElement) { loadingElement.outerHTML = ` <img src="${thumb.imageUrl}" alt="${games.find(g => g.universeId == thumb.targetId)?.name || 'Game'}" class="ROLOCATE_SMARTSEARCH_game-thumbnail"> `; } }); } catch (error) { ConsoleLogEnabled('Error fetching game thumbnails:', error); } } } else { contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_error">Error loading results</div>'; } } catch (error) { ConsoleLogEnabled('Error in game search:', error); contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_error">Error loading results</div>'; } } async function fetchUserSearchResults(query) { const sessionId = Date.now(); const apiUrl = `https://apis.roblox.com/search-api/omni-search?verticalType=user&searchQuery=${encodeURIComponent(query)}&pageToken=&globalSessionId=${sessionId}&sessionId=${sessionId}`; contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_loading">Loading users...</div>'; try { const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: apiUrl, headers: { "Accept": "application/json" }, onload: resolve, onerror: reject }); }); if (response.status === 200) { const data = JSON.parse(response.responseText); const userGroup = data.searchResults?.find(group => group.contentGroupType === "User"); const users = userGroup?.contents || []; if (users.length === 0) { contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_no-results">No users found</div>'; return; } // Render cards immediately with loading state contentArea.innerHTML = users.map(user => ` <a href="https://www.roblox.com/users/${user.contentId}/profile" class="ROLOCATE_SMARTSEARCH_user-card-link" target="_self"> <div class="ROLOCATE_SMARTSEARCH_user-card"> <div class="ROLOCATE_SMARTSEARCH_thumbnail-loading" data-user-id="${user.contentId}"></div> <div class="ROLOCATE_SMARTSEARCH_user-info"> <h3 class="ROLOCATE_SMARTSEARCH_user-display-name">${user.displayName || user.username}</h3> <p class="ROLOCATE_SMARTSEARCH_user-username">@${user.username}</p> </div> </div> </a> `).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 = ` <img src="${thumb.imageUrl}" alt="${users.find(u => u.contentId == thumb.targetId)?.username || 'User'}" class="ROLOCATE_SMARTSEARCH_user-thumbnail"> `; } }); } catch (error) { ConsoleLogEnabled('Error fetching user thumbnails:', error); } } } else { contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_error">Error loading user results</div>'; } } catch (error) { ConsoleLogEnabled('Error in user search:', error); contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_error">Error loading user results</div>'; } } async function fetchGroupSearchResults(query) { const apiUrl = `https://groups.roblox.com/v1/groups/search?cursor=&keyword=${encodeURIComponent(query)}&limit=25&prioritizeExactMatch=true&sortOrder=Asc`; contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_loading">Loading groups...</div>'; try { const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: apiUrl, headers: { "Accept": "application/json" }, onload: resolve, onerror: reject }); }); if (response.status === 200) { const data = JSON.parse(response.responseText); const groups = data.data || []; if (groups.length === 0) { contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_no-results">No groups found</div>'; return; } // Render cards immediately with loading state contentArea.innerHTML = groups.map(group => ` <a href="https://www.roblox.com/groups/${group.id}" class="ROLOCATE_SMARTSEARCH_group-card-link" target="_self"> <div class="ROLOCATE_SMARTSEARCH_group-card"> <div class="ROLOCATE_SMARTSEARCH_thumbnail-loading" data-group-id="${group.id}"></div> <div class="ROLOCATE_SMARTSEARCH_group-info"> <h3 class="ROLOCATE_SMARTSEARCH_group-name">${group.name}</h3> <p class="ROLOCATE_SMARTSEARCH_group-members">Members: ${formatNumberCount(group.memberCount)}</p> <p class="ROLOCATE_SMARTSEARCH_group-created">Created: ${formatDate(group.created)}</p> </div> </div> </a> `).join(''); // Load thumbnails in batches const groupIds = groups.map(group => group.id); const thumbnailBatches = chunkArray(groupIds, 10); for (const batch of thumbnailBatches) { try { const thumbnails = await fetchGroupIconsBatch(batch); thumbnails.forEach(thumb => { const loadingElement = document.querySelector(`.ROLOCATE_SMARTSEARCH_thumbnail-loading[data-group-id="${thumb.targetId}"]`); if (loadingElement) { loadingElement.outerHTML = ` <img src="${thumb.imageUrl}" alt="${groups.find(g => g.id == thumb.targetId)?.name || 'Group'}" class="ROLOCATE_SMARTSEARCH_group-thumbnail"> `; } }); } catch (error) { ConsoleLogEnabled('Error fetching group thumbnails:', error); } } } else { contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_error">Error loading group results</div>'; } } catch (error) { ConsoleLogEnabled('Error in group search:', error); contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_error">Error loading group results</div>'; } } const originalSearchContainer = document.querySelector('[data-testid="navigation-search-input"]'); if (!originalSearchContainer) { ConsoleLogEnabled('Search container not found'); return false; } originalSearchContainer.remove(); const customSearchContainer = document.createElement('div'); customSearchContainer.className = 'navbar-left navbar-search col-xs-5 col-sm-6 col-md-2 col-lg-3 shown'; customSearchContainer.setAttribute('role', 'search'); customSearchContainer.style.marginTop = '4px'; customSearchContainer.style.position = 'relative'; const form = document.createElement('form'); form.name = 'custom-search-form'; form.addEventListener('submit', (e) => { e.preventDefault(); const query = searchInput.value.trim(); if (!query) return; const activeTab = document.querySelector('.ROLOCATE_SMARTSEARCH_dropdown-tab.ROLOCATE_SMARTSEARCH_active')?.dataset.tab; let url = ''; switch (activeTab) { case 'games': url = `https://www.roblox.com/discover/?Keyword=${encodeURIComponent(query)}`; break; case 'users': url = `https://www.roblox.com/search/users?keyword=${encodeURIComponent(query)}`; break; case 'groups': url = `https://www.roblox.com/search/communities?keyword=${encodeURIComponent(query)}`; break; default: url = `https://www.roblox.com/discover/?Keyword=${encodeURIComponent(query)}`; break; } window.location.href = url; }); const formWrapper = document.createElement('div'); formWrapper.className = 'ROLOCATE_SMARTSEARCH_form-has-feedback'; const searchInput = document.createElement('input'); let wasPreviouslyBlurred = true; let lastInputValue = ''; searchInput.addEventListener('focus', () => { if (wasPreviouslyBlurred) { const activeTab = document.querySelector('.ROLOCATE_SMARTSEARCH_dropdown-tab.ROLOCATE_SMARTSEARCH_active')?.textContent || 'Unknown'; const typedText = searchInput.value.trim(); ConsoleLogEnabled(`[SmartSearch] Search bar focused | Tab: ${activeTab} | Input: "${typedText}"`); wasPreviouslyBlurred = false; } }); searchInput.addEventListener('blur', () => { wasPreviouslyBlurred = true; }); searchInput.id = 'custom-navbar-search-input'; searchInput.type = 'search'; searchInput.className = 'form-control input-field ROLOCATE_SMARTSEARCH_custom-search-input'; searchInput.placeholder = 'SmartSearch | RoLocate by Oqarshi'; searchInput.maxLength = 120; searchInput.autocomplete = 'off'; const searchIcon = document.createElement('span'); searchIcon.className = 'icon-common-search-sm ROLOCATE_SMARTSEARCH_custom-search-icon'; const dropdownMenu = document.createElement('div'); dropdownMenu.className = 'ROLOCATE_SMARTSEARCH_search-dropdown-menu'; dropdownMenu.style.display = 'none'; const navTabs = document.createElement('div'); navTabs.className = 'ROLOCATE_SMARTSEARCH_dropdown-nav-tabs'; const tabs = ['Games', 'Users', 'Groups']; const tabButtons = []; tabs.forEach((tabName, index) => { const tabButton = document.createElement('button'); tabButton.className = `ROLOCATE_SMARTSEARCH_dropdown-tab ${index === 0 ? 'ROLOCATE_SMARTSEARCH_active' : ''}`; tabButton.textContent = tabName; tabButton.type = 'button'; tabButton.dataset.tab = tabName.toLowerCase(); tabButtons.push(tabButton); navTabs.appendChild(tabButton); }); const contentArea = document.createElement('div'); contentArea.className = 'ROLOCATE_SMARTSEARCH_dropdown-content'; contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_content-text">Quickly search for <strong>games</strong> above!</div>'; dropdownMenu.appendChild(navTabs); dropdownMenu.appendChild(contentArea); formWrapper.appendChild(searchInput); formWrapper.appendChild(searchIcon); form.appendChild(formWrapper); customSearchContainer.appendChild(form); customSearchContainer.appendChild(dropdownMenu); let isMenuOpen = false; searchInput.addEventListener('click', showDropdownMenu); searchInput.addEventListener('focus', showDropdownMenu); searchInput.addEventListener('input', function() { const currentValue = this.value.trim(); if (currentValue && currentValue !== lastInputValue && !isMenuOpen) { showDropdownMenu(); } lastInputValue = currentValue; }); tabButtons.forEach(button => { button.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); tabButtons.forEach(tab => tab.classList.remove('ROLOCATE_SMARTSEARCH_active')); button.classList.add('ROLOCATE_SMARTSEARCH_active'); const query = searchInput.value.trim(); if (query) { if (button.textContent === "Games") { fetchGameSearchResults(query); } else if (button.textContent === "Users") { fetchUserSearchResults(query); } else if (button.textContent === "Groups") { fetchGroupSearchResults(query); } } else { if (button.textContent === "Games") { contentArea.innerHTML = ` <div class="ROLOCATE_SMARTSEARCH_content-text"> Quickly search for <strong>games</strong> above! </div> `; } else if (button.textContent === "Users") { contentArea.innerHTML = ` <div class="ROLOCATE_SMARTSEARCH_content-text"> Instantly find the <strong>user</strong> you're looking for! </div> `; } else if (button.textContent === "Groups") { contentArea.innerHTML = ` <div class="ROLOCATE_SMARTSEARCH_content-text"> Search for <strong>groups</strong> rapidly. </div> `; } } }); }); document.addEventListener('click', (e) => { if (!customSearchContainer.contains(e.target)) { hideDropdownMenu(); } }); dropdownMenu.addEventListener('click', (e) => { e.stopPropagation(); }); function showDropdownMenu() { isMenuOpen = true; dropdownMenu.style.display = 'block'; formWrapper.classList.add('ROLOCATE_SMARTSEARCH_menu-open'); setTimeout(() => { dropdownMenu.classList.add('ROLOCATE_SMARTSEARCH_show'); }, 10); const activeTab = document.querySelector('.ROLOCATE_SMARTSEARCH_dropdown-tab.ROLOCATE_SMARTSEARCH_active')?.textContent; const query = searchInput.value.trim(); if (query) { if (activeTab === "Games" && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_game-card') === null && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_no-results') === null) { fetchGameSearchResults(query); } else if (activeTab === "Users" && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_user-card') === null && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_no-results') === null) { fetchUserSearchResults(query); } else if (activeTab === "Groups" && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_group-card') === null && contentArea.querySelector('.ROLOCATE_SMARTSEARCH_no-results') === null) { fetchGroupSearchResults(query); } } } function hideDropdownMenu() { isMenuOpen = false; dropdownMenu.classList.remove('ROLOCATE_SMARTSEARCH_show'); formWrapper.classList.remove('ROLOCATE_SMARTSEARCH_menu-open'); setTimeout(() => { if (!isMenuOpen) { dropdownMenu.style.display = 'none'; } }, 200); } const rightNavigation = document.getElementById('right-navigation-header'); if (rightNavigation) { rightNavigation.insertBefore(customSearchContainer, rightNavigation.firstChild); } let debounceTimeout; searchInput.addEventListener('input', () => { if (searchInput.value.trim() && !isMenuOpen) { showDropdownMenu(); } clearTimeout(debounceTimeout); debounceTimeout = setTimeout(() => { const query = searchInput.value.trim(); const activeTab = document.querySelector('.ROLOCATE_SMARTSEARCH_dropdown-tab.ROLOCATE_SMARTSEARCH_active')?.textContent; if (!query) { if (activeTab === "Games") { contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_content-text">Quickly search for <strong>games</strong> above!</div>'; } else if (activeTab === "Users") { contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_content-text">Instantly find the <strong>user</strong> you\'re looking for!</div>'; } else if (activeTab === "Groups") { contentArea.innerHTML = '<div class="ROLOCATE_SMARTSEARCH_content-text">Search for <strong>groups</strong> rapidly.</div>'; } return; } if (activeTab === "Games") { fetchGameSearchResults(query); } else if (activeTab === "Users") { fetchUserSearchResults(query); } else if (activeTab === "Groups") { fetchGroupSearchResults(query); } }, 250); }); const style = document.createElement('style'); style.textContent = ` .ROLOCATE_SMARTSEARCH_form-has-feedback { position: relative !important; display: flex !important; align-items: center !important; border: 2px solid #2c2f36 !important; border-radius: 8px !important; background-color: #191a1f !important; transition: all 0.3s ease !important; z-index: 1000 !important; } .ROLOCATE_SMARTSEARCH_form-has-feedback:focus-within, .ROLOCATE_SMARTSEARCH_form-has-feedback.ROLOCATE_SMARTSEARCH_menu-open { border-color: #00b2ff !important; } .ROLOCATE_SMARTSEARCH_form-has-feedback.ROLOCATE_SMARTSEARCH_menu-open { border-bottom-left-radius: 0 !important; border-bottom-right-radius: 0 !important; border-bottom-color: transparent !important; position: relative !important; } .ROLOCATE_SMARTSEARCH_form-has-feedback.ROLOCATE_SMARTSEARCH_menu-open::after { content: '' !important; position: absolute !important; bottom: -12px !important; left: -2px !important; right: -2px !important; height: 12px !important; border-left: 2px solid #00b2ff !important; border-right: 2px solid #00b2ff !important; background-color: transparent !important; z-index: 1000 !important; } .ROLOCATE_SMARTSEARCH_custom-search-input { width: 100% !important; border: none !important; background-color: transparent !important; color: #ffffff !important; padding: 8px 36px 8px 12px !important; font-size: 16px !important; height: 27px !important; border-radius: 8px !important; } .ROLOCATE_SMARTSEARCH_custom-search-input:focus { outline: none !important; box-shadow: none !important; } .ROLOCATE_SMARTSEARCH_custom-search-input::placeholder { color: #8a8d93 !important; opacity: 1 !important; } .ROLOCATE_SMARTSEARCH_custom-search-icon { position: absolute !important; right: 10px !important; top: 50% !important; transform: translateY(-50%) !important; pointer-events: none !important; font-size: 14px !important; color: #8a8d93 !important; } .ROLOCATE_SMARTSEARCH_form-has-feedback:focus-within .ROLOCATE_SMARTSEARCH_custom-search-icon, .ROLOCATE_SMARTSEARCH_form-has-feedback.ROLOCATE_SMARTSEARCH_menu-open .ROLOCATE_SMARTSEARCH_custom-search-icon { color: #00b2ff !important; } .ROLOCATE_SMARTSEARCH_search-dropdown-menu { position: absolute !important; top: calc(100% - 2px) !important; left: 0 !important; width: 100% !important; background-color: #191a1f !important; border-left: 2px solid #00b2ff !important; border-right: 2px solid #00b2ff !important; border-bottom: 2px solid #00b2ff !important; border-top: none !important; border-radius: 0 0 8px 8px !important; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important; z-index: 999 !important; opacity: 0 !important; transform: translateY(-10px) !important; transition: all 0.2s ease !important; box-sizing: border-box !important; } .ROLOCATE_SMARTSEARCH_search-dropdown-menu.ROLOCATE_SMARTSEARCH_show { opacity: 1 !important; transform: translateY(0) !important; } .ROLOCATE_SMARTSEARCH_dropdown-nav-tabs { display: flex !important; background-color: #1e2025 !important; border-bottom: 1px solid #2c2f36 !important; } .ROLOCATE_SMARTSEARCH_dropdown-tab { flex: 1 !important; padding: 12px 16px !important; background: none !important; border: none !important; color: #8a8d93 !important; font-size: 14px !important; font-weight: 500 !important; cursor: pointer !important; transition: all 0.2s ease !important; border-bottom: 2px solid transparent !important; } .ROLOCATE_SMARTSEARCH_dropdown-tab:hover { color: #ffffff !important; background-color: rgba(255, 255, 255, 0.05) !important; } .ROLOCATE_SMARTSEARCH_dropdown-tab.ROLOCATE_SMARTSEARCH_active { color: #00b2ff !important; border-bottom-color: #00b2ff !important; background-color: rgba(0, 178, 255, 0.1) !important; } .ROLOCATE_SMARTSEARCH_dropdown-content { padding: 10px !important; max-height: 350px !important; overflow-y: auto !important; display: block !important; } .ROLOCATE_SMARTSEARCH_content-text { color: #ffffff !important; font-size: 16px !important; text-align: center !important; } .ROLOCATE_SMARTSEARCH_content-text strong { color: #00b2ff !important; } .navbar-left.navbar-search { z-index: 1001 !important; position: relative !important; } /* Game card styles with play button */ .ROLOCATE_SMARTSEARCH_game-card-container { position: relative; margin: 6px 0; } .ROLOCATE_SMARTSEARCH_game-card-link { display: block; text-decoration: none; color: inherit; } .ROLOCATE_SMARTSEARCH_game-card { display: flex; align-items: center; padding: 8px; background-color: #1e2025; border-radius: 8px; transition: background-color 0.2s ease; } .ROLOCATE_SMARTSEARCH_game-card:hover { background-color: #2c2f36; } .ROLOCATE_SMARTSEARCH_thumbnail-loading { width: 50px; height: 50px; border-radius: 4px; margin-right: 10px; background-color: #2c2f36; position: relative; overflow: hidden; } .ROLOCATE_SMARTSEARCH_thumbnail-loading::after { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent); animation: loading 1.5s infinite; } @keyframes loading { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } .ROLOCATE_SMARTSEARCH_game-thumbnail { width: 50px; height: 50px; border-radius: 4px; margin-right: 10px; object-fit: cover; } .ROLOCATE_SMARTSEARCH_game-info { flex: 1; overflow: hidden; padding-right: 40px !important; } .ROLOCATE_SMARTSEARCH_game-name { font-size: 14px; color: #ffffff; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: calc(100% - 40px); } .ROLOCATE_SMARTSEARCH_game-stats { font-size: 12px; color: #8a8d93; margin: 2px 0 0 0; } .ROLOCATE_SMARTSEARCH_thumbs-up { color: #4caf50; } .ROLOCATE_SMARTSEARCH_thumbs-down { color: #f44336; } /* Play button styles - square with rounded edges */ .ROLOCATE_SMARTSEARCH_play-button { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); width: 36px; height: 36px; border-radius: 6px; background: rgba(76, 175, 80, 0.2); border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; z-index: 2; } .ROLOCATE_SMARTSEARCH_play-button:hover { background: rgba(76, 175, 80, 0.3); transform: translateY(-50%) scale(1.05); } .ROLOCATE_SMARTSEARCH_play-button svg { width: 18px; height: 18px; } /* User card styles */ .ROLOCATE_SMARTSEARCH_user-card-link { display: block; text-decoration: none; color: inherit; } .ROLOCATE_SMARTSEARCH_user-card { display: flex; align-items: center; padding: 8px; margin: 6px 0; background-color: #1e2025; border-radius: 8px; transition: background-color 0.2s ease; } .ROLOCATE_SMARTSEARCH_user-card:hover { background-color: #2c2f36; } .ROLOCATE_SMARTSEARCH_user-thumbnail { width: 50px; height: 50px; border-radius: 50%; margin-right: 12px; object-fit: cover; } .ROLOCATE_SMARTSEARCH_user-info { flex: 1; overflow: hidden; } .ROLOCATE_SMARTSEARCH_user-display-name { font-size: 14px; font-weight: 500; color: #ffffff; margin: 0 0 2px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .ROLOCATE_SMARTSEARCH_user-username { font-size: 12px; color: #8a8d93; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* Group card styles */ .ROLOCATE_SMARTSEARCH_group-card-link { display: block; text-decoration: none; color: inherit; } .ROLOCATE_SMARTSEARCH_group-card { display: flex; align-items: center; padding: 8px; margin: 6px 0; background-color: #1e2025; border-radius: 8px; transition: background-color 0.2s ease; } .ROLOCATE_SMARTSEARCH_group-card:hover { background-color: #2c2f36; } .ROLOCATE_SMARTSEARCH_group-thumbnail { width: 50px; height: 50px; border-radius: 4px; margin-right: 12px; object-fit: cover; } .ROLOCATE_SMARTSEARCH_group-info { flex: 1; overflow: hidden; } .ROLOCATE_SMARTSEARCH_group-name { font-size: 14px; font-weight: 500; color: #ffffff; margin: 0 0 4px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .ROLOCATE_SMARTSEARCH_group-members { font-size: 12px; color: #8a8d93; margin: 0 0 2px 0; } .ROLOCATE_SMARTSEARCH_group-created { font-size: 11px; color: #6d717a; margin: 0; } /* Status messages */ .ROLOCATE_SMARTSEARCH_loading, .ROLOCATE_SMARTSEARCH_no-results, .ROLOCATE_SMARTSEARCH_error { text-align: center; color: #8a8d93; padding: 20px; font-size: 14px; } `; 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 = ` <div class="ROLOCATE_SMARTSEARCH_content-text"> Quickly search for <strong>games</strong> above! </div> `; } else if (btn.textContent === "Users") { contentArea.innerHTML = ` <div class="ROLOCATE_SMARTSEARCH_content-text"> Instantly find the <strong>user</strong> you're looking for! </div> `; } else if (btn.textContent === "Groups") { contentArea.innerHTML = ` <div class="ROLOCATE_SMARTSEARCH_content-text"> Search for <strong>groups</strong> rapidly. </div> `; } } else { btn.classList.remove('ROLOCATE_SMARTSEARCH_active'); } }); } return true; } /******************************************************* name of function: quicklaunchgamesfunction description: adds quick launch *******************************************************/ function quicklaunchgamesfunction() { if (!/^https?:\/\/(www\.)?roblox\.com(\/[a-z]{2})?\/home\/?$/i.test(window.location.href)) return; if (localStorage.getItem('ROLOCATE_quicklaunchgames') === 'true') { const observer = new MutationObserver((mutations, obs) => { const friendsSection = document.querySelector('.friend-carousel-container'); const friendTiles = document.querySelectorAll('.friends-carousel-tile'); if (friendsSection && friendTiles.length > 1) { obs.disconnect(); // Create new games section with premium styling const newGamesContainer = document.createElement('div'); newGamesContainer.className = 'ROLOCATE_QUICKLAUNCHGAMES_new-games-container'; newGamesContainer.innerHTML = ` <div class="container-header people-list-header"> <div class="ROLOCATE_QUICKLAUNCHGAMES_header-content"> <div class="ROLOCATE_QUICKLAUNCHGAMES_title">Quick Launch Games</div> <div class="ROLOCATE_QUICKLAUNCHGAMES_subtitle">Quickly play your games from here!</div> </div> </div> <div class="ROLOCATE_QUICKLAUNCHGAMES_game-grid-container"> <div class="ROLOCATE_QUICKLAUNCHGAMES_game-grid"> <div class="ROLOCATE_QUICKLAUNCHGAMES_add-tile" id="ROLOCATE_QUICKLAUNCHGAMES_add-button"> <div class="ROLOCATE_QUICKLAUNCHGAMES_add-content"> <svg class="ROLOCATE_QUICKLAUNCHGAMES_add-icon" width="28" height="28" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M12 5V19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M5 12H19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> <div class="ROLOCATE_QUICKLAUNCHGAMES_add-text">Add Game</div> </div> </div> </div> </div> `; // 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 = ` <div class="ROLOCATE_QUICKLAUNCHGAMES_popup"> <h3>Add New Game</h3> <label for="gameIdInput">Game ID:</label> <input type="text" id="gameIdInput" placeholder="Enter game ID | RoLocate by Oqarshi"> <small style="display:block; margin-top:4px; color:#aaa;"> Example: roblox.com/games/<b style="color:#4da6ff;">17625359962</b>/RIVALS </small> <div class="ROLOCATE_QUICKLAUNCHGAMES_popup-buttons" style="margin-top:12px;"> <button class="ROLOCATE_QUICKLAUNCHGAMES_popup-button cancel">Cancel</button> <button class="ROLOCATE_QUICKLAUNCHGAMES_popup-button confirm">Add Game</button> </div> </div> `; document.body.appendChild(overlay); setTimeout(() => { document.getElementById('gameIdInput').focus(); }, 100); // Event listeners const cancelBtn = overlay.querySelector('.cancel'); const confirmBtn = overlay.querySelector('.confirm'); cancelBtn.addEventListener('click', () => { overlay.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_popup').classList.add('fade-out'); setTimeout(() => overlay.remove(), 300); }); confirmBtn.addEventListener('click', async () => { const gameId = document.getElementById('gameIdInput').value.trim(); if (!gameId) { notifications('Please enter a game ID', 'error', '⚠️', '4000'); return; } if (!/^\d+$/.test(gameId)) { notifications('Game ID must be numeric', 'error', '⚠️', '4000'); return; } const games = JSON.parse(localStorage.getItem('ROLOCATE_quicklaunch_games_storage') || '[]'); if (games.includes(gameId)) { notifications('Game already added!', 'error', '⚠️', '4000'); return; } // Show loading state confirmBtn.textContent = 'Adding...'; confirmBtn.disabled = true; try { // Get game details const universeId = await getUniverseIdFromPlaceId_quicklaunch(gameId); const gameDetails = await getGameDetails(universeId); games.push(gameId); localStorage.setItem('ROLOCATE_quicklaunch_games_storage', JSON.stringify(games)); addGameTile(gameId, gameDetails); // Only fade out on success overlay.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_popup').classList.add('fade-out'); setTimeout(() => overlay.remove(), 300); } catch (error) { notifications('Error adding game: ' + error.message, 'error', '⚠️', '4000'); confirmBtn.textContent = 'Add Game'; confirmBtn.disabled = false; } // Remove these two lines - they were causing the problem }); } // Add game tile with animations and API data function addGameTile(gameId, gameDetails = null) { const gameGrid = document.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_game-grid'); if (!gameGrid) return; const gameTile = document.createElement('div'); gameTile.className = 'ROLOCATE_QUICKLAUNCHGAMES_game-tile'; gameTile.dataset.gameId = gameId; // Create tile with placeholder content gameTile.innerHTML = ` <a href="https://www.roblox.com/games/${gameId}#?ROLOCATE_QUICKJOIN" target="_blank"> <div class="thumbnail-container"> <div style="width:100%;height:100%;background:linear-gradient(135deg,#23252d,#1e2028);display:flex;align-items:center;justify-content:center;"> <svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M4 8H20V16H4V8Z" stroke="#4a4d56" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M8 4V8" stroke="#4a4d56" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M16 4V8" stroke="#4a4d56" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> </div> </div> <div class="ROLOCATE_QUICKLAUNCHGAMES_game-name">Loading...</div> <div class="ROLOCATE_QUICKLAUNCHGAMES_game-info"> <div class="ROLOCATE_QUICKLAUNCHGAMES_like-ratio"> <span class="thumb">👍</span> - </div> <div class="ROLOCATE_QUICKLAUNCHGAMES_game-stat ROLOCATE_QUICKLAUNCHGAMES_player-count">-</div> </div> </a> <div class="ROLOCATE_QUICKLAUNCHGAMES_remove-button"></div> `; gameGrid.insertBefore(gameTile, gameGrid.firstChild); // Add remove functionality const removeBtn = gameTile.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_remove-button'); removeBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); // Animated removal with bounce effect gameTile.classList.add('removing'); setTimeout(() => { const games = JSON.parse(localStorage.getItem('ROLOCATE_quicklaunch_games_storage') || '[]'); const updatedGames = games.filter(id => id !== gameId); localStorage.setItem('ROLOCATE_quicklaunch_games_storage', JSON.stringify(updatedGames)); gameTile.remove(); }, 400); }); // Load game details asynchronously const loadGameDetails = async () => { try { const universeId = await getUniverseIdFromPlaceId_quicklaunch(gameId); const [iconUrl, details] = await Promise.all([ getGameIconFromUniverseId_quicklaunch(universeId), gameDetails || getGameDetails(universeId) ]); // Update thumbnail const thumbContainer = gameTile.querySelector('.thumbnail-container'); thumbContainer.innerHTML = `<img src="${iconUrl}" alt="${details.name}" onerror="this.src='https://via.placeholder.com/160x160?text=No+Image'">`; // Update game name const gameName = gameTile.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_game-name'); gameName.textContent = details.name || 'Unknown Game'; // Update stats const playerCount = gameTile.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_player-count'); const likeRatio = gameTile.querySelector('.ROLOCATE_QUICKLAUNCHGAMES_like-ratio'); playerCount.textContent = formatNumber(details.playing); // Calculate like ratio (using favorites as proxy) const ratio = details.favoritedCount > 0 ? Math.round((details.favoritedCount / (details.favoritedCount + (details.favoritedCount * 0.1))) * 100) : 0; likeRatio.innerHTML = `<span class="thumb">👍</span> ${ratio}%`; } catch (error) { 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 = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="opacity: 0.7; margin-right: 10px;"> <path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M9.09 9C9.3251 8.33167 9.78915 7.76811 10.4 7.40913C11.0108 7.05016 11.7289 6.91894 12.4272 7.03871C13.1255 7.15849 13.7588 7.52152 14.2151 8.06353C14.6713 8.60553 14.9211 9.29152 14.92 10C14.92 12 11.92 13 11.92 13" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12 17H12.01" stroke="${theme.accentPrimary}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg>No Recent Servers Found`; emptyMessage.style.cssText = ` color: ${theme.textSecondary}; text-align: center; padding: 28px 0; font-size: 14px; letter-spacing: 0.3px; font-weight: 500; display: flex; align-items: center; justify-content: center; background: rgba(20, 22, 26, 0.4); 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 = ` <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M2 17L12 22L22 17" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M2 12L12 17L22 12" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M2 7L12 12L22 7L12 2L2 7Z" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg> `; serverIconWrapper.appendChild(serverIcon); // 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 = `<span style="color: ${theme.accentPrimary};">Region:</span> `; if (flagElement && (flagElement.nodeType === Node.ELEMENT_NODE || flagElement.nodeType === Node.TEXT_NODE)) { if (flagElement.nodeType === Node.ELEMENT_NODE) { flagElement.style.position = 'relative'; flagElement.style.top = '-2px'; } regionInfo.appendChild(flagElement); } else { regionInfo.appendChild(document.createTextNode('🌍')); } // 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 = ` <svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> `; removeButton.className = 'btn-control-xs remove-button'; removeButton.style.cssText = ` background: ${theme.dangerGradient}; color: white; border: none; padding: 6px; border-radius: 8px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all 0.15s ease; letter-spacing: 0.4px; box-shadow: 0 2px 8px rgba(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 = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="opacity: 0.7; margin-right: 10px;"> <path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M9.09 9C9.3251 8.33167 9.78915 7.76811 10.4 7.40913C11.0108 7.05016 11.7289 6.91894 12.4272 7.03871C13.1255 7.15849 13.7588 7.52152 14.2151 8.06353C14.6713 8.60553 14.9211 9.29152 14.92 10C14.92 12 11.92 13 11.92 13" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12 17H12.01" stroke="${theme.accentPrimary}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg>No Recent Servers Found`; emptyMessage.style.cssText = ` color: ${theme.textSecondary}; text-align: center; padding: 28px 0; font-size: 14px; letter-spacing: 0.3px; font-weight: 500; display: flex; align-items: center; justify-content: center; background: rgba(20, 22, 26, 0.4); 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 = ` <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 6px;"> <path d="M5 12H19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12 5L19 12L12 19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> Join `; joinButton.className = 'btn-control-xs join-button'; joinButton.style.cssText = ` background: ${theme.accentGradient}; color: white; border: none; padding: 8px 18px; border-radius: 10px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all 0.15s ease; letter-spacing: 0.4px; box-shadow: 0 2px 10px rgba(52, 100, 201, 0.3); display: flex; align-items: center; justify-content: center; `; // 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 = ` <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 6px;"> <path d="M16 18L18 20L22 16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M20 12V13.4C20 13.4 19.5 13 19 13C18.5 13 18 13.5 18 14C18 14.5 18.5 15 19 15C19.5 15 20 14.6 20 14.6V16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M4 20C4 17 7 17 8 17C9 17 13 17 13 17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> <path d="M9.5 10C10.8807 10 12 8.88071 12 7.5C12 6.11929 10.8807 5 9.5 5C8.11929 5 7 6.11929 7 7.5C7 8.88071 8.11929 10 9.5 10Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg> Invite `; inviteButton.className = 'btn-control-xs invite-button'; inviteButton.style.cssText = ` background: rgba(28, 31, 37, 0.6); color: ${theme.textPrimary}; border: 1px solid rgba(255, 255, 255, 0.12); padding: 8px 18px; border-radius: 10px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.15s ease; display: flex; align-items: center; justify-content: center; 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 = ` <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 6px;"> <path d="M20 6L9 17L4 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> Copied! `; ConsoleLogEnabled(`Invite link copied to clipboard`); notifications('Success! Invite link copied to clipboard!', 'success', '🎉', '2000'); // 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 = ` <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M12 8V12V8ZM12 16H12.01H12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="currentColor" stroke-width="1.5"/> </svg> `; moreInfoButton.className = 'btn-control-xs more-info-button'; moreInfoButton.style.cssText = ` background: rgba(28, 31, 37, 0.6); color: ${theme.textPrimary}; border: 1px solid rgba(255, 255, 255, 0.12); padding: 8px; border-radius: 10px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.15s ease; display: flex; align-items: center; justify-content: center; 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 = ` <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M2 17L12 22L22 17" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M2 12L12 17L22 12" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M2 7L12 12L22 7L12 2L2 7Z" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg> `; popupTitle.prepend(serverIconPopup); popupHeader.appendChild(popupTitle); // 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 || ` <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M12 8V12V8ZM12 16H12.01H12Z" stroke="${theme.accentPrimary}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="${theme.accentPrimary}" stroke-width="1.5"/> </svg> `; const textContainer = document.createElement('div'); const labelEl = document.createElement('div'); labelEl.textContent = label; labelEl.style.cssText = ` color: ${theme.textSecondary}; font-size: 12px; font-weight: 500; margin-bottom: 4px; `; const valueEl = document.createElement('div'); valueEl.textContent = value; valueEl.style.cssText = ` color: ${theme.textPrimary}; font-size: 14px; font-weight: 600; word-break: break-all; `; textContainer.appendChild(labelEl); textContainer.appendChild(valueEl); item.appendChild(iconContainer); item.appendChild(textContainer); return item; } // Add info items infoItems.appendChild(createInfoItem('Game ID', gameId, ` <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M3.27 6.96L12 12.01L20.73 6.96" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12 22.08V12" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg> `)); infoItems.appendChild(createInfoItem('Server ID', serverId, ` <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M5 12.55L11 17.75L19 6.95" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="${theme.accentPrimary}" stroke-width="1.5"/> </svg> `)); infoItems.appendChild(createInfoItem('Region', region, ` <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M12 2C8.13 2 5 5.13 5 9C5 14.25 12 22 12 22C12 22 19 14.25 19 9C19 5.13 15.87 2 12 2Z" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12 11.5C13.3807 11.5 14.5 10.3807 14.5 9C14.5 7.61929 13.3807 6.5 12 6.5C10.6193 6.5 9.5 7.61929 9.5 9C9.5 10.3807 10.6193 11.5 12 11.5Z" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg> `)); const formattedLastPlayed = formatLastPlayedWithRelative(lastPlayed); infoItems.appendChild(createInfoItem('Last Played', formattedLastPlayed, `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M12 8V12L15 15" stroke="${theme.accentPrimary}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="${theme.accentPrimary}" stroke-width="1.5"/> </svg>` )); // 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 = ` <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M8 4H4C3.46957 4 2.96086 4.21071 2.58579 4.58579C2.21071 4.96086 2 5.46957 2 6V18C2 18.5304 2.21071 19.0391 2.58579 19.4142C2.96086 19.7893 3.46957 20 4 20H16C16.5304 20 17.0391 19.7893 17.4142 19.4142C17.7893 19.0391 18 18.5304 18 18V14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M16 4H20C20.5304 4 21.0391 4.21071 21.4142 4.58579C21.7893 4.96086 22 5.46957 22 6V10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M8 10H7C6.73478 10 6.48043 10.1054 6.29289 10.2929C6.10536 10.4804 6 10.7348 6 11V17C6 17.2652 6.10536 17.5196 6.29289 17.7071C6.48043 17.8946 6.73478 18 7 18H13C13.2652 18 13.5196 17.8946 13.7071 17.7071C13.8946 17.5196 14 17.2652 14 17V16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M16 16H22" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M16 20H22" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg> Copy Info `; copyButton.addEventListener('click', function() { const infoText = `Game ID: ${gameId}\nServer ID: ${serverId}\nRegion: ${region}\nLast Played: ${lastPlayed}`; navigator.clipboard.writeText(infoText); copyButton.innerHTML = ` <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M20 6L9 17L4 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> Copied! `; setTimeout(() => { copyButton.innerHTML = ` <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M8 4H4C3.46957 4 2.96086 4.21071 2.58579 4.58579C2.21071 4.96086 2 5.46957 2 6V18C2 18.5304 2.21071 19.0391 2.58579 19.4142C2.96086 19.7893 3.46957 20 4 20H16C16.5304 20 17.0391 19.7893 17.4142 19.4142C17.7893 19.0391 18 18.5304 18 18V14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M16 4H20C20.5304 4 21.0391 4.21071 21.4142 4.58579C21.7893 4.96086 22 5.46957 22 6V10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M8 10H7C6.73478 10 6.48043 10.1054 6.29289 10.2929C6.10536 10.4804 6 10.7348 6 11V17C6 17.2652 6.10536 17.5196 6.29289 17.7071C6.48043 17.8946 6.73478 18 7 18H13C13.2652 18 13.5196 17.8946 13.7071 17.7071C13.8946 17.5196 14 17.2652 14 17V16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M16 16H22" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M16 20H22" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg> 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, "<b style='color: #068f00;'>$1</b>"); const buttonText = document.createElement('p'); buttonText.style.cssText = ` margin: 0; color: white; font-size: 16px; `; buttonText.textContent = data.name; // Add "DISABLED" style if the button is disabled if (data.disabled) { // Show explanation tooltip (left side like experimental) const disabledTooltip = document.createElement('div'); disabledTooltip.className = 'disabled-tooltip'; disabledTooltip.style.cssText = ` display: none; position: absolute; top: 0; right: 200px; width: 200px; background-color: #191B1D; color: white; padding: 5px; border-radius: 5px; font-size: 14px; white-space: pre-wrap; z-index: 1001; opacity: 1; `; disabledTooltip.innerHTML = data.disabledExplanation.replace(/\*\*(.*?)\*\*/g, '<span style="font-weight: bold; color: #ff5555;">$1</span>'); buttonContainer.appendChild(disabledTooltip); // Add disabled indicator const disabledIndicator = document.createElement('span'); disabledIndicator.textContent = 'DISABLED'; disabledIndicator.style.cssText = ` margin-left: 8px; color: #ff5555; font-size: 10px; font-weight: bold; background-color: rgba(255, 85, 85, 0.1); padding: 1px 4px; border-radius: 3px; `; buttonText.appendChild(disabledIndicator); // Show on hover buttonContainer.addEventListener('mouseenter', () => { disabledTooltip.style.display = 'block'; }); buttonContainer.addEventListener('mouseleave', () => { disabledTooltip.style.display = 'none'; }); } // Add "EXP" label if the button is experimental if (data.experimental) { const expLabel = document.createElement('span'); expLabel.textContent = 'EXP'; expLabel.style.cssText = ` margin-left: 8px; color: gold; font-size: 12px; font-weight: bold; background-color: rgba(255, 215, 0, 0.1); padding: 2px 6px; border-radius: 3px; `; buttonText.appendChild(expLabel); } // Add "POPULAR" label if the button is popular if (data.popular) { const popularLabel = document.createElement('span'); popularLabel.textContent = 'Popular'; popularLabel.style.cssText = ` margin-left: 8px; color: #4CAF50; font-size: 10px; font-weight: bold; background-color: rgba(76, 175, 80, 0.1); padding: 2px 6px; border-radius: 3px; `; buttonText.appendChild(popularLabel); } // add new tooltip let newTooltip = null; if (data.new) { const newLabel = document.createElement('span'); newLabel.textContent = 'NEW'; newLabel.style.cssText = ` margin-left: 8px; color: #2196F3; font-size: 12px; font-weight: bold; background-color: rgba(33, 150, 243, 0.1); padding: 2px 6px; border-radius: 3px; `; buttonText.appendChild(newLabel); // Add NEW explanation tooltip (left side) const newTooltip = document.createElement('div'); newTooltip.className = 'new-tooltip'; newTooltip.style.cssText = ` display: none; position: absolute; top: 0; right: 200px; width: 200px; background-color: #191B1D; color: white; padding: 5px; border-radius: 5px; font-size: 14px; white-space: pre-wrap; z-index: 1001; opacity: 1; `; newTooltip.innerHTML = "<span style='font-weight: bold; color: #2196F3;'>New Feature</span>: This feature was recently added. There is no guarantee it will work."; buttonContainer.appendChild(newTooltip); // Show on hover buttonContainer.addEventListener('mouseenter', () => { newTooltip.style.display = 'block'; }); buttonContainer.addEventListener('mouseleave', () => { newTooltip.style.display = 'none'; }); } // Add experimental explanation tooltip (left side) let experimentalTooltip = null; if (data.experimental) { experimentalTooltip = document.createElement('div'); experimentalTooltip.className = 'experimental-tooltip'; experimentalTooltip.style.cssText = ` display: none; position: absolute; top: 0; right: 200px; width: 200px; background-color: #191B1D; color: white; padding: 5px; border-radius: 5px; font-size: 14px; white-space: pre-wrap; z-index: 1001; opacity: 1; `; // Function to replace **text** with bold and gold styled text const formatText = (text) => { return text.replace(/\*\*(.*?)\*\*/g, '<span style="font-weight: bold; color: gold;">$1</span>'); }; // Apply the formatting to the experimental explanation experimentalTooltip.innerHTML = formatText(data.experimentalExplanation); buttonContainer.appendChild(experimentalTooltip); } // Append tooltip directly to button container so it won't inherit opacity buttonContainer.appendChild(tooltip); // Append button text to content wrapper buttonContentWrapper.appendChild(buttonText); // Append content wrapper to button container buttonContainer.appendChild(buttonContentWrapper); // In the event listeners: buttonContainer.addEventListener('mouseover', () => { tooltip.style.display = 'block'; if (data.experimental && experimentalTooltip) { experimentalTooltip.style.display = 'block'; } if (data.new && newTooltip) { // <-- Only show if it exists newTooltip.style.display = 'block'; } if (!data.disabled) { buttonContainer.style.backgroundColor = '#4A4C4E'; buttonContainer.style.transform = 'translateY(0px) scale(1.02)'; } }); buttonContainer.addEventListener('mouseout', () => { tooltip.style.display = 'none'; if (data.experimental && experimentalTooltip) { experimentalTooltip.style.display = 'none'; } if (data.new && newTooltip) { // <-- Only hide if it exists newTooltip.style.display = 'none'; } if (!data.disabled) { buttonContainer.style.backgroundColor = '#393B3D'; buttonContainer.style.transform = 'translateY(0px) scale(1)'; } }); buttonContainer.addEventListener('click', () => { // Prevent click functionality for disabled buttons if (data.disabled) { return; } // Add click animation buttonContainer.style.transform = 'translateY(0px) scale(0.95)'; setTimeout(() => { buttonContainer.style.transform = 'translateY(0px) scale(1)'; }, 150); switch (index) { case 0: smallest_servers(); break; case 1: available_space_servers(); break; case 2: player_count_tab(); break; case 3: random_servers(); break; case 4: createServerCountPopup((totalLimit) => { rebuildServerList(gameId, totalLimit); }); break; case 5: rebuildServerList(gameId, 50, true); break; case 6: auto_join_small_server(); break; case 7: scanRobloxServers(); break; } }); popup.appendChild(buttonContainer); }); // trigger the button animations after DOM insertion // this should be called after the popup is added to the DOM setTimeout(() => { // animate buttons in sequence from top to bottom const buttons = popup.querySelectorAll('.server-filter-option'); buttons.forEach((button, index) => { setTimeout(() => { button.style.transform = 'translateY(0px)'; button.style.opacity = '1'; }, index * 30); // 30 ms from each button }); }, 20); return popup; } /******************************************************* name of function: ServerHop description: Handles server hopping by fetching and joining a random server, excluding recently joined servers. *******************************************************/ function ServerHop() { ConsoleLogEnabled("Starting server hop..."); // 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 <style> element const style = document.createElement('style'); style.textContent = ` /* Overlay for the modal background */ .overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.85); /* Solid black overlay */ z-index: 1000; /* Ensure overlay is below the popup */ opacity: 0; /* Start invisible */ animation: fadeIn 0.3s ease forwards; /* Fade-in animation */ } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } /* Popup Container for the server region */ .filter-popup { background-color: #1e1e1e; /* Darker background */ color: #ffffff; /* White text */ padding: 25px; border-radius: 12px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.5); width: 320px; max-width: 90%; position: fixed; /* Fixed positioning */ top: 50%; /* Center vertically */ left: 50%; /* Center horizontally */ transform: translate(-50%, -50%); /* Offset to truly center */ text-align: center; z-index: 1001; /* Ensure popup is above the overlay */ border: 1px solid #444; /* Subtle border */ opacity: 0; /* Start invisible */ animation: fadeInPopup 0.3s ease 0.1s forwards; /* Fade-in animation with delay */ } @keyframes fadeInPopup { from { opacity: 0; transform: translate(-50%, -55%); /* Slight upward offset */ } to { opacity: 1; transform: translate(-50%, -50%); /* Center position */ } } /* Fade-out animation for overlay and popup */ .overlay.fade-out { animation: fadeOut 0.3s ease forwards; } .filter-popup.fade-out { animation: fadeOutPopup 0.3s ease forwards; } @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } @keyframes fadeOutPopup { from { opacity: 1; transform: translate(-50%, -50%); /* Center position */ } to { opacity: 0; transform: translate(-50%, -55%); /* Slight upward offset */ } } /* Label */ .filter-popup label { display: block; margin-bottom: 12px; font-size: 16px; color: #ffffff; font-weight: 500; /* Slightly bolder text */ } /* Dropdown */ .filter-popup select { background-color: #333; /* Darker gray background */ color: #ffffff; /* White text */ padding: 10px; border-radius: 6px; border: 1px solid #555; /* Darker border */ width: 100%; margin-bottom: 12px; font-size: 14px; transition: border-color 0.3s ease; } .filter-popup select:focus { border-color: #888; /* Lighter border on focus */ outline: none; } /* Custom Input */ .filter-popup input[type="number"] { background-color: #333; /* Darker gray background */ color: #ffffff; /* White text */ padding: 10px; border-radius: 6px; border: 1px solid #555; /* Darker border */ width: 100%; margin-bottom: 12px; font-size: 14px; transition: border-color 0.3s ease; } .filter-popup input[type="number"]:focus { border-color: #888; /* Lighter border on focus */ outline: none; } /* Confirm Button */ #confirmServerCount { background-color: #444; /* Dark gray background */ color: #ffffff; /* White text */ padding: 10px 20px; border: 1px solid #666; /* Gray border */ border-radius: 6px; cursor: pointer; font-size: 14px; width: 100%; transition: background-color 0.3s ease, transform 0.2s ease; } #confirmServerCount:hover { background-color: #555; /* Lighter gray on hover */ transform: translateY(-1px); /* Slight lift effect */ } #confirmServerCount:active { transform: translateY(0); /* Reset lift effect on click */ } /* Highlighted server item */ .rbx-game-server-item.highlighted { border: 2px solid #4caf50; /* Green border */ border-radius: 8px; background-color: rgba(76, 175, 80, 0.1); /* Subtle green background */ } /* Disabled fetch button */ .fetch-button:disabled { opacity: 0.5; cursor: not-allowed; } /* Popup Header for server coutnodwn */ .popup-header { margin-bottom: 24px; text-align: left; padding: 16px; background-color: rgba(255, 255, 255, 0.05); /* Subtle background for contrast */ border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.1); /* Subtle border */ transition: background-color 0.3s ease, border-color 0.3s ease; } .popup-header:hover { background-color: rgba(255, 255, 255, 0.08); /* Slightly brighter on hover */ border-color: rgba(255, 255, 255, 0.2); } .popup-header h3 { margin: 0 0 12px 0; font-size: 22px; color: #ffffff; font-weight: 700; /* Bolder for emphasis */ letter-spacing: -0.5px; /* Tighter letter spacing for modern look */ } .popup-header p { margin: 0; font-size: 14px; color: #cccccc; line-height: 1.6; /* Improved line height for readability */ opacity: 0.9; /* Slightly transparent for a softer look */ } /* Popup Footer */ .popup-footer { margin-top: 20px; text-align: left; font-size: 14px; color: #ffcc00; /* Yellow color for warnings */ background-color: rgba(255, 204, 0, 0.15); /* Lighter yellow background */ padding: 12px; border-radius: 8px; border: 1px solid rgba(255, 204, 0, 0.15); /* Subtle border */ transition: background-color 0.3s ease, border-color 0.3s ease; } .popup-footer:hover { background-color: rgba(255, 204, 0, 0.25); /* Slightly brighter on hover */ border-color: rgba(255, 204, 0, 0.25); } .popup-footer p { margin: 0; line-height: 1.5; font-weight: 500; /* Slightly bolder for emphasis */ } /* Label */ .filter-popup label { display: block; margin-bottom: 12px; font-size: 15px; color: #ffffff; font-weight: 500; text-align: left; opacity: 0.9; /* Slightly transparent for a softer look */ transition: opacity 0.3s ease; } .filter-popup label:hover { opacity: 1; /* Fully opaque on hover */ } select:hover, select:focus { border-color: #ffffff; outline: none; } `; // Append the <style> element to the document head document.head.appendChild(style); } /******************************************************* name of function: showMessage description: Shows the good looking messages on the bottom of server region search *******************************************************/ function showMessage(message) { const loadMoreButtonContainer = document.querySelector('.rbx-public-running-games-footer'); if (!loadMoreButtonContainer) { ConsoleLogEnabled("Error: 'Load More' button container not found! Ensure the element exists in the DOM."); return; } const existingMessage = loadMoreButtonContainer.querySelector('.premium-message-container'); // If message is "END", remove any existing message and exit if (message === "END") { if (existingMessage) { existingMessage.remove(); ConsoleLogEnabled("Message container removed."); } else { ConsoleLogEnabled("No message container found to remove."); } return; } // Remove existing message if present before showing a new one if (existingMessage) { existingMessage.remove(); ConsoleLogEnabled("Warning: An existing message was found and replaced."); } // Inject CSS only once if (!document.getElementById('premium-message-styles')) { const style = document.createElement('style'); style.id = 'premium-message-styles'; style.textContent = ` .premium-message-container { margin-top: 20px; padding: 18px 26px; background: linear-gradient(145deg, #2b0000, #1a0000); border-radius: 14px; box-shadow: 0 6px 20px rgba(255, 0, 0, 0.2); font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 16px; color: #ffdddd; transition: all 0.3s ease-in-out, transform 0.3s ease, box-shadow 0.3s ease; opacity: 0; animation: fadeIn 0.6s ease forwards; border: 1px solid #440000; backdrop-filter: blur(6px); display: flex; align-items: center; gap: 16px; cursor: default; user-select: none; } .premium-message-container:hover { transform: scale(1.015); box-shadow: 0 8px 24px rgba(255, 0, 0, 0.25); background: linear-gradient(145deg, #330000, #220000); color: #ffe5e5; } .premium-message-logo { width: 28px; height: 28px; border-radius: 6px; object-fit: contain; box-shadow: 0 0 8px rgba(255, 0, 0, 0.2); background-color: #000; } .premium-message-text { flex: 1; text-align: left; font-weight: 500; letter-spacing: 0.3px; } @keyframes fadeIn { to { opacity: 1; } } `; document.head.appendChild(style); } // Create the message container const container = document.createElement('div'); container.className = 'premium-message-container'; // Create and insert the logo const logo = document.createElement('img'); logo.className = 'premium-message-logo'; logo.src = window.Base64Images.logo; // Create and insert the message text const messageText = document.createElement('div'); messageText.className = 'premium-message-text'; messageText.textContent = message; // Build the full component container.appendChild(logo); container.appendChild(messageText); loadMoreButtonContainer.appendChild(container); ConsoleLogEnabled("Message displayed successfully:", message); return container; } /******************************************************* name of function: fetchServerDetails description: Function to fetch server details so game id and job id. yea! *******************************************************/ // WARNING: Do not republish this script. Licensed for personal use only. async function fetchServerDetails(gameId, jobId) { //here! const useBatching = localStorage.ROLOCATE_fastservers === "true"; if (!useBatching) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: "https://gamejoin.roblox.com/v1/join-game-instance", headers: { "Content-Type": "application/json", "User-Agent": "Roblox/WinInet", }, data: JSON.stringify({ placeId: gameId, gameId: jobId }), onload: function(response) { const json = JSON.parse(response.responseText); ConsoleLogEnabled("API Response:", json); if (json.status === 12 && json.message === 'You need to purchase access to this game before you can play.') { reject('purchase_required'); return; } if (json.status === 12 && json.message === 'Cannot join this non-root place due to join restrictions') { reject('subplace_join_restriction'); return; } const address = json?.joinScript?.UdmuxEndpoints?.[0]?.Address ?? json?.joinScript?.MachineAddress; if (!address) { ConsoleLogEnabled("API Response (Unknown Location) Which means Full Server!:", json); reject(`Unable to fetch server location: Status ${json.status}`); return; } function findBestMatchingIp(address) { // If IP starts with 128.116.*, replace last octet with 0 if (/^128\.116\.\d+\.\d+$/.test(address)) { return address.replace(/^(128\.116\.\d+)\.\d+$/, "$1.0"); } // Otherwise, find any IP in serverRegionsByIp sharing first two octets const firstTwoOctetsMatch = address.match(/^(\d+\.\d+)\./); if (firstTwoOctetsMatch) { const firstTwoOctets = firstTwoOctetsMatch[1]; const match = Object.keys(serverRegionsByIp).find(ip => ip.startsWith(firstTwoOctets + ".")); return match || address; } // If format unexpected, fallback to original IP return address; } const lookupIp = findBestMatchingIp(address); const location = serverRegionsByIp[lookupIp]; if (!location) { ConsoleLogEnabled("API Response (Unknown Location):", json); reject(`Unknown server address ${address}`); return; } resolve(location); }, onerror: function(error) { ConsoleLogEnabled("API Request Failed:", error); reject(`Failed to fetch server details: ${error}`); }, }); }); } // Batching logic with rate limit handling const queue = fetchServerDetails._queue || []; const concurrencyLimit = 10; if (!fetchServerDetails._queue) { fetchServerDetails._queue = queue; fetchServerDetails._activeCount = 0; fetchServerDetails._rateLimited = false; } return new Promise((resolve, reject) => { const makeRequest = async (gameId, jobId) => { return new Promise((innerResolve, innerReject) => { GM_xmlhttpRequest({ method: "POST", url: "https://gamejoin.roblox.com/v1/join-game-instance", headers: { "Content-Type": "application/json", "User-Agent": "Roblox/WinInet", }, data: JSON.stringify({ placeId: gameId, gameId: jobId }), onload: function(response) { const json = JSON.parse(response.responseText); ConsoleLogEnabled("API Response:", json); // Check if we got rate limited (status undefined) if (json.status === undefined) { ConsoleLogEnabled("Rate limited detected - status undefined"); innerReject('rate_limited'); return; } if (json.status === 12 && json.message === 'You need to purchase access to this game before you can play.') { innerReject('purchase_required'); return; } if (json.status === 12 && json.message === 'Cannot join this non-root place due to join restrictions') { innerReject('subplace_join_restriction'); return; } const address = json?.joinScript?.UdmuxEndpoints?.[0]?.Address ?? json?.joinScript?.MachineAddress; if (!address) { ConsoleLogEnabled("API Response (Unknown Location) Which means Full Server!:", json); innerReject(`Unable to fetch server location: Status ${json.status}`); return; } function findBestMatchingIp(address) { // If IP starts with 128.116.*, replace last octet with 0 if (/^128\.116\.\d+\.\d+$/.test(address)) { ConsoleLogEnabled("using 128.116 rule"); return address.replace(/^(128\.116\.\d+)\.\d+$/, "$1.0"); } // Otherwise, find any IP in serverRegionsByIp sharing first two octets const firstTwoOctetsMatch = address.match(/^(\d+\.\d+)\./); if (firstTwoOctetsMatch) { ConsoleLogEnabled("using two octect rule"); const firstTwoOctets = firstTwoOctetsMatch[1]; const match = Object.keys(serverRegionsByIp).find(ip => ip.startsWith(firstTwoOctets + ".")); return match || address; } // If format unexpected, fallback to original IP return address; } const lookupIp = findBestMatchingIp(address); const location = serverRegionsByIp[lookupIp]; if (!location) { ConsoleLogEnabled("API Response (Unknown Location):", json); innerReject(`Unknown server address ${address}`); return; } innerResolve(location); }, onerror: function(error) { ConsoleLogEnabled("API Request Failed:", error); innerReject(`Failed to fetch server details: ${error}`); }, }); }); }; const task = async () => { try { fetchServerDetails._activeCount++; let result; let attempts = 0; const maxAttempts = 100; // Prevent infinite loops while (attempts < maxAttempts) { try { result = await makeRequest(gameId, jobId); // If we get here, request was successful if (fetchServerDetails._rateLimited) { ConsoleLogEnabled("Rate limit cleared, resuming normal operation"); fetchServerDetails._rateLimited = false; } break; } catch (err) { if (err === 'rate_limited') { if (!fetchServerDetails._rateLimited) { ConsoleLogEnabled("Rate limited - retrying every second until cleared"); fetchServerDetails._rateLimited = true; } ConsoleLogEnabled(`Rate limit retry attempt ${attempts + 1}`); await delay(1000); // Wait 1 second before retry attempts++; } else { // For other errors, don't retry throw err; } } } if (attempts >= maxAttempts) { throw new Error(`Rate limited for too long, exceeded ${maxAttempts} attempts`); } resolve(result); } catch (err) { reject(err); } finally { fetchServerDetails._activeCount--; if (queue.length > 0) { const next = queue.shift(); next(); } } }; if (fetchServerDetails._activeCount < concurrencyLimit) { task(); } else { queue.push(task); } }); } /******************************************************* name of function: delay description: custom delay also known as sleep function in js cause this language sucks and doesent have a default built-in sleep. *******************************************************/ function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /******************************************************* name of function: createServerCountPopup description: Creates the first time popup and allows user to pick the amount of servers they want. *******************************************************/ // WARNING: Do not republish this script. Licensed for personal use only. function createServerCountPopup(callback) { const overlay = document.createElement('div'); overlay.className = 'overlay'; const popup = document.createElement('div'); popup.className = 'filter-popup'; popup.style.width = '460px'; // get current player count preference from localStorage const currentPlayerCountPreference = localStorage.getItem('ROLOCATE_invertplayercount'); const isLowPlayerCount = currentPlayerCountPreference === 'true'; // inject styles for dropdown icon const style = document.createElement('style'); style.textContent = ` /* NEW: Grid container for the dropdowns */ .filter-grid { display: grid; grid-template-columns: 1fr 1fr; /* Create two equal columns */ gap: 20px; /* Space between the columns */ margin-bottom: 15px; } .dropdown-wrapper { position: relative; display: inline-block; width: 100%; } .dropdown-wrapper select { width: 100%; padding-right: 30px; appearance: none; -webkit-appearance: none; -moz-appearance: none; } .dropdown-wrapper .dropdown-icon { position: absolute; right: 10px; top: 40%; transform: translateY(-50%); pointer-events: none; font-size: 12px; color: #fff; } .filter-section label { display: block; margin-bottom: 5px; font-weight: 600; } #cancelServerCount { background-color: #2a1f1f; border: 1px solid #3d2626; border-radius: 6px; font-size: 14px; cursor: pointer; transition: background-color 0.3s ease, transform 0.2s ease; } #cancelServerCount:hover { background-color: #332222; transform: translateY(-1px); /* Slight lift effect */ } #cancelServerCount:active { transform: translateY(0); /* Reset lift effect on click */ } `; document.head.appendChild(style); popup.innerHTML = ` <div class="popup-header"> <h3>Select Number of Servers</h3> <p><strong>More servers = more variety, but longer search times.</strong></p> </div> <div class="filter-grid"> <div class="filter-section"> <label for="serverCount">Number of Servers:</label> <div class="dropdown-wrapper"> <select id="serverCount"> <option value="10">10 Servers</option> <option value="25" selected>25 Servers</option> <option value="100">100 Servers</option> <option value="200">200 Servers</option> <option value="500">500 Servers</option> <option value="1000">1000 Servers</option> <option value="2000">2000 Servers</option> <option value="custom">Custom</option> </select> <span class="dropdown-icon">▼</span> </div> <input id="customServerCount" type="number" min="1" max="2000" placeholder="Enter number (1–2000)" style="display: none; margin-top: 5px; width: calc(100% - 10px);"> </div> <div class="filter-section"> <label for="playerCountFilter">Find Servers with:</label> <div class="dropdown-wrapper"> <select id="playerCountFilter"> <option value="high" ${!isLowPlayerCount ? 'selected' : ''}>High Player Counts</option> <option value="low" ${isLowPlayerCount ? 'selected' : ''}>Low Player Counts</option> </select> <span class="dropdown-icon">▼</span> </div> </div> </div> <div class="popup-footer" style="text-align: left; margin-top: 0;"> <p><strong>Note:</strong> If you have fast servers on, the buildman thumbnails are intentional! It's because it saves time for the search.</p> </div> <div style="display: flex; gap: 10px; margin-top: 15px;"> <button id="cancelServerCount" style="width:25%;">Cancel</button> <button id="confirmServerCount" style="width: 75%;">Confirm</button> </div> `; document.body.appendChild(overlay); document.body.appendChild(popup); const serverCountDropdown = popup.querySelector('#serverCount'); const customServerCountInput = popup.querySelector('#customServerCount'); const playerCountFilter = popup.querySelector('#playerCountFilter'); const confirmButton = popup.querySelector('#confirmServerCount'); const cancelButton = popup.querySelector('#cancelServerCount'); serverCountDropdown.addEventListener('change', () => { if (serverCountDropdown.value === 'custom') { customServerCountInput.style.display = 'block'; } else { customServerCountInput.style.display = 'none'; } }); confirmButton.addEventListener('click', () => { let serverCount; if (serverCountDropdown.value === 'custom') { serverCount = parseInt(customServerCountInput.value); if (isNaN(serverCount) || serverCount < 1 || serverCount > 2000) { notifications('Error: Please enter a valid number between 1 and 2000.', 'error', '⚠️', '5000'); return; } } else { serverCount = parseInt(serverCountDropdown.value); } const playerCountPreference = playerCountFilter.value; localStorage.setItem('ROLOCATE_invertplayercount', playerCountPreference === 'low' ? 'true' : 'false'); callback(serverCount); disableFilterButton(true); disableLoadMoreButton(true); hidePopup(); Loadingbar(true); }); cancelButton.addEventListener('click', () => { hidePopup(); }); function hidePopup() { const overlay = document.querySelector('.overlay'); const popup = document.querySelector('.filter-popup'); overlay.classList.add('fade-out'); popup.classList.add('fade-out'); setTimeout(() => { overlay.remove(); popup.remove(); }, 300); } } /******************************************************* name of function: fetchPublicServers description: Function to fetch public servers with rate limtiing and stuff (Server regions) *******************************************************/ // WARNING: Do not republish this script. Licensed for personal use only. async function fetchPublicServers(gameId, totalLimit) { let servers = []; let cursor = null; let delayTime = 250; // Start with 0.25 seconds let retryingDueToRateLimit = false; let pageCount = 0; const invertPlayerCount = localStorage.getItem("ROLOCATE_invertplayercount") === "true"; ConsoleLogEnabled(`Starting to fetch up to ${totalLimit} public servers for game ${gameId}...`); ConsoleLogEnabled(`Invert player count: ${invertPlayerCount}`); while (servers.length < totalLimit) { const url = `https://games.roblox.com/v1/games/${gameId}/servers/public?excludeFullGames=true&limit=100${invertPlayerCount ? '&sortOrder=1' : ''}${cursor ? `&cursor=${cursor}` : ''}`; pageCount++; ConsoleLogEnabled(`Fetching page ${pageCount}... (Current delay: ${delayTime}ms)`); let responseData; try { responseData = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, onload: function(response) { if (response.status === 429 || !response.responseText) { reject({ rateLimited: true }); } else { try { const json = JSON.parse(response.responseText); resolve(json); } catch (err) { reject({ rateLimited: true }); } } }, onerror: function(error) { reject({ rateLimited: false, error }); }, }); }); if (retryingDueToRateLimit) { delayTime = 250; retryingDueToRateLimit = false; ConsoleLogEnabled(`Rate limit cleared. Resuming normal delay (${delayTime}ms).`); } const newServers = responseData.data || []; servers = servers.concat(newServers); ConsoleLogEnabled(`Fetched ${newServers.length} servers (Total: ${servers.length}/${totalLimit})`); if (!responseData.nextPageCursor || servers.length >= totalLimit) { ConsoleLogEnabled("No more pages or reached limit."); break; } cursor = responseData.nextPageCursor; } catch (err) { if (err.rateLimited) { delayTime = 750; retryingDueToRateLimit = true; ConsoleLogEnabled("⚠️ Rate limited. Increasing delay to 0.75s..."); } else { ConsoleLogEnabled("❌ Failed to fetch due to error:", err.error); break; } } await delay(delayTime); } ConsoleLogEnabled(`✅ Done. Fetched ${servers.length} servers in total.`); return servers.slice(0, totalLimit); } /******************************************************* name of function: createFilterDropdowns description: Creates the server selecting dropdown with country flags. *******************************************************/ function createFilterDropdowns(servers) { // Initialize flag data at the start getFlagEmoji(); // Load flag data without country code // Create the main filter container with premium styling const filterContainer = document.createElement('div'); Object.assign(filterContainer.style, { display: 'flex', gap: '32px', alignItems: 'center', padding: '40px 48px', background: 'linear-gradient(145deg, rgba(12,12,12,0.98) 0%, rgba(8,8,8,0.98) 25%, rgba(15,10,10,0.98) 75%, rgba(10,8,8,0.98) 100%)', borderRadius: '28px', boxShadow: '0 32px 64px rgba(0,0,0,0.6), 0 0 0 1px rgba(200,30,30,0.15), inset 0 1px 0 rgba(255,255,255,0.02)', backdropFilter: 'blur(40px)', opacity: '0', transform: 'translateY(-50px) scale(0.94)', transition: 'all 1.2s cubic-bezier(0.16, 1, 0.3, 1)', position: 'relative', border: '1px solid rgba(200,30,30,0.12)', margin: '40px', fontFamily: "'Inter', 'SF Pro Display', system-ui, -apple-system, sans-serif", fontSize: '16px', overflow: 'hidden' }); // Premium animated border with subtle red glow const borderGlow = document.createElement('div'); Object.assign(borderGlow.style, { position: 'absolute', inset: '-2px', borderRadius: '30px', pointerEvents: 'none', background: 'linear-gradient(60deg, rgba(200,25,25,0.25), rgba(50,50,50,0.1), rgba(200,25,25,0.15), rgba(30,30,30,0.1), rgba(200,25,25,0.2))', backgroundSize: '300% 300%', zIndex: '-1', animation: 'premiumFlow 20s ease infinite', opacity: '0.7', filter: 'blur(0.5px)' }); filterContainer.appendChild(borderGlow); // Add premium CSS animations and styling const style = document.createElement('style'); style.textContent = ` @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); @keyframes premiumFlow { 0% { background-position: 0% 50%; transform: rotate(0deg); } 25% { background-position: 100% 25%; } 50% { background-position: 100% 100%; transform: rotate(0.5deg); } 75% { background-position: 0% 75%; } 100% { background-position: 0% 50%; transform: rotate(0deg); } } @keyframes premiumPulse { 0% { box-shadow: 0 0 0 0 rgba(200, 30, 30, 0.4); } 50% { box-shadow: 0 0 0 20px rgba(200, 30, 30, 0); } 100% { box-shadow: 0 0 0 0 rgba(200, 30, 30, 0); } } @keyframes shimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } @keyframes iconFloat { 0%, 100% { transform: translateY(0px); } 50% { transform: translateY(-2px); } } .premium-select { scrollbar-width: thin; scrollbar-color: rgba(200,30,30,0.6) rgba(20,20,20,0.4); } .premium-select::-webkit-scrollbar { width: 6px; } .premium-select::-webkit-scrollbar-track { background: rgba(15,15,15,0.8); border-radius: 10px; } .premium-select::-webkit-scrollbar-thumb { background: linear-gradient(180deg, rgba(200,30,30,0.8), rgba(150,25,25,0.6)); border-radius: 10px; border: 1px solid rgba(0,0,0,0.2); } .premium-select::-webkit-scrollbar-thumb:hover { background: linear-gradient(180deg, rgba(220,35,35,0.9), rgba(170,30,30,0.7)); } .logo-premium-pulse { animation: premiumPulse 3s infinite; } .shimmer-effect { position: relative; overflow: hidden; } .shimmer-effect::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.08), transparent); animation: shimmer 3s infinite; } .premium-icon { animation: iconFloat 3s ease-in-out infinite; } /* Enhanced flag styling for dropdowns */ .flag-option-container { display: flex; align-items: center; gap: 8px; padding: 8px 12px; transition: all 0.3s ease; } .flag-option-container:hover { background: rgba(200,30,30,0.1); border-radius: 6px; } .flag-image { width: 26px !important; /* Slightly larger */ height: 20px !important; /* Slightly larger */ object-fit: cover; object-position: center; overflow: hidden; border-radius: 3px; box-shadow: 0 2px 4px rgba(0,0,0,0.3); flex-shrink: 0; /* Crop the edges to hide outline */ clip-path: inset(1px 1px 1px 1px); } /* Custom select styling for flags */ .premium-select option { padding: 12px 16px; background: rgba(15,15,15,0.98) !important; color: rgba(200,30,30,0.9) !important; border-radius: 8px; margin: 2px; display: flex; align-items: center; } `; document.head.appendChild(style); // Enhanced premium logo with sophisticated hover effects const logoWrapper = document.createElement('div'); Object.assign(logoWrapper.style, { position: 'relative', marginRight: '36px', display: 'flex', alignItems: 'center', cursor: 'pointer' }); const logoContainer = document.createElement('div'); Object.assign(logoContainer.style, { position: 'relative', padding: '8px', borderRadius: '20px', background: 'linear-gradient(145deg, rgba(25,25,25,0.8), rgba(15,15,15,0.9))', border: '1px solid rgba(200,30,30,0.2)', transition: 'all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1)' }); const logo = document.createElement('img'); logo.src = window.Base64Images.logo; Object.assign(logo.style, { width: '64px', height: '64px', borderRadius: '14px', transition: 'all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1)', filter: 'drop-shadow(0 12px 24px rgba(200,30,30,0.4))', border: '2px solid rgba(200,30,30,0.3)', }); const logoGlow = document.createElement('div'); Object.assign(logoGlow.style, { position: 'absolute', inset: '-6px', borderRadius: '24px', background: 'radial-gradient(circle at center, rgba(200,30,30,0.5) 0%, rgba(200,30,30,0.1) 50%, transparent 70%)', opacity: '0', transition: 'all 0.6s ease', pointerEvents: 'none', zIndex: '-1', }); // Premium logo interactions logoContainer.addEventListener('mouseover', () => { logo.style.transform = 'rotate(-6deg) scale(1.12)'; logo.style.filter = 'drop-shadow(0 16px 32px rgba(200,30,30,0.6))'; logo.style.border = '2px solid rgba(200,30,30,0.7)'; logoContainer.style.background = 'linear-gradient(145deg, rgba(35,35,35,0.9), rgba(20,20,20,0.95))'; logoContainer.style.border = '1px solid rgba(200,30,30,0.4)'; logoGlow.style.opacity = '1'; logo.classList.add('logo-premium-pulse'); }); logoContainer.addEventListener('mouseout', () => { logo.style.transform = 'rotate(0) scale(1)'; logo.style.filter = 'drop-shadow(0 12px 24px rgba(200,30,30,0.4))'; logo.style.border = '2px solid rgba(200,30,30,0.3)'; logoContainer.style.background = 'linear-gradient(145deg, rgba(25,25,25,0.8), rgba(15,15,15,0.9))'; logoContainer.style.border = '1px solid rgba(200,30,30,0.2)'; logoGlow.style.opacity = '0'; logo.classList.remove('logo-premium-pulse'); }); logoContainer.appendChild(logoGlow); logoContainer.appendChild(logo); logoWrapper.appendChild(logoContainer); filterContainer.appendChild(logoWrapper); // Function to create premium icon const createIcon = (type) => { const iconMap = { globe: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="m16 12 4-4-4-4"/><path d="m8 12-4 4 4 4"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>`, city: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z"/><path d="M6 12H4a2 2 0 0 0-2 2v8h4"/><path d="M18 9h2a2 2 0 0 1 2 2v11h-4"/><path d="M10 6h4"/><path d="M10 10h4"/><path d="M10 14h4"/><path d="M10 18h4"/></svg>`, chevron: `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>` }; return iconMap[type] || ''; }; // Helper function to get country code from country name const getCountryCode = (countryName) => { // Common country name to code mappings - extend as needed const countryCodeMap = { 'Australia': 'AU', 'Brazil': 'BR', 'Germany': 'DE', 'France': 'FR', 'United Kingdom': 'GB', 'Hong Kong': 'HK', 'India': 'IN', 'Japan': 'JP', 'Netherlands': 'NL', 'Poland': 'PL', 'Singapore': 'SG', 'United States': 'US' }; // Return the country code or the first two letters of the country name as fallback return countryCodeMap[countryName] || countryName.substring(0, 2).toUpperCase(); }; // Function to create a premium dropdown with enhanced styling and icons const createDropdown = (id, placeholder, iconType) => { const wrapper = document.createElement('div'); Object.assign(wrapper.style, { position: 'relative', minWidth: '280px', flex: '1' }); // Premium label with icon const labelContainer = document.createElement('div'); Object.assign(labelContainer.style, { display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '14px', opacity: '0', transform: 'translateX(-10px)', transition: 'all 0.6s ease' }); const labelIcon = document.createElement('span'); labelIcon.innerHTML = createIcon(iconType); labelIcon.className = 'premium-icon'; Object.assign(labelIcon.style, { color: 'rgba(200,30,30,0.8)', display: 'flex', alignItems: 'center', filter: 'drop-shadow(0 2px 4px rgba(200,30,30,0.3))' }); const label = document.createElement('div'); label.textContent = placeholder.replace('All ', '').toUpperCase(); Object.assign(label.style, { color: 'rgba(255,255,255,0.85)', fontSize: '13px', fontWeight: '600', letterSpacing: '1px', transition: 'all 0.4s ease', fontFamily: "'Inter', sans-serif" }); labelContainer.appendChild(labelIcon); labelContainer.appendChild(label); wrapper.appendChild(labelContainer); // Premium dropdown with enhanced design const dropdownContainer = document.createElement('div'); dropdownContainer.className = 'shimmer-effect'; Object.assign(dropdownContainer.style, { position: 'relative', borderRadius: '16px', background: 'linear-gradient(145deg, rgba(20,20,20,0.95), rgba(12,12,12,0.98))', border: '1px solid rgba(200,30,30,0.15)', overflow: 'hidden', transition: 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)', boxShadow: '0 12px 24px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.03)' }); const dropdown = document.createElement('select'); dropdown.id = id; dropdown.className = 'premium-select'; dropdown.innerHTML = `<option value="">${placeholder}</option>`; Object.assign(dropdown.style, { width: '100%', padding: '20px 60px 20px 28px', fontSize: '16px', fontWeight: '500', background: 'transparent', color: 'rgba(200,30,30,0.95)', border: 'none', borderRadius: '16px', appearance: 'none', cursor: 'pointer', transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)', opacity: '0', transform: 'translateY(-25px)', letterSpacing: '0.4px', fontFamily: "'Inter', sans-serif", outline: 'none' }); // Premium chevron with enhanced styling const chevronContainer = document.createElement('div'); Object.assign(chevronContainer.style, { position: 'absolute', right: '20px', top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none', transition: 'all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1)', color: 'rgba(200,30,30,0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '6px', borderRadius: '8px', background: 'rgba(200,30,30,0.1)', border: '1px solid rgba(200,30,30,0.2)' }); chevronContainer.innerHTML = createIcon('chevron'); // Enhanced dropdown interactions with premium effects const addHoverEffect = () => { dropdownContainer.style.background = 'linear-gradient(145deg, rgba(30,30,30,0.98), rgba(18,18,18,1))'; dropdownContainer.style.boxShadow = '0 20px 40px rgba(0,0,0,0.5), 0 0 0 2px rgba(200,30,30,0.3), inset 0 1px 0 rgba(255,255,255,0.05)'; dropdownContainer.style.border = '1px solid rgba(200,30,30,0.3)'; dropdownContainer.style.transform = 'translateY(-2px)'; label.style.color = 'rgba(200,30,30,0.95)'; labelIcon.style.color = 'rgba(200,30,30,1)'; chevronContainer.style.transform = 'translateY(-50%) rotate(180deg)'; chevronContainer.style.background = 'rgba(200,30,30,0.2)'; chevronContainer.style.border = '1px solid rgba(200,30,30,0.4)'; }; const removeHoverEffect = () => { if (document.activeElement !== dropdown) { dropdownContainer.style.background = 'linear-gradient(145deg, rgba(20,20,20,0.95), rgba(12,12,12,0.98))'; dropdownContainer.style.boxShadow = '0 12px 24px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.03)'; dropdownContainer.style.border = '1px solid rgba(200,30,30,0.15)'; dropdownContainer.style.transform = 'translateY(0)'; label.style.color = 'rgba(255,255,255,0.85)'; labelIcon.style.color = 'rgba(200,30,30,0.8)'; chevronContainer.style.transform = 'translateY(-50%) rotate(0deg)'; chevronContainer.style.background = 'rgba(200,30,30,0.1)'; chevronContainer.style.border = '1px solid rgba(200,30,30,0.2)'; } }; dropdownContainer.addEventListener('mouseover', addHoverEffect); dropdownContainer.addEventListener('mouseout', removeHoverEffect); dropdown.addEventListener('focus', () => { dropdownContainer.style.outline = 'none'; dropdownContainer.style.border = '1px solid rgba(200,30,30,0.5)'; dropdownContainer.style.boxShadow = '0 20px 40px rgba(0,0,0,0.5), 0 0 0 4px rgba(200,30,30,0.25), inset 0 1px 0 rgba(255,255,255,0.05)'; label.style.color = 'rgba(200,30,30,1)'; labelIcon.style.color = 'rgba(200,30,30,1)'; chevronContainer.style.transform = 'translateY(-50%) rotate(180deg)'; }); dropdown.addEventListener('blur', removeHoverEffect); dropdown.addEventListener('change', () => { // Premium selection animation dropdownContainer.style.transform = 'translateY(-2px) scale(0.98)'; setTimeout(() => { dropdownContainer.style.transform = 'translateY(-2px) scale(1)'; }, 150); // Enhanced flash effect const flash = document.createElement('div'); Object.assign(flash.style, { position: 'absolute', inset: '0', borderRadius: '16px', background: 'linear-gradient(145deg, rgba(200,30,30,0.2), rgba(200,30,30,0.1))', pointerEvents: 'none', opacity: '0', transition: 'opacity 0.4s ease' }); dropdownContainer.appendChild(flash); flash.style.opacity = '1'; setTimeout(() => { flash.style.opacity = '0'; setTimeout(() => dropdownContainer.removeChild(flash), 400); }, 80); }); // Staggered fade-in animation setTimeout(() => { labelContainer.style.opacity = '1'; labelContainer.style.transform = 'translateX(0)'; }, 400); setTimeout(() => { dropdown.style.opacity = '1'; dropdown.style.transform = 'translateY(0)'; }, 600); dropdownContainer.appendChild(dropdown); dropdownContainer.appendChild(chevronContainer); wrapper.appendChild(dropdownContainer); return wrapper; }; // Create premium dropdowns with icons const countryDropdown = createDropdown('countryFilter', 'All Countries', 'globe'); const cityDropdown = createDropdown('cityFilter', 'All Cities', 'city'); // Populate dropdowns with server data and flags const countryCounts = {}; const countryServerMap = {}; // To store server info for each country servers.forEach(server => { const country = server.location.country.name; countryCounts[country] = (countryCounts[country] || 0) + 1; if (!countryServerMap[country]) { countryServerMap[country] = server; // Store first server for country code reference } }); const sortedCountries = Object.keys(countryCounts).sort(); const countrySelect = countryDropdown.querySelector('select'); sortedCountries.forEach(country => { const option = document.createElement('option'); option.value = country; // Try to get country code from server data first, then fallback to mapping let countryCode; const server = countryServerMap[country]; if (server && server.location.country.code) { countryCode = server.location.country.code; } else { countryCode = getCountryCode(country); } // Create flag element try { const flagImg = getFlagEmoji(countryCode); if (flagImg) { flagImg.className = 'flag-image'; // Since we can't directly add HTML to option text, we'll use a data attribute // and handle the display with CSS or JavaScript option.setAttribute('data-flag-src', flagImg.src); option.setAttribute('data-country-code', countryCode); option.textContent = `${country} (${countryCounts[country]})`; } } catch (error) { ConsoleLogEnabled(`Could not load flag for ${country} (${countryCode}):`, error); option.textContent = `${country} (${countryCounts[country]})`; } Object.assign(option.style, { background: 'rgba(15,15,15,0.98)', color: 'rgba(200,30,30,0.9)', padding: '12px', borderRadius: '8px', margin: '2px' }); countrySelect.appendChild(option); }); // Create a custom dropdown display that shows flags const createCustomDropdownDisplay = (selectElement) => { const customDisplay = document.createElement('div'); Object.assign(customDisplay.style, { position: 'absolute', top: '0', left: '0', right: '0', bottom: '0', display: 'flex', alignItems: 'center', padding: '20px 60px 20px 28px', pointerEvents: 'none', zIndex: '1', color: 'rgba(200,30,30,0.95)', fontSize: '16px', fontWeight: '500', letterSpacing: '0.4px', fontFamily: "'Inter', sans-serif" }); const updateDisplay = () => { const selectedOption = selectElement.options[selectElement.selectedIndex]; if (selectedOption && selectedOption.getAttribute('data-flag-src')) { const flagSrc = selectedOption.getAttribute('data-flag-src'); const countryCode = selectedOption.getAttribute('data-country-code'); customDisplay.innerHTML = ` <img src="${flagSrc}" alt="${countryCode}" class="flag-image" style="width: 24px; height: 18px; margin-right: 12px; border-radius: 3px; box-shadow: 0 2px 4px rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1);"> <span>${selectedOption.textContent}</span> `; } else { customDisplay.textContent = selectedOption ? selectedOption.textContent : selectElement.options[0].textContent; } }; selectElement.addEventListener('change', updateDisplay); updateDisplay(); // Initial display return customDisplay; }; // Add custom display to country dropdown const countryDropdownContainer = countryDropdown.querySelector('.shimmer-effect'); const countryCustomDisplay = createCustomDropdownDisplay(countrySelect); countryDropdownContainer.appendChild(countryCustomDisplay); // Make the original select transparent when it has a selection countrySelect.addEventListener('change', () => { if (countrySelect.value) { countrySelect.style.color = 'transparent'; } else { countrySelect.style.color = 'rgba(200,30,30,0.95)'; } }); // Premium separator with gradient const separator = document.createElement('div'); Object.assign(separator.style, { height: '80px', width: '2px', background: 'linear-gradient(to bottom, rgba(255,255,255,0), rgba(200,30,30,0.4) 20%, rgba(200,30,30,0.6) 50%, rgba(200,30,30,0.4) 80%, rgba(255,255,255,0))', margin: '0 8px', borderRadius: '2px', position: 'relative', overflow: 'hidden' }); // Add subtle animation to separator const separatorGlow = document.createElement('div'); Object.assign(separatorGlow.style, { position: 'absolute', inset: '0', background: 'linear-gradient(to bottom, transparent, rgba(200,30,30,0.8), transparent)', animation: 'shimmer 4s infinite', opacity: '0.3' }); separator.appendChild(separatorGlow); // Enhanced country change handler with flag support countrySelect.addEventListener('change', () => { const selectedCountry = countrySelect.value; const citySelect = cityDropdown.querySelector('select'); citySelect.innerHTML = '<option value="">All Cities</option>'; if (selectedCountry) { const cityCounts = {}; servers .filter(server => server.location.country.name === selectedCountry) .forEach(server => { const city = server.location.city; const region = server.location.region?.name; const cityKey = region ? `${city}, ${region}` : city; cityCounts[cityKey] = (cityCounts[cityKey] || 0) + 1; }); const sortedCities = Object.keys(cityCounts).sort(); sortedCities.forEach(city => { const option = document.createElement('option'); option.value = city; option.textContent = `${city} (${cityCounts[city]})`; Object.assign(option.style, { background: 'rgba(15,15,15,0.98)', color: 'rgba(200,30,30,0.9)', padding: '12px' }); citySelect.appendChild(option); }); // Premium update animation const cityContainer = cityDropdown.querySelector('div'); cityContainer.style.opacity = '0.4'; cityContainer.style.transform = 'translateY(-15px)'; setTimeout(() => { cityContainer.style.opacity = '1'; cityContainer.style.transform = 'translateY(0)'; }, 200); // Visual update indicator const updateRipple = document.createElement('div'); Object.assign(updateRipple.style, { position: 'absolute', inset: '0', borderRadius: '16px', background: 'radial-gradient(circle at center, rgba(200,30,30,0.3) 0%, rgba(200,30,30,0.1) 40%, transparent 70%)', pointerEvents: 'none', opacity: '1', transition: 'all 1s ease', transform: 'scale(0.8)' }); cityDropdown.style.position = 'relative'; cityDropdown.appendChild(updateRipple); setTimeout(() => { updateRipple.style.opacity = '0'; updateRipple.style.transform = 'scale(1.2)'; setTimeout(() => cityDropdown.removeChild(updateRipple), 1000); }, 100); } }); // Append elements to container filterContainer.appendChild(countryDropdown); filterContainer.appendChild(separator); filterContainer.appendChild(cityDropdown); // Premium container entrance animation setTimeout(() => { filterContainer.style.opacity = '1'; filterContainer.style.transform = 'translateY(0) scale(1)'; }, 200); return filterContainer; } /******************************************************* name of function: filterServers description: Function to filter servers based on selected country and city cause im lazy *******************************************************/ function filterServers(servers, country, city) { return servers.filter(server => { const matchesCountry = !country || server.location.country.name === country; const matchesCity = !city || `${server.location.city}${server.location.region?.name ? `, ${server.location.region.name}` : ''}` === city; return matchesCountry && matchesCity; }); } /******************************************************* name of function: fetchPlayerThumbnails_servers description: not really a function but idc. Finds player thumbnails (Server regions) *******************************************************/ const fetchPlayerThumbnails_servers = (() => { const queue = []; let processing = false; // Simple transparent 1x1 base64 PNG const randomBase64Image = () => { const placeholders = [ window.Base64Images.roblox_avatar, window.Base64Images.builderman_avatar, ]; const index = Math.floor(Math.random() * placeholders.length); return placeholders[index]; }; return async function(playerTokens) { ConsoleLogEnabled("Function called with playerTokens:", playerTokens); // Check if fast server mode is enabled if (localStorage.getItem("ROLOCATE_fastservers") === "true") { ConsoleLogEnabled("ROLOCATE_fastservers is enabled. Returning mock base64 images."); const mockData = playerTokens.map(token => ({ requestId: `0:${token}:AvatarHeadshot:150x150:png:regular`, targetId: 0, state: "Completed", imageUrl: randomBase64Image(), })); return mockData; } const waitHalfSecond = (ms = 250) => new Promise(res => setTimeout(res, ms)); return new Promise(resolve => { ConsoleLogEnabled("Pushing to queue:", playerTokens); queue.push({ playerTokens, resolve }); const processQueue = async () => { if (processing) { ConsoleLogEnabled("Already processing, exiting..."); return; } processing = true; ConsoleLogEnabled("Started processing queue..."); while (queue.length > 0) { const { playerTokens, resolve } = queue.shift(); ConsoleLogEnabled("Processing batch:", playerTokens); const body = playerTokens.map(token => ({ requestId: `0:${token}:AvatarHeadshot:150x150:png:regular`, type: "AvatarHeadShot", targetId: 0, token, format: "png", size: "150x150", })); let success = false; let data = []; while (!success) { ConsoleLogEnabled("Sending request to thumbnails.roblox.com..."); const response = await fetch("https://thumbnails.roblox.com/v1/batch", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify(body), }); ConsoleLogEnabled("Response status:", response.status); if (response.status === 429) { ConsoleLogEnabled("Rate limited. Waiting..."); await waitHalfSecond(); } else { const json = await response.json(); data = json.data || []; success = true; ConsoleLogEnabled("Received data:", data); } } resolve(data); ConsoleLogEnabled("Resolved promise with data"); } processing = false; ConsoleLogEnabled("Finished processing queue."); }; processQueue(); }); }; })(); /******************************************************* name of function: rebuildServerList description: A thicc function to find server regions and create the server cards *******************************************************/ async function rebuildServerList(gameId, totalLimit, best_connection, quick_join = false) { const serverListContainer = document.getElementById("rbx-public-game-server-item-container"); const isJoinMode = best_connection || quick_join; // If in any join mode (best connection or quick join) if (isJoinMode) { const originalInvert = localStorage.getItem('ROLOCATE_invertplayercount') === 'true'; let foundServer = false; try { // Only disable filter button for best_connection, not for quick_join if (best_connection) { disableFilterButton(true); } notifications("Retrieving Location...", "success", "🌎", '5000'); const userLocation = await getUserLocation(true); // finally bruh this too too long if (!userLocation) { notifications('Error: Unable to fetch your location. Please enable location access or set it to manual in settings.', 'error', '⚠️', '5000'); return; } // Attempt to find server (up to 2 attempts) for (let attempt = 0; attempt < 2 && !foundServer; attempt++) { const servers = await fetchPublicServers(gameId, 50); if (servers.length === 0) { notifications('No servers found for this game.', 'error', '⚠️', '3000'); continue; } const isFastServers = localStorage.getItem("ROLOCATE_fastservers") === "true"; let closestServer = null; let minDistance = Infinity; let closestServerLocation = null; if (isFastServers) { // Parallel processing for fast servers const results = await Promise.allSettled( servers.map(async server => { const { id: serverId, maxPlayers, playing } = server; if (playing >= maxPlayers) return null; try { const location = await fetchServerDetails(gameId, serverId); const distance = calculateDistance( userLocation.latitude, userLocation.longitude, location.latitude, location.longitude ); return { server, location, distance }; } catch (error) { ConsoleLogEnabled(`Error fetching details for server ${serverId}:`, error); return null; } }) ); for (const result of results) { if (result.status === "fulfilled" && result.value) { const { server, location, distance } = result.value; if (distance < minDistance) { minDistance = distance; closestServer = server; closestServerLocation = location; } } } } else { // Sequential processing for regular servers for (const server of servers) { const { id: serverId, maxPlayers, playing } = server; if (playing >= maxPlayers) continue; try { const location = await fetchServerDetails(gameId, serverId); const distance = calculateDistance( userLocation.latitude, userLocation.longitude, location.latitude, location.longitude ); if (distance < minDistance) { minDistance = distance; closestServer = server; closestServerLocation = location; } } catch (error) { ConsoleLogEnabled(`Error fetching details for server ${serverId}:`, error); continue; } } } if (closestServer) { Roblox.GameLauncher.joinGameInstance(gameId, closestServer.id); notifications(`Joining nearest server! \nDistance: ${Math.round(minDistance / 1.609)} miles | ${Math.round(minDistance)} km`, 'success', '🚀', '5000'); foundServer = true; } else if (attempt === 0) { // First attempt failed - try inverted player count localStorage.setItem('ROLOCATE_invertplayercount', 'true'); notifications('No available servers found. Trying smallest servers...', 'info', '🔄', '3000'); } } if (!foundServer) { notifications('No valid servers found. This game might be popular right now. Try using \'Server Region\' or refresh the page and try again later.', 'error', '⚠️', '8000'); } } catch (error) { ConsoleLogEnabled("Error in join mode:", error); notifications('Error during server search: ' + error.message, 'error', '⚠️', '5000'); } finally { // Reset to original setting localStorage.setItem('ROLOCATE_invertplayercount', originalInvert ? 'true' : 'false'); if (best_connection) { disableFilterButton(false); } Loadingbar(false); } return; } // Rest of the function for normal server list display if (!serverListContainer) { ConsoleLogEnabled("Server list container not found!"); notifications('Error: No Servers found. There is nobody playing this game. Please refresh the page.', 'error', '⚠️', '8000'); Loadingbar(false); return; } const messageElement = showMessage("Just a moment — to detect your location accurately, please stay on this page..."); const premium_message = messageElement.querySelector('.premium-message-text'); try { // Retrieve user's location for distance calculations const userLocation = await getUserLocation(); if (!userLocation) { notifications('Error: Unable to fetch your location. Please enable location access.', 'error', '⚠️', '5000'); disableFilterButton(false); return; } const servers = await fetchPublicServers(gameId, totalLimit); const totalServers = servers.length; let skippedServers = 0; if (premium_message) { premium_message.textContent = `Filtering servers... Please stay on this page to ensure a faster and more accurate search. ${totalServers} servers found, 0 loaded so far.`; } notifications(`Please do not leave this page as it slows down the search. \nFound a total of ${totalServers} servers.`, 'success', '👍', '3000'); const serverDetails = []; const useBatching = localStorage.ROLOCATE_fastservers === "true"; if (useBatching) { // Process servers in batches of 100 const batchSize = 100; let processedCount = 0; for (let i = 0; i < servers.length; i += batchSize) { const batch = servers.slice(i, i + batchSize); const batchPromises = batch.map(async (server) => { const { id: serverId, maxPlayers, playing, ping, fps, playerTokens } = server; // Skip full servers early to avoid unnecessary API calls if (playing >= maxPlayers) { skippedServers++; return null; } try { const location = await fetchServerDetails(gameId, serverId); if (location.city === "Unknown") { ConsoleLogEnabled(`Skipping server ${serverId} because location is unknown.`); skippedServers++; return null; } // Fetch player thumbnails const playerThumbnails = playerTokens && playerTokens.length > 0 ? await fetchPlayerThumbnails_servers(playerTokens) : []; return { server, location, playerThumbnails }; } catch (error) { if (error === 'purchase_required') { throw error; } else if (error === 'subplace_join_restriction') { throw error; } else { ConsoleLogEnabled(error); skippedServers++; return null; } } }); // Smoothly update the processed count function updateProcessedCountSmoothly(startCount, targetCount) { const increment = 1; let currentCount = startCount; const interval = setInterval(() => { if (currentCount < targetCount) { currentCount += increment; if (premium_message) { premium_message.textContent = `Filtering servers, please do not leave this page...\n${totalServers} servers found, ${currentCount} server locations found`; } } else { clearInterval(interval); } }, 2); } const batchResults = await Promise.all(batchPromises); const previousProcessedCount = processedCount; // Filter out null results and add valid ones to serverDetails const validResults = batchResults.filter(result => result !== null); serverDetails.push(...validResults); // Gradually update processedCount after processing the batch processedCount += batch.length; updateProcessedCountSmoothly(previousProcessedCount, processedCount); } } else { // Original sequential processing for (let i = 0; i < servers.length; i++) { const server = servers[i]; const { id: serverId, maxPlayers, playing, ping, fps, playerTokens } = server; let location; try { location = await fetchServerDetails(gameId, serverId); } catch (error) { if (error === 'purchase_required') { if (premium_message) { premium_message.textContent = "Error: Cannot access server regions because you have not purchased the game."; } notifications('Cannot access server regions because you have not purchased the game.', 'error', '⚠️', '15000'); Loadingbar(false); return; } else if (error === 'subplace_join_restriction') { if (premium_message) { premium_message.textContent = "Error: This game requires users to teleport to a subplace. As a result, server regions cannot be retrieved."; } notifications('Error: This game requires users to teleport to a subplace. As a result, server regions cannot be retrieved.', 'error', '⚠️', '15000'); Loadingbar(false); return; } else { ConsoleLogEnabled(error); location = { city: "Unknown", country: { name: "Unknown", code: "??" } }; } } if (location.city === "Unknown" || playing >= maxPlayers) { ConsoleLogEnabled(`Skipping server ${serverId} because it is full or location is unknown.`); skippedServers++; continue; } // Fetch player thumbnails const playerThumbnails = playerTokens && playerTokens.length > 0 ? await fetchPlayerThumbnails_servers(playerTokens) : []; serverDetails.push({ server, location, playerThumbnails, }); if (premium_message) { premium_message.textContent = `Filtering servers, please do not leave this page...\n${totalServers} servers found, ${i + 1} server locations found`; } } } if (serverDetails.length === 0) { showMessage("END"); if (servers.every(s => s.maxPlayers === 1)) { notifications('All servers have a max player count of 1. These are likely solo servers and cannot be joined normally.', 'error', '⚠️', '8000'); } else { notifications('Error: No servers found. Try increasing the search limit or enabling "Invert Player Count" in Settings > General.', 'error', '⚠️', '8000'); } Loadingbar(false); return; } const loadedServers = totalServers - skippedServers; notifications(`Filtering complete!\n${totalServers} servers found, ${loadedServers} servers loaded, ${skippedServers} servers skipped (full).`, 'success', '👍', '2000'); if (localStorage.getItem('ROLOCATE_fastservers') === 'true') { let secondsSaved = (loadedServers * 150) / 1000; if (secondsSaved < 0.1 && secondsSaved > 0) { secondsSaved = '0.1'; } else { secondsSaved = secondsSaved.toFixed(1); } notifications(`FastServers: Thumbnails replaced with Builderman and Roblox. Saved ${secondsSaved} seconds`, 'info', '🚀', '2500'); } if (typeof GM_info !== 'undefined' && GM_info.scriptHandler?.toLowerCase().includes('violentmonkey')) { const fastServers = localStorage.getItem('ROLOCATE_fastservers'); if (fastServers === 'false') { notifications(`You're using Violentmonkey, which supports Fast Servers. Enable it in Settings → General → Fast Server Search!`, 'info', '🚀', '12000'); } } showMessage("END"); Loadingbar(false); // Add filter dropdowns const filterContainer = createFilterDropdowns(serverDetails); serverListContainer.parentNode.insertBefore(filterContainer, serverListContainer); // Style the server list container serverListContainer.style.display = "grid"; serverListContainer.style.gridTemplateColumns = "repeat(4, 1fr)"; serverListContainer.style.gap = "0px"; const displayFilteredServers = (country, city) => { serverListContainer.innerHTML = ""; const filteredServers = filterServers(serverDetails, country, city); const sortedServers = filteredServers.sort((a, b) => { const distanceA = calculateDistance(userLocation.latitude, userLocation.longitude, a.location.latitude, a.location.longitude); const distanceB = calculateDistance(userLocation.latitude, userLocation.longitude, b.location.latitude, b.location.longitude); return distanceA - distanceB; }); sortedServers.forEach(({ server, location, playerThumbnails }) => { const serverCard = document.createElement("li"); serverCard.className = "rbx-game-server-item col-md-3 col-sm-4 col-xs-6"; serverCard.style.width = "100%"; serverCard.style.minHeight = "400px"; serverCard.style.display = "flex"; serverCard.style.flexDirection = "column"; serverCard.style.justifyContent = "space-between"; serverCard.style.boxSizing = "border-box"; serverCard.style.outline = 'none'; serverCard.style.padding = '6px'; serverCard.style.borderRadius = '8px'; // Create ping label const pingLabel = document.createElement("div"); pingLabel.style.marginBottom = "5px"; pingLabel.style.padding = "5px 10px"; pingLabel.style.borderRadius = "8px"; pingLabel.style.fontWeight = "bold"; pingLabel.style.textAlign = "center"; // Calculate distance and ping const distance = calculateDistance( userLocation.latitude, userLocation.longitude, location.latitude, location.longitude ); const calculatedPing = 40 + 0.004 * distance + 1.2 * Math.sqrt(distance); if (distance < 1250) { pingLabel.textContent = "⚡ Fast"; pingLabel.style.backgroundColor = "#014737"; pingLabel.style.color = "#73e1bc"; } else if (distance < 5000) { pingLabel.textContent = "⏳ OK"; pingLabel.style.backgroundColor = "#c75a00"; pingLabel.style.color = "#ffe8c2"; } else { pingLabel.textContent = "🐌 Slow"; pingLabel.style.backgroundColor = "#771d1d"; pingLabel.style.color = "#fcc468"; } // Create thumbnails container const thumbnailsContainer = document.createElement("div"); thumbnailsContainer.className = "player-thumbnails-container"; thumbnailsContainer.style.display = "grid"; thumbnailsContainer.style.gridTemplateColumns = "repeat(3, 60px)"; thumbnailsContainer.style.gridTemplateRows = "repeat(2, 60px)"; thumbnailsContainer.style.gap = "5px"; thumbnailsContainer.style.marginBottom = "10px"; // Add player thumbnails const maxThumbnails = 5; const displayedThumbnails = playerThumbnails.slice(0, maxThumbnails); displayedThumbnails.forEach(thumb => { if (thumb && thumb.imageUrl) { const img = document.createElement("img"); img.src = thumb.imageUrl; img.className = "avatar-card-image"; img.style.width = "60px"; img.style.height = "60px"; img.style.borderRadius = "50%"; thumbnailsContainer.appendChild(img); } }); // Add placeholder for hidden players const hiddenPlayers = server.playing - displayedThumbnails.length; if (hiddenPlayers > 0) { const placeholder = document.createElement("div"); placeholder.className = "avatar-card-image"; placeholder.style.width = "60px"; placeholder.style.height = "60px"; placeholder.style.borderRadius = "50%"; placeholder.style.backgroundColor = "#6a6f81"; placeholder.style.display = "flex"; placeholder.style.alignItems = "center"; placeholder.style.justifyContent = "center"; placeholder.style.color = "#fff"; placeholder.style.fontSize = "14px"; placeholder.textContent = `+${hiddenPlayers}`; thumbnailsContainer.appendChild(placeholder); } // Server card content const cardItem = document.createElement("div"); cardItem.className = "card-item"; cardItem.style.display = "flex"; cardItem.style.flexDirection = "column"; cardItem.style.justifyContent = "space-between"; cardItem.style.height = "100%"; cardItem.innerHTML = ` ${thumbnailsContainer.outerHTML} <div class="rbx-game-server-details game-server-details"> <div class="text-info rbx-game-status rbx-game-server-status text-overflow"> ${server.playing} of ${server.maxPlayers} people max </div> <div class="server-player-count-gauge border"> <div class="gauge-inner-bar border" style="width: ${(server.playing / server.maxPlayers) * 100}%;"></div> </div> <span data-placeid="${gameId}"> <button type="button" class="btn-full-width btn-control-xs rbx-game-server-join game-server-join-btn btn-primary-md btn-min-width">Join</button> </span> </div> <div style="margin-top: 10px; text-align: center;"> ${pingLabel.outerHTML} <div class="info-lines" style="margin-top: 8px;"> <div class="ping-info">Ping: ${calculatedPing.toFixed(2)} ms</div> <hr style="margin: 6px 0;"> <div class="ping-info">Distance: ${distance.toFixed(2)} km</div> <hr style="margin: 6px 0;"> <div class="location-info">${location.city}, ${location.country.name}</div> <hr style="margin: 6px 0;"> <div class="fps-info">FPS: ${Math.round(server.fps)}</div> </div> </div> `; const joinButton = cardItem.querySelector(".rbx-game-server-join"); joinButton.addEventListener("click", () => { ConsoleLogEnabled(`Roblox.GameLauncher.joinGameInstance(${gameId}, "${server.id}")`); Roblox.GameLauncher.joinGameInstance(gameId, server.id); }); const container = adjustJoinButtonContainer(joinButton); const inviteButton = createInviteButton(gameId, server.id); container.appendChild(inviteButton); serverCard.appendChild(cardItem); serverListContainer.appendChild(serverCard); }); }; // Add event listeners to dropdowns const countryFilter = document.getElementById('countryFilter'); const cityFilter = document.getElementById('cityFilter'); countryFilter.addEventListener('change', () => { displayFilteredServers(countryFilter.value, cityFilter.value); }); cityFilter.addEventListener('change', () => { displayFilteredServers(countryFilter.value, cityFilter.value); }); // Display all servers initially displayFilteredServers("", ""); } catch (error) { if (error === 'purchase_required') { if (premium_message) { premium_message.textContent = "Error: Cannot access server regions because you have not purchased the game."; } notifications('Cannot access server regions because you have not purchased the game.', 'error', '⚠️', '15000'); Loadingbar(false); return; } else if (error === 'subplace_join_restriction') { if (premium_message) { premium_message.textContent = "Error: This game requires users to teleport to a subplace. As a result, server regions cannot be retrieved."; } notifications('Error: This game requires users to teleport to a subplace. As a result, server regions cannot be retrieved.', 'error', '⚠️', '15000'); Loadingbar(false); return; } else { ConsoleLogEnabled("Error rebuilding server list:", error); notifications('Filtering Error: Failed to obtain permission to send API requests to the Roblox API. Please allow the script to enable request sending.', 'error', '⚠️ ', '8000'); if (premium_message) { premium_message.textContent = "Filtering Error: Failed to obtain permission to send API requests to the Roblox API. Please allow the script to enable request sending."; } Loadingbar(false); } } finally { Loadingbar(false); disableFilterButton(false); } } // this is used for best connection and server regions const gameId = /^https:\/\/www\.roblox\.com(\/[a-z]{2})?\/games\//.test(window.location.href) ? (window.location.href.match(/\/games\/(\d+)/) || [])[1] || null : null; /******************************************************* name of function: createInviteButton description: Creates the invite button (server region) *******************************************************/ function createInviteButton(placeId, serverId) { const inviteButton = document.createElement('button'); inviteButton.textContent = 'Invite'; inviteButton.className = 'btn-control-xs btn-primary-md btn-min-width btn-full-width'; inviteButton.style.width = '25%'; inviteButton.style.marginLeft = '5px'; inviteButton.style.padding = '4px 8px'; inviteButton.style.fontSize = '12px'; inviteButton.style.borderRadius = '8px'; inviteButton.style.backgroundColor = '#3b3e49'; inviteButton.style.borderColor = '#3b3e49'; inviteButton.style.color = '#ffffff'; inviteButton.style.cursor = 'pointer'; inviteButton.style.fontWeight = '500'; inviteButton.style.textAlign = 'center'; inviteButton.style.whiteSpace = 'nowrap'; inviteButton.style.verticalAlign = 'middle'; inviteButton.style.lineHeight = '100%'; inviteButton.style.fontFamily = 'Builder Sans, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif'; inviteButton.style.textRendering = 'auto'; inviteButton.style.webkitFontSmoothing = 'antialiased'; inviteButton.style.mozOsxFontSmoothing = 'grayscale'; let resetTextTimeout = null; inviteButton.addEventListener('click', () => { const inviteLink = `https://oqarshi.github.io/Invite/?placeid=${placeId}&serverid=${serverId}`; navigator.clipboard.writeText(inviteLink).then(() => { ConsoleLogEnabled(`Invite link copied to clipboard: ${inviteLink}`); notifications('Success! Invite link copied to clipboard!', 'success', '🎉', '2000'); // Prevent spam clicking inviteButton.disabled = true; inviteButton.style.opacity = '0.6'; inviteButton.style.cursor = 'not-allowed'; // Reset any previous timeout if (resetTextTimeout !== null) { clearTimeout(resetTextTimeout); } inviteButton.textContent = 'Copied!'; resetTextTimeout = setTimeout(() => { inviteButton.textContent = 'Invite'; inviteButton.disabled = false; inviteButton.style.opacity = '1'; inviteButton.style.cursor = 'pointer'; resetTextTimeout = null; }, 1000); }).catch(() => { ConsoleLogEnabled('Failed to copy invite link.'); notifications('Error: Failed to copy invite link', 'error', '😔', '2000'); }); }); return inviteButton; } /******************************************************* name of function: adjustJoinButtonContainer description: Function to adjust the Join button and its container but it fails lmao and does 50/50 instead of 75/25 *******************************************************/ // Function to adjust the Join button and its container function adjustJoinButtonContainer(joinButton) { const container = document.createElement('div'); container.style.display = 'flex'; container.style.width = '100%'; joinButton.style.width = '75%'; joinButton.parentNode.insertBefore(container, joinButton); container.appendChild(joinButton); return container; } /********************************************************************************************************************************************************************************************************************************************* Functions for the 6th button. *********************************************************************************************************************************************************************************************************************************************/ /******************************************************* name of function: calculateDistance description: finds the distance between two points on a sphere (Earth) *******************************************************/ function calculateDistance(lat1, lon1, lat2, lon2) { const R = 6371; // Radius of the Earth in kilometers as a perfect sphere but obv its not a perfect sphere const dLat = (lat2 - lat1) * (Math.PI / 180); const dLon = (lon2 - lon1) * (Math.PI / 180); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; // Distance in kilometers } /******************************************************* name of function: resolveOfflineFallbackLocation description: estimate user location if user declines *******************************************************/ // Fallback location resolver with timezone-based estimation function resolveOfflineFallbackLocation(resolve) { ConsoleLogEnabled("Attempting offline location estimation..."); let guessedLocation = null; let closestLocation = null; let closestDistance = Infinity; const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || ""; const timezoneMap = { "America/Los_Angeles": { lat: 34.0522, lon: -118.2437 }, "America/Denver": { lat: 39.7392, lon: -104.9903 }, "America/Chicago": { lat: 41.8781, lon: -87.6298 }, "America/New_York": { lat: 40.7128, lon: -74.006 }, "Europe/London": { lat: 51.5074, lon: -0.1278 }, "Europe/Berlin": { lat: 52.52, lon: 13.405 }, "Europe/Paris": { lat: 48.8566, lon: 2.3522 }, "Asia/Tokyo": { lat: 35.6895, lon: 139.6917 }, "Asia/Kolkata": { lat: 28.6139, lon: 77.209 }, "Australia/Sydney": { lat: -33.8688, lon: 151.2093 }, "America/Argentina/Buenos_Aires": { lat: -34.6037, lon: -58.3816 }, "Africa/Nairobi": { lat: -1.286389, lon: 36.817223 }, "Asia/Singapore": { lat: 1.3521, lon: 103.8198 }, "America/Toronto": { lat: 43.65107, lon: -79.347015 }, "Europe/Moscow": { lat: 55.7558, lon: 37.6173 }, "Europe/Madrid": { lat: 40.4168, lon: -3.7038 }, "Asia/Shanghai": { lat: 31.2304, lon: 121.4737 }, "Africa/Cairo": { lat: 30.0444, lon: 31.2357 }, "Africa/Johannesburg": { lat: -26.2041, lon: 28.0473 }, "Europe/Amsterdam": { lat: 52.3676, lon: 4.9041 }, "Asia/Manila": { lat: 14.5995, lon: 120.9842 }, "Asia/Seoul": { lat: 37.5665, lon: 126.978 } }; // If user's timezone is available in the map if (timezoneMap[timezone]) { guessedLocation = timezoneMap[timezone]; ConsoleLogEnabled("User's timezone found:", timezone); } // If the timezone is not found, find the closest match if (!guessedLocation) { ConsoleLogEnabled("User's timezone not found. Finding closest match..."); Object.keys(timezoneMap).forEach((tz) => { const location = timezoneMap[tz]; const distance = calculateDistance(location.lat, location.lon, 0, 0); // Distance from the equator (0,0) if (distance < closestDistance) { closestDistance = distance; closestLocation = location; } }); guessedLocation = closestLocation; } // If we found a location, return it, otherwise default to New York if (guessedLocation) { notifications("Estimated location based on timezone. Please allow location access to see what servers are closest to you or change to manual in settings.", "info", "🕒", "6000"); resolve({ latitude: guessedLocation.lat, longitude: guessedLocation.lon }); } else { notifications("Error: Could not estimate location. Fatal error, please report on Greasyfork. Using default (New York).", "error", "⚠️", "6000"); resolve({ latitude: 40.7128, longitude: -74.0060 }); // Default to NYC } } /******************************************************* name of function: getUserLocation description: gets the user's location @param {boolean} [quickJoin=false] – when true, operates in lightweight "quick join" mode *******************************************************/ function getUserLocation(quickJoin = false) { return new Promise((resolve, reject) => { // Check priority location setting const priorityLocation = localStorage.getItem("ROLOCATE_prioritylocation") || "automatic"; // If in manual mode, use stored coordinates if (priorityLocation === "manual") { try { const coords = JSON.parse(GM_getValue("ROLOCATE_coordinates", '{"lat":"","lng":""}')); if (coords.lat && coords.lng) { ConsoleLogEnabled("Using manual location from storage"); notifications("We successfully detected your location.", "success", "🌎", "2000"); return resolve({ latitude: parseFloat(coords.lat), // Changed to match automatic mode longitude: parseFloat(coords.lng), // Changed to match automatic mode source: "manual", accuracy: 0 // Manual coordinates have no accuracy metric }); } else { ConsoleLogEnabled("Manual mode selected but no coordinates set - falling back to automatic behavior"); notifications("Manual mode selected but no coordinates set. Fatal error: Report on greasyfork. Using Automatic Mode.", "error", "", "2000"); // Fall through to automatic behavior } } catch (e) { ConsoleLogEnabled("Error reading manual coordinates:", e); notifications("Error reading manual coordinates. Fatal error: Report on greasyfork. Using Automatic Mode.", "error", "", "2000"); // Fall through to automatic behavior } } // Automatic mode behavior if (!navigator.geolocation) { ConsoleLogEnabled("Geolocation not supported."); notifications("Geolocation is not supported by your browser.", "error", "⚠️", "15000"); return resolveOfflineFallbackLocation(resolve); } navigator.geolocation.getCurrentPosition( (position) => resolveSuccess(position, resolve, quickJoin), async (error) => { ConsoleLogEnabled("Geolocation error:", error); // Attempt to inspect geolocation permission state try { if (navigator.permissions && navigator.permissions.query) { const permissionStatus = await navigator.permissions.query({ name: "geolocation" }); ConsoleLogEnabled("Geolocation permission status:", permissionStatus.state); if (permissionStatus.state === "denied") { return resolveOfflineFallbackLocation(resolve); } } } catch (permError) { ConsoleLogEnabled("Permission check failed:", permError); } // Retry geolocation once with a slightly relaxed setting navigator.geolocation.getCurrentPosition( (position) => resolveSuccess(position, resolve, quickJoin), (retryError) => { ConsoleLogEnabled("Second geolocation attempt failed:", retryError); notifications("Could not get your location. Using fallback.", "error", "⚠️", "15000"); resolveOfflineFallbackLocation(resolve); }, { maximumAge: 5000, timeout: 10000, } ); }, { timeout: 10000, maximumAge: 0, } ); }); } /******************************************************* name of function: resolveSuccess description: tells the user that location was detected @param {GeolocationPosition} position – browser geolocation position @param {Function} resolve – promise resolver @param {boolean} [quickJoin=false] – when true, skips UI-disabling side‑effects *******************************************************/ function resolveSuccess(position, resolve, quickJoin = false) { notifications("We successfully detected your location.", "success", "🌎", "2000"); if (!quickJoin) { disableLoadMoreButton(true); disableFilterButton(true); Loadingbar(true); } resolve({ latitude: position.coords.latitude, longitude: position.coords.longitude, source: "geolocation", accuracy: position.coords.accuracy }); } /********************************************************************************************************************************************************************************************************************************************* Functions for the 7th button. *********************************************************************************************************************************************************************************************************************************************/ /******************************************************* name of function: auto_join_small_server description: Automatically joins the smallest server *******************************************************/ async function auto_join_small_server() { // Disable the "Load More" button and show the loading bar Loadingbar(true); disableFilterButton(true); disableLoadMoreButton(); // Get the game ID from the URL const gameId = ((p = window.location.pathname.split('/')) => { const i = p.indexOf('games'); return i !== -1 && p.length > i + 1 ? p[i + 1] : null; })(); // Retry mechanism for 429 errors let retries = 3; // Number of retries let success = false; while (retries > 0 && !success) { try { // Fetch server data using GM_xmlhttpRequest const data = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `https://games.roblox.com/v1/games/${gameId}/servers/public?sortOrder=1&excludeFullGames=true&limit=100`, onload: function(response) { if (response.status === 429) { reject('429: Too Many Requests'); } else if (response.status >= 200 && response.status < 300) { resolve(JSON.parse(response.responseText)); } else { reject(`HTTP error: ${response.status}`); } }, onerror: function(error) { reject(error); }, }); }); // find servers with low player count, prob doesnet work with bloxfruits cause bots let minPlayers = Infinity; let targetServer = null; for (const server of data.data) { if (server.playing < minPlayers) { minPlayers = server.playing; targetServer = server; } } if (targetServer) { // Join the server with the lowest player count //showLoadingOverlay(); Roblox.GameLauncher.joinGameInstance(gameId, targetServer.id); notifications(`Joining a server with ${targetServer.playing} player(s).`, 'success', '🚀'); success = true; // Mark as successful } else { notifications('No available servers found.', 'error', '⚠️'); break; // Exit the loop if no servers are found } } catch (error) { if (error === '429: Too Many Requests' && retries > 0) { ConsoleLogEnabled('Rate limited. Retrying in 10 seconds...'); notifications('Rate limited. Retrying in 10 seconds...', 'warning', '⏳', '10000'); await delay(10000); // Wait 10 seconds before retrying retries--; } else { ConsoleLogEnabled('Error fetching server data:', error); notifications('Error: Failed to fetch server data. Please try again later.', 'error', '⚠️', '5000'); Loadingbar(false); break; // Exit the loop if it's not a 429 error or no retries left } } } // Hide the loading bar and enable the filter button Loadingbar(false); disableFilterButton(false); } /********************************************************************************************************************************************************************************************************************************************* Functions for the 8th button. roblox borke it lmao. basically fillter code, might remove it one day *********************************************************************************************************************************************************************************************************************************************/ async function scanRobloxServers() { const BRAND_LOGO = window.Base64Images.logo; notifications('Note: This may not actually work and is just experimental.', 'info', '⚠️', '5000'); // Create popup UI function createScannerPopup() { const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 10000; display: flex; justify-content: center; align-items: center; opacity: 0; transition: opacity 0.3s ease; `; const popup = document.createElement('div'); popup.style.cssText = ` width: 400px; background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 50%, #2a2a2a 100%); border: 2px solid #333; border-radius: 16px; padding: 24px; color: #fff; font-family: 'Segoe UI', sans-serif; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255, 255, 255, 0.1); transform: scale(0.9); transition: transform 0.3s ease; position: relative; overflow: hidden; `; // Animated background pattern const bgPattern = document.createElement('div'); bgPattern.style.cssText = ` position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; background: radial-gradient(circle, rgba(100, 100, 100, 0.05) 0%, transparent 70%); animation: rotate 20s linear infinite; pointer-events: none; `; const style = document.createElement('style'); style.textContent = ` @keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes pulse { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } } @keyframes slideUp { from { transform: translateY(10px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } `; document.head.appendChild(style); popup.appendChild(bgPattern); // Header with logo const header = document.createElement('div'); header.style.cssText = ` text-align: center; margin-bottom: 20px; position: relative; z-index: 1; `; // Brand logo const logoContainer = document.createElement('div'); logoContainer.style.cssText = ` display: flex; justify-content: center; align-items: center; margin-bottom: 12px; `; const logo = document.createElement('img'); logo.src = BRAND_LOGO; logo.style.cssText = ` width: 32px; height: 32px; margin-right: 8px; border-radius: 6px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); `; const title = document.createElement('div'); title.textContent = 'RoLocate'; title.style.cssText = ` font-size: 24px; font-weight: bold; color: #e0e0e0; text-shadow: 0 0 20px rgba(255, 255, 255, 0.2); `; logoContainer.appendChild(logo); logoContainer.appendChild(title); const subtitle = document.createElement('div'); subtitle.textContent = 'Trying to search for new servers...'; subtitle.style.cssText = ` color: #999; font-size: 14px; `; header.appendChild(logoContainer); header.appendChild(subtitle); // Status section const statusDiv = document.createElement('div'); statusDiv.style.cssText = ` margin-bottom: 24px; position: relative; z-index: 1; `; const statusText = document.createElement('div'); statusText.style.cssText = ` color: #fff; margin-bottom: 12px; font-size: 16px; text-align: center; min-height: 24px; animation: slideUp 0.5s ease; `; statusText.textContent = 'Initializing...'; const progressContainer = document.createElement('div'); progressContainer.style.cssText = ` width: 100%; height: 8px; background: #1a1a1a; border-radius: 4px; overflow: hidden; box-shadow: inset 0 2px 4px rgba(0,0,0,0.5); border: 1px solid #333; `; const progressFill = document.createElement('div'); progressFill.style.cssText = ` height: 100%; background: linear-gradient(90deg, #4a4a4a, #666, #4a4a4a); background-size: 200% 100%; width: 0%; transition: width 0.5s ease; animation: shimmer 2s infinite; `; const shimmerStyle = document.createElement('style'); shimmerStyle.textContent = ` @keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } `; document.head.appendChild(shimmerStyle); progressContainer.appendChild(progressFill); statusDiv.appendChild(statusText); statusDiv.appendChild(progressContainer); // Stats section const statsDiv = document.createElement('div'); statsDiv.style.cssText = ` background: rgba(0, 0, 0, 0.4); border: 1px solid #333; border-radius: 12px; padding: 16px; margin-bottom: 24px; position: relative; z-index: 1; backdrop-filter: blur(10px); `; const statsGrid = document.createElement('div'); statsGrid.style.cssText = ` display: grid; grid-template-columns: 1fr 1fr; gap: 16px; `; const createStatItem = (label, id, color) => { const item = document.createElement('div'); item.style.cssText = 'text-align: center;'; const value = document.createElement('div'); value.id = id; value.style.cssText = ` font-size: 24px; font-weight: bold; color: ${color}; margin-bottom: 4px; text-shadow: 0 0 10px ${color}40; `; value.textContent = '0'; const labelDiv = document.createElement('div'); labelDiv.style.cssText = 'color: #aaa; font-size: 12px;'; labelDiv.textContent = label; item.appendChild(value); item.appendChild(labelDiv); return item; }; statsGrid.appendChild(createStatItem('Total Scans', 'totalScans', '#e0e0e0')); statsGrid.appendChild(createStatItem('Servers Tracked', 'serversFound', '#4CAF50')); statsGrid.appendChild(createStatItem('New Servers', 'newServers', '#64B5F6')); statsGrid.appendChild(createStatItem('Cards Created', 'cardsCreated', '#FFB74D')); statsDiv.appendChild(statsGrid); // Button container const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; justify-content: center; position: relative; z-index: 1; `; const cancelBtn = document.createElement('button'); cancelBtn.textContent = '✕ Stop'; cancelBtn.style.cssText = ` background: linear-gradient(135deg, #333, #444); border: 1px solid #555; color: white; padding: 12px 24px; border-radius: 25px; font-size: 14px; font-weight: bold; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); `; cancelBtn.onmouseover = () => { cancelBtn.style.transform = 'translateY(-2px)'; cancelBtn.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.5)'; cancelBtn.style.background = 'linear-gradient(135deg, #444, #555)'; }; cancelBtn.onmouseout = () => { cancelBtn.style.transform = 'translateY(0)'; cancelBtn.style.boxShadow = '0 4px 15px rgba(0, 0, 0, 0.3)'; cancelBtn.style.background = 'linear-gradient(135deg, #333, #444)'; }; let cancelled = false; cancelBtn.onclick = () => { Loadingbar(false); disableFilterButton(false); cancelled = true; overlay.style.opacity = '0'; popup.style.transform = 'scale(0.9)'; notifications('Scan stopped', 'info', '🚫'); setTimeout(() => overlay.remove(), 300); }; buttonContainer.appendChild(cancelBtn); // Assemble popup popup.appendChild(header); popup.appendChild(statusDiv); popup.appendChild(statsDiv); popup.appendChild(buttonContainer); overlay.appendChild(popup); document.body.appendChild(overlay); // Animate in setTimeout(() => { overlay.style.opacity = '1'; popup.style.transform = 'scale(1)'; }, 10); return { updateStatus: (text, progress = null) => { statusText.textContent = text; statusText.style.animation = 'slideUp 0.5s ease'; if (progress !== null) { progressFill.style.width = `${Math.min(100, Math.max(0, progress))}%`; } }, updateStats: (stats) => { const updateStat = (id, value) => { const element = document.getElementById(id); if (element && value !== undefined) { element.textContent = value; element.style.animation = 'pulse 0.5s ease'; } }; updateStat('totalScans', stats.totalScans); updateStat('serversFound', stats.serversFound); updateStat('newServers', stats.newServers); updateStat('cardsCreated', stats.cardsCreated); }, isCancelled: () => cancelled, close: () => { overlay.style.opacity = '0'; popup.style.transform = 'scale(0.9)'; setTimeout(() => overlay.remove(), 300); } }; } // Initialize popup const ui = createScannerPopup(); ui.updateStatus('🚀 Starting continuous scanner...', 0); ConsoleLogEnabled("Starting Roblox continuous server scanner..."); // Run startup functions once ConsoleLogEnabled("Running startup functions..."); ui.updateStatus('⚙️ Setting up scanner...', 5); try { Loadingbar(true); disableFilterButton(true); disableLoadMoreButton(); ConsoleLogEnabled("Startup functions executed successfully"); } catch (error) { ConsoleLogEnabled("❌ Error running startup functions:", error); ui.updateStatus('❌ Setup failed', 100); Loadingbar(false); disableFilterButton(false); notifications('An error occured. Please try again', 'error', '⚠️'); return; } // Check for cancellation if (ui.isCancelled()) { ConsoleLogEnabled("Scanner cancelled by user"); Loadingbar(false); disableFilterButton(false); notifications('Scanner cancelled', 'info', '🚫'); return; } // Get the game ID from the URL const gameId = ((p => { const i = p.indexOf('games'); return i !== -1 && p.length > i + 1 ? p[i + 1] : null; })(window.location.pathname.split('/'))); if (!gameId) { ConsoleLogEnabled("Could not extract game ID from URL"); ui.updateStatus('❌ Game ID not found', 100); notifications('An error occured. Please try again', 'error', '⚠️'); return; } ConsoleLogEnabled(`Game ID extracted: ${gameId}`); // Rate limiting variables let requestDelay = 250; const normalDelay = 250; const rateLimitedDelay = 750; // Store all known server IDs let knownServerIds = new Set(); // Track totals let totalScansPerformed = 0; let totalNewServersFound = 0; let totalCardsCreated = 0; ConsoleLogEnabled(`🎯 Starting continuous scanning mode - will run until user cancels`); // Function to fetch all servers from API with dynamic rate limiting async function fetchAllServers() { if (ui.isCancelled()) return []; ConsoleLogEnabled("Starting to fetch all servers..."); ConsoleLogEnabled(`Current request delay: ${requestDelay}ms`); const allServers = []; let cursor = null; let pageCount = 0; do { if (ui.isCancelled()) return []; pageCount++; ConsoleLogEnabled(`Fetching page ${pageCount}${cursor ? ` with cursor: ${cursor.substring(0, 50)}...` : ' (first page)'}`); try { const url = `https://games.roblox.com/v1/games/${gameId}/servers/0?sortOrder=2&excludeFullGames=false&limit=100${cursor ? `&cursor=${cursor}` : ''}`; ConsoleLogEnabled(`Request URL: ${url}`); const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' }, onload: function(response) { resolve(response); }, onerror: function(error) { reject(error); }, ontimeout: function() { reject(new Error('Request timeout')); }, timeout: 10000 }); }); // Check for rate limiting if (response.status === 429) { ConsoleLogEnabled(`Rate limited! Status: ${response.status}`); if (requestDelay !== rateLimitedDelay) { requestDelay = rateLimitedDelay; ConsoleLogEnabled(`Switching to slower requests: ${requestDelay}ms delay`); } ConsoleLogEnabled(`Waiting ${requestDelay}ms before retry...`); await new Promise(resolve => setTimeout(resolve, requestDelay)); continue; } // Check if we can go back to normal speed if (response.status >= 200 && response.status < 300 && requestDelay === rateLimitedDelay) { requestDelay = normalDelay; ConsoleLogEnabled(`Rate limit cleared! Back to normal speed: ${requestDelay}ms delay`); } if (response.status < 200 || response.status >= 300) { ConsoleLogEnabled(`API request failed with status ${response.status}: ${response.statusText}`); notifications('An error occured. Please try again. Roblox API blocked.', 'error', '⚠️'); break; } const data = JSON.parse(response.responseText); ConsoleLogEnabled(`Page ${pageCount} fetched successfully - Found ${data.data.length} servers`); if (data.data && Array.isArray(data.data)) { allServers.push(...data.data); ConsoleLogEnabled(`Total servers collected so far: ${allServers.length}`); } else { ConsoleLogEnabled(`Unexpected data format on page ${pageCount}`); } cursor = data.nextPageCursor; ConsoleLogEnabled(`Next page cursor: ${cursor ? cursor.substring(0, 50) + '...' : 'null (no more pages)'}`); } catch (error) { ConsoleLogEnabled(`Error fetching page ${pageCount}:`, error); if (error.message && (error.message.includes('timeout') || error.message.includes('network'))) { ConsoleLogEnabled(`Network/timeout error detected, might be rate limiting`); if (requestDelay !== rateLimitedDelay) { requestDelay = rateLimitedDelay; ConsoleLogEnabled(`Switching to slower requests due to network error: ${requestDelay}ms delay`); } } ConsoleLogEnabled(`Waiting ${requestDelay}ms before continuing...`); await new Promise(resolve => setTimeout(resolve, requestDelay)); continue; } if (cursor) { ConsoleLogEnabled(`Waiting ${requestDelay}ms before next request...`); await new Promise(resolve => setTimeout(resolve, requestDelay)); } } while (cursor && !ui.isCancelled()); ConsoleLogEnabled(`Finished fetching all servers. Total pages: ${pageCount}, Total servers: ${allServers.length}`); return allServers; } // Function to create card for a new server async function createCardForServer(server) { ConsoleLogEnabled(`Creating card for new server: ${server.id}`); ConsoleLogEnabled(` - Players: ${server.playing}/${server.maxPlayers}`); ConsoleLogEnabled(` - Player tokens: ${server.playerTokens ? server.playerTokens.length : 0}`); ConsoleLogEnabled(` - FPS: ${server.fps}`); ConsoleLogEnabled(` - Ping: ${server.ping}`); try { await rbx_card(server.id, server.playerTokens, server.maxPlayers, server.playing, gameId); ConsoleLogEnabled(`✅ Card created successfully for server: ${server.id}`); totalCardsCreated++; notifications(`New server found! Card created (${server.playing}/${server.maxPlayers} players)`, 'success', '🎉'); return true; } catch (error) { ConsoleLogEnabled(`❌ Error creating card for server ${server.id}:`, error); return false; } } // INITIAL SCAN: Build baseline of known servers ConsoleLogEnabled("\n=== INITIAL SCAN - Building baseline ==="); ui.updateStatus('🔍 Scan 1 - Searching for new servers...', 15); totalScansPerformed++; ui.updateStats({ totalScans: totalScansPerformed }); const initialServers = await fetchAllServers(); if (ui.isCancelled()) return; initialServers.forEach(server => { if (server && server.id) { knownServerIds.add(server.id); ConsoleLogEnabled(`Added baseline server ID: ${server.id}`); } }); ConsoleLogEnabled(`INITIAL SCAN COMPLETE: Stored ${knownServerIds.size} baseline server IDs`); ui.updateStats({ serversFound: knownServerIds.size }); // CONTINUOUS SCANNING LOOP ConsoleLogEnabled("\n=== STARTING CONTINUOUS SCANNING ==="); let scanNumber = 2; const scanDelay = 1000; // Wait between scans while (!ui.isCancelled()) { ConsoleLogEnabled(`\n=== SCAN ${scanNumber} - Looking for new servers ===`); ui.updateStatus(`🔍 Scan ${scanNumber} - Searching for new servers...`, null); totalScansPerformed++; ui.updateStats({ totalScans: totalScansPerformed }); const currentScanServers = await fetchAllServers(); if (ui.isCancelled()) break; let newServersFoundThisScan = 0; // Process each server for (const server of currentScanServers) { if (ui.isCancelled()) break; if (server && server.id) { if (!knownServerIds.has(server.id)) { // This is a completely new server ConsoleLogEnabled(`🆕 BRAND NEW SERVER DETECTED: ${server.id}`); // Add to known servers immediately knownServerIds.add(server.id); ConsoleLogEnabled(`Added server ${server.id} to known servers list`); function getAdaptivePlayerThreshold(maxPlayers) { const curve = 1.18; // re-tuned for better match const minRatio = 0.38; // minimum allowed ratio (~38% of maxPlayers) const rawRatio = 1 - Math.exp(-curve * (maxPlayers / 25)); const adjustedRatio = Math.max(minRatio, rawRatio); return Math.floor(maxPlayers * adjustedRatio); } const adaptiveThreshold = getAdaptivePlayerThreshold(server.maxPlayers); if (server.playing < adaptiveThreshold) { ConsoleLogEnabled(`✅ Server meets criteria (${server.playing}/${server.maxPlayers} players - under ${adaptiveThreshold})`); // Create card immediately const cardCreated = await createCardForServer(server); if (cardCreated) { newServersFoundThisScan++; totalNewServersFound++; } } else { ConsoleLogEnabled(`❌ Server rejected (too full): ${server.id} (${server.playing}/${server.maxPlayers} players - need less than ${adaptiveThreshold})`); } } } } ConsoleLogEnabled(`\nSCAN ${scanNumber} RESULTS:`); ConsoleLogEnabled(` - Total servers scanned: ${currentScanServers.length}`); ConsoleLogEnabled(` - Total known server IDs: ${knownServerIds.size}`); ConsoleLogEnabled(` - NEW SERVERS FOUND THIS SCAN: ${newServersFoundThisScan}`); ConsoleLogEnabled(` - TOTAL NEW SERVERS FOUND: ${totalNewServersFound}`); ConsoleLogEnabled(` - TOTAL CARDS CREATED: ${totalCardsCreated}`); // Update UI stats ui.updateStats({ totalScans: totalScansPerformed, serversFound: knownServerIds.size, newServers: totalNewServersFound, cardsCreated: totalCardsCreated }); if (newServersFoundThisScan > 0) { ui.updateStatus(`🎉 Found ${newServersFoundThisScan} new server${newServersFoundThisScan > 1 ? 's' : ''}!`, null); } else { ui.updateStatus(`🔍 Scan ${scanNumber} - Searching for new servers...`, null); } // Wait before next scan ConsoleLogEnabled(`Waiting ${scanDelay}ms before next scan...`); await new Promise(resolve => setTimeout(resolve, scanDelay)); scanNumber++; } // Scanner was cancelled ConsoleLogEnabled(`\n=== SCANNER STOPPED BY USER ===`); ConsoleLogEnabled(`Total scans performed: ${totalScansPerformed}`); ConsoleLogEnabled(`Total known server IDs: ${knownServerIds.size}`); ConsoleLogEnabled(`New servers found: ${totalNewServersFound}`); ConsoleLogEnabled(`Cards created: ${totalCardsCreated}`); ui.updateStatus('🛑 Scanner stopped by user', null); Loadingbar(false); disableFilterButton(false); } // old locater player code, keeping just incase it comes possible again // // Enhanced popup styling for the username input dialog // function find_user_server_tab() { // // Create the overlay (backdrop) with improved animation // 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.6); // z-index: 9999; // opacity: 0; // transition: opacity 0.4s ease; // backdrop-filter: blur(3px); // `; // document.body.appendChild(overlay); // // // Create the popup container with improved styling // const popup = document.createElement('div'); // popup.className = 'player-count-popup'; // popup.style.cssText = ` // position: fixed; // top: 50%; // left: 50%; // transform: translate(-50%, -50%) scale(0.95); // background-color: rgb(30, 32, 34); // padding: 30px 25px; // border-radius: 12px; // z-index: 10000; // box-shadow: 0 10px 25px rgba(0, 0, 0, 0.8); // display: flex; // flex-direction: column; // align-items: center; // gap: 20px; // width: 340px; // opacity: 0; // transition: opacity 0.4s ease, transform 0.4s ease; // border: 1px solid rgba(255, 255, 255, 0.1); // `; // // // Improved close button with better hover effects // const closeButton = document.createElement('button'); // closeButton.innerHTML = '×'; // closeButton.style.cssText = ` // position: absolute; // top: 12px; // right: 12px; // background: transparent; // border: none; // color: #999999; // font-size: 26px; // cursor: pointer; // width: 36px; // height: 36px; // border-radius: 50%; // display: flex; // align-items: center; // justify-content: center; // transition: all 0.2s ease; // `; // closeButton.addEventListener('mouseenter', () => { // closeButton.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'; // closeButton.style.color = '#ff4444'; // closeButton.style.transform = 'rotate(90deg)'; // }); // closeButton.addEventListener('mouseleave', () => { // closeButton.style.backgroundColor = 'transparent'; // closeButton.style.color = '#999999'; // closeButton.style.transform = 'rotate(0deg)'; // }); // // // Enhanced title with icon // const titleContainer = document.createElement('div'); // titleContainer.style.cssText = ` // display: flex; // align-items: center; // gap: 10px; // margin-bottom: 5px; // `; // // const titleIcon = document.createElement('span'); // titleIcon.innerHTML = '🔍'; // titleIcon.style.cssText = `font-size: 22px;`; // // const title = document.createElement('h3'); // title.textContent = 'Locate User'; // title.style.cssText = ` // color: white; // margin: 0; // font-size: 20px; // font-weight: 600; // `; // // titleContainer.appendChild(titleIcon); // titleContainer.appendChild(title); // popup.appendChild(titleContainer); // // // Add subtitle with instructions // const subtitle = document.createElement('p'); // subtitle.textContent = 'Enter the exact Roblox username to find their server'; // subtitle.style.cssText = ` // color: #aaaaaa; // margin: -10px 0 0 0; // font-size: 14px; // text-align: center; // `; // popup.appendChild(subtitle); // // // Improved input box with focus styling // const usernameInput = document.createElement('input'); // usernameInput.type = 'text'; // usernameInput.placeholder = 'Username'; // usernameInput.style.cssText = ` // width: 100%; // padding: 12px 15px; // font-size: 16px; // border: 2px solid #444; // border-radius: 8px; // background-color: #252729; // color: white; // outline: none; // transition: all 0.3s ease; // box-sizing: border-box; // `; // usernameInput.addEventListener('focus', () => { // usernameInput.style.borderColor = '#b71c1c'; // usernameInput.style.boxShadow = '0 0 5px rgba(211, 47, 47, 0.5)'; // }); // usernameInput.addEventListener('blur', () => { // usernameInput.style.borderColor = '#444'; // usernameInput.style.boxShadow = 'none'; // }); // popup.appendChild(usernameInput); // // // Enhanced confirm button with better styling and effects // const confirmButton = document.createElement('button'); // confirmButton.textContent = 'Search'; // confirmButton.style.cssText = ` // padding: 12px 0; // width: 100%; // font-size: 16px; // font-weight: 600; // background-color: #b71c1c; // color: white; // border: none; // border-radius: 8px; // cursor: pointer; // transition: all 0.3s ease; // display: flex; // align-items: center; // justify-content: center; // gap: 8px; // `; // // // Add icon to button // const searchIcon = document.createElement('span'); // searchIcon.textContent = '🔍'; // searchIcon.style.fontSize = '18px'; // confirmButton.prepend(searchIcon); // // confirmButton.addEventListener('mouseenter', () => { // confirmButton.style.backgroundColor = '#d32f2f'; // confirmButton.style.transform = 'translateY(-2px)'; // confirmButton.style.boxShadow = '0 5px 15px rgba(211, 47, 47, 0.5)'; // }); // confirmButton.addEventListener('mouseleave', () => { // confirmButton.style.backgroundColor = '#b71c1c'; // confirmButton.style.transform = 'translateY(0)'; // confirmButton.style.boxShadow = 'none'; // }); // confirmButton.addEventListener('mousedown', () => { // confirmButton.style.transform = 'translateY(1px)'; // }); // confirmButton.addEventListener('mouseup', () => { // confirmButton.style.transform = 'translateY(-2px)'; // }); // // // Handle keyboard enter for better UX // usernameInput.addEventListener('keydown', (e) => { // if (e.key === 'Enter') { // confirmButton.click(); // } // }); // // // Handle confirm button click with enhanced validation feedback // confirmButton.addEventListener('click', () => { // const username = usernameInput.value.trim(); // if (username) { // if (username.length >= 3 && username.length <= 20) { // FindPlayerGameServer(username); // notifications("Searching for the user's server...", "info", "🔍", "5000"); // fadeOutAndRemove_7th(popup, overlay); // } else { // usernameInput.style.borderColor = '#ff4444'; // usernameInput.style.boxShadow = '0 0 5px rgba(255, 68, 68, 0.5)'; // notifications('Username must be between 3 and 20 characters', 'error', '⚠️', '3000'); // // // Reset input styling after delay // setTimeout(() => { // if (document.body.contains(usernameInput)) { // usernameInput.style.borderColor = '#444'; // usernameInput.style.boxShadow = 'none'; // } // }, 2000); // } // } else { // usernameInput.style.borderColor = '#ff4444'; // usernameInput.style.boxShadow = '0 0 5px rgba(255, 68, 68, 0.5)'; // notifications('Please enter a username', 'error', '⚠️', '2500'); // } // }); // // // Append elements to popup // popup.appendChild(confirmButton); // popup.appendChild(closeButton); // // // Append popup to body // document.body.appendChild(popup); // // // Focus the input field automatically // setTimeout(() => { // usernameInput.focus(); // }, 300); // // // Fade in the overlay and popup with improved animation // setTimeout(() => { // overlay.style.opacity = '1'; // popup.style.opacity = '1'; // popup.style.transform = 'translate(-50%, -50%) scale(1)'; // }, 10); // // // Keep the same fadeOutAndRemove function name for compatibility // function fadeOutAndRemove_7th(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); // } // // // Close the popup when clicking outside // overlay.addEventListener('click', () => { // fadeOutAndRemove_7th(popup, overlay); // }); // // // Close the popup when the close button is clicked // closeButton.addEventListener('click', () => { // fadeOutAndRemove_7th(popup, overlay); // }); // } // // // // const checkIfUserOnline = async (userId) => { // try { // const response = await fetch("https://presence.roblox.com/v1/presence/users", { // method: "POST", // headers: { // "Content-Type": "application/json" // }, // body: JSON.stringify({ // userIds: [parseInt(userId)] // }) // }); // const data = await response.json(); // const presenceType = data.userPresences?.[0]?.userPresenceType ?? 0; // return presenceType !== 0; // true if online or in-game/studio // } catch (error) { // ConsoleLogEnabled("Presence check failed:", error); // return false; // fail-safe: treat as offline // } // }; // // // async function FindPlayerGameServer(playerName) { // disableLoadMoreButton(); // Loadingbar(true); // disableFilterButton(true); // const wait = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)); // // const fetchData = async (url, options = {}) => { // if (!options.headers) options.headers = {}; // return fetch(url, options) // .then(response => response.json()) // .catch(error => ConsoleLogEnabled("Fetch error:", error)); // }; // // const fetchDataGM = (url, options = {}) => { // return new Promise((resolve, reject) => { // GM_xmlhttpRequest({ // method: options.method || 'GET', // url: url, // headers: options.headers || {}, // anonymous: true, // Prevents sending cookies // nocache: true, // Prevents caching // onload: function(response) { // try { // const parsedData = JSON.parse(response.responseText); // resolve(parsedData); // } catch (error) { // ConsoleLogEnabled("JSON parsing error:", error); // reject(error); // } // }, // onerror: function(error) { // ConsoleLogEnabled("Request error:", error); // reject(error); // } // }); // }); // }; // // ConsoleLogEnabled(`Initiating search for player: ${playerName}`); // // const gameId = ((p = window.location.href.split('/')) => { // const i = p.indexOf('games'); // return i !== -1 && p.length > i + 1 ? p[i + 1] : null; // })(); // ConsoleLogEnabled(`Game ID identified: ${gameId}`); // // let userId; // // try { // ConsoleLogEnabled(`Retrieving user ID for player: ${playerName}`); // const userProfile = await fetch(`https://www.roblox.com/users/profile?username=${playerName}`); // if (!userProfile.ok) { // notifications("Error: User does not exist on Roblox!", "error", "⚠️", "2500"); // Loadingbar(false); // disableFilterButton(false); // throw `Player "${playerName}" not found`; // } // userId = userProfile.url.match(/\d+/)[0]; // const isOnline = await checkIfUserOnline(userId); // if (!isOnline) { // notifications("User is currently offline.", "error", "📴", "4000"); // Loadingbar(false); // disableFilterButton(false); // fadeOutAndRemove_7th_progress(progressPopup.popup, progressPopup.overlay); // return `User "${playerName}" is offline.`; // } // ConsoleLogEnabled(`User is online — proceeding with server scan.`); // // ConsoleLogEnabled(`User ID retrieved: ${userId}`); // } catch (error) { // ConsoleLogEnabled("Error:", error); // return `Error: ${error}`; // } // // ConsoleLogEnabled(`Fetching avatar thumbnail for user ID: ${userId}`); // const avatarThumbnail = (await fetchData(`https://thumbnails.roblox.com/v1/users/avatar-headshot?userIds=${userId}&format=Png&size=150x150`)).data[0].imageUrl; // ConsoleLogEnabled(`Avatar thumbnail URL: ${avatarThumbnail}`); // // let pageCursor = null; // let playerFound = false; // let totalServersChecked = 0; // let startTime = Date.now(); // // // Show the search progress popup with the player's thumbnail // const progressPopup = showSearchProgressPopup(avatarThumbnail); // // while (true) { // let apiUrl = `https://games.roblox.com/v1/games/${gameId}/servers/0?limit=100`; // if (pageCursor) apiUrl += "&cursor=" + pageCursor; // // ConsoleLogEnabled(`Accessing servers with URL: ${apiUrl}`); // const serverList = await fetchDataGM(apiUrl, { // credentials: "omit" // }).catch(() => null); // // if (serverList && serverList.data) { // ConsoleLogEnabled(`Discovered ${serverList.data.length} servers in this set.`); // for (let index = 0; index < serverList.data.length; index++) { // const server = serverList.data[index]; // if (!playerFound) { // totalServersChecked++; // // // Calculate time elapsed // const timeElapsed = Math.floor((Date.now() - startTime) / 1000); // // // Update the progress popup // updateSearchProgressPopup( // progressPopup, // totalServersChecked, // timeElapsed // ); // // if (server.playerTokens.length === 0) { // ConsoleLogEnabled(`Server ${index + 1} is empty. Proceeding to next server.`); // continue; // } // // ConsoleLogEnabled(`Inspecting server ${index + 1} hosting ${server.playing} players...`); // // let thumbnailData; // while (true) { // let requestBody = []; // // const generateRequestEntry = (playerToken) => ({ // requestId: `0:${playerToken}:AvatarHeadshot:150x150:png:regular`, // type: "AvatarHeadShot", // targetId: 0, // token: playerToken, // format: "png", // size: "150x150" // }); // // server.playerTokens.forEach(token => { // requestBody.push(generateRequestEntry(token)); // }); // // try { // ConsoleLogEnabled(`Fetching thumbnails for ${server.playerTokens.length} player(s)...`); // thumbnailData = await fetchData("https://thumbnails.roblox.com/v1/batch", { // method: "POST", // headers: { // "Content-Type": "application/json", // Accept: "application/json" // }, // body: JSON.stringify(requestBody) // }); // ConsoleLogEnabled("Thumbnail Data Response:", thumbnailData); // break; // } catch (error) { // ConsoleLogEnabled("Thumbnail retrieval error:", error); // await wait(1000); // } // } // // if (!thumbnailData.data) { // ConsoleLogEnabled("No thumbnail data available. Moving to next server."); // continue; // } // // for (let thumbIndex = 0; thumbIndex < thumbnailData.data.length; thumbIndex++) { // const thumbnail = thumbnailData.data[thumbIndex]; // if (thumbnail && thumbnail.imageUrl === avatarThumbnail) { // playerFound = true; // ConsoleLogEnabled(`Player located in server ${index + 1}!`); // notifications("Found User's Server! Joining Server...", "success", "🚀", "5000"); // //showLoadingOverlay(); // Roblox.GameLauncher.joinGameInstance(gameId, server.id); // fadeOutAndRemove_7th_progress(progressPopup.popup, progressPopup.overlay); // Loadingbar(false); // disableFilterButton(false); // return { // gameId, // serverId: server.id, // currentPlayers: server.playing, // maximumPlayers: server.maxPlayers, // }; // } // } // } else { // break; // } // } // // pageCursor = serverList.nextPageCursor; // if (!pageCursor || playerFound) break; // else { // ConsoleLogEnabled("Pausing for 2.5 seconds before next server batch..."); // await wait(2500); // } // } else { // ConsoleLogEnabled("Server fetch failed. Retrying in 10 seconds..."); // notifications("Got rate limited. Waiting 10 seconds...", "info", "❗", "10000") // await wait(10000); // } // } // // if (!playerFound) { // // Wait for 2 seconds before calling another function // setTimeout(() => { // fadeOutAndRemove_7th_progress(progressPopup.popup, progressPopup.overlay); // notifications("User not found playing this game!", "error", "⚠️", "5000"); // notifications("If the user is playing, try again in a minute. The Roblox API may not be updated yet.", "info", "🔄", "20000"); // }, 2000); // 2000 milliseconds = 2 seconds // // // Existing logic (unchanged) // Loadingbar(false); // disableFilterButton(false); // ConsoleLogEnabled(`Player not found in the searched servers (${totalServersChecked} servers checked)`); // // Update the progress popup with the final count // updateSearchProgressPopup( // progressPopup, // totalServersChecked, // Math.floor((Date.now() - startTime) / 1000), // true // ); // return `Player not found in the searched servers (${totalServersChecked} servers checked)`; // } // } // // // Enhanced progress popup for search visualization // function showSearchProgressPopup(avatarThumbnail) { // // Create the overlay with blur effect // 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.6); // z-index: 9999; // opacity: 0; // transition: opacity 0.4s ease; // backdrop-filter: blur(3px); // `; // document.body.appendChild(overlay); // // // Create the popup container with improved styling // const popup = document.createElement('div'); // popup.className = 'search-progress-popup'; // popup.style.cssText = ` // position: fixed; // top: 50%; // left: 50%; // transform: translate(-50%, -50%) scale(0.95); // background-color: rgb(30, 32, 34); // padding: 30px; // border-radius: 12px; // z-index: 10000; // box-shadow: 0 10px 25px rgba(0, 0, 0, 0.8); // display: flex; // flex-direction: column; // align-items: center; // gap: 20px; // width: 340px; // opacity: 0; // transition: opacity 0.4s ease, transform 0.4s ease; // border: 1px solid rgba(255, 255, 255, 0.1); // `; // // // Improved title with visual hierarchy // const title = document.createElement('h3'); // title.textContent = 'Searching for Player...'; // title.style.cssText = ` // color: white; // margin: 0 0 10px 0; // font-size: 20px; // font-weight: 600; // text-align: center; // `; // popup.appendChild(title); // // // Add animated loader element // const loaderContainer = document.createElement('div'); // loaderContainer.style.cssText = ` // position: relative; // width: 120px; // height: 120px; // display: flex; // justify-content: center; // align-items: center; // `; // // // Create ripple effect around avatar // const ripple = document.createElement('div'); // ripple.style.cssText = ` // position: absolute; // width: 110px; // height: 110px; // border-radius: 50%; // border: 3px solid #b71c1c; // animation: ripple 1.5s ease-out infinite; // opacity: 0; // `; // // // Add the keyframe animation // const style = document.createElement('style'); // style.textContent = ` // @keyframes ripple { // 0% { // transform: scale(0.8); // opacity: 0.8; // } // 100% { // transform: scale(1.2); // opacity: 0; // } // } // @keyframes pulse { // 0% { box-shadow: 0 0 0 0 rgba(255, 68, 68, 0.5); } // 70% { box-shadow: 0 0 0 10px rgba(58, 122, 255, 0); } // 100% { box-shadow: 0 0 0 0 rgba(58, 122, 255, 0); } // } // `; // document.head.appendChild(style); // // // Enhanced player thumbnail display // const thumbnail = document.createElement('img'); // thumbnail.src = avatarThumbnail; // thumbnail.style.cssText = ` // width: 100px; // height: 100px; // border-radius: 50%; // object-fit: cover; // border: 4px solid #b71c1c; // box-shadow: 0 0 20px rgba(255, 68, 68, 0.5); // animation: pulse 2s infinite; // z-index: 2; // `; // // loaderContainer.appendChild(ripple); // loaderContainer.appendChild(thumbnail); // popup.appendChild(loaderContainer); // // // Status container // const statusContainer = document.createElement('div'); // statusContainer.style.cssText = ` // background-color: rgba(0, 0, 0, 0.2); // border-radius: 8px; // padding: 15px; // width: 100%; // box-sizing: border-box; // `; // // // Improved progress indicators with icons // const serversSearchedContainer = document.createElement('div'); // serversSearchedContainer.style.cssText = ` // display: flex; // align-items: center; // gap: 10px; // margin-bottom: 10px; // `; // // const serverIcon = document.createElement('span'); // serverIcon.textContent = '🔍'; // serverIcon.style.fontSize = '18px'; // // const serversSearchedText = document.createElement('p'); // serversSearchedText.textContent = 'Servers searched: 0'; // serversSearchedText.style.cssText = ` // color: white; // margin: 0; // font-size: 16px; // flex-grow: 1; // `; // // serversSearchedContainer.appendChild(serverIcon); // serversSearchedContainer.appendChild(serversSearchedText); // // const timeElapsedContainer = document.createElement('div'); // timeElapsedContainer.style.cssText = ` // display: flex; // align-items: center; // gap: 10px; // `; // // const timeIcon = document.createElement('span'); // timeIcon.textContent = '⏱️'; // timeIcon.style.fontSize = '18px'; // // const timeElapsedText = document.createElement('p'); // timeElapsedText.textContent = 'Time elapsed: 0s'; // timeElapsedText.style.cssText = ` // color: white; // margin: 0; // font-size: 16px; // flex-grow: 1; // `; // // timeElapsedContainer.appendChild(timeIcon); // timeElapsedContainer.appendChild(timeElapsedText); // // statusContainer.appendChild(serversSearchedContainer); // statusContainer.appendChild(timeElapsedContainer); // popup.appendChild(statusContainer); // // // Add a status message that can be updated // const statusMessage = document.createElement('p'); // statusMessage.textContent = 'Scanning servers...'; // statusMessage.style.cssText = ` // color: #aaaaaa; // margin: -10px 0 0 0; // font-size: 14px; // text-align: center; // font-style: italic; // `; // popup.appendChild(statusMessage); // // // 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); // // return { // popup, // overlay, // serversSearchedText, // timeElapsedText, // statusMessage, // ripple // }; // } // // // Enhanced update function for search progress popup // function updateSearchProgressPopup( // progressPopup, // totalServersChecked, // timeElapsed, // isFinal = false // ) { // // Update numbers with animation effect // const currentServers = parseInt(progressPopup.serversSearchedText.textContent.match(/\d+/)[0]); // const currentTime = parseInt(progressPopup.timeElapsedText.textContent.match(/\d+/)[0]); // // // Animate the number changes // progressPopup.serversSearchedText.textContent = `Servers searched: ${totalServersChecked}`; // progressPopup.timeElapsedText.textContent = `Time elapsed: ${timeElapsed}s`; // // // Update status message based on progress // if (totalServersChecked % 5 === 0) { // const messages = [ // "Scanning servers...", // "Checking player lists...", // "Searching for matches...", // "Processing server data...", // "Analyzing player tokens..." // ]; // progressPopup.statusMessage.textContent = messages[Math.floor(Math.random() * messages.length)]; // } // // if (isFinal) { // progressPopup.statusMessage.textContent = "Search completed"; // progressPopup.statusMessage.style.color = "#ffaa00"; // progressPopup.statusMessage.style.fontWeight = "bold"; // progressPopup.ripple.style.animationPlayState = "paused"; // progressPopup.serversSearchedText.textContent = `Servers searched: ${totalServersChecked} (complete)`; // } // } // // // Enhanced removal function for progress popup // function fadeOutAndRemove_7th_progress(popup, overlay) { // popup.style.opacity = '0'; // popup.style.transform = 'translate(-50%, -50%) scale(0.9)'; // overlay.style.opacity = '0'; // setTimeout(() => { // popup.remove(); // overlay.remove(); // }, 400); // } // // // // /********************************************************************************************************************************************************************************************************************************************* End of: This is all the functions for the 8 buttons *********************************************************************************************************************************************************************************************************************************************/ /********************************************************************************************************************************************************************************************************************************************* The Universal Functions *********************************************************************************************************************************************************************************************************************************************/ /******************************************************* name of function: disableLoadMoreButton description: Disables the "Load More" button *******************************************************/ function disableLoadMoreButton() { const loadMoreButton = document.querySelector('.rbx-running-games-load-more'); if (loadMoreButton) { loadMoreButton.disabled = true; loadMoreButton.style.opacity = '0.5'; loadMoreButton.style.cursor = 'not-allowed'; // only add the label if it doesnt already exist if (!loadMoreButton.textContent.includes('(Disabled by Rolocate)')) { loadMoreButton.textContent += ' (Disabled by Rolocate)'; } ConsoleLogEnabled('Load More button disabled with text change'); } else { ConsoleLogEnabled('Load More button not found!'); } } /******************************************************* name of function: Loadingbar description: Shows or hides a loading bar (now using pulsing boxes) *******************************************************/ function Loadingbar(disable) { const serverListSection = document.querySelector('#rbx-public-running-games'); const serverCardsContainer = document.querySelector('#rbx-public-game-server-item-container'); const emptyGameInstancesContainer = document.querySelector('.section-content-off.empty-game-instances-container'); const noServersMessage = emptyGameInstancesContainer?.querySelector('.no-servers-message'); // check if the "Unable to load servers." message is visible if (!serverCardsContainer && noServersMessage?.textContent.includes('Unable to load servers.')) { notifications('Unable to load servers. Please refresh the page.', 'error', '⚠️', '8000'); return; } // reset if (disable) { if (serverCardsContainer) { serverCardsContainer.innerHTML = ''; // Clear contents serverCardsContainer.removeAttribute('style'); // Remove inline styles if present } // no duplicate ones const existingLoadingBar = document.querySelector('#loading-bar'); if (existingLoadingBar) { existingLoadingBar.remove(); // Remove the existing loading bar if it exists } // Create and display the loading boxes const loadingContainer = document.createElement('div'); loadingContainer.id = 'loading-bar'; loadingContainer.style.cssText = ` display: flex; justify-content: center; align-items: center; gap: 5px; margin-top: 10px; `; const fragment = document.createDocumentFragment(); for (let i = 0; i < 3; i++) { const box = document.createElement('div'); box.style.cssText = ` width: 10px; height: 10px; background-color: white; margin: 0 5px; border-radius: 2px; animation: pulse 1.2s ${i * 0.2}s infinite; `; fragment.appendChild(box); } loadingContainer.appendChild(fragment); if (serverListSection) { serverListSection.appendChild(loadingContainer); } // make thing look good const existingStyle = document.querySelector('#loading-style'); if (!existingStyle) { const styleSheet = document.createElement('style'); styleSheet.id = 'loading-style'; styleSheet.textContent = ` @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.5); } } `; document.head.appendChild(styleSheet); } // target by the unique select IDs that are created in the component const countryFilter = document.getElementById('countryFilter'); const cityFilter = document.getElementById('cityFilter'); // find the dumb container let outerDiv = null; if (countryFilter) { outerDiv = countryFilter.closest('div[style*="display: flex"][style*="gap: 32px"]'); } else if (cityFilter) { outerDiv = cityFilter.closest('div[style*="display: flex"][style*="gap: 32px"]'); } // remove it if (outerDiv) { outerDiv.remove(); } // ik this approach sucks but its the best i can do. it remove ths premium messages with this specific // text so it doesnet remove the other stuff, you prob cant even understand what im sayin right now const premiumMessageDiv = document.querySelector('.premium-message-text'); if (premiumMessageDiv) { const messageText = premiumMessageDiv.textContent.trim(); const errorMessages = [ "Error: Cannot access server regions because you have not purchased the game.", "Error: This game requires users to teleport to a subplace. As a result, server regions cannot be retrieved." ]; if (errorMessages.includes(messageText)) { showMessage("END"); } } } else { // If disable is false, remove the loading bar const loadingBar = document.querySelector('#loading-bar'); if (loadingBar) { loadingBar.remove(); } // Reset any applied styles const styleSheet = document.querySelector('#loading-style'); if (styleSheet) { styleSheet.remove(); } } } /******************************************************* name of function: fetchPlayerThumbnails description: Fetches player thumbnails for up to 5 players. Skips the batch if an error occurs. *******************************************************/ async function fetchPlayerThumbnails(playerTokens) { const limitedTokens = playerTokens.slice(0, 5); const body = limitedTokens.map(token => ({ requestId: `0:${token}:AvatarHeadshot:150x150:png:regular`, type: "AvatarHeadShot", targetId: 0, token, format: "png", size: "150x150", })); return new Promise((resolve) => { GM_xmlhttpRequest({ method: "POST", url: "https://thumbnails.roblox.com/v1/batch", headers: { "Content-Type": "application/json", "Accept": "application/json" }, data: JSON.stringify(body), onload: function(response) { try { if (response.status >= 200 && response.status < 300) { const data = JSON.parse(response.responseText); resolve(data.data || []); } else { ConsoleLogEnabled(`HTTP error! Status: ${response.status}`); resolve([]); } } catch (error) { ConsoleLogEnabled('Error parsing batch thumbnail response:', error); resolve([]); } }, onerror: function(err) { ConsoleLogEnabled('Request error fetching batch thumbnails:', err); resolve([]); } }); }); } /******************************************************* name of function: disableFilterButton description: Disables or enables the filter button based on the input. *******************************************************/ function disableFilterButton(disable) { const filterButton = document.querySelector('.RL-filter-button'); const refreshButtons = document.querySelectorAll('.btn-more.rbx-refresh.refresh-link-icon.btn-control-xs.btn-min-width'); const filterOverlayId = 'filter-button-overlay'; const refreshOverlayClass = 'refresh-button-overlay'; if (filterButton) { const parent = filterButton.parentElement; if (disable) { // kill the filter button so it cant be clicked filterButton.disabled = true; filterButton.style.opacity = '0.5'; filterButton.style.cursor = 'not-allowed'; // an invisible overlay on it so no sneaky clicks let overlay = document.getElementById(filterOverlayId); if (!overlay) { overlay = document.createElement('div'); overlay.id = filterOverlayId; overlay.style.position = 'absolute'; overlay.style.top = '-10px'; overlay.style.left = '-10px'; overlay.style.width = 'calc(100% + 20px)'; overlay.style.height = 'calc(100% + 20px)'; overlay.style.backgroundColor = 'transparent'; overlay.style.zIndex = '9999'; overlay.style.pointerEvents = 'all'; // block clicks like a boss parent.style.position = 'relative'; parent.appendChild(overlay); } } else { // bring the filter button back to life filterButton.disabled = false; filterButton.style.opacity = '1'; filterButton.style.cursor = 'pointer'; // remove that annoying overlay const overlay = document.getElementById(filterOverlayId); if (overlay) { overlay.remove(); } } } else { ConsoleLogEnabled('Filter button not found! Something is wrong!'); notifications("Something's wrong. Please report an issue on Greasyfork.", "error", "⚠️", "15000"); } if (refreshButtons.length > 0) { refreshButtons.forEach((refreshButton) => { const refreshParent = refreshButton.parentElement; if (disable) { // same overlay trick but for refresh buttons let refreshOverlay = refreshParent.querySelector(`.${refreshOverlayClass}`); if (!refreshOverlay) { refreshOverlay = document.createElement('div'); refreshOverlay.className = refreshOverlayClass; refreshOverlay.style.position = 'absolute'; refreshOverlay.style.top = '-10px'; refreshOverlay.style.left = '-10px'; refreshOverlay.style.width = 'calc(100% + 20px)'; refreshOverlay.style.height = 'calc(100% + 20px)'; refreshOverlay.style.backgroundColor = 'transparent'; refreshOverlay.style.zIndex = '9999'; refreshOverlay.style.pointerEvents = 'all'; // no clicks allowed here either refreshParent.style.position = 'relative'; refreshParent.appendChild(refreshOverlay); } } else { // remove overlays and let buttons live again const refreshOverlay = refreshParent.querySelector(`.${refreshOverlayClass}`); if (refreshOverlay) { refreshOverlay.remove(); } } }); } else { ConsoleLogEnabled('Refresh button not found!'); notifications("Something's wrong. Please report an issue on Greasyfork.", "error", "⚠️", "15000"); } } /******************************************************* name of function: rbx_card description: Creates the roblox cards that are not from server regions *******************************************************/ async function rbx_card(serverId, playerTokens, maxPlayers, playing, gameId) { // Fetch player thumbnails (up to 5) const thumbnails = await fetchPlayerThumbnails(playerTokens); // Create the server card container const cardItem = document.createElement('li'); cardItem.className = 'rbx-game-server-item col-md-3 col-sm-4 col-xs-6'; // Create the player thumbnails container const playerThumbnailsContainer = document.createElement('div'); playerThumbnailsContainer.className = 'player-thumbnails-container'; // add player thumbnails to the container (up to 5) thumbnails.forEach(thumbnail => { const playerAvatar = document.createElement('span'); playerAvatar.className = 'avatar avatar-headshot-md player-avatar'; const thumbnailImage = document.createElement('span'); thumbnailImage.className = 'thumbnail-2d-container avatar-card-image'; const img = document.createElement('img'); img.src = thumbnail.imageUrl; img.alt = ''; img.title = ''; thumbnailImage.appendChild(img); playerAvatar.appendChild(thumbnailImage); playerThumbnailsContainer.appendChild(playerAvatar); }); // Add the 6th placeholder for remaining players if (playing > 5) { const remainingPlayers = playing - 5; const placeholder = document.createElement('span'); placeholder.className = 'avatar avatar-headshot-md player-avatar hidden-players-placeholder'; placeholder.textContent = `+${remainingPlayers}`; placeholder.style.cssText = ` background-color: #6a6f81; /* Grayish-blue background */ color: white; display: flex; align-items: center; justify-content: center; border-radius: 50%; /* Fully round */ font-size: 16px; /* Larger font size */ width: 60px; /* Larger width */ height: 60px; /* Larger height */ `; playerThumbnailsContainer.appendChild(placeholder); } // Create the server details container const serverDetails = document.createElement('div'); serverDetails.className = 'rbx-game-server-details game-server-details'; // add sevrer stautus yea const serverStatus = document.createElement('div'); serverStatus.className = 'text-info rbx-game-status rbx-game-server-status text-overflow'; serverStatus.textContent = `${playing} of ${maxPlayers} people max`; serverDetails.appendChild(serverStatus); // Add the player count gauge const gaugeContainer = document.createElement('div'); gaugeContainer.className = 'server-player-count-gauge border'; const gaugeInner = document.createElement('div'); gaugeInner.className = 'gauge-inner-bar border'; gaugeInner.style.width = `${(playing / maxPlayers) * 100}%`; gaugeContainer.appendChild(gaugeInner); serverDetails.appendChild(gaugeContainer); // Create a container for the buttons const buttonContainer = document.createElement('div'); buttonContainer.className = 'button-container'; buttonContainer.style.cssText = ` display: flex; gap: 8px; /* Space between buttons */ `; // add the "Join" button const joinButton = document.createElement('button'); joinButton.type = 'button'; joinButton.className = 'btn-full-width btn-control-xs rbx-game-server-join game-server-join-btn btn-primary-md btn-min-width'; joinButton.textContent = 'Join'; // add click event to join the server joinButton.addEventListener('click', () => { //showLoadingOverlay(); Roblox.GameLauncher.joinGameInstance(gameId, serverId); }); buttonContainer.appendChild(joinButton); // add the "Invite" button const inviteButton = document.createElement('button'); inviteButton.type = 'button'; inviteButton.className = 'btn-full-width btn-control-xs rbx-game-server-invite game-server-invite-btn btn-secondary-md btn-min-width'; inviteButton.textContent = 'Invite'; // invite copy. inviteButton.addEventListener('click', () => { const inviteLink = `https://oqarshi.github.io/Invite/?placeid=${gameId}&serverid=${serverId}`; ConsoleLogEnabled('Copied invite link:', inviteLink); navigator.clipboard.writeText(inviteLink).then(() => { notifications('Success! Invite link copied to clipboard!', 'success', '🎉', '2000'); ConsoleLogEnabled('Invite link copied to clipboard'); // Change text to "Copied!" const originalText = inviteButton.textContent; inviteButton.textContent = 'Copied!'; inviteButton.disabled = true; // Revert text after 2 seconds setTimeout(() => { inviteButton.textContent = originalText; inviteButton.disabled = false; }, 1000); }).catch(err => { ConsoleLogEnabled('Failed to copy invite link:', err); notifications('Failed! Invite link copied to clipboard!', 'error', '⚠️', '2000'); }); }); buttonContainer.appendChild(inviteButton); // add the button container to the server details serverDetails.appendChild(buttonContainer); // assemble the card const cardContainer = document.createElement('div'); cardContainer.className = 'card-item'; cardContainer.appendChild(playerThumbnailsContainer); cardContainer.appendChild(serverDetails); cardItem.appendChild(cardContainer); // add the card to the server list const serverList = document.querySelector('#rbx-public-game-server-item-container'); serverList.appendChild(cardItem); } /********************************************************************************************************************************************************************************************************************************************* End of function for the notification function *********************************************************************************************************************************************************************************************************************************************/ /********************************************************************************************************************************************************************************************************************************************* Launching Function *********************************************************************************************************************************************************************************************************************************************/ /******************************************************* name of function: showLoadingOverlay description: Loading box when joining a server + Shows server location *******************************************************/ // WARNING: Do not republish this script. Licensed for personal use only. async function showLoadingOverlay(gameId, serverId) { // Background overlay with blur effect const overlay = document.createElement('div'); 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.12)'; overlay.style.zIndex = '999999'; overlay.style.opacity = '0'; overlay.style.transition = 'opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1)'; // Main loading container - Golden ratio 1:1.618 (width:height) - optimized dimensions const container = document.createElement('div'); container.style.position = 'fixed'; container.style.top = '50%'; container.style.left = '50%'; container.style.transform = 'translate(-50%, -50%)'; container.style.width = '534px'; // 580px * 0.92 container.style.height = '380px'; // Increased height to accommodate repositioned server details container.style.background = 'linear-gradient(145deg, #1e1e1e, #161616)'; container.style.borderRadius = '22px'; // 24px * 0.92 container.style.boxShadow = ` 0 18px 55px rgba(0, 0, 0, 0.5), 0 7px 23px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1) `; container.style.border = '1px solid rgba(255, 255, 255, 0.12)'; container.style.display = 'flex'; container.style.flexDirection = 'column'; container.style.padding = '33px'; // 36px * 0.92 container.style.fontFamily = 'system-ui, -apple-system, sans-serif'; container.style.zIndex = '1000000'; container.style.opacity = '0'; container.style.transition = 'opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1), transform 0.5s cubic-bezier(0.4, 0, 0.2, 1)'; container.style.transform = 'translate(-50%, -55%) scale(0.9)'; // Exit button const exitButton = document.createElement('button'); exitButton.innerHTML = '✕'; exitButton.style.position = 'absolute'; exitButton.style.top = '15px'; // 16px * 0.92 exitButton.style.right = '15px'; // 16px * 0.92 exitButton.style.width = '33px'; // 36px * 0.92 exitButton.style.height = '33px'; // 36px * 0.92 exitButton.style.borderRadius = '11px'; // 12px * 0.92 exitButton.style.border = '1px solid rgba(255, 255, 255, 0.15)'; exitButton.style.background = 'linear-gradient(145deg, #2a2a2a, #1f1f1f)'; exitButton.style.color = '#a0a0a0'; exitButton.style.fontSize = '15px'; // 16px * 0.92 exitButton.style.fontWeight = '600'; exitButton.style.cursor = 'pointer'; exitButton.style.display = 'flex'; exitButton.style.alignItems = 'center'; exitButton.style.justifyContent = 'center'; exitButton.style.transition = 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)'; exitButton.style.zIndex = '1000001'; exitButton.style.boxShadow = '0 2px 7px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1)'; exitButton.style.outline = 'none'; // Exit button hover effects exitButton.addEventListener('mouseenter', () => { exitButton.style.background = 'linear-gradient(145deg, #333333, #262626)'; exitButton.style.color = '#ffffff'; exitButton.style.transform = 'scale(1.05)'; exitButton.style.boxShadow = '0 4px 11px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.15)'; }); exitButton.addEventListener('mouseleave', () => { exitButton.style.background = 'linear-gradient(145deg, #2a2a2a, #1f1f1f)'; exitButton.style.color = '#a0a0a0'; exitButton.style.transform = 'scale(1)'; exitButton.style.boxShadow = '0 2px 7px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1)'; }); exitButton.addEventListener('mousedown', () => { exitButton.style.transform = 'scale(0.95)'; }); exitButton.addEventListener('mouseup', () => { exitButton.style.transform = 'scale(1.05)'; }); // Exit button click handler exitButton.addEventListener('click', () => { overlay.style.opacity = '0'; container.style.transform = 'translate(-50%, -55%) scale(0.9)'; setTimeout(() => { if (document.body.contains(overlay)) { document.body.removeChild(overlay); } if (document.head.contains(style)) { document.head.removeChild(style); } }, 400); }); // Top section with icon and main text const topSection = document.createElement('div'); topSection.style.display = 'flex'; topSection.style.alignItems = 'center'; topSection.style.marginBottom = '26px'; // 28px * 0.92 // Game icon container with glow effect const iconContainer = document.createElement('div'); iconContainer.style.width = '77px'; // 84px * 0.92 iconContainer.style.height = '77px'; // 84px * 0.92 iconContainer.style.borderRadius = '18px'; // 20px * 0.92 iconContainer.style.background = 'linear-gradient(145deg, #2a2a2a, #1f1f1f)'; iconContainer.style.display = 'flex'; iconContainer.style.alignItems = 'center'; iconContainer.style.justifyContent = 'center'; iconContainer.style.marginRight = '22px'; // 24px * 0.92 iconContainer.style.border = '1px solid rgba(255, 255, 255, 0.15)'; iconContainer.style.overflow = 'hidden'; iconContainer.style.flexShrink = '0'; iconContainer.style.boxShadow = '0 4px 15px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1)'; iconContainer.style.transition = 'transform 0.3s ease, box-shadow 0.3s ease'; // Default logo (placeholder) with subtle animation const defaultLogo = document.createElement('div'); defaultLogo.style.width = '40px'; // 44px * 0.92 defaultLogo.style.height = '40px'; // 44px * 0.92 defaultLogo.style.borderRadius = '11px'; // 12px * 0.92 defaultLogo.style.background = 'linear-gradient(145deg, #404040, #333333)'; defaultLogo.style.display = 'flex'; defaultLogo.style.alignItems = 'center'; defaultLogo.style.justifyContent = 'center'; defaultLogo.style.fontSize = '18px'; // 20px * 0.92 defaultLogo.style.animation = 'pulse-glow 2s ease-in-out infinite'; defaultLogo.innerHTML = `<img src="${window.Base64Images.logo}" alt="Logo" width="80" height="80">`; // Game icon (will replace default when loaded) const gameIcon = document.createElement('img'); gameIcon.style.width = '100%'; gameIcon.style.height = '100%'; gameIcon.style.objectFit = 'cover'; gameIcon.style.borderRadius = '18px'; // 20px * 0.92 gameIcon.style.display = 'none'; gameIcon.style.transition = 'opacity 0.3s ease'; iconContainer.appendChild(defaultLogo); iconContainer.appendChild(gameIcon); // Text container with server details const textContainer = document.createElement('div'); textContainer.style.flex = '1'; textContainer.style.display = 'flex'; textContainer.style.flexDirection = 'column'; // Main loading text with gradient const loadingText = document.createElement('div'); loadingText.textContent = 'Joining Roblox Game'; loadingText.style.fontSize = '24px'; // 26px * 0.92 loadingText.style.fontWeight = '700'; loadingText.style.background = 'linear-gradient(135deg, #ffffff, #e5e5e5)'; loadingText.style.webkitBackgroundClip = 'text'; loadingText.style.webkitTextFillColor = 'transparent'; loadingText.style.backgroundClip = 'text'; loadingText.style.marginBottom = '6px'; loadingText.style.letterSpacing = '-0.03em'; loadingText.style.lineHeight = '1.2'; // Animated dots const dotsSpan = document.createElement('span'); dotsSpan.style.animation = 'dots 1.5s steps(4, end) infinite'; loadingText.appendChild(dotsSpan); // Status text with better styling const statusText = document.createElement('div'); statusText.textContent = 'Please wait while we connect you'; statusText.style.fontSize = '14px'; // 15px * 0.92 statusText.style.color = '#a0a0a0'; statusText.style.lineHeight = '1.4'; statusText.style.fontWeight = '500'; statusText.style.marginBottom = '12px'; textContainer.appendChild(loadingText); textContainer.appendChild(statusText); topSection.appendChild(iconContainer); topSection.appendChild(textContainer); // Server location section with enhanced styling const locationSection = document.createElement('div'); locationSection.style.display = 'flex'; locationSection.style.flexDirection = 'column'; locationSection.style.alignItems = 'center'; locationSection.style.justifyContent = 'center'; locationSection.style.marginBottom = '16px'; locationSection.style.padding = '18px'; locationSection.style.background = 'linear-gradient(145deg, #282828, #202020)'; locationSection.style.borderRadius = '15px'; locationSection.style.border = '1px solid rgba(255, 255, 255, 0.1)'; locationSection.style.minHeight = '60px'; locationSection.style.boxShadow = 'inset 0 2px 4px rgba(0, 0, 0, 0.2)'; locationSection.style.position = 'relative'; locationSection.style.overflow = 'hidden'; // Subtle background pattern const pattern = document.createElement('div'); pattern.style.position = 'absolute'; pattern.style.top = '0'; pattern.style.left = '0'; pattern.style.right = '0'; pattern.style.bottom = '0'; pattern.style.opacity = '0.03'; pattern.style.backgroundImage = 'radial-gradient(circle at 2px 2px, white 1px, transparent 0)'; pattern.style.backgroundSize = '18px 18px'; locationSection.appendChild(pattern); // Location content const locationContent = document.createElement('div'); locationContent.style.textAlign = 'center'; locationContent.style.opacity = '0'; locationContent.style.transition = 'opacity 0.4s ease, transform 0.4s ease'; locationContent.style.transform = 'translateY(10px)'; locationContent.style.zIndex = '1'; locationContent.style.position = 'relative'; // Flag and location text const locationDisplay = document.createElement('div'); locationDisplay.style.fontSize = '17px'; locationDisplay.style.color = '#ffffff'; locationDisplay.style.fontWeight = '600'; locationDisplay.style.marginBottom = '4px'; locationDisplay.style.letterSpacing = '-0.01em'; const locationSubtext = document.createElement('div'); locationSubtext.style.fontSize = '12px'; locationSubtext.style.color = '#999999'; locationSubtext.style.fontWeight = '500'; locationSubtext.style.textTransform = 'uppercase'; locationSubtext.style.letterSpacing = '0.5px'; locationContent.appendChild(locationDisplay); locationContent.appendChild(locationSubtext); locationSection.appendChild(locationContent); // Server details container - repositioned between location and loading bar const serverDetailsContainer = document.createElement('div'); serverDetailsContainer.style.display = 'flex'; serverDetailsContainer.style.justifyContent = 'space-between'; serverDetailsContainer.style.alignItems = 'center'; serverDetailsContainer.style.gap = '12px'; serverDetailsContainer.style.marginBottom = '12px'; serverDetailsContainer.style.padding = '12px 16px'; serverDetailsContainer.style.background = 'linear-gradient(145deg, #262626, #1e1e1e)'; serverDetailsContainer.style.borderRadius = '12px'; serverDetailsContainer.style.border = '1px solid rgba(255, 255, 255, 0.08)'; serverDetailsContainer.style.opacity = '0'; serverDetailsContainer.style.transition = 'opacity 0.4s ease 0.2s'; serverDetailsContainer.style.boxShadow = 'inset 0 1px 3px rgba(0, 0, 0, 0.2)'; // Helper function to create ID display elements - horizontal layout function createIdDisplay(label, value, color) { const container = document.createElement('div'); container.style.display = 'flex'; container.style.alignItems = 'center'; container.style.gap = '8px'; container.style.padding = '6px 10px'; container.style.background = 'rgba(255, 255, 255, 0.03)'; container.style.borderRadius = '8px'; container.style.border = '1px solid rgba(255, 255, 255, 0.06)'; container.style.flex = '1'; container.style.minWidth = '0'; const labelSpan = document.createElement('span'); labelSpan.textContent = label; labelSpan.style.fontSize = '9px'; labelSpan.style.color = '#888888'; labelSpan.style.fontWeight = '600'; labelSpan.style.textTransform = 'uppercase'; labelSpan.style.letterSpacing = '0.5px'; labelSpan.style.flexShrink = '0'; const valueSpan = document.createElement('span'); valueSpan.textContent = value || 'N/A'; valueSpan.style.fontSize = '11px'; valueSpan.style.color = color; valueSpan.style.fontWeight = '600'; valueSpan.style.fontFamily = 'Monaco, Consolas, monospace'; valueSpan.style.overflow = 'hidden'; valueSpan.style.textOverflow = 'ellipsis'; valueSpan.style.whiteSpace = 'nowrap'; valueSpan.style.minWidth = '0'; valueSpan.style.flex = '1'; container.appendChild(labelSpan); container.appendChild(valueSpan); return container; } // Create Game ID and Server ID displays const gameIdDisplay = createIdDisplay('Game', gameId, '#60a5fa'); const serverIdDisplay = createIdDisplay('Server', serverId, '#34d399'); serverDetailsContainer.appendChild(gameIdDisplay); serverDetailsContainer.appendChild(serverIdDisplay); // Enhanced loading bar container const loadingBarContainer = document.createElement('div'); loadingBarContainer.style.width = '100%'; loadingBarContainer.style.height = '7px'; loadingBarContainer.style.backgroundColor = '#2a2a2a'; loadingBarContainer.style.borderRadius = '4px'; loadingBarContainer.style.overflow = 'hidden'; loadingBarContainer.style.marginBottom = '12px'; loadingBarContainer.style.boxShadow = 'inset 0 2px 4px rgba(0, 0, 0, 0.3)'; loadingBarContainer.style.border = '1px solid rgba(255, 255, 255, 0.05)'; // Enhanced loading bar with better gradient const loadingBar = document.createElement('div'); loadingBar.style.height = '100%'; loadingBar.style.background = 'linear-gradient(90deg, #3b82f6, #60a5fa, #93c5fd, #60a5fa, #3b82f6)'; loadingBar.style.backgroundSize = '300% 100%'; loadingBar.style.borderRadius = '4px'; loadingBar.style.animation = 'loading-slide 2s ease-in-out infinite'; loadingBar.style.width = '60%'; loadingBar.style.boxShadow = '0 0 11px rgba(96, 165, 250, 0.4)'; // RoLocate branding section const brandingSection = document.createElement('div'); brandingSection.style.textAlign = 'center'; brandingSection.style.marginTop = 'auto'; brandingSection.style.paddingTop = '8px'; brandingSection.style.borderTop = '1px solid rgba(255, 255, 255, 0.06)'; const brandingText = document.createElement('div'); brandingText.textContent = 'RoLocate by Oqarshi'; brandingText.style.fontSize = '11px'; brandingText.style.color = '#666666'; brandingText.style.fontWeight = '600'; brandingText.style.letterSpacing = '0.8px'; brandingText.style.textTransform = 'uppercase'; brandingText.style.opacity = '0.7'; brandingText.style.transition = 'opacity 0.2s ease, color 0.2s ease'; // Subtle hover effect for branding brandingText.addEventListener('mouseenter', () => { brandingText.style.opacity = '1'; brandingText.style.color = '#888888'; }); brandingText.addEventListener('mouseleave', () => { brandingText.style.opacity = '0.7'; brandingText.style.color = '#666666'; }); brandingSection.appendChild(brandingText); // Enhanced CSS animations const style = document.createElement('style'); style.textContent = ` @keyframes loading-slide { 0% { transform: translateX(-100%); background-position: 0% 50%; } 100% { transform: translateX(250%); background-position: 100% 50%; } } @keyframes pulse-glow { 0%, 100% { opacity: 0.7; transform: scale(1); } 50% { opacity: 1; transform: scale(1.05); } } @keyframes dots { 0%, 20% { content: ''; } 40% { content: '.'; } 60% { content: '..'; } 80%, 100% { content: '...'; } } @keyframes shimmer { 0% { background-position: -200px 0; } 100% { background-position: 200px 0; } } `; document.head.appendChild(style); // Assemble the overlay loadingBarContainer.appendChild(loadingBar); container.appendChild(exitButton); container.appendChild(topSection); container.appendChild(locationSection); container.appendChild(serverDetailsContainer); // Repositioned here container.appendChild(loadingBarContainer); container.appendChild(brandingSection); overlay.appendChild(container); document.body.appendChild(overlay); // Enhanced icon hover effect iconContainer.addEventListener('mouseenter', () => { iconContainer.style.transform = 'scale(1.05)'; iconContainer.style.boxShadow = '0 7px 23px rgba(0, 0, 0, 0.4), 0 0 18px rgba(96, 165, 250, 0.2)'; }); iconContainer.addEventListener('mouseleave', () => { iconContainer.style.transform = 'scale(1)'; iconContainer.style.boxShadow = '0 4px 15px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1)'; }); // Fetch and display game icon if (gameId) { getUniverseIdFromPlaceId(gameId) .then(universeId => getGameIconFromUniverseId(universeId)) .then(iconUrl => { gameIcon.src = iconUrl; gameIcon.onload = () => { defaultLogo.style.opacity = '0'; setTimeout(() => { defaultLogo.style.display = 'none'; gameIcon.style.display = 'block'; gameIcon.style.opacity = '1'; }, 200); }; gameIcon.onerror = () => { ConsoleLogEnabled('Failed to load game icon, using default'); }; }) .catch(error => { ConsoleLogEnabled('Error fetching game icon:', error); }); } // Enhanced fade in animation setTimeout(() => { overlay.style.opacity = '1'; container.style.opacity = '1'; container.style.transform = 'translate(-50%, -50%) scale(1)'; }, 50); // Show server details setTimeout(() => { serverDetailsContainer.style.opacity = '1'; }, 300); // Server location detection with artificial delay and enhanced animations (async () => { // Show initial locating message statusText.textContent = 'Locating server location...'; // Artificial delay of 1 second await new Promise(resolve => setTimeout(resolve, 1000)); try { // Fetch server location after the delay const locationData = await fetchServerDetails(gameId, serverId); // Clear previous content locationDisplay.textContent = ''; // Get flag image or fallback emoji const flagEmoji = getFlagEmoji(locationData.country.code); locationDisplay.appendChild(flagEmoji); // Append location text locationDisplay.append(` ${locationData.city}, ${locationData.country.name}`); locationSubtext.textContent = `Server Located`; // Show location with enhanced animation locationContent.style.opacity = '1'; locationContent.style.transform = 'translateY(0)'; // Update status with highlight statusText.innerHTML = `Connecting to <strong style="color: #60a5fa; font-weight: 600;">${locationData.city}</strong> server`; } catch (error) { ConsoleLogEnabled('Error fetching server location:', error); // Show fallback location info locationDisplay.innerHTML = `🌍 Unknown Server Location`; locationSubtext.textContent = `JOINING FULL/RESTRICTED SERVER`; locationContent.style.opacity = '1'; locationContent.style.transform = 'translateY(0)'; statusText.textContent = 'Joining Server...'; } })(); // Enhanced fade out and remove after 8 seconds setTimeout(() => { overlay.style.opacity = '0'; container.style.transform = 'translate(-50%, -55%) scale(0.9)'; setTimeout(() => { if (document.body.contains(overlay)) { document.body.removeChild(overlay); } if (document.head.contains(style)) { document.head.removeChild(style); } }, 500); }, 6000); } /** * Fetch Universe ID from Place ID using GM_xmlhttpRequest (Tampermonkey/Greasemonkey) * @param {number|string} placeId * @returns {Promise<number>} resolves with universeId or rejects on error */ function getUniverseIdFromPlaceId(placeId) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `https://games.roblox.com/v1/games/multiget-place-details?placeIds=${placeId}`, headers: { "Accept": "application/json" }, onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); if (Array.isArray(data) && data.length > 0 && data[0].universeId) { // Console log inside the function ConsoleLogEnabled(`Universe ID for place ${placeId}: ${data[0].universeId}`); resolve(data[0].universeId); } else { reject(new Error("Universe ID not found in response.")); } } catch (e) { reject(e); } } else { reject(new Error(`HTTP error! Status: ${response.status}`)); } }, onerror: function(err) { reject(err); } }); }); } /** * Fetches the game icon thumbnail URL using universeId via GM_xmlhttpRequest * @param {number|string} universeId - The Universe ID of the game * @returns {Promise<string>} Resolves with the image URL of the game icon */ function getGameIconFromUniverseId(universeId) { const apiUrl = `https://thumbnails.roblox.com/v1/games/icons?universeIds=${universeId}&size=512x512&format=Png&isCircular=false&returnPolicy=PlaceHolder`; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: apiUrl, headers: { "Accept": "application/json" }, onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); if (Array.isArray(data.data) && data.data.length > 0 && data.data[0].imageUrl) { ConsoleLogEnabled(`Game icon URL for universe ${universeId}: ${data.data[0].imageUrl}`); resolve(data.data[0].imageUrl); } else { reject(new Error("Image URL not found in response.")); } } catch (err) { reject(err); } } else { reject(new Error(`HTTP error! Status: ${response.status}`)); } }, onerror: function(err) { reject(err); } }); }); } /********************************************************************************************************************************************************************************************************************************************* End of function for the launching function *********************************************************************************************************************************************************************************************************************************************/ } /******************************************************* End of code for the random hop button and the filter button on roblox.com/games/* *******************************************************/ })();