Last active
May 12, 2026 09:22
-
-
Save Rongmario/2895b855f6644ce81c4dd307b0ab6a25 to your computer and use it in GitHub Desktop.
Flare Viewer Cleanroom Auto-Remapper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Flare Viewer Cleanroom Auto-Remapper | |
| // @namespace https://cleanroommc.com/ | |
| // @version 1.0.0 | |
| // @description Auto-select MCP mappings for Flare reports on Cleanroom | |
| // @author CleanroomMC, Rongmario | |
| // @license MIT | |
| // @match https://spark.lucko.me/* | |
| // @updateURL https://gist.github.com/Rongmario/2895b855f6644ce81c4dd307b0ab6a25/raw/flare-viewer-cleanroom-autoremapper.user.js | |
| // @downloadURL https://gist.github.com/Rongmario/2895b855f6644ce81c4dd307b0ab6a25/raw/flare-viewer-cleanroom-autoremapper.user.js | |
| // @grant none | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| const TARGET_MAPPING = 'mcp-1_12_2'; | |
| const LOG = (...args) => console.log('[Flare]', ...args); | |
| function hasCleanroomSpan() { | |
| const detailContent = document.querySelector('.metadata-detail-content'); | |
| if (!detailContent) { | |
| return false; | |
| } | |
| const firstParagraph = detailContent.querySelector('p'); | |
| if (!firstParagraph) { | |
| return false; | |
| } | |
| const spans = firstParagraph.querySelectorAll('span'); | |
| for (const span of spans) { | |
| if (span.textContent.trim() === 'Cleanroom') { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| function findSelectElement() { | |
| const allSelects = document.querySelectorAll('select'); | |
| for (const sel of allSelects) { | |
| if (Array.from(sel.options).some(o => o.value === TARGET_MAPPING)) { | |
| LOG('Found target select via option scan:', sel); | |
| return { el: sel, kind: 'native' }; | |
| } | |
| } | |
| return null; | |
| } | |
| function setReactSelectValue(selectEl, value) { | |
| const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, 'value').set; | |
| nativeSetter.call(selectEl, value); | |
| selectEl.dispatchEvent(new Event('input', { bubbles: true })); | |
| selectEl.dispatchEvent(new Event('change', { bubbles: true })); | |
| } | |
| let applied = false; | |
| let settingsOpened = false; | |
| function findSettingsButton() { | |
| return document.querySelector('.button.textbox[title="Click to show the settings menu"]'); | |
| } | |
| function openSettingsMenu() { | |
| if (settingsOpened) { | |
| return; | |
| } | |
| const btn = findSettingsButton(); | |
| if (!btn) { | |
| return; | |
| } | |
| LOG('Opening settings menu.'); | |
| btn.click(); | |
| settingsOpened = true; | |
| } | |
| function closeSettingsMenu() { | |
| const btn = findSettingsButton(); | |
| if (!btn) { | |
| return; | |
| } | |
| LOG('Closing settings menu.'); | |
| btn.click(); | |
| } | |
| function tryApply() { | |
| if (applied) { | |
| return true; | |
| } | |
| if (!hasCleanroomSpan()) { | |
| return false; | |
| } | |
| LOG('Cleanroom detected.'); | |
| openSettingsMenu(); | |
| const found = findSelectElement(); | |
| if (!found) { | |
| LOG('Mappings selector not in DOM yet.'); | |
| return false; | |
| } | |
| const { el: selectEl } = found; | |
| const optionExists = Array.from(selectEl.options).some(o => o.value === TARGET_MAPPING); | |
| if (!optionExists) { | |
| LOG('Option', TARGET_MAPPING, 'not present yet. Available:', Array.from(selectEl.options).map(o => o.value)); | |
| return false; | |
| } | |
| if (selectEl.value === TARGET_MAPPING) { | |
| LOG('Already set to', TARGET_MAPPING); | |
| applied = true; | |
| return true; | |
| } | |
| setReactSelectValue(selectEl, TARGET_MAPPING); | |
| LOG('Set mappings selector to', TARGET_MAPPING, '— verify current value:', selectEl.value); | |
| setTimeout(() => { | |
| if (selectEl.value === TARGET_MAPPING) { | |
| LOG('Confirmed: value held after React re-render.'); | |
| applied = true; | |
| closeSettingsMenu(); | |
| } else { | |
| LOG('React reset the value — retrying next mutation.'); | |
| } | |
| }, 50); | |
| return selectEl.value === TARGET_MAPPING; | |
| } | |
| let observer = null; | |
| function startObserving() { | |
| if (observer) { | |
| observer.disconnect(); | |
| } | |
| observer = new MutationObserver(() => { | |
| if (tryApply()) { | |
| observer.disconnect(); | |
| } | |
| }); | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| } | |
| function onNavigate() { | |
| applied = false; | |
| settingsOpened = false; | |
| startObserving(); | |
| tryApply(); | |
| } | |
| function hookNavigation() { | |
| const patch = (original) => function (...args) { | |
| const result = original.apply(this, args); | |
| onNavigate(); | |
| return result; | |
| }; | |
| history.pushState = patch(history.pushState); | |
| history.replaceState = patch(history.replaceState); | |
| window.addEventListener('popstate', onNavigate); | |
| } | |
| hookNavigation(); | |
| startObserving(); | |
| tryApply(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment