Arcaea Score Update

Upload your Arcaea score to bot.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Arcaea Score Update
// @namespace    arcaea-score-update
// @version      1.2.3
// @icon         https://chinosk.top/g.jpg
// @description  Upload your Arcaea score to bot.
// @author       Chinosk
// @match        https://arcaea.lowiro.com/*
// @grant        GM_xmlhttpRequest
// @connect      *
// ==/UserScript==


(function() {
    'use strict';

    let div = document.createElement("div");
    div.style.position = "fixed";
    div.style.bottom = "30px";
    div.style.right = "30px";
    div.style.backgroundColor = "#ccc";
    div.style.padding = "3px"
    div.style.maxWidth = "250px"

    let fromRankRatio = document.createElement("input");
    fromRankRatio.type = "radio";
    fromRankRatio.name = "inputType";
    fromRankRatio.id = "inputFromRankRatio";
    fromRankRatio.checked = true;
    let fromRankLabel = document.createElement("label");
    fromRankLabel.htmlFor = "inputFromRankRatio";
    fromRankLabel.textContent = "From Rank";
    fromRankLabel.style.marginRight = "2px";

    let fromPaidRatio = document.createElement("input");
    fromPaidRatio.type = "radio";
    fromPaidRatio.name = "inputType";
    fromPaidRatio.id = "inputFromPaid";
    let fromPaidLabel = document.createElement("label")
    fromPaidLabel.htmlFor = "inputFromPaid";
    fromPaidLabel.textContent = "From Purchase";

    let syncCookieCheck = document.createElement("input");
    syncCookieCheck.type = "checkbox";
    syncCookieCheck.id = "syncCookieCheck";
    syncCookieCheck.checked = true;
    let syncCookieLabel = document.createElement("label")
    syncCookieLabel.htmlFor = "syncCookieCheck";
    syncCookieLabel.textContent = "Upload Cookie";

    let buttonUpdateMe = document.createElement("button");
    buttonUpdateMe.innerHTML = "Update Me";
    buttonUpdateMe.style.width = "100%";
    buttonUpdateMe.style.height = "40px";
    buttonUpdateMe.style.marginBottom = "2px";

    let buttonDefaultText = "Update Bests";
    let button = document.createElement("button");
    button.innerHTML = buttonDefaultText;
    button.style.width = "100%";
    button.style.height = "40px";
    button.style.marginBottom = "2px";

    let buttonStop = document.createElement("button");
    buttonStop.innerHTML = "Stop";
    buttonStop.style.width = "100%";
    buttonStop.style.height = "40px";
    buttonStop.style.marginBottom = "2px";
    buttonStop.style.display = "none";

    let buttonClose = document.createElement("button");
    buttonClose.innerHTML = "Close";
    buttonClose.style.width = "100%";
    buttonClose.style.height = "40px";
    buttonClose.style.marginBottom = "2px";

    let querySelfRatio = document.createElement("input");
    querySelfRatio.type = "radio";
    querySelfRatio.name = "queryType";
    querySelfRatio.id = "queryTypeSelf";
    querySelfRatio.checked = true;
    let querySelfLabel = document.createElement("label")
    querySelfLabel.htmlFor = "queryTypeSelf";
    querySelfLabel.textContent = "Update Self Bests";

    let queryOthersRatio = document.createElement("input");
    queryOthersRatio.type = "radio";
    queryOthersRatio.name = "queryType";
    queryOthersRatio.id = "queryTypeOthers";
    let queryOthersLabel = document.createElement("label")
    queryOthersLabel.htmlFor = "queryTypeOthers";
    queryOthersLabel.textContent = "Update Others Bests";

    let queryOtherInput = document.createElement("select");
    queryOtherInput.style.width = "100%"
    queryOtherInput.style.display = "none"

    div.appendChild(fromRankRatio);
    div.appendChild(fromRankLabel);
    div.appendChild(fromPaidRatio);
    div.appendChild(fromPaidLabel);
    div.appendChild(document.createElement("br"));
    div.appendChild(syncCookieCheck);
    div.appendChild(syncCookieLabel);
    let syncCookieBr = document.createElement("br");
    div.appendChild(syncCookieBr);

    div.appendChild(querySelfRatio)
    div.appendChild(querySelfLabel)
    div.appendChild(document.createElement("br"));
    div.appendChild(queryOthersRatio)
    div.appendChild(queryOthersLabel)
    let queryOthersBr = document.createElement("br");
    div.appendChild(queryOthersBr);
    div.appendChild(queryOtherInput)

    div.appendChild(buttonUpdateMe);
    div.appendChild(button);
    div.appendChild(buttonStop);
    div.appendChild(buttonClose);
    let powerLabel = document.createElement("label");
    powerLabel.innerHTML = "Powered By @Chinosk";
    powerLabel.style.fontSize = "10px";
    div.appendChild(powerLabel);
    document.body.appendChild(div);

    let stopQueryFlag = false;

    buttonClose.addEventListener("click", () => {
        div.style.display = "none";
    })

    buttonStop.addEventListener("click", () => {
        stopQueryFlag = true;
    })

    querySelfRatio.addEventListener("change", onQueryRatioChange);
    queryOthersRatio.addEventListener("change", onQueryRatioChange);
    fromPaidRatio.addEventListener("change", () => {
        if (fromPaidRatio.checked) {
            querySelfRatio.checked = true;
            onQueryRatioChange();
            queryOthersRatio.style.display = "none";
            queryOthersLabel.style.display = "none";
            queryOthersBr.style.display = "none";
        }
    })
    fromRankRatio.addEventListener("change", () => {
        if (fromRankRatio.checked) {
            queryOthersRatio.style.display = "";
            queryOthersLabel.style.display = "";
            queryOthersBr.style.display = "";
        }
    })

    function GM_Fetch(details) {
        let saveResp = null;
        let det = {
            method: details.method,
            redirect: 'follow',
            credentials: "include"
        };
        if (details.headers) {
            det.headers = details.headers;
        }
        if (details.data) {
            det.body = details.data;
        }
        fetch(details.url, det)
            .then(response => {
                saveResp = response;
                return response.text();
            })
            .then(result => {
                let newResp = {};
                newResp.responseText = result;
                newResp.responseHeaders = saveResp.headers;
                newResp.status = saveResp.status;
                details.onload(newResp);
            })
            .catch(error => {
                if (!details.onerror) {
                    console.log("Request error", error);
                    alert("Request error: " + error);
                }
                details.onerror(error);
            });
    }

    const userAgent = navigator.userAgent.toLowerCase();
    const isIOS = /iphone|ipad|ipod/.test(userAgent);
    const notSupportReq = typeof GM_xmlhttpRequest !== 'function';
    let RequestFunc;
    if (isIOS || notSupportReq) {
        console.log("isIOS", isIOS, "notSupportReq", notSupportReq);
        syncCookieCheck.checked = false;
        syncCookieCheck.setAttribute("disabled", "true");
        syncCookieLabel.textContent += "(系统限制, 无法同步 Cookie)";
        RequestFunc = GM_Fetch;
    }
    else {
        RequestFunc = GM_xmlhttpRequest;
    }

    function onQueryRatioChange() {
        if (queryOthersRatio.checked) {
            fromRankRatio.checked = true;
            queryOtherInput.style.display = "";
            queryOtherInput.innerHTML = "";
            syncCookieCheck.style.display = "none";
            syncCookieLabel.style.display = "none";
            syncCookieBr.style.display = "none";
            RequestFunc({
                method: "GET",
                url: "https://webapi.lowiro.com/webapi/user/me",
                withCredentials: true,
                onload: (response) => {
                    let user_me_json = JSON.parse(response.responseText);
                    if (user_me_json.success) {
                        for (const friends of user_me_json.value.friends) {
                            let optionElem = document.createElement('option');
                            optionElem.value = friends.user_id;
                            optionElem.text = friends.name;
                            queryOtherInput.appendChild(optionElem)
                        }
                    }
                },
                onerror: (response) => {
                    alert("Get friend list failed. " + response.status + " " + response.responseText)
                }
            });
        }
        else {
            queryOtherInput.style.display = "none";
            syncCookieCheck.style.display = ""
            syncCookieLabel.style.display = ""
            syncCookieBr.style.display = ""
        }

    }

    buttonUpdateMe.addEventListener("click", () => {
        RequestFunc({
            method: "GET",
            url: "https://webapi.lowiro.com/webapi/user/me",
            withCredentials: true,
            onload: (response) => {
                let user_me_json = JSON.parse(response.responseText);
                if (user_me_json.success) {
                    uploadUserMeDataToServer(JSON.parse(response.responseText), getSetCookieFromStr(response.responseHeaders));
                    alert("success.");
                }
            },
            onerror: (response) => {
                alert("Get friend list failed. " + response.status + " " + response.responseText)
            }
        });
    })

    button.addEventListener("click", () => {
        try {
            button.setAttribute("disabled", "true")
            RequestFunc({
                method: "GET",
                url: "https://webapi.lowiro.com/webapi/user/me",
                withCredentials: true,
                onload: (response) => {
                    let user_me_json = JSON.parse(response.responseText);
                    if (!user_me_json.success) {
                        endQuery();
                        alert("Get user info failed!");
                        return;
                    }
                    if (querySelfRatio.checked) {
                        uploadUserMeDataToServer(JSON.parse(response.responseText), getSetCookieFromStr(response.responseHeaders));
                        if (fromRankRatio.checked) {
                            updateData(user_me_json);
                        }
                        else if (fromPaidRatio.checked) {
                            updateDataFromPurchase(user_me_json);
                        }
                    }
                    else if (queryOthersRatio.checked) {
                        let queryUserId = parseInt(queryOtherInput.value);
                        let friendData = null;
                        for (const friends of user_me_json.value.friends) {
                            if (friends.user_id === queryUserId) {
                                friendData = friends;
                                break;
                            }
                        }
                        if (friendData !== null) {
                            user_me_json.value.rating = friendData.rating;
                            user_me_json.value.user_id = friendData.user_id;
                            user_me_json.value.is_skill_sealed = friendData.is_skill_sealed;
                            user_me_json.value.join_date = friendData.join_date;
                            user_me_json.value.character = friendData.character;
                            user_me_json.value.recent_score = friendData.recent_score;
                            user_me_json.value.name = friendData.name;
                            user_me_json.value.display_name = friendData.name;
                            user_me_json.value.user_id = friendData.user_id;
                            user_me_json.value.user_code = "000000000";
                            updateData(user_me_json);
                        }
                        else {
                            endQuery();
                        }
                    }

                },
                onerror: (response) => {
                    endQuery();
                }
            });
        }
        catch (e) {
            endQuery();
            alert(`Error: ${e}`);
            throw e
        }
    });

    window.onerror = (message, source, line, column, error) => {
        endQuery();
        const errorMessage = error ? error.message : message;
        alert(`Error: ${errorMessage}`);
        console.log(error);
    };

    function endQuery() {
        button.removeAttribute("disabled");
        button.innerHTML = buttonDefaultText;
        buttonStop.style.display = "none";
    }

    function uploadUserMeDataToServer(user_me_json, cookieSid) {
        if (!user_me_json.success) {
            return;
        }
        let reqHeaders = {
            "Content-Type": "application/json"
        };
        if (syncCookieCheck.checked) {
            reqHeaders["Sync-Sid"] = cookieSid;
        }

        RequestFunc({
            method: "POST",
            url: "https://www.chinosk6.cn/arcscore/update/me",
            headers: reqHeaders,
            data: JSON.stringify(user_me_json),
            onload: (response) => {
                if (response.status !== 200) {
                    throw Error("Upload user info failed.");
                }
            }
        });
    }

    function uploadBestDataToServer(data_json) {
        console.log("Upload Data", data_json);
        RequestFunc({
            method: "POST",
            url: "https://www.chinosk6.cn/arcscore/update/bests",
            headers: {
                "Content-Type": "application/json"
            },
            data: JSON.stringify(data_json),
            onload: (response) => {
                if (response.status !== 200) {
                    throw Error("Upload bests failed.");
                }
                else {
                    alert("Upload Success!");
                }
            }
        });
    }

    function updateDataFromPurchase(user_me_json) {
        let user_id = user_me_json["value"]["user_id"];
        let user_name = user_me_json["value"]["name"];
        let user_character = user_me_json["value"]["character"];
        let user_is_skill_sealed = user_me_json["value"]["is_skill_sealed"];
        // let user_rating = user_me_json["value"]["rating"]

        RequestFunc({
            method: "GET",
            url: "https://webapi.lowiro.com/webapi/score/rating/me",
            onload: (response) => {
                let data = JSON.parse(response.responseText)
                if ((response.status === 200) && data.success) {
                    let postData = []
                    for (let i of data["value"]["best_rated_scores"]) {
                        postData.push({
                            "user_id": user_id,
                            "song_id": i.song_id,
                            "difficulty": i.difficulty,
                            "score": i.score,
                            "shiny_perfect_count": 0,
                            "perfect_count": 0,
                            "near_count": 0,
                            "miss_count": 0,
                            "health": 100,
                            "modifier": i.modifier,
                            "time_played": Math.round(new Date().getTime()),
                            "best_clear_type": i.clear_type,
                            "clear_type": i.clear_type,
                            "name": user_name,
                            "character": user_character,
                            "is_skill_sealed": user_is_skill_sealed,
                            "is_char_uncapped": true,
                            "rank": 1
                        })
                    }
                    uploadBestDataToServer(postData);
                }
                else {
                    alert(`Get rating failed (${response.status}).\n${response.responseText}`);
                }
                endQuery();
            },
            onerror: (response) => {
                endQuery();
            }
        });
    }

    function updateData(user_me_json) {
        let user_id = user_me_json.value.user_id;
        let user_rating = user_me_json.value.rating;

        RequestFunc({
            method: "GET",
            url: "https://www.chinosk6.cn/arcscore/get_slst",
            onload: (response) => {
                if (response.status !== 200) {
                    throw Error("Get song list failed.");
                }

                let songs = JSON.parse(response.responseText)
                songs.sort((a, b) => b.rating - a.rating);
                startCalcB30(songs, user_rating / 100, user_id)
                    .then((results) => {
                        if (results !== null) {
                            if (results.length > 0) {
                                button.innerHTML = "Uploading...";
                                uploadBestDataToServer(results);
                            }
                        }
                        endQuery();
                    })
            }
        });
    }

    async function startCalcB30(songs, user_rating, user_id) {
        function calcSongRating(sid, difficultyIndex, score) {
            let result = 0;
            for (const songsKey in songs) {
                if ((songs[songsKey].sid === sid) && (songs[songsKey].difficulty === difficultyIndex)) {
                    const rating = songs[songsKey].rating / 10;
                    if (score < 9800000) {
                        result = rating + (score - 9500000) / 300000
                    }
                    else if (9800000 <= score <= 10000000) {
                        result = rating + 1 + (score - 9800000) / 200000
                    }
                    else if (score <= 10020000) {
                        result = rating + 2
                    }
                    else {
                        result = rating + 1 + (score - 9800000) / 200000
                    }
                    break;
                }
            }
            if (result < 0) result = 0;
            return result;
        }

        let retData = []
        function getRetMinRating (maxCount=40) {
            retData.sort((a, b) => {return b.rating - a.rating;});
            retData = retData.slice(0, maxCount);
            return retData[retData.length - 1].rating;
        }

        let querySongs = []
        for (let n in songs) {
            if (songs[n].rating <= 0) {
                continue;
            }
            querySongs.push(songs[n]);
        }
        querySongs.sort((a, b) => {return b.rating - a.rating;})

        button.innerHTML = `Query song (0)`;
        buttonStop.style.display = ""
        let endPoint = "https://webapi.lowiro.com/webapi/score/song/friend"
        if (querySelfRatio.checked) {
            endPoint = "https://webapi.lowiro.com/webapi/score/song/me"
        }

        for (let n in querySongs) {
            let songId = songs[n].sid;
            let songDifficulty = songs[n].difficulty;
            if (stopQueryFlag) {
                buttonStop.style.display = "none";
                stopQueryFlag = false;
                return null;
            }
            button.innerHTML = `Query song (${parseInt(n) + 1})...`
            const response = await new Promise((resolve, reject) => {
                RequestFunc({
                    method: "GET",
                    url: `${endPoint}?song_id=${songId}&difficulty=${songDifficulty}&start=0&limit=30`,
                    onload: (response) => {
                        resolve(response);
                    },
                    onerror: (error) => reject(error),
                });
            });
            let currData = getScoreFromFriendRank(response.responseText, user_id);
            if (currData !== null) {
                currData.rating = calcSongRating(currData.song_id, currData.difficulty, currData.score)
                console.log(currData)
                if (retData.length >= 40) {
                    let minRt = getRetMinRating();
                    if (querySongs[n].rating / 10 + 2 < minRt) {
                        break;
                    }
                }
                retData.push(currData);
            }
        }
        buttonStop.style.display = "none"
        return retData;
    }

    function getScoreFromFriendRank(responseText, user_id) {
        let data = JSON.parse(responseText);
        if (data.success) {
            if (data.value && (data.value.length > 0)) {
                for (const i of data.value) {
                    if (i.user_id.toString() === user_id.toString()) {
                        return i;
                    }
                }
            }
        } else {
            console.log("Query bests failed: ", responseText);
            if (data.error_code === 1) {
                responseText += "\n访问被拒绝,可能需要订阅 Arcaea Online."
            }
            alert(`Query bests failed:\n${responseText}`);
            stopQueryFlag = true;
        }
        return null;
    }

    function getSetCookieFromStr(cookieStr) {
        if (!(typeof cookieStr === "string")) {
            return null;
        }
        let setCookie = cookieStr.match(/set-cookie\s*:\s*([\s\S]+?)\n/i);
        if (setCookie) {
            for (let spData of setCookie[1].split(";")) {
                spData = spData.trim();
                if (spData.startsWith("sid=")) {
                    return spData.trim();
                }
            }
        }
        return null;
    }
})();