Guess retriever

Retrieves guesses under a certain score threshold from your duels

  1. // ==UserScript==
  2. // @name Guess retriever
  3. // @version 0.2
  4. // @description Retrieves guesses under a certain score threshold from your duels
  5. // @author You
  6. // @license MIT
  7. // @match https://www.geoguessr.com/*
  8. // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
  9. // @grant unsafeWindow
  10. // @require https://unpkg.com/@popperjs/core@2.11.5/dist/umd/popper.min.js
  11. // @namespace https://greasyfork.org/users/1011193
  12. // @grant GM_addStyle
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // @require https://greasyfork.org/scripts/460322-geoguessr-styles-scan/code/Geoguessr%20Styles%20Scan.js?version=1151668
  16. // ==/UserScript==
  17.  
  18.  
  19. let hide = false;
  20. let styles = GM_getValue("guessFinderStyles");
  21. if (!styles) {
  22. hide = true;
  23. styles = {};
  24. }
  25. let css =`
  26. #guessFinderPopupWrapper, #guessFinderSearchWrapper, #guessFinderSlantedRoot, #guessFinderSlantedStart, #guessFinderInputWrapper, #guessFinderPopup, #guessFinderToggle, #guessFinderTogglePicture, #runButton, .buttonContainer {
  27. box-sizing: border-box;
  28. }
  29.  
  30. #guessFinderPopup {
  31. background: rgba(26, 26, 46, 0.9);
  32. padding: 15px;
  33. border-radius: 10px;
  34. max-height: 80vh;
  35. overflow-y: auto;
  36. width: 28em;
  37. }
  38.  
  39. #guessFinderPopup div {
  40. display: flex;
  41. justify-content: space-between;
  42. align-items: center;
  43. margin-top: 10px;
  44. }
  45.  
  46. #guessFinderPopup input {
  47. background: rgba(255,255,255,0.1);
  48. color: white;
  49. border: none;
  50. border-radius: 5px;
  51. }
  52.  
  53. .buttonContainer {
  54. display: flex;
  55. justify-content: center;
  56. align-items: center;
  57. margin-top: 20px;
  58. }
  59.  
  60. #runButton {
  61. background: rgba(255, 255, 255, 0.1);
  62. color: white;
  63. border: none;
  64. border-radius: 5px;
  65. padding: 10px 20px;
  66. }
  67.  
  68. #runButton:hover {
  69. background: rgba(255, 255, 255, 0.25);
  70. cursor: pointer;
  71. }
  72.  
  73. #guessFinderToggle {
  74. width: 59.19px;
  75. }
  76.  
  77. #guessFinderTogglePicture {
  78. justify-content: center;
  79. }
  80.  
  81. #guessFinderTogglePicture img {
  82. width: 20px;
  83. filter: brightness(0) invert(1);
  84. opacity: 60%;
  85. }
  86.  
  87. .inputContainer {
  88. display: flex;
  89. justify-content: space-between;
  90. align-items: center;
  91. margin-top: 10px;
  92. }
  93.  
  94. .inputLabel {
  95. margin: 0;
  96. padding-right: 6px;
  97. color: white; /* Adjust if necessary */
  98. }
  99.  
  100. #dropdownInput {
  101. background: rgba(255,255,255,0.1);
  102. color: white;
  103. border: none;
  104. border-radius: 5px;
  105. padding: 5px; /* Adjust padding as needed */
  106. }
  107. `
  108.  
  109. GM_addStyle(css);
  110.  
  111. const guiHTMLHeader = `
  112. <div id="guessFinderPopupWrapper">
  113. <div id="guessFinderSearchWrapper">
  114. <div id="guessFinderSlantedRoot">
  115. <div id="guessFinderSlantedStart"></div>
  116. <div id="guessFinderInputWrapper">
  117. <div id="guessFinderPopup" style="background: rgba(26, 26, 46, 0.9); padding: 15px; border-radius: 10px; max-height: 80vh; overflow-y: auto; width: 28em">
  118. <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 10px;">
  119. <span id="mmaAPIkey" style="margin: 0; padding-right: 6px;"> Map Making App API</span>
  120. <input id="mmaAPIkeyInput" name="mmaAPIkey" style="background: rgba(255,255,255,0.1); color: white; border: none; border-radius: 5px">
  121. </div>
  122. <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 10px;">
  123. <span id="scoreThresholdLabel" style="margin: 0; padding-right: 6px;">Score Threshold</span>
  124. <input type="number" id="scoreThresholdInput" name="scoreThreshold" style="background: rgba(255,255,255,0.1); color: white; border: none; border-radius: 5px">
  125. </div>
  126.  
  127. <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 10px;">
  128. <span id="monthsOfDuelsLabel" style="margin: 0; padding-right: 6px;">Months of Duels</span>
  129. <input type="number" id="monthsOfDuelsInput" name="monthsOfDuels" style="background: rgba(255,255,255,0.1); color: white; border: none; border-radius: 5px">
  130. </div>
  131. <div class="inputContainer">
  132. <span class="inputLabel">Map Making App Map:</span>
  133. <select id="dropdownInput" name="dropdownInput">
  134. <option value="option1">Option 1</option>
  135. <option value="option2">Option 2</option>
  136. <option value="option3">Option 3</option>
  137. <option value="option4">Option 4</option>
  138. </select>
  139. </div>
  140. <div style="display: flex; justify-content: center; align-items: center; margin-top: 20px;">
  141. <button id="runButton" >Run</button>
  142. </div>
  143. <div style="display: flex; justify-content: center; align-items: center; margin-top: 20px;">
  144. <p id="guessFinderStatus"></p>
  145. </div>
  146. </div>
  147. <button style="width: 59.19px" id="guessFinderToggle"><picture id="guessFinderTogglePicture" style="justify-content: center"><img src="https://www.svgrepo.com/show/532540/location-pin-alt-1.svg" style="width: 20px; filter: brightness(0) invert(1); opacity: 60%;"></picture></button>
  148. </div>
  149. <div id="guessFinderSlantedEnd"></div>
  150. </div>
  151. </div>
  152. </div>
  153.  
  154. `
  155.  
  156. const showPopup = (showButton, popup) => {
  157. popup.style.display = 'block';
  158. Popper.createPopper(showButton, popup, {
  159. placement: 'bottom',
  160. modifiers: [
  161. {
  162. name: 'offset',
  163. options: {
  164. offset: [0, 10],
  165. },
  166. },
  167. ],
  168. });
  169. }
  170.  
  171. const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
  172.  
  173. const iterativeSetTimeout = async (func, initDelay, cond) => {
  174. while (!cond()) {
  175. await delay(initDelay);
  176. await func();
  177. initDelay *= 2;
  178. }
  179. };
  180.  
  181.  
  182. const stylesUsed = [
  183. "header_item__",
  184. "quick-search_wrapper__",
  185. "slanted-wrapper_root__",
  186. "slanted-wrapper_variantGrayTransparent__",
  187. "slanted-wrapper_start__",
  188. "quick-search_searchInputWrapper__",
  189. "slanted-wrapper_end__",
  190. "slanted-wrapper_right__",
  191. "quick-search_searchInputButton__",
  192. "quick-search_iconSection__",
  193. ];
  194.  
  195. const uploadDownloadStyles = async () => {
  196. stylesUsed.forEach(style => {
  197. });
  198. await iterativeSetTimeout(scanStyles, 0.1, () => checkAllStylesFound(stylesUsed) !== undefined);
  199. if (hide) {
  200. document.querySelector("#guessFinderPopupWrapper").hidden = "";
  201. }
  202. stylesUsed.forEach(style => {
  203. styles[style] = cn(style);
  204. });
  205. setStyles();
  206. GM_setValue("guessFinderStyles", styles);
  207. }
  208.  
  209. const getStyle = style => {
  210. return styles[style];
  211. }
  212.  
  213. const setStyles = () => {
  214. try {
  215. document.querySelector("#guessFinderPopupWrapper").className = getStyle("header_item__");
  216. document.querySelector("#guessFinderSearchWrapper").className = getStyle("quick-search_wrapper__");
  217. document.querySelector("#guessFinderSlantedRoot").className = getStyle("slanted-wrapper_root__") + " " + getStyle("slanted-wrapper_variantGrayTransparent__");
  218. document.querySelector("#guessFinderSlantedStart").className = getStyle("slanted-wrapper_start__")+ " " + getStyle("slanted-wrapper_right__");
  219. document.querySelector("#guessFinderInputWrapper").className = getStyle("quick-search_searchInputWrapper__");
  220. document.querySelector("#guessFinderSlantedEnd").className = getStyle("slanted-wrapper_end__")+ " " + getStyle("slanted-wrapper_right__");
  221. document.querySelector("#guessFinderToggle").className = getStyle("quick-search_searchInputButton__");
  222. document.querySelector("#guessFinderLabel1").className = getStyle("label_sizeXSmall__") + getStyle("label_variantWhite__");
  223. document.querySelector("#guessFinderLabel2").className = getStyle("label_sizeXSmall__") + getStyle("label_variantWhite__");
  224. document.querySelector("#guessFinderTogglePicture").className = getStyle("quick-search_iconSection__");
  225. document.querySelectorAll(".deleteButton").forEach(el => el.className = el.className + " " + getStyle("quick-search_searchInputButton__"));
  226. } catch (err) {
  227. console.error(err);
  228. }
  229. }
  230.  
  231.  
  232. const insertHeaderGui = async (header, gui) => {
  233.  
  234. header.insertAdjacentHTML('afterbegin', gui);
  235.  
  236. // Resolve class names
  237. if (hide) {
  238. document.querySelector("#guessFinderPopupWrapper").hidden = "true"
  239. }
  240.  
  241. scanStyles().then(() => uploadDownloadStyles());
  242. setStyles();
  243.  
  244. const showButton = document.querySelector('#guessFinderToggle');
  245. const popup = document.querySelector('#guessFinderPopup');
  246. popup.style.display = 'none';
  247.  
  248. document.addEventListener('click', (e) => {
  249. const target = e.target;
  250. if (target == popup || popup.contains(target) || !document.contains(target)) return;
  251. if (target.matches('#guessFinderToggle, #guessFinderToggle *')) {
  252. e.preventDefault();
  253. showPopup(showButton, popup);
  254. } else {
  255. popup.style.display = 'none';
  256. }
  257. });
  258. }
  259.  
  260. let MAP_MAKING_API_KEY = localStorage.getItem("guessFinderMMAApiKey");
  261.  
  262. async function mmaFetch(url, options = {}) {
  263. const response = await fetch(new URL(url, 'https://map-making.app'), {
  264. ...options,
  265. headers: {
  266. accept: 'application/json',
  267. authorization: `API ${MAP_MAKING_API_KEY.trim()}`,
  268. ...options.headers
  269. }
  270. });
  271. if (!response.ok) {
  272. let message = 'Unknown error';
  273. try {
  274. const res = await response.json();
  275. if (res.message) {
  276. message = res.message;
  277. }
  278. } catch {}
  279. alert(`An error occurred while trying to connect to Map Making App. ${message}`);
  280. throw Object.assign(new Error(message), { response });
  281. }
  282. return response;
  283. }
  284.  
  285. async function getMaps(suppress = false) {
  286. if (MAP_MAKING_API_KEY === null) {
  287. if (!suppress) alert("Please input an API key for Map Making App");
  288. return;
  289. }
  290. const response = await mmaFetch(`/api/maps`);
  291. const maps = await response.json();
  292. return maps;
  293. }
  294.  
  295. async function importLocations(mapId, locations) {
  296. const response = await mmaFetch(`/api/maps/${mapId}/locations`, {
  297. method: 'post',
  298. headers: {
  299. 'content-type': 'application/json'
  300. },
  301. body: JSON.stringify({
  302. edits: [{
  303. action: { type: 4 },
  304. create: locations,
  305. remove: []
  306. }]
  307. })
  308. });
  309. await response.json();
  310. }
  311.  
  312. const API_Key = 'bdc_e4a84278a5684f4786dd8277e4948ac4';
  313.  
  314. const ERROR_RESP = -1000000;
  315.  
  316. let count = 0;
  317.  
  318. async function getCountryCode(coords) {
  319. if (coords[0] <= -85.05) return 'AQ';
  320. count++
  321. if (API_Key.toLowerCase().match("^(bdc_)?[a-f0-9]{32}$") != null) {
  322. const api = "https://api.bigdatacloud.net/data/reverse-geocode?latitude="+coords.lat+"&longitude="+coords.lng+"&localityLanguage=en&key="+API_Key;
  323. return await fetch(api)
  324. .then(res => (res.status !== 200) ? ERROR_RESP : res.json())
  325. .then(out => (out === ERROR_RESP) ? ERROR_RESP : CountryDict[out.countryCode]);
  326. } else {
  327. const api = `https://nominatim.openstreetmap.org/reverse.php?lat=${coords.lat}&lon=${coords.lng}&zoom=21&format=jsonv2&accept-language=en`;
  328. return await fetch(api)
  329. .then(res => (res.status !== 200) ? ERROR_RESP : res.json())
  330. .then(out => (out === ERROR_RESP) ? ERROR_RESP : CountryDict[out?.address?.country_code?.toUpperCase()]);
  331. }
  332. };
  333.  
  334. const fetchGeoGuessrData = async (id, cutoffDate) => {
  335. let after = null;
  336. let paginationToken = "", fetchedGames = [];
  337. let page = 0;
  338. let duelsFound = 0;
  339. while (true) {
  340. page++
  341. let url = "https://www.geoguessr.com/api/v4/feed/private";
  342. if (paginationToken !== "") {
  343. url += "?paginationToken=" + paginationToken;
  344. }
  345. let response = await fetch(url),
  346. n = await response.text(),
  347. jsonData = JSON.parse(n);
  348. if (jsonData.entries.length === 0) {
  349. console.log("All data fetched.");
  350. break;
  351. }
  352. const filteredEntries = jsonData.entries.filter(entry => {
  353. const entryTime = new Date(entry.time);
  354. const cutoffTime = new Date(cutoffDate);
  355. return entryTime >= cutoffTime;
  356. });
  357. if (filteredEntries.length === 0) {
  358. console.log("All data fetched.");
  359. break;
  360. }
  361.  
  362. // Extract game IDs from filtered entries
  363. for (const entry of filteredEntries) {
  364. // Parse the payload as JSON
  365. const payloadData = JSON.parse(entry.payload);
  366. if (Array.isArray(payloadData)) {
  367. // If payloadData is an array, process each item
  368. for (const payloadItem of payloadData) {
  369. if (payloadItem.payload && payloadItem.payload.gameId && payloadItem.payload.gameMode === "Duels") {
  370. fetchedGames.push(payloadItem.payload.gameId);
  371. duelsFound++;
  372. }
  373. }
  374. } else {
  375. // If payloadData is an object, check and process directly
  376. if (payloadData.gameId && payloadData.gameMode === "Duels") {
  377. fetchedGames.push(payloadData.gameId);
  378. }
  379. }
  380. }
  381.  
  382. statusUpdater.updateStatus("fetchingDuels", duelsFound, new Date(filteredEntries[filteredEntries.length-1].time).toISOString());
  383. paginationToken = jsonData.paginationToken;
  384. await new Promise(resolve => {
  385. setTimeout(() => {
  386. resolve();
  387. }, 500);
  388. });
  389. }
  390. statusUpdater.updateStatus("filteringDuplicates");
  391. fetchedGames = fetchedGames.filter((game, index, array) => array.indexOf(game) === index); // removes duplicates
  392. // return fetchedGames.includes(after) ? fetchedGames.slice(0, fetchedGames.indexOf(after)) : fetchedGames;
  393. return fetchedGames;
  394. };
  395.  
  396. const findIncorrectGuesses = async (gameData, playerId, status, score = null) => {
  397. let incorrectRounds = [];
  398.  
  399. const rounds = gameData.rounds;
  400.  
  401. // Finding the team and player by playerId
  402. let foundPlayer = null;
  403. let t = null;
  404. gameData.teams.forEach(team => {
  405. team.players.forEach(player => {
  406. if (player.playerId == playerId) {
  407. foundPlayer = player;
  408. t = team;
  409. }
  410. });
  411. });
  412.  
  413. if (!foundPlayer) {
  414. return []; // Player not found, returning empty array
  415. }
  416.  
  417. for (const round of t.roundResults) {
  418. let guessedCountryCode = null, actualCountryCode = null
  419. const roundLocation = rounds.find(r => r.roundNumber === round.roundNumber).panorama;
  420. status.guesses++;
  421. console.log(round);
  422. if (score === null) {
  423. return;
  424. guessedCountryCode = await getCountryCode({ lat: round.bestGuess.lat, lng: round.bestGuess.lng });
  425. actualCountryCode = await getCountryCode({ lat: roundLocation.lat, lng: roundLocation.lng });
  426. if (guessedCountryCode !== actualCountryCode) {
  427. status.matching++;
  428. incorrectRounds.push({
  429. roundNumber: round.roundNumber,
  430. score: round.score,
  431. guessedCountryCode,
  432. actualCountryCode,
  433. panorama: roundLocation
  434. });
  435. }
  436. } else if (round.score <= score) {
  437. status.matching++;
  438. incorrectRounds.push({
  439. roundNumber: round.roundNumber,
  440. score: round.score,
  441. guessedCountryCode,
  442. actualCountryCode,
  443. panorama: roundLocation
  444. });
  445. }
  446. statusUpdater.updateStatus('findingGuesses', status.guesses, score === null ? 'wrongCountry' : "belowThreshold", status.matching);
  447. }
  448. return incorrectRounds;
  449. };
  450.  
  451. const decodePanoId = (hexString) => {
  452. let decodedString = "";
  453. for (let i = 0; i < hexString.length; i += 2) {
  454. const hexPair = hexString.substr(i, 2);
  455. const char = String.fromCharCode(parseInt(hexPair, 16));
  456. decodedString += char;
  457. }
  458. return decodedString;
  459. };
  460.  
  461. function extractDate(timestamp) {
  462. var match = timestamp.match(/^(\d{4}-\d{2})/);
  463. return match ? match[1] : null;
  464. }
  465.  
  466. const formatScore = score => {
  467. const base = Math.floor(score / 1000);
  468. const decimal = Math.floor((score % 1000) / 100) * 0.1;
  469. const formattedScore = base + decimal;
  470.  
  471. return formattedScore.toFixed(1) + 'k';
  472. };
  473.  
  474. const subtractMonths = (months) => {
  475. if (months < 0) {
  476. throw new Error('Number of months must be positive');
  477. }
  478.  
  479. const date = new Date();
  480. date.setMonth(date.getMonth() - months);
  481.  
  482. return date.toISOString().split('T')[0]; // Returns the date in 'YYYY-MM-DD' format
  483. };
  484.  
  485. function StatusUpdater(elementId) {
  486. this.element = document.getElementById(elementId);
  487. this.states = {
  488. fetchingDuels: (duelsCount, currentDate) =>
  489. `Fetching duel ids. <br/> Duels found: ${duelsCount} <br/> Checked through date: ${currentDate}`,
  490. filteringDuplicates: () => `Filtering out duplicate ids`,
  491. findingGuesses: (guessesCount, additionalState, additionalCount) => {
  492. let additionalText = '';
  493. if (additionalState === 'wrongCountry') {
  494. additionalText = ` Finding wrong country guesses. Guesses found: ${additionalCount}`;
  495. } else if (additionalState === 'belowThreshold') {
  496. additionalText = ` Finding guesses below score threshold. Guesses found: ${additionalCount}`;
  497. }
  498. return `Finding guesses. <br/> Guesses checked: ${guessesCount} <br/> ${additionalText}`;
  499. },
  500. pushingLocations: (mapName, locationsCount) =>
  501. `Pushing locations to ${mapName} <br/> locations pushed: ${locationsCount}`,
  502. done: () => `Done!`
  503. };
  504. }
  505.  
  506. StatusUpdater.prototype.updateStatus = function(state, ...args) {
  507. if (this.states[state]) {
  508. this.element.innerHTML = this.states[state](...args);
  509. } else {
  510. console.error("Invalid state");
  511. }
  512. };
  513.  
  514. let statusUpdater;
  515.  
  516. let userId = localStorage.getItem("guessFinderUserId")
  517. if (userId == null) {
  518. fetch('https://geoguessr.com/api/v3/profiles', {method: "GET", "credentials": "include"})
  519. .then(response => response.json())
  520. .then(data => {
  521. if(data && data.user.id) {
  522. userId = data.user.id;
  523. localStorage.setItem("guessFinderUserId", userId);
  524. } else {
  525. console.log('ID not found in the response');
  526. }
  527. })
  528. .catch(error => console.error('Error:', error));
  529. }
  530.  
  531. const run = async () => {
  532. MAP_MAKING_API_KEY = document.getElementById('mmaAPIkeyInput').value
  533. localStorage.setItem('guessFinderMMAApiKey', MAP_MAKING_API_KEY);
  534. const maps = await getMaps();
  535. const mapId = document.getElementById('dropdownInput').value;
  536. if (mapId == "") return;
  537. const months = document.getElementById("monthsOfDuelsInput").value;
  538. if (months <= 0) return;
  539. const scoreThreshold = document.getElementById("scoreThresholdInput").value;
  540. if (scoreThreshold == "" || scoreThreshold < 0) return;
  541. const map = maps.find(map => map.id == mapId);
  542. const duelIds = await fetchGeoGuessrData(userId, subtractMonths(months));
  543. let locsPushed = 0;
  544. let status = {matching: 0, guesses: 0}
  545. for (const id of duelIds) {
  546. let api_url = `https://game-server.geoguessr.com/api/duels/${id}`;
  547. let res = await fetch(api_url, {method: "GET", "credentials": "include"})
  548. let json = await res.json();
  549. const incorrectGuesses = await findIncorrectGuesses(json, userId, status, scoreThreshold);
  550. if (incorrectGuesses.length) {
  551. // console.log(`https://www.geoguessr.com/duels/${res}/summary`);
  552. // console.log(incorrectGuesses);
  553. for (const guess of incorrectGuesses) {
  554. let loc = guess.panorama;
  555. if (loc.panoId) loc.panoId = decodePanoId(loc.panoId);
  556. let tags = [];
  557. if (guess.guessedCountryCode) tags.push(`guessed ${guess.guessedCountryCode}`)
  558. if (guess.actualCountryCode) tags.push(`actual ${guess.actualCountryCode}`)
  559. tags.push(`date: ${extractDate(json.rounds[0].startTime)}`)
  560. tags.push(`score: ${formatScore(guess.score)}`)
  561. await importLocations(map.id, [{
  562. id: -1,
  563. location: loc,
  564. panoId: loc.panoId ?? null,
  565. heading: loc.heading ?? 0,
  566. pitch: loc.pitch ?? 0,
  567. zoom: loc.zoom === 0 ? null : loc.zoom,
  568. tags,
  569. flags: loc.panoId ? 1 : 0
  570. }]);
  571. locsPushed++;
  572. }
  573. }
  574. }
  575. statusUpdater.updateStatus("done");
  576. }
  577.  
  578. const populateMaps = maps => {
  579. const dropdown = document.querySelector("#dropdownInput");
  580. maps.forEach( item => {
  581. let option = document.createElement('option');
  582. option.value = item.id;
  583. option.textContent = item.name;
  584. dropdown.appendChild(option);
  585. });
  586. };
  587.  
  588. const addPopup = async (refresh=false) => {
  589. if (refresh || (document.querySelector('[class^=header_header__]') && document.querySelector('#guessFinderPopupWrapper') === null)) {
  590. if (!refresh) {
  591. insertHeaderGui(document.querySelector('[class^=header_context__]'), guiHTMLHeader)
  592. const dropdown = document.querySelector("#dropdownInput");
  593. const maps = await getMaps(true);
  594.  
  595. // Clear existing options
  596. dropdown.innerHTML = '';
  597. // Append new options
  598. let defaultOption = document.createElement('option');
  599. defaultOption.value = '';
  600. defaultOption.textContent = ''; // Empty text for default option
  601. dropdown.appendChild(defaultOption);
  602.  
  603. if (maps) {
  604. populateMaps(maps);
  605. }
  606.  
  607. let apiKey = document.getElementById("mmaAPIkeyInput");
  608. apiKey.value = MAP_MAKING_API_KEY;
  609. // console.log(MAP_MAKING_API_KEY);
  610.  
  611. apiKey.addEventListener("input", async () => {
  612. MAP_MAKING_API_KEY = apiKey.value
  613. const maps = await getMaps();
  614. populateMaps(maps);
  615. });
  616.  
  617. const runButton = document.getElementById('runButton');
  618. if (runButton) {
  619. runButton.addEventListener('click', run);
  620. }
  621. statusUpdater = new StatusUpdater("guessFinderStatus");
  622. }
  623. }
  624. }
  625.  
  626. const updateImage = (refresh=false) => {
  627. // Don't do anything while the page is loading
  628. if (document.querySelector("[class^=page-loading_loading__]")) return;
  629. addPopup();
  630. }
  631.  
  632.  
  633. new MutationObserver(async (mutations) => {
  634. updateImage()
  635. }).observe(document.body, { subtree: true, childList: true });
  636.  
  637. const CountryDict = {
  638. AF: 'AF',
  639. AX: 'FI', // Aland Islands
  640. AL: 'AL',
  641. DZ: 'DZ',
  642. AS: 'US', // American Samoa
  643. AD: 'AD',
  644. AO: 'AO',
  645. AI: 'GB', // Anguilla
  646. AQ: 'AQ', // Antarctica
  647. AG: 'AG',
  648. AR: 'AR',
  649. AM: 'AM',
  650. AW: 'NL', // Aruba
  651. AU: 'AU',
  652. AT: 'AT',
  653. AZ: 'AZ',
  654. BS: 'BS',
  655. BH: 'BH',
  656. BD: 'BD',
  657. BB: 'BB',
  658. BY: 'BY',
  659. BE: 'BE',
  660. BZ: 'BZ',
  661. BJ: 'BJ',
  662. BM: 'GB', // Bermuda
  663. BT: 'BT',
  664. BO: 'BO',
  665. BQ: 'NL', // Bonaire, Sint Eustatius, Saba
  666. BA: 'BA',
  667. BW: 'BW',
  668. BV: 'NO', // Bouvet Island
  669. BR: 'BR',
  670. IO: 'GB', // British Indian Ocean Territory
  671. BN: 'BN',
  672. BG: 'BG',
  673. BF: 'BF',
  674. BI: 'BI',
  675. KH: 'KH',
  676. CM: 'CM',
  677. CA: 'CA',
  678. CV: 'CV',
  679. KY: 'UK', // Cayman Islands
  680. CF: 'CF',
  681. TD: 'TD',
  682. CL: 'CL',
  683. CN: 'CN',
  684. CX: 'AU', // Christmas Islands
  685. CC: 'AU', // Cocos (Keeling) Islands
  686. CO: 'CO',
  687. KM: 'KM',
  688. CG: 'CG',
  689. CD: 'CD',
  690. CK: 'NZ', // Cook Islands
  691. CR: 'CR',
  692. CI: 'CI',
  693. HR: 'HR',
  694. CU: 'CU',
  695. CW: 'NL', // Curacao
  696. CY: 'CY',
  697. CZ: 'CZ',
  698. DK: 'DK',
  699. DJ: 'DJ',
  700. DM: 'DM',
  701. DO: 'DO',
  702. EC: 'EC',
  703. EG: 'EG',
  704. SV: 'SV',
  705. GQ: 'GQ',
  706. ER: 'ER',
  707. EE: 'EE',
  708. ET: 'ET',
  709. FK: 'GB', // Falkland Islands
  710. FO: 'DK', // Faroe Islands
  711. FJ: 'FJ',
  712. FI: 'FI',
  713. FR: 'FR',
  714. GF: 'FR', // French Guiana
  715. PF: 'FR', // French Polynesia
  716. TF: 'FR', // French Southern Territories
  717. GA: 'GA',
  718. GM: 'GM',
  719. GE: 'GE',
  720. DE: 'DE',
  721. GH: 'GH',
  722. GI: 'UK', // Gibraltar
  723. GR: 'GR',
  724. GL: 'DK', // Greenland
  725. GD: 'GD',
  726. GP: 'FR', // Guadeloupe
  727. GU: 'US', // Guam
  728. GT: 'GT',
  729. GG: 'GB', // Guernsey
  730. GN: 'GN',
  731. GW: 'GW',
  732. GY: 'GY',
  733. HT: 'HT',
  734. HM: 'AU', // Heard Island and McDonald Islands
  735. VA: 'VA',
  736. HN: 'HN',
  737. HK: 'CN', // Hong Kong
  738. HU: 'HU',
  739. IS: 'IS',
  740. IN: 'IN',
  741. ID: 'ID',
  742. IR: 'IR',
  743. IQ: 'IQ',
  744. IE: 'IE',
  745. IM: 'GB', // Isle of Man
  746. IL: 'IL',
  747. IT: 'IT',
  748. JM: 'JM',
  749. JP: 'JP',
  750. JE: 'GB', // Jersey
  751. JO: 'JO',
  752. KZ: 'KZ',
  753. KE: 'KE',
  754. KI: 'KI',
  755. KR: 'KR',
  756. KW: 'KW',
  757. KG: 'KG',
  758. LA: 'LA',
  759. LV: 'LV',
  760. LB: 'LB',
  761. LS: 'LS',
  762. LR: 'LR',
  763. LY: 'LY',
  764. LI: 'LI',
  765. LT: 'LT',
  766. LU: 'LU',
  767. MO: 'CN', // Macao
  768. MK: 'MK',
  769. MG: 'MG',
  770. MW: 'MW',
  771. MY: 'MY',
  772. MV: 'MV',
  773. ML: 'ML',
  774. MT: 'MT',
  775. MH: 'MH',
  776. MQ: 'FR', // Martinique
  777. MR: 'MR',
  778. MU: 'MU',
  779. YT: 'FR', // Mayotte
  780. MX: 'MX',
  781. FM: 'FM',
  782. MD: 'MD',
  783. MC: 'MC',
  784. MN: 'MN',
  785. ME: 'ME',
  786. MS: 'GB', // Montserrat
  787. MA: 'MA',
  788. MZ: 'MZ',
  789. MM: 'MM',
  790. NA: 'NA',
  791. NR: 'NR',
  792. NP: 'NP',
  793. NL: 'NL',
  794. AN: 'NL', // Netherlands Antilles
  795. NC: 'FR', // New Caledonia
  796. NZ: 'NZ',
  797. NI: 'NI',
  798. NE: 'NE',
  799. NG: 'NG',
  800. NU: 'NZ', // Niue
  801. NF: 'AU', // Norfolk Island
  802. MP: 'US', // Northern Mariana Islands
  803. NO: 'NO',
  804. OM: 'OM',
  805. PK: 'PK',
  806. PW: 'PW',
  807. PS: 'IL', // Palestine
  808. PA: 'PA',
  809. PG: 'PG',
  810. PY: 'PY',
  811. PE: 'PE',
  812. PH: 'PH',
  813. PN: 'GB', // Pitcairn
  814. PL: 'PL',
  815. PT: 'PT',
  816. PR: 'US', // Puerto Rico
  817. QA: 'QA',
  818. RE: 'FR', // Reunion
  819. RO: 'RO',
  820. RU: 'RU',
  821. RW: 'RW',
  822. BL: 'FR', // Saint Barthelemy
  823. SH: 'GB', // Saint Helena
  824. KN: 'KN',
  825. LC: 'LC',
  826. MF: 'FR', // Saint Martin
  827. PM: 'FR', // Saint Pierre and Miquelon
  828. VC: 'VC',
  829. WS: 'WS',
  830. SM: 'SM',
  831. ST: 'ST',
  832. SA: 'SA',
  833. SN: 'SN',
  834. RS: 'RS',
  835. SC: 'SC',
  836. SL: 'SL',
  837. SG: 'SG',
  838. SX: 'NL', // Sint Maarten
  839. SK: 'SK',
  840. SI: 'SI',
  841. SB: 'SB',
  842. SO: 'SO',
  843. ZA: 'ZA',
  844. GS: 'GB', // South Georgia and the South Sandwich Islands
  845. ES: 'ES',
  846. LK: 'LK',
  847. SD: 'SD',
  848. SR: 'SR',
  849. SJ: 'NO', // Svalbard and Jan Mayen
  850. SZ: 'SZ',
  851. SE: 'SE',
  852. CH: 'CH',
  853. SY: 'SY',
  854. TW: 'TW', // Taiwan
  855. TJ: 'TJ',
  856. TZ: 'TZ',
  857. TH: 'TH',
  858. TL: 'TL',
  859. TG: 'TG',
  860. TK: 'NZ', // Tokelau
  861. TO: 'TO',
  862. TT: 'TT',
  863. TN: 'TN',
  864. TR: 'TR',
  865. TM: 'TM',
  866. TC: 'GB', // Turcs and Caicos Islands
  867. TV: 'TV',
  868. UG: 'UG',
  869. UA: 'UA',
  870. AE: 'AE',
  871. GB: 'GB',
  872. US: 'US',
  873. UM: 'US', // US Minor Outlying Islands
  874. UY: 'UY',
  875. UZ: 'UZ',
  876. VU: 'VU',
  877. VE: 'VE',
  878. VN: 'VN',
  879. VG: 'GB', // British Virgin Islands
  880. VI: 'US', // US Virgin Islands
  881. WF: 'FR', // Wallis and Futuna
  882. EH: 'MA', // Western Sahara
  883. YE: 'YE',
  884. ZM: 'ZM',
  885. ZW: 'ZW'
  886. };