#!/usr/bin/env python3 """ BetterCurseForge — Graphical Patcher & Management Suite ======================================================== Single executable GUI for patching CurseForge, managing themes/plugins, and providing zero-timer automatic re-patching after updates. """ import os import sys if os.name == "nt": if len(sys.argv) > 1 and sys.argv[1].lower() not in ("--watch", "--wait-for-update", "--ensure-watcher", "--repatch-splash", "--splash"): import ctypes ctypes.windll.kernel32.AttachConsole(-1) if sys.stdout is not None: sys.stdout.reconfigure(encoding="utf-8", errors="replace") if sys.stderr is not None: sys.stderr.reconfigure(encoding="utf-8", errors="replace") import json import subprocess import threading import tkinter as tk from tkinter import ttk, messagebox, scrolledtext from pathlib import Path from datetime import datetime try: from PIL import Image, ImageTk HAS_PIL = True except ImportError: HAS_PIL = False # Import patch core logic try: from . import patch_core except ImportError: import patch_core VERSION = "1.0.0" def find_asset(filename: str) -> Path | None: candidates = [ Path(getattr(sys, "_MEIPASS", "")) / "assets" / filename if hasattr(sys, "_MEIPASS") else None, Path(getattr(sys, "_MEIPASS", "")) / filename if hasattr(sys, "_MEIPASS") else None, Path(sys.executable).parent / "assets" / filename if getattr(sys, "frozen", False) else None, getattr(patch_core, "PERMANENT_ASSETS", Path()) / filename, Path(__file__).parent.parent / "assets" / filename, Path.cwd() / "assets" / filename, ] for c in candidates: if c and c.exists(): return c return None # ═══════════════════════════════════════════════════════════════════════════════ # COLOR PALETTE (Modern Discord / Vencord Dark Theme) # ═══════════════════════════════════════════════════════════════════════════════ COLORS = { "bg": "#121214", "bg_card": "#18191c", "bg_elevated": "#202225", "bg_input": "#0e0f10", "purple": "#5865f2", "purple_hover": "#4752c4", "purple_dark": "#3c45a5", "purple_glow": "#7983f5", "green": "#57f287", "green_dark": "#237e44", "red": "#ed4245", "yellow": "#fee75c", "text": "#f2f3f5", "text_dim": "#949ba4", "text_muted": "#5c6067", "border": "#2b2d31", "border_focus": "#5865f2", } # ═══════════════════════════════════════════════════════════════════════════════ # GUI APPLICATION # ═══════════════════════════════════════════════════════════════════════════════ class BetterCFApp: def __init__(self): self.root = tk.Tk() self.root.title("BetterCurseForge Patcher") self.root.geometry("780x560") self.root.minsize(720, 500) self.root.configure(bg=COLORS["bg"]) self.install_dir = None self.is_working = False self._icon_photo = None self._header_icon_photo = None self._configure_styles() self._setup_window_icon() self._build_ui() self._refresh_status() def _setup_window_icon(self): ico_path = find_asset("icon.ico") if ico_path and os.name == "nt": try: self.root.iconbitmap(str(ico_path.resolve())) self.root.iconbitmap(default=str(ico_path.resolve())) except Exception: pass try: import ctypes user32 = ctypes.windll.user32 GA_ROOT = 2 WM_SETICON = 0x0080 IMAGE_ICON = 1 LR_LOADFROMFILE = 0x0010 self.root.update_idletasks() inner_hwnd = self.root.winfo_id() root_hwnd = user32.GetAncestor(inner_hwnd, GA_ROOT) or inner_hwnd self._win32_icon_sm = user32.LoadImageW(None, str(ico_path.resolve()), IMAGE_ICON, 16, 16, LR_LOADFROMFILE) self._win32_icon_bg = user32.LoadImageW(None, str(ico_path.resolve()), IMAGE_ICON, 32, 32, LR_LOADFROMFILE) for h in (inner_hwnd, root_hwnd): if self._win32_icon_sm: user32.SendMessageW(h, WM_SETICON, 0, self._win32_icon_sm) if self._win32_icon_bg: user32.SendMessageW(h, WM_SETICON, 1, self._win32_icon_bg) except Exception: pass elif os.name != "nt": png_path = find_asset("icon_512.png") or find_asset("icon.png") if png_path: try: if HAS_PIL: im = Image.open(png_path) self._icon_photo = ImageTk.PhotoImage(im.resize((64, 64), Image.Resampling.LANCZOS)) self.root.iconphoto(True, self._icon_photo) else: self._icon_photo = tk.PhotoImage(file=str(png_path)) self.root.iconphoto(True, self._icon_photo) except Exception: pass def _configure_styles(self): style = ttk.Style() style.theme_use("clam") style.configure(".", background=COLORS["bg"], foreground=COLORS["text"], font=("Segoe UI", 10)) style.configure("TFrame", background=COLORS["bg"]) style.configure("Card.TFrame", background=COLORS["bg_card"]) style.configure("CardTitle.TLabel", font=("Segoe UI", 11, "bold"), foreground=COLORS["text"]) style.configure("StatusKey.TLabel", font=("Segoe UI", 9), foreground=COLORS["text_dim"]) style.configure("StatusVal.TLabel", font=("Segoe UI", 9, "bold"), foreground=COLORS["text"]) style.configure("Patched.TLabel", font=("Segoe UI", 9, "bold"), foreground=COLORS["green"]) style.configure("NotPatched.TLabel", font=("Segoe UI", 9, "bold"), foreground=COLORS["red"]) style.configure("Error.TLabel", font=("Segoe UI", 9, "bold"), foreground=COLORS["red"]) def _build_ui(self): # Header header = tk.Frame(self.root, bg=COLORS["bg_card"], padx=20, pady=12) header.pack(fill="x") title_frame = tk.Frame(header, bg=COLORS["bg_card"]) title_frame.pack(side="left") # Header icon thumbnail png_path = find_asset("icon_512.png") or find_asset("icon.png") if png_path and HAS_PIL: try: header_img = Image.open(png_path).resize((32, 32), Image.Resampling.LANCZOS) self._header_icon_photo = ImageTk.PhotoImage(header_img) icon_lbl = tk.Label(title_frame, image=self._header_icon_photo, bg=COLORS["bg_card"]) icon_lbl.pack(side="left", padx=(0, 10)) except Exception: pass title_lbl = tk.Label( title_frame, text="BetterCurseForge", font=("Segoe UI", 16, "bold"), bg=COLORS["bg_card"], fg=COLORS["purple_glow"] ) title_lbl.pack(side="left") ver_lbl = tk.Label( title_frame, text=f"v{VERSION}", font=("Segoe UI", 9), bg=COLORS["bg_card"], fg=COLORS["text_dim"] ) ver_lbl.pack(side="left", padx=(8, 0), pady=(4, 0)) sub_lbl = tk.Label( header, text="Ad-free launcher with custom themes and plugins", font=("Segoe UI", 9), bg=COLORS["bg_card"], fg=COLORS["text_dim"] ) sub_lbl.pack(side="left", padx=(16, 0), pady=(4, 0)) # Main Layout (2 Columns) main = ttk.Frame(self.root, padding="14") main.pack(fill="both", expand=True) left = ttk.Frame(main) left.pack(side="left", fill="both", expand=False, padx=(0, 10)) # ── Status Card ── status_card = tk.Frame(left, bg=COLORS["bg_card"], highlightbackground=COLORS["border"], highlightthickness=1, padx=14, pady=12) status_card.pack(fill="x", pady=(0, 10)) ttk.Label(status_card, text="📋 Launcher Status", style="CardTitle.TLabel").pack(anchor="w") tk.Frame(status_card, height=1, bg=COLORS["border"]).pack(fill="x", pady=(6, 8)) self.status_grid = tk.Frame(status_card, bg=COLORS["bg_card"]) self.status_grid.pack(fill="x") self._status_labels = {} for i, (key, label) in enumerate([ ("install", "Install Path"), ("status", "Patch Status"), ("watcher", "Auto-Repatch"), ("themes", "Themes"), ("plugins", "Plugins"), ]): ttk.Label(self.status_grid, text=label, style="StatusKey.TLabel").grid( row=i, column=0, sticky="w", pady=2, padx=(0, 12)) lbl = ttk.Label(self.status_grid, text="—", style="StatusVal.TLabel") lbl.grid(row=i, column=1, sticky="w", pady=2) self._status_labels[key] = lbl # ── Action Buttons ── btn_frame = tk.Frame(left, bg=COLORS["bg"]) btn_frame.pack(fill="x") self.patch_btn = tk.Button( btn_frame, text="⚡ Patch CurseForge", font=("Segoe UI", 11, "bold"), bg=COLORS["purple"], fg="#ffffff", activebackground=COLORS["purple_hover"], activeforeground="#ffffff", relief="flat", cursor="hand2", pady=9, command=self._on_patch, ) self.patch_btn.pack(fill="x", pady=(0, 5)) self.unpatch_btn = tk.Button( btn_frame, text="🔄 Restore Vanilla", font=("Segoe UI", 10), bg=COLORS["bg_elevated"], fg=COLORS["text_dim"], activebackground=COLORS["border"], activeforeground=COLORS["text"], relief="flat", cursor="hand2", pady=7, command=self._on_unpatch, ) self.unpatch_btn.pack(fill="x", pady=(0, 5)) self.watcher_btn = tk.Button( btn_frame, text="🛡️ Auto-Repatch on Update", font=("Segoe UI", 10), bg=COLORS["bg_elevated"], fg=COLORS["text_dim"], activebackground=COLORS["border"], activeforeground=COLORS["text"], relief="flat", cursor="hand2", pady=7, command=self._on_toggle_watcher, ) self.watcher_btn.pack(fill="x", pady=(0, 8)) # Secondary Actions sub_row = tk.Frame(btn_frame, bg=COLORS["bg"]) sub_row.pack(fill="x") self.launch_btn = tk.Button( sub_row, text="🚀 Launch", font=("Segoe UI", 9), bg=COLORS["bg_elevated"], fg=COLORS["text_dim"], activebackground=COLORS["border"], activeforeground=COLORS["text"], relief="flat", cursor="hand2", padx=10, pady=5, command=self._on_launch, ) self.launch_btn.pack(side="left", padx=(0, 4), fill="x", expand=True) self.themes_btn = tk.Button( sub_row, text="🎨 Themes", font=("Segoe UI", 9), bg=COLORS["bg_elevated"], fg=COLORS["text_dim"], activebackground=COLORS["border"], activeforeground=COLORS["text"], relief="flat", cursor="hand2", padx=8, pady=5, command=self._on_open_themes, ) self.themes_btn.pack(side="left", padx=(0, 4), fill="x", expand=True) self.plugins_btn = tk.Button( sub_row, text="🔌 Plugins", font=("Segoe UI", 9), bg=COLORS["bg_elevated"], fg=COLORS["text_dim"], activebackground=COLORS["border"], activeforeground=COLORS["text"], relief="flat", cursor="hand2", padx=8, pady=5, command=self._on_open_plugins, ) self.plugins_btn.pack(side="left", fill="x", expand=True) # ── Right Column: Logs ── right = ttk.Frame(main) right.pack(side="right", fill="both", expand=True) log_card = tk.Frame(right, bg=COLORS["bg_card"], highlightbackground=COLORS["border"], highlightthickness=1, padx=12, pady=10) log_card.pack(fill="both", expand=True) log_header = tk.Frame(log_card, bg=COLORS["bg_card"]) log_header.pack(fill="x", pady=(0, 6)) ttk.Label(log_header, text="📝 Activity Log", style="CardTitle.TLabel").pack(side="left") clear_btn = tk.Button( log_header, text="Clear", font=("Segoe UI", 8), bg=COLORS["bg_card"], fg=COLORS["text_muted"], activebackground=COLORS["bg_card"], activeforeground=COLORS["text"], relief="flat", cursor="hand2", command=self._clear_log ) clear_btn.pack(side="right") self.log_text = scrolledtext.ScrolledText( log_card, wrap="word", font=("Cascadia Code", 9), height=18, bg=COLORS["bg_input"], fg=COLORS["text_dim"], insertbackground=COLORS["text"], relief="flat", highlightbackground=COLORS["border"], highlightthickness=1, padx=8, pady=8, ) self.log_text.pack(fill="both", expand=True) self.log_text.tag_configure("timestamp", foreground=COLORS["text_muted"]) self.log_text.tag_configure("info", foreground=COLORS["text"]) self.log_text.tag_configure("success", foreground=COLORS["green"]) self.log_text.tag_configure("error", foreground=COLORS["red"]) self.log_text.tag_configure("warn", foreground=COLORS["yellow"]) self.log_text.tag_configure("header", foreground=COLORS["purple_glow"], font=("Segoe UI", 9, "bold")) self.log_text.configure(state="disabled") # ── Logging ───────────────────────────────────────────────────────────── def log(self, message: str, level: str = "info"): ts = datetime.now().strftime("%H:%M:%S") self.log_text.configure(state="normal") self.log_text.insert("end", f"[{ts}] ", "timestamp") tag = level if message.startswith("[+]") or message.startswith("✓") or "success" in message.lower(): tag = "success" elif message.startswith("[-]") or message.startswith("✗") or "fail" in message.lower() or "error" in message.lower(): tag = "error" elif message.startswith("[!]") or "warn" in message.lower(): tag = "warn" self.log_text.insert("end", f"{message}\n", tag) self.log_text.see("end") self.log_text.configure(state="disabled") def _clear_log(self): self.log_text.configure(state="normal") self.log_text.delete("1.0", "end") self.log_text.configure(state="disabled") # ── Refresh Status ────────────────────────────────────────────────────── def _refresh_status(self): self.install_dir = patch_core.get_curseforge_dir() if not self.install_dir: self._status_labels["install"].configure(text="Not Found", style="Error.TLabel") self._status_labels["status"].configure(text="N/A", style="StatusVal.TLabel") self.patch_btn.configure(state="disabled") self.unpatch_btn.configure(state="disabled") self.log("CurseForge installation not found!", "error") return path_str = str(self.install_dir) if len(path_str) > 32: path_str = "..." + path_str[-29:] self._status_labels["install"].configure(text=path_str, style="StatusVal.TLabel") is_patched = patch_core.is_curseforge_patched(self.install_dir) if is_patched: self._status_labels["status"].configure(text="✓ PATCHED", style="Patched.TLabel") self.patch_btn.configure(text="🔁 Re-Patch CurseForge") self.unpatch_btn.configure(state="normal") else: self._status_labels["status"].configure(text="✗ NOT PATCHED", style="NotPatched.TLabel") self.patch_btn.configure(text="⚡ Patch CurseForge") self.unpatch_btn.configure(state="disabled") # Watcher status autostart = patch_core.is_autostart_watcher_enabled() running = patch_core.is_watcher_running() if is_patched and not running and autostart: patch_core.start_background_watcher() running = patch_core.is_watcher_running() if running: self._status_labels["watcher"].configure(text="Active (Instant)", style="Patched.TLabel") self.watcher_btn.configure(text="🛡️ Auto-Repatch: ON", bg=COLORS["green_dark"], fg="#ffffff") elif autostart: self._status_labels["watcher"].configure(text="Enabled (Idle)", style="Patched.TLabel") self.watcher_btn.configure(text="🛡️ Auto-Repatch: ON", bg=COLORS["green_dark"], fg="#ffffff") else: self._status_labels["watcher"].configure(text="Inactive", style="StatusKey.TLabel") self.watcher_btn.configure(text="🛡️ Enable Auto-Repatch", bg=COLORS["bg_elevated"], fg=COLORS["text_dim"]) # Themes & Plugins counts user_dir = patch_core.get_user_data_dir() n_themes = len(list((user_dir / "themes").glob("*.css"))) n_plugins = len([p for p in (user_dir / "plugins").iterdir() if p.is_dir()]) self._status_labels["themes"].configure(text=f"{n_themes} installed") self._status_labels["plugins"].configure(text=f"{n_plugins} installed") self.patch_btn.configure(state="normal") # ── Actions ───────────────────────────────────────────────────────────── def _on_patch(self): if self.is_working: return self.is_working = True self.patch_btn.configure(state="disabled") self.unpatch_btn.configure(state="disabled") def task(): self.log("Starting patch process...", "header") success = patch_core.patch_curseforge(log_callback=lambda msg: self.root.after(0, self.log, msg)) if success: self.root.after(0, self.log, "[+] Auto-repatch active. Watcher running in background.", "success") self.root.after(0, self._after_action) threading.Thread(target=task, daemon=True).start() def _on_unpatch(self): if self.is_working: return self.is_working = True self.patch_btn.configure(state="disabled") self.unpatch_btn.configure(state="disabled") def task(): self.log("Restoring vanilla CurseForge...", "header") success = patch_core.unpatch_curseforge(log_callback=lambda msg: self.root.after(0, self.log, msg)) self.root.after(0, self._after_action) threading.Thread(target=task, daemon=True).start() def _after_action(self): self.is_working = False self._refresh_status() def _on_toggle_watcher(self): current = patch_core.is_autostart_watcher_enabled() or patch_core.is_watcher_running() new_state = not current patch_core.set_autostart_watcher(new_state) if new_state: patch_core.start_background_watcher() self.log("[+] Auto-repatch enabled. Watching resources/ directory for updates.", "success") else: patch_core.stop_background_watcher() self.log("[-] Auto-repatch disabled.", "warn") self._refresh_status() def _on_launch(self): if not self.install_dir: return exe = self.install_dir / "CurseForge.exe" if exe.exists(): self.log(f"Launching CurseForge: {exe}") creation_flags = 0x00000008 | 0x00000200 # DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP subprocess.Popen([str(exe)], cwd=str(self.install_dir), creationflags=creation_flags) else: self.log("CurseForge.exe not found!", "error") def _on_open_themes(self): user_dir = patch_core.get_user_data_dir() themes_dir = user_dir / "themes" os.startfile(str(themes_dir)) self.log(f"Opened themes folder: {themes_dir}") def _on_open_plugins(self): user_dir = patch_core.get_user_data_dir() plugins_dir = user_dir / "plugins" os.startfile(str(plugins_dir)) self.log(f"Opened plugins folder: {plugins_dir}") def run(self): self.log(f"BetterCurseForge Patcher v{VERSION} ready.") self.root.mainloop() # ═══════════════════════════════════════════════════════════════════════════════ # DEDICATED SPLASH UPDATER GUI # ═══════════════════════════════════════════════════════════════════════════════ class SplashUpdaterWindow: def __init__(self, target_pid: int | None = None): self.target_pid = target_pid self.root = tk.Tk() self.root.title("BetterCurseForge Updater") width, height = 460, 215 screen_w = self.root.winfo_screenwidth() screen_h = self.root.winfo_screenheight() x = (screen_w - width) // 2 y = (screen_h - height) // 2 self.root.geometry(f"{width}x{height}+{x}+{y}") self.root.overrideredirect(True) self.root.attributes("-topmost", True) self.root.configure(bg=COLORS["bg"]) self._icon_photo = None self._drag_x = 0 self._drag_y = 0 self._setup_window_icon() self._build_ui() def _setup_window_icon(self): ico_path = find_asset("icon.ico") if ico_path and os.name == "nt": try: self.root.iconbitmap(str(ico_path.resolve())) self.root.iconbitmap(default=str(ico_path.resolve())) except Exception: pass def _build_ui(self): card = tk.Frame( self.root, bg=COLORS["bg_card"], highlightbackground=COLORS["purple_dark"], highlightthickness=1, padx=20, pady=18 ) card.pack(fill="both", expand=True) def start_drag(e): self._drag_x = e.x self._drag_y = e.y def do_drag(e): x = self.root.winfo_x() + (e.x - self._drag_x) y = self.root.winfo_y() + (e.y - self._drag_y) self.root.geometry(f"+{x}+{y}") card.bind("", start_drag) card.bind("", do_drag) # Header header = tk.Frame(card, bg=COLORS["bg_card"]) header.pack(fill="x") header.bind("", start_drag) header.bind("", do_drag) png_path = find_asset("icon_512.png") or find_asset("icon.png") if png_path and HAS_PIL: try: im = Image.open(png_path).resize((44, 44), Image.Resampling.LANCZOS) self._icon_photo = ImageTk.PhotoImage(im) icon_lbl = tk.Label(header, image=self._icon_photo, bg=COLORS["bg_card"]) icon_lbl.pack(side="left", padx=(0, 14)) icon_lbl.bind("", start_drag) icon_lbl.bind("", do_drag) except Exception: pass text_col = tk.Frame(header, bg=COLORS["bg_card"]) text_col.pack(side="left", fill="both", expand=True) text_col.bind("", start_drag) text_col.bind("", do_drag) title_row = tk.Frame(text_col, bg=COLORS["bg_card"]) title_row.pack(anchor="w") title_row.bind("", start_drag) title_row.bind("", do_drag) t_lbl = tk.Label( title_row, text="BetterCurseForge", font=("Segoe UI", 13, "bold"), fg=COLORS["text"], bg=COLORS["bg_card"] ) t_lbl.pack(side="left") t_lbl.bind("", start_drag) t_lbl.bind("", do_drag) badge = tk.Frame( title_row, bg="#261b3d", highlightbackground=COLORS["purple_glow"], highlightthickness=1, padx=6, pady=1 ) badge.pack(side="left", padx=(10, 0)) b_lbl = tk.Label( badge, text="AUTO-UPDATE", font=("Segoe UI", 8, "bold"), fg="#c4b5fd", bg="#261b3d" ) b_lbl.pack() sub_lbl = tk.Label( text_col, text="CurseForge update detected. Applying enhancements...", font=("Segoe UI", 9), fg=COLORS["text_dim"], bg=COLORS["bg_card"] ) sub_lbl.pack(anchor="w", pady=(3, 0)) sub_lbl.bind("", start_drag) sub_lbl.bind("", do_drag) # Progress bar style = ttk.Style() style.theme_use("clam") style.configure( "Splash.Horizontal.TProgressbar", troughcolor="#1e1f22", background=COLORS["purple"], bordercolor="#1e1f22", lightcolor=COLORS["purple"], darkcolor=COLORS["purple"] ) self.pbar = ttk.Progressbar( card, style="Splash.Horizontal.TProgressbar", mode="indeterminate", length=410 ) self.pbar.pack(fill="x", pady=(16, 10)) self.pbar.start(12) # Status labels self.status_lbl = tk.Label( card, text="Waiting for CurseForge installer to finish...", font=("Segoe UI", 9, "bold"), fg=COLORS["text"], bg=COLORS["bg_card"], anchor="w" ) self.status_lbl.pack(fill="x") self.detail_lbl = tk.Label( card, text="Silencing vanilla client in background...", font=("Segoe UI", 8), fg=COLORS["text_muted"], bg=COLORS["bg_card"], anchor="w" ) self.detail_lbl.pack(fill="x", pady=(2, 0)) def set_status(self, main_text: str, detail_text: str = "", status_type: str = "normal"): color = COLORS["text"] if status_type == "success": color = COLORS["green"] elif status_type == "error": color = COLORS["red"] elif status_type == "warn": color = COLORS["yellow"] self.status_lbl.configure(text=main_text, fg=color) if detail_text: self.detail_lbl.configure(text=detail_text) def run(self): threading.Thread(target=self._worker, daemon=True).start() self.root.mainloop() def _worker(self): import time with patch_core.GlobalNamedMutex(patch_core.SPLASH_MUTEX_NAME, timeout_ms=500) as splash_lock: if not splash_lock.acquired: self.root.after(0, self.root.destroy) return try: cf_dir = patch_core.get_curseforge_dir() if not cf_dir: self.root.after(0, self.set_status, "CurseForge not found!", "Please reinstall CurseForge.", "error") time.sleep(3.0) return # Early guard: if no target_pid, CurseForge is already patched, and no updater is running, exit immediately if not self.target_pid and patch_core.is_curseforge_patched(cf_dir) and not patch_core.is_updater_running(): self.root.after(0, self.root.destroy) return was_running = patch_core.is_curseforge_running() or (self.target_pid is not None) was_intercepted = [False] if self.target_pid: self.root.after(0, self.set_status, "Waiting for previous instance to exit...", f"CurseForge PID {self.target_pid}") for _ in range(30): pids = patch_core.get_process_pids("curseforge.exe") if self.target_pid not in pids: break time.sleep(0.5) self.root.after(0, self.set_status, "Waiting for CurseForge installer...", "Silencing vanilla launcher in background...") def on_settle_log(msg: str): if "running" in msg.lower(): self.root.after(0, self.set_status, "CurseForge installer running...", "Silencing vanilla launcher in background...") elif "settle" in msg.lower() or "mb" in msg.lower(): self.root.after(0, self.set_status, "Finalizing update files...", msg) elif "silenc" in msg.lower(): was_intercepted[0] = True self.root.after(0, self.set_status, "Silenced vanilla launcher in background...", "Preventing unpatched window display...") settled = patch_core.wait_for_files_settled(cf_dir, timeout=90.0, log_fn=on_settle_log, intercept_unpatched=True) if not settled: self.root.after(0, self.set_status, "Installer wait timed out.", "Will attempt patch now...", "warn") patch_core.kill_curseforge() self.root.after(0, self.set_status, "Extracting application archive...", "Unpacking update files...") def on_patch_log(msg: str): if "Extracting" in msg: self.root.after(0, self.set_status, "Extracting application archive...", "Unpacking update files...") elif "Unlocked native premium" in msg: self.root.after(0, self.set_status, "Unlocking native premium themes...", "Patching client theme routes...") elif "premium theme state" in msg or "subscription plan" in msg: self.root.after(0, self.set_status, "Enabling theme engine...", "Bypassing subscription restrictions...") elif "Injected zero-ad" in msg: self.root.after(0, self.set_status, "Injecting ad-block rules...", "Removing sponsored banners and ad containers...") elif "Bundled BetterCurseForge runtime" in msg: self.root.after(0, self.set_status, "Bundling BetterCurseForge runtime...", "Injecting core modules...") elif "main-process hooks" in msg: self.root.after(0, self.set_status, "Hooking main process...", "Configuring network filter and IPC...") elif "Repacking app.asar" in msg: self.root.after(0, self.set_status, "Repacking application archive...", "Rebuilding app.asar package...") elif "integrity signature" in msg: self.root.after(0, self.set_status, "Updating security signatures...", "Synchronizing OWEASARSIG header hash...") elif "successfully patched" in msg: self.root.after(0, self.set_status, "✓ Complete! Launching BetterCurseForge...", "All enhancements applied successfully.", "success") success = False for attempt in range(1, 4): success = patch_core.patch_curseforge(log_callback=on_patch_log) if success: break self.root.after(0, self.set_status, f"Retrying patch ({attempt+1}/3)...", "Resolving temporary file lock...", "warn") time.sleep(2.0) if success: should_launch = was_running or was_intercepted[0] or (self.target_pid is not None) if should_launch: self.root.after(0, self.set_status, "✓ Complete! Launching BetterCurseForge...", "Enjoy your ad-free experience!", "success") exe = cf_dir / "CurseForge.exe" if exe.exists(): creation_flags = 0x00000008 | 0x00000200 if os.name == "nt" else 0 # DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP subprocess.Popen([str(exe)], cwd=str(cf_dir), creationflags=creation_flags) else: self.root.after(0, self.set_status, "✓ Complete! BetterCurseForge is updated.", "CurseForge updated in background.", "success") time.sleep(0.9) else: self.root.after(0, self.set_status, "Failed to apply patch.", "Check watcher.log for details.", "error") time.sleep(3.0) except Exception as e: self.root.after(0, self.set_status, "Error during update.", str(e), "error") time.sleep(3.0) finally: self.root.after(0, self.root.destroy) # ═══════════════════════════════════════════════════════════════════════════════ # MAIN ENTRY POINT # ═══════════════════════════════════════════════════════════════════════════════ if __name__ == "__main__": try: if len(sys.argv) > 1: arg = sys.argv[1].lower() if arg in ("--watch", "watch"): patch_core.run_watcher_loop() sys.exit(0) elif arg in ("--repatch-splash", "--splash", "splash", "--wait-for-update", "wait-for-update"): pid = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2].isdigit() else None cf = patch_core.get_curseforge_dir() if not pid and cf and patch_core.is_curseforge_patched(cf) and not patch_core.is_updater_running(): sys.exit(0) splash = SplashUpdaterWindow(pid) splash.run() sys.exit(0) elif arg in ("--ensure-watcher", "ensure-watcher"): if not patch_core.is_watcher_running(): patch_core.start_background_watcher() sys.exit(0) elif arg in ("--patch", "patch"): patch_core.patch_curseforge() sys.exit(0) elif arg in ("--unpatch", "unpatch"): patch_core.unpatch_curseforge() sys.exit(0) elif arg in ("--status", "status"): cf = patch_core.get_curseforge_dir() print("CurseForge:", cf) print("Patched:", patch_core.is_curseforge_patched(cf) if cf else False) print("Watcher:", "RUNNING" if patch_core.is_watcher_running() else "STOPPED") sys.exit(0) app = BetterCFApp() app.run() except SystemExit: pass except Exception as e: if len(sys.argv) > 1 and sys.argv[1].lower() in ("--watch", "--wait-for-update", "--ensure-watcher", "--repatch-splash", "--splash"): try: log_dir = patch_core.get_user_data_dir() with open(log_dir / "watcher.log", "a", encoding="utf-8") as f: f.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] [BackgroundError] {e}\n") except Exception: pass else: raise