Bonk NSFW map filter

Blocks NSFW bonk maps

当前为 2025-04-15 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Bonk NSFW map filter
// @namespace   salama.xyz
// @author      Salama
// @version     1.8
// @match       https://*.bonk.io/gameframe-release.html
// @match       https://*.bonkisback.io/gameframe-release.html
// @supportURL  https://discord.gg/Dj6usq7ww3
// @grant       none
// @description Blocks NSFW bonk maps
// @run-at document-end
// ==/UserScript==

////////////////////////
////    SETTINGS    ////
////////////////////////

/* Optionally depends on code injector and BonkLIB
 * ----------------------------------------------------------------
 * https://greasyfork.org/en/scripts/433861-code-injector-bonk-io
 * - Adds warning to map suggestions
 * - Blurs or blacks out map in lobby and in game
 * ----------------------------------------------------------------
 * https://greasyfork.org/en/scripts/508104-bonklib
 * - Adds GUI settings
 * ----------------------------------------------------------------
 */

// This script connects to GitHub to get an up to date list.

// Commas in front for ease of use
// true = yes
// false = no

const settings = {
	 HIDE_NSFW_REPLAYS: true
	,DISABLE_REPLAYS: false

	// If false, maps will be blurred instead
	,HIDE_MAPS_FROM_MAP_SELECTOR: true

	// If true, map details won't be blurred
	,BLUR_ONLY_MAP_PREVIEW: false

	,UNBLUR_MAP_ON_MOUSE_HOVER: false

	,INCLUDE_REMIXES_OF_NSFW_MAPS: true

	/* You can enable this in case injection fails due to a
	 * bonk update. The mod will still work for its main purpose
	 */
	,DISABLE_INJECTION_FAIL_WARNING: false

	,WARN_ABOUT_MAP_REQUESTS: true

	,HIDE_GAME_ON_NSFW: true

	// Blocklist is cached for 4 * 60 sec = 4 minutes
	,CACHE_DURATION: 4 * 60

	,AUTHOR_BLOCKLIST: []
}

const guiSettings = {
    noWindow: true,
    // Must be defined before bonkHUD.createMod
    settingsContent: null,
    // Version (optional)
    bonkLIBVersion: "1.1.3",
    modVersion: "1.8",

}


////////////////////////
///       CODE      ////
////////////////////////

'use strict';

const NSFWLIST_VERSION = 2;

const global = {
	cacheTime: 0,
	NSFWList: [],
	NSFWMaps: new Set(),
	replays: [],
	ignoreNextReport: false,
	injected: false
};

function addToNSFWList(id, author = "fi", name = "fi") {
	if (global.injected) {
		global.NSFWMaps.add([id.toString(), author, name].join("ff"));
	}
	else {
		global.NSFWMaps.add(id.toString());
	}
}

function getFromNSFWList(id, author = "fi", name = "fi") {
	if (global.injected) {
		return global.NSFWMaps.has([id.toString(), author, name].join("ff"));
	}
	else {
		return global.NSFWMaps.has(id.toString());
	}
}

