您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Play sounds when users join, leave, or mention you in Multiplayer Piano
// ==UserScript== // @name Multiplayer Piano Optimizations [Sounds] // @namespace https://tampermonkey.net/ // @version 1.7.1 // @description Play sounds when users join, leave, or mention you in Multiplayer Piano // @author zackiboiz, cheezburger0, ccjit // @match *://multiplayerpiano.com/* // @match *://multiplayerpiano.net/* // @match *://dev.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/* // @match *://mpp.smp-meow.net/* // @match *://piano.ourworldofpixels.com/* // @match *://mpp.lapishusky.dev/* // @icon https://www.google.com/s2/favicons?sz=64&domain=multiplayerpiano.net // @grant GM_info // @license MIT // ==/UserScript== (async () => { const dl = GM_info.script.downloadURL || GM_info.script.updateURL || GM_info.script.homepageURL || ""; const match = dl.match(/greasyfork\.org\/scripts\/(\d+)/); if (!match) { console.warn("Could not find Greasy Fork script ID in downloadURL/updateURL/homepageURL:", dl); } else { const scriptId = match[1]; const localVersion = GM_info.script.version; const apiUrl = `https://greasyfork.org/scripts/${scriptId}.json`; fetch(apiUrl, { mode: "cors", headers: { Accept: "application/json" } }).then(r => { if (!r.ok) throw new Error("Failed to fetch Greasy Fork data."); return r.json(); }).then(data => { const remoteVersion = data.version; if (compareVersions(localVersion, remoteVersion) < 0) { new MPP.Notification({ "m": "notification", "duration": 15000, "title": "Update Available", "html": "<p>A new version of this script is available!</p>" + `<p style='margin-top: 10px;'>Script: ${GM_info.script.name}</p>` + `<p>Local: v${localVersion}</p>` + `<p>Latest: v${remoteVersion}</p>` + `<a href='https://greasyfork.org/scripts/${scriptId}' target='_blank' style='position: absolute; right: 0;bottom: 0; margin: 10px; font-size: 0.5rem;'>Open Greasy Fork to update?</a>` }) } }).catch(err => console.error("Update check failed:", err)); } function compareVersions(a, b) { const pa = a.split(".").map(n => parseInt(n, 10) || 0); const pb = b.split(".").map(n => parseInt(n, 10) || 0); const len = Math.max(pa.length, pb.length); for (let i = 0; i < len; i++) { if ((pa[i] || 0) < (pb[i] || 0)) return -1; if ((pa[i] || 0) > (pb[i] || 0)) return 1; } return 0; } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } await sleep(1000); const builtin = [ { NAME: "Default", AUTHOR: "Zacki", MENTION: "https://files.catbox.moe/f5tzag.mp3", JOIN: "https://files.catbox.moe/t3ztlz.mp3", LEAVE: "https://files.catbox.moe/kmpz7e.mp3" }, { NAME: "PRISM", AUTHOR: "ccjit", MENTION: "https://file.garden/aHFDYCLeNBLSceNi/Swoosh.wav", JOIN: "https://file.garden/aHFDYCLeNBLSceNi/Plug%20In.wav", LEAVE: "https://file.garden/aHFDYCLeNBLSceNi/Plug%20Out.wav" }, { NAME: "Win7", AUTHOR: "cheezburger0", MENTION: "https://file.garden/ZXdl6GMYuz15ftGp/Windows%20Notify.wav", JOIN: "https://file.garden/ZXdl6GMYuz15ftGp/Windows%20Logon%20Sound.wav", LEAVE: "https://file.garden/ZXdl6GMYuz15ftGp/Windows%20Recycle.wav" }, { NAME: "Win11", AUTHOR: "ccjit", MENTION: "https://file.garden/aHFDYCLeNBLSceNi/Win11%20Notify.wav", JOIN: "https://file.garden/aHFDYCLeNBLSceNi/Win11%20Plug%20In.wav", LEAVE: "https://file.garden/aHFDYCLeNBLSceNi/Win11%20Plug%20Out.wav" }, { NAME: "Discord", AUTHOR: "ccjit", MENTION: "https://file.garden/aHFDYCLeNBLSceNi/Discord%20Ping.mp3", JOIN: "https://file.garden/aHFDYCLeNBLSceNi/Discord%20Join.mp3", LEAVE: "https://file.garden/aHFDYCLeNBLSceNi/Discord%20Leave.mp3" }, { NAME: "Xylo", AUTHOR: "cheezburger0", MENTION: "https://file.garden/ZXdl6GMYuz15ftGp/mention.wav", JOIN: "https://file.garden/ZXdl6GMYuz15ftGp/join.wav", LEAVE: "https://file.garden/ZXdl6GMYuz15ftGp/leave.wav" }, { NAME: "Skype 1", AUTHOR: "ccjit", MENTION: "https://file.garden/aHFDYCLeNBLSceNi/SKYPE_IM_ACC_MENTION.flac", JOIN: "https://file.garden/aHFDYCLeNBLSceNi/SKYPE_USER_ADDED.flac", LEAVE: "https://file.garden/aHFDYCLeNBLSceNi/SKYPE_USER_LEFT.flac" }, { NAME: "Piano", AUTHOR: "cheezburger0", MENTION: "https://file.garden/ZXdl6GMYuz15ftGp/mention-2.wav", JOIN: "https://file.garden/ZXdl6GMYuz15ftGp/join-2.wav", LEAVE: "https://file.garden/ZXdl6GMYuz15ftGp/leave-2.wav" } ]; const defaultName = builtin[0].NAME; class SoundManager { constructor(version) { this.version = version; this.GAP_MS = 200; this.soundTypes = []; this.volumes = {}; this.lastPlayed = {}; this.audioCache = {}; this._loadSoundpacks(); const sample = Object.values(this.soundpacks)[0] || {}; this.soundTypes = Object.keys(sample).filter(k => !["NAME", "AUTHOR"].includes(k)); this.savedVolumes = JSON.parse(localStorage.savedVolumes || "{}"); this._loadVolumesForPack(); const stored = localStorage.currentSoundpack; this.currentSoundpack = (stored && this.soundpacks[stored]) ? stored : ""; this.SOUNDS = this.soundpacks[this.currentSoundpack] || {}; this._loadVolumesForPack(); this._loadAssetsForCurrentPack(); this.soundTypes.forEach(type => { const $i = $(`#vol-${type}`); if ($i.length) { const current = Math.round(this.volumes[type] * 100); $i.val(current); } }); } _loadVolumesForPack() { const pack = localStorage.currentSoundpack; this.volumes = this.savedVolumes[pack] || {}; this.soundTypes.forEach(t => { if (this.volumes[t] == null) this.volumes[t] = 1.0; }); } _saveVolumesForPack() { const pack = localStorage.currentSoundpack; this.savedVolumes[pack] = this.volumes; localStorage.savedVolumes = JSON.stringify(this.savedVolumes); } setVolumeForType(type, volume) { if (!this.soundTypes.includes(type)) return; this.volumes[type] = volume; this._saveVolumesForPack(); const src = this.SOUNDS[type]; if (this.audioCache[src]) this.audioCache[src].volume = volume; } _loadSoundpacks() { let saved = {}; let shouldReset = false; try { saved = JSON.parse(localStorage.savedSoundpacks) || {}; } catch { console.warn("Invalid savedSoundpacks JSON. Resetting saved list."); saved = {}; shouldReset = true; } this.soundpacks = { ...saved }; const didInit = localStorage.initializedSoundpacks === "true"; if ((!didInit && Object.keys(this.soundpacks).length === 0) || shouldReset) { builtin.forEach(sp => this.saveSoundpack(sp, true)); localStorage.initializedSoundpacks = "true"; } localStorage.savedSoundpacks = JSON.stringify(this.soundpacks); } setCurrentSoundpack(name) { if (name && !this.soundpacks[name]) { console.warn(`Soundpack "${name}" does not exist.`); return; } this.currentSoundpack = name; localStorage.currentSoundpack = name; this.SOUNDS = this.soundpacks[name] || {}; this._loadVolumesForPack(); this.soundTypes.forEach(type => { const $i = $(`#vol-${type}`); if ($i.length) { const current = Math.round(this.volumes[type] * 100); $i.val(current); $(`#vol-percent-${type}`).text(current); } }); this._refreshDropdown(); this._loadAssetsForCurrentPack(); } saveSoundpack(obj, loading = false) { const { NAME, AUTHOR, MENTION, JOIN, LEAVE } = obj; if (!NAME || !AUTHOR || !MENTION || !JOIN || !LEAVE) { if (!loading) alert("All fields (NAME, AUTHOR, MENTION, JOIN, LEAVE) are required."); return; } for (const [k, sp] of Object.entries(this.soundpacks)) { if (sp.MENTION === MENTION && sp.JOIN === JOIN && sp.LEAVE === LEAVE) { if (!loading) alert(`Imported soundpack "${NAME}" is identical to soundpack "${k}".`); return; } } let unique = NAME, i = 1; while (this.soundpacks[unique]) unique = `${NAME} (${i++})`; this.soundpacks[unique] = { NAME: unique, AUTHOR, MENTION, JOIN, LEAVE }; localStorage.savedSoundpacks = JSON.stringify(this.soundpacks); if (!loading) alert(`Imported soundpack "${unique}".`); this._refreshDropdown(); } deleteSoundpack(name) { if (!this.soundpacks[name]) return; const keys = Object.keys(this.soundpacks); if (keys.length <= 1) { if (!confirm("This is your last soundpack. Deleting it will leave you with no sounds at all. Are you sure?")) { return; } } delete this.soundpacks[name]; localStorage.savedSoundpacks = JSON.stringify(this.soundpacks); const remain = Object.keys(this.soundpacks); const next = remain.length ? remain[0] : ""; this.setCurrentSoundpack(next); } _loadAssetsForCurrentPack() { this.audioCache = {}; this.soundTypes.forEach(key => { const base = this.SOUNDS[key]; if (!base) return; const sep = base.includes("?") ? "&" : "?"; const busted = `${base}${sep}_=${Date.now()}`; const a = new Audio(busted); a.preload = "auto"; a.volume = this.volumes[key] || 1.0; this.audioCache[base] = a; }); } playType(type) { const src = this.SOUNDS[type]; if (!src) return; const now = Date.now(); if (!this.lastPlayed[src] || now - this.lastPlayed[src] >= this.GAP_MS) { this.lastPlayed[src] = now; const orig = this.audioCache[src]; if (orig) { const c = orig.cloneNode(); c.volume = this.volumes[type]; c.play().catch(() => { }); } else { new Audio(src).play().catch(() => { }); } } } _refreshDropdown() { const sel = document.querySelector("#soundpack-select"); if (!sel) return; sel.innerHTML = ""; const noneOpt = document.createElement("option"); noneOpt.value = ""; noneOpt.textContent = "No Sounds"; if (!this.currentSoundpack) noneOpt.selected = true; sel.appendChild(noneOpt); for (const [k, sp] of Object.entries(this.soundpacks)) { const o = document.createElement("option"); o.value = k; o.textContent = `${sp.NAME} [${sp.AUTHOR}]`; if (k === this.currentSoundpack) o.selected = true; sel.appendChild(o); } } } const soundManager = new SoundManager(GM_info.script.version); let replyTo = {}, users = {}; function onMessage(msg) { const sender = msg.p ?? msg.sender; replyTo[msg.id] = sender._id; const me = MPP.client.user._id; const mention = msg.a.includes(`@${me}`); const replyMention = msg.r && replyTo[msg.r] === me; if ( (mention || replyMention) && (!document.hasFocus() || MPP.client.getOwnParticipant().afk) && !(localStorage.chatMutes.split(",") ?? []).includes(sender._id) ) { soundManager.playType("MENTION"); } } MPP.client.on("a", onMessage); MPP.client.on("dm", onMessage); MPP.client.on("ch", ch => { users = {}; ch.ppl.forEach(u => (users[u._id] = u)); }); MPP.client.on("p", p => { if (!users[p._id]) soundManager.playType("JOIN"); users[p._id] = p; }); MPP.client.on("bye", u => { soundManager.playType("LEAVE"); delete users[u.p]; }); const topOff = document.getElementsByClassName("mpp-hats-button").length ? 84 : 58; const $btn = $(` <button id="soundpack-btn" class="top-button" style="position: fixed; right: 6px; top: ${topOff}px; z-index: 100; padding: 5px;"> MPP Sounds </button> `); $("body").append($btn); const $modal = $(` <div id="soundpack-modal" class="dialog" style="height: 400px; margin-top: -200px; width: 550px; margin-left: -300px; display: none;"> <header> <h3>MPP Sounds</h3> <hr> </header> <div> <table style="width: 100%; border-collapse: collapse;"> <tr> <td style="vertical-align: top;"> <fieldset style="border: 1px solid #ffffff; width:270px; padding: 0.25em; margin: 0;"> <legend style="font-size: 18px; padding: 0 0.5em; white-space: nowrap;">Select soundpack</legend> <select id="soundpack-select"></select> </fieldset> </td> </tr> <tr> <td style="vertical-align: top;"> <fieldset style="border: 1px solid #ffffff; width:250px; padding: 0.25em; margin: 0;"> <legend style="font-size: 18px; padding: 0 0.5em; white-space: nowrap;">Import from JSON</legend> <input type="file" id="soundpack-file" accept=".json" multiple> </fieldset> </td> </tr> <tr> <td style="vertical-align: top;"> <fieldset style="border: 1px solid #ffffff; width:250px; padding: 0.25em; margin: 0;"> <legend style="font-size: 18px; padding: 0 0.5em; white-space: nowrap;">Manage soundpacks</legend> <button type="button" id="delete-soundpack">Delete current soundpack</button> <button type="button" id="reset-soundpacks">Reset all soundpacks</button> </fieldset> </td> </tr> <tr style="position: relative; left: 300px; top: -247px"> <td style="vertical-align: top;"> <fieldset style="border: 1px solid #ffffff; width:200px; padding: 0.25em; margin: 0;"> <legend style="font-size: 18px; padding: 0 0.5em; white-space: nowrap;">Preview sounds</legend> <button type="button" id="preview-mention">Mention</button> <button type="button" id="preview-join">Join</button> <button type="button" id="preview-leave">Leave</button> </fieldset> </td> </tr> <tr style="position: relative; left: 300px; top: -247px"> <td style="vertical-align: top;"> <fieldset id="volume-sliders" style="border: 1px solid #ffffff; width:20px; padding: 0.25em; margin: 0;"></fieldset> </td> </tr> </table> </div> <p> <button id="soundpack-submit" class="submit">OK</button> </p> <p> <a href="https://github.com/ZackiBoiz/Multiplayer-Piano-Optimizations/tree/main/soundpacks" target="_blank" style="position: absolute; left: 0;bottom: 0; margin: 10px; font-size: 0.5rem;"> Find more soundpacks </a> </p> </div> `); $("#modal #modals").append($modal); function hideAllModals() { $("#modal #modals > *").hide(); $("#modal").hide(); } function showModal() { if (MPP.chat) MPP.chat.blur(); hideAllModals(); soundManager._refreshDropdown(); const $vol = $("#volume-sliders").html(`<legend style="font-size: 18px; padding: 0 0.5em; white-space: nowrap;">Adjust volume</legend>`); soundManager.soundTypes.forEach(type => { const cur = Math.round(soundManager.volumes[type] * 100); $vol.append(` <label> <div class="vol-label" style="font-size: 20px;"> ${type} </div> <div class="vol-slider" style="width: 100px;"> <input type="range" id="vol-${type}" min="0" max="100" value="${cur}" data-type="${type}" style="width: 100%; height: 100%; background: url(/volume2.png) no-repeat; background-position: 50% 50%; box-shadow: none; border: 0;"/> </div> <div class="vol-label" style="position: relative; right: 50px; bottom: 8px; font-size: 10px; color: #ccc; text-align: right;"> Volume: <span id="vol-percent-${type}">${cur}</span>% </div> </label> `); }); $("#modal").fadeIn(250); $modal.show(); } $btn.on("click", showModal); document.getElementById("soundpack-select").addEventListener("change", (event) => { const sel = event.target.value; soundManager.setCurrentSoundpack(sel); }); $(document).on("input", "#volume-sliders input[type=range]", function () { const type = $(this).data("type"); const val = $(this).val(); const vol = val / 100; $(`#vol-percent-${type}`).text(val); soundManager.setVolumeForType(type, vol); }); $("#preview-mention").on("click", () => { soundManager.playType("MENTION"); }); $("#preview-join").on("click", () => { soundManager.playType("JOIN"); }); $("#preview-leave").on("click", () => { soundManager.playType("LEAVE"); }); $("#soundpack-file").on("change", function () { const files = Array.from(this.files); if (!files.length) return; files.forEach((file) => { const reader = new FileReader(); reader.onload = e => { try { const data = JSON.parse(e.target.result); soundManager.saveSoundpack(data); } catch (err) { alert(`Failed to import "${file.name}".`); } }; reader.onerror = () => { alert(`Failed to read file "${file.name}".`); }; reader.readAsText(file); }); this.value = ""; }); $("#soundpack-submit").on("click", () => { const sel = $("#soundpack-select").val(); soundManager.setCurrentSoundpack(sel); hideAllModals(); }); $("#delete-soundpack").on("click", () => { if (!confirm("Are you sure you want to delete this soundpack?")) return; const cur = soundManager.currentSoundpack; if (!cur) { alert("No soundpack selected to delete."); return; } soundManager.deleteSoundpack(cur); }); $("#reset-soundpacks").on("click", () => { if (!confirm("Are you sure you want to reset your soundpacks?")) return; if (!confirm("Are you absolutely sure? This will erase all your custom packs.")) return; if (!confirm("ARE YOU TOTALLY ABSOLUTELY 100% SURE? THIS IS NOT REVERSABLE!")) return; localStorage.savedSoundpacks = "{}"; localStorage.currentSoundpack = defaultName; localStorage.initializedSoundpacks = "false"; soundManager._loadSoundpacks(); soundManager.setCurrentSoundpack(defaultName); alert("Successfully reset your soundpacks!"); }); })();