// ==UserScript==
// @name Geoguessr Local records
// @include /^(https?)?(\:)?(\/\/)?([^\/]*\.)?geoguessr\.com($|\/.*)/
// @description Keeps local records on GeoGuessr website
// @version 0.6
// @grant GM_addStyle
// @namespace https://greasyfork.org/users/736457
// ==/UserScript==
(function () {
const LOCAL_STORAGE_RESULTS_KEY = "__geoguessr_results__";
const LOCAL_STORAGE_HIGHSCORE_TAB_KEY = "__geoguessr_highscore_tab__";
const RESULTS_DIV_ID = "__geoguessrResult";
const HIGHSCORES_DIV_ID = "__geoguessrLocalHighscores";
const HIGHSCORES_BUTTON_BAR_ID = "__geoguessrLocalHighscoresButtonBar";
let USER_ID = undefined;
/*
* Utilities
*/
function onLocationChange(callback) {
let currentLocation = "";
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (currentLocation != document.location.href) {
currentLocation = document.location.href;
callback();
}
});
});
observer.observe(document.querySelector("body"), {
childList: true,
subtree: true
});
}
function migrateHighscores(highscores) {
if (!highscores) {
return highscores;
}
for (const [user, maps] of Object.entries(highscores)) {
for (const [map, modes] of Object.entries(maps)) {
for (const [mode, score] of Object.entries(modes)) {
if (typeof score !== 'number') {
// Already dealing with new format highscores. we can return ealier.
return highscores;
}
modes[mode] = [{
"points": score,
"gameId": undefined,
"time": undefined,
}]
}
}
}
return highscores;
}
function getCurrentHighscoreTab() {
return localStorage.getItem(LOCAL_STORAGE_HIGHSCORE_TAB_KEY)
|| getGameMode({
forbidMoving: false,
forbidRotating: false,
forbidZooming: false,
});
}
function setCurrentHighscoreTab(mode) {
return localStorage.setItem(LOCAL_STORAGE_HIGHSCORE_TAB_KEY, mode);
}
function getResults() {
let stored = localStorage.getItem(LOCAL_STORAGE_RESULTS_KEY);
if (stored) {
return migrateHighscores(JSON.parse(stored));
} else {
return {};
}
}
function saveResults(results) {
localStorage.setItem(LOCAL_STORAGE_RESULTS_KEY, JSON.stringify(results));
}
function getGameMode(config) {
return `${config.forbidMoving ? "NoMove" : "Move"}${config.forbidRotating ? "NoPan" : "Pan"}${config.forbidZooming ? "NoZoom" : "Zoom"}`;
}
function formatSeconds(duration) {
// Hours, minutes and seconds
var hrs = Math.floor(duration / 3600);
var mins = Math.floor((duration % 3600) / 60);
var secs = duration % 60;
var ret = "";
if (hrs > 0) {
ret += "" + hrs + " hr, ";
}
if (mins > 0) {
ret += "" + mins + " min, ";
}
ret += "" + secs + " sec";
return ret;
}
async function getUserId() {
if (USER_ID) {
return USER_ID;
}
let response = await fetch("https://www.geoguessr.com/api/v3/profiles/", { "credentials": "include" })
.then(res => res.json())
.then(res => {
USER_ID = res.user.id;
return USER_ID
});
return response;
}
/*
* Game summary page
*/
function alreadyHasScore() {
return !!document.getElementById(RESULTS_DIV_ID);
}
function displayBest(best) {
if (alreadyHasScore()) {
return;
}
const previousBestDiv = document.createElement("div");
previousBestDiv.id = RESULTS_DIV_ID;
document.getElementsByClassName("score-bar")[0].parentElement.appendChild(previousBestDiv);
previousBestDiv.innerHTML = `<span>Your best score is: <b>${Number(best.points).toLocaleString()}</b></span>`;
};
function processGame(game) {
if (game.map === "country-streak") {
return;
}
const mapId = game.map;
const mapName = game.mapName;
const player = game.player.id;
const points = parseInt(game.player.totalScore.amount);
const time = game.player.totalTime;
const results = getResults();
const gameMode = getGameMode(game);;
if (!(player in results)) {
results[player] = {};
}
if (!(mapId in results[player])) {
// Migrate from results based on map name to map id
if (mapName in results[player]) {
results[player][mapId] = results[player][mapName];
delete results[player][mapName];
} else {
results[player][mapId] = {};
}
}
if (!(gameMode in results[player][mapId])) {
results[player][mapId][gameMode] = [];
}
results[player][mapId][gameMode].push({
"points": points,
"gameId": game.token,
"time": time,
});
results[player][mapId][gameMode] = results[player][mapId][gameMode]
.sort((a, b) => b.points - a.points || a.time - b.time)
.reduce((uniques, game) => uniques.some(other => other.gameId === game.gameId) ? uniques : [...uniques, game], []);
results[player][mapId][gameMode].splice(3);
saveResults(results);
displayBest(results[player][mapId][gameMode][0]);
}
function checkGame() {
const challengeTag = window.location.href.substring(window.location.href.lastIndexOf('/') + 1);
fetch(`https://www.geoguessr.com/api/v3/games/${challengeTag}`)
.then(res => res.json())
.then(processGame)
.catch(err => { throw err });
};
function checkChallenge() {
const challengeTag = window.location.href.substring(window.location.href.lastIndexOf('/') + 1);
fetch(`https://www.geoguessr.com/api/v3/challenges/${challengeTag}/game`, { "credentials": "include" })
.then(res => res.json())
.then(processGame)
.catch(err => { throw err });
};
function check() {
if (alreadyHasScore()) {
return;
}
let matched = false;
const internalCheck = () => {
if (matched) return;
if (location.pathname.startsWith("/game/") && !!document.querySelector('.title')) {
matched = true;
checkGame();
} else if (location.pathname.startsWith("/challenge/") && !!document.querySelector('.title')) {
matched = true;
checkChallenge();
}
};
internalCheck();
setTimeout(internalCheck, 250);
setTimeout(internalCheck, 500);
setTimeout(internalCheck, 1000);
setTimeout(internalCheck, 2000);
};
/*
* Map page
*/
function alreadyHasHighscores() {
return !!document.getElementById(HIGHSCORES_DIV_ID);
}
function createTabButton(key, label, active) {
const button = document.createElement("button");
button.type = "button";
button.classList.add("button-bar__button");
if (key === active) {
button.classList.add("button-bar__button--active");
}
button.innerText = label;
button.onclick = function () {
setCurrentHighscoreTab(key);
addLocalHighscores(true);
}
return button;
}
function createTableRowString(position, score) {
let gameLink = "";
if (score && score.gameId) {
gameLink = `<a class="highscore__result-link" href="/results/${score.gameId}" target="_blank" rel="noopener noreferrer" title="View results"><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAzNDRhIiBkPSJNNSAxOWgtNHYtOGg0djh6bTYgMGgtNHYtMThoNHYxOHptNiAwaC00di0xMmg0djEyem02IDBoLTR2LTRoNHY0em0xIDJoLTI0djJoMjR2LTJ6IiAvPjwvc3ZnPg==" alt="View results"></a>`
}
return `
<tr class="table__row">
<td class="table__cell table__cell--no-wrap highscore__number">${position}.</td>
<td class="table__cell table__cell--collapse-left table__cell--span label-2"></td>
<td class="table__cell table__cell--align-right table__cell--collapse-right table__cell--no-wrap"><span class="highscore__score">${score && score.points ? Number(score.points).toLocaleString() + " points" : ""}</span></td>
<td class="table__cell table__cell--align-right table__cell--collapse-right table__cell--no-wrap"><span class="highscore__total-time">${score && score.time ? formatSeconds(score.time) : ""}</span></td>
<td class="table__cell table__cell--collapse-left">${gameLink}</td>
</tr>
`
}
function createResultsTableString(scores) {
return `
<table class="table table--spacing-small highscore">
<tbody>
${createTableRowString(1, scores[0])}
${createTableRowString(2, scores[1])}
${createTableRowString(3, scores[2])}
</tbody>
</table>
`
}
function addLocalHighscores(reload = false) {
if (alreadyHasHighscores()) {
if (reload) {
const oldTable = document.getElementById(HIGHSCORES_DIV_ID);
oldTable.parentNode.removeChild(oldTable);
} else {
return true;
}
}
const mapInfo = document.getElementsByClassName('map-block')[0];
if (!mapInfo) {
return false;
}
const highscores = mapInfo.nextElementSibling;
if (!highscores) {
return false;
}
const peopleWhoLikeThisMap = highscores.nextElementSibling;
if (!peopleWhoLikeThisMap) {
return false;
}
getUserId().then(userId => {
const currentTab = getCurrentHighscoreTab();
const mapId = window.location.href.substring(window.location.href.lastIndexOf('/') + 1);
const mapName = document.querySelector('.map-block .map-block__title').textContent;
let results = [];
const globalResults = getResults();
const userResults = globalResults[userId];
if (userResults) {
const mapResults = userResults[mapId] || userResults[mapName];
if (mapResults) {
results = mapResults[currentTab];
}
}
let scoresTableString;
if (!results || results.length == 0) {
scoresTableString = `<p class="center-content">No recorded results.</p>`;
} else {
scoresTableString = createResultsTableString(results);
}
const localHighscoresDiv = document.createElement("div");
peopleWhoLikeThisMap.parentNode.insertBefore(localHighscoresDiv, peopleWhoLikeThisMap);
localHighscoresDiv.id = HIGHSCORES_DIV_ID;
localHighscoresDiv.className = 'margin--top-large';
localHighscoresDiv.innerHTML = `
<div class="margin--top-large">
<section class="grid grid--gutter-size-large grid--num-columns-1">
<section class="grid__column">
<h1 class="title title--medium title--dark">Local Highscore</h1>
${scoresTableString}
<div class="center-content margin--top">
<div id="${HIGHSCORES_BUTTON_BAR_ID}" class="button-bar">
</div>
</div>
</section>
</section>
</div>
`;
const buttonBar = document.getElementById(HIGHSCORES_BUTTON_BAR_ID);
const movingButton = createTabButton(getGameMode({ forbidMoving: false, forbidRotating: false, forbidZooming: false }), "Moving", currentTab);
const noMoveButton = createTabButton(getGameMode({ forbidMoving: true, forbidRotating: false, forbidZooming: false }), "No move", currentTab);
const noMoveNoZoomButton = createTabButton(getGameMode({ forbidMoving: true, forbidRotating: false, forbidZooming: true }), "No move, No zoom", currentTab);
const noMoveNoZoomNoPanButton = createTabButton(getGameMode({ forbidMoving: true, forbidRotating: true, forbidZooming: true }), "No move, No pan, No zoom", currentTab);
buttonBar.appendChild(movingButton);
buttonBar.appendChild(noMoveButton);
buttonBar.appendChild(noMoveNoZoomButton);
buttonBar.appendChild(noMoveNoZoomNoPanButton);
});
return true;
}
function attemptToCreateHighscoresTable(attemptsLeft) {
if (!location.pathname.startsWith("/maps/")) {
return;
}
setTimeout(function () {
attemptsLeft -= 1;
if (!addLocalHighscores() && attemptsLeft > 0) {
attemptToCreateHighscoresTable(attemptsLeft);
}
}, 100);
}
document.addEventListener('click', check, false);
onLocationChange(function () { attemptToCreateHighscoresTable(20) });
})();