function requestHandler(original) {
	return function(url,body,success,type) {
		if (global.ignoreNextReport &&
			url.endsWith("/replay_report.php")
		) {
			global.ignoreNextReport = false;
			return {
				done: () => {
					return {
						fail: () => {}
					}
				}
			}
		}

		if (settings.DISABLE_REPLAYS &&
			url.endsWith("/replay_get.php")
		) {
			return {
				done: () => {
					return {
						fail: () => {}
					}
				}
			}
		}


		// Send request
		const response = original.apply(this, arguments);

		// Hijack response callback
		const responseDone = response.done;
		response.done = function(responseCallback) {
			/* The originally synchronous responseCallback can
			 * be replaced with an asynchronous function, because
			 * its return value is never saved or used anywhere.
			 */
			const originalResponseCallback = responseCallback;
			responseCallback = async function(data, status) {
				// Data is sometimes string and sometimes JSON

				let wasParsed = false;

				if (typeof data === "string") {
					try {
						let parsed = JSON.parse(data);
						wasParsed = true;

						data = parsed;
					}
					catch {
						wasParsed = false;
					}
				}

				if (typeof data === "object") {
					// If the request response contains a map array
					if (Object.keys(data).includes("maps")) {
						// Bonk 2 maps
						if (typeof data.maps === "object") {
							const NSFWList = await getNSFWList();

							if (/^G/.test(read(pretty_top_level)) ||
								await isOK([read(pretty_top_name)]) ||
								pushOK(read(pretty_top_name))
							) {
								data.maps.ok = true;
							}

							for (let i = 0; i < data.maps.length; i++) {
								const map = data.maps[i];

								const hash = await getHash(map.id.toString(), map.authorname, map.name);

								if (isNSFW(NSFWList, hash, map.authorname)) {
									addToNSFWList(map.id, map.authorname, map.name);
								}
								else if (settings.INCLUDE_REMIXES_OF_NSFW_MAPS) {
									if (map.remixid > 0) {
										const rxhash = await getHash(map.remixid.toString(), map.remixauthor, map.remixname);

										if (isNSFW(NSFWList, rxhash, map.remixauthor)) {
											addToNSFWList(map.id, map.authorname, map.name);
										}
									}
								}
							}
						}
						// Bonk 1 maps
						else if (typeof data.maps === "string") {
							const NSFWList = await getNSFWList();

							let maps = [];
							data.maps.split("&").forEach(m => {
								if(!m) return;
								let [prop, value, index, _] = m.split("=");

								if (prop === "cant") return;

								[_, prop, index] = [...prop.match("(.+?)([0-9]+)")];

								if (prop === "mapname") {
									value = decodeURIComponent(value.replaceAll('+', '%20'));
								}

								maps[index] = maps[index] || {};
								maps[index][prop] = value;
							});

							if (/^G/.test(read(pretty_top_level)) ||
								await isOK([read(pretty_top_name)]) ||
								pushOK(read(pretty_top_name))
							) {
								maps.ok = true;
							}

							for (let i = 0; i < maps.length; i++) {
								const map = maps[i];

								const hash = await getHash(map.mapid, map.authorname, map.mapname);

								if (isNSFW(NSFWList, hash, map.authorname)) {
									addToNSFWList(map.mapid, map.authorname, map.mapname);
								}
							}
						}
					}
					else if (Object.keys(data).includes("replays")) {
						// Make sure the list has loaded before we allow replays pass through
						await getNSFWList();
						if (body.offset === 0) {
							global.replays = [];
						}
						global.replays = global.replays.concat(data.replays);
					}
				}

				if (wasParsed) {
					data = JSON.stringify(data);
				}

				// Call original response callback
				return originalResponseCallback.apply(this, arguments);
			}

			// Set our own function as the response callback
			return responseDone.call(this, responseCallback);
		}

		return response;
	}
}

async function isOK(map) {
	return new Promise(async resolve => {
		resolve(getOK().includes(
			await sha256(map[Object.keys(map).sort((a, b) => a.localeCompare(b))[0]])
		));
	});
}

function isNSFW(list, hash, author) {
	return list.includes(hash) || (author && settings.AUTHOR_BLOCKLIST.includes(author));
}

async function getNSFWList() {
	return new Promise(resolve => {
		if (Date.now() - global.cacheTime > settings.CACHE_DURATION * 1000) {
			global.cacheTime = Date.now();

			window.$.get("https://gist.githubusercontent.com/Salama/c93f26e0468aa743453339c8c993adaa/raw?" + Date.now()).done(r => {
				let NSFWList = r.split("\n");
				let version = parseInt(NSFWList.splice(0, 1));

				if (version > NSFWLIST_VERSION) {
					alert("NSFW map blocker is outdated!");

					// Prevent future requests
					global.cacheTime = Infinity;
					resolve([]);
					return;
				}
				global.NSFWList = NSFWList;
			});
		}

		resolve(global.NSFWList);
	});
}

async function sha256(text) {
	const encoder = new TextEncoder();
	const data = encoder.encode(text);
	const hashBuffer = await window.crypto.subtle.digest("SHA-256", data);
	const hashArray = Array.from(new Uint8Array(hashBuffer));
	const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
	return hashHex;
}

async function pushOK(props) {
	let current = getOK();
	current.push(await sha256(props));
	return window.localStorage.setItem("nsfwok", current.join(""));
}

function getOK() {
	let ok = window.localStorage.getItem("nsfwok");
	if (!ok) {
		window.localStorage.setItem("nsfwok", "");
		return [];
	}
	return [...ok.match(/.{64}/g)];
}

