Others' challenge results

Shows where other people went on a challenge round as you play it, similar to Geoguessr

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Others' challenge results
// @namespace    http://tampermonkey.net/
// @version      2025-09-23
// @description  Shows where other people went on a challenge round as you play it, similar to Geoguessr
// @author       You
// @match        *://geotastic.net/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=geotastic.net
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==
// === BEGIN MD5 ===
// credit: https://www.myersdaily.org/joseph/javascript/md5-text.html
function md5cycle(x, k) {
    var a = x[0],
        b = x[1],
        c = x[2],
        d = x[3];

    a = ff(a, b, c, d, k[0], 7, -680876936);
    d = ff(d, a, b, c, k[1], 12, -389564586);
    c = ff(c, d, a, b, k[2], 17, 606105819);
    b = ff(b, c, d, a, k[3], 22, -1044525330);
    a = ff(a, b, c, d, k[4], 7, -176418897);
    d = ff(d, a, b, c, k[5], 12, 1200080426);
    c = ff(c, d, a, b, k[6], 17, -1473231341);
    b = ff(b, c, d, a, k[7], 22, -45705983);
    a = ff(a, b, c, d, k[8], 7, 1770035416);
    d = ff(d, a, b, c, k[9], 12, -1958414417);
    c = ff(c, d, a, b, k[10], 17, -42063);
    b = ff(b, c, d, a, k[11], 22, -1990404162);
    a = ff(a, b, c, d, k[12], 7, 1804603682);
    d = ff(d, a, b, c, k[13], 12, -40341101);
    c = ff(c, d, a, b, k[14], 17, -1502002290);
    b = ff(b, c, d, a, k[15], 22, 1236535329);

    a = gg(a, b, c, d, k[1], 5, -165796510);
    d = gg(d, a, b, c, k[6], 9, -1069501632);
    c = gg(c, d, a, b, k[11], 14, 643717713);
    b = gg(b, c, d, a, k[0], 20, -373897302);
    a = gg(a, b, c, d, k[5], 5, -701558691);
    d = gg(d, a, b, c, k[10], 9, 38016083);
    c = gg(c, d, a, b, k[15], 14, -660478335);
    b = gg(b, c, d, a, k[4], 20, -405537848);
    a = gg(a, b, c, d, k[9], 5, 568446438);
    d = gg(d, a, b, c, k[14], 9, -1019803690);
    c = gg(c, d, a, b, k[3], 14, -187363961);
    b = gg(b, c, d, a, k[8], 20, 1163531501);
    a = gg(a, b, c, d, k[13], 5, -1444681467);
    d = gg(d, a, b, c, k[2], 9, -51403784);
    c = gg(c, d, a, b, k[7], 14, 1735328473);
    b = gg(b, c, d, a, k[12], 20, -1926607734);

    a = hh(a, b, c, d, k[5], 4, -378558);
    d = hh(d, a, b, c, k[8], 11, -2022574463);
    c = hh(c, d, a, b, k[11], 16, 1839030562);
    b = hh(b, c, d, a, k[14], 23, -35309556);
    a = hh(a, b, c, d, k[1], 4, -1530992060);
    d = hh(d, a, b, c, k[4], 11, 1272893353);
    c = hh(c, d, a, b, k[7], 16, -155497632);
    b = hh(b, c, d, a, k[10], 23, -1094730640);
    a = hh(a, b, c, d, k[13], 4, 681279174);
    d = hh(d, a, b, c, k[0], 11, -358537222);
    c = hh(c, d, a, b, k[3], 16, -722521979);
    b = hh(b, c, d, a, k[6], 23, 76029189);
    a = hh(a, b, c, d, k[9], 4, -640364487);
    d = hh(d, a, b, c, k[12], 11, -421815835);
    c = hh(c, d, a, b, k[15], 16, 530742520);
    b = hh(b, c, d, a, k[2], 23, -995338651);

    a = ii(a, b, c, d, k[0], 6, -198630844);
    d = ii(d, a, b, c, k[7], 10, 1126891415);
    c = ii(c, d, a, b, k[14], 15, -1416354905);
    b = ii(b, c, d, a, k[5], 21, -57434055);
    a = ii(a, b, c, d, k[12], 6, 1700485571);
    d = ii(d, a, b, c, k[3], 10, -1894986606);
    c = ii(c, d, a, b, k[10], 15, -1051523);
    b = ii(b, c, d, a, k[1], 21, -2054922799);
    a = ii(a, b, c, d, k[8], 6, 1873313359);
    d = ii(d, a, b, c, k[15], 10, -30611744);
    c = ii(c, d, a, b, k[6], 15, -1560198380);
    b = ii(b, c, d, a, k[13], 21, 1309151649);
    a = ii(a, b, c, d, k[4], 6, -145523070);
    d = ii(d, a, b, c, k[11], 10, -1120210379);
    c = ii(c, d, a, b, k[2], 15, 718787259);
    b = ii(b, c, d, a, k[9], 21, -343485551);

    x[0] = add32(a, x[0]);
    x[1] = add32(b, x[1]);
    x[2] = add32(c, x[2]);
    x[3] = add32(d, x[3]);

}

