Display emoticons in chat!
当前为
// ==UserScript==
// @name Multiplayer Piano Optimizations [Emotes]
// @namespace https://tampermonkey.net/
// @version 1.0.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";
class EmotesManager {
constructor(version, baseUrl) {
this.version = version;
this.baseUrl = baseUrl;
this.emotes = {};
this.tokenRegex = null;
this._init();
}
async _init() {
try {
await this._loadEmotes();
this._buildTokenRegex();
this._initChatObserver();
MPP.chat.sendPrivate({
name: `[MPP Emotes] v${this.version}`,
color: "#ffaa00",
message: "Emoticons loaded.",
});
} catch (err) {
MPP.chat.sendPrivate({
name: `[MPP Emotes] v${this.version}`,
color: "#ff0000",
message: "EmotesManager initialization failed. Check console for details",
});
console.error("EmotesManager initialization failed:", err);
}
}
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
});
}
_replaceEmotesInElement(element) {
for (const child of Array.from(element.childNodes)) {
if (child.nodeType === Node.TEXT_NODE) {
const text = child.nodeValue;
if (!this.tokenRegex.test(text)) continue;
this.tokenRegex.lastIndex = 0;
const frag = document.createDocumentFragment();
let lastIndex = 0;
let match;
while ((match = this.tokenRegex.exec(text)) !== null) {
const fullMatch = 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 = fullMatch;
img.style.height = "0.75rem";
img.style.verticalAlign = "middle";
img.style.margin = "0 0.1rem";
frag.appendChild(img);
lastIndex = idx + fullMatch.length;
}
if (lastIndex < text.length) {
frag.appendChild(document.createTextNode(text.slice(lastIndex)));
}
element.replaceChild(frag, child);
}
}
}
}
const emotesManager = new EmotesManager(GM_info.script.version, BASE_URL);
MPP.client.on("hi", () => {
if (!MPP.chat.sendPrivate) {
MPP.chat.sendPrivate = ({ name, color, message }) => {
MPP.chat.receive({
m: "a",
t: Date.now(),
a: message,
p: { _id: "usrscr", id: "userscript", name, color },
});
};
}
});
MPP.client.on("c", () => {
if (MPP.chat.sendPrivate) {
MPP.chat.sendPrivate({
name: `[MPP Emotes] v${emotesManager.version}`,
color: "#ffaa00",
message: "Ready to catch emotes.",
});
}
});
})();