Sodium BC

Accelerated renderings!

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==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);
})();