428 lines
19 KiB
Python
428 lines
19 KiB
Python
|
|
#!/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"):
|
||
|
|
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
|
||
|
|
|
||
|
|
# Import patch core logic
|
||
|
|
try:
|
||
|
|
from . import patch_core
|
||
|
|
except ImportError:
|
||
|
|
import patch_core
|
||
|
|
|
||
|
|
VERSION = "1.0.0"
|
||
|
|
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
# 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._configure_styles()
|
||
|
|
self._build_ui()
|
||
|
|
self._refresh_status()
|
||
|
|
|
||
|
|
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")
|
||
|
|
|
||
|
|
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()
|
||
|
|
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
# MAIN ENTRY POINT
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
if len(sys.argv) > 1:
|
||
|
|
arg = sys.argv[1].lower()
|
||
|
|
if arg == "--watch":
|
||
|
|
# Silent background watcher mode
|
||
|
|
patch_core.run_watcher_loop()
|
||
|
|
sys.exit(0)
|
||
|
|
elif arg == "--wait-for-update":
|
||
|
|
pid = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2].isdigit() else None
|
||
|
|
patch_core.wait_for_update_and_repatch(pid)
|
||
|
|
sys.exit(0)
|
||
|
|
elif arg == "--ensure-watcher":
|
||
|
|
if not patch_core.is_watcher_running():
|
||
|
|
patch_core.start_background_watcher()
|
||
|
|
sys.exit(0)
|
||
|
|
elif arg == "patch":
|
||
|
|
patch_core.patch_curseforge()
|
||
|
|
sys.exit(0)
|
||
|
|
elif arg == "unpatch":
|
||
|
|
patch_core.unpatch_curseforge()
|
||
|
|
sys.exit(0)
|
||
|
|
elif arg == "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()
|