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

目前為 2022-01-31 提交的版本,檢視 最新版本

// ==UserScript==
// @name        Melvor Additive Skilling Anti-Lag
// @namespace   http://tampermonkey.net/
// @version     0.2.8
// @description Adjusts game speed to compensate for lag so that the original intervals match realtime. Based on anti-lag by 8992
// @author      GMiclotte
// @include		https://melvoridle.com/*
// @include		https://*.melvoridle.com/*
// @exclude		https://melvoridle.com/index.php
// @exclude		https://*.melvoridle.com/index.php
// @exclude		https://wiki.melvoridle.com*
// @exclude		https://*.wiki.melvoridle.com*
// @inject-into page
// @noframes
// @grant        none
// ==/UserScript==

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

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

        log(...args) {
            console.log('ASAL:', ...args)
        }

        warn(...args) {
            console.warn('ASAL:', ...args)
        }

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

        editInterval(code, skip = []) {
            // get name and add startThread
            code = this.patchCode(code);
            const match = code.match(/(?<=^window\.)[^ =]+/);
            if (match === null) {
                this.warn('Some skill function could not be patched.');
                return;
            }
            const name = match[0];
            code = code.replace(/{/, `{let KEY = ${this.name}.startThread('${name}');`);
            // split around timeouts
            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);
                let edited = 'setTimeout';
                if (!skip.includes(i)) {
                    // default
                    part = part.replace(
                        /},([^,]*$)/s,
                        (m, $1, $2) => `${this.name}.recordCloseInterval(KEY, '${name}');},${this.name}.recordTimeout(KEY, (${$1}), '${name}')`
                    );
                }
                edited += part;
                edited += arr[i].slice(found);
                arr[i - 1] += edited;
            }
            return arr[0];
        }

        startThread(thread) {
            const KEY = this.lagKeyIncrement++;
            // this.log(`start function ${KEY} ${thread}`);
            if (this.intervalLog[thread] === undefined) {
                this.intervalLog[thread] = {timestamps: [], results: []};
                this.lagDifference[thread] = 0;
            }
            this.intervalLog[thread].timestamps[KEY] = {T: new Date()};
            return KEY;
        }

        recordTimeout(KEY, baseInterval, thread) {
            if (this.intervalLog[thread].timestamps[KEY] !== undefined) {
                this.intervalLog[thread].timestamps[KEY].I = baseInterval;
            }
            const interval = baseInterval - this.lagDifference[thread];
            // this.log(`start time out ${interval} ${KEY} ${thread}`);
            return interval;
        }

        recordCloseInterval(KEY, thread) {
            if (this.intervalLog[thread] === undefined || this.intervalLog[thread].timestamps[KEY] === undefined) {
                // log has been cleared, don't record this one
                return;
            }
            const expected = this.intervalLog[thread].timestamps[KEY].I;
            // measured time is limited to 1.5x expected time
            const measured = Math.min(
                new Date() - this.intervalLog[thread].timestamps[KEY].T,
                1.5 * expected,
            );
            this.intervalLog[thread].results.push({
                T: measured,
                I: expected,
            });
            delete this.intervalLog[thread].timestamps[KEY];
        }

        calcLag() {
            // this.log('calculate lag')
            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 startASAL(skillFunctions) {
        const name = 'asal';
        window[name] = new ASAL(name);
        const asal = window[name];
        asal.log('loading...');
        asal.warn('Cooking is not supported.');
        asal.warn('Astrology is not supported.');
        let codeStrings = skillFunctions.map((a, i) => asal.editInterval(a, i === 0 ? [1] : [])).filter(x => x);
        codeStrings.forEach(a => eval(a));
        setInterval(() => asal.calcLag(), 2 * 60 * 1000);
        asal.log('Loaded');
    }

    function loadScript() {
        const skillFunctions = [
            startFishing,
            startFletching,
            startCrafting,
            startRunecrafting,
            startAgility,
            createSummon,
        ];
        if (skillFunctions.every((a) => a !== undefined)) {
            // Only load script after all functions have been defined
            clearInterval(scriptLoader);
            startASAL(skillFunctions);
        }
    }

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