// ==UserScript== // @name GeoGuessr Path Logger Plus // @namespace Odinman9847 // @version 1.0.1 // @description The 2026 Path Logger Upgrade. Now with duels support, customization, gradients, RDP smoothing, fixed bugs, and more. // @author Odinman9847 (Original script by xsanda) // @copyright 2026, Odinman9847; 2021, xsanda; // @match https://www.geoguessr.com/* // @require https://openuserjs.org/src/libs/xsanda/Run_code_as_client.js // @require https://openuserjs.org/src/libs/xsanda/Google_Maps_Promise.js // @run-at document-start // @grant none // @license MIT // @downloadURL none // ==/UserScript== // --- PART 1: IMMEDIATE EXECUTION (Network & UI) --- (function() { 'use strict'; runAsClient(() => { window.__GPL_GAME_ID = null; window.__GPL_HAS_GUESSED = false; const checkURL = (url) => { if (typeof url !== 'string') return; if (url.includes('/api/lobby/') && url.includes('/join')) { const match = url.match(/\/api\/lobby\/([0-9a-f]{24})\/join/); if (match && match[1]) { window.__GPL_GAME_ID = match[1]; window.__GPL_HAS_GUESSED = false; } } if (url.endsWith('/guess')) { window.__GPL_HAS_GUESSED = true; } }; const originalFetch = window.fetch; window.fetch = function(...args) { if (args[0]) checkURL(args[0]); return originalFetch.apply(this, args); }; const originalOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url) { checkURL(url); return originalOpen.apply(this, arguments); }; }); const SETTINGS_KEY = 'pl_settings_v2'; let state = { enabled: true, style: 'gradient', solidColor: '#ff0000', gradStart: '#22c55e', gradMiddle: '#eab308', gradEnd: '#ef4444', thickness: 4 }; function loadSettings() { const saved = localStorage.getItem(SETTINGS_KEY); if (saved) state = { ...state, ...JSON.parse(saved) }; } function saveSettings() { localStorage.setItem(SETTINGS_KEY, JSON.stringify(state)); } loadSettings(); function uiHexToHsl(hex) { let r = parseInt(hex.slice(1, 3), 16) / 255, g = parseInt(hex.slice(3, 5), 16) / 255, b = parseInt(hex.slice(5, 7), 16) / 255; let max = Math.max(r, g, b), min = Math.min(r, g, b), h, s, l = (max + min) / 2; if (max === min) h = s = 0; else { let d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return { h: h * 360, s: s * 100, l: l * 100 }; } const uiHslToHex = (h, s, l) => { l /= 100; s /= 100; const a = s * Math.min(l, 1 - l); const f = n => { const k = (n + h / 30) % 12; const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); return Math.round(255 * color).toString(16).padStart(2, '0'); }; return `#${f(0)}${f(8)}${f(4)}`; } const uiInterpolateHSL = (c1, c2, t) => { const h1 = uiHexToHsl(c1), h2 = uiHexToHsl(c2); let hue1 = h1.h, hue2 = h2.h; if (hue2 - hue1 > 180) hue1 += 360; else if (hue2 - hue1 < -180) hue2 += 360; return uiHslToHex((hue1 + (hue2 - hue1) * t) % 360, h1.s + (h2.s - h1.s) * t, h1.l + (h2.l - h1.l) * t); } const presets = [ { name: 'The Classic', start: '#22c55e', middle: '#eab308', end: '#ef4444' }, { name: 'The Fire', start: '#fef08a', middle: '#fb923c', end: '#dc2626' }, { name: 'Ocean', start: '#70e1d4', middle: '#2d568b', end: '#161b5a' }, { name: 'Rose', start: '#fddbff', middle: '#bc57b4', end: '#3a123b' }, { name: 'Forest', start: '#aef29c', middle: '#246149', end: '#06280a' }, { name: 'Peanut', start: '#eae79f', middle: '#ffa500', end: '#171107' } ]; const style = document.createElement('style'); style.innerHTML = ` :root { --pl-bg-modal: #1e1b3a; --pl-bg-accent: #2a2650; --pl-bg-hover: #332d5c; --pl-blue: #3b82f6; --pl-blue-hover: #2563eb; --pl-text: #ffffff; --pl-dim: #9ca3af; --pl-border: #2a2650; } #pl-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(8px); z-index: 99999; display: none; justify-content: center; align-items: center; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } #pl-modal { background-color: var(--pl-bg-modal); width: 100%; max-width: 550px; max-height: 90vh; border-radius: 20px; border: 1px solid var(--pl-border); color: var(--pl-text); box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); display: flex; flex-direction: column; animation: pl-fade-in 0.2s ease-out; overflow: hidden; } @keyframes pl-fade-in { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } .pl-header { padding: 20px 24px; border-bottom: 1px solid var(--pl-border); flex-shrink: 0; } .pl-header h2 { margin: 0; font-size: 20px; font-weight: 700; } .pl-header p { margin: 4px 0 0 0; color: var(--pl-dim); font-size: 13px; } .pl-content { padding: 20px 24px; display: flex; flex-direction: column; gap: 20px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--pl-bg-accent) transparent; } .pl-content::-webkit-scrollbar { width: 6px; } .pl-content::-webkit-scrollbar-thumb { background: var(--pl-bg-accent); border-radius: 10px; } .pl-section { display: flex; flex-direction: column; } .pl-title { font-size: 16px; font-weight: 500; color: white; margin-bottom: 10px; } .pl-sub-label { font-size: 13px; color: var(--pl-dim); margin-bottom: 6px; display: block; } .pl-row { display: flex; justify-content: space-between; align-items: center; gap: 10px; } .pl-btn-group { display: flex; gap: 8px; } .pl-btn-toggle { flex: 1; padding: 10px; border-radius: 10px; border: none; background: var(--pl-bg-accent); color: var(--pl-dim); cursor: pointer; font-weight: 500; transition: 0.2s; font-size: 14px;} .pl-btn-toggle.active { background: var(--pl-blue); color: white; } .pl-color-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; } .pl-swatch { height: 40px; border-radius: 8px; border: 2px solid var(--pl-border); position: relative; cursor: pointer; overflow: hidden; } .pl-native-picker { position: absolute; opacity: 0; width: 100%; height: 100%; cursor: pointer; } .pl-preview-bar { height: 48px; border-radius: 10px; border: 2px solid var(--pl-border); margin-top: 4px; } .pl-preset-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } .pl-preset { background: transparent; border: none; padding: 0; cursor: pointer; text-align: center; } .pl-preset-bar { height: 32px; border-radius: 6px; border: 2px solid var(--pl-border); margin-bottom: 4px; cursor: pointer; } .pl-preset span { font-size: 10px; color: var(--pl-dim); cursor: pointer; } .pl-range { -webkit-appearance: none; width: 100%; height: 22px; background: transparent; outline: none; border: none !important; box-shadow: none !important; margin-top: 4px; } .pl-range::-webkit-slider-runnable-track { width: 100%; height: 6px; cursor: pointer; background: var(--pl-bg-accent); border-radius: 10px; border: none !important; } .pl-range::-webkit-slider-thumb { -webkit-appearance: none; height: 20px; width: 20px; border-radius: 50%; background: var(--pl-blue); cursor: pointer; border: 2px solid white !important; box-shadow: 0 0 8px rgba(0,0,0,0.4); margin-top: -7px; } .pl-preview-box { background: #0f0d1f; border-radius: 12px; border: 1px solid var(--pl-border); padding: 15px; height: 80px; display: flex; align-items: center; flex-shrink: 0; } .pl-footer { padding: 16px 24px; border-top: 1px solid var(--pl-border); display: flex; justify-content: flex-end; gap: 12px; flex-shrink: 0; } .pl-btn { padding: 10px 20px; border-radius: 10px; border: none; cursor: pointer; font-weight: 600; font-size: 14px; transition: 0.2s; } .pl-btn-cancel { background: var(--pl-bg-accent); color: var(--pl-dim); } .pl-btn-save { background: var(--pl-blue); color: white; } .pl-hidden { display: none; } .pl-switch { position: relative; width: 50px; height: 26px; cursor: pointer; flex-shrink: 0; } .pl-switch input { opacity: 0; width: 0; height: 0; } .pl-slider-round { position: absolute; inset: 0; background: #4b5563; border-radius: 34px; transition: .3s; } .pl-slider-round:before { position: absolute; content: ""; height: 18px; width: 18px; left: 4px; bottom: 4px; background: white; border-radius: 50%; transition: .3s; } .pl-switch input:checked + .pl-slider-round { background: var(--pl-blue); } .pl-switch input:checked + .pl-slider-round:before { transform: translateX(24px); } `; document.head.appendChild(style); const backdrop = document.createElement('div'); backdrop.id = 'pl-backdrop'; backdrop.innerHTML = `

