您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Hold Shift to move quickly, matching spacebar trick speed. Drives towards user heading direction, rather than following pano chain. Disabled in NM / NMPZ modes.
// ==UserScript== // @name Geoguessr Fast Move // @description Hold Shift to move quickly, matching spacebar trick speed. Drives towards user heading direction, rather than following pano chain. Disabled in NM / NMPZ modes. // @version 3.0 // @author James C // @match *://*.geoguessr.com/* // @run-at document-start // @icon https://www.google.com/s2/favicons?domain=geoguessr.com // @grant none // @license MIT // @namespace http://tampermonkey.net/ // @downloadURL // @updateURL // ==/UserScript== (function() { 'use strict'; // --- Configuration --- const MOVE_ATTEMPT_DELAY_MS = 10; // Delay AFTER successful pano_changed before attempting next move. (Lower = potentially faster but might need more retries) const MAX_ANGLE_DIFF = 160; // Angle tolerance for choosing next link. Higher = follows curves easier, Lower = stricter path. const ENABLE_LOGGING = false; // SET TO FALSE for normal use. Set true for debugging/timing. const ENABLE_MANUAL_LOG_MODE = false; // SET TO FALSE for normal use. Set true + hold Ctrl to time manual moves. const MANUAL_LOG_KEY = 'Control'; // Key to hold for manual logging mode. const STUCK_RETRY_DELAY_MS = 25; // Delay before retrying if no link OR only self-link found. (Lower = recovers faster) const INSTANCE_WAIT_TIMEOUT = 3000; // Max time (ms) to wait for StreetView instance on startup. const INSTANCE_CHECK_INTERVAL = 100; // How often (ms) to check for instance if initially missing. // --- End Configuration --- let JCStreetViewInstance = null; let isMoving = false; let previousPanoId = null; let googleMapsApiLoaded = false; let scriptStartTime = null; let scriptMoveCounter = 0; let isLoggingManual = false; let manualStartTime = null; let manualMoveCounter = 0; let lastLoggedManualPano = null; let isWaitingForInstance = false; let moveTimeoutId = null; // Stores setTimeout ID for next attempt // Logging functions function log(...args) { if (ENABLE_LOGGING) console.log('[FastMove]', ...args); } function errorLog(...args) { console.error('[FastMove]', ...args); } // Always log errors log("Script starting execution."); // --- Google Maps API Injection & Override --- function findAndOverrideGoogleMaps(overrider) { log("Setting up MutationObserver..."); const observer = new MutationObserver((mutations, obs) => { const googleScript = mutations.flatMap(m => Array.from(m.addedNodes)).find(node => node.tagName === 'SCRIPT' && node.src?.startsWith('https://maps.googleapis.com/')); if (googleScript) { log("Google Maps API script tag found:", googleScript.src); const oldOnload = googleScript.onload; googleScript.onload = (event) => { log("Google Maps API script onload event fired."); if (window.google && window.google.maps && !googleMapsApiLoaded) { googleMapsApiLoaded = true; obs.disconnect(); log("MutationObserver disconnected."); try { log("Calling API overrider function."); overrider(window.google); } catch (e) { errorLog("Error during override call:", e); } } else if (!googleMapsApiLoaded) { log("WARNING: onload fired but maps not found yet."); } if (typeof oldOnload === 'function') { try { oldOnload.call(googleScript, event); } catch (e) { errorLog("Error calling original onload:", e); } } }; if (window.google && window.google.maps && !googleMapsApiLoaded) { log("Google Maps API likely already loaded..."); if(googleScript.onload !== oldOnload || typeof oldOnload !== 'function') { googleScript.onload(null); } else { log("Manual trigger skipped."); } } else if (!googleMapsApiLoaded) { log("API object not present yet..."); } } }); observer.observe(document.documentElement, { childList: true, subtree: true }); } function overrideStreetViewPanorama(google) { log("Attempting to override google.maps.StreetViewPanorama..."); if (!google || !google.maps || !google.maps.StreetViewPanorama) { errorLog("Cannot override: google.maps.StreetViewPanorama not found!"); return; } const original = google.maps.StreetViewPanorama; google.maps.StreetViewPanorama = class CustomStreetViewPanorama extends original { constructor(...args) { log(">>> Custom StreetViewPanorama constructor CALLED."); super(...args); JCStreetViewInstance = this; log(">>> JCStreetViewInstance ASSIGNED:", JCStreetViewInstance ? "Success" : "Failed"); this.addListener('pano_changed', handlePanoChange); this.addListener('position_changed', () => {}); // Can add logging here if needed log("Listeners added."); } }; log("StreetViewPanorama overridden successfully."); } // --- Pano Change Handler (Drives the loop) --- function handlePanoChange() { const newPanoId = JCStreetViewInstance?.getPano(); if (!newPanoId) return; // Log manual move if applicable if (ENABLE_MANUAL_LOG_MODE && isLoggingManual && newPanoId !== lastLoggedManualPano) { manualMoveCounter++; const elapsed = manualStartTime ? Date.now() - manualStartTime : 0; log(`Manual Move #${manualMoveCounter}: -> ${newPanoId} (Elapsed: ${elapsed}ms)`); lastLoggedManualPano = newPanoId; } // If the script is running, schedule the next move attempt if (isMoving) { log(`Pano Change Confirmed: ${newPanoId}. Scheduling next move attempt.`); if (moveTimeoutId) clearTimeout(moveTimeoutId); moveTimeoutId = setTimeout(attemptMove, MOVE_ATTEMPT_DELAY_MS); } } // --- Start/Stop Script Movement --- function startMoving() { if (isMoving || isLoggingManual || isWaitingForInstance) return; // Prevent multiple starts/interference if (!JCStreetViewInstance) { log("Instance not ready on Shift press. Waiting..."); isWaitingForInstance = true; const waitStartTime = Date.now(); const intervalId = setInterval(() => { if (!JCStreetViewInstance) { // Try DOM query as fallback // Attempt to find the instance via DOM element if necessary const el = document.querySelector('[class*="street-view-container_root"]'); // Example generic selector if (el && el.__panorama && el.__panorama.addListener) { // Check if it looks like a pano instance JCStreetViewInstance = el.__panorama; log("Found instance via DOM query fallback."); } } if (JCStreetViewInstance) { // Instance found! clearInterval(intervalId); isWaitingForInstance = false; log(`Instance ready after ${Date.now() - waitStartTime}ms.`); executeStartMoving(); } else if (Date.now() - waitStartTime > INSTANCE_WAIT_TIMEOUT) { // Timeout clearInterval(intervalId); isWaitingForInstance = false; errorLog(`Instance not found after ${INSTANCE_WAIT_TIMEOUT}ms timeout. Cannot start moving.`); } }, INSTANCE_CHECK_INTERVAL); return; // Wait for interval to succeed or fail } executeStartMoving(); // Instance was ready immediately } function executeStartMoving() { if (!JCStreetViewInstance) { errorLog("ExecuteStartMoving: Instance missing!"); return; } // Final safety check isMoving = true; previousPanoId = null; // Reset previous ID scriptMoveCounter = 0; scriptStartTime = Date.now(); const initialPano = JCStreetViewInstance.getPano(); log(`SCRIPT Movement STARTED. Time: ${scriptStartTime}. Initial Pano: ${initialPano || 'Unknown'}`); log("Triggering first move attempt."); attemptMove(); // Start the event chain } function stopMoving() { if (!isMoving) return; // Only stop if actually moving const endTime = Date.now(); const duration = scriptStartTime ? endTime - scriptStartTime : 0; isMoving = false; if (moveTimeoutId) { // Clear any pending move attempt clearTimeout(moveTimeoutId); moveTimeoutId = null; log("Pending move attempt cleared."); } previousPanoId = null; log(`SCRIPT Movement STOPPED. Time: ${endTime}. Duration: ${duration}ms. Successful moves: ${scriptMoveCounter}.`); scriptStartTime = null; } // --- Core Movement Logic --- function attemptMove() { moveTimeoutId = null; // Clear ID, as we are executing this attempt // Check essential conditions if (!isMoving || !JCStreetViewInstance) { if (isMoving) stopMoving(); return; } try { const currentPano = JCStreetViewInstance.getPano(); if (!currentPano) { log("AttemptMove: Failed to get currentPano."); scheduleRetry(); return; } // Retry if pano ID fails const pov = JCStreetViewInstance.getPov(); if (!pov) { log(`AttemptMove: Failed to get POV for ${currentPano}.`); scheduleRetry(); return; } // Retry if POV fails if (typeof JCStreetViewInstance.getLinks !== 'function') { errorLog("getLinks not a function!"); stopMoving(); return; } // Fatal error const links = JCStreetViewInstance.getLinks(); let currentHeading = pov.heading; currentHeading = (currentHeading % 360 + 360) % 360; let bestLink = null; let minDiff = MAX_ANGLE_DIFF; let foundValidLink = false; log(`Attempting move from ${currentPano}. Heading: ${currentHeading.toFixed(1)}`); if (Array.isArray(links)) { for (const link of links) { if (!link || typeof link.pano !== 'string' || typeof link.heading !== 'number') continue; // Skip immediate U-turn if (link.pano === previousPanoId) { log(` Skipping link to previous: ${link.pano}`); continue; } let linkHeading = link.heading; linkHeading = (linkHeading % 360 + 360) % 360; let diff = Math.abs(currentHeading - linkHeading); if (diff > 180) diff = 360 - diff; if (diff < minDiff) { minDiff = diff; bestLink = link; log(` Found potential link: ${link.pano} (Diff: ${minDiff.toFixed(1)})`); } } } else { log(` No links array found for ${currentPano}.`); } // --- Decision --- if (bestLink) { // Avoid moving to the *exact same* pano (can happen with API glitches) if (bestLink.pano === currentPano) { log(`Script Move: Avoided self-move ${currentPano}`); // Continue to retry logic below } else { // Execute the valid move log(`>>> Executing Move: ${currentPano} -> ${bestLink.pano} (Diff: ${minDiff.toFixed(1)})`); scriptMoveCounter++; previousPanoId = currentPano; // Record the pano we are leaving JCStreetViewInstance.setPano(bestLink.pano); // Trigger the move foundValidLink = true; // Mark success // The handlePanoChange listener will schedule the next attempt } } else { log(`Script Move: No suitable link found from ${currentPano} within ${MAX_ANGLE_DIFF} degrees.`); // Continue to retry logic below } // Schedule a retry if no valid move was executed if (!foundValidLink) { scheduleRetry(); } } catch (e) { errorLog("Error during move attempt:", e); stopMoving(); } } // Helper to schedule a retry if still moving function scheduleRetry() { if (isMoving) { log(`Scheduling retry attempt in ${STUCK_RETRY_DELAY_MS}ms`); if (moveTimeoutId) clearTimeout(moveTimeoutId); // Clear existing timeout first moveTimeoutId = setTimeout(attemptMove, STUCK_RETRY_DELAY_MS); } } // --- Manual Logging & Key Listeners --- function startManualLogging() { if (!ENABLE_MANUAL_LOG_MODE || isLoggingManual || isMoving) return; if (!JCStreetViewInstance) { errorLog("Instance not ready."); return; } isLoggingManual = true; manualMoveCounter = 0; manualStartTime = Date.now(); lastLoggedManualPano = JCStreetViewInstance.getPano(); log(`MANUAL Logging STARTED...`); } function stopManualLogging() { if (!ENABLE_MANUAL_LOG_MODE || !isLoggingManual) return; const endTime = Date.now(); const duration = manualStartTime ? endTime - manualStartTime : 0; isLoggingManual = false; log(`MANUAL Logging STOPPED. Duration: ${duration}ms. Moves: ${manualMoveCounter}.`); manualStartTime = null; lastLoggedManualPano = null; } function handleKeyDown(event) { // Ignore key presses if focused on input fields, text areas, or editable content const target = event.target; const targetTagName = target.tagName.toLowerCase(); if (targetTagName === 'input' || targetTagName === 'textarea' || target.isContentEditable) { return; } // Handle Shift key for fast movement if (event.key === 'Shift' && !event.repeat) { // --- NM/NMPZ Check using 'quickplay-variant' --- // Read the quickplay variant from localStorage. const quickplayVariant = window.localStorage.getItem('quickplay-variant'); // If variant is "1" (NM) or "2" (NMPZ), do not activate fast move. // Note: localStorage stores values as strings. if (quickplayVariant === "1" || quickplayVariant === "2") { log(`Shift key ignored: Movement disabled in NM/NMPZ mode (variant: ${quickplayVariant}).`); return; // Exit without starting movement } // Log if variant is not 0, 1, or 2 (or null), but proceed anyway as default if (quickplayVariant !== "0" && quickplayVariant !== null) { log(`Shift key allowed: Unknown quickplay-variant '${quickplayVariant}'. Assuming moving allowed.`); } else if (quickplayVariant === "0") { log(`Shift key allowed: Moving game detected (variant: ${quickplayVariant}).`); } else { log(`Shift key allowed: quickplay-variant not found. Assuming moving allowed.`); } // --- End NM/NMPZ Check --- // If checks pass (i.e., not variant 1 or 2), start the fast movement startMoving(); } // Handle manual logging key (if enabled) else if (ENABLE_MANUAL_LOG_MODE && event.key === MANUAL_LOG_KEY && !event.repeat && !isLoggingManual) { startManualLogging(); } } function handleKeyUp(event) { // Stop fast movement when Shift key is released if (event.key === 'Shift') { stopMoving(); } // Stop manual logging when its key is released (if enabled and active) else if (ENABLE_MANUAL_LOG_MODE && event.key === MANUAL_LOG_KEY && isLoggingManual) { stopManualLogging(); } } // --- Initialization --- log("Attempting to find and override Google Maps API..."); findAndOverrideGoogleMaps(overrideStreetViewPanorama); log("Adding keydown/keyup event listeners to window."); window.addEventListener('keydown', handleKeyDown); window.addEventListener('keyup', handleKeyUp); })();