Others' challenge results

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
}