From 4e19e6f54ec52b60c5a983632e79d0f9952fa93c Mon Sep 17 00:00:00 2001 From: tomasalias Date: Tue, 6 Oct 2026 19:27:02 +0200 Subject: [PATCH] fix: resolve React DOM insertBefore reconciliation crash on launcher start --- bettercf/core/renderer.js | 123 +++++++++++--------------------------- patcher/patch_core.py | 52 +++++++++------- 2 files changed, 67 insertions(+), 108 deletions(-) diff --git a/bettercf/core/renderer.js b/bettercf/core/renderer.js index 88e7bf5..00217e1 100644 --- a/bettercf/core/renderer.js +++ b/bettercf/core/renderer.js @@ -28,7 +28,6 @@ // ═══════════════════════════════════════════════════════════════ const AdBlocker = { - _observer: null, _style: null, init() { @@ -44,32 +43,26 @@ .subscribe-link, .subscribe-link-small-resolution, .subscribe-link-large-resolution, - .promo-banner { + .promo-banner, + owadview, + [class*="ad-container"], + [class*="ad-banner"], + [class*="AdView"], + [class*="high-impact"], + [data-ad], + [data-ad-slot] { display: none !important; width: 0 !important; height: 0 !important; min-width: 0 !important; max-width: 0 !important; + margin: 0 !important; + padding: 0 !important; overflow: hidden !important; pointer-events: none !important; position: absolute !important; visibility: hidden !important; - } - - owadview { - display: none !important; - width: 0 !important; - height: 0 !important; - position: absolute !important; - visibility: hidden !important; - } - - [class*="ad-container"], - [class*="ad-banner"], - [class*="high-impact-ad"], - [data-ad], - [data-ad-slot] { - display: none !important; + opacity: 0 !important; } [style*="grid-template"][style*="ads"] { @@ -79,80 +72,28 @@ `; (document.head || document.documentElement).appendChild(this._style); - this._observer = new MutationObserver((mutations) => { - for (const mutation of mutations) { - for (const node of mutation.addedNodes) { - if (node.nodeType !== Node.ELEMENT_NODE) continue; - this._removeAds(node); - } - } - }); - - this._observer.observe(document.documentElement, { - childList: true, - subtree: true, - }); - - setTimeout(() => this._sweepAds(), 1000); - setTimeout(() => this._sweepAds(), 3000); - setTimeout(() => this._sweepAds(), 6000); - }, - - _removeAds(node) { - if (node.tagName?.toLowerCase() === 'owadview') { - const parent = node.parentElement; - node.remove(); - if (parent && parent.children.length === 0) { - parent.style.display = 'none'; - } - return; - } - - const classes = node.className?.toString() || ''; - if (/\b(ad-container|ad-banner|ads-wrapper|AdView|high-impact|display-ad)\b/i.test(classes)) { - node.remove(); - return; - } - - const adChildren = node.querySelectorAll?.('owadview, [class*="ad-container"], [class*="AdView"], [class*="high-impact"]'); - if (adChildren) { - for (const child of adChildren) { - child.remove(); - } - } + // Safe cleanup of ad text wrappers without removing DOM nodes (avoids breaking React Fiber) + setTimeout(() => this._sweepAds(), 1500); + setTimeout(() => this._sweepAds(), 4000); }, _sweepAds() { - const adSelectors = [ - '.curseforge-ad', - '.ad-inner', - '.overwolf-ad', - '.affiliate-banner', - '.subscribe-link', - '.subscribe-link-small-resolution', - '.subscribe-link-large-resolution', - '.promo-banner', - 'owadview', - ]; - const els = document.querySelectorAll(adSelectors.join(', ')); - for (const el of els) { - el.remove(); - } - - const walker = document.createTreeWalker(document.body || document.documentElement, NodeFilter.SHOW_TEXT, null); - while (walker.nextNode()) { - const text = walker.currentNode.textContent?.trim(); - if (text && /^Ads support authors/i.test(text)) { - let parent = walker.currentNode.parentElement; - while (parent && parent !== document.body) { - if (parent.children.length <= 2) { - parent.style.display = 'none'; - break; + try { + const walker = document.createTreeWalker(document.body || document.documentElement, NodeFilter.SHOW_TEXT, null); + while (walker.nextNode()) { + const text = walker.currentNode.textContent?.trim(); + if (text && /^Ads support authors/i.test(text)) { + let parent = walker.currentNode.parentElement; + while (parent && parent !== document.body) { + if (parent.children.length <= 2) { + parent.style.display = 'none'; + break; + } + parent = parent.parentElement; } - parent = parent.parentElement; } } - } + } catch {} }, }; @@ -515,6 +456,8 @@ try { await renderCustomThemesSection(view); + } catch (err) { + console.warn('[BetterCF] Themes section rendering failed:', err); } finally { isInjecting = false; } @@ -592,8 +535,12 @@ lastRow.after(section); } else { const upsell = view.querySelector('.upsell') || view.querySelector('[class*="upsell"]'); - if (upsell) { - view.insertBefore(section, upsell); + if (upsell && upsell.parentNode) { + try { + upsell.before(section); + } catch { + view.appendChild(section); + } } else { view.appendChild(section); } diff --git a/patcher/patch_core.py b/patcher/patch_core.py index abbdd90..65b3898 100644 --- a/patcher/patch_core.py +++ b/patcher/patch_core.py @@ -14,6 +14,7 @@ import hashlib import winreg import time import subprocess +import re from pathlib import Path CREATE_NO_WINDOW = 0x08000000 if os.name == "nt" else 0 @@ -401,35 +402,46 @@ def patch_curseforge(log_callback=print) -> bool: return False try: - # 1. Zero-Ad & Themes Patch in desktop.js + # 1. Themes Patch in desktop.js (Ads are fully handled by desktop.css & network blocking) desktop_js = temp_extract / "dist" / "desktop" / "desktop.js" if desktop_js.exists(): content = desktop_js.read_text(encoding="utf-8") - # Clean up any legacy or duplicate __orig_eV replacements + # Restore clean eV component if previously patched (in modern CurseForge, eV is the modpack card author component, not ads) + if "/*BCF_AD_OFF*/eV=e=>{return null;let" in content: + content = content.replace("/*BCF_AD_OFF*/eV=e=>{return null;let", "eV=e=>{let") + log_callback("[+] Restored intact modpack card component (eV)") while "let __orig_eV=e=>null;" in content: content = content.replace("let __orig_eV=e=>null;", "") if "eV=e=>null;let __orig_eV=e=>{" in content: content = content.replace("eV=e=>null;let __orig_eV=e=>{", "eV=e=>{") - # Idempotently disable React ad component (eV -> return null) - if "/*BCF_AD_OFF*/" not in content and "eV=e=>{let" in content: - content = content.replace("eV=e=>{let", "/*BCF_AD_OFF*/eV=e=>{return null;let", 1) - log_callback("[+] Disabled React ad component (eV -> return null)") - if "showSwapAdsButton:T" in content: - content = content.replace("showSwapAdsButton:T", "showSwapAdsButton:!1") - log_callback("[+] Disabled swap ads button") - if "R=e=>!!e.forceUnlock||!e.isPremium||!!M" in content: - content = content.replace("R=e=>!!e.forceUnlock||!e.isPremium||!!M", "R=e=>!0") - log_callback("[+] Unlocked all native premium themes (Settings route)") - if "P=e=>!!e.forceUnlock||!e.isPremium||f" in content: - content = content.replace("P=e=>!!e.forceUnlock||!e.isPremium||f", "P=e=>!0") - log_callback("[+] Unlocked all native premium themes (Popup route)") - if "return!r?.forceUnlock&&r?.isPremium&&!t?u.Sx.Classic:n" in content: - content = content.replace("return!r?.forceUnlock&&r?.isPremium&&!t?u.Sx.Classic:n", "return n") + # Unlock all native premium themes (Settings & Popup routes) across versions + content, c1 = re.subn( + r'([a-zA-Z0-9_$]+)=e=>!!e\.forceUnlock\|\|!e\.isPremium\|\|[^,;]+', + r'\1=e=>!0', + content + ) + if c1 > 0: + log_callback(f"[+] Unlocked native premium themes ({c1} route checks)") + + # Unlock premium theme state validation + content, c2 = re.subn( + r'return!r\?\.forceUnlock&&r\?\.isPremium&&!t\?[a-zA-Z0-9_$.]+:n', + r'return n', + content + ) + if c2 > 0: log_callback("[+] Unlocked premium theme state validation") - if "function iy(e){return e.subscription.plan?.active}" in content: - content = content.replace("function iy(e){return e.subscription.plan?.active}", "function iy(e){return !0}") - log_callback("[+] Unlocked subscription plan status for themes") + + # Unlock subscription plan status for themes + content, c3 = re.subn( + r'function ([a-zA-Z0-9_$]+)\(e\)\{return e\.subscription\.plan\?\.active\}', + r'function \1(e){return !0}', + content + ) + if c3 > 0: + log_callback(f"[+] Unlocked subscription plan status for themes ({c3} checks)") + desktop_js.write_text(content, encoding="utf-8") # 2. Zero-Ad & Settings Themes CSS in desktop.css