// ==UserScript== // @name Better Scoreboard [ Geoguessr ] [ geoguessr.com ] // @version 1.2.2 // @author Han75 - @Han75#4985 // @description Improved lookup tool for geoguesser challenge leaderboard. // @match https://www.geoguessr.com/results/* // @require http://code.jquery.com/jquery-latest.js // @namespace han75.com // @downloadURL none // ==/UserScript== /* * API endpoint for the Geoguessr challenge mode * * Edpoint: api/ v3/ results/ scores/ / */ const endpoint = "https://www.geoguessr.com/api/v3/results/scores/"; /** * data IS * Map..(2) >> * WHERE * {ID:DATA}= {Scoreboard_Pos : Name, uid, pfp, Game_Token, Total_Tcore, Total_Distance, All_Scores, All_Distances}} */ // This can be refactored as an array of maps instead of a map of then. as they are already indexed by nonnegative integers var data = {} /* * nameMap IS * Map< String : Number > * WHERE * {NAME : ID} = {name:coreBd_Pos} */ var nameMap = {} // Number of players in the lobby let numPlayers=0; $(document).ready(function () { //render context $('.results_switch__Qj1HI').after('
'); $('#H75lookup').append('
'); $('#H75lookup').append('
'); $('#H75Controls').append(``); $('#H75Controls').append(``); $("#H75searchPosBtn").prop("disabled", true); $("#H75searchNameBtn").prop("disabled", true); $("#H75searchPosBtn").click(findPosition); $("#H75searchNameBtn").click(findUsername); id = $(`meta[property='og:url']`).attr("content").split("/")[4]; // The final stack trace is || RoundUp(NumPlayers/50) fetch requests <- getData() <- getNumPlayers(recursive) // I thought this was a bad choice at first, but it’s probably better than accidentally sending sh*t loads of API requests because of preliminary fetch delays and getting slammed with a rate limit findNumberOfPlayers(0,6000); }); /** * Finds how many players are in a lobby * @param {2 Numbers} LowerBound,UpperBound */ async function findNumberOfPlayers(lowerBound, upperBound) { //Binary search babyy if (Math.ceil(lowerBound) >= Math.floor(upperBound) - 1) { getData(Math.ceil(lowerBound)); } else { let midpoint = Math.ceil((lowerBound + upperBound) / 2); //Fetch 1 entry from API, change search bounds and search again fetch(`${endpoint}${id}/${midpoint}/1`, { accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" }).then((response) => response.json()) .then((resp) => { if (resp.length == 0) { findNumberOfPlayers(lowerBound, midpoint); } else { findNumberOfPlayers(midpoint, upperBound); } });; } } /** * Loads all of the data and stores it in data dictionary. * */ async function getData(nPlayers) { numPlayers=nPlayers; //Fetch all the players. //Can fetch a max of 50 datapoints at a time for(let i = 0; i < nPlayers; i += 50) { //accept parameter is used by native application and I added it to ensure that it Satan himself(CORS) doesn't stop me fetch(`${endpoint}${id}/${i}/50`, { accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" }).then((response) => response.json()).then((resp) => { // I hate the variable name. resp ? Like ew for (let k = 0; k < resp.length; k++) { let scores = []; let dists = []; let tDist=0; for (let round = 0; round < resp[k]["game"]["player"]["guesses"].length; round++) { scores.push(resp[k]["game"]["player"]["guesses"][round]["roundScoreInPoints"]); dists.push((resp[k]["game"]["player"]["guesses"][round]["distanceInMeters"] / 1000).toFixed(3)); tDist+=resp[k]["game"]["player"]["guesses"][round]["distanceInMeters"] / 1000; } data[i + k] = {"name": resp[k]["playerName"], "uid":resp[k]["userId"],"pic":resp[k]["pinUrl"],"gametoken": resp[k]["gameToken"], "totscore": resp[k]["game"]["player"]["totalScore"]["amount"],"totDist":tDist.toFixed(3), "scores": scores, "dists": dists } // Map user name to score for 1 : 1 correlation nameMap[resp[k]["playerName"].toLowerCase()] = i + k; } }) } /* * Display number of players in the lobby, * H75Lookup >> H75Control Context Map: * @p[lobby size] * @label@input[Number, GetInfoBySB_Position] * @button[Call findPosition] * @labrel@input[text, GetInfoByUsername] * @button[ */ $('#H75Controls').prepend(`

${nPlayers} players

`); $("#H75searchPosBtn").prop("disabled", false); $("#H75searchNameBtn").prop("disabled", false); } // TODO: I want the @p to say “N records found (download)” function download(){ //TODO: do this function later take a break, jesus. console.log(`File will have ${Object.keys(data).length} entries`); var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data)); console.log(`File will have ${Object.keys(data).length} entries`); var dlAnchorElem = document.getElementById('H75exportData'); dlAnchorElem.setAttribute("href", dataStr ); dlAnchorElem.setAttribute("download", "data_.json"); } /** * Searches data by Scoreboard Position from the input box */ // refactor to a better name function findPosition() { let position = Math.floor($('#searchPosition').val() - 1); if (position in data) { let a = data[position]; // I’m not gonna cap I straight up ripped these fancy ahh stylized ahh divs straight from the results table, whose gonna stop me? 😈 $("#H75Response").empty(); $("#H75Response").append(`
Round 1
Round 2
Round 3
Round 4
Round 5
Total
`); //Literally what is this naming convention? Nothing about this makes sense. You don’t have to make it this hard on yourself, @GeoGuessr’s singular front end engineer $("#H75Response").append(`
${position+1}.
${a["scores"][0]} pts
${a["dists"][0]} km
${a["scores"][1]} pts
${a["dists"][1]} km
${a["scores"][2]} pts
${a["dists"][2]} km
${a["scores"][3]} pts
${a["dists"][3]} km
${a["scores"][4]} pts
${a["dists"][4]} km
${a["totscore"]} pts
${a["totDist"]} km
`); // When I click the div I open game overview in a new tab // I can’t show it on this page because I can’t add the the native event listener to the new div because it’s disgustingly obfuscated $("#H75Entry").click(function(e){ e.preventDefault(); window.open(`https://www.geoguessr.com/results/${a["gametoken"]}`); }); } else if(position<1||position>numPlayers) { $('#searchPosition').val(""); }else{ $("#H75Response").empty(); $("#H75Response").append(`
Undefined error or you broke the script probably. This isn’t gonna show up any other way
`); } } /** * searches data by username */ function findUsername() { // Get that shi from the input box. MAN I love jquery let name = $('#searchName').val(); if (name.toLowerCase() in nameMap) { let position=nameMap[name]; let a=data[nameMap[name]]; // Everything below this like is exactly the same as searchPosition $("#H75Response").empty(); $("#H75Response").append(`
Round 1
Round 2
Round 3
Round 4
Round 5
Total
`); $("#H75Response").append(`
${position+1}.
${a["scores"][0]} pts
${a["dists"][0]} km
${a["scores"][1]} pts
${a["dists"][1]} km
${a["scores"][2]} pts
${a["dists"][2]} km
${a["scores"][3]} pts
${a["dists"][3]} km
${a["scores"][4]} pts
${a["dists"][4]} km
${a["totscore"]} pts
${a["totDist"]} km
`) $("#H75Entry").click(function(e){ e.preventDefault(); window.open(`https://www.geoguessr.com/results/${a["gametoken"]}`); }); } else { $("#H75Response").empty(); $("#H75Response").append(`
User not found
`); } }