Melvor additive skilling anti-lag

Adjusts game speed to compensate for lag so that the original intervals match realtime. Based on anti-lag by 8992

当前为 2021-08-13 提交的版本,查看 最新版本

// ==UserScript==
// @name         Melvor additive skilling anti-lag
// @version      0.2.0
// @description  Adjusts game speed to compensate for lag so that the original intervals match realtime. Based on anti-lag by 8992
// @author       GMiclotte
// @match        https://*.melvoridle.com/*
// @exclude      https://wiki.melvoridle.com/*
// @grant        none
// @namespace    http://tampermonkey.net/
// @noframes
// ==/UserScript==

function script() {
    const skillFunctions = [
        cutTree,
        startFishing,
        burnLog,
        startCooking,
        mineRock,
        rockReset,
        startSmithing,
        pickpocket,
        startFletching,
        startCrafting,
        startRunecrafting,
        startHerblore,
        startAgility,
        createSummon,
        castMagic,
    ];

    class ASAL {
        constructor(name) {
            this.name = name;
            this.lagDifference = {};
            this.intervalLog = {};
            this.lagKeyIncrement = 0;
        }

        log(...args) {
            console.log('Melvor Additive Skilling Anti-Lag', ...args)
        }

        patchCode(code, match, replacement) {
            return code
                .toString()
                .replace(match, replacement)
                .replace(/^function (\w+)/, "window.$1 = function");
        }

        editInterval(code, skip = []) {
            const name = code.match(/(?<=^window\.)[^ =]+/)[0];
            const arr = code.split("setTimeout");
            //loop through array backwards to avoid problems with nested setTimeouts
            for (let i = arr.length - 1; i > 0; i--) {
                let b = 0;
                let index = 0;
                let found = -1;
                arr[i].replace(/./gs, (char) => {
                    char === "(" ? b++ : char === ")" ? b-- : 0;
                    if (found < 0 && b === 0) {
                        found = index;
                    }
                    index++;
                    return char;
                });
                //insert interval recording
                let part = arr[i].slice(0, found);
                const close = `${this.name}.recordInterval(KEY, false, null, "${name}"),`;
                if (part.includes(name)) {
                    part = part.replace(
                        new RegExp(`(?<!${close.replace(/([()])/g, (m, $1) => `\\${$1}`)})(${name}\\()`, "g"),
                        (m, $1) => `${close}${$1}`
                    );
                } else if (!skip.includes(i)) {
                    part = part.replace(
                        /(}[^}]*$)/s,
                        (m, $1) => `${this.name}.recordInterval(KEY, false, null, "${name}");${$1}`
                    );
                }
                let edited = 'setTimeout';
                if (!skip.includes(i)) {
                    edited += part.replace(
                        /,([^,]*$)/s,
                        (m, $1) => `,${this.name}.recordInterval(KEY, true, (${$1}), "${name}")`
                    );
                } else {
                    edited += part;
                }
                edited += arr[i].slice(found);
                arr[i - 1] += edited;
            }
            return arr[0].replace("(KEY, true)", `(KEY, true, null, "${name}")`);
        }

        recordInterval(KEY, open, baseInterval = null, thread) {
            if (open && baseInterval != null) {
                try {
                    this.intervalLog[thread].timestamps[KEY].I = baseInterval;
                    return baseInterval - this.lagDifference[thread];
                } catch {
                    return baseInterval;
                }
            } else if (open) {
                try {
                    this.intervalLog[thread].timestamps[KEY] = {T: new Date()};
                } catch {
                    this.intervalLog[thread] = {timestamps: [], results: []};
                    this.intervalLog[thread].timestamps[KEY] = {T: new Date()};
                    this.lagDifference[thread] = 0;
                }
            } else if (this.intervalLog[thread] !== undefined && this.intervalLog[thread].timestamps[KEY] !== undefined) {
                this.intervalLog[thread].results.push({
                    T: new Date() - this.intervalLog[thread].timestamps[KEY].T,
                    I: this.intervalLog[thread].timestamps[KEY].I,
                });
                delete this.intervalLog[thread].timestamps[KEY];
            }
        }

        calcLag() {
            for (let thread in this.intervalLog) {
                // sum measured and expected times
                const sum = this.intervalLog[thread].results.reduce(
                    (sum, a) => (a.T * a.I === 0 ? sum : {T: sum.T + a.T, I: sum.I + a.I}),
                    {T: 0, I: 0}
                );
                // nothing to adjust
                if (sum.T * sum.I === 0) continue;
                // compute lag difference
                const delays = this.intervalLog[thread].results.map(x => x.T - x.I);
                const remainingDifference = delays.reduce((sum, x) => sum + x, 0) / delays.length;
                this.lagDifference[thread] += remainingDifference;
                // reset this.intervalLog
                this.intervalLog[thread].results = [];
                this.intervalLog[thread].timestamps = [];
                this.lagKeyIncrement = 0;
            }
        }
    }

    function loadScript() {
        const name = 'asal';
        window[name] = new ASAL(name);
        const asal = window[name];
        asal.log('loading...');
        let codeStrings = skillFunctions
            .map(a => asal.patchCode(a, /{/, `{let KEY = ${asal.name}.lagKeyIncrement++;${asal.name}.recordInterval(KEY, true);`))
            .map((a, i) => asal.editInterval(a, i === 0 ? [1] : []));
        codeStrings.forEach(a => eval(a));
        setInterval(() => asal.calcLag(), 120000);
        asal.log('Loaded');
    }

    loadScript();
}

(function () {
    function injectScript(main) {
        const scriptElement = document.createElement('script');
        scriptElement.textContent = `try {(${main})();} catch (e) {console.log(e);}`;
        document.body.appendChild(scriptElement).parentNode.removeChild(scriptElement);
    }

    function loadScript() {
        if ((window.isLoaded && !window.currentlyCatchingUp)
            || (typeof unsafeWindow !== 'undefined' && unsafeWindow.isLoaded && !unsafeWindow.currentlyCatchingUp)) {
            // Only load script after game has opened
            clearInterval(scriptLoader);
            injectScript(script);
        }
    }

    const scriptLoader = setInterval(loadScript, 200);
})();