Smoother Note Quota for MPP
// ==UserScript==
// @name Dynamic NQ
// @namespace seq.wtf
// @match *://mppclone.com/*
// @match *://mpp.8448.space/*
// @match *://multiplayerpiano.net/*
// @match *://multiplayerpiano.org/*
// @match *://beta-mpp.cedms.jp/*
// @match *://beta-mpp.csys64.com/*
// @match *://mpp.terrium.net/*
// @match *://mppfork.netlify.app/*
// @match *://mpp.hri7566.info/*
// @match *://www.multiplayerpiano.dev/*
// @match *://mc.hri7566.info:8080/*
// @match *://mpp176.tk/*
// @match *://piano.ourworldofpixels.com/*
// @match *://mpp.lapishusky.dev/*
// @match *://mpp.hyye.tk/*
// @match *://openmpp.tk/*
// @match *://staging-mpp.sad.ovh/*
// @run-at document-start
// @grant none
// @version 1.0.2
// @author Seq
// @description Smoother Note Quota for MPP
// @license Beerware
// ==/UserScript==
class NoteQuota {
// allowance and maxHistLen are unused but kept for backwards compatibility
static PARAMS_LOBBY = { allowance: 200, max: 600, maxHistLen: 3 };
static PARAMS_NORMAL = { allowance: 400, max: 1200, maxHistLen: 3 };
static PARAMS_RIDICULOUS = { allowance: 600, max: 1800, maxHistLen: 3 };
static PARAMS_OFFLINE = { allowance: 8000, max: 24000, maxHistLen: 3 };
static PARAMS_UNLIMITED = { allowance: 1000000, max: 3000000, maxHistLen: 3 };
static PARAMS_INFINITE = { allowance: Infinity, max: Infinity, maxHistLen: 3 };
#points = 0;
#cachedPoints = 0;
#regenRate = 0; // pts/ms; dynamic based on max
#cacheTimestamp = 0; // 0 = invalid
#lastUpdateTimestamp = 0;
#recalc(timestamp) {
const timeElapsed = timestamp - this.#lastUpdateTimestamp;
const regeneratedPoints = timeElapsed > 0 ? timeElapsed * this.#regenRate : 0;
return Math.min(this.#points + regeneratedPoints, this.max);
}
constructor(cb, params) {
// this.cb = cb; // UPDATES MANUALLY -> updateQuotaBar()
this.max = 0;
this.CACHE_DURATION_MS = 10;
this.setParams(params);
}
get points() {
const now = Date.now();
if (this.#cacheTimestamp > 0 && now - this.#cacheTimestamp < this.CACHE_DURATION_MS) {
return this.#cachedPoints;
}
const currentPoints = this.#recalc(now);
// prime cache
this.#cachedPoints = currentPoints;
this.#cacheTimestamp = now;
return currentPoints;
}
getParams() {
return {
m: "nq",
allowance: this.max / 3,
max: this.max,
maxHistLen: 3
};
}
setParams(params) {
params = params || NoteQuota.PARAMS_OFFLINE;
const max = params.max || this.max || NoteQuota.PARAMS_OFFLINE.max;
if (max !== this.max) {
this.max = max;
this.#regenRate = max / 6000;
this.resetPoints();
return true;
}
return false;
}
resetPoints() {
this.#points = this.max;
this.#lastUpdateTimestamp = Date.now();
// invalidate cache
// priming would also work, but this is rarely called,
// so invalidating is acceptable
this.#cacheTimestamp = 0;
if (this.cb) this.cb(this.points);
}
spend(needed) {
const now = Date.now();
// saves a Date.now() call over just using this.points
const currentPoints = this.#recalc(now);
if (currentPoints < needed) return false;
this.#points = currentPoints - needed;
this.#lastUpdateTimestamp = now;
// prime cache
this.#cachedPoints = this.#points;
this.#cacheTimestamp = now;
if (this.cb) this.cb(this.#points);
return true;
}
// legacy
tick() {
// the MPP client will call this every two seconds
// do nothing
}
get allowance() {
return this.max / 3;
}
get maxHistLen() {
return 3;
}
get history() {
const p = this.points;
return [p, p, p];
}
}
// trap the NoteQuota definition
let patched = false;
Object.defineProperty(window, "NoteQuota", {
configurable: true,
enumerable: true,
get: () => NoteQuota,
set: () => {
patched = true;
delete window.NoteQuota;
window.NoteQuota = NoteQuota;
}
});
// very cursed race condition fallback
// (the MPP object could become initialized before we get the chance to hijack the NoteQuota object)
const fallbackInterval = setInterval(() => {
if (patched || window.MPP?.noteQuota?.CACHE_DURATION_MS) return clearInterval(fallbackInterval); // patched successfully
if (!window.MPP?.noteQuota) return; // not initialized yet
console.warn("[DynamicNQ] Race condition lost! Post-patching...");
const originalInstance = window.MPP.noteQuota;
const customInstance = new NoteQuota(originalInstance.cb);
const customProps = new Set([
...Object.getOwnPropertyNames(customInstance),
...Object.getOwnPropertyNames(NoteQuota.prototype)
]);
for (const prop of customProps) {
if (prop === "constructor") continue;
const descriptor = Object.getOwnPropertyDescriptor(customInstance, prop) ||
Object.getOwnPropertyDescriptor(NoteQuota.prototype, prop);
if (typeof descriptor.value === 'function') {
originalInstance[prop] = descriptor.value.bind(customInstance);
} else {
Object.defineProperty(originalInstance, prop, {
configurable: true,
get: () => customInstance[prop],
set: v => customInstance[prop] = v,
});
}
}
delete originalInstance.cb;
patched = true;
clearInterval(fallbackInterval);
});
let nqBar = null;
(function updateQuotaBar() {
window.requestAnimationFrame(updateQuotaBar);
if (!window.$ || !window.MPP?.noteQuota) return;
if (!nqBar) nqBar = $("#quota .value");
const nq = window.MPP.noteQuota;
nqBar.css("width", `${(nq.points / nq.max) * 100}%`);
})();