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