function cmn(q, a, b, x, s, t) {
    a = add32(add32(a, q), add32(x, t));
    return add32((a << s) | (a >>> (32 - s)), b);
}

function ff(a, b, c, d, x, s, t) {
    return cmn((b & c) | ((~b) & d), a, b, x, s, t);
}

function gg(a, b, c, d, x, s, t) {
    return cmn((b & d) | (c & (~d)), a, b, x, s, t);
}

function hh(a, b, c, d, x, s, t) {
    return cmn(b ^ c ^ d, a, b, x, s, t);
}

function ii(a, b, c, d, x, s, t) {
    return cmn(c ^ (b | (~d)), a, b, x, s, t);
}

function md51(s) {
    var n = s.length,
        state = [1732584193, -271733879, -1732584194, 271733878],
        i;
    for (i = 64; i <= s.length; i += 64) {
        md5cycle(state, md5blk(s.slice(i - 64, i)));
    }
    s = s.slice(i - 64);
    var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
    for (i = 0; i < s.length; i++)
        tail[i >> 2] |= s[i] << ((i % 4) << 3);
    tail[i >> 2] |= 0x80 << ((i % 4) << 3);
    if (i > 55) {
        md5cycle(state, tail);
        for (i = 0; i < 16; i++) tail[i] = 0;
    }
    tail[14] = n * 8;
    md5cycle(state, tail);
    return new Uint8Array((new Int32Array(state)).buffer);
}

/* there needs to be support for Unicode here,
 * unless we pretend that we can redefine the MD-5
 * algorithm for multi-byte characters (perhaps
 * by adding every four 16-bit characters and
 * shortening the sum to 32 bits). Otherwise
 * I suggest performing MD-5 as if every character
 * was two bytes--e.g., 0040 0025 = @%--but then
 * how will an ordinary MD-5 sum be matched?
 * There is no way to standardize text to something
 * like UTF-8 before transformation; speed cost is
 * utterly prohibitive. The JavaScript standard
 * itself needs to look at this: it should start
 * providing access to strings as preformed UTF-8
 * 8-bit unsigned value arrays.
 */
function md5blk(s) {
    /* I figured global was faster.   */
    var md5blks = [],
        i; /* Andy King said do it this way. */
    for (i = 0; i < 64; i += 4) {
        md5blks[i >> 2] = s[i] +
            (s[i + 1] << 8) +
            (s[i + 2] << 16) +
            (s[i + 3] << 24);
    }
    return md5blks;
}

function add32(a, b) {
    return (a + b) & 0xFFFFFFFF;
}
// === END MD5 ===
const PASSWORD = "4317969d37f68d4f54eae689d8088eba";

function evpBytesToKey(salt, password, keyLen = 32, ivLen = 16) {
    let d = new Uint8Array(0);
    let passwdArr = new Uint8Array(PASSWORD.split('').map(k => k.charCodeAt(0)));
    while (d.length < (keyLen + ivLen)) {
        let rem = d.slice(-16);
        let toHash = new Uint8Array(rem.length + passwdArr.length + salt.length);
        toHash.set(rem, 0);
        toHash.set(passwdArr, rem.length);
        toHash.set(salt, rem.length + passwdArr.length);
        let addD = md51(toHash);
        let newD = new Uint8Array(d.length + addD.length);
        newD.set(d, 0);
        newD.set(addD, d.length);
        d = newD;
    }
    return {
        key: d.slice(0, keyLen),
        iv: d.slice(keyLen, keyLen + ivLen)
    }
}
async function aesDecrypt(ct, salt) {
    const keyIv = evpBytesToKey(salt, PASSWORD);
    const cryptoKey = await crypto.subtle.importKey("raw", keyIv.key, "AES-CBC", false, ["decrypt"]);
    const decrypted = await crypto.subtle.decrypt({
        name: "AES-CBC",
        iv: keyIv.iv
    }, cryptoKey, ct);
    return new TextDecoder().decode(decrypted);
}
async function decodeEncdata(json) {
    const salt = Uint8Array.fromHex(json.s);
    const ct = Uint8Array.fromBase64(json.ct);
    return JSON.parse(await aesDecrypt(ct, salt));
}
const challengeDropsRegex = /getChallengeDrops.php\?id=(\d+)/;
const lastRoundSeenRegex = /updateLastRoundSeen.php/;
const challengePickRegex = /updateChallengePick.php/;
const finishRegex = /finishChallenge.php/;
const itemsRegex = /getItems.php/;