function read(e) {
	return e.textContent;
}

async function getHash(first, second, third, fourth = isOK([second])) {
	return sha256(
		["zV6IqJQDncyqGv7t2efz︁1jQQyOpRwO︀", await sha256(first), await sha256(second), await sha256(third), await fourth].join("᠎")
	);
}

function addBlurStyle() {
	let blurStyle = document.createElement("style");
	blurStyle.innerHTML = `
		/* Blur map preview in map selector */
		.blurNSFW > .maploadwindowtextname {
			filter: blur(6px);
		}
		.blurNSFW > .maploadwindowtextauthor {
			filter: blur(4px);
		}
		.blurNSFW > img {
			filter: blur(12px);
			overflow: hidden;
		}
		/* Blur map preview in vote */
		.blurNSFW > #newbonklobby_votewindow_maptitle {
			filter: blur(6px);
		}
		.blurNSFW > #newbonklobby_votewindow_mapauthor {
			filter: blur(6px);
		}

		/* Unblur map preview in map selector on mouse hover */
		.hoverUnblurNSFW:hover > .maploadwindowtextname {
			filter: unset !important;
		}
		.hoverUnblurNSFW:hover > .maploadwindowtextauthor {
			filter: unset !important;
		}
		.hoverUnblurNSFW:hover > img {
			filter: unset !important;
		}

		/* Blur map preview in lobby */
		.blurNSFW > #newbonklobby_maptext {
			filter: blur(6px);
		}
		.blurNSFW > #newbonklobby_mapauthortext {
			filter: blur(4px);
		}
		.blurNSFW > #newbonklobby_mappreviewcontainer {
			filter: blur(12px);
		}

		/* Unblur map preview in lobby on mouse hover */
		.hoverUnblurNSFW > #newbonklobby_maptext:hover {
			filter: unset !important;
		}
		.hoverUnblurNSFW > #newbonklobby_mapauthortext:hover {
			filter: unset !important;
		}
		.hoverUnblurNSFW > #newbonklobby_mappreviewcontainer:hover {
			filter: unset !important;
		}

		/*Disable blurred map background in lobby */
		.disableMapthumbBig > #newbonklobby_mapthumb_big {
			display: none !important;
		}

		/* Disable map */
		.fullyTransparent {
			opacity: 0 !important;
		}
	`;
	document.head.appendChild(blurStyle);
}

(async () => {
	addBlurStyle();

	// Hijack requests
	const jqGet = requestHandler(window.$.get);
	const jqPost = requestHandler(window.$.post);
	window.$.get = jqGet;
	window.$.post = jqPost;

	getNSFWList();

	const mapObserver = new MutationObserver(mutations => {
		// Don't handle bonk 1 maps for now...
		// Using this due to uncertainty of dbv accuracy
		//if (read(document.getElementById("maploadtypedropdowntitle")).includes("BONK 1")) return;

		for (const mutation of mutations) {
			for (const node of mutation.addedNodes) {
				if (getFromNSFWList(node.map.m.dbid, node.map.m.a, node.map.m.n)) {
					if (settings.HIDE_MAPS_FROM_MAP_SELECTOR) {
						node.remove();
					}
					else {
						node.classList.add("blurNSFW");
						if (settings.BLUR_ONLY_MAP_PREVIEW) {
							node.getElementsByClassName("maploadwindowtextname")[0].style.filter = "unset";
							node.getElementsByClassName("maploadwindowtextauthor")[0].style.filter = "unset";
						}
						if (settings.UNBLUR_MAP_ON_MOUSE_HOVER) {
							node.classList.add("hoverUnblurNSFW");
						}
					}
				}
			}
		}
	});
	mapObserver.observe(document.getElementById("maploadwindowmapscontainer"), {childList: true});

	// Replay section

	if (settings.DISABLE_REPLAYS) {
		document.getElementById("bgreplay").style.display = "none";
	}

	// TODO fix replayIndex drifting when spamming next replay.
	// Possibly fixed now?
	let replayIndex = -1;

	let ignoreReplayChange = false;
	const replayObserver = new MutationObserver(async mutations => {
		if (settings.DISABLE_REPLAYS || !settings.HIDE_NSFW_REPLAYS) return;

		let author = "";
		let name = "";
		for (const mutation of mutations) {
			if (mutation.type === "childList") {
				author = read(mutation.target.children[2]);
				name = read(mutation.target.children[0]);
			}
			else if (mutation.type === "attributes") {
				/* We need to override the visibility status to another visibility type
				 * to prevent bonk from periodically updating replay credits, which
				 * messes up replayIndex
				 */
				if (mutation.target.style.visibility === "inherit") {
					mutation.target.style.visibility = "visible";
				}
			}
		}

		if (!author && !name) return;

		if (!ignoreReplayChange) {
			replayIndex++;
		}
		ignoreReplayChange = false;


		if (!global.replays[replayIndex]) {
			return;
		}

		const NSFWList = await getNSFWList();

		const hash = await getHash(global.replays[replayIndex].mapid.toString(), author, name);

		if (isNSFW(NSFWList, hash, author) ||
			getFromNSFWList(global.replays[replayIndex].mapid)
		) {
			addToNSFWList(global.replays[replayIndex].mapid);
			global.ignoreNextReport = true;
			document.getElementById("pretty_top_replay_report").click();
		}
	});
	replayObserver.observe(document.getElementById("pretty_top_replay_text"), {childList: true, attributes: true});

	document.getElementById("pretty_top_replay_back").addEventListener("click", () => {
		replayIndex--;
		replayIndex = Math.max(replayIndex, 0);
		ignoreReplayChange = true;
	}, true);

	document.getElementById("pretty_top_replay_next").addEventListener("click", () => {
		replayIndex++;
		replayIndex = Math.min(replayIndex, global.replays.length - 1);
		ignoreReplayChange = true;
	}, true);

	document.getElementById("pretty_top_replay_report").addEventListener("click", () => {
		ignoreReplayChange = true;
		global.replays.splice(replayIndex, 1);
	});
})();

