// ==UserScript==
// @name Multiplayer Piano Optimizations [Sounds]
// @namespace https://tampermonkey.net/
// @version 1.1.3
// @description Play sounds when users join, leave, or mention you in Multiplayer Piano
// @author zackiboiz, cheezburger0, ccjit
// @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 injectScript(src) {
return new Promise((resolve, reject) => {
const s = document.createElement("script");
s.src = src;
s.async = false;
s.onload = resolve;
s.onerror = reject;
document.head.appendChild(s);
});
}
await injectScript("https://code.jquery.com/ui/1.12.1/jquery-ui.js");
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
}
});
};
}
const builtin = [
{
NAME: "Default [Zacki]",
MENTION: "https://files.catbox.moe/f5tzag.mp3",
JOIN: "https://files.catbox.moe/t3ztlz.mp3",
LEAVE: "https://files.catbox.moe/kmpz7e.mp3"
},
{
NAME: "PRISM [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: "Win11 [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 [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 [cheezburger0]",
MENTION: "https://file.garden/ZXdl6GMYuz15ftGp/mention.wav",
JOIN: "https://file.garden/ZXdl6GMYuz15ftGp/join.wav",
LEAVE: "https://file.garden/ZXdl6GMYuz15ftGp/leave.wav"
}
];
const defaultName = builtin[0].NAME;
class SoundManager {
constructor(version) {
this.version = version;
this.GAP_MS = 200;
this.volume = 1.0;
this.lastPlayed = {};
this._loadSoundpacks();
const stored = localStorage.getItem("currentSoundpack") || defaultName;
this.currentSoundpack = this.soundpacks[stored] ? stored : defaultName;
this.SOUNDS = this.soundpacks[this.currentSoundpack];
}
_loadSoundpacks() {
let saved = {};
try {
saved = JSON.parse(localStorage.getItem("savedSoundpacks")) || {};
} catch (err) {
console.warn("Invalid savedSoundpacks JSON, reverting to builtin only.", err);
}
this.soundpacks = { ...saved };
builtin.forEach(soundpack => {
this.saveSoundpack(soundpack, true);
});
localStorage.setItem("savedSoundpacks", JSON.stringify(this.soundpacks));
}
setCurrentSoundpack(name) {
if (!this.soundpacks[name]) {
console.warn(`Soundpack "${name}" does not exist.`);
return;
}
this.currentSoundpack = name;
this.SOUNDS = this.soundpacks[name];
localStorage.setItem("currentSoundpack", name);
this._refreshDropdown();
}
saveSoundpack({ NAME, MENTION, JOIN, LEAVE }, loading = false) {
if (!NAME || !MENTION || !JOIN || !LEAVE) {
if (!loading) alert("All fields (NAME, MENTION, JOIN, LEAVE) are required.");
return;
}
for (const [existingName, sp] of Object.entries(this.soundpacks)) {
if (sp.MENTION === MENTION && sp.JOIN === JOIN && sp.LEAVE === LEAVE) {
if (!loading) alert(`This soundpack is identical to "${existingName}".`);
return;
}
}
let unique = NAME;
let counter = 1;
while (this.soundpacks[unique]) {
unique = `${NAME} (${counter++})`;
}
this.soundpacks[unique] = { MENTION, JOIN, LEAVE };
localStorage.setItem("savedSoundpacks", JSON.stringify(this.soundpacks));
if (!loading) alert(`Imported soundpack "${unique}".`);
this._refreshDropdown?.();
}
play(src) {
const now = Date.now();
if (!this.lastPlayed[src] || now - this.lastPlayed[src] >= this.GAP_MS) {
this.lastPlayed[src] = now;
const audio = new Audio(src);
audio.volume = this.volume;
audio.play().catch(() => { });
}
}
_refreshDropdown() {
const select = document.querySelector("#soundpack-select");
if (!select) return;
select.innerHTML = "";
for (const name of Object.keys(this.soundpacks)) {
const opt = document.createElement("option");
opt.value = name;
opt.textContent = name;
if (name === this.currentSoundpack) {
opt.selected = true;
}
select.appendChild(opt);
}
}
}
const soundManager = new SoundManager(GM_info.script.version);
let replyTo = {};
let 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()) {
soundManager.play(soundManager.SOUNDS.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.play(soundManager.SOUNDS.JOIN);
}
users[p._id] = p;
});
MPP.client.on("bye", u => {
soundManager.play(soundManager.SOUNDS.LEAVE);
delete users[u.p];
});
MPP.client.on("c", () => {
MPP.chat.sendPrivate({
name: `[MPP Sounds] v${soundManager.version}`,
color: "#ffaa00",
message: "Sound alerts loaded."
});
});
// check for compatibility with hri mpp-hats
const $btn = $(`<button id="soundpack-btn" class="ugly-button top-button" style="position: fixed; right: 6px; top: ${$('.mpp-hats-button').length ? 84 : 58}px; z-index: 100; padding: 5px;">MPP Sounds</button>`);
$("body").append($btn);
const $modal = $(`
<div id="soundpack-modal" class="dialog" style="height: 240px; margin-top: -120px; display: none;">
<h3>MPP Soundpacks</h3><hr>
<p>
<label>Select soundpack:
<select id="soundpack-select" class="text"></select>
</label>
</p>
<p>
<label>Import from JSON:
<input type="file" id="soundpack-file" accept=".json"/>
</label>
</p>
<p>
<label>Reset Soundpacks:
<button id="reset-soundpacks">Reset to default</button>
</label>
</p>
<p><button id="soundpack-submit" class="submit">OK</button></p>
</div>
`);
$("#modal #modals").append($modal);
function hideAllModals() {
$("#modal #modals > *").hide();
$("#modal").hide();
}
function showModal() {
if (MPP.chat) MPP.chat.blur();
hideAllModals();
soundManager._refreshDropdown();
$("#modal").fadeIn(250);
$modal.show();
}
$btn.on("click", showModal);
$("#soundpack-file").on("change", function() {
const file = this.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
try {
const data = JSON.parse(e.target.result);
soundManager.saveSoundpack(data);
} catch {
alert("Invalid JSON file.");
} finally {
this.value = "";
}
};
reader.onerror = () => alert("Failed to read file.");
reader.readAsText(file);
});
$("#soundpack-submit").on("click", () => {
const sel = $("#soundpack-select").val();
soundManager.setCurrentSoundpack(sel);
hideAllModals();
});
$("#reset-soundpacks").on("click", () => {
if (confirm("Are you sure you want to reset your soundpacks?")) {
soundManager.soundpacks = {};
builtin.forEach(soundpack => {
soundManager.saveSoundpack(soundpack, true);
});
localStorage.setItem("savedSoundpacks", JSON.stringify(soundManager.soundpacks));
alert("Successfully reset your soundpacks!");
soundManager.setCurrentSoundpack(defaultName);
}
});
})();