// ==UserScript==
// @name Country Streak Counter
// @version 1.3.4
// @description Adds a country streak counter to the GeoGuessr website
// @match https://www.geoguessr.com/*
// @author victheturtle#5159
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=geoguessr.com
// @namespace https://gf.qytechs.cn/users/967692-victheturtle
// ==/UserScript==
// Credits to subsymmetry for the original version of the Streak Counter
const ENABLED_ON_CHALLENGES = false; //Replace with true or false
const API_Key = 'ENTER_API_KEY_HERE'; //Replace ENTER_API_KEY_HERE with your API key (so keep the quote marks)
let AUTOMATIC = true; //Replace with false for a manual counter. Without an API key, the counter will still be manual
if (sessionStorage.getItem("Streak") == null) {
sessionStorage.setItem("Streak", 0);
};
if (sessionStorage.getItem("StreakBackup") == null) {
sessionStorage.setItem("StreakBackup", 0);
};
if (sessionStorage.getItem("Checked") == null) {
sessionStorage.setItem("Checked", 0);
};
let streak = parseInt(sessionStorage.getItem("Streak"), 10);
const ERROR_RESP = -1000000;
var CountryDict = {
AF: 'AF',
AX: 'FI', // Aland Islands
AL: 'AL',
DZ: 'DZ',
AS: 'US', // American Samoa
AD: 'AD',
AO: 'AO',
AI: 'GB', // Anguilla
AQ: 'AQ', // Antarctica
AG: 'AG',
AR: 'AR',
AM: 'AM',
AW: 'NL', // Aruba
AU: 'AU',
AT: 'AT',
AZ: 'AZ',
BS: 'BS',
BH: 'BH',
BD: 'BD',
BB: 'BB',
BY: 'BY',
BE: 'BE',
BZ: 'BZ',
BJ: 'BJ',
BM: 'GB', // Bermuda
BT: 'BT',
BO: 'BO',
BQ: 'NL', // Bonaire, Sint Eustatius, Saba
BA: 'BA',
BW: 'BW',
BV: 'NO', // Bouvet Island
BR: 'BR',
IO: 'GB', // British Indian Ocean Territory
BN: 'BN',
BG: 'BG',
BF: 'BF',
BI: 'BI',
KH: 'KH',
CM: 'CM',
CA: 'CA',
CV: 'CV',
KY: 'UK', // Cayman Islands
CF: 'CF',
TD: 'TD',
CL: 'CL',
CN: 'CN',
CX: 'AU', // Christmas Islands
CC: 'AU', // Cocos (Keeling) Islands
CO: 'CO',
KM: 'KM',
CG: 'CG',
CD: 'CD',
CK: 'NZ', // Cook Islands
CR: 'CR',
CI: 'CI',
HR: 'HR',
CU: 'CU',
CW: 'NL', // Curacao
CY: 'CY',
CZ: 'CZ',
DK: 'DK',
DJ: 'DJ',
DM: 'DM',
DO: 'DO',
EC: 'EC',
EG: 'EG',
SV: 'SV',
GQ: 'GQ',
ER: 'ER',
EE: 'EE',
ET: 'ET',
FK: 'GB', // Falkland Islands
FO: 'DK', // Faroe Islands
FJ: 'FJ',
FI: 'FI',
FR: 'FR',
GF: 'FR', // French Guiana
PF: 'FR', // French Polynesia
TF: 'FR', // French Southern Territories
GA: 'GA',
GM: 'GM',
GE: 'GE',
DE: 'DE',
GH: 'GH',
GI: 'UK', // Gibraltar
GR: 'GR',
GL: 'DK', // Greenland
GD: 'GD',
GP: 'FR', // Guadeloupe
GU: 'US', // Guam
GT: 'GT',
GG: 'GB', // Guernsey
GN: 'GN',
GW: 'GW',
GY: 'GY',
HT: 'HT',
HM: 'AU', // Heard Island and McDonald Islands
VA: 'VA',
HN: 'HN',
HK: 'CN', // Hong Kong
HU: 'HU',
IS: 'IS',
IN: 'IN',
ID: 'ID',
IR: 'IR',
IQ: 'IQ',
IE: 'IE',
IM: 'GB', // Isle of Man
IL: 'IL',
IT: 'IT',
JM: 'JM',
JP: 'JP',
JE: 'GB', // Jersey
JO: 'JO',
KZ: 'KZ',
KE: 'KE',
KI: 'KI',
KR: 'KR',
KW: 'KW',
KG: 'KG',
LA: 'LA',
LV: 'LV',
LB: 'LB',
LS: 'LS',
LR: 'LR',
LY: 'LY',
LI: 'LI',
LT: 'LT',
LU: 'LU',
MO: 'CN', // Macao
MK: 'MK',
MG: 'MG',
MW: 'MW',
MY: 'MY',
MV: 'MV',
ML: 'ML',
MT: 'MT',
MH: 'MH',
MQ: 'FR', // Martinique
MR: 'MR',
MU: 'MU',
YT: 'FR', // Mayotte
MX: 'MX',
FM: 'FM',
MD: 'MD',
MC: 'MC',
MN: 'MN',
ME: 'ME',
MS: 'GB', // Montserrat
MA: 'MA',
MZ: 'MZ',
MM: 'MM',
NA: 'NA',
NR: 'NR',
NP: 'NP',
NL: 'NL',
AN: 'NL', // Netherlands Antilles
NC: 'FR', // New Caledonia
NZ: 'NZ',
NI: 'NI',
NE: 'NE',
NG: 'NG',
NU: 'NZ', // Niue
NF: 'AU', // Norfolk Island
MP: 'US', // Northern Mariana Islands
NO: 'NO',
OM: 'OM',
PK: 'PK',
PW: 'PW',
PS: 'IL', // Palestine
PA: 'PA',
PG: 'PG',
PY: 'PY',
PE: 'PE',
PH: 'PH',
PN: 'GB', // Pitcairn
PL: 'PL',
PT: 'PT',
PR: 'US', // Puerto Rico
QA: 'QA',
RE: 'FR', // Reunion
RO: 'RO',
RU: 'RU',
RW: 'RW',
BL: 'FR', // Saint Barthelemy
SH: 'GB', // Saint Helena
KN: 'KN',
LC: 'LC',
MF: 'FR', // Saint Martin
PM: 'FR', // Saint Pierre and Miquelon
VC: 'VC',
WS: 'WS',
SM: 'SM',
ST: 'ST',
SA: 'SA',
SN: 'SN',
RS: 'RS',
SC: 'SC',
SL: 'SL',
SG: 'SG',
SX: 'NL', // Sint Maarten
SK: 'SK',
SI: 'SI',
SB: 'SB',
SO: 'SO',
ZA: 'ZA',
GS: 'GB', // South Georgia and the South Sandwich Islands
ES: 'ES',
LK: 'LK',
SD: 'SD',
SR: 'SR',
SJ: 'NO', // Svalbard and Jan Mayen
SZ: 'SZ',
SE: 'SE',
CH: 'CH',
SY: 'SY',
TW: 'TW', // Taiwan
TJ: 'TJ',
TZ: 'TZ',
TH: 'TH',
TL: 'TL',
TG: 'TG',
TK: 'NZ', // Tokelau
TO: 'TO',
TT: 'TT',
TN: 'TN',
TR: 'TR',
TM: 'TM',
TC: 'GB', // Turcs and Caicos Islands
TV: 'TV',
UG: 'UG',
UA: 'UA',
AE: 'AE',
GB: 'GB',
US: 'US',
UM: 'US', // US Minor Outlying Islands
UY: 'UY',
UZ: 'UZ',
VU: 'VU',
VE: 'VE',
VN: 'VN',
VG: 'GB', // British Virgin Islands
VI: 'US', // US Virgin Islands
WF: 'FR', // Wallis and Futuna
EH: 'MA', // Western Sahara
YE: 'YE',
ZM: 'ZM',
ZW: 'ZW'
};
if (AUTOMATIC && (API_Key.length <= 24 || API_Key.match("^[a-fA-F0-9_]*$") == null)) {
AUTOMATIC = false;
};
function checkGameMode() {
return (location.pathname.startsWith("/game/") || (ENABLED_ON_CHALLENGES && location.pathname.startsWith("/challenge/")));
};
let _cndic = {};
function cn(classNameStart) { // cn("status_section__") -> "status_section__8uP8o"
let memorized = _cndic[classNameStart];
if (memorized != null) return memorized;
let selected = document.querySelector(`div[class*="${classNameStart}"]`);
if (selected == null) return classNameStart;
for (let className of selected.classList) {
if (className.startsWith(classNameStart)) {
_cndic[classNameStart] = className;
return className;
}
}
}
function geoguessrStyle(number) {
return `<div class="${cn("guess-description-distance_distanceLabel__")}">
<div class="${cn("slanted-wrapper_root__")} ${cn("slanted-wrapper_variantWhiteTransparent__")} ${cn("slanted-wrapper_roundnessSmall__")}">
<div class="${cn("slanted-wrapper_start__")} ${cn("slanted-wrapper_right__")}"></div>
<div class="${cn("guess-description-distance_distanceValue__")}">${number}</div>
<div class="${cn("slanted-wrapper_end__")} ${cn("slanted-wrapper_right__")}"></div>
</div>
</div>`;
};
function addStreakStatusBar() {
let status_length = document.getElementsByClassName(cn("status_section__")).length;
if (document.getElementById("country-streak") == null && status_length >= 3) {
let newDiv = document.createElement("div");
newDiv.className = cn('status_section__');
newDiv.innerHTML = `<div class="${cn("status_label__")}">Streak</div>
<div id="country-streak" class="${cn("status_value__")}">${streak}</div>`;
let statusBar = document.getElementsByClassName(cn("status_inner__"))[0];
statusBar.insertBefore(newDiv, statusBar.children[3]);
};
};
function addStreakRoundResult() {
if (document.getElementById("country-streak2") == null && !!document.querySelector('div[data-qa="guess-description"]')
&& !document.querySelector('div[class*="standard-final-result_section__"]')) {
let newDiv = document.createElement("div");
newDiv.innerHTML = `<div id="country-streak2" style="text-align:center;margin-top:10px;"><h2><i>Country Streak: ${streak}</i></h2></div>`;
document.querySelector('div[data-qa="guess-description"]').appendChild(newDiv);
};
};
function addStreakGameSummary() {
if (document.getElementById("country-streak2") == null && !!document.querySelector('div[class*="standard-final-result_section__"]')) {
let newDiv = document.createElement("div");
newDiv.innerHTML = `<div id="country-streak2" style="text-align:center;margin-top:10px;"><h2><i>Country Streak: ${streak}</i></h2></div>`;
let progressSection = document.querySelector('div[class*="standard-final-result_progressSection__"]');
progressSection.parentNode.insertBefore(newDiv, progressSection.parentNode.children[2]);
progressSection.style.marginTop = "10px";
progressSection.style.marginBottom = "10px";
};
};
function updateStreak(newStreak) {
geoguessrStyle() // call cn() for the geoguessrStyle styles to memorize them while they are there
if (newStreak === ERROR_RESP) {
if (document.getElementById("country-streak2") != null && (!!document.querySelector('div[data-qa="guess-description"]'))) {
document.getElementById("country-streak2").innerHTML =
`<div><i>Country codes could not be fetched. If your API key is new, it should activate soon.</i></div>
<div><i>Check for typos in the API key. You might also see this message if bigdatacloud is down</i></div>
<div><i>or in the unlikely event that you have exceeded you quota limit of 50,000 requests.</i></div>
<div><i>In the meantime, you can press 1 to count the country as correct, or press 0 otherwise.</i></div>`;
}
return;
}
sessionStorage.setItem("Streak", newStreak);
if (!(streak > 0 && newStreak == 0)) {
sessionStorage.setItem("StreakBackup", newStreak);
};
if (document.getElementById("country-streak") != null) {
document.getElementById("country-streak").innerHTML = newStreak;
};
if (document.getElementById("country-streak2") != null) {
document.getElementById("country-streak2").innerHTML = `<h2><i>Country Streak: ${newStreak}</i></h2>`;
if (newStreak == 0) {
if (streak >= 2) {
document.getElementById("country-streak2").innerHTML = `<h2><i>Country Streak: 0</i></h2>
Your streak ended after correctly guessing ${geoguessrStyle(streak)} countries in a row.`;
} else if (streak == 1) {
document.getElementById("country-streak2").innerHTML = `<h2><i>Country Streak: 0</i></h2>
Your streak ended after correctly guessing ${geoguessrStyle(1)} country.`;
};
};
};
streak = newStreak;
};
async function getUserAsync(coords) {
if (coords[0] <= -85.05) {
return 'AQ';
};
const api = "https://api.bigdatacloud.net/data/reverse-geocode?latitude="+coords[0]+"&longitude="+coords[1]+"&localityLanguage=en&key="+API_Key
const response = await fetch(api)
.then(res => (res.status !== 200) ? ERROR_RESP : res.json())
.then(out => (out === ERROR_RESP) ? ERROR_RESP : CountryDict[out.countryCode]);
return response;
};
let lastGuess = [0,0];
function check() {
const gameTag = window.location.href.substring(window.location.href.lastIndexOf('/') + 1)
let apiUrl = ""
if (location.pathname.startsWith("/game/")) {
apiUrl = "https://www.geoguessr.com/api/v3/games/"+gameTag;
} else if (location.pathname.startsWith("/challenge/")) {
apiUrl = "https://www.geoguessr.com/api/v3/challenges/"+gameTag+"/game";
};
fetch(apiUrl)
.then(res => res.json())
.then((out) => {
const guessCounter = out.player.guesses.length;
const guess = [out.player.guesses[guessCounter-1].lat,out.player.guesses[guessCounter-1].lng];
if (guess[0] == lastGuess[0] && guess[1] == lastGuess[1]) {
return;
};
lastGuess = guess;
const round = [out.rounds[guessCounter-1].lat,out.rounds[guessCounter-1].lng];
getUserAsync(guess)
.then(gue => {
getUserAsync(round)
.then(loc => {
if (loc == ERROR_RESP || gue == ERROR_RESP) {
updateStreak(ERROR_RESP);
} else if (loc == gue) {
updateStreak(streak + 1);
} else {
updateStreak(0);
};
});
});
}).catch(err => { throw err });
};
let lastDoCheckCall = 0
function doCheck() {
if (lastDoCheckCall >= (Date.now() - 200)) return;
lastDoCheckCall = Date.now();
if (!document.querySelector('div[class*="result-layout_root__"]')) {
sessionStorage.setItem("Checked", 0);
} else if (sessionStorage.getItem("Checked") == 0) {
check();
sessionStorage.setItem("Checked", 1);
}
};
let observer = new MutationObserver((mutations) => {
if (!checkGameMode()) return;
if (AUTOMATIC) doCheck();
addStreakStatusBar();
addStreakRoundResult();
addStreakGameSummary();
});
observer.observe(document.body, { subtree: true, childList: true });
document.addEventListener('keypress', (e) => {
let streakBackup = parseInt(sessionStorage.getItem("StreakBackup"), 10);
switch (e.key) {
case '1':
updateStreak(streak + 1);
break;
case '2':
updateStreak(streak - 1);
break;
case '8':
updateStreak(streakBackup + 1);
break;
case '0':
updateStreak(0);
sessionStorage.setItem("StreakBackup", 0);
};
});