您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Display emoticons in chat!
当前为
// ==UserScript== // @name Multiplayer Piano Optimizations [Emotes] // @namespace https://tampermonkey.net/ // @version 1.1.1 // @description Display emoticons in chat! // @author zackiboiz // @match *://multiplayerpiano.com/* // @match *://multiplayerpiano.net/* // @match *://multiplayerpiano.org/* // @match *://piano.mpp.community/* // @match *://mpp.7458.space/* // @match *://qmppv2.qwerty0301.repl.co/* // @match *://mpp.8448.space/* // @match *://mpp.autoplayer.xyz/* // @match *://mpp.hyye.xyz/* // @icon https://www.google.com/s2/favicons?sz=64&domain=multiplayerpiano.net // @grant GM_info // @license MIT // ==/UserScript== (async () => { function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } await sleep(1000); const BASE_URL = "https://raw.githubusercontent.com/ZackiBoiz/Multiplayer-Piano-Optimizations/refs/heads/main"; const RGB_PREFIX = "\u0D9E"; class EmotesManager { constructor(version, baseUrl) { this.version = version; this.baseUrl = baseUrl; this.emotes = {}; this.tokenRegex = null; } async init() { try { await this._loadEmotes(); this._buildTokenRegex(); this._initChatObserver(); this._replaceExistingMessages(); } catch (err) { console.error("EmotesManager failed:", err); MPP.chat.sendPrivate({ name: `[MPP Emotes] v${this.version}`, color: "#ff0000", message: "EmotesManager initialization failed. Check console for details", }); } } async _loadEmotes() { const res = await fetch(`${this.baseUrl}/emotes/meta.json?_=${Date.now()}`); if (!res.ok) { throw new Error(`Failed to load emote metadata: ${res.status}`); } const data = await res.json(); if (typeof data !== "object" || Array.isArray(data)) { throw new Error("Unexpected emote metadata shape"); } this.emotes = data; } _buildTokenRegex() { const tokens = Object.keys(this.emotes).map(t => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); tokens.sort((a, b) => b.length - a.length); this.tokenRegex = new RegExp(`:(${tokens.join("|")}):`, "g"); } _initChatObserver() { const chatList = document.querySelector("#chat > ul"); if (!chatList) { console.warn("EmotesManager: chat container not found"); return; } const observer = new MutationObserver(mutations => { for (const m of mutations) { if (m.type === "childList" && m.addedNodes.length) { for (const node of m.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && node.tagName === "LI") { this._replaceEmotesInElement(node.querySelector(".message")); } } } } }); observer.observe(chatList, { childList: true }); } _replaceExistingMessages() { const messages = document.querySelectorAll("#chat > ul li .message"); messages.forEach(msgEl => { this._replaceEmotesInElement(msgEl); }); } _replaceEmotesInElement(element) { if (!element) return; const prelim = []; for (const node of Array.from(element.childNodes)) { if (node.nodeType === Node.TEXT_NODE && node.nodeValue.includes("\\n")) { const parts = node.nodeValue.split("\\n"); parts.forEach((part, i) => { prelim.push(document.createTextNode(part)); if (i < parts.length - 1) { prelim.push(document.createElement("br")); } }); } else { prelim.push(node); } } element.textContent = ""; prelim.forEach(n => element.appendChild(n)); Array.from(element.childNodes).forEach(child => { if (child.nodeType !== Node.TEXT_NODE) return; const text = child.nodeValue; if (!this.tokenRegex || !this.tokenRegex.test(text)) return; this.tokenRegex.lastIndex = 0; const frag = document.createDocumentFragment(); let lastIndex = 0; let match; while ((match = this.tokenRegex.exec(text)) !== null) { const full = match[0]; const token = match[1]; const idx = match.index; if (idx > lastIndex) { frag.appendChild(document.createTextNode(text.slice(lastIndex, idx))); } const ext = this.emotes[token] || "png"; const url = `${this.baseUrl}/emotes/assets/${token}.${ext}`; const img = document.createElement("img"); img.src = url; img.title = full; img.alt = full; img.style.height = "0.75rem"; img.style.verticalAlign = "middle"; img.style.cursor = "pointer"; img.addEventListener("click", () => { navigator.clipboard.writeText(full).catch(err => { console.error("Failed copying emote token:", err); }); }); frag.appendChild(img); lastIndex = idx + full.length; } if (lastIndex < text.length) { frag.appendChild(document.createTextNode(text.slice(lastIndex))); } element.replaceChild(frag, child); }); this._replaceRGBSquaresInElement(element); } _replaceRGBSquaresInElement(element) { for (const node of Array.from(element.childNodes)) { if (node.nodeType !== Node.TEXT_NODE) continue; const text = node.nodeValue; if (!text.includes(RGB_PREFIX)) continue; const frag = document.createDocumentFragment(); const rgbRegex = new RegExp(`${RGB_PREFIX}([\uFF00-\uFFFF]{3})`, "g"); let lastIndex = 0, match; while ((match = rgbRegex.exec(text)) !== null) { const idx = match.index; if (idx > lastIndex) { frag.appendChild(document.createTextNode(text.slice(lastIndex, idx))); } const trio = match[1]; const r = trio.charCodeAt(0) & 0xFF; const g = trio.charCodeAt(1) & 0xFF; const b = trio.charCodeAt(2) & 0xFF; const hex = ((r << 16) | (g << 8) | b).toString(16).padStart(6, "0"); const span = document.createElement("span"); span.style.display = "inline-block"; span.style.width = "0.75rem"; span.style.height = "0.75rem"; span.style.verticalAlign = "middle"; span.style.backgroundColor = `#${hex}`; span.style.cursor = "pointer"; const token = RGB_PREFIX + trio; span.title = `#${hex}`; span.addEventListener("click", () => { navigator.clipboard.writeText(token).catch(err => console.error("Clipboard failed", err)); }); frag.appendChild(span); lastIndex = idx + RGB_PREFIX.length + trio.length; } if (lastIndex < text.length) { frag.appendChild(document.createTextNode(text.slice(lastIndex))); } element.replaceChild(frag, node); } } } const emotesManager = new EmotesManager(GM_info.script.version, BASE_URL); emotesManager.init(); })();