Sodium BC

Accelerated renderings!

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @license              WTFPL
// @name                 Sodium BC
// @description          Accelerated renderings!
// @version              Alpha 0.6
// @namespace            Alvin
// @grant                none
// @icon                 https://huzpsb.github.io/na.png
// @match                *://*/*BondageClub*
// @match                *://*.bondage-europe.com/*
// @match                *://*.elementfx.com/*
// ==/UserScript==

const th = 1;

(function () {
    async function applyHooks() {
        function sodiumLog(...args) {
            console.log("[Sodium]", ...args);
        }

        function logo() {
            sodiumLog(" > https://greasyfork.org/en/scripts/581899-sodium-bc < \n  _________          .___.__               \n" + " /   _____/ ____   __| _/|__|__ __  _____  \n" + " \\_____  \\ /  _ \\ / __ | |  |  |  \\/     \\ \n" + " /        (  <_> ) /_/ | |  |  |  /  Y Y  \\\n" + "/_______  /\\____/\\____ | |__|____/|__|_|  /\n" + "        \\/            \\/                \\/ ");
        }

        function xhrFetch(url) {
            return new Promise((resolve, reject) => {
                const xhr = new XMLHttpRequest();
                xhr.open('GET', url, true);
                xhr.onload = function () {
                    if (xhr.status >= 200 && xhr.status < 300) {
                        resolve(xhr.responseText);
                    } else {
                        reject(xhr.status);
                    }
                };
                xhr.onerror = () => reject(0);
                xhr.send();
            });
        }

        function extractFunctionCode(sourceText, functionName) {
            const regex = new RegExp(`function\\s+${functionName}\\s*\\(`, 'g');
            const match = regex.exec(sourceText);
            if (!match) return null;
            const startIndex = match.index;
            let braceCount = 0;
            let hasStarted = false;
            let endIndex = -1;
            for (let i = startIndex; i < sourceText.length; i++) {
                if (sourceText[i] === '{') {
                    braceCount++;
                    hasStarted = true;
                } else if (sourceText[i] === '}') {
                    braceCount--;
                }
                if (hasStarted && braceCount === 0) {
                    endIndex = i + 1;
                    break;
                }
            }
            return endIndex !== -1 ? sourceText.slice(startIndex, endIndex) : null;
        }

        function sodiumHash(updater, pointer) {
            let h = pointer[0];
            if (!updater) {
                h = h * 31 + 17;
            } else {
                const len = updater.length;
                h = h * 31 + len;
                h = h * 31 + (updater.charCodeAt(0) || 0);
                h = h * 31 + (updater.charCodeAt(len >> 1) || 0);
                h = h * 31 + (updater.charCodeAt(len - 1) || 0);
            }
            pointer[0] = h & 0xFFFF;
        }

        function dfsNode(node) {
            for (let i = 0; i < node.childNodes.length; i++) {
                const result = dfsNode(node.childNodes[i]);
                if (result) return result;
            }
            if (node.style && node.style[0] === 'cursor' && node.innerText === 'Close') return node.onclick;
        }

        const originalAppendChild = Element.prototype.appendChild;
        Element.prototype.appendChild = function (child) {
            const e = originalAppendChild.call(this, child);
            for (let i = 0; i < this.childNodes.length; i++) {
                const result = dfsNode(this.childNodes[i]);
                if (result) {
                    console.log("STFU, BCX!");
                    result();
                }
            }
            return e;
        };

        const svgCode = `<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'>
<rect width='140' height='140' rx='12' fill='#389cf4' />
<text x='70' y='28' fill='white' font-family='sans-serif' font-size='16' text-anchor='middle'>11</text>
<text x='70' y='85' fill='white' font-family='sans-serif' font-size='70' font-weight='bold' text-anchor='middle'>Na</text>
<text x='70' y='112' fill='white' font-family='sans-serif' font-size='20' text-anchor='middle'>Sodium</text>
<text x='70' y='132' fill='white' font-family='sans-serif' font-size='16' text-anchor='middle'>22.990</text>
</svg>`;
        const svgDataUri = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgCode)}`;
        const style = `background-image: url("${svgDataUri}");
background-size: 140px 140px;
background-repeat: no-repeat;
padding: 70px;
font-size: 1px;
color: transparent;`;
        console.log("%c ", style);
        logo();
        sodiumLog("Unpatching critical functions...");
        const [ChatRoomCharacterViewJs, DrawingJs] = await Promise.all([xhrFetch('Screens/Online/ChatRoom/ChatRoomCharacterView.js'), xhrFetch('Scripts/Drawing.js')]);
        let ChatRoomCharacterViewDrawJs = extractFunctionCode(ChatRoomCharacterViewJs, 'ChatRoomCharacterViewDraw');
        let DrawCharacterJs = extractFunctionCode(DrawingJs, 'DrawCharacter');
        eval(ChatRoomCharacterViewDrawJs.replace(/function\s+ChatRoomCharacterViewDraw/, 'window.ChatRoomCharacterViewDraw0 = function'));
        eval(DrawCharacterJs.replace(/function\s+DrawCharacter/, 'window.DrawCharacter0 = function'));

        const echoExt = !!window.Asset.filter(w => w.Name === '人偶').length;
        if (echoExt) {
            sodiumLog("Echo Extension detected! Compatibility is EXPERIMENTAL. Expect bugs and report them to the developer.");
            let DrawCharacterSegmentJs = extractFunctionCode(DrawingJs, 'DrawCharacterSegment');
            eval(DrawCharacterSegmentJs.replace(/function\s+DrawCharacterSegment/, 'window.DrawCharacterSegment0 = function'));
            window.DrawCharacterSegment = function DrawCharacterSegment() {
                window.DrawCharacterSegment0.apply(this, arguments);
            }

            const [GLDrawJs, CommonDrawJs] = await Promise.all([xhrFetch('Scripts/GLDraw.js'), xhrFetch('Scripts/CommonDraw.js')]);
            let GLDrawLoadJs = extractFunctionCode(GLDrawJs, 'GLDrawLoad');
            eval(GLDrawLoadJs.replace(/function\s+GLDrawLoad/, 'window.GLDrawLoad0 = function'));
            window.GLDrawLoad = function GLDrawLoad() {
                window.GLDrawLoad0.apply(this, arguments);
            }
            let GLDrawAppearanceBuildJs = extractFunctionCode(GLDrawJs, 'GLDrawAppearanceBuild');
            eval(GLDrawAppearanceBuildJs.replace(/function\s+GLDrawAppearanceBuild/, 'window.GLDrawAppearanceBuild0 = function'));
            window.GLDrawAppearanceBuild = function GLDrawAppearanceBuild() {
                window.GLDrawAppearanceBuild0.apply(this, arguments);
            }
            let CommonDrawCanvasPrepareJs = extractFunctionCode(CommonDrawJs, 'CommonDrawCanvasPrepare');
            eval(CommonDrawCanvasPrepareJs.replace(/function\s+CommonDrawCanvasPrepare/, 'window.CommonDrawCanvasPrepare0 = function'));
            window.CommonDrawCanvasPrepare = function CommonDrawCanvasPrepare() {
                window.CommonDrawCanvasPrepare0.apply(this, arguments);
            }

            sodiumLog("Clearing character canvas caches...");
            Character.forEach(C => {
                C.Canvas = null;
                C.CanvasBlink = null;
                window.CharacterRefresh(C, false);
            });
            window.GLDrawResetCanvas();
            sodiumLog("Done reverting echo extension patches!");
        }
        sodiumLog("Functions unpatched!");

        let whisperId = null;
        const drawInfoMap = new Map();
        let frameDrawnChars = [];
        let frameSkippedChars = [];
        const tickRecords = [];

        let globalForceDrawID = 0;
        setInterval(() => {
            globalForceDrawID++;
        }, 1000);

        window.ChatRoomCharacterViewDraw = function () {
            const tickBegin = performance.now();
            window.isDrawingChatRoomCharacter = true;
            frameDrawnChars = [];
            frameSkippedChars = [];
            try {
                try {
                    return window.ChatRoomCharacterViewDraw0.apply(this, arguments);
                } catch (e) {
                    debugger;
                }
            } finally {
                for (const charInfo of frameDrawnChars) {
                    const info = drawInfoMap.get(charInfo.id);
                    if (info && !info.skipCache) {
                        if (!info.cacheCanvas) {
                            info.cacheCanvas = document.createElement("canvas");
                            info.cacheCtx = info.cacheCanvas.getContext("2d", {willReadFrequently: true});
                        }
                        const w = Math.ceil(500 * charInfo.zoom);
                        const h = Math.ceil(1000 * charInfo.zoom);
                        info.cacheCanvas.width = w;
                        info.cacheCanvas.height = h;
                        info.cacheCtx.clearRect(0, 0, w, h);
                        try {
                            info.cacheCtx.drawImage(MainCanvas.canvas, charInfo.x, charInfo.y, w, h, 0, 0, w, h);
                        } catch (e) {
                            debugger;
                        }
                    }
                }

                for (const charInfo of frameSkippedChars) {
                    const info = drawInfoMap.get(charInfo.id);
                    if (info && info.cacheCanvas) {
                        try {
                            MainCanvas.drawImage(info.cacheCanvas, charInfo.x, charInfo.y);
                        } catch (e) {
                            debugger;
                        }
                    }
                }
                window.isDrawingChatRoomCharacter = false;
                const duration = performance.now() - tickBegin;
                tickRecords.push({time: Date.now(), duration});
            }
        };

        window.DrawCharacter = function (C, X, Y, Zoom, IsHeightResizeAllowed, DrawCanvas) {
            if (!C || C.CharacterID == null) return;

            if (!window.isDrawingChatRoomCharacter || DrawCanvas != null || window.CurrentScreen !== "ChatRoom" || window.CurrentCharacter) {
                return window.DrawCharacter0.call(this, C, X, Y, Zoom, IsHeightResizeAllowed, DrawCanvas);
            }

            const pointer = [0];
            const initStr = X + " " + Y + " " + Zoom + " " + IsHeightResizeAllowed;
            sodiumHash(initStr, pointer);

            if (C.Appearance) {
                for (let i = 0; i < C.Appearance.length; i++) {
                    const item = C.Appearance[i];
                    const desc = item && item.Asset ? item.Asset.Description : null;
                    sodiumHash(desc, pointer);
                }
            }
            const currentHash = pointer[0];
            const now = Date.now();

            let info = drawInfoMap.get(C.CharacterID);
            if (!info) {
                info = {
                    lastDrawHash: undefined,
                    nextStaleDraw: 0,
                    lastForceDrawID: -1,
                    drawTimes: 0,
                    drawPerformanceSum: 0,
                    createdAt: now,
                    cacheCanvas: null,
                    cacheCtx: null,
                    lastSeen: now,
                    consecutiveDraws: 0,
                    skipCache: false
                };
                drawInfoMap.set(C.CharacterID, info);
            }

            info.lastSeen = now;

            const isMeasuring = (now - info.createdAt < 15000);
            const avg = info.drawTimes > 0 ? (info.drawPerformanceSum / info.drawTimes) : 0;
            const isOptimized = !isMeasuring && (avg > th);

            let skipCheck = false;
            if (whisperId !== window.ChatRoomTargetMemberNumber) {
                if (whisperId === C.MemberNumber || window.ChatRoomTargetMemberNumber === C.MemberNumber) {
                    skipCheck = true;
                    whisperId = window.ChatRoomTargetMemberNumber;
                }
            }

            let shouldDraw = true;
            if (isOptimized && !skipCheck) {
                if (info.lastForceDrawID !== globalForceDrawID) {
                    shouldDraw = true;
                    info.lastForceDrawID = globalForceDrawID;
                } else if (info.nextStaleDraw > now && info.lastDrawHash === currentHash) {
                    shouldDraw = false;
                } else if (info.lastDrawHash !== currentHash) {
                    shouldDraw = true;
                }
            }

            if (!shouldDraw) {
                info.consecutiveDraws = 0;
                if (info.cacheCanvas) {
                    frameSkippedChars.push({
                        id: C.CharacterID, x: X, y: Y, zoom: Zoom
                    });
                    return;
                }
            }

            info.consecutiveDraws++;
            info.skipCache = (info.consecutiveDraws > 5);

            frameDrawnChars.push({
                id: C.CharacterID, x: X, y: Y, zoom: Zoom
            });

            const startDraw = Date.now();
            DrawCharacter0.call(this, C, X, Y, Zoom, IsHeightResizeAllowed, DrawCanvas);
            const afterDraw = Date.now();

            info.drawPerformanceSum += (afterDraw - startDraw);
            info.drawTimes += 1;
            const newAvg = info.drawPerformanceSum / info.drawTimes;

            info.nextStaleDraw = afterDraw + (100 * newAvg);
            info.lastDrawHash = currentHash;
        };

        if (typeof window.ChatRoomViews !== 'undefined' && window.ChatRoomViews.Character && window.ChatRoomViews.Character.Draw) {
            window.ChatRoomViews.Character.Draw = window.ChatRoomCharacterViewDraw;
        }

        window.DrawText0 = window.DrawText;
        window.DrawText = function (Text, X, Y, Color, BackColor) {
            if (window.isDrawingChatRoomCharacter && Text && MainCanvas) {
                function measureW(t) {
                    return MainCanvas.measureText(t).width;
                }

                function truncate(t, maxW) {
                    while (t.length > 0 && measureW(t + '..') > maxW) {
                        t = t.slice(0, -1);
                    }
                    return t.length > 0 ? t + '..' : '..';
                }

                if (measureW(Text) > 400) {
                    Text = truncate(Text, 400);
                }
            }
            return window.DrawText0.call(this, Text, X, Y, Color, BackColor);
        };
        
        setInterval(function () {
            const roomSeps = document.querySelectorAll("#TextAreaChatLog .chat-room-sep");
            if (roomSeps.length === 0) return;
            const lastSep = roomSeps[roomSeps.length - 1];
            const parent = lastSep.parentElement;
            let currentMsgCount = 0;
            let sib = lastSep.nextSibling;
            while (sib) {
                currentMsgCount++;
                sib = sib.nextSibling;
            }
            if (currentMsgCount <= 50) return;
            while (lastSep.previousSibling) {
                parent.removeChild(lastSep.previousSibling);
            }
            while (parent.childElementCount > 51) {
                if (lastSep.nextSibling) {
                    parent.removeChild(lastSep.nextSibling);
                } else {
                    break;
                }
            }
        }, 60000);

        // GC
        setInterval(function () {
            const now = Date.now();
            for (const [id, info] of drawInfoMap.entries()) {
                if (now - info.lastSeen > 60000) {
                    if (info.cacheCanvas) {
                        info.cacheCanvas.width = 0;
                        info.cacheCanvas.height = 0;
                        info.cacheCanvas = null;
                        info.cacheCtx = null;
                    }
                    drawInfoMap.delete(id);
                }
            }

            const cutoff = Date.now() - 305000;
            while (tickRecords.length > 0 && tickRecords[0].time < cutoff) {
                tickRecords.shift();
            }
        }, 5000);

        window.tps = function () {
            logo();
            sodiumLog("=== Performance Stats ===");
            const now = Date.now();
            [1, 5, 30, 300].forEach(sec => {
                const cutoff = now - sec * 1000;
                const records = tickRecords.filter(r => r.time >= cutoff);
                const label = sec === 300 ? "5min" : sec + "s";
                if (records.length === 0) {
                    sodiumLog(`[${label}] No data`);
                    return;
                }
                const tpsNum = records.length / sec;
                const tps = tpsNum.toFixed(2);
                const durations = records.map(r => r.duration).sort((a, b) => a - b);
                const max = durations[durations.length - 1].toFixed(2);
                const avg = (durations.reduce((a, b) => a + b, 0) / durations.length).toFixed(2);
                let p9idx = Math.floor(durations.length * 0.9);
                if (p9idx >= durations.length) p9idx = durations.length - 1;
                const p9 = durations[p9idx].toFixed(2);

                sodiumLog(`[${label.padEnd(4, ' ')}] TPS: ${tps.padStart(5, ' ')} | MSPT (avg/90%/max): ${avg.padStart(6, ' ')} / ${p9.padStart(6, ' ')} / ${max.padStart(6, ' ')}`);
            });

            sodiumLog("=== Character Stats ===");
            const charCount = drawInfoMap.size;
            if (charCount === 0) {
                sodiumLog("(No character data)");
                return;
            }
            sodiumLog(`Total characters tracked: ${charCount}`);
            for (const [id, info] of drawInfoMap.entries()) {
                const avgDrawTime = info.drawTimes > 0 ? (info.drawPerformanceSum / info.drawTimes).toFixed(2) : "N/A";
                const charObj = window.Character.find(w => w.CharacterID === id);
                const nickname = charObj && charObj.Nickname ? ` | nickname: ${charObj.Nickname}` : "";
                sodiumLog(`  Character #${id} | drawTimes: ${info.drawTimes} | avgDraw: ${avgDrawTime}ms | lastSeen: ${Math.round((now - info.lastSeen) / 1000)}s ago${nickname}`);
            }
        };
    }

    const timer = setInterval(() => {
        if (!window.Player || !window.Player.MemberNumber) {
            return;
        }
        clearInterval(timer);
        applyHooks();
    }, 1000);
})();