2026-10-06 15:03:16 +00:00
|
|
|
#!/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":
|
2026-10-12 15:12:24 +00:00
|
|
|
if len(sys.argv) > 1 and sys.argv[1].lower() not in ("--watch", "--wait-for-update", "--ensure-watcher", "--repatch-splash", "--splash"):
|
2026-10-06 15:03:16 +00:00
|
|
|
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
|
|
|
|
|
|
2026-10-08 19:18:55 +00:00
|
|
|
try:
|
|
|
|
|
from PIL import Image, ImageTk
|
|
|
|
|
HAS_PIL = True
|
|
|
|
|
except ImportError:
|
|
|
|
|
HAS_PIL = False
|
|
|
|
|
|
2026-10-06 15:03:16 +00:00
|
|
|
# Import patch core logic
|
|
|
|
|
try:
|
|
|
|
|
from . import patch_core
|
|
|
|
|
except ImportError:
|
|
|
|
|
import patch_core
|
|
|
|
|
|
|
|
|
|
VERSION = "1.0.0"
|
|
|
|
|
|
2026-10-08 19:18:55 +00:00
|
|
|
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
|
|
|
|
|
|
2026-10-06 15:03:16 +00:00
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
# 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
|
2026-10-08 19:18:55 +00:00
|
|
|
self._icon_photo = None
|
|
|
|
|
self._header_icon_photo = None
|
2026-10-06 15:03:16 +00:00
|
|
|
|
|
|
|
|
self._configure_styles()
|
2026-10-08 19:18:55 +00:00
|
|
|
self._setup_window_icon()
|
2026-10-06 15:03:16 +00:00
|
|
|
self._build_ui()
|
|
|
|
|
self._refresh_status()
|
|
|
|
|
|
2026-10-08 19:18:55 +00:00
|
|
|
def _setup_window_icon(self):
|
|
|
|
|
ico_path = find_asset("icon.ico")
|
|
|
|
|
if ico_path and os.name == "nt":
|
|
|
|
|
try:
|
2026-10-08 20:06:42 +00:00
|
|
|
self.root.iconbitmap(str(ico_path.resolve()))
|
2026-10-08 19:18:55 +00:00
|
|
|
self.root.iconbitmap(default=str(ico_path.resolve()))
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
try:
|
2026-10-08 20:06:42 +00:00
|
|
|
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)
|
2026-10-08 19:18:55 +00:00
|
|
|
except Exception:
|
|
|
|
|
pass
|
2026-10-08 20:06:42 +00:00
|
|
|
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
|
2026-10-08 19:18:55 +00:00
|
|
|
|
2026-10-06 15:03:16 +00:00
|
|
|
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")
|
|
|
|
|
|
2026-10-08 19:18:55 +00:00
|
|
|
# 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
|
|
|
|
|
|
2026-10-06 15:03:16 +00:00
|
|
|
title_lbl = tk.Label(
|
2026-10-08 19:18:55 +00:00
|
|
|
title_frame, text="BetterCurseForge", font=("Segoe UI", 16, "bold"),
|
2026-10-06 15:03:16 +00:00
|
|
|
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()
|
|
|
|
|
|
2026-10-12 15:12:24 +00:00
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
# 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("<Button-1>", start_drag)
|
|
|
|
|
card.bind("<B1-Motion>", do_drag)
|
|
|
|
|
|
|
|
|
|
# Header
|
|
|
|
|
header = tk.Frame(card, bg=COLORS["bg_card"])
|
|
|
|
|
header.pack(fill="x")
|
|
|
|
|
header.bind("<Button-1>", start_drag)
|
|
|
|
|
header.bind("<B1-Motion>", 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("<Button-1>", start_drag)
|
|
|
|
|
icon_lbl.bind("<B1-Motion>", 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("<Button-1>", start_drag)
|
|
|
|
|
text_col.bind("<B1-Motion>", do_drag)
|
|
|
|
|
|
|
|
|
|
title_row = tk.Frame(text_col, bg=COLORS["bg_card"])
|
|
|
|
|
title_row.pack(anchor="w")
|
|
|
|
|
title_row.bind("<Button-1>", start_drag)
|
|
|
|
|
title_row.bind("<B1-Motion>", 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("<Button-1>", start_drag)
|
|
|
|
|
t_lbl.bind("<B1-Motion>", 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("<Button-1>", start_drag)
|
|
|
|
|
sub_lbl.bind("<B1-Motion>", 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
|
|
|
|
|
|
|
|
|
|
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():
|
|
|
|
|
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:
|
|
|
|
|
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)
|
|
|
|
|
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)
|
|
|
|
|
|
2026-10-06 15:03:16 +00:00
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
# MAIN ENTRY POINT
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2026-10-06 16:49:12 +00:00
|
|
|
try:
|
|
|
|
|
if len(sys.argv) > 1:
|
|
|
|
|
arg = sys.argv[1].lower()
|
2026-10-12 17:44:33 +00:00
|
|
|
if arg in ("--watch", "watch"):
|
2026-10-06 16:49:12 +00:00
|
|
|
patch_core.run_watcher_loop()
|
|
|
|
|
sys.exit(0)
|
2026-10-12 17:44:33 +00:00
|
|
|
elif arg in ("--repatch-splash", "--splash", "splash"):
|
2026-10-12 15:12:24 +00:00
|
|
|
pid = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2].isdigit() else None
|
|
|
|
|
splash = SplashUpdaterWindow(pid)
|
|
|
|
|
splash.run()
|
|
|
|
|
sys.exit(0)
|
2026-10-12 17:44:33 +00:00
|
|
|
elif arg in ("--wait-for-update", "wait-for-update"):
|
2026-10-06 16:49:12 +00:00
|
|
|
pid = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2].isdigit() else None
|
2026-10-12 15:12:24 +00:00
|
|
|
splash = SplashUpdaterWindow(pid)
|
|
|
|
|
splash.run()
|
2026-10-06 16:49:12 +00:00
|
|
|
sys.exit(0)
|
2026-10-12 17:44:33 +00:00
|
|
|
elif arg in ("--ensure-watcher", "ensure-watcher"):
|
2026-10-06 16:49:12 +00:00
|
|
|
if not patch_core.is_watcher_running():
|
|
|
|
|
patch_core.start_background_watcher()
|
|
|
|
|
sys.exit(0)
|
2026-10-12 17:44:33 +00:00
|
|
|
elif arg in ("--patch", "patch"):
|
2026-10-06 16:49:12 +00:00
|
|
|
patch_core.patch_curseforge()
|
|
|
|
|
sys.exit(0)
|
2026-10-12 17:44:33 +00:00
|
|
|
elif arg in ("--unpatch", "unpatch"):
|
2026-10-06 16:49:12 +00:00
|
|
|
patch_core.unpatch_curseforge()
|
|
|
|
|
sys.exit(0)
|
2026-10-12 17:44:33 +00:00
|
|
|
elif arg in ("--status", "status"):
|
2026-10-06 16:49:12 +00:00
|
|
|
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:
|
2026-10-12 15:12:24 +00:00
|
|
|
if len(sys.argv) > 1 and sys.argv[1].lower() in ("--watch", "--wait-for-update", "--ensure-watcher", "--repatch-splash", "--splash"):
|
2026-10-06 16:49:12 +00:00
|
|
|
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
|