Sodium BC

Accelerated renderings!

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 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);
})();