// ==UserScript==
// @name GeoGuessr - Let's explore the world!
// @namespace https://github.com/JD-YH03D/release
// @version 1.7.1
// @description Universal geography game assistant with Mini Map - GeoGuessr, WorldGuessr, OpenGuessr, FreeGuessr
// @author Bintang Toba Pro
// @license MIT
// @match *://*.geoguessr.com/*
// @match *://openguessr.com/*
// @match *://*.worldguessr.com/*
// @match *://*.worldguessr.net/*
// @match *://freeguessr.com/*
// @match *://geoduels.io/*
// @match *://guesswhereyouare.com/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @connect nominatim.openstreetmap.org
// @connect discord.com
// @run-at document-idle
// @icon https://www.geoguessr.com/favicon.ico
// @downloadURL https://update.greasyfork.org/scripts/578278/GeoGuessr%20-%20Let%27s%20explore%20the%20world%21.user.js
// @updateURL https://update.greasyfork.org/scripts/578278/GeoGuessr%20-%20Let%27s%20explore%20the%20world%21.meta.js
// ==/UserScript==
/* global google, L,jshint esversion: 11, */
/* eslint-disable no-unused-vars */
/*
┌───────────────────────────────────────────────────────────┐
| Key | Function | Description |
| ----- | -------------- | -------------------------------- |
| `Tab` | Settings Panel | Open/close settings interface |
| `V` | Info Panel | Toggle location info display |
| `M` | Manual Marker | Place marker on game map |
| `X` | Refresh | Reset for next round |
| `1` | Auto Place | Place marker (exact position) |
| `2` | Safe Place | Place marker (with offset) |
| `S` | Zoom In | Increase mini-map zoom level |
| `A` | Zoom Out | Decrease mini-map zoom level |
| `C` | Copy Coords | Copy coordinates to clipboard |
| `G` | Google Maps | Open location in Google Maps |
| `D` | Discord | Send location to Discord webhook |
└───────────────────────────────────────────────────────────┘
*/
(function() {
'use strict';
// ==================== CONFIG ====================
const CONFIG = {
NAME: 'Bintang Toba Pro',
VERSION: '1.7.1',
NOMINATIM_URL: 'https://nominatim.openstreetmap.org/reverse',
DISCORD_STORAGE_KEY: 'bintang_toba_discord_webhook',
HOTKEYS_STORAGE_KEY: 'bintang_toba_hotkeys',
DEFAULT_HOTKEYS: {
panel: 'Tab',
marker: 'M',
info: 'V',
refresh: 'X',
zoomIn: 'S',
zoomOut: 'A',
copyCoords: 'C',
googleMaps: 'G',
discord: 'D',
autoPlace: '1',
safePlace: '2'
},
FEATURES_STORAGE_KEY: 'bintang_toba_features',
DEFAULT_FEATURES: {
autoMarker: false,
safeMode: false
}
};
// ==================== STATE ====================
let state = {
platform: null,
coords: { lat: null, lng: null },
address: null,
gameMap: null,
marker: null,
panel: null,
infoVisible: false,
hotkeys: null,
features: null,
miniMap: null,
miniMapMarker: null,
miniMapVisible: false,
markerPlacedThisRound: false
};
// Cleanup tracking for memory management
let monitoringInterval = null;
// XHR Interception for coordinate extraction (from Release.js)
let interceptedCoords = { lat: null, lng: null };
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
if (method.toUpperCase() === 'POST' &&
(url.startsWith('https://maps.googleapis.com/$rpc/google.internal.maps.mapsjs.v1.MapsJsInternalService/GetMetadata') ||
url.startsWith('https://maps.googleapis.com/$rpc/google.internal.maps.mapsjs.v1.MapsJsInternalService/SingleImageSearch'))) {
this.addEventListener('load', function () {
try {
const interceptedResult = this.responseText;
const pattern = /-?\d+\.\d+,-?\d+\.\d+/g;
const matches = interceptedResult.match(pattern);
if (matches && matches.length > 0) {
const split = matches[0].split(",");
const lat = Number.parseFloat(split[0]);
const lng = Number.parseFloat(split[1]);
if (isValidCoord(lat, lng)) {
interceptedCoords = { lat, lng };
log('📡 XHR intercepted coordinates:', lat.toFixed(6), lng.toFixed(6));
}
}
} catch (e) {
// Silent - parsing failed
}
});
}
return originalOpen.apply(this, arguments);
};
// ==================== HELPER FUNCTIONS ====================
function log(...args) {
console.log('[BintangTobaPro]', ...args);
}
// XSS safe text escaping
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = String(text);
return div.innerHTML;
}
// showNotification removed - using status text instead
function safeGM_getValue(key, defaultValue) {
try {
if (typeof GM_getValue !== 'undefined') {
const val = GM_getValue(key);
return val !== undefined ? val : defaultValue;
}
const stored = localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : defaultValue;
} catch (e) {
return defaultValue;
}
}
function safeGM_setValue(key, value) {
try {
if (typeof GM_setValue !== 'undefined') {
GM_setValue(key, value);
} else {
localStorage.setItem(key, JSON.stringify(value));
}
} catch (e) {
console.error('[BintangTobaPro] Storage error:', e);
}
}
function detectPlatform() {
const url = window.location.href.toLowerCase();
if (url.includes('geoguessr')) return 'geoguessr';
if (url.includes('worldguessr')) return 'worldguessr';
if (url.includes('openguessr')) return 'openguessr';
if (url.includes('freeguessr') || url.includes('guesswhereyouare')) return 'freeguessr';
if (url.includes('geoduel')) return 'geoduels';
return 'unknown';
}
function isValidCoord(lat, lng) {
return typeof lat === 'number' && typeof lng === 'number' &&
!isNaN(lat) && !isNaN(lng) &&
lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180;
}
// ==================== COORDINATE EXTRACTION ====================
// Get coordinates from XHR interception (Release.js method)
function getInterceptedCoords() {
if (isValidCoord(interceptedCoords.lat, interceptedCoords.lng)) {
return { lat: interceptedCoords.lat, lng: interceptedCoords.lng };
}
return null;
}
// Deep walk React Fiber tree to find streetview/panorama object
function walkFiber(fiber, depth = 0) {
if (!fiber || depth > 15) return null;
try {
// Check memoizedProps for panorama/streetview objects
const props = fiber.memoizedProps;
if (props) {
// Direct panorama object
if (props.panorama?.location?.latLng) return props.panorama;
// Map with streetview
if (props.streetView?.location?.latLng) return props.streetView;
// Nested in children
if (props.children?.props?.panorama?.location?.latLng) return props.children.props.panorama;
}
// Check updateQueue for effects with deps
const queue = fiber.updateQueue;
if (queue?.lastEffect) {
let effect = queue.lastEffect;
const seen = new Set();
do {
if (seen.has(effect)) break;
seen.add(effect);
if (effect.deps) {
for (const dep of effect.deps) {
if (dep?.location?.latLng) return dep;
}
}
effect = effect.next;
} while (effect && effect !== queue.lastEffect);
}
// Walk sibling and return
const fromSibling = walkFiber(fiber.sibling, depth + 1);
if (fromSibling) return fromSibling;
const fromReturn = walkFiber(fiber.return, depth + 1);
if (fromReturn) return fromReturn;
} catch (e) {
// Silent - fiber walking can throw
}
return null;
}
// Extract from Google Maps StreetView on the page
function extractFromGoogleSV() {
try {
// Try to find any StreetView instance via Google Maps API
const canvases = document.querySelectorAll('.widget-scene-canvas, canvas[class*="scene"]');
for (const canvas of canvases) {
let el = canvas;
// Walk up to find the panorama container
for (let i = 0; i < 10 && el; i++) {
el = el.parentElement;
if (!el) break;
const fiberKey = Object.keys(el).find(k => k.startsWith('__reactFiber'));
if (fiberKey) {
const sv = walkFiber(el[fiberKey], 0);
if (sv?.location?.latLng) {
const lat = typeof sv.location.latLng.lat === 'function'
? sv.location.latLng.lat() : sv.location.latLng.lat;
const lng = typeof sv.location.latLng.lng === 'function'
? sv.location.latLng.lng() : sv.location.latLng.lng;
if (isValidCoord(lat, lng)) return { lat, lng };
}
}
}
}
} catch (e) {
// Silent
}
return null;
}
let lastExtractLog = 0;
function extractCoordinates() {
try {
// ── Method 0: XHR Interception (Release.js method - fastest) ──
const fromXHR = getInterceptedCoords();
if (fromXHR) {
return fromXHR;
}
// ── Method 1: GeoGuessr - React Fiber ──
if (state.platform === 'geoguessr') {
// 1a. Try data-qa="panorama" container
const panorama = document.querySelector('div[data-qa="panorama"]');
if (panorama) {
const fiberKey = Object.keys(panorama).find(k => k.startsWith('__reactFiber'));
if (fiberKey) {
const fiber = panorama[fiberKey];
// Try known paths first (fast)
const paths = [
fiber.return?.return?.return?.sibling?.memoizedProps?.panorama,
fiber.return?.return?.return?.return?.sibling?.memoizedProps?.panorama,
fiber.return?.updateQueue?.lastEffect?.deps?.[0],
fiber.return?.return?.updateQueue?.lastEffect?.deps?.[0],
fiber.child?.memoizedProps?.panorama,
fiber.return?.memoizedProps?.panorama,
];
for (const sv of paths) {
if (sv?.location?.latLng) {
const lat = typeof sv.location.latLng.lat === 'function'
? sv.location.latLng.lat() : sv.location.latLng.lat;
const lng = typeof sv.location.latLng.lng === 'function'
? sv.location.latLng.lng() : sv.location.latLng.lng;
if (isValidCoord(lat, lng)) {
return { lat, lng };
}
}
}
// Fallback: deep walk fiber tree
const sv = walkFiber(fiber, 0);
if (sv?.location?.latLng) {
const lat = typeof sv.location.latLng.lat === 'function'
? sv.location.latLng.lat() : sv.location.latLng.lat;
const lng = typeof sv.location.latLng.lng === 'function'
? sv.location.latLng.lng() : sv.location.latLng.lng;
if (isValidCoord(lat, lng)) {
return { lat, lng };
}
}
}
}
// 1b. Try Google Maps StreetView canvas approach
const fromSV = extractFromGoogleSV();
if (fromSV) return fromSV;
}
// ── Method 2: Iframe with location parameter ──
const iframes = document.querySelectorAll('iframe');
for (const iframe of iframes) {
const src = iframe.src || iframe.getAttribute('data-src') || '';
if (!src || src.length < 10) continue;
try {
const baseUrl = src.startsWith('http') ? src : window.location.origin + src;
const url = new URL(baseUrl);
// location=lat,lng
const location = url.searchParams.get('location');
if (location) {
const parts = location.split(',');
if (parts.length >= 2) {
const lat = parseFloat(parts[0]);
const lng = parseFloat(parts[1]);
if (isValidCoord(lat, lng)) return { lat, lng };
}
}
// lat=...&lng=... or lat=...&lon=...
const iLat = url.searchParams.get('lat');
const iLng = url.searchParams.get('lng') || url.searchParams.get('lon');
if (iLat && iLng) {
const lat = parseFloat(iLat);
const lng = parseFloat(iLng);
if (isValidCoord(lat, lng)) return { lat, lng };
}
// cbll=lat,lng (Google StreetView embed)
const cbll = url.searchParams.get('cbll');
if (cbll) {
const parts = cbll.split(',');
if (parts.length >= 2) {
const lat = parseFloat(parts[0]);
const lng = parseFloat(parts[1]);
if (isValidCoord(lat, lng)) return { lat, lng };
}
}
// viewpoint=lat,lng
const viewpoint = url.searchParams.get('viewpoint');
if (viewpoint) {
const parts = viewpoint.split(',');
if (parts.length >= 2) {
const lat = parseFloat(parts[0]);
const lng = parseFloat(parts[1]);
if (isValidCoord(lat, lng)) return { lat, lng };
}
}
} catch (e) {
continue;
}
}
// ── Method 3: FreeGuessr / React Fiber latLong ──
const freeGuessrSelectors = ['.iframeWithStreetView', '[class*="streetview"]', '[class*="panorama"]'];
for (const selector of freeGuessrSelectors) {
const el = document.querySelector(selector);
if (!el) continue;
const fiberKey = Object.keys(el).find(k => k.startsWith('__reactFiber'));
if (!fiberKey) continue;
const fiber = el[fiberKey];
// Try latLong prop
const latLong = fiber.return?.memoizedProps?.latLong
|| fiber.return?.return?.memoizedProps?.latLong
|| fiber.memoizedProps?.latLong;
if (Array.isArray(latLong) && latLong.length === 2) {
const lat = latLong[0];
const lng = latLong[1];
if (isValidCoord(lat, lng)) return { lat, lng };
}
// Try coordinates prop
const coordinates = fiber.return?.memoizedProps?.coordinates
|| fiber.return?.return?.memoizedProps?.coordinates;
if (coordinates) {
const lat = coordinates.lat || coordinates.latitude;
const lng = coordinates.lng || coordinates.lon || coordinates.longitude;
if (isValidCoord(lat, lng)) return { lat, lng };
}
}
// ── Method 4: Google StreetView canvas (all platforms) ──
if (state.platform !== 'geoguessr') {
const fromSV = extractFromGoogleSV();
if (fromSV) return fromSV;
}
// ── Method 5: URL parameters ──
const urlParams = new URLSearchParams(window.location.search);
const lat = urlParams.get('lat');
const lng = urlParams.get('lng') || urlParams.get('lon');
if (lat && lng) {
const parsedLat = parseFloat(lat);
const parsedLng = parseFloat(lng);
if (isValidCoord(parsedLat, parsedLng)) {
return { lat: parsedLat, lng: parsedLng };
}
}
// ── Method 6: Window/global game state ──
try {
const safeWin = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
// Some games expose coords globally
if (safeWin.__gameState?.coords) {
const c = safeWin.__gameState.coords;
if (isValidCoord(c.lat, c.lng)) return { lat: c.lat, lng: c.lng };
}
if (safeWin.gameCoordinates) {
const c = safeWin.gameCoordinates;
if (isValidCoord(c.lat, c.lng)) return { lat: c.lat, lng: c.lng };
}
} catch (e) {
// Silent - not all platforms expose globals
}
} catch (e) {
console.error('[BintangTobaPro] Extract error:', e);
}
// Log only once every 5 seconds to avoid console spam
const now = Date.now();
if (now - lastExtractLog > 5000) {
lastExtractLog = now;
log('No coordinates found (platform:', state.platform, ')');
}
return null;
}
// ==================== ADDRESS LOOKUP ====================
let addressQueue = [];
let addressProcessing = false;
let lastAddressCall = 0;
async function lookupAddress(lat, lng) {
return new Promise((resolve, reject) => {
if (!isValidCoord(lat, lng)) {
reject(new Error('Invalid coordinates'));
return;
}
addressQueue.push({ lat, lng, resolve, reject });
processAddressQueue();
});
}
function processAddressQueue() {
if (addressProcessing || addressQueue.length === 0) return;
const now = Date.now();
const minInterval = state.platform === 'geoguessr' ? 1000 : 1500;
const elapsed = now - lastAddressCall;
if (elapsed >= minInterval) {
addressProcessing = true;
const { lat, lng, resolve, reject } = addressQueue.shift();
const url = `${CONFIG.NOMINATIM_URL}?lat=${lat}&lon=${lng}&format=json&accept-language=en`;
if (typeof GM_xmlhttpRequest !== 'undefined') {
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: { 'Accept': 'application/json' },
onload: (res) => {
lastAddressCall = Date.now();
addressProcessing = false;
if (res.status === 200) {
try {
resolve(JSON.parse(res.responseText));
} catch (e) {
reject(e);
}
} else {
reject(new Error(`HTTP ${res.status}`));
}
setTimeout(processAddressQueue, minInterval);
},
onerror: (e) => {
lastAddressCall = Date.now();
addressProcessing = false;
reject(e);
setTimeout(processAddressQueue, minInterval);
}
});
} else {
fetch(url, { headers: { 'Accept': 'application/json' } })
.then(res => {
lastAddressCall = Date.now();
addressProcessing = false;
if (res.ok) return res.json();
throw new Error(`HTTP ${res.status}`);
})
.then(data => resolve(data))
.catch(e => reject(e))
.finally(() => {
setTimeout(processAddressQueue, minInterval);
});
}
} else {
setTimeout(processAddressQueue, minInterval - elapsed);
}
}
function formatAddress(addr) {
if (!addr?.address) return null;
const a = addr.address;
const parts = [
a.road || a.street,
a.city || a.town || a.village,
a.state || a.province,
a.country
].filter(Boolean);
return parts.join(', ') || a.country || 'Unknown';
}
// ==================== MAP MARKER ====================
// Apply safe mode offset to coordinates
function applySafeMode(coords) {
if (!state.features || !state.features.safeMode) {
return coords;
}
// Add random offset 0-4 meters (makes score 4500-5000 instead of perfect 5000)
const sway = [Math.random() > 0.5, Math.random() > 0.5];
const multiplier = Math.random() * 4;
const horizontalAmount = Math.random() * multiplier;
const verticalAmount = Math.random() * multiplier;
let lat = coords.lat;
let lng = coords.lng;
sway[0] ? lat += verticalAmount : lat -= verticalAmount;
sway[1] ? lng += horizontalAmount : lng -= horizontalAmount;
return { lat, lng };
}
function toggleMarker(forceCoords = null) {
const coords = forceCoords || extractCoordinates();
if (!coords || !isValidCoord(coords.lat, coords.lng)) {
log('No valid coordinates');
return false;
}
// Apply safe mode if enabled
const finalCoords = applySafeMode(coords);
// Remove existing marker
if (state.marker) {
try {
if (typeof google !== 'undefined' && google.maps && state.marker.setMap) {
state.marker.setMap(null);
} else if (typeof L !== 'undefined' && state.gameMap) {
state.gameMap.removeLayer(state.marker);
}
} catch (e) {
log('Marker removal error:', e.message);
}
state.marker = null;
log('Marker removed');
return false;
}
// Add Google Maps marker
if (typeof google !== 'undefined' && google.maps && state.gameMap) {
state.marker = new google.maps.Marker({
position: new google.maps.LatLng(finalCoords.lat, finalCoords.lng),
map: state.gameMap,
icon: {
path: google.maps.SymbolPath.CIRCLE,
scale: 8,
fillColor: '#ff4444',
fillOpacity: 1,
strokeColor: '#ffffff',
strokeWeight: 2
}
});
log('Marker added (Google Maps)', state.features.safeMode ? '[Safe Mode]' : '[Exact]');
return true;
}
// Add Leaflet marker
if (typeof L !== 'undefined' && state.gameMap) {
state.marker = L.marker([finalCoords.lat, finalCoords.lng]).addTo(state.gameMap);
log('Marker added (Leaflet)', state.features.safeMode ? '[Safe Mode]' : '[Exact]');
return true;
}
log('No map available');
return false;
}
// Auto place marker (called when coordinates detected + auto marker enabled)
function autoPlaceMarker() {
if (!state.features || !state.features.autoMarker) return;
if (state.markerPlacedThisRound) return; // Only once per round
const coords = extractCoordinates();
if (!coords || !isValidCoord(coords.lat, coords.lng)) return;
// Check if map is ready
if (!state.gameMap) {
findMapInstance();
if (!state.gameMap) return;
}
// Place marker
const success = toggleMarker(coords);
if (success) {
state.markerPlacedThisRound = true;
log('🎯 Auto marker placed', state.features.safeMode ? '(Safe Mode)' : '(Exact)');
}
}
function findMapInstance() {
try {
// React Fiber approach
const containers = document.querySelectorAll(
"[class*='guess-map_canvas'], .leaflet-container, [class*='mapCanvas']"
);
for (const container of containers) {
const fiberKey = Object.keys(container).find(k => k.startsWith('__reactFiber'));
if (fiberKey) {
const fiber = container[fiberKey];
state.gameMap = fiber.return?.memoizedProps?.map ||
fiber.return?.return?.memoizedProps?.map ||
fiber.child?.memoizedProps?.value?.map ||
null;
if (state.gameMap) {
log('Map found via React Fiber');
return true;
}
}
}
} catch (e) {
console.error('[BintangTobaPro] Map find error:', e);
}
return false;
}
// ==================== INFO DISPLAY WITH MINI MAP ====================
function createInfoDisplay() {
const display = document.createElement('div');
display.id = 'geohelper-info';
display.style.cssText = `
position: fixed;
top: 80px;
left: 10px;
width: 280px;
background: rgba(15,15,20,0.95);
color: #fff;
border-radius: 10px;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 12px;
z-index: 999998;
display: none;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
border: 1px solid rgba(255,255,255,0.1);
overflow: hidden;
`;
display.innerHTML = `
Waiting for location data...
`;
document.body.appendChild(display);
return display;
}
function initMiniMap() {
if (typeof L === 'undefined') {
log('Leaflet not available, loading from CDN...');
// Load Leaflet CSS
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
document.head.appendChild(link);
// Load Leaflet JS
const script = document.createElement('script');
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
script.onload = () => {
log('Leaflet loaded successfully');
setTimeout(() => setupMiniMap(), 100);
};
document.head.appendChild(script);
return;
}
setupMiniMap();
}
function setupMiniMap() {
if (state.miniMap || typeof L === 'undefined') return;
try {
// Create mini map
state.miniMap = L.map('geohelper-minimap', {
zoomControl: false,
attributionControl: false,
zoomAnimation: true,
fadeAnimation: true
});
// Add tile layer (OpenStreetMap)
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19
}).addTo(state.miniMap);
// Set initial view
state.miniMap.setView([0, 0], 2);
log('Mini map initialized');
const zoomLevel = document.getElementById('geohelper-zoom-level');
// Setup zoom controls with level display
document.getElementById('geohelper-zoom-in').onclick = () => {
if (state.miniMap) {
state.miniMap.zoomIn();
if (zoomLevel) zoomLevel.textContent = 'x' + state.miniMap.getZoom();
}
};
document.getElementById('geohelper-zoom-out').onclick = () => {
if (state.miniMap) {
state.miniMap.zoomOut();
if (zoomLevel) zoomLevel.textContent = 'x' + state.miniMap.getZoom();
}
};
// Update zoom level on map zoom events
state.miniMap.on('zoomend', () => {
if (zoomLevel) zoomLevel.textContent = 'x' + state.miniMap.getZoom();
});
// Setup status text (click = refresh)
const statusText = document.getElementById('geohelper-status-text');
if (statusText) {
statusText.onclick = () => {
refreshLocation();
};
}
} catch (e) {
console.error('[BintangTobaPro] Mini map setup error:', e);
}
}
function updateMiniMap() {
if (!state.miniMap) return;
const coords = extractCoordinates();
if (!coords || !isValidCoord(coords.lat, coords.lng)) return;
// Update or create marker
if (state.miniMapMarker) {
state.miniMapMarker.setLatLng([coords.lat, coords.lng]);
} else {
state.miniMapMarker = L.marker([coords.lat, coords.lng]).addTo(state.miniMap);
}
// Update map view
state.miniMap.setView([coords.lat, coords.lng], 12);
// Update coordinates overlay
const overlay = document.getElementById('geohelper-coords-overlay');
if (overlay) {
overlay.textContent = `${coords.lat.toFixed(6)}, ${coords.lng.toFixed(6)}`;
}
// Invalidate size to fix rendering
setTimeout(() => {
if (state.miniMap) {
state.miniMap.invalidateSize();
}
}, 100);
}
function updateStatusText(text, color) {
const el = document.getElementById('geohelper-status-text');
if (el) {
el.textContent = text;
el.style.color = color;
}
}
function refreshLocation() {
log('Refreshing location for next round...');
// Update status
updateStatusText('Refreshing...', '#fbbf24');
// Reset state
state.coords = { lat: null, lng: null };
state.address = null;
state.markerPlacedThisRound = false;
interceptedCoords = { lat: null, lng: null };
// Remove mini map marker safely
if (state.miniMapMarker && state.miniMap) {
try {
state.miniMap.removeLayer(state.miniMapMarker);
} catch (e) {
log('Mini map marker removal warning:', e.message);
}
state.miniMapMarker = null;
}
// Reset mini map view safely
if (state.miniMap) {
try {
state.miniMap.setView([0, 0], 2);
} catch (e) {
log('Mini map reset warning:', e.message);
}
const overlay = document.getElementById('geohelper-coords-overlay');
if (overlay) overlay.textContent = '--, --';
const zoomLevel = document.getElementById('geohelper-zoom-level');
if (zoomLevel) zoomLevel.textContent = 'x2';
}
// Update info display
updateInfoDisplay();
// Re-extract coordinates after short delay
setTimeout(async () => {
const newCoords = extractCoordinates();
if (newCoords && isValidCoord(newCoords.lat, newCoords.lng)) {
state.coords = newCoords;
try {
state.address = await lookupAddress(newCoords.lat, newCoords.lng);
} catch (e) {
log('Address lookup failed after refresh');
}
updateInfoDisplay();
updateMiniMap();
updateStatusText('Ready', '#4ade80');
} else {
updateStatusText('Waiting...', '#888');
// Keep trying
setTimeout(() => {
const retry = extractCoordinates();
if (retry && isValidCoord(retry.lat, retry.lng)) {
updateStatusText('Ready', '#4ade80');
}
}, 3000);
}
}, 2000);
}
async function updateInfoDisplay() {
let display = document.getElementById('geohelper-info');
if (!display) {
display = createInfoDisplay();
}
const content = document.getElementById('geohelper-info-content');
if (!content) return;
if (state.infoVisible) {
// Use fresh extraction first, fallback to cached state.coords
const freshCoords = extractCoordinates();
const coords = (freshCoords && isValidCoord(freshCoords.lat, freshCoords.lng))
? freshCoords
: (isValidCoord(state.coords.lat, state.coords.lng) ? state.coords : null);
let html = '';
if (coords && isValidCoord(coords.lat, coords.lng)) {
state.coords = coords;
html += `
COORDINATES
${coords.lat.toFixed(6)}, ${coords.lng.toFixed(6)}
`;
if (!state.address) {
try {
state.address = await lookupAddress(coords.lat, coords.lng);
} catch (e) {
log('Address lookup pending...');
}
}
} else {
html += `
⚠️ Waiting for location...
`;
}
if (state.address) {
const formatted = formatAddress(state.address);
const countryCode = state.address.address?.country_code;
html += `
LOCATION
${escapeHtml(formatted)}
${countryCode ? `
${escapeHtml(countryCode.toUpperCase())}` : ''}
`;
}
if (coords && isValidCoord(coords.lat, coords.lng)) {
html += ``;
}
content.innerHTML = html || 'No data available
';
display.style.display = 'block';
state.miniMapVisible = true;
// Initialize mini map after display is shown
setTimeout(() => {
initMiniMap();
updateMiniMap();
}, 200);
} else {
display.style.display = 'none';
state.miniMapVisible = false;
}
}
// ==================== SETTINGS PANEL WITH TABS ====================
let activeTab = 'main';
function createPanel() {
const panel = document.createElement('div');
panel.id = 'geohelper-panel';
panel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 320px;
background: rgba(18,18,22,0.96);
color: #fff;
border-radius: 12px;
z-index: 999999;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.6);
border: 1px solid rgba(255,255,255,0.08);
overflow: hidden;
`;
const savedHotkeys = safeGM_getValue(CONFIG.HOTKEYS_STORAGE_KEY, CONFIG.DEFAULT_HOTKEYS);
const savedWebhook = safeGM_getValue(CONFIG.DISCORD_STORAGE_KEY, '');
const savedFeatures = safeGM_getValue(CONFIG.FEATURES_STORAGE_KEY, CONFIG.DEFAULT_FEATURES);
state.features = { ...CONFIG.DEFAULT_FEATURES, ...(savedFeatures || {}) };
panel.innerHTML = `
⚙️ Bintang Toba Pro
📍 Current Location
Detecting...
⌨️ Key Bindings
${Object.entries(CONFIG.DEFAULT_HOTKEYS).map(([key, val]) => `
`).join('')}
${state.platform} | v${CONFIG.VERSION}
`;
document.body.appendChild(panel);
state.panel = panel;
// ── Tab Switching ──
panel.querySelectorAll('.geohelper-tab').forEach(btn => {
btn.onclick = () => {
const tab = btn.dataset.tab;
activeTab = tab;
// Toggle content visibility
panel.querySelectorAll('.geohelper-tab-content').forEach(c => c.style.display = 'none');
const target = document.getElementById('geohelper-tab-' + tab);
if (target) target.style.display = 'block';
// Toggle tab button styles
panel.querySelectorAll('.geohelper-tab').forEach(t => {
t.style.background = 'transparent';
t.style.color = '#888';
t.style.borderBottom = '2px solid transparent';
});
btn.style.background = 'rgba(255,255,255,0.06)';
btn.style.color = '#fff';
btn.style.borderBottom = '2px solid #4ade80';
};
});
// ── Close Button ──
document.getElementById('geohelper-close').onclick = () => togglePanel();
// ── Save Button ──
document.getElementById('geohelper-save').onclick = () => {
const newHotkeys = {};
panel.querySelectorAll('[data-hotkey]').forEach(input => {
newHotkeys[input.dataset.hotkey] = input.value.trim() || CONFIG.DEFAULT_HOTKEYS[input.dataset.hotkey];
});
safeGM_setValue(CONFIG.HOTKEYS_STORAGE_KEY, newHotkeys);
safeGM_setValue(CONFIG.DISCORD_STORAGE_KEY, (document.getElementById('geohelper-webhook')?.value || '').trim());
safeGM_setValue(CONFIG.FEATURES_STORAGE_KEY, state.features);
const saveBtn = document.getElementById('geohelper-save');
saveBtn.textContent = '✅ Saved!';
saveBtn.disabled = true;
setTimeout(() => { saveBtn.textContent = '💾 Save'; saveBtn.disabled = false; }, 1500);
};
// ── Reset Button ──
document.getElementById('geohelper-reset').onclick = () => {
safeGM_setValue(CONFIG.HOTKEYS_STORAGE_KEY, CONFIG.DEFAULT_HOTKEYS);
safeGM_setValue(CONFIG.DISCORD_STORAGE_KEY, '');
safeGM_setValue(CONFIG.FEATURES_STORAGE_KEY, CONFIG.DEFAULT_FEATURES);
panel.querySelectorAll('[data-hotkey]').forEach(input => {
input.value = CONFIG.DEFAULT_HOTKEYS[input.dataset.hotkey];
});
const webhookEl = document.getElementById('geohelper-webhook');
if (webhookEl) webhookEl.value = '';
state.features = { ...CONFIG.DEFAULT_FEATURES };
const am = document.getElementById('geohelper-auto-marker');
const sm = document.getElementById('geohelper-safe-mode');
if (am) { am.checked = false; document.getElementById('geohelper-auto-marker-slider').style.backgroundColor = 'rgba(255,255,255,0.2)'; document.getElementById('geohelper-auto-marker-dot').style.left = '3px'; }
if (sm) { sm.checked = false; document.getElementById('geohelper-safe-mode-slider').style.backgroundColor = 'rgba(255,255,255,0.2)'; document.getElementById('geohelper-safe-mode-dot').style.left = '3px'; }
const resetBtn = document.getElementById('geohelper-reset');
resetBtn.textContent = '✅ Reset!';
setTimeout(() => { resetBtn.textContent = '🔄 Reset'; }, 1500);
};
// ── Hotkey Input Handling ──
panel.querySelectorAll('[data-hotkey]').forEach(input => {
input.addEventListener('keydown', (e) => {
e.preventDefault();
if (['Control', 'Alt', 'Shift', 'Meta'].includes(e.key)) return;
let key = e.key;
if (key === ' ') key = 'Space';
if (key.length === 1) key = key.toUpperCase();
const mods = [];
if (e.ctrlKey) mods.push('Ctrl');
if (e.altKey) mods.push('Alt');
if (e.shiftKey) mods.push('Shift');
input.value = [...mods, key].join('+');
});
});
// ── Toggle: Auto Marker ──
const amCb = document.getElementById('geohelper-auto-marker');
if (amCb) {
amCb.addEventListener('change', () => {
state.features.autoMarker = amCb.checked;
document.getElementById('geohelper-auto-marker-slider').style.backgroundColor = amCb.checked ? '#4ade80' : 'rgba(255,255,255,0.2)';
document.getElementById('geohelper-auto-marker-dot').style.left = amCb.checked ? '23px' : '3px';
});
}
// ── Toggle: Safe Mode ──
const smCb = document.getElementById('geohelper-safe-mode');
if (smCb) {
smCb.addEventListener('change', () => {
state.features.safeMode = smCb.checked;
document.getElementById('geohelper-safe-mode-slider').style.backgroundColor = smCb.checked ? '#4ade80' : 'rgba(255,255,255,0.2)';
document.getElementById('geohelper-safe-mode-dot').style.left = smCb.checked ? '23px' : '3px';
});
}
// ── Google Maps Button ──
document.getElementById('geohelper-maps-btn').onclick = () => {
const coords = extractCoordinates();
if (coords && isValidCoord(coords.lat, coords.lng)) {
window.open(`https://www.google.com/maps?q=${coords.lat.toFixed(6)},${coords.lng.toFixed(6)}&ll=${coords.lat.toFixed(6)},${coords.lng.toFixed(6)}&z=6`, '_blank');
} else {
updateStatusText('No coords', '#ef4444');
}
};
// ── Coords Display Update ──
const coordsDisplay = document.getElementById('geohelper-coords-display');
if (coordsDisplay) {
let coordsIntervalId = null;
const updateCoords = () => {
// Stop updating if panel is hidden
if (state.panel && state.panel.style.display === 'none') return;
const coords = extractCoordinates();
if (coords && isValidCoord(coords.lat, coords.lng)) {
coordsDisplay.innerHTML = `
Lat: ${coords.lat.toFixed(8)}
Lng: ${coords.lng.toFixed(8)}
${state.address ? `${formatAddress(state.address)}
` : ''}
`;
} else {
coordsDisplay.innerHTML = 'Waiting for location data...';
}
};
updateCoords();
coordsIntervalId = setInterval(updateCoords, 1000);
}
}
function togglePanel() {
if (!state.panel) {
createPanel();
} else {
state.panel.style.display = state.panel.style.display === 'none' ? 'block' : 'none';
}
}
// ==================== DISCORD ====================
function isValidDiscordWebhook(url) {
if (!url || typeof url !== 'string') return false;
try {
const parsed = new URL(url);
return parsed.protocol === 'https:' &&
parsed.hostname === 'discord.com' &&
parsed.pathname.includes('/api/webhooks/');
} catch (e) {
return false;
}
}
async function sendToDiscord() {
const webhook = safeGM_getValue(CONFIG.DISCORD_STORAGE_KEY, '');
if (!webhook) {
alert('Please set Discord webhook URL in settings (Tab)');
return;
}
// Validate webhook URL for security
if (!isValidDiscordWebhook(webhook)) {
alert('Invalid Discord webhook URL. Please check your settings.');
log('Invalid webhook URL detected');
return;
}
const coords = extractCoordinates();
if (!coords || !isValidCoord(coords.lat, coords.lng)) {
alert('No valid coordinates');
return;
}
const embed = {
title: '📍 Location Tracked',
description: `**${formatAddress(state.address) || 'Unknown location'}**\n\n[🗺️ Google Maps](https://www.google.com/maps?q=${coords.lat},${coords.lng})`,
color: 516235,
fields: [
{ name: 'Coordinates', value: `\`${coords.lat.toFixed(6)}, ${coords.lng.toFixed(6)}\``, inline: true },
{ name: 'Platform', value: state.platform, inline: true }
],
footer: { text: CONFIG.NAME },
timestamp: new Date().toISOString()
};
try {
if (typeof GM_xmlhttpRequest !== 'undefined') {
GM_xmlhttpRequest({
method: 'POST',
url: webhook,
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({ embeds: [embed] })
});
} else {
await fetch(webhook, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ embeds: [embed] })
});
}
log('Discord message sent');
} catch (e) {
console.error('[BintangTobaPro] Discord error:', e);
alert('Failed to send to Discord');
}
}
// ==================== KEYBOARD HANDLER ====================
function handleKeydown(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) {
return;
}
const hotkeys = safeGM_getValue(CONFIG.HOTKEYS_STORAGE_KEY, CONFIG.DEFAULT_HOTKEYS);
const key = e.key.toLowerCase();
// Panel
if (key === hotkeys.panel.toLowerCase()) {
e.preventDefault();
togglePanel();
return;
}
// Marker (on game map)
if (key === hotkeys.marker.toLowerCase()) {
e.preventDefault();
if (!state.gameMap) findMapInstance();
toggleMarker();
return;
}
// Refresh location (when info panel is open)
if (key === hotkeys.refresh.toLowerCase() && state.infoVisible) {
e.preventDefault();
e.stopPropagation();
refreshLocation();
return;
}
// Info
if (key === hotkeys.info.toLowerCase()) {
e.preventDefault();
state.infoVisible = !state.infoVisible;
updateInfoDisplay();
return;
}
// Copy coords
if (key === hotkeys.copyCoords.toLowerCase()) {
e.preventDefault();
const coords = extractCoordinates();
if (coords && isValidCoord(coords.lat, coords.lng)) {
navigator.clipboard.writeText(`${coords.lat}, ${coords.lng}`)
.then(() => log('Coords copied'))
.catch(() => log('Clipboard failed'));
}
return;
}
// Google Maps
if (key === hotkeys.googleMaps.toLowerCase()) {
e.preventDefault();
e.stopPropagation();
const coords = extractCoordinates();
log('G pressed - Coordinates:', coords);
if (coords && isValidCoord(coords.lat, coords.lng)) {
const mapsUrl = `https://www.google.com/maps?q=${coords.lat.toFixed(6)},${coords.lng.toFixed(6)}&ll=${coords.lat.toFixed(6)},${coords.lng.toFixed(6)}&z=6`;
log('Opening Google Maps:', mapsUrl);
try {
const newWindow = window.open(mapsUrl, '_blank');
if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') {
log('Popup may be blocked, trying fallback...');
window.location.href = mapsUrl;
}
} catch (err) {
console.error('[BintangTobaPro] window.open error:', err);
window.location.href = mapsUrl;
}
} else {
log('No valid coordinates available');
updateStatusText('No coords', '#ef4444');
}
return;
}
// Auto Place Marker (key 1)
if (key === hotkeys.autoPlace.toLowerCase()) {
e.preventDefault();
e.stopPropagation();
if (!state.gameMap) findMapInstance();
const success = toggleMarker();
updateStatusText(success ? 'Marked' : 'No map', success ? '#4ade80' : '#ef4444');
return;
}
// Safe Place Marker (key 2)
if (key === hotkeys.safePlace.toLowerCase()) {
e.preventDefault();
e.stopPropagation();
const wasSafeMode = state.features.safeMode;
state.features.safeMode = true;
if (!state.gameMap) findMapInstance();
const success = toggleMarker();
state.features.safeMode = wasSafeMode;
updateStatusText(success ? 'Safe Marked' : 'No map', success ? '#4ade80' : '#ef4444');
return;
}
// Zoom In (Mini Map)
if (key === hotkeys.zoomIn.toLowerCase() && state.miniMapVisible) {
e.preventDefault();
e.stopPropagation();
if (state.miniMap) {
state.miniMap.zoomIn();
}
return;
}
// Zoom Out (Mini Map)
if (key === hotkeys.zoomOut.toLowerCase() && state.miniMapVisible) {
e.preventDefault();
e.stopPropagation();
if (state.miniMap) {
state.miniMap.zoomOut();
}
return;
}
// Discord
if (key === hotkeys.discord.toLowerCase()) {
e.preventDefault();
sendToDiscord();
return;
}
}
// ==================== COORD MONITORING ====================
let lastCoords = { lat: null, lng: null };
function startMonitoring() {
// Clear existing interval to prevent memory leak
if (monitoringInterval) {
clearInterval(monitoringInterval);
}
let failCount = 0;
let lastValidCoords = null;
monitoringInterval = setInterval(async () => {
const coords = extractCoordinates();
if (coords && isValidCoord(coords.lat, coords.lng)) {
failCount = 0;
const changed = coords.lat !== lastCoords.lat || coords.lng !== lastCoords.lng;
if (changed) {
lastCoords = { lat: coords.lat, lng: coords.lng };
state.coords = { lat: coords.lat, lng: coords.lng };
log('📍 New coordinates detected:', coords.lat.toFixed(6), coords.lng.toFixed(6));
// Detect new round (big coordinate change = new round)
if (lastValidCoords) {
const distance = Math.abs(coords.lat - lastValidCoords.lat) + Math.abs(coords.lng - lastValidCoords.lng);
if (distance > 0.1) { // Significant change = new round
log('🔄 New round detected!');
state.markerPlacedThisRound = false;
// Remove old marker if exists
if (state.marker) {
if (typeof google !== 'undefined' && google.maps) {
state.marker.setMap(null);
} else if (typeof L !== 'undefined') {
state.gameMap.removeLayer(state.marker);
}
state.marker = null;
}
}
}
lastValidCoords = { lat: coords.lat, lng: coords.lng };
try {
state.address = await lookupAddress(coords.lat, coords.lng);
log('📮 Address:', formatAddress(state.address));
} catch (e) {
log('Address lookup pending...');
}
if (state.infoVisible) {
updateInfoDisplay();
}
// Update mini map if visible
if (state.miniMapVisible && state.miniMap) {
updateMiniMap();
}
// Auto place marker if enabled
autoPlaceMarker();
// Update marker position on game map
if (state.marker) {
try {
if (typeof google !== 'undefined' && state.marker.setPosition) {
state.marker.setPosition(new google.maps.LatLng(coords.lat, coords.lng));
} else if (typeof L !== 'undefined' && state.marker.setLatLng) {
state.marker.setLatLng([coords.lat, coords.lng]);
}
} catch (e) {
log('Marker position update failed');
}
}
}
} else {
failCount++;
if (failCount === 10) {
log('⏳ Still waiting for coordinates... (platform:', state.platform, ')');
failCount = 0;
}
}
}, 500);
}
// ==================== INITIALIZATION ====================
function init() {
state.platform = detectPlatform();
log('========================================');
log('Bintang Toba Pro v' + CONFIG.VERSION);
log('Platform:', state.platform);
log('========================================');
// Load saved hotkeys
state.hotkeys = safeGM_getValue(CONFIG.HOTKEYS_STORAGE_KEY, CONFIG.DEFAULT_HOTKEYS);
log('Hotkeys loaded:', state.hotkeys);
// Load saved features (with safe merge for new keys)
const loadedFeatures = safeGM_getValue(CONFIG.FEATURES_STORAGE_KEY, null);
state.features = { ...CONFIG.DEFAULT_FEATURES, ...(loadedFeatures || {}) };
log('Features loaded:', state.features);
// Event listeners
document.addEventListener('keydown', handleKeydown, true);
log('Keyboard listener attached');
// Start monitoring
startMonitoring();
log('Coordinate monitoring started');
// Find map instance
setTimeout(() => {
log('Attempting to find map instance...');
findMapInstance();
}, 2000);
// Add pulse animation (used in info panel header)
const style = document.createElement('style');
style.textContent = '@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}';
document.head.appendChild(style);
log('✅ Initialization complete!');
log('📌 HOTKEYS:');
log(' ', state.hotkeys.panel, '- Settings Panel');
log(' ', state.hotkeys.info, '- Location Info + Mini Map');
log(' ', state.hotkeys.marker, '- Marker on Game Map (Manual)');
log(' ', state.hotkeys.autoPlace, '- Auto Place Marker');
log(' ', state.hotkeys.safePlace, '- Safe Place (4500-5000 pts)');
log(' ', state.hotkeys.refresh, '- Refresh (when info open)');
log(' ', state.hotkeys.zoomIn, '- Zoom In Mini Map');
log(' ', state.hotkeys.zoomOut, '- Zoom Out Mini Map');
log(' ', state.hotkeys.copyCoords, '- Copy Coordinates');
log(' ', state.hotkeys.googleMaps, '- Open Google Maps');
log(' ', state.hotkeys.discord, '- Send to Discord');
log('');
log('🤖 AUTO FEATURES:');
log(' Auto Marker:', state.features.autoMarker ? '✅ ON' : '❌ OFF');
log(' Safe Mode:', state.features.safeMode ? '✅ ON' : '❌ OFF');
log('🚀 Press', state.hotkeys.info, 'to open info panel');
}
// Cleanup function for page unload
function cleanup() {
log('Cleaning up...');
// Clear monitoring interval
if (monitoringInterval) {
clearInterval(monitoringInterval);
monitoringInterval = null;
}
// Remove keyboard listener
document.removeEventListener('keydown', handleKeydown, true);
// Remove markers safely
if (state.marker) {
try {
if (typeof google !== 'undefined' && google.maps && state.marker.setMap) {
state.marker.setMap(null);
} else if (typeof L !== 'undefined' && state.gameMap && state.marker) {
state.gameMap.removeLayer(state.marker);
}
} catch (e) {
// Silent cleanup
}
state.marker = null;
}
// Remove mini map marker
if (state.miniMapMarker && state.miniMap) {
try {
state.miniMap.removeLayer(state.miniMapMarker);
} catch (e) {
// Silent cleanup
}
state.miniMapMarker = null;
}
// Destroy mini map
if (state.miniMap) {
try {
state.miniMap.off(); // Remove all event listeners
state.miniMap.remove();
} catch (e) {
// Silent cleanup
}
state.miniMap = null;
}
// Remove injected DOM elements
const infoPanel = document.getElementById('geohelper-info');
if (infoPanel) infoPanel.remove();
if (state.panel) { state.panel.remove(); state.panel = null; }
log('Cleanup complete');
}
// Start when DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Cleanup on page unload
window.addEventListener('beforeunload', cleanup);
window.addEventListener('pagehide', cleanup);
})();