Better Scoreboard [ Geoguessr ] [ geoguessr.com ]

Improved lookup tool for geoguesser challenge leaderboard.

当前为 2022-10-22 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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
// ==/UserScript==
/*
* API endpoint for the Geoguessr challenge mode
*
* Edpoint: api/ v3/ results/ scores/ <match ID>/ <Lowest_Checked(max:50)>
*/
const endpoint = "https://www.geoguessr.com/api/v3/results/scores/";
/**
* data IS
* Map<String : Map<String..(4),Number..(2),Array< Number >..(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('<div id="H75lookup"></div>');
    $('#H75lookup').append('<div id="H75Controls"></div>');
    $('#H75lookup').append('<div class="results_table__FHKQm" id="H75Response"></div>');
    $('#H75Controls').append(`<label for="searchPosition">Rank:</label><input type="number" id="searchPosition"><button id="H75searchPosBtn">Search By Rank</button>`);
    $('#H75Controls').append(`<label for="searchName">Username:</label><input type="text" id="searchName"><button id="H75searchNameBtn">Search By Username</button>`);
    $("#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(`<p>${nPlayers} players</p>`); 
    $("#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(`<div class="results_row__2iTV4 results_headerRow__C91Ks"><div></div><div class="results_hideOnSmallScreen__hrv5O">Round 1</div><div class="results_hideOnSmallScreen__hrv5O">Round 2</div><div class="results_hideOnSmallScreen__hrv5O">Round 3</div><div class="results_hideOnSmallScreen__hrv5O">Round 4</div><div class="results_hideOnSmallScreen__hrv5O">Round 5</div><div>Total</div></div>`);
         //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(`<div class="results_row__2iTV4" id="H75Entry"><div class="results_column__BTeok results_player__F8U_T"><span class="results_position__KWMOY">${position+1}.</span><div class="results_userLink__V6cBI"><a target="_blank" href="/user/${a["uid"]}"><div class="user-nick_root__DUfvc"><div class="user-nick_avatar__lW3e2"><div class="styles_rectangle___6gqv" style="padding-top: 100%;"><div class="styles_circle__QFYEk styles_variantFloating__Srm_N styles_colorWhite__Y640w styles_borderSizeFactorOne__8iP_3"><div class="styles_rectangle___6gqv" style="padding-top: 100%;"><div class="styles_innerCircle__Y_L_e"><div class="styles_content__otIVG"><img src="/images/auto/48/48/ce/0/plain/${a["pic"]}" class="styles_image__8M_kp" alt="Efesoturum" loading="auto" style="object-fit: cover;"></div></div></div></div></div></div><div class="user-nick_nickWrapper__8Tnk4"><div class="user-nick_nick__y4VIt">${a["name"]}&nbsp;</div></div></div></a></div></div><div class="results_column__BTeok results_hideOnSmallScreen__hrv5O"><div class="results_score__jUqyZ">${a["scores"][0]} pts</div><div class="results_scoreDetails__rvWSm">${a["dists"][0]} km</div></div><div class="results_column__BTeok results_hideOnSmallScreen__hrv5O"><div class="results_score__jUqyZ">${a["scores"][1]} pts</div><div class="results_scoreDetails__rvWSm">${a["dists"][1]} km</div></div><div class="results_column__BTeok results_hideOnSmallScreen__hrv5O"><div class="results_score__jUqyZ">${a["scores"][2]} pts</div><div class="results_scoreDetails__rvWSm">${a["dists"][2]} km</div></div><div class="results_column__BTeok results_hideOnSmallScreen__hrv5O"><div class="results_score__jUqyZ">${a["scores"][3]} pts</div><div class="results_scoreDetails__rvWSm">${a["dists"][3]} km</div></div><div class="results_column__BTeok results_hideOnSmallScreen__hrv5O"><div class="results_score__jUqyZ">${a["scores"][4]} pts</div><div class="results_scoreDetails__rvWSm">${a["dists"][4]} km</div></div><div class="results_column__BTeok"><div class="results_score__jUqyZ">${a["totscore"]} pts</div><div class="results_scoreDetails__rvWSm">${a["totDist"]} km</div></div></div>`);

        // 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(`<div class="results_row__2iTV4" id="H75Entry">Undefined error or you broke the script probably. This isn’t gonna show up any other way </div>`); 
    }
}
/**
 * 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(`<div class="results_row__2iTV4 results_headerRow__C91Ks"><div></div><div class="results_hideOnSmallScreen__hrv5O">Round 1</div><div class="results_hideOnSmallScreen__hrv5O">Round 2</div><div class="results_hideOnSmallScreen__hrv5O">Round 3</div><div class="results_hideOnSmallScreen__hrv5O">Round 4</div><div class="results_hideOnSmallScreen__hrv5O">Round 5</div><div>Total</div></div>`);
        $("#H75Response").append(`<div class="results_row__2iTV4" id="H75Entry"><div class="results_column__BTeok results_player__F8U_T"><span class="results_position__KWMOY">${position+1}.</span><div class="results_userLink__V6cBI"><a target="_blank" href="/user/${a["uid"]}"><div class="user-nick_root__DUfvc"><div class="user-nick_avatar__lW3e2"><div class="styles_rectangle___6gqv" style="padding-top: 100%;"><div class="styles_circle__QFYEk styles_variantFloating__Srm_N styles_colorWhite__Y640w styles_borderSizeFactorOne__8iP_3"><div class="styles_rectangle___6gqv" style="padding-top: 100%;"><div class="styles_innerCircle__Y_L_e"><div class="styles_content__otIVG"><img src="/images/auto/48/48/ce/0/plain/${a["pic"]}" class="styles_image__8M_kp" alt="Efesoturum" loading="auto" style="object-fit: cover;"></div></div></div></div></div></div><div class="user-nick_nickWrapper__8Tnk4"><div class="user-nick_nick__y4VIt">${a["name"]}&nbsp;</div></div></div></a></div></div><div class="results_column__BTeok results_hideOnSmallScreen__hrv5O"><div class="results_score__jUqyZ">${a["scores"][0]} pts</div><div class="results_scoreDetails__rvWSm">${a["dists"][0]} km</div></div><div class="results_column__BTeok results_hideOnSmallScreen__hrv5O"><div class="results_score__jUqyZ">${a["scores"][1]} pts</div><div class="results_scoreDetails__rvWSm">${a["dists"][1]} km</div></div><div class="results_column__BTeok results_hideOnSmallScreen__hrv5O"><div class="results_score__jUqyZ">${a["scores"][2]} pts</div><div class="results_scoreDetails__rvWSm">${a["dists"][2]} km</div></div><div class="results_column__BTeok results_hideOnSmallScreen__hrv5O"><div class="results_score__jUqyZ">${a["scores"][3]} pts</div><div class="results_scoreDetails__rvWSm">${a["dists"][3]} km</div></div><div class="results_column__BTeok results_hideOnSmallScreen__hrv5O"><div class="results_score__jUqyZ">${a["scores"][4]} pts</div><div class="results_scoreDetails__rvWSm">${a["dists"][4]} km</div></div><div class="results_column__BTeok"><div class="results_score__jUqyZ">${a["totscore"]} pts</div><div class="results_scoreDetails__rvWSm">${a["totDist"]} km</div></div></div>`)
        
        $("#H75Entry").click(function(e){
            e.preventDefault();
            window.open(`https://www.geoguessr.com/results/${a["gametoken"]}`);
            
   });
        
    } else {
        $("#H75Response").empty();
        $("#H75Response").append(`<div class="results_row__2iTV4" id="H75Entry">User not found</div>`);  
    }

}