window.NSFWFilter = {
	wrap: () => {
		const gameLoadedWaiter = setInterval(async() => {
			if (
				window.NSFWFilter.menuFunctions !== undefined &&
				Object.keys(window.NSFWFilter.menuFunctions).length >= 27) {
				clearInterval(gameLoadedWaiter);
			}
			else return;

			for (const i of Object.keys(window.NSFWFilter.menuFunctions)) {
				if (typeof window.NSFWFilter.menuFunctions[i] !== "function") continue;
				const ogFunc = window.NSFWFilter.menuFunctions[i];

				window.NSFWFilter.menuFunctions[i] = function() {
					switch (i) {
						case "recvMapSuggest":
							if(!settings.WARN_ABOUT_MAP_REQUESTS) break;

							const suggestion = arguments[0];
							getHash(suggestion.m.dbid.toString(), suggestion.m.a, suggestion.m.n).then(async hash => {
								const NSFWList = await getNSFWList();

								if (isNSFW(NSFWList, hash, suggestion.m.a)) {
									addToNSFWList(suggestion.m.dbid, suggestion.m.a, suggestion.m.n);
								}
								else if (settings.INCLUDE_REMIXES_OF_NSFW_MAPS) {
									if (suggestion.m.rxid > 0) {
										const rxhash = await getHash(suggestion.m.rxid.toString(), suggestion.m.rxa, suggestion.m.rxn);

										if (isNSFW(NSFWList, rxhash, suggestion.m.rxa)) {
											addToNSFWList(suggestion.m.dbid, suggestion.m.a, suggestion.m.n);
										}
									}
								}
								if (getFromNSFWList(suggestion.m.dbid, suggestion.m.a, suggestion.m.n)) {
									window.NSFWFilter.menuFunctions.showStatusMessage("* NSFW map request", "#ff0000", false);
								}
							});
							break;
						case "setGameSettings":
							handleLobbyMap(arguments[0].map.m);
							break;
					}
					let response = ogFunc.apply(window.NSFWFilter.menuFunctions, arguments);
					return response;
				}
			}

			for (const i of Object.keys(window.NSFWFilter.toolFunctions.networkEngine)) {
				if (typeof window.NSFWFilter.toolFunctions.networkEngine[i] !== "function") continue;
				const ogFunc = window.NSFWFilter.toolFunctions.networkEngine[i];

				window.NSFWFilter.toolFunctions.networkEngine[i] = function() {
					switch (i) {
						case "sendMapAdd":
							unblurLobby();
							break;
					}
					let response = ogFunc.apply(window.NSFWFilter.toolFunctions.networkEngine, arguments);
					return response;
				}
			}

			window.NSFWFilter.toolFunctions.networkEngine.on("mapAdd", async map => {
				await handleLobbyMap(map.m);
			});
		}, 50);
	},
	checkReplay: async map => {
		if (!settings.INCLUDE_REMIXES_OF_NSFW_MAPS) return;

		const NSFWList = await getNSFWList();

		const hash = await getHash(map.dbid.toString(), map.a, map.n);
		const rxhash = await getHash(map.rxid.toString(), map.rxa, map.rxn);

		if (isNSFW(NSFWList, hash, map.a) || isNSFW(NSFWList, rxhash, map.rxa)) {
			// No author or name
			//addToNSFWList(map.dbid, map.a, map.n);
			addToNSFWList(map.dbid);
		}
	}
}

