// ==UserScript==
// @name Game Filter List
// @namespace http://tampermonkey.net/
// @version 2025-02-21
// @description stop showing the same bad games over and over
// @author Anon
// @match https://www.roblox.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=roblox.com
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
// README
// Q: How do I install this?
// A: https://www.tampermonkey.net/
//
// Q: How do I block a game?
// A: Click the block button that appears when you hover your cursor over the game's image.
//
// Q: How do I unblock a game?
// A: Remove it from the "filteredGames" table in the Storage tab of Tampermonkey.
//
// Q: How do I access the 'Storage' tab in Tampermonkey?
// A: Via the "Storage" tab in tampermonkey, which is hidden by default.
// To unhide it, set "Settings>General>Config Mode" to "Advanced"
// It will then appear above the script, beside 'Editor' and 'Settings'.
//
// Q: How do I add a regex filter?
// A: Inside the 'Storage' tab, add regex filters to the "filteredNames" section.
// For instance, to block any game with the word 'outfit' or 'outfits'
// and any game with the word 'rng', add the following to the list:
//
// "filteredNames": [
// "/\boutfits?\b/i",
// "/\brng\b/i"
// ]
//
// Then, click 'Save'.
// In order to save, every line in a list must have a comma, except the last line which must not.
// If you click 'Save' without following this rule, Tampermonkey will show an error popup saying
// "Unable to parse this! :("
// and it will not work.
// CHANGELOG
// 2025-2-12 Script created.
// 2025-2-21 Fixed bug where script did not initialize when installed.
(function() {
'use strict';
// Enable JSON.stringify usage on RegExp, e.g., 'JSON.stringify(/example/i);'
RegExp.prototype.toJSON = RegExp.prototype.toString;
const ENABLE_LOGGING = true;
const ENABLE_INTERVAL = false;
const FILTER_NAME = true;
const FILTER_ID = true;
// Regex that matches the different parts of a game link. 'id' matches the numerical id in the url.
// 'name' matches the name displayed in the url. 'args' matches the text after the ? standard in urls.
const REGEX_GAME = /^https?:\/\/www.roblox.com\/games\/(?<id>\d+)(?:\/(?<name>.*?)(?:\?(?<args>.*))?)?$/;
// Regex that matches the different parts of a regex literal ('/example/i').
// 'regex' matches the regex.
// 'args' matches the letters after
const REGEX_LITERAL = /\/(?<regex>.+)\/(?<args>.*)/
// CSS selector that matches the root of any element showing a game to you in any of the different formats.
const QUERY_TILE = "*:is(.game-tile, .hover-game-tile, *:is(.game-grid, .game-carousel) > .game-card-container:not(.invisible))";
function GM_getOrCreateValue(key, defaultValue) {
const existing = GM_getValue(key);
if (existing !== undefined) return existing;
GM_setValue(key, defaultValue);
return defaultValue;
}
const filteredGames = GM_getOrCreateValue("filteredGames", {});
const filteredNames = GM_getOrCreateValue("filteredNames", []);
function CONSOLE_LOG(...args) {
if (ENABLE_LOGGING) {
return console.log(...args);
}
}
// deserializes a regex literal that is in a string: '/example/i' -> /example/i == new RegExp("example", "i")
function parseRegex(regex) {
const regexResult = regex.match(REGEX_LITERAL);
return new RegExp(regexResult.groups.regex, regexResult.groups.args);
}
function hideAll(id) {
for (const tile of document.body.querySelectorAll(QUERY_TILE)) {
// CONSOLE_LOG(tile);
const tileId = tile.querySelector(".game-card-link").getAttribute("href").match(REGEX_GAME).groups.id;
if (id === tileId) {
tile.style.setProperty("display", "none", "important");
}
}
}
function showAll(id) {
for (const tile of document.body.querySelectorAll(QUERY_TILE)) {
const tileId = tile.querySelector(".game-card-link").getAttribute("href").match(REGEX_GAME).groups.id;
if (id === tileId) {
tile.style.setProperty("display", "");
}
}
}
function addGameToFilterList(id, name) {
if (!filteredGames[id]) {
filteredGames[id] = name;
}
hideAll(id);
GM_setValue("filteredGames", filteredGames);
}
function removeGameFromFilterList(id) {
delete filteredGames[id]
showAll(id);
GM_setValue("filteredGames", filteredGames);
}
function createBlockLink(id, name) {
const link = document.createElement("a")
link.innerText = filteredGames[id] ? "Unblock" : "Block";
link.setAttribute("href", "javascript:;");
link.setAttribute("title", "stop showing me this game");
link.style.setProperty("color", "#7eaaff", "important");
link.style.setProperty("decoration", "underline", "important");
link.style.setProperty("cursor", "pointer", "important");
link.classList.add("block-button");
link.onclick = function() {
if (link.innerText === "Block") {
addGameToFilterList(id, name);
link.innerText = "Unblock"
} else {
removeGameFromFilterList(id);
link.innerText = "Block"
}
}
return link;
}
function createBlockSpan(id, name) {
const blockButton = document.createElement("span")
blockButton.append(" [", createBlockLink(id, name), "]");
return blockButton;
}
function onGameTileAdded(tile) {
// do not process posts we've already hidden because it will just result in us hiding them again
if (tile.style.display === "none") {
return;
}
// do not process posts we've already added buttons to because it will just result in us adding another button to them
let blockButton = tile.querySelector(".block-button");
if (blockButton !== null) {
return;
}
// hide games which have been blocked by the user previously
// CONSOLE_LOG(tile);
const regexResult = tile.querySelector(".game-card-link").getAttribute("href").match(REGEX_GAME);
const id = regexResult.groups.id;
const name = tile.querySelector(".game-card-name").innerText;
if (filteredGames[regexResult.groups.id]) {
tile.style.setProperty("display", "none", "important");
CONSOLE_LOG("game hidden (blocked):", name);
return;
}
// hide games with names matching the user's regex filters
const filters = GM_getValue("filteredNames");
for (let i = 0; i < filters.length; i++) {
const filter = parseRegex(filters[i]);
if (name.match(filter)) {
tile.style.setProperty("display", "none", "important");
CONSOLE_LOG("game hidden (name filter):", name);
return;
}
}
blockButton = createBlockLink(id, name);
blockButton.style.setProperty("position", "absolute", "important");
blockButton.style.setProperty("top", "0px", "important");
blockButton.style.setProperty("left", "4px", "important");
blockButton.style.setProperty("z-index", "1", "important");
blockButton.style.setProperty("opacity", "0", "important");
blockButton.style.setProperty("cursor", "pointer", "important");
// set the properties of the tile
tile.style.setProperty("position", "relative", "important");
tile.onmouseenter = function() {
blockButton.style.setProperty("opacity", "1", "important");
}
tile.onmouseleave = function() {
blockButton.style.setProperty("opacity", "0", "important");
}
// add the block button to the tile
tile.querySelector(".thumbnail-2d-container").appendChild(blockButton);
}
function onNode(node) {
if (node.nodeType === Node.ELEMENT_NODE) {
// insert block button into game tile
if (node.matches(QUERY_TILE)) {
onGameTileAdded(node);
}
// insert block button into game page
else if (node.id === "game-detail-page") {
const gameName = node.querySelector(".game-name");
const regexResult = window.location.href.match(REGEX_GAME);
const blockButton = createBlockSpan(regexResult.groups.id, gameName.innerText);
gameName.appendChild(blockButton);
}
}
}
for (const node of document.body.getElementsByTagName("*")) {
onNode(node);
}
function observerCallback(mutations, observer) {
for (const mutation of mutations) {
switch (mutation.type) {
case "childList": {
for (const node of mutation.addedNodes) {
onNode(node);
if (node.nodeType === Node.ELEMENT_NODE) {
for (const descendant of node.getElementsByTagName("*")) {
onNode(descendant);
}
}
}
break;
}
case "attributes": {
onNode(mutation.target);
break;
}
}
};
}
const observer = new MutationObserver(observerCallback);
observer.observe(document.body, {childList:true,subtree:true,attributes:true});
function onInterval() {
for (const node of document.body.querySelectorAll("*")) {
onNode(node);
}
}
if (ENABLE_INTERVAL) {
setInterval(onInterval, 1000);
}
})();