Sodium BC

Accelerated renderings!

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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