function blurLobby() {
	if (settings.HIDE_GAME_ON_NSFW) {
		document.getElementById("newbonkgamecontainer").classList.add("disableMapthumbBig");
		document.getElementById("gamerenderer").classList.add("fullyTransparent");
	}

	if (settings.HIDE_MAPS_FROM_MAP_SELECTOR) {
		document.getElementById("newbonklobby_mappreviewcontainer").style.display = "none";
	}
	else {
		document.getElementById("newbonklobby_settingsbox").classList.add("blurNSFW");
		document.getElementById("newbonklobby_votewindow_thumbcontainer").classList.add("blurNSFW");
		document.getElementById("newbonklobby_votewindow").classList.add("blurNSFW");
		if (settings.UNBLUR_MAP_ON_MOUSE_HOVER) {
			document.getElementById("newbonklobby_settingsbox").classList.add("hoverUnblurNSFW");
		}
		if (settings.BLUR_ONLY_MAP_PREVIEW) {
			document.getElementById("newbonklobby_maptext").style.filter = "";
			document.getElementById("newbonklobby_mapauthortext").style.filter = "";
			document.getElementById("newbonklobby_votewindow_maptitle").style.filter = "";
			document.getElementById("newbonklobby_votewindow_mapauthor").style.filter = "";
		}
	}
}

function unblurLobby() {
	document.getElementById("newbonkgamecontainer").classList.remove("disableMapthumbBig");
	document.getElementById("newbonklobby_settingsbox").classList.remove("blurNSFW", "hoverUnblurNSFW");

	document.getElementById("newbonklobby_votewindow_thumbcontainer").classList.remove("blurNSFW");
	document.getElementById("newbonklobby_votewindow").classList.remove("blurNSFW");

	document.getElementById("gamerenderer").classList.remove("fullyTransparent");
	document.getElementById("newbonklobby_mappreviewcontainer").style.display = "";
	document.getElementById("newbonklobby_maptext").style.filter = "";
	document.getElementById("newbonklobby_mapauthortext").style.filter = "";
}

async function handleLobbyMap(map) {
	const NSFWList = await getNSFWList();

	const hash = await getHash(map.dbid.toString(), map.a, map.n);

	if (isNSFW(NSFWList, hash, map.a)) {
		addToNSFWList(map.dbid, map.a, map.n);
	}
	else if (settings.INCLUDE_REMIXES_OF_NSFW_MAPS) {
		if (map.rxid > 0) {
			const rxhash = await getHash(map.rxid.toString(), map.rxa, map.rxn);

			if (isNSFW(NSFWList, rxhash, map.rxa)) {
				addToNSFWList(map.dbid, map.a, map.n);
			}
		}
	}

	if (getFromNSFWList(map.dbid, map.a, map.n)) {
		blurLobby();
	}
	else {
		unblurLobby();
	}
}

function injector(str) {
	let newStr = str;

	const menuRegex = newStr.match(/== 13\){...\(\);}}/)[0];
	newStr = newStr.replace(menuRegex, menuRegex + "window.NSFWFilter.menuFunctions = this; window.NSFWFilter.wrap();");
	const toolRegex = newStr.match(/=new [A-Za-z0-9\$_]{1,3}\(this,[A-Za-z0-9\$_]{1,3}\[0\]\[0\],[A-Za-z0-9\$_]{1,3}\[0\]\[1\]\);/);
	newStr = newStr.replace(toolRegex, toolRegex + "window.NSFWFilter.toolFunctions = this;");
	const replayRegex = newStr.match(/if\(([A-Za-z0-9\$_]{1,3}\[[0-9]+\])[^\)]+? < 5 \|\| [^\)]+? > 30\)/);
	newStr = newStr.replace(replayRegex[0], `{
		window.NSFWFilter.checkReplay(${replayRegex[1]}.startingState.mm);
	}` + replayRegex[0]);

	global.injected = true;

	return newStr;
}