Path Logger Settings

Customize your GeoGuessr path visualization

Enable Path Logger

Show path on the map

Line Style

Gradient Colors

Start
Middle
End
Preview Bar
Presets

Solid Color

Line Thickness: ${state.thickness}px

Line Preview

`; function updateUI() { document.getElementById('pl-solid-ui').classList.toggle('pl-hidden', state.style === 'gradient'); document.getElementById('pl-grad-ui').classList.toggle('pl-hidden', state.style === 'solid'); document.getElementById('pl-style-solid').classList.toggle('active', state.style === 'solid'); document.getElementById('pl-style-grad').classList.toggle('active', state.style === 'gradient'); document.getElementById('pl-swatch-start').style.backgroundColor = state.gradStart; document.getElementById('pl-swatch-mid').style.backgroundColor = state.gradMiddle; document.getElementById('pl-swatch-end').style.backgroundColor = state.gradEnd; document.getElementById('pl-swatch-solid').style.backgroundColor = state.solidColor; document.getElementById('pl-pick-start').value = state.gradStart; document.getElementById('pl-pick-mid').value = state.gradMiddle; document.getElementById('pl-pick-end').value = state.gradEnd; document.getElementById('pl-pick-solid').value = state.solidColor; let gradStr = 'linear-gradient(to right, '; const svgGrad = document.getElementById('pl-svg-grad'); svgGrad.innerHTML = ''; for (let i = 0; i <= 40; i++) { const t = i / 40; const color = t < 0.5 ? uiInterpolateHSL(state.gradStart, state.gradMiddle, t * 2) : uiInterpolateHSL(state.gradMiddle, state.gradEnd, (t - 0.5) * 2); const pos = (t * 100).toFixed(1) + '%'; gradStr += `${color} ${pos}${i < 40 ? ', ' : ')'}`; const stop = document.createElementNS("http://www.w3.org/2000/svg", "stop"); stop.setAttribute("offset", pos); stop.setAttribute("stop-color", color); svgGrad.appendChild(stop); } document.getElementById('pl-grad-bar').style.background = gradStr; document.getElementById('pl-thick-val').innerText = state.thickness + 'px'; const path = document.getElementById('pl-svg-path'); path.setAttribute('stroke-width', state.thickness); path.setAttribute('stroke', state.style === 'solid' ? state.solidColor : 'url(#pl-svg-grad)'); saveSettings(); } const showModal = () => { backdrop.style.display = 'flex'; updateUI(); }; const hideModal = () => { backdrop.style.display = 'none'; }; const injectUI = () => { if (!document.getElementById('pl-backdrop')) document.body.appendChild(backdrop); document.getElementById('pl-presets').innerHTML = presets.map(p => ` `).join(''); document.querySelectorAll('.pl-preset').forEach(b => b.onclick = () => { state.gradStart = b.dataset.s; state.gradMiddle = b.dataset.m; state.gradEnd = b.dataset.e; updateUI(); }); document.getElementById('pl-enable-toggle').onchange = (e) => { state.enabled = e.target.checked; saveSettings(); }; document.getElementById('pl-style-solid').onclick = () => { state.style = 'solid'; updateUI(); }; document.getElementById('pl-style-grad').onclick = () => { state.style = 'gradient'; updateUI(); }; document.getElementById('pl-pick-start').oninput = (e) => { state.gradStart = e.target.value; updateUI(); }; document.getElementById('pl-pick-mid').oninput = (e) => { state.gradMiddle = e.target.value; updateUI(); }; document.getElementById('pl-pick-end').oninput = (e) => { state.gradEnd = e.target.value; updateUI(); }; document.getElementById('pl-pick-solid').oninput = (e) => { state.solidColor = e.target.value; updateUI(); }; document.getElementById('pl-thick-range').oninput = (e) => { state.thickness = e.target.value; updateUI(); }; document.getElementById('pl-cancel').onclick = hideModal; document.getElementById('pl-save').onclick = hideModal; backdrop.onclick = (e) => { if (e.target === backdrop) hideModal(); }; }; const injectButton = () => { const headerRight = document.querySelector('[class*=header-desktop_desktopSectionRight__]'); if (headerRight && !document.getElementById('pl-settings-btn')) { const btn = document.createElement('div'); btn.id = 'pl-settings-btn'; btn.style = 'cursor:pointer; display:flex; align-items:center; margin-right:15px; transition:opacity 0.2s;'; btn.innerHTML = ``; btn.onclick = showModal; headerRight.insertBefore(btn, headerRight.firstChild); injectUI(); } }; const uiObserver = new MutationObserver(injectButton); uiObserver.observe(document.body, { childList: true, subtree: true }); })(); // --- PART 2: MAP LOGIC (DEFERRED) --- googleMapsPromise.then(() => runAsClient(() => { const google = window.google; const SETTINGS_KEY = 'pl_settings_v2'; const RDP_EPSILON = 0.00002; const TELEPORT_DISTANCE = 120; const getSettings = () => { const defaults = { enabled: true, style: 'gradient', solidColor: '#ff0000', gradStart: '#22c55e', gradMiddle: '#eab308', gradEnd: '#ef4444', thickness: 4 }; try { return { ...defaults, ...JSON.parse(localStorage.getItem(SETTINGS_KEY) || "{}") }; } catch(e) { return defaults; } }; // --- Helpers --- const hexToHsl = (hex) => { let r = parseInt(hex.slice(1, 3), 16) / 255, g = parseInt(hex.slice(3, 5), 16) / 255, b = parseInt(hex.slice(5, 7), 16) / 255; let max = Math.max(r, g, b), min = Math.min(r, g, b), h, s, l = (max + min) / 2; if (max === min) h = s = 0; else { let d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return { h: h * 360, s: s * 100, l: l * 100 }; }; const hslToHex = (h, s, l) => { l /= 100; s /= 100; const a = s * Math.min(l, 1 - l); const f = n => { const k = (n + h / 30) % 12; const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); return Math.round(255 * color).toString(16).padStart(2, '0'); }; return `#${f(0)}${f(8)}${f(4)}`; }; const interpolateHSL = (c1, c2, t) => { const h1 = hexToHsl(c1), h2 = hexToHsl(c2); let hue1 = h1.h, hue2 = h2.h; if (hue2 - hue1 > 180) hue1 += 360; else if (hue2 - hue1 < -180) hue2 += 360; return hslToHex((hue1 + (hue2 - hue1) * t) % 360, h1.s + (h2.s - h1.s) * t, h1.l + (h2.l - h1.l) * t); }; const getDistMeters = (p1, p2) => { const R = 6371e3; const dLat = (p2.lat - p1.lat) * Math.PI/180; const dLng = (p2.lng - p1.lng) * Math.PI/180; const a = Math.sin(dLat/2)**2 + Math.cos(p1.lat * Math.PI/180) * Math.cos(p2.lat * Math.PI/180) * Math.sin(dLng/2)**2; return R * (2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))); }; const findPerpDist = (p, l1, l2) => { if (l1.lat === l2.lat && l1.lng === l2.lng) return Math.sqrt((p.lat - l1.lat)**2 + (p.lng - l1.lng)**2); let num = Math.abs((l2.lng - l1.lng) * p.lat - (l2.lat - l1.lat) * p.lng + l2.lat * l1.lng - l2.lng * l1.lat); let den = Math.sqrt((l2.lng - l1.lng)**2 + (l2.lat - l1.lat)**2); return num / den; }; const rdp = (points, epsilon) => { if (points.length <= 2) return points; let dmax = 0, index = 0, end = points.length - 1; for (let i = 1; i < end; i++) { let d = findPerpDist(points[i], points[0], points[end]); if (d > dmax) { index = i; dmax = d; } } if (dmax > epsilon) { let res1 = rdp(points.slice(0, index + 1), epsilon); let res2 = rdp(points.slice(index), epsilon); return res1.slice(0, res1.length - 1).concat(res2); } else { return [points[0], points[end]]; } }; const saveToStorage = (key, value) => { const val = JSON.stringify(value); while (JSON.stringify(localStorage).length + val.length > 5242880) { const ts = JSON.parse(localStorage.timestamps || "{}"); const oldest = Object.entries(ts).sort((a,b) => a[1]-b[1])[0]; if (!oldest) break; delete ts[oldest[0]]; Object.keys(localStorage).forEach(k => { if(k.startsWith(oldest[0])) localStorage.removeItem(k); }); localStorage.timestamps = JSON.stringify(ts); } localStorage.setItem(key, val); }; // --- State Detection --- let markers = [], inGame = false, route = [], currentRound = undefined, mapState = 0; let lastObservedSpawn = null; const isGamePage = () => { const path = location.pathname; return path.includes("/challenge/") || path.includes("/results/") || path.includes("/game/") || path.includes("/duels/") || path.includes("/multiplayer") || path.includes("/summary"); }; const resultShown = () => { if (document.querySelector('[data-qa="result-view-bottom"]')) return true; if (document.querySelector('[class*="round-score-2_root"]')) return true; if (location.href.includes('results') || location.href.includes('summary')) return true; return false; }; const isGameFinished = () => { if (location.href.includes('results') || location.href.includes('summary')) return true; if (document.querySelector('[data-qa="play-again-button"]') || document.querySelector('[class*="play-again-button"]')) return true; return false; }; const getGameID = () => { const urlMatch = location.href.match(/\w{15,}/); if (urlMatch && !location.pathname.includes('multiplayer')) return urlMatch[0]; if (window.__GPL_GAME_ID) return window.__GPL_GAME_ID; if (urlMatch) return urlMatch[0]; return "unknown_game"; }; const getRoundNumber = () => { const spEl = document.querySelector('[data-qa=round-number] :nth-child(2)'); if (spEl) return parseInt(spEl.innerHTML); const duelEl = document.querySelector('[class*="round-score-2_roundNumber"]'); if (duelEl) return parseInt(duelEl.innerText.replace(/\D/g, '')); return 0; }; const onMove = (sv) => { if (!getSettings().enabled || !isGamePage()) return; const pos = { lat: sv.position.lat(), lng: sv.position.lng() }; // 1. Result visible? ALWAYS update spawn buffer and reset guess flag. if (resultShown()) { lastObservedSpawn = pos; if (window.__GPL_HAS_GUESSED) window.__GPL_HAS_GUESSED = false; return; } // 2. Spectating? Stop. if (window.__GPL_HAS_GUESSED) return; // 3. Start Recording Logic if (!inGame) { inGame = true; // Use buffered spawn point as point 0 route = lastObservedSpawn ? [[lastObservedSpawn]] : [[]]; } // 4. Teleport Check const currentSeg = route[route.length - 1]; const last = currentSeg[currentSeg.length - 1]; if (last && getDistMeters(last, pos) > TELEPORT_DISTANCE) { route.push([]); } route[route.length - 1].push(pos); }; const onMapUpdate = (map) => { if (!isGamePage() || !google.maps.geometry) return; // Add Round Number to checksum to handle persistent Duel pages const newState = (inGame ? 5 : 0) + (resultShown() ? 10 : 0) + (isGameFinished() ? 20 : 0) + getRoundNumber(); if (newState === mapState) return; mapState = newState; markers.forEach(m => m.setMap(null)); markers = []; if (resultShown()) { const settings = getSettings(); const currentGameID = getGameID(); // SAVE Logic if (inGame) { const rNum = getRoundNumber(); const saveID = currentGameID + '-' + rNum; const simplifiedRoute = route.map(segment => rdp(segment, RDP_EPSILON)); const encoded = simplifiedRoute.map(p => google.maps.geometry.encoding.encodePath(p.map(x => new google.maps.LatLng(x)))); saveToStorage(saveID, encoded); const ts = JSON.parse(localStorage.timestamps || "{}"); ts[currentGameID] = Date.now(); localStorage.timestamps = JSON.stringify(ts); inGame = false; } // RENDER if (!settings.enabled) return; let keysToShow = []; if (isGameFinished()) { keysToShow = Object.keys(localStorage).filter(k => k.startsWith(currentGameID) && !k.includes('timestamp')); } else { const rNum = getRoundNumber(); keysToShow = [currentGameID + '-' + rNum]; } keysToShow.forEach(k => { const raw = localStorage.getItem(k); if (raw) { const segs = JSON.parse(raw).map(x => google.maps.geometry.encoding.decodePath(x)); const total = segs.reduce((a, b) => a + b.length, 0); let count = 0; segs.forEach(path => { const step = Math.max(2, Math.ceil(total / 100)); for (let i = 0; i < path.length - 1; i += (step - 1)) { const chunk = path.slice(i, i + step); const t = count / (total || 1); const color = settings.style === 'solid' ? settings.solidColor : (t < 0.5 ? interpolateHSL(settings.gradStart, settings.gradMiddle, t * 2) : interpolateHSL(settings.gradMiddle, settings.gradEnd, (t - 0.5) * 2)); markers.push(new google.maps.Polyline({ path: chunk, strokeColor: color, strokeWeight: settings.thickness, geodesic: true, zIndex: Math.floor(t * 100), clickable: false })); count += (chunk.length - 1); } }); } }); markers.forEach(m => m.setMap(map)); } }; const oldSV = google.maps.StreetViewPanorama; google.maps.StreetViewPanorama = Object.assign(function (...args) { const res = oldSV.apply(this, args); this.addListener('position_changed', () => onMove(this)); return res; }, { prototype: Object.create(oldSV.prototype) }); const oldMap = google.maps.Map; google.maps.Map = Object.assign(function (...args) { const res = oldMap.apply(this, args); this.addListener('idle', () => onMapUpdate(this)); return res; }, { prototype: Object.create(oldMap.prototype) }); }));