const lineSymbol = {
    path: 'M 0,-1 0,1',
    strokeOpacity: 1,
    scale: 2,
};

window.markersToClear = [];
window.finished = new Set();
var checkInterval = setInterval(function() {
    if (typeof google === 'object' && typeof google.maps === 'object' && typeof google.maps.Map === 'function') {
        
        var originalMap = google.maps.Map;
        google.maps.Map = function() {
            var instance = new originalMap(...arguments);
            window.capturedMap = instance
            return instance
        }
        clearInterval(checkInterval); // Stop checking once the module is loaded
    }
}, 10);

function getMarkerUrl(itemUid) {
    if (!window.items) {
        return null;
    }
    for (var item of window.items) {
        if (item.uid === itemUid && item.type == "marker") {
            return `https://static.geo.edutastic.de/${item.imageUrl}`;
        }
    }
}

function fetchChallengeResults(challengeId) {
    fetch(`https://backend01.geotastic.net/v1/challenge/getChallengeResults.php?id=${challengeId}`, {
        headers: {
            'X-Auth-Token': window.localStorage.token
        }
    }).then(
        (result) => {
            result.json().then(
                (json) => {
                    
                    window.challengeResults = json
                }
            )
        }
    );
}
var oldOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
    
    // starting a challenge
    if (method === "GET" && challengeDropsRegex.test(url)) {
        window.challengeId = challengeDropsRegex.exec(url)[1];
        if (finished.has(window.challengeId)) {
            return oldOpen.apply(this, arguments);
        }
        window.roundNo = -1;
        fetchChallengeResults(window.challengeId);
        
        this.addEventListener("load", function() {
            const response = JSON.parse(this.responseText);
            const encoded = JSON.parse(response.encData);
            decodeEncdata(encoded).then((result) => {
                window.challengeDrops = result
            });
        });
    }
    // new round
    else if (method === "POST" && lastRoundSeenRegex.test(url)) {
        window.roundNo++;
        for (var m of window.markersToClear) {
            m.setMap(null);
        }
    }
    // end round
    else if (method === "POST" && challengePickRegex.test(url)) {
        //find top 5
        let topResults = window.challengeResults.data.sort((a, b) => ((b.picks[window.roundNo] ? b.picks[window.roundNo].score : (-1)) - (a.picks[window.roundNo] ? a.picks[window.roundNo].score : (-1)))).slice(0, 5);
       
        window.markersToClear = [];
        for (var result of topResults) {
            if(!result.picks[window.roundNo]){
                continue;
            }
            let markerItemUid = result.userData.markerItemUid;
            let markerUrl = getMarkerUrl(markerItemUid);
            let markerSettings = {
                position: {
                    lat: result.picks[window.roundNo].lat,
                    lng: result.picks[window.roundNo].lng
                },
                map: window.capturedMap,
            };
            if (markerUrl) {
                markerSettings.icon = markerUrl;
            }
            window.markersToClear.push(new google.maps.Marker(
                markerSettings
            ));
            window.markersToClear.push(new google.maps.Polyline({
                path: [{
                        lat: window.challengeDrops[window.roundNo].lat,
                        lng: window.challengeDrops[window.roundNo].lng
                    },
                    {
                        lat: result.picks[window.roundNo].lat,
                        lng: result.picks[window.roundNo].lng
                    }
                ],
                map: window.capturedMap,
                strokeOpacity: 0,
                icons: [{
                    icon: lineSymbol,
                    offset: "0",
                    repeat: "15px",
                }, ],
            }));

        }
    }
    // load items(markers)
    else if (method === "GET" && itemsRegex.test(url)) {
        this.addEventListener('load', function() {
            window.items = JSON.parse(this.responseText).data;
        });
    } else if (method === "POST" && finishRegex.test(url)) {
        window.finished.add(challengeId);
    }
    return oldOpen.apply(this, arguments);
}