Sodium BC

Accelerated renderings!

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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