// ==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);
    }
})();








Edit

Pub: 31 May 2025 08:50 UTC

Views: 96