if (!window.bonkCodeInjectors) window.bonkCodeInjectors = [];

window.bonkCodeInjectors.push(bonkCode => {
	try {
		return injector(bonkCode);
	}
	catch (e) {
		if (settings.DISABLE_INJECTION_FAIL_WARNING) return;
		throw e;
	}
});

if (window.bonkHUD) {
	const createLabel = text => {
		const label = document.createElement("label");
		label.classList.add("bonkhud-settings-label");
		label.innerHTML = text;
		label.style.marginRight = "5px";
		label.style.display = "inline-block";
		label.style.verticalAlign = "middle";
		return label;
	}

	const addTextarea = (target, name, variable) => {
		const label = createLabel(name);
		label.style.verticalAlign = "top";

		const input = document.createElement("textarea");
		input.rows = settings[variable].length + 5;
		input.oninput = () => {
			settings[variable] = input.value.split(/[\n,]/).map(a => a.trim());
			window.bonkHUD.saveModSetting(ind, settings);
		}

		input.value = settings[variable].join("\n");

		const container = document.createElement("div");
		container.id = "nsfw_settings_" + variable;
		container.appendChild(label);
		container.appendChild(input);

		target.appendChild(container);
	}

	const addCheckbox = (target, name, variable) => {
		const label = createLabel(name);

		const checkbox = document.createElement("input");
		checkbox.type = "checkbox";
		checkbox.style.display = "inline-block";
		checkbox.style.verticalAlign = "middle";
		checkbox.checked = settings[variable];

		checkbox.oninput = () => {
			settings[variable] = checkbox.checked;
			window.bonkHUD.saveModSetting(ind, settings);
		}

		const container = document.createElement("div");
		container.id = "nsfw_settings_" + variable;
		container.appendChild(label);
		container.appendChild(checkbox);

		target.appendChild(container);
	}

	let nsfwSettings = window.bonkHUD.generateSection();
	guiSettings.settingsContent = nsfwSettings;

	const ind = window.bonkHUD.createMod("NSFW Filter", guiSettings);

	if (window.bonkHUD.getModSetting(ind) != null) {
		const savedSettings = window.bonkHUD.getModSetting(ind);
		for (const setting of Object.keys(savedSettings)) {
			settings[setting] = savedSettings[setting];
		}
	}

	addCheckbox(nsfwSettings, "Hide NSFW replays",                "HIDE_NSFW_REPLAYS");
	addCheckbox(nsfwSettings, "Hide all replays",                 "DISABLE_REPLAYS");
	addCheckbox(nsfwSettings, "Hide NSFW maps from map selector", "HIDE_MAPS_FROM_MAP_SELECTOR");
	addCheckbox(nsfwSettings, "Black out nsfw while in game",     "HIDE_GAME_ON_NSFW");
	addCheckbox(nsfwSettings, "Blur only map preview",            "BLUR_ONLY_MAP_PREVIEW");
	addCheckbox(nsfwSettings, "Unblur on mouse hover",            "UNBLUR_MAP_ON_MOUSE_HOVER");
	addCheckbox(nsfwSettings, "Include remixes",                  "INCLUDE_REMIXES_OF_NSFW_MAPS");
	addCheckbox(nsfwSettings, "Disable injection fail warning",   "DISABLE_INJECTION_FAIL_WARNING");

	addTextarea(nsfwSettings, "Author blocklist<br />(separate users with a comma or a newline)", "AUTHOR_BLOCKLIST")

	const clearCache = document.createElement("button");
	clearCache.textContent = "Clear Cache";
	clearCache.onclick = () => {
		global.cacheTime = 0;
		global.NSFWList = [];
		global.NSFWMaps = new Set();
	}
	nsfwSettings.appendChild(clearCache);

	window.bonkHUD.updateStyleSettings();
}