From 9bf79994acad82f3d417c069019856f59b84dbcf Mon Sep 17 00:00:00 2001 From: Microchip Date: Fri, 18 Sep 2026 11:07:22 -0400 Subject: [PATCH] Initial commit --- .gitignore | 19 + LICENSE | 31 + OctoUpdater.ico | Bin 0 -> 283550 bytes README.md | 171 ++ octo_updater.py | 6563 +++++++++++++++++++++++++++++++++++++++++++++++ screenshot.png | Bin 0 -> 74071 bytes 6 files changed, 6784 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 OctoUpdater.ico create mode 100644 README.md create mode 100644 octo_updater.py create mode 100644 screenshot.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..773dc07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Per-user runtime state +octo_updater_config.json +octo_updater_hash_cache.json +*.tmp + +# Python +__pycache__/ +*.py[cod] + +# PyInstaller build output +build/ +dist/ +*.spec + +# Local editor / OS files +.vscode/ +.idea/ +Thumbs.db +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6316bcc --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +Octo Updater License + +Copyright (c) 2026 rebasedkon +Original project: https://github.com/rebasedkon/octo-updater + +Support the author: + - Ko-fi: https://ko-fi.com/rebased + - Buy Me a Coffee: https://buymeacoffee.com/rebased + +Contact: + - Email: inskon@proton.me + - Discord: https://discord.com/users/287467238573867018 (rebazed) + +Permission is granted to use, copy, modify, merge, publish and distribute +this software, including creating derivative works, provided that: + + 1. The original author's copyright notice must remain unchanged. + 2. The original author's name and contact information must remain unchanged. + 3. The original donation links must remain unchanged and visible. + 4. Forks and derivative projects must clearly indicate the original author. + 5. Removing, replacing, hiding or modifying attribution information is + prohibited. + +This software may be redistributed in modified or unmodified form. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/OctoUpdater.ico b/OctoUpdater.ico new file mode 100644 index 0000000000000000000000000000000000000000..80f45f1aef926a718a3813f1ea086142bd3865b1 GIT binary patch literal 283550 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 ?9hcn!dyPF3w3};h# zO6USu6YsXOa1%FTu!`ZX^hhFEv(enwB6*Z1iHh^7dxx!Gj)ctKD}|?SNX%h0Fz$Gf z+V&d~GKAj?>v@cKfjHnJs(iaGaplvHXDAUCXf6Kr^Gd?*X`HP|_>@81i5$^i zag3}LyP{FCfvsa*v_h<5U|7?`;5=Su{I}{H^-S*+H}+?@HP67ncvx(RrCM-z^XBhN zV4f>hm)dKh8Q*Mr>MP;G)R`s0#iq~PGzRU9P&9J9xzDk0V5cbNs$wi!unz3f2gI)b zw79SbIm~~IJ~1>@u&)p6&XKuc0XdmAo32V+u8a(U}r7?I29i9Eput$zCZ_C6oV7*&Sf zzx()4+JD`){aQ`$u-4i)+a+Pj+Q(cg7NB=kDlSzc;)cegYdehjPSu;bQp^Gh#fm(K z7k^^}XJ{I~Q$9XJ0sXB?KPHqjQO8_&Vjeqokl&)WKMb~U`0f|QYYE&PxN$bbO1+8e zgb|bU3@s9m(T7C8?s;*9Ut`zKS)AM{P9}`6ac@)mnxBygR&7d;8{DfR#-L2S=6w;I zuuR4!3$|J&EWSAAsWWTL+CNKdOIL_h{~6=ja)GbmS~3GRzlw1XYcQabQMnGb`@<5n z^99b3>u}H?;O@5zzVkfMIY)^<`(HGDsqqu&!$fZ)7DkeaW2m^cCFASF_e)iE`Y_MK z_AmzYyoD`-35%a>8P!bOa^{N#EknNPwbQiW`Rjht{==(xXezhjpVg@qHBPJ1Mb~_5 zMgOil`ElnC702HzdS2Vaw~85>Mb4aYN|#1yVHL9PmzXFfh97g^T@2XG3GG71cJkaS zuENTOsX)JS=*oK;2l`((p@mYvSzO@XhNv8hGYnW*#P0AZ++a#tbR9R|cg>lDN~cHd z^QqXs1O`%_=}pa=DiiwRC!`p)Ybs3Qq(Jf6_jh;&hEqH|pXv z!yc?04{r!oRV=X_NA@<`P-2wKeM?xMUhAI~_bso$>ZI?}!C#ziF%vuIFmao4QoPZ+ zb%?4IXPDJG*ErZs%Ov2y>ta~;nAq2E6J1a-+9*!O*mjZ`_X7IY{h4E4*k9?OSdOz6 zd(Qm$_4LKj{}r(Qar9y6`&xX8b8|90q{hv6tz39p6MbC|zxmxynP2N2m_t2lhgMY6 z<&NI^%eI*?6vz>}bgQ|)k9Dverg(!m!1njWw@~vwA1-<(ep&`Gg)H?j29fv>`t)vi zVW(jdTioB6?|muVM7#U{o}88TXzs&WT!dW?h#Z>&CTy74sAZXy<+-3(lFljiqZm z_ZEAE@u_QLzj!hy&7H!jPaG1P#(iST90)%03-M$P`i-0w2mD44{6`-& zS{&f|d$ukW@5%JE(mx);Jw<7>+S>-8X~+F?&5QgE4~Pz5+uqWE4lwyQd?Ka26o9W9 z!F&tnZ}%s*?MObthBXwm0){5~Y(8sYr*9%&%${nK)FbHFFHp9gH*2}JZSS2wn*DF@ z_Z&Q`)pyU(y8DMrsg=CnHmL)xzeDUf*q`}t=-Xq|HFl(TIRw8U3IC$Z1b!Ch?M>T< zNCVn8RVL_K!VB=KCk{IAA#uW&w`tiWw(!X!xepYucgER9i&sLM1bF7MhR_OvQC12M zM``K!v$q8-xnDewen8&oUART}!uUEUHhBxl!B)Wrbd@3)l-;K2UN8n76VkDLB77

h>nsWG> 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%0DDgxp;@e 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 F0stJ!TS5Q; literal 0 HcmV?d00001 diff --git a/README.md b/README.md new file mode 100644 index 0000000..d94eeab --- /dev/null +++ b/README.md @@ -0,0 +1,171 @@ +# Octo Updater + +A standalone desktop updater and mod manager for the **OctoWoW** client. +It updates and patches the game client, manages community **mods**, **addons** +and **content patches**, applies client **tweaks**, and shows server **news**. + +![Octo Updater](screenshot.png) + +--- + +## Features + +### Game updates + +- Game files are synced over **BitTorrent** using a bundled, checksum-verified + `aria2c` — only missing or wrong-sized files are downloaded, transfers resume + automatically, and an optional integrity pass re-checks every piece hash to + catch same-size corruption. +- Legacy locale folders and old client patch archives are pruned automatically + after a sync. +- Reads and displays the installed game client version straight from `WoW.exe`. + +### Mods + +Curated set of client modifications, installed from their official GitHub / +Codeberg releases and registered in `dlls.txt`: + +| Mod | Purpose | +| ----------------------- | ----------------------------------------------------------------------------------- | +| AuctionQueryThrottle | Removes the fixed 5-second wait between auction house searches | +| ClassicAPI | Adds later-version Lua API calls to the client; required by some addons | +| DXVK | Vulkan-based rendering for better performance | +| Nampower | Reduces input lag on higher latency; expands the addon API | +| No1600x1200 | Fixes incorrect resolution when the monitor's native res isn't detected (optional) | +| SuperWoW | Backported client API features; required by some addons | +| TransmogFix | Fixes transmog-related frame drops on character death | +| UnitXP_SP3 | Frame limiter, improved targeting, anti-aliased combat text, and more | +| VanillaFixes | Eliminates stutter and animation lag (also the DLL loader; required by other mods) | +| VanillaHelpers | Raises the max supported texture resolution and improves memory allocation | +| VanillaMultiMonitorFix | Fixes multi-monitor resolution issues (optional) | + +- Essential mods auto-install on a fresh game folder, or on any game + folder when **Install essential mods** is enabled in Settings. +- Per-mod **update** / **retry** actions and an update-count badge on the tab. +- When VanillaFixes is installed, **PLAY** launches through it instead of + `WoW.exe` directly. + +### Tweaks + +Patches `WoW.exe` and writes `Config.wtf` for quality-of-life settings, +grouped by category: + +- **General** — in-game language (English, Deutsch, Русский, 中文, Español, + Português), auto-loot, nameplate range. +- **Camera** — field of view (with a recommended value per aspect ratio), + camera zoom distance. +- **Graphics** — world/terrain render distance, ground clutter distance. +- **Sound** — allow sound to play while the game is in the background. + +Invalid values are clamped; Apply/Reset appear only when something changed. + +### Addons + +- Installs addons directly from Git hosts (**GitHub, GitLab, Gitea, Codeberg**) + by downloading the repo archive pinned to a commit SHA — no Git client needed. +- Curated **recommended** list plus everything from the server catalog. +- **Add custom git addon** dialog for any allowed host; a custom addon is + never silently overridden by a same-named recommended one. +- Update detection by comparing the installed commit against the latest, + one-click **Update** / **Update all**, and an update-count badge. +- pfUI gets a curated **"Default"** profile injected and added to its firstrun + picker after each install/update. + +### MPQ patches + +Optional content patches distributed as `.mpq` files, separate from mods and +addons — currently **Octo Raid Visuals**, which adds ground markers and +sounds for boss abilities in raids. Each patch is downloaded over HTTPS and +verified against a published checksum before it's placed in the client's +`Data` folder; the tab flags available updates. + +### News + +Shows the latest **Announcements** post and the current **Patch Notes** list +from the server's forum feed. + +### Settings + +Change the game folder, check download mirror status, verify game files, +view session logs, and add the game folder to Defender exclusions. General +options include clearing the client's WDB cache on every game launch, +minimizing Octo Updater when the game launches, auto-installing essential +mods and recommended addons, and keeping a custom `speech.mpq` untouched by +verification/updates. + +### Security & robustness + +- Hardened TLS (system trust store, hostname check, TLS 1.2+ floor). +- HTTPS-only with per-host allowlists for all downloads; redirects stay HTTPS. +- `aria2c` itself is fetched once and SHA-256-verified before it's ever run. +- Atomic config writes (temp + rename) with a lock — safe against concurrent + workers and interrupted saves. +- Path-traversal-safe archive extraction. +- DPI-aware layout — the UI scales as a unit on high-DPI displays instead of + fonts overflowing a fixed-pixel layout. +- Automatic self-update check against this repo's GitHub releases (once a day). + +--- + +## Requirements + +- **Windows**, or **Linux via Proton/Wine** — Octo Updater is a Windows build, + but it runs on Linux under Proton/Wine. + A couple of Windows-only conveniences (Defender exclusions, DPI-scale + auto-detection) simply don't apply outside Windows. +- **Python 3.10+** — only if running from source. Runs on the standard + library, and will also use [`certifi`](https://pypi.org/project/certifi/) if + installed, for more robust TLS verification on machines with an out-of-date + root store (otherwise falls back to the system trust store). +- The prebuilt `OctoUpdater.exe` needs nothing installed. `aria2c` is + downloaded automatically the first time a game update runs. + +--- + +## Usage + +### Prebuilt executable + +Download `OctoUpdater.exe` from the [latest release](https://github.com/rebasedkon/octo-updater/releases/latest) and +run it. Point the **Game folder** (Settings) at your OctoWoW game folder — +or let the default create one next to the executable — then click **UPDATE**, +and **PLAY** when it finishes. + +### From source + +``` +python octo_updater.py +``` + +The updater keeps its data in a per-user app-data folder. + +--- + +## Building + +Compile a single-file Windows executable with [PyInstaller](https://pyinstaller.org/): + +``` +pip install pyinstaller certifi +pyinstaller --onefile --windowed --name OctoUpdater --icon OctoUpdater.ico octo_updater.py +``` + +Installing `certifi` before building bundles an up-to-date CA certificate set +into the executable, so TLS verification works even on machines whose Windows +root store is stale. `aria2c` does not need to be bundled — the app fetches +and verifies it itself on first run. + +--- + +## Support the Developer + +If Octo Updater is useful to you, consider supporting its development: + +- [Ko-fi](https://ko-fi.com/rebased) +- [Buy Me a Coffee](https://buymeacoffee.com/rebased) + +--- + +## License + +See [LICENSE](https://github.com/rebasedkon/octo-updater/blob/main/LICENSE). \ No newline at end of file diff --git a/octo_updater.py b/octo_updater.py new file mode 100644 index 0000000..8616442 --- /dev/null +++ b/octo_updater.py @@ -0,0 +1,6563 @@ +""" +Octo Updater — a standalone updater for the OctoWoW client: game files, +mods, tweaks and addons. Standard library, optional 'certifi' for TLS; +Python 3.10+. +""" + +import json +import hashlib +import os +import sys +import ssl +import re +import subprocess +import urllib.request +from urllib.parse import urlsplit +import shutil +import stat +import struct +import time +import math +import threading +import queue +from functools import cache +import tkinter as tk +from tkinter import filedialog +from pathlib import Path + +# ────────────────────────────────────────────────────────────────────────────── +# Constants +# ────────────────────────────────────────────────────────────────────────────── + +UPDATER_VERSION = "1.3.1" +SERVER = "https://octowow.st" +UA = f"OctoUpdater/{UPDATER_VERSION}" +DOWNLOAD_RETRY = 5 +DOWNLOAD_TIMEOUT = 10 # seconds without any data before a transfer aborts + +# Where the app lives: next to the .exe when frozen (PyInstaller), otherwise +# next to this script — never the current working directory, which varies with +# how the app was launched. This anchors the default game folder. +if getattr(sys, "frozen", False): + APP_DIR = os.path.dirname(os.path.abspath(sys.executable)) +else: + APP_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def _default_app_data_dir() -> str: + base = os.environ.get("LOCALAPPDATA") + if base: + path = os.path.join(base, "OctoUpdater") + try: + os.makedirs(path, exist_ok=True) + return path + except OSError: + pass + return APP_DIR + + +APP_DATA_DIR = _default_app_data_dir() + +CONFIG_FILE = os.path.join(APP_DATA_DIR, "config.json") + +# First-run default game folder, anchored to the app dir (not the CWD). +DEFAULT_GAME_DIR = os.path.join(APP_DIR, "OctoWoW") + + +def _relocate_legacy_data(): + # Old config (<1.3: octo_updater_config.json beside the app) becomes + # config.json in APP_DATA_DIR. Rename + relocate, only when the old name + # exists and the new path doesn't (idempotent). The legacy game-hash cache + # is no longer used and is deleted by _migrate_1_3. + old_name, new = "octo_updater_config.json", CONFIG_FILE + for old in (os.path.join(APP_DIR, old_name), + os.path.join(APP_DATA_DIR, old_name)): + if old == new or not os.path.exists(old) or os.path.exists(new): + continue + try: + shutil.move(old, new) + break + except OSError: + pass + +# News tab: the latest announcement (forum 2, full post) fills the left panel, +# the patch-notes list (forum 4) fills the right. +NEWS_FEATURED_URL = f"{SERVER}/forum/octonews.php?forum=2&mode=full" +PATCHNOTES_URL = f"{SERVER}/forum/octonews.php?mode=list&forum=4&limit=8" +NEWS_TIMEOUT = 8 +NEWS_CACHE_TTL = 300 + +# Design dimensions, authored at 96 DPI (100% scaling). At startup the app +# multiplies these — and every other hard-coded pixel value, via self._px() — +# by a single DPI scale factor, so the whole fixed-pixel layout scales as a +# unit instead of point-fonts overflowing fixed geometry on high-DPI displays. +WIN_W, WIN_H = 1000, 700 +FOOT_H = 130 + +C_BG = "#120e1a" +C_PANEL = "#161120" +C_PANEL_BDR = "#261d3a" +C_HDR = "#0d0a14" +C_DIVIDER = "#2a2142" +C_GOLD = "#c8922a" +C_GOLD_LT = "#e8b84b" +C_PURPLE = "#8a4fa5" +C_GREEN_BTN = "#4a7c2f" +C_GREEN_HOV = "#5a9438" +C_TEXT = "#d8d4cc" +C_TEXT_DIM = "#7a7670" +C_LOG_BG = "#0f0b16" +C_OK = "#6abf69" +C_ERR = "#bf6969" +C_MOD_HL = "#a8b83c" # olive-green highlight for installed mods + +# Parchment palette for the featured news post +C_PARCH = "#e9dcb8" +C_PARCH_BAND = "#ddcda0" +C_PARCH_LINE = "#c3b083" +C_PARCH_TITLE = "#7c5a12" +C_PARCH_TEXT = "#3a352a" +C_PARCH_DIM = "#8b8064" +C_PARCH_LINK = "#a3561c" +C_PARCH_EDGE = "#b7a678" + +FONT_BODY = ("Segoe UI", 9) +FONT_MONO = ("Consolas", 9) +FONT_VER = ("Segoe UI", 8) + + +# ────────────────────────────────────────────────────────────────────────────── +# Secure networking +# ────────────────────────────────────────────────────────────────────────────── + +# Hardened TLS: verify the server certificate against the system trust store, +# require the hostname to match, and refuse anything below TLS 1.2. This is +# the primary defence against a man-in-the-middle tampering with downloads. +SSL_CTX = ssl.create_default_context() +SSL_CTX.check_hostname = True +SSL_CTX.verify_mode = ssl.CERT_REQUIRED +# Trust certifi's curated roots *in addition to* the system store, so a stale +# or incomplete Windows root store (Python's ssl uses a static snapshot and +# never triggers Windows' on-demand root update) can't break verification. +# If certifi isn't bundled, fall back to the system store alone. +try: + import certifi + SSL_CTX.load_verify_locations(certifi.where()) +except Exception: + pass +try: + SSL_CTX.minimum_version = ssl.TLSVersion.TLSv1_2 +except (AttributeError, ValueError): + pass + +# Binaries may only be fetched from these hosts. TLS already stops a MITM from +# impersonating them; this additionally stops a tampered API response from +# redirecting a download (e.g. a mod DLL) to an unexpected host. +ALLOWED_DOWNLOAD_HOSTS = { + "octowow.st", + "dl.octowow.st", + "github.com", + "raw.githubusercontent.com", + "objects.githubusercontent.com", + "release-assets.githubusercontent.com", + "codeberg.org", +} + + +def _check_url(url: str, allowed_hosts): + """Enforce HTTPS and (optionally) an allowlist on a URL.""" + parts = urlsplit(url) + if parts.scheme != "https": + raise RuntimeError(f"Refusing non-HTTPS URL: {url}") + if allowed_hosts is not None: + host = (parts.hostname or "").lower() + if host not in allowed_hosts: + raise RuntimeError(f"Refusing download from unexpected host: {host}") + + +class _HttpsOnlyRedirectHandler(urllib.request.HTTPRedirectHandler): + """Require every redirect target to stay HTTPS (blocks an https→http + downgrade). The host allowlist is deliberately *not* re-applied on + redirects: an allowlisted host controls its own redirects — legitimately + to its CDN (e.g. octowow.st→dl.octowow.st, github.com→codeload) — and TLS + protects wherever it lands. The allowlist's job is to vet the *initial* + URL (against a tampered API response), which secure_urlopen still does.""" + def redirect_request(self, req, fp, code, msg, headers, newurl): + _check_url(newurl, None) # HTTPS-only, no host check + return super().redirect_request(req, fp, code, msg, headers, newurl) + + +# Shared opener with the hardened TLS context and the HTTPS-only redirect +# guard, built once. +_SECURE_OPENER = urllib.request.build_opener( + urllib.request.HTTPSHandler(context=SSL_CTX), + _HttpsOnlyRedirectHandler()) + + +def secure_urlopen(req, timeout, allowed_hosts=None): + """urlopen wrapper that enforces HTTPS + an optional host allowlist on the + initial URL, keeps redirects on HTTPS, and uses the hardened TLS context. + `req` may be a URL string or a urllib Request.""" + url = req.full_url if isinstance(req, urllib.request.Request) else req + _check_url(url, allowed_hosts) + return _SECURE_OPENER.open(req, timeout=timeout) + + +# Serializes all config read-modify-write cycles across the many worker +# threads (mods, addons, caches) and the main thread, so concurrent updates +# can't clobber each other. Reentrant so update_config() can call save. +_CONFIG_LOCK = threading.RLock() + + +def load_config() -> dict: + try: + with open(CONFIG_FILE) as f: + return json.load(f) + except FileNotFoundError: + return {} + except Exception: + return {} + + +def _atomic_write(path: str, text: str): + """Write via a temp file + atomic rename so a crash mid-write can never + leave a truncated/corrupt file at `path`.""" + tmp = path + ".tmp" + with open(tmp, "w") as f: + f.write(text) + f.flush() + os.fsync(f.fileno()) + os.replace(tmp, path) + + +def save_config(data: dict): + with _CONFIG_LOCK: + try: + _atomic_write(CONFIG_FILE, json.dumps(data, indent=2)) + except Exception: + pass + + +def update_config(mutator): + """Load the current on-disk config under the lock, apply `mutator(cfg)`, + save atomically, and return the result. Every config change — main thread + or worker — should go through this so no stale in-memory snapshot can + overwrite keys another thread just persisted.""" + with _CONFIG_LOCK: + cfg = load_config() + mutator(cfg) + save_config(cfg) + return cfg + + +def ensure_dir(path): + Path(path).mkdir(parents=True, exist_ok=True) + + +# ── logging ───────────────────────────────────────────────────────────────── +# One thread-safe log sink for the whole app. Any function — worker thread or +# main — calls log(); the GUI drains _LOG_Q on the main thread (see +# OctoUpdaterApp._poll) and renders each line. This keeps all Tk access on the +# main thread without threading a log_fn argument through every function. +_LOG_Q: queue.Queue = queue.Queue() + + +def log(msg: str, tag: str = ""): + """Append a line to the app log. Thread-safe; safe to call before the GUI + exists (the queue just buffers until it's drained).""" + _LOG_Q.put((msg, tag)) + + +def remove_wdb(client_dir: str): + """Delete the client's WDB folder (server-data cache, safe to drop).""" + wdb = os.path.join(client_dir, "WDB") + if not os.path.isdir(wdb): + return + try: + shutil.rmtree(wdb) + log("WDB cache cleared.", "dim") + except Exception as e: + log(f"Could not clear WDB: {e}", "err") + + +def get_client_version(out_dir: str) -> str: + """Read version + build from fixed offsets in the client's WoW.exe.""" + exe_path = os.path.join(out_dir, "WoW.exe") + if not os.path.exists(exe_path): + return "" + try: + # Read only the two small fields, not the whole ~5 MB binary. + with open(exe_path, "rb") as f: + f.seek(0x00437bfc) + build = f.read(4).decode("utf-8", errors="replace").rstrip("\x00") + f.seek(0x00437c04) + version = f.read(6).decode("utf-8", errors="replace").rstrip("\x00") + except Exception: + return "" + # Unexpected build, or an exe half-written mid-update, reads junk there - + # show nothing rather than garbage. A real read looks like version "1.12.1", + # build "5875". + if not re.fullmatch(r"\d+(?:\.\d+)+", version) or not build.isdigit(): + return "" + return f"{version} ({build})" + + +def fmt_size(num_bytes: float) -> str: + """Human-readable size: KB, MB, GB. Uses binary (1024) steps but the + familiar KB/MB/GB labels. Truncated, not rounded, so 8.7998 GiB reads as + '8.79 GB' instead of rounding up to 8.80 and overstating the size.""" + if num_bytes < 1024 ** 2: + return f"{int(num_bytes / 1024)} KB" + if num_bytes < 1024 ** 3: + return f"{int(num_bytes / 1024 ** 2 * 10) / 10:.1f} MB" + return f"{int(num_bytes / 1024 ** 3 * 100) / 100:.2f} GB" + + +def fmt_speed(bytes_per_sec: float) -> str: + if bytes_per_sec < 1024 * 1024: + return f"{bytes_per_sec / 1024:.0f} KB/s" + return f"{bytes_per_sec / 1024 / 1024:.1f} MB/s" + + +# ────────────────────────────────────────────────────────────────────────────── +# Torrent-based client sync (aria2c) +# ────────────────────────────────────────────────────────────────────────────── +# Client files are synced over BitTorrent with aria2c, fetched on first use. +# The download is differential: only files that are missing or the wrong size +# are pulled. + +CLIENT_TORRENT_URL = "https://dl.octowow.st/download/client.torrent" + +# Pinned aria2 Windows build. aria2 is GPLv2+, fetched and run unmodified; only +# aria2c.exe is used. The sha256 is of the release .zip (verified once). +ARIA2_ZIP_URL = ("https://github.com/aria2/aria2/releases/download/" + "release-1.37.0/aria2-1.37.0-win-32bit-build1.zip") +ARIA2_ZIP_SHA256 = "35f6514cc5dd7e98a87b3c4c2d25a0754b9b063dbe59bc0f22d483464f61e5b6" +ARIA2C_PATH = os.path.join(APP_DATA_DIR, "aria2c.exe") + +# The torrent's top-level folder name: aria2 writes files under

//…, +# so a junction /client → the real client dir lands them in place. +TORRENT_NAME = "client" +TORRENT_STAGING_DIR = os.path.join(APP_DATA_DIR, "torrent-root") +PRISTINE_WOW_PATH = os.path.join(APP_DATA_DIR, "base-WoW.exe") + +# The pristine (unpatched) WoW.exe carries 0xa1 at this offset; a patched one +# carries 0xb8 (see the locale patch). Used to cache a clean base for re-patch. +_LOCALE_ASSERT_OFFSET = 0x1b2115 + +# Game-language (WoW.exe locale) patch +# The language is switched by patching three spots in WoW.exe. +# LOCALE_NAMES is the exe's built-in 8-slot locale table (at +# 0x45591c - index*8). Selectable languages map to a slot index; ruRU and ptBR +# reuse the unused zhTW / xxYY slots and rename them. Offsets/bytes verified +# against the pristine base-WoW.exe. +_LOCALE_TAG_OFFSET = 0x1b2115 # the 0xa1/0xb8 assert instruction +_LOCALE_INDEX_OFFSET = 0x253c +_LOCALE_NAME_OFFSET = lambda i: 0x45591c - i * 8 +LOCALE_NAMES = ["enUS", "koKR", "frFR", "deDE", + "zhCN", "zhTW", "esES", "xxYY"] +# selectable code -> (slot index, display label), in menu order +LOCALES = { + "enUS": (0, "English"), + "deDE": (3, "Deutsch"), + "ruRU": (5, "Русский"), + "zhCN": (4, "中文 (简体)"), + "esES": (6, "Español"), + "ptBR": (7, "Português (BR)"), +} +DEFAULT_LOCALE = "enUS" + + +def locale_patches(locale: str): + """The three WoW.exe byte edits that force the given game language, as + (offset, bytes) tuples applied on top of the pristine base.""" + if locale not in LOCALES: + locale = DEFAULT_LOCALE + idx, _ = LOCALES[locale] + carrier = LOCALE_NAMES[idx] # exe slot name at this index + return [ + # assert instruction: mov eax, (was mov eax,[imm]) + (_LOCALE_TAG_OFFSET, + bytes([0xb8]) + bytes(carrier, "latin1")[::-1]), + # locale index: mov esi, ; jmp +0x1f + (_LOCALE_INDEX_OFFSET, + bytes([0xbe, idx, 0x00, 0x00, 0x00, 0xeb, 0x1f])), + # rename the slot's locale-name string to the selected tag + (_LOCALE_NAME_OFFSET(idx), bytes(locale, "latin1")), + ] + + +_NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0) + + +def ensure_aria2c(log_fn=log) -> str: + """Return the path to aria2c.exe, downloading + checksum-verifying it into + APP_DATA_DIR on first use. Raises on failure.""" + if os.path.exists(ARIA2C_PATH): + return ARIA2C_PATH + log_fn("Fetching aria2c (one-time, ~2.5 MB)…", "acct") + req = urllib.request.Request(ARIA2_ZIP_URL, headers={"User-Agent": UA}) + with secure_urlopen(req, timeout=60, allowed_hosts=ALLOWED_DOWNLOAD_HOSTS) as r: + data = r.read() + digest = hashlib.sha256(data).hexdigest() + if digest != ARIA2_ZIP_SHA256: + raise RuntimeError( + f"aria2 checksum mismatch (got {digest[:12]}…); refusing to run it") + import zipfile, io + with zipfile.ZipFile(io.BytesIO(data)) as zf: + name = next((n for n in zf.namelist() + if n.lower().endswith("aria2c.exe")), None) + if not name: + raise RuntimeError("aria2c.exe not found in the aria2 archive") + exe = zf.read(name) + ensure_dir(APP_DATA_DIR) + tmp = ARIA2C_PATH + ".part" + with open(tmp, "wb") as f: + f.write(exe) + os.replace(tmp, ARIA2C_PATH) + log_fn("aria2c ready.", "ok") + return ARIA2C_PATH + + +def _bdecode(buf: bytes, pos: int = 0): + """Minimal bencode decoder → (value, next_pos). Strings stay bytes.""" + ch = buf[pos] + if ch == 0x69: # ie + end = buf.index(b"e", pos) + return int(buf[pos + 1:end]), end + 1 + if ch == 0x6c: # le + lst, p = [], pos + 1 + while buf[p] != 0x65: + v, p = _bdecode(buf, p) + lst.append(v) + return lst, p + 1 + if ch == 0x64: # de + d, p = {}, pos + 1 + while buf[p] != 0x65: + k, p = _bdecode(buf, p) + v, p = _bdecode(buf, p) + d[k] = v + return d, p + 1 + colon = buf.index(b":", pos) # : + n = int(buf[pos:colon]) + start = colon + 1 + return buf[start:start + n], start + n + + +def fetch_torrent(url: str = CLIENT_TORRENT_URL) -> tuple: + """Download the .torrent → (raw_bytes, files). `files` is a list of + (path_parts, length). The raw bytes' SHA-1 is the client 'version'.""" + req = urllib.request.Request(url, headers={"User-Agent": UA}) + with secure_urlopen(req, timeout=DOWNLOAD_TIMEOUT, + allowed_hosts=ALLOWED_DOWNLOAD_HOSTS) as r: + raw = r.read() + decoded, _ = _bdecode(raw) + info = decoded.get(b"info", {}) + files = [([p.decode("latin1") for p in f.get(b"path", [])], + int(f.get(b"length", 0))) + for f in info.get(b"files", [])] + return raw, files + + +def torrent_version(raw: bytes) -> str: + """SHA-1 of the whole .torrent file — the client 'version'. Changes whenever + the torrent is re-rolled, so it's what we compare to detect a new build and + decide whether aria2's resume state is stale. An occasional re-check when + only the announce/date changed (content unchanged) is harmless.""" + return hashlib.sha1(raw).hexdigest() + + +@cache +def _torrent_excluded_files() -> frozenset: + """Lower-cased basenames of every file the Mods tab installs, across all + mods — the client-root files the torrent sync leaves to the mod system. + + The torrent ships several of them (VfPatcher.dll, nampower.dll, d3d9.dll, + …), but the Mods tab is the single source of truth for installing / + disabling / versioning them, so the sync must never fetch, re-add, or flag + them. Computed once and memoized (MODS_REGISTRY is static at import).""" + return frozenset( + os.path.basename(f).lower() + for mod in MODS_REGISTRY + for f in mod.get("installed_files", []) + ) + + +def _is_torrent_excluded(parts) -> bool: + return len(parts) == 1 and parts[0].lower() in _torrent_excluded_files() + + +def _torrent_skip(parts, ignore_speech: bool, client_dir: str) -> bool: + """Files the sync must leave alone: mod-owned client-root files, plus a + speech.MPQ the user opted to keep (Settings) — but only when it already + exists on disk. A missing speech.MPQ is still downloaded so a clean client + can launch; 'ignore' protects a custom copy, it doesn't skip a required file + that isn't there.""" + if _is_torrent_excluded(parts): + return True + if ignore_speech and parts and parts[-1].lower() == "speech.mpq": + return os.path.exists(os.path.join(client_dir, *parts)) + return False + + +# Files the sync must not rewrite (mod-owned client-root files, plus a kept +# custom speech.MPQ). Excluding them from --select-file stops aria2 fetching +# them on their own, but a torrent piece can straddle a file boundary, so +# repairing a selected neighbour re-downloads the shared piece and rewrites the +# excluded file's bytes too. We move them aside (rename) for the sync and put +# them back — instant and RAM-free, unlike copying a large speech.MPQ. +_SHIELD_SUFFIX = ".octobak" + + +def shield_protected_files(client_dir: str, files, ignore_speech: bool) -> list: + """Prepare each sync-protected file (see _torrent_skip) for the sync. An + existing one is moved aside via a same-dir rename; an absent one is recorded + with backup=None, because the sync must never create it (only the Mods tab + installs these) yet aria2 may write a partial stub for it through a shared + piece. Returns [(orig, backup_or_None)] for unshielding.""" + shielded = [] + for parts, _length in files: + if not _torrent_skip(parts, ignore_speech, client_dir): + continue + p = os.path.join(client_dir, *parts) + if os.path.exists(p): + bak = p + _SHIELD_SUFFIX + try: + os.replace(p, bak) # same-fs, instant; aria2 sees p missing + shielded.append((p, bak)) + except OSError: + pass + else: + shielded.append((p, None)) # must stay absent afterwards + return shielded + + +def unshield_protected_files(shielded) -> list: + """Move each shielded file back, or delete the stub the sync created for a + file that was absent before. Returns the basenames restored.""" + restored = [] + for p, bak in shielded: + if bak is None: + try: + os.remove(p) # drop aria2's partial stub; stay absent + except OSError: + pass + continue + try: + os.replace(bak, p) # our version wins over aria2's partial + restored.append(os.path.basename(p)) + except OSError: + pass + return restored + + +def recover_protected_files(client_dir: str, files): + """Undo a shield interrupted by a crash: an orphaned '.octobak' beside a + torrent file is the real file — move it back into place.""" + for parts, _length in files: + p = os.path.join(client_dir, *parts) + bak = p + _SHIELD_SUFFIX + if os.path.exists(bak): + try: + os.replace(bak, p) + except OSError: + pass + + +def torrent_selection(client_dir: str, files, drop_mismatched=False, + ignore_speech=False): + """1-indexed list of torrent files that are missing or the wrong size on + disk (aria2 --select-file), plus whether any were entirely missing.""" + need, missing = [], False + for i, (parts, length) in enumerate(files): + if _torrent_skip(parts, ignore_speech, client_dir): + continue + dest = os.path.join(client_dir, *parts) + try: + size = os.path.getsize(dest) + except OSError: + missing = True + need.append(i + 1) + continue + if size != length: + # oversized/corrupt file poisons resume — drop it; a short file is + # kept so aria2 can resume it + if drop_mismatched or size > length: + try: + os.remove(dest) + except OSError: + pass + need.append(i + 1) + return need, missing + + +def torrent_all_selection(client_dir: str, files, ignore_speech=False) -> list: + """1-indexed list of every non-mod file in the torrent. Used for an + integrity pass: aria2 --check-integrity verifies each selected file's piece + hashes and re-downloads only the bad/missing pieces — catching same-size but + corrupted files that the size-based torrent_selection can't.""" + return [i + 1 for i, (parts, _) in enumerate(files) + if not _torrent_skip(parts, ignore_speech, client_dir)] + + +def torrent_tree_intact(client_dir: str, files, ignore_speech=False) -> bool: + for parts, length in files: + if _torrent_skip(parts, ignore_speech, client_dir): + continue + try: + if os.path.getsize(os.path.join(client_dir, *parts)) != length: + return False + except OSError: + return False + return True + + +# Legacy leftovers the current client no longer ships. Locale data folders are +# matched by name; the old patch archives by name AND exact size, so a player +# mod that reused one of these names is never deleted. +_LOCALE_DATA_DIRS = { + "enus", "engb", "encn", "entw", "kokr", "frfr", "dede", "zhcn", + "zhtw", "eses", "esmx", "ruru", "ptbr", "ptpt", "itit", +} +_LEGACY_ARCHIVES = { + "patch-6.mpq": 451195806, + "patch-7.mpq": 175256564, + "patch-8.mpq": 484649870, + "patch-9.mpq": 506808141, + "patch-a.mpq": 241751337, +} + + +def prune_stale_client_files(client_dir: str, files) -> list: + """Remove legacy Data// folders and known old-client patch MPQs the + current torrent no longer ships. Folders are removed by name (unless the torrent + still uses them); archives only when the name AND the exact size match a known + legacy one. Returns removed names.""" + data_dir = os.path.join(client_dir, "Data") + if not os.path.isdir(data_dir): + return [] + # what the current torrent puts directly in Data/ (.mpq files and subdirs) + expected, used_dirs = set(), set() + for parts, _length in files: + if not parts or parts[0] != "Data": + continue + if len(parts) == 2 and parts[1].lower().endswith(".mpq"): + expected.add(parts[1].lower()) + elif len(parts) >= 3: + used_dirs.add(parts[1].lower()) + + removed = [] + for name in os.listdir(data_dir): + lc, full = name.lower(), os.path.join(data_dir, name) + if os.path.isdir(full): + if lc in _LOCALE_DATA_DIRS and lc not in used_dirs: + try: + shutil.rmtree(full) + removed.append(name + "/") + except OSError: + pass + continue + if not lc.endswith(".mpq") or lc in expected: + continue + try: + size = os.path.getsize(full) + except OSError: + continue + if _LEGACY_ARCHIVES.get(lc) == size: + try: + os.remove(full) + removed.append(name) + except OSError: + pass + return removed + + +# Downloadable MPQ content patches (not shipped with the client). Each is +# a single HTTP file with a `.sha256` sidecar; an update is available when +# that published sha differs from the on-disk file's. +MPQ_PATCHES = [ + { + "file": "patch-O.mpq", + "name": "Octo Raid Visuals", + "description": "Adds ground markers and sounds for boss abilities in raids.", + "url": "https://dl.octowow.st/client/latest/Data/patch-O.mpq", + }, +] + + +def mpq_patch_for(filename: str): + """The registry entry whose file matches `filename` (case-insensitive).""" + lc = filename.lower() + return next((e for e in MPQ_PATCHES if e["file"].lower() == lc), None) + + +def sha256_file(path: str) -> str: + h = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + h.update(chunk) + return h.hexdigest().lower() + + +def fetch_mpq_sha256(url: str) -> str: + """The published SHA256 of an MPQ patch, from its `.sha256` sidecar.""" + req = urllib.request.Request(url + ".sha256", headers={"User-Agent": UA}) + with secure_urlopen(req, timeout=NEWS_TIMEOUT, + allowed_hosts=ALLOWED_DOWNLOAD_HOSTS) as r: + return r.read().decode("ascii", "ignore").strip().split()[0].lower() + + +def download_mpq_patch(entry: dict, data_dir: str, on_progress=None): + """Download the patch into /, verifying its published + SHA256. Writes to a .part file and renames on success. on_progress(done, + total) is called as bytes arrive.""" + url = entry["url"] + want = fetch_mpq_sha256(url) + ensure_dir(data_dir) + tmp = os.path.join(data_dir, entry["file"] + ".part") + h = hashlib.sha256() + req = urllib.request.Request(url, headers={"User-Agent": UA}) + with secure_urlopen(req, timeout=DOWNLOAD_TIMEOUT, + allowed_hosts=ALLOWED_DOWNLOAD_HOSTS) as r: + total = int(r.headers.get("Content-Length") or 0) + done = 0 + with open(tmp, "wb") as f: + while True: + chunk = r.read(256 * 1024) + if not chunk: + break + f.write(chunk) + h.update(chunk) + done += len(chunk) + if on_progress: + on_progress(done, total) + if h.hexdigest().lower() != want: + try: + os.remove(tmp) + except OSError: + pass + raise RuntimeError("checksum verification failed") + os.replace(tmp, os.path.join(data_dir, entry["file"])) + + +def clear_torrent_resume_state(): + """Delete aria2's saved control (.aria2) and metadata (.torrent) files in + the staging dir. They pin a specific torrent revision (info-hash) and record + which pieces are 'done', so a stale one causes an 'info hash mismatch' error + after the client torrent is re-rolled, or blocks re-downloading a file the + user deleted. Safe to call when nothing is there.""" + try: + for name in os.listdir(TORRENT_STAGING_DIR): + if name.endswith((".aria2", ".torrent")): + try: + os.remove(os.path.join(TORRENT_STAGING_DIR, name)) + except OSError: + pass + except OSError: + pass + + +def refresh_pristine_wow(client_dir: str): + """Cache the freshly-synced (unpatched) WoW.exe as the pristine base, so a + later re-patch always starts from clean bytes.""" + exe = os.path.join(client_dir, "WoW.exe") + try: + with open(exe, "rb") as f: + f.seek(_LOCALE_ASSERT_OFFSET) + pristine = f.read(1) == b"\xa1" + if pristine: + shutil.copyfile(exe, PRISTINE_WOW_PATH) + log("Cached pristine WoW.exe base.", "dim") + except OSError: + pass + + +def read_pristine_wow(client_dir: str) -> bytes: + """The clean base to patch from: the cached pristine exe if present, else + the on-disk WoW.exe.""" + if os.path.exists(PRISTINE_WOW_PATH): + with open(PRISTINE_WOW_PATH, "rb") as f: + return f.read() + with open(os.path.join(client_dir, "WoW.exe"), "rb") as f: + return f.read() + + +def _ensure_torrent_junction(client_dir: str) -> str: + """Point /client at client_dir via an NTFS junction (no admin) so + aria2 writes the torrent's files straight into the real client dir. Returns + the staging dir to pass as aria2 --dir.""" + staging = TORRENT_STAGING_DIR + ensure_dir(staging) + link = os.path.join(staging, TORRENT_NAME) + target = os.path.abspath(client_dir) + try: + if os.path.isdir(link) and \ + os.path.abspath(os.path.realpath(link)) == target: + return staging + except OSError: + pass + # remove a stale junction/link (rmdir drops the reparse point, not its + # target's contents) then recreate it + try: + os.rmdir(link) + except OSError: + try: + os.remove(link) + except OSError: + pass + r = subprocess.run(["cmd", "/c", "mklink", "/J", link, target], + capture_output=True, text=True, creationflags=_NO_WINDOW) + if not os.path.isdir(link): + raise RuntimeError("could not create download junction: " + + (r.stderr or r.stdout or "").strip()) + return staging + + +_SIZE_UNITS = {"B": 1, "KiB": 1024, "MiB": 1024 ** 2, + "GiB": 1024 ** 3, "TiB": 1024 ** 4} +_UNIT = "|".join(_SIZE_UNITS) # B|KiB|MiB|GiB|TiB +_SIZE = rf"[\d.]+(?:{_UNIT})" # e.g. 8.8GiB +_FRAC = rf"({_SIZE})/({_SIZE})\((\d+)%\)" # done/total(percent%) +_ARIA_FRAC = re.compile(_FRAC) +_ARIA_DL = re.compile(rf"DL:({_SIZE})") +# During --check-integrity aria2 prints the hash-check progress in a separate +# field, e.g. '… [Checksum:#f66a3e 236MiB/1.5GiB(15%)]', while the leading +# completed/total stays at 0%/0B (a complete client downloads nothing). Read the +# Checksum fraction so the bar tracks the check instead of freezing at 0. +_ARIA_CHK = re.compile(rf"Checksum:#\w+\s+{_FRAC}") + + +def _to_bytes(s: str) -> float: + m = re.match(rf"^([\d.]+)({_UNIT})$", s.strip()) + return float(m.group(1)) * _SIZE_UNITS[m.group(2)] if m else 0.0 + + +def parse_aria_progress(line: str): + """Parse an aria2 summary line like '1.2GiB/8.8GiB(13%) … DL:5.0MiB' → + {progress, done, total, bps, checking}, or None for non-progress lines. + A checksum-check line is preferred over the (idle) download fraction.""" + chk = _ARIA_CHK.search(line) + m = chk or _ARIA_FRAC.search(line) + if not m: + return None + dl = _ARIA_DL.search(line) + return {"progress": int(m.group(3)) / 100.0, + "done": _to_bytes(m.group(1)), + "total": _to_bytes(m.group(2)), + "bps": _to_bytes(dl.group(1)) if dl else 0.0, + "checking": chk is not None} + + +# The currently-running aria2c child, so it can be killed when the app quits +# (a daemon worker thread dying would otherwise orphan it, still downloading +# headless). --stop-with-process is aria2's own belt-and-suspenders for this, +# but it's unreliable on Windows — hence the explicit kill too. +_active_aria2: "subprocess.Popen | None" = None +_active_aria2_lock = threading.Lock() + + +def stop_aria2c(): + """Terminate the running aria2c child, if any. Safe to call from any thread + (e.g. the app's close handler).""" + global _active_aria2 + with _active_aria2_lock: + proc, _active_aria2 = _active_aria2, None + if proc and proc.poll() is None: + try: + proc.terminate() + except Exception: + pass + + +def run_aria2c(client_dir, select_files=None, check_integrity=False, + on_progress=None, should_cancel=None, log_fn=log): + """Sync the client torrent into client_dir with aria2c (leech-only). Blocks + until aria2c exits; raises on a non-zero exit or cancellation. Calls + on_progress(dict) per update and should_cancel()->bool to abort. + + aria2 is handed the .torrent URL (not a local copy), so it always fetches + the server's current torrent at download time — no chance of running a stale + local .torrent if the user starts the update long after the verify.""" + global _active_aria2 + exe = ensure_aria2c(log_fn) + staging = _ensure_torrent_junction(client_dir) + args = [ + exe, + f"--dir={staging}", + # aria2 exits when this PID (the updater) does — stops an orphaned + # download if we're killed before the explicit stop_aria2c() runs. + f"--stop-with-process={os.getpid()}", + "--seed-time=0", + f"--check-integrity={'true' if check_integrity else 'false'}", + "--bt-remove-unselected-file=false", + "--continue=true", + "--allow-overwrite=true", + "--auto-file-renaming=false", + "--file-allocation=none", + "--disk-cache=128M", + "--stream-piece-selector=inorder", + "--max-tries=0", + "--retry-wait=5", + "--bt-stop-timeout=120", + "--auto-save-interval=15", + "--summary-interval=1", + "--human-readable=false", + "--truncate-console-readout=false", + "--console-log-level=warn", + "--enable-dht=true", + "--bt-enable-lpd=true", + "--max-connection-per-server=8", + "--split=16", + "--min-split-size=1M", + ] + if select_files: + args.append("--select-file=" + ",".join(str(i) for i in select_files)) + args.append(CLIENT_TORRENT_URL) + + proc = subprocess.Popen(args, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, + bufsize=1, creationflags=_NO_WINDOW) + with _active_aria2_lock: + _active_aria2 = proc + try: + for line in proc.stdout: + if should_cancel and should_cancel(): + proc.terminate() + raise RuntimeError("Cancelled") + line = line.strip() + if not line: + continue + p = parse_aria_progress(line) + if p: + # aria2's console readout floods idle '0B/total(0%)' lines (no + # bytes, no checksum) between the once-a-second summary blocks + # that carry the real figure. Drop the idle ones so they can't + # stomp progress back to 0 under the UI's latest-wins draining. + informative = (p["done"] or p["progress"] + or p["bps"] or p["checking"]) + if on_progress and informative: + on_progress(p) + else: + log_fn(f"[aria2] {line}", "dim") + finally: + try: + proc.stdout.close() + except Exception: + pass + with _active_aria2_lock: + if _active_aria2 is proc: + _active_aria2 = None + code = proc.wait() + if code != 0: + raise RuntimeError(f"aria2c exited with code {code}") + + +class VerifyWorker: + def __init__(self, out_dir: str, log_q: queue.Queue, prog_q: queue.Queue): + self.out_dir = out_dir + self.log_q = log_q + self.prog_q = prog_q + self._cancel = False + + def cancel(self): + self._cancel = True + + def log(self, msg, tag=""): + self.log_q.put((msg, tag)) + + def progress(self, value, label=""): + self.prog_q.put((value, label)) + + def run(self): + try: + self.log("Checking for updates…", "acct") + raw, files = fetch_torrent() + self.log("Checking game files…", "acct") + + # aria2 selects by size (a patched WoW.exe keeps the torrent's size, + # so it's never flagged); need == files missing or wrong-sized. + ignore_speech = bool(load_config().get("ignore_speech", False)) + need, _missing = torrent_selection(self.out_dir, files, + ignore_speech=ignore_speech) + have_exe = os.path.exists(os.path.join(self.out_dir, "WoW.exe")) + + if have_exe and not need: + self.log("Everything is up to date!", "ok") + self.log_q.put(("__UP_TO_DATE__", "")) + else: + self.log("Update available.", "acct") + self.log_q.put(("__UPDATE_NEEDED__", "")) + except Exception as e: + self.log(f"Verification failed: {e}", "err") + self.log_q.put(("__UPDATE_NEEDED__", "")) + + +class UpdateWorker: + def __init__(self, out_dir: str, log_q: queue.Queue, prog_q: queue.Queue, + check_integrity: bool = False, overwrite_config: bool = False): + self.out_dir = out_dir + self.log_q = log_q + self.prog_q = prog_q + self._cancel = False + # Integrity mode: aria2 hash-checks every file's pieces and repairs + # them (catches same-size corruption size-based selection misses). + self.check_integrity = check_integrity + # Write a fresh Config.wtf on a reconcile. + self.overwrite_config = overwrite_config + + def cancel(self): + self._cancel = True + + def log(self, msg: str, tag: str = ""): + self.log_q.put((msg, tag)) + + def progress(self, value: float, label: str = "", status: str | None = None): + # status (when given) updates the big status line; None leaves it as-is. + self.prog_q.put((value, label, status)) + + def build_tweaks(self, buf, tweaks: dict | None = None): + if tweaks is None: + tweaks = load_tweaks_config() + + fov_deg = tweaks.get("fieldOfView", TWEAKS_DEFAULTS["fieldOfView"]) + fov = fov_deg * (math.pi / 180.0) + flags = struct.unpack_from(" tuple: + """'v1.2.0' → (1, 2, 0); non-numeric parts become 0.""" + parts = [] + for p in (v or "").strip().lstrip("vV").split("."): + digits = "".join(ch for ch in p if ch.isdigit()) + parts.append(int(digits) if digits else 0) + return tuple(parts) or (0,) + + +def fetch_updater_latest_tag(force: bool = False) -> str | None: + """Latest release tag of the updater's own repo, cached for a day. Returns + None when there are no releases yet (GitHub 404) or on any error.""" + now = time.time() + if not force: + entry = load_config().get("updater_release_cache", {}) + if (entry.get("tag") is not None + and (now - entry.get("timestamp", 0)) < UPDATER_CHECK_TTL): + return entry["tag"] + try: + req = urllib.request.Request( + f"{GITHUB_API}/repos/{UPDATER_REPO}/releases/latest", + headers={"User-Agent": UA}) + with secure_urlopen(req, timeout=10) as r: + tag = json.load(r).get("tag_name") + except Exception: + return None + if tag: + update_config(lambda c: c.__setitem__( + "updater_release_cache", {"timestamp": now, "tag": tag})) + return tag + + +def updater_update_available(latest_tag: str) -> bool: + if not latest_tag: + return False + a, b = _parse_version(latest_tag), _parse_version(UPDATER_VERSION) + n = max(len(a), len(b)) # zero-pad so 1.1 == 1.1.0 + a += (0,) * (n - len(a)) + b += (0,) * (n - len(b)) + return a > b + + +def _codeberg_latest(owner: str, repo: str, raise_errors=False) -> dict | None: + url = f"https://codeberg.org/api/v1/repos/{owner}/{repo}/releases?limit=10&pre-release=false" + try: + req = urllib.request.Request(url, headers={"User-Agent": UA}) + with secure_urlopen(req, timeout=10) as r: + releases = json.load(r) + for rel in releases: + if not rel.get("prerelease", False) and not rel.get("draft", False): + return rel + return releases[0] if releases else None + except Exception as e: + if raise_errors: + raise RuntimeError(_describe_net_error(e)) from e + return None + + +DXVK_CONF_CONTENT = """# Low latency - limit queued frames - helps input lag +d3d9.maxFrameLatency = 1 +# Forces clamp for AF through DXVK - if you see grass textures shimmering or shaking try false +d3d9.clampNegativeLodBias = True +# Disable logging for performance +dxvk.logLevel = none +# Overrides synchronization interval (Vsync) for presentation. +# Setting this to 0 disables vertical synchronization entirely. +# A positive value 'n' will enable Vsync and repeat the same +# image n times, and a negative value will have no effect. +# +# Value -1 uses the application's setting +d3d9.presentInterval = -1 +# VanillaFix handles DPI awareness; avoid double-scaling +d3d9.dpiAware = False +# Enable GPL if supported to reduce stuttering (NVIDIA 473.33+, AMD 24.6.1+) +dxvk.enableGraphicsPipelineLibrary = Auto +# Track pipeline lifetimes to reduce memory usage +dxvk.trackPipelineLifetime = True +# Limit compiler threads to reduce memory usage +dxvk.numCompilerThreads = 2 +""" + + +def _write_dxvk_conf(client_dir: str): + path = os.path.join(client_dir, "dxvk.conf") + with open(path, "w", encoding="utf-8") as f: + f.write(DXVK_CONF_CONTENT) + log(" Wrote dxvk.conf") + + +def _github_latest(owner: str, repo: str, raise_errors=False) -> dict | None: + url = f"{GITHUB_API}/repos/{owner}/{repo}/releases/latest" + try: + req = urllib.request.Request(url, headers={"User-Agent": UA}) + with secure_urlopen(req, timeout=10) as r: + return json.load(r) + except Exception as e: + if raise_errors: + raise RuntimeError(_describe_net_error(e)) from e + return None + + +def _pick_asset(assets: list, pattern: str, prefer_no) -> dict | None: + import fnmatch + candidates = [a for a in assets if fnmatch.fnmatch(a["name"], pattern)] + if prefer_no: + preferred = [a for a in candidates if prefer_no not in a["name"]] + if preferred: + candidates = preferred + return candidates[0] if candidates else None + + +def _release_version(mod: dict, rel: dict) -> str | None: + """Version string for a github/codeberg release. Normally the tag name — + but some mods (e.g. SuperWoW) keep a static tag and edit the release in + place, so their tag never changes. For those, derive the version from the + matched asset instead: its filename embeds the real version.""" + src = mod["source"] + if src.get("version_from") == "asset": + asset = _pick_asset(rel.get("assets", []), src["asset_pattern"], + src.get("prefer_no")) + if asset and asset.get("name"): + import re + m = re.search(r"\d+(?:[._]\d+)+", asset["name"]) + return m.group(0) if m else asset["name"] + return rel.get("tag_name") + + +def fetch_mod_latest_version(mod: dict) -> str | None: + src = mod["source"] + kind = src["kind"] + if kind == "github_release": + rel = _github_latest(src["owner"], src["repo"]) + if rel: + return _release_version(mod, rel) + elif kind == "codeberg_release": + rel = _codeberg_latest(src["owner"], src["repo"]) + if rel: + return _release_version(mod, rel) + elif kind in ("direct_file", "direct_tar"): + return src.get("pinned_version") + return None + + +_MOD_VERSION_CACHE_TTL = 3600 + + +def _slim_release(rel: dict) -> dict: + """Reduce an API release object to the fields the updater actually uses, + so the persisted cache stays small.""" + return { + "tag_name": rel.get("tag_name"), + "assets": [{"name": a.get("name"), + "size": a.get("size", 0), + "browser_download_url": a.get("browser_download_url")} + for a in rel.get("assets", [])], + } + + +def _fetch_release_cached(mod: dict, force: bool = False) -> dict | None: + """Latest-release lookup backed by a persistent cache in the config file + ({"mod_release_cache": {mod_id: {"timestamp": epoch, "release": {…}}}}), + so restarts within the TTL don't re-hit the GitHub/Codeberg APIs.""" + src = mod["source"] + kind = src["kind"] + if kind not in ("github_release", "codeberg_release"): + return None + mid = mod["id"] + now = time.time() + if not force: + entry = load_config().get("mod_release_cache", {}).get(mid) + if entry and (now - entry.get("timestamp", 0)) < _MOD_VERSION_CACHE_TTL: + return entry.get("release") + if kind == "github_release": + rel = _github_latest(src["owner"], src["repo"]) + else: + rel = _codeberg_latest(src["owner"], src["repo"]) + if rel is None: + return None + rel = _slim_release(rel) + update_config(lambda c: c.setdefault("mod_release_cache", {}).__setitem__( + mid, {"timestamp": now, "release": rel})) + return rel + + +def fetch_mod_latest_version_cached(mod: dict, force: bool = False) -> str | None: + src = mod["source"] + kind = src["kind"] + if kind in ("direct_file", "direct_tar"): + return src.get("pinned_version") + rel = _fetch_release_cached(mod, force=force) + if rel: + return _release_version(mod, rel) + return None + + +def install_mod(mod: dict, client_dir: str, release: dict | None = None) -> list: + src = mod["source"] + written = [] + + if src["kind"] == "codeberg_release": + rel = release if release is not None else \ + _codeberg_latest(src["owner"], src["repo"], raise_errors=True) + if not rel: + raise RuntimeError("no release found on Codeberg") + import fnmatch + assets = rel.get("assets", []) + asset = next( + (a for a in assets if fnmatch.fnmatch(a["name"], src["asset_pattern"]) + and (not src.get("prefer_no") or src["prefer_no"] not in a["name"])), + None + ) + if not asset: + raise RuntimeError( + f"No matching asset '{src['asset_pattern']}' in {mod['id']} release") + log(f" Downloading {asset['name']} ({asset['size']//1024} KB)...") + req = urllib.request.Request(asset["browser_download_url"], + headers={"User-Agent": UA}) + with secure_urlopen(req, timeout=120, + allowed_hosts=ALLOWED_DOWNLOAD_HOSTS) as r: + data = r.read() + if src.get("extract_map") is None: + dest_rel = asset["name"] + dest = os.path.join(client_dir, dest_rel) + os.makedirs(os.path.dirname(dest) or client_dir, exist_ok=True) + with open(dest, "wb") as f: + f.write(data) + written.append(dest_rel) + log(f" Installed {dest_rel}") + else: + import zipfile + tmp_path = os.path.join(client_dir, f"_mod_tmp_{mod['id']}.zip") + try: + with open(tmp_path, "wb") as f: + f.write(data) + with zipfile.ZipFile(tmp_path) as zf: + for zip_path, dest_rel in src["extract_map"].items(): + try: + zip_data = zf.read(zip_path) + except KeyError: + log(f" Warning: {zip_path} not in zip, skipping") + continue + dest = os.path.join(client_dir, dest_rel) + os.makedirs(os.path.dirname(dest) or client_dir, exist_ok=True) + with open(dest, "wb") as f: + f.write(zip_data) + written.append(dest_rel) + log(f" Installed {dest_rel}") + finally: + if os.path.exists(tmp_path): + os.unlink(tmp_path) + + elif src["kind"] == "github_release": + rel = release if release is not None else \ + _github_latest(src["owner"], src["repo"], raise_errors=True) + if not rel: + raise RuntimeError("no release found on GitHub") + asset = _pick_asset(rel.get("assets", []), src["asset_pattern"], src["prefer_no"]) + if not asset: + raise RuntimeError( + f"No matching asset '{src['asset_pattern']}' in {mod['id']} release") + log(f" Downloading {asset['name']} ({asset['size']//1024} KB)...") + req = urllib.request.Request(asset["browser_download_url"], + headers={"User-Agent": UA}) + with secure_urlopen(req, timeout=120, + allowed_hosts=ALLOWED_DOWNLOAD_HOSTS) as r: + data = r.read() + + if src.get("extract_map") is None: + dest_rel = asset["name"] + dest = os.path.join(client_dir, dest_rel) + os.makedirs(os.path.dirname(dest) or client_dir, exist_ok=True) + with open(dest, "wb") as f: + f.write(data) + written.append(dest_rel) + log(f" Installed {dest_rel}") + elif asset["name"].endswith(".tar.gz") or asset["name"].endswith(".tgz"): + import tarfile, io, fnmatch as _fnmatch + with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as tf: + all_names = tf.getnames() + for pattern, dest_rel in src["extract_map"].items(): + matched = ( + pattern if pattern in all_names + else next( + (n for n in all_names if _fnmatch.fnmatch(n, pattern)), + None + ) + ) + if matched is None: + log(f" Warning: no file matching '{pattern}' in tar, skipping") + continue + f_obj = tf.extractfile(tf.getmember(matched)) + tar_data = f_obj.read() + dest = os.path.join(client_dir, dest_rel) + os.makedirs(os.path.dirname(dest) or client_dir, exist_ok=True) + with open(dest, "wb") as f: + f.write(tar_data) + written.append(dest_rel) + log(f" Installed {dest_rel}") + else: + import zipfile + tmp_path = os.path.join(client_dir, f"_mod_tmp_{mod['id']}.zip") + try: + with open(tmp_path, "wb") as f: + f.write(data) + with zipfile.ZipFile(tmp_path) as zf: + for zip_path, dest_rel in src["extract_map"].items(): + try: + zip_data = zf.read(zip_path) + except KeyError: + log(f" Warning: {zip_path} not found in zip, skipping") + continue + dest = os.path.join(client_dir, dest_rel) + os.makedirs(os.path.dirname(dest) or client_dir, exist_ok=True) + with open(dest, "wb") as f: + f.write(zip_data) + written.append(dest_rel) + log(f" Installed {dest_rel}") + finally: + if os.path.exists(tmp_path): + os.unlink(tmp_path) + + elif src["kind"] == "direct_tar": + log(f" Downloading {src['url'].split('/')[-1]}...") + req = urllib.request.Request(src["url"], headers={"User-Agent": UA}) + with secure_urlopen(req, timeout=120, + allowed_hosts=ALLOWED_DOWNLOAD_HOSTS) as r: + data = r.read() + import tarfile, io as _io, fnmatch as _fnmatch + with tarfile.open(fileobj=_io.BytesIO(data), mode="r:gz") as tf: + all_names = tf.getnames() + for pattern, dest_rel in src["extract_map"].items(): + matched = pattern if pattern in all_names else next( + (n for n in all_names if _fnmatch.fnmatch(n, pattern)), None) + if matched is None: + log(f" Warning: no file matching '{pattern}' in tar, skipping") + continue + tar_data = tf.extractfile(tf.getmember(matched)).read() + dest = os.path.join(client_dir, dest_rel) + os.makedirs(os.path.dirname(dest) or client_dir, exist_ok=True) + with open(dest, "wb") as f: + f.write(tar_data) + written.append(dest_rel) + log(f" Installed {dest_rel}") + if src.get("pinned_version"): + mod["_resolved_version"] = src["pinned_version"] + + elif src["kind"] == "direct_file": + url = src["url"] + log(f" Downloading {url.rsplit('/', 1)[-1]}...") + req = urllib.request.Request(url, headers={"User-Agent": UA}) + with secure_urlopen(req, timeout=120, + allowed_hosts=ALLOWED_DOWNLOAD_HOSTS) as r: + data = r.read() + + if src.get("extract_map") is None: + dest_rel = src["dest"] + dest = os.path.join(client_dir, dest_rel) + os.makedirs(os.path.dirname(dest) or client_dir, exist_ok=True) + with open(dest, "wb") as f: + f.write(data) + written.append(dest_rel) + log(f" Installed {dest_rel}") + elif url.endswith(".tar.gz") or url.endswith(".tgz"): + import tarfile, io, fnmatch as _fnmatch + with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as tf: + all_names = tf.getnames() + for pattern, dest_rel in src["extract_map"].items(): + matched = ( + pattern if pattern in all_names + else next( + (n for n in all_names if _fnmatch.fnmatch(n, pattern)), + None + ) + ) + if matched is None: + log(f" Warning: no file matching '{pattern}' in tar, skipping") + continue + f_obj = tf.extractfile(tf.getmember(matched)) + tar_data = f_obj.read() + dest = os.path.join(client_dir, dest_rel) + os.makedirs(os.path.dirname(dest) or client_dir, exist_ok=True) + with open(dest, "wb") as f: + f.write(tar_data) + written.append(dest_rel) + log(f" Installed {dest_rel}") + else: + import zipfile + tmp_path = os.path.join(client_dir, f"_mod_tmp_{mod['id']}.zip") + try: + with open(tmp_path, "wb") as f: + f.write(data) + with zipfile.ZipFile(tmp_path) as zf: + for zip_path, dest_rel in src["extract_map"].items(): + try: + zip_data = zf.read(zip_path) + except KeyError: + log(f" Warning: {zip_path} not in zip, skipping") + continue + dest = os.path.join(client_dir, dest_rel) + os.makedirs(os.path.dirname(dest) or client_dir, exist_ok=True) + with open(dest, "wb") as f: + f.write(zip_data) + written.append(dest_rel) + log(f" Installed {dest_rel}") + finally: + if os.path.exists(tmp_path): + os.unlink(tmp_path) + + if src.get("pinned_version"): + mod["_resolved_version"] = src["pinned_version"] + + for hook in src.get("post_install", []): + if hook == "write_dxvk_conf": + _write_dxvk_conf(client_dir) + written.append("dxvk.conf") + + return written + + +def uninstall_mod(mod: dict, client_dir: str): + cfg = load_config() + state = cfg.get("mods", {}).get(mod["id"], {}) + files = state.get("installed_files", mod.get("installed_files", [])) + for rel in files: + full = os.path.join(client_dir, rel) + if os.path.exists(full): + os.remove(full) + log(f" Removed {rel}") + + +def _dlls_txt_path(client_dir: str) -> str: + return os.path.join(client_dir, "dlls.txt") + + +def add_dll(client_dir: str, name: str): + path = _dlls_txt_path(client_dir) + lines = open(path).read().splitlines() if os.path.exists(path) else [] + if any(l.strip().lower() == name.lower() for l in lines): + return + lines = [l for l in lines if l.strip()] + [name] + with open(path, "w") as f: + f.write("\n".join(lines) + "\n") + + +def remove_dll(client_dir: str, name: str): + path = _dlls_txt_path(client_dir) + if not os.path.exists(path): + return + lines = [l for l in open(path).read().splitlines() + if l.strip().lower() != name.lower()] + if not lines: + os.remove(path) + else: + with open(path, "w") as f: + f.write("\n".join(lines) + "\n") + + +def mod_installed_files_present(mod: dict, client_dir: str) -> bool: + cfg = load_config() + state = cfg.get("mods", {}).get(mod["id"], {}) + files = state.get("installed_files", []) + return bool(files) and all( + os.path.exists(os.path.join(client_dir, f)) for f in files) + + +def mod_supports_update_check(mod: dict) -> bool: + return mod["source"]["kind"] not in ("direct_file", "direct_tar") + + +def mod_update_available(mod: dict, state: dict, live: dict | None) -> bool: + if not mod_supports_update_check(mod): + return False + if not state.get("enabled", False): + return False + if state.get("ignore_updates", False): + return False + installed_ver = state.get("installed_version") + if not installed_ver: + return False + latest_ver = (live or {}).get("latest_version") + if not latest_ver: + return False + return latest_ver != installed_ver + + +# ────────────────────────────────────────────────────────────────────────────── +# Addons definition & engine (installs via git-archive zips pinned to a +# commit sha, so no full git client is needed) +# ────────────────────────────────────────────────────────────────────────────── + +ADDONS_URL = f"{SERVER}/api/addons.json" +ADDONS_CATALOG_TTL = 86400 # 1 day, persisted in the config file +ADDON_SHA_CACHE_TTL = 3600 +ADDONS_VERIFY_TTL = 300 # skip re-verify on tab switches within this + +# Curated recommended addons: {folder_name: git_url}. Every entry carries +# its git link explicitly, so a recommended addon always appears — even if +# addons.json renames or drops it. Where the link differs from the catalog, +# it's a deliberate fork preference. +RECOMMENDED_ADDONS = { + "AtlasLoot": "https://github.com/Otari98/AtlasLoot", + "aux-addon": "https://github.com/OldManAlpha/aux-addon", + "BetterCharacterStats": "https://github.com/pepopo978/BetterCharacterStats", + "DoiteAuras": "https://github.com/deceius/DoiteAuras", + "FlightTracker": "https://github.com/Lexxoi/FlightTracker", + "InstanceJournal": "https://github.com/Arthur-Helias/InstanceJournal", + "ItemRack": "https://github.com/Otari98/ItemRack", + "LevelRange-Octo": "https://github.com/Dusk-92/LevelRange-Octo", + "Magnify": "https://github.com/paokkerkir/Magnify", + "ModernMapMarkers": "https://github.com/tilare/ModernMapMarkers", + "NampowerSettings": "https://github.com/Dusk-92/NampowerSettings", + "PallyPowerTW": "https://github.com/ShikawaLePaladin/PallyPowerTW", + "pfQuest": "https://github.com/The-Kludge-Bureau/pfQuest", + "pfQuest-turtle": "https://github.com/KameleonUK/pfQuest-turtle", + "pfUI": "https://github.com/brues-code/pfUI", + "ShaguDPS": "https://github.com/shagu/ShaguDPS", + "SUCC-bag": "https://github.com/Otari98/SUCC-bag", + "SuperAPI": "https://github.com/balakethelock/SuperAPI", + "SuperCleveRoidMacros": "https://github.com/brues-code/SuperCleveRoidMacros", + "Tmog": "https://github.com/Otari98/Tmog", + "TrinketMenu": "https://github.com/jrc13245/TrinketMenu", + "TurtleCalendar": "https://github.com/Wayoff333/TurtleCalendar", + "TurtleMail": "https://github.com/sica42/TurtleMail", + "UnitXP_SP3_Addon": "https://github.com/rebasedkon/UnitXP_SP3_Addon", + "WhatsTraining_Turtle": "https://github.com/rebasedkon/WhatsTraining_Turtle", +} + +# Never shown in the updater, even when present in addons.json. +BLOCKED_ADDONS = { + "SuperMacro", # SuperMacro - SuperWoW Support + "Rested", # Rested XP (hazlema) + "LevelRange-Turtle", # LevelRange [Turtle] — LevelRange-Octo replaces it + "CleveRoidMacros", # SuperCleveRoidMacros replaces it + "BlizzPlates", # Blizzard Plates + "PallyPower", # CosminPOP PallyPower — PallyPowerTW replaces it +} + + +def _same_git_repo(a, b) -> bool: + """Compare git URLs ignoring a trailing '.git' / slash and case.""" + def norm(u): + u = (u or "").rstrip("/") + return (u[:-4] if u.endswith(".git") else u).lower() + return norm(a) == norm(b) + +ADDON_GIT_HOSTS = ("github.com", "gitlab.com", "gitea.com", "codeberg.org") + +ADDON_ZIP_HOSTS = {"github.com", "codeload.github.com", "gitlab.com", + "gitea.com", "codeberg.org"} + + +def addons_path(client_dir: str) -> str: + return os.path.join(client_dir, "Interface", "AddOns") + + +def is_allowed_git_url(url: str) -> bool: + try: + parts = urlsplit(url) + except ValueError: + return False + if parts.scheme != "https": + return False + host = (parts.hostname or "").lower() + return any(host == h or host.endswith("." + h) for h in ADDON_GIT_HOSTS) + + +def _slim_addon_catalog(catalog: list) -> list: + """Keep only the catalog fields the updater actually uses, so the + persisted cache stays small.""" + slim = [] + for a in catalog: + toc = a.get("toc") or {} + slim.append({ + "name": a.get("name"), + "git": a.get("git"), + "branch": a.get("branch"), + "ref": a.get("ref"), + "description": a.get("description"), + "toc": {k: toc[k] for k in ("Title", "Notes", "Interface") + if k in toc}, + }) + return slim + + +def fetch_addons_catalog(force=False) -> list: + """Addon catalog, cached in the config file for a day + ({"addons_catalog_cache": {"timestamp": epoch, "catalog": […]}}).""" + now = time.time() + if not force: + entry = load_config().get("addons_catalog_cache", {}) + if (entry.get("catalog") is not None + and (now - entry.get("timestamp", 0)) < ADDONS_CATALOG_TTL): + return entry["catalog"] + req = urllib.request.Request(ADDONS_URL, headers={"User-Agent": UA}) + with secure_urlopen(req, timeout=10) as r: + catalog = _slim_addon_catalog(json.load(r)) + update_config(lambda c: c.__setitem__( + "addons_catalog_cache", {"timestamp": now, "catalog": catalog})) + return catalog + + +def read_toc_file(path: str) -> dict: + """Parse '## Key: Value' metadata lines from a WoW addon .toc file.""" + toc = {} + try: + with open(path, "r", encoding="utf-8", errors="replace") as f: + content = f.read() + except OSError: + return toc + if content.startswith("\ufeff"): # strip UTF-8 BOM + content = content[1:] + for line in content.splitlines(): + if not line.startswith("## "): + continue + key, sep, value = line[3:].partition(":") + if sep: + toc[key.strip()] = value.strip() + return toc + + +def parse_wow_colored(text: str): + """Split a string containing WoW colour escapes (|cAARRGGBB … |r) into + [(segment, "#rrggbb" | None), …] for rendering.""" + import re + segments = [] + color = None + pos = 0 + for m in re.finditer(r"\|c[0-9a-fA-F]{8}|\|r", text): + if m.start() > pos: + segments.append((text[pos:m.start()], color)) + tok = m.group(0) + color = f"#{tok[4:]}" if tok.startswith("|c") else None + pos = m.end() + if pos < len(text): + segments.append((text[pos:], color)) + return [(t, c) for t, c in segments if t] + + +def strip_wow_colors(text: str) -> str: + return "".join(t for t, _c in parse_wow_colored(text)) + + +def _git_parts(git_url: str): + """→ (kind, repo_url, owner, repo, api_base); kind ∈ github/gitlab/gitea. + Handles path prefixes like octowow.st/git//.""" + parts = urlsplit(git_url) + host = (parts.hostname or "").lower() + segs = [s for s in parts.path.split("/") if s] + if len(segs) < 2: + raise ValueError(f"Unsupported git URL: {git_url}") + owner, repo = segs[-2], segs[-1] + if repo.endswith(".git"): + repo = repo[:-4] + prefix = "/".join(segs[:-2]) + origin = f"https://{parts.netloc}" + repo_url = origin + (f"/{prefix}" if prefix else "") + f"/{owner}/{repo}" + if host == "github.com" or host.endswith(".github.com"): + return "github", repo_url, owner, repo, GITHUB_API + if host == "gitlab.com" or host.endswith(".gitlab.com"): + return "gitlab", repo_url, owner, repo, f"{origin}/api/v4" + api = origin + (f"/{prefix}" if prefix else "") + "/api/v1" + return "gitea", repo_url, owner, repo, api + + +def _api_json(url: str, timeout=10): + req = urllib.request.Request(url, headers={"User-Agent": UA}) + with secure_urlopen(req, timeout=timeout) as r: + return json.load(r) + + +def _describe_net_error(e: Exception) -> str: + """Human-readable cause for a failed API request.""" + import urllib.error + if isinstance(e, urllib.error.HTTPError): + if e.code == 403: + return "API rate limit exceeded — try again in 1 hour" + if e.code == 404: + return "repository or branch not found" + host = (e.url or "").split("/")[2] if (e.url or "").count("/") >= 2 \ + else "server" + return f"HTTP {e.code} from {host}" + if isinstance(e, urllib.error.URLError): + return f"network error ({e.reason})" + return str(e) + + +def describe_install_error(e: Exception) -> str: + """Map an install/update failure to a message the user can act on.""" + import zipfile + import urllib.error + if isinstance(e, (urllib.error.HTTPError, urllib.error.URLError)): + return _describe_net_error(e) + if isinstance(e, OSError) and getattr(e, "errno", None) in (2, 13, 22): + # The archive/file vanished or got locked mid-operation — on Windows + # that's almost always the antivirus quarantining the download. + return ("Blocked by antivirus — open Settings (⚙) → " + "'Add game folder to Defender exclusions', then retry") + if isinstance(e, zipfile.BadZipFile): + return ("Downloaded archive is corrupted (possibly blocked by " + "antivirus) — retry, or use Settings (⚙) → " + "'Add game folder to Defender exclusions'") + return str(e) + + +def addon_remote_sha(git_url: str, branch=None, ref=None, + force=False, raise_errors=False) -> str | None: + """Latest commit sha of a repo's branch (or pinned ref), cached in the + config file so repeated verifies don't burn API quota. Returns None on + failure — or raises with a readable cause when raise_errors is set.""" + key = f"{git_url}#{ref or branch or ''}" + now = time.time() + if not force: + entry = load_config().get("addon_sha_cache", {}).get(key) + if entry and (now - entry.get("timestamp", 0)) < ADDON_SHA_CACHE_TTL: + return entry.get("sha") + + kind, _repo_url, owner, repo, api = _git_parts(git_url) + pin = ref or branch # explicit branch/ref when the caller has one + sha = None + try: + if kind == "github": + if pin: + sha = _api_json( + f"{api}/repos/{owner}/{repo}/commits/{pin}").get("sha") + else: + lst = _api_json( + f"{api}/repos/{owner}/{repo}/commits?per_page=1") + sha = lst[0].get("sha") if lst else None + elif kind == "gitlab": + from urllib.parse import quote + proj = quote(f"{owner}/{repo}", safe="") + if pin: + sha = _api_json( + f"{api}/projects/{proj}/repository/commits/" + f"{quote(pin, safe='')}").get("id") + else: + lst = _api_json( + f"{api}/projects/{proj}/repository/commits?per_page=1") + sha = lst[0].get("id") if lst else None + else: # gitea / codeberg + q = f"?sha={pin}&limit=1" if pin else "?limit=1" + lst = _api_json(f"{api}/repos/{owner}/{repo}/commits{q}") + sha = lst[0].get("sha") if lst else None + except Exception as e: + if raise_errors: + raise RuntimeError(_describe_net_error(e)) from e + return None + if sha: + update_config(lambda c: c.setdefault("addon_sha_cache", {}).__setitem__( + key, {"timestamp": now, "sha": sha})) + return sha + + +def addon_cached_sha(git_url: str, branch=None, ref=None): + """Cached remote sha regardless of age — never touches the network.""" + key = f"{git_url}#{ref or branch or ''}" + entry = load_config().get("addon_sha_cache", {}).get(key) + return entry.get("sha") if entry else None + + +def addon_zip_url(git_url: str, sha: str) -> str: + kind, repo_url, _owner, repo, _api = _git_parts(git_url) + if kind == "gitlab": + return f"{repo_url}/-/archive/{sha}/{repo}-{sha}.zip" + return f"{repo_url}/archive/{sha}.zip" + + +def _rmtree_force(path): + """Like shutil.rmtree, but also removes read-only files. Plain rmtree + raises PermissionError on Windows when it meets a read-only file (e.g. a + .git object store from a manual clone, or a read-only addon shipped in an + old zip); this clears the read-only bit and retries.""" + def handler(func, p, _exc): + os.chmod(p, stat.S_IWRITE) + func(p) + if sys.version_info >= (3, 12): + shutil.rmtree(path, onexc=handler) # onerror deprecated in 3.12 + else: + shutil.rmtree(path, onerror=handler) + + +def install_addon_files(client_dir: str, folder: str, git_url: str, sha: str): + """Download the repo archive at `sha` and unpack it into + Interface/AddOns/, atomically replacing any existing copy.""" + url = addon_zip_url(git_url, sha) + log(f" Downloading {folder} @ {sha[:10]}…") + req = urllib.request.Request(url, headers={"User-Agent": UA}) + with secure_urlopen(req, timeout=120, + allowed_hosts=ADDON_ZIP_HOSTS) as r: + data = r.read() + + import zipfile + import io + dest_root = os.path.join(addons_path(client_dir), folder) + tmp_root = dest_root + ".tmp_install" + tmp_abs = os.path.abspath(tmp_root) + if os.path.isdir(tmp_root): + _rmtree_force(tmp_root) + try: + with zipfile.ZipFile(io.BytesIO(data)) as zf: + for info in zf.infolist(): + if info.is_dir(): + continue + # Strip the archive's top-level "-/" directory and + # normalise separators (a zip entry may use "/" or "\"). + parts = [p for p in info.filename.replace("\\", "/").split("/")[1:] + if p not in ("", ".")] + if not parts or ".." in parts: + continue + target = os.path.join(tmp_root, *parts) + # Defence in depth: never write outside the target folder even + # if the guards above are somehow bypassed. + if not os.path.abspath(target).startswith(tmp_abs + os.sep): + continue + os.makedirs(os.path.dirname(target), exist_ok=True) + with zf.open(info) as src, open(target, "wb") as dst: + shutil.copyfileobj(src, dst) + if os.path.isdir(dest_root): + _rmtree_force(dest_root) + os.replace(tmp_root, dest_root) + except BaseException: + # Never leave a half-written ".tmp_install" behind on failure + if os.path.isdir(tmp_root): + try: + _rmtree_force(tmp_root) + except Exception: + pass + raise + log(f" Installed addon {folder}") + + +# ── pfUI "Default" profile patch ───────────────────────────────────────────── +# pfUI ships a set of built-in design profiles. After every pfUI install/update +# we add a curated "Default" profile and make it the firstrun default. Because +# an update overwrites pfUI's files, the patch is re-applied each time and is +# idempotent (marked blocks are replaced, not duplicated). + +# The curated profile (JSON captured from a configured pfUI, profile renamed to +# "Default"). Loaded as a Python dict and emitted as a Lua table at patch time. +PFUI_DEFAULT_PROFILE = json.loads(r''' +{"appearance":{"border":{"default":"-1"},"castbar":{"castbarcolor":"1,0.796,0.251,0.8"},"cd":{"debuffs":"1","font":"Interface\\AddOns\\pfUI\\fonts\\Myriad-Pro.ttf","milliseconds":"0"},"infight":{"health":"0"},"minimap":{"arrowscale":"2"}},"buffs":{"hidelist":"","showoverflow":"1","showspillover":"1"},"castbar":{"focus":{"showicon":"1","showtimer":"0"},"player":{"hide_blizz":"0","hide_pfui":"1","showtimer":"0"},"target":{"showicon":"1","showtimer":"0"}},"character":{"inventory":{"durability":"0"},"reputation":{"repRequired":"0"}},"disabled":{"actionbar":"1","addonbuttons":"0","addoncompat":"0","addons":"0","afkcam":"0","autoshift":"0","autovendor":"0","bags":"1","bgscore":"0","bubbles":"1","buff":"1","buffwatch":"0","castbar":"0","chat":"1","chatcopy":"0","combopoints":"0","cooldown":"0","custom":"0","easteregg":"0","energytick":"0","eqcompare":"0","equipmentmanager":"0","farmmode":"0","feigndeath":"0","firstrun":"0","focus":"0","gm":"0","group":"0","gryphons":"0","hdgraphic":"0","hoverbind":"0","hunterbar":"0","infight":"0","innervatecall":"0","itemclick":"0","itemcount":"1","loot":"1","macrotweak":"0","map":"0","mapcolors":"0","mapreveal":"0","marktracking":"0","minimap":"0","mirrortimers":"0","mouseover":"0","nameplates":"0","nampower":"0","panel":"0","pet":"0","pettarget":"0","pixelperfect":"0","player":"0","questitem":"0","raid":"0","roll":"1","screenshot":"0","sellvalue":"0","share":"0","skin":"0","skin_Auctionhouse":"0","skin_Barbershop":"0","skin_Battlefield":"0","skin_Battlefield Minimap":"0","skin_Battlefield Score":"0","skin_Books":"0","skin_Character":"0","skin_Coin Pickup":"0","skin_Color Picker":"0","skin_Dress Up Frame":"0","skin_Everlook Broadcasting":"0","skin_Flightmaster":"1","skin_Friends":"1","skin_GM Survey":"0","skin_Game Menu":"0","skin_Gossip and Quest":"1","skin_Guild Registrar":"0","skin_Guild Tabard":"0","skin_Help":"0","skin_Inspect":"0","skin_KeyBindings":"0","skin_Macro":"0","skin_Mailbox":"1","skin_Merchant":"1","skin_Opacity":"0","skin_Options - Interface":"0","skin_Options - New":"0","skin_Options - Sound":"0","skin_Options - Video":"0","skin_Pet Stable Master":"0","skin_Petition":"0","skin_Popup Dialogs":"0","skin_Profession":"1","skin_Quest Log":"1","skin_Quest Timer":"0","skin_RaidUI":"0","skin_Readycheck":"0","skin_Spellbook":"1","skin_Stack Split":"0","skin_Talents":"1","skin_Tooltips":"0","skin_Trade":"1","skin_Trainer":"1","skin_Transmog":"0","skin_Turtle LFT":"0","skin_Turtle Shop":"0","skin_TurtleCalendar":"0","skin_TurtleMail":"0","skin_Tutorial":"0","socialmod":"0","superwow":"0","swingtimer":"0","target":"0","targettarget":"0","targettargettarget":"0","thirdparty":"0","thirdparty-vanilla":"0","tooltip":"0","totems":"0","tracking":"0","turtle-wow":"0","uf_tukui":"0","unitxp":"0","unlock":"0","unusable":"0","updatenotify":"0","whisperproxy":"0","xpbar":"1"},"global":{"autosell":"1","font_blizzard":"1","font_unit":"Interface\\AddOns\\pfUI\\fonts\\Myriad-Pro.ttf","font_unit_name":"Interface\\AddOns\\pfUI\\fonts\\Continuum.ttf","profile":"Default"},"gui":{"showdisabled":"0"},"nameplates":{"barcombatstate":"0","ccombatnothreat":"0","ccombatofftank":"0","ccombatstun":"0","ccombatthreat":"0","debuffanim":"1","debuffs":{"blacklist":"#Mana Attuned#Mercenary","filter":"blacklist","showstacks":"1"},"debuffsize":"20","guessdebuffs":"1","outcombatstate":"0","overlap":"1","spellname":"1"},"panel":{"left":{"center":"none","left":"none","right":"none"},"other":{"minimap":"time"},"right":{"center":"none","left":"none","right":"none"},"seconds":"0"},"position":{"TicketStatusFrame":{"anchor":"CENTER","parent":"UIParent","xpos":1100,"ypos":300},"WorldMapFrame":{"alpha":1,"anchor":"TOPLEFT","scale":0.69999998807907104,"xpos":1434,"ypos":-150},"pfFocus":{"anchor":"CENTER","parent":"UIParent","xpos":-200,"ypos":220},"pfFocusCastbar":{"anchor":"CENTER","parent":"UIParent","xpos":-200,"ypos":162},"pfGroup1":{"anchor":"TOP","parent":"UIParent","scale":1,"xpos":-468,"ypos":-135},"pfGroup2":{"anchor":"TOP","parent":"UIParent","xpos":-468,"ypos":-210},"pfGroup3":{"anchor":"TOP","parent":"UIParent","xpos":-468,"ypos":-285},"pfGroup4":{"anchor":"TOP","parent":"UIParent","xpos":-468,"ypos":-360},"pfMarkTracking":{"anchor":"CENTER","parent":"UIParent","xpos":270,"ypos":270},"pfPartyPet1":{"scale":0.90000000000000002},"pfPartyPet2":{"scale":0.90000000000000002},"pfPartyPet3":{"scale":0.90000000000000002},"pfPartyPet4":{"scale":0.90000000000000002},"pfPet":{"anchor":"CENTER","parent":"UIParent","scale":1,"xpos":-490,"ypos":338},"pfPlayer":{"anchor":"CENTER","parent":"UIParent","xpos":-445,"ypos":380},"pfRaidCluster":{"anchor":"CENTER","parent":"UIParent","xpos":-1420,"ypos":-120},"pfSwingTimerMainhand":{"anchor":"CENTER","parent":"UIParent","xpos":0,"ypos":-252},"pfSwingTimerRanged":{"anchor":"CENTER","parent":"UIParent","xpos":0,"ypos":-228},"pfTarget":{"anchor":"CENTER","parent":"UIParent","xpos":-215,"ypos":380},"pfTargetCastbar":{"anchor":"CENTER","parent":"UIParent","xpos":-215,"ypos":290},"pfTargetTarget":{"anchor":"CENTER","parent":"UIParent","scale":0.90000000000000002,"xpos":-116,"ypos":385},"pfTargetTargetTarget":{"anchor":"CENTER","parent":"UIParent","scale":0.90000000000000002,"xpos":-8,"ypos":385},"pfTooltipAnchor":{"anchor":"CENTER","parent":"UIParent","xpos":720,"ypos":-386},"pfTotems":{"anchor":"CENTER","parent":"UIParent","xpos":-482,"ypos":334}},"thirdparty":{"statcompare":{"enable":"1"},"wim":{"enable":"0"}},"tooltip":{"position":"free","vendor":{"showalways":"0"}},"unitframes":{"abbrevnum":"0","always2dportrait":"1","animation_speed":"1","castbardecimals":"1","custombgcolor":"0.502,0.2,0.2,1","customcolor":"0.055,0.749,0,1","druidmanaheight":"2","druidmanatext":"0","fallback":{"debuff_ind_range":"0"},"focus":{"buffs":"BOTTOMLEFT","buffsize":"18","cooldown_anim":"1","debuff_ind_range":"0","debuffs":"BOTTOMLEFT","debuffsize":"20","hitindicatorfont":"Interface\\AddOns\\pfUI\\fonts\\Myriad-Pro.ttf","hitindicatorsize":"16","portrait":"right","powercolor":"0","txthpright":"health"},"focustarget":{"debuff_ind_range":"0"},"group":{"buffs":"off","debuff_ind_range":"0","height":"22","txthpright":"none","width":"144"},"grouppet":{"debuff_ind_range":"0"},"grouptarget":{"debuff_ind_range":"0","visible":"0"},"nampower_buffs":"1","pet":{"debuff_ind_range":"0"},"player":{"buffs":"off","debuff_ind_range":"0","debuffs":"off","display_spellpower":"0","height":"26","hitindicator":"1","hitindicatorfont":"Interface\\AddOns\\pfUI\\fonts\\Myriad-Pro.ttf","hitindicatorsize":"16","portrait":"left","powercolor":"0","txthpright":"health","txtpowerright":"power","width":"150"},"ptarget":{"debuff_ind_range":"0"},"raid":{"debuff_ind_range":"0"},"rangecheck_mode":"unitxp","rangechecki":"4","spellqueue":"0","swingtimerattackspeed":"1","swingtimerfontsize":"14","swingtimerlabel":"0","swingtimermhcolor":"0.31,0.596,1,1","swingtimerrangedcolor":"0.686,0.447,1,1","swingtimerwidth":"192","target":{"buffperrow":"7","buffs":"BOTTOMLEFT","buffsize":"18","cooldown_anim":"1","debuff_ind_range":"0","debuffperrow":"6","debuffs":"BOTTOMLEFT","debuffsize":"22","height":"26","hitindicator":"1","hitindicatorfont":"Interface\\AddOns\\pfUI\\fonts\\Myriad-Pro.ttf","hitindicatorsize":"16","portrait":"right","powercolor":"0","showPVP":"1","txthpright":"health","txtpowerright":"power","width":"150"},"track_group":"1","ttarget":{"debuff_ind_range":"0","portrait":"off","powercolor":"0"},"tttarget":{"debuff_ind_range":"0","portrait":"off"}},"version":"999.999.999"} +''') + + +def _lua_value(v, indent: int = 0) -> str: + """Serialize a JSON-derived value to a pfUI-style Lua literal.""" + if isinstance(v, dict): + pad, cpad = " " * (indent + 2), " " * indent + items = "".join(f'{pad}["{k}"] = {_lua_value(val, indent + 2)},\n' + for k, val in v.items()) + return "{\n" + items + cpad + "}" + if isinstance(v, bool): + return "true" if v else "false" + if isinstance(v, (int, float)): + return repr(v) + s = str(v).replace("\\", "\\\\").replace('"', '\\"') + return f'"{s}"' + + +_PFUI_MARK_BEGIN = "-- OCTO_UPDATER_DEFAULT_PROFILE_BEGIN" +_PFUI_MARK_END = "-- OCTO_UPDATER_DEFAULT_PROFILE_END" +_PFUI_CHAT_BEGIN = "-- OCTO_UPDATER_CHAT_SKIP_BEGIN" +_PFUI_CHAT_END = "-- OCTO_UPDATER_CHAT_SKIP_END" + +# Strips any Octo Updater injected block, regardless of which marker pair. +_PFUI_STRIP_RE = r"[ \t]*-- OCTO_UPDATER_[A-Z_]+?_BEGIN.*?-- OCTO_UPDATER_[A-Z_]+?_END\n?" + + +def patch_pfui_default_profile(client_dir: str): + """Add the curated 'Default' profile to a freshly installed/updated pfUI + and make it the firstrun default. Idempotent; degrades gracefully if + pfUI's file layout has changed.""" + import re + + base = os.path.join(addons_path(client_dir), "pfUI") + profiles_lua = os.path.join(base, "env", "profiles.lua") + firstrun_lua = os.path.join(base, "modules", "firstrun.lua") + if not os.path.exists(profiles_lua): + return + + # 1) profiles.lua — append (or replace) a marked block defining Default. + block = (f"{_PFUI_MARK_BEGIN}\n" + f"local octo_default = {_lua_value(PFUI_DEFAULT_PROFILE)}\n" + f'pfUI_profiles["Default"] = octo_default\n' + f"{_PFUI_MARK_END}\n") + try: + with open(profiles_lua, encoding="utf-8", errors="replace") as f: + txt = f.read() + txt = re.sub(re.escape(_PFUI_MARK_BEGIN) + r".*?" + + re.escape(_PFUI_MARK_END) + r"\n?", "", txt, flags=re.S) + with open(profiles_lua, "w", encoding="utf-8") as f: + f.write(txt.rstrip() + "\n\n" + block) + log(" pfUI: 'Default' profile installed.") + except OSError as e: + log(f" pfUI: could not patch profiles.lua ({e})") + return + + # 2) pfUI.lua — use 'Default' (not 'Modern') as the fresh-install config, + # so the very first login already lands in the Default profile. + pfui_lua = os.path.join(base, "pfUI.lua") + if os.path.exists(pfui_lua): + try: + with open(pfui_lua, encoding="utf-8", errors="replace") as f: + pf = f.read() + old = 'CopyTable(pfUI_profiles["Modern"]) or {}' + if old in pf: + pf = pf.replace( + old, 'CopyTable(pfUI_profiles["Default"]) or {}', 1) + with open(pfui_lua, "w", encoding="utf-8") as f: + f.write(pf) + log(" pfUI: 'Default' set as the fresh-install profile.") + except OSError as e: + log(f" pfUI: could not patch pfUI.lua ({e})") + + # 3) firstrun.lua — add a 'Default' button, make it the fallback profile, + # and skip the chat wizard steps whenever the chat module is disabled. + if not os.path.exists(firstrun_lua): + return + try: + with open(firstrun_lua, encoding="utf-8", errors="replace") as f: + fr = f.read() + + # Remove any previous injections (idempotent re-apply after updates). + fr = re.sub(_PFUI_STRIP_RE, "", fr, flags=re.S) + + # When the chat module is disabled (e.g. the "Default" profile), the + # chat firstrun steps can't apply anything, so pre-mark them done to + # keep them from showing. Injected right after the step table is made. + chat_skip = ( + f" {_PFUI_CHAT_BEGIN}\n" + " if pfUI_config and pfUI_config.disabled" + ' and pfUI_config.disabled.chat == "1" then\n' + " pfUI_init = pfUI_init or {}\n" + ' pfUI_init["chat_right"] = true\n' + ' pfUI_init["chat_position"] = true\n' + ' pfUI_init["chat_channels"] = true\n' + " end\n" + f" {_PFUI_CHAT_END}\n") + chat_anchor = " pfUI.firstrun.steps = {}\n" + if chat_anchor in fr: + fr = fr.replace(chat_anchor, chat_anchor + chat_skip, 1) + + # Insert a Default button just before the built-in "Modern" button. + button = ( + f" {_PFUI_MARK_BEGIN}\n" + ' f.Default = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")\n' + " f.Default:SetWidth(250)\n" + " f.Default:SetHeight(20)\n" + ' f.Default:SetPoint("BOTTOM", 0, 120)\n' + " f.Default:SetTextColor(1,1,1)\n" + ' f.Default:SetText("Default (recommended)")\n' + " f.Default:SetScript(\"OnClick\", function()\n" + ' _G["pfUI_config"] = CopyTable(pfUI_profiles["Default"])\n' + ' pfUI_init.selected_profile = "Default"\n' + " pfUI:LoadConfig()\n" + " ReloadUI()\n" + " end)\n" + " SkinButton(f.Default)\n" + f" {_PFUI_MARK_END}\n\n") + anchor = ' f.Modern = CreateFrame("Button"' + if anchor in fr: + fr = fr.replace(anchor, button + anchor, 1) + + # Make Default the profile used when the user doesn't pick one. + fr = fr.replace('pfUI_init.selected_profile or "Modern"', + 'pfUI_init.selected_profile or "Default"') + + with open(firstrun_lua, "w", encoding="utf-8") as f: + f.write(fr) + log(" pfUI: 'Default' added to the firstrun profile picker.") + except OSError as e: + log(f" pfUI: could not patch firstrun.lua ({e})") + + +# ────────────────────────────────────────────────────────────────────────────── +# Tweaks definition +# ────────────────────────────────────────────────────────────────────────────── + +TWEAKS_DEFAULTS = { + "locale": DEFAULT_LOCALE, + "alwaysAutoLoot": True, + "nameplateRange": 41, + "fieldOfView": 110, + "farClip": 777, + "frillDistance": 120, + "cameraDistance": 50, + "soundInBackground": True, +} + +TWEAKS_ITEMS = [ + (None, "GENERAL", "section", False, None, None, None, None, None), + + ("locale", "Game Language", "dropdown", False, None, + None, + None, None, None), + + ("alwaysAutoLoot", "Auto Loot", "checkbox", True, None, + "Reverses the auto-loot behavior to always auto-loot.", + None, None, None), + + ("nameplateRange", "Nameplate Range", "number", False, None, + "Distance at which nameplates are visible. [0 - 41]", + 0, 41, 1), + + (None, "CAMERA", "section", False, None, None, None, None, None), + + ("fieldOfView", "Field of View", "number", False, None, + "Recommended values by aspect ratio: [4:3 = 90] [16:9 = 110] [21:9 = 150] [32:9 = 180]", + 90, 180, 5), + + ("cameraDistance", "Camera Distance", "number", False, None, + "Maximum camera zoom-out distance. [50 - 100]", + 50, 100, 1), + + (None, "GRAPHICS", "section", False, None, None, None, None, None), + + ("farClip", "World Distance", "number", False, None, + "Terrain and world objects rendering distance. [100 - 10,000]", + 100, 10000, 1), + + ("frillDistance", "Ground Clutter Distance", "number", False, None, + "Grass and small rocks rendering distance. [0 - 300]", + 0, 300, 1), + + (None, "SOUND", "section", False, None, None, None, None, None), + + ("soundInBackground", "Sound in Background", "checkbox", True, None, + "Allows game sounds to play in the background.", + None, None, None), +] + + +# {tweak_id: (min, max)} for every numeric tweak — the single source of +# truth for clamping, wherever the value is read from the UI. +TWEAKS_LIMITS = {t[0]: (t[6], t[7]) for t in TWEAKS_ITEMS + if t[0] is not None and t[2] == "number"} + + +_FOV_REFS = [ + (4 / 3, 90), + (16 / 9, 110), + (21 / 9, 150), + (32 / 9, 180), +] + + +def fov_default_for_display() -> int: + try: + info = _get_display_info_safe() + ratio = info["width"] / info["height"] if info["height"] else 16 / 9 + except Exception: + ratio = 16 / 9 + + if ratio <= _FOV_REFS[0][0]: + return _FOV_REFS[0][1] + if ratio >= _FOV_REFS[-1][0]: + return _FOV_REFS[-1][1] + for i in range(len(_FOV_REFS) - 1): + r0, f0 = _FOV_REFS[i] + r1, f1 = _FOV_REFS[i + 1] + if r0 <= ratio <= r1: + t = (ratio - r0) / (r1 - r0) + raw = f0 + t * (f1 - f0) + return round(round(raw / 5) * 5) + return 110 + + +def _get_display_info_safe() -> dict: + import ctypes + ENUM_CURRENT_SETTINGS = -1 + + class DEVMODE(ctypes.Structure): + _fields_ = [ + ("dmDeviceName", ctypes.c_wchar * 32), + ("dmSpecVersion", ctypes.c_ushort), + ("dmDriverVersion", ctypes.c_ushort), + ("dmSize", ctypes.c_ushort), + ("dmDriverExtra", ctypes.c_ushort), + ("dmFields", ctypes.c_ulong), + ("dmPositionX", ctypes.c_long), + ("dmPositionY", ctypes.c_long), + ("dmDisplayOrientation", ctypes.c_ulong), + ("dmDisplayFixedOutput", ctypes.c_ulong), + ("dmColor", ctypes.c_short), + ("dmDuplex", ctypes.c_short), + ("dmYResolution", ctypes.c_short), + ("dmTTOption", ctypes.c_short), + ("dmCollate", ctypes.c_short), + ("dmFormName", ctypes.c_wchar * 32), + ("dmLogPixels", ctypes.c_ushort), + ("dmBitsPerPel", ctypes.c_ulong), + ("dmPelsWidth", ctypes.c_ulong), + ("dmPelsHeight", ctypes.c_ulong), + ("dmDisplayFlags", ctypes.c_ulong), + ("dmDisplayFrequency", ctypes.c_ulong), + ] + + dm = DEVMODE() + dm.dmSize = ctypes.sizeof(DEVMODE) + ctypes.windll.user32.EnumDisplaySettingsW( + None, ENUM_CURRENT_SETTINGS, ctypes.byref(dm)) + return {"width": dm.dmPelsWidth, "height": dm.dmPelsHeight, + "refresh_rate": dm.dmDisplayFrequency} + + +def load_tweaks_config() -> dict: + cfg = load_config() + stored = cfg.get("tweaks", {}) + defaults = dict(TWEAKS_DEFAULTS) + defaults["fieldOfView"] = fov_default_for_display() + return {k: stored.get(k, v) for k, v in defaults.items()} + + +def save_tweaks_config(values: dict): + update_config(lambda c: c.__setitem__("tweaks", values)) + +# ────────────────────────────────────────────────────────────────────────────── +# News feed +# ────────────────────────────────────────────────────────────────────────────── + + +def _strip_html(raw: str) -> str: + """Reduce forum HTML to readable plain text for a Tk widget.""" + import re + import html as html_mod + txt = re.sub(r"(?is)<(script|style)[^>]*>.*?", "", raw) + txt = re.sub(r"(?i)", "\n", txt) + txt = re.sub(r"(?i)]*>", "\n• ", txt) + txt = re.sub(r"(?i)", "\n", txt) + txt = re.sub(r"<[^>]+>", "", txt) + txt = html_mod.unescape(txt) + txt = re.sub(r"[ \t]+", " ", txt) + txt = re.sub(r" ?\n ?", "\n", txt) + txt = re.sub(r"\n{3,}", "\n\n", txt) + return txt.strip() + + +def _format_news_date(iso: str) -> str: + from datetime import datetime + try: + return datetime.fromisoformat(iso).strftime("%d %b %Y") + except Exception: + return iso + + +def fetch_patch_notes() -> list: + """Patch-notes list → [{id, title, date, body, url?, author?}, …]""" + req = urllib.request.Request(PATCHNOTES_URL, headers={"User-Agent": UA}) + with secure_urlopen(req, timeout=NEWS_TIMEOUT) as r: + data = json.load(r) + items = data.get("items", []) + # The feed lists topics in forum order — show newest first (ISO dates + # with a fixed offset sort correctly as strings). + items.sort(key=lambda it: it.get("date", ""), reverse=True) + return items + + +def fetch_featured_post() -> dict | None: + """Latest announcements-forum post → {id, title, author?, date, url, html}""" + req = urllib.request.Request(NEWS_FEATURED_URL, headers={"User-Agent": UA}) + with secure_urlopen(req, timeout=NEWS_TIMEOUT) as r: + data = json.load(r) + return data if isinstance(data, dict) and data.get("id") else None + +# ────────────────────────────────────────────────────────────────────────────── +# GUI +# ────────────────────────────────────────────────────────────────────────────── + + +class SlimScrollbar(tk.Canvas): + """Flat minimal scrollbar (the native tk.Scrollbar can't be themed on + Windows). Speaks the standard set()/command scrollbar protocol.""" + + def __init__(self, parent, command=None, width=10, + bg=C_PANEL, thumb="#3a2f55", thumb_hover=C_GOLD, **kw): + super().__init__(parent, width=width, bg=bg, + highlightthickness=0, bd=0, **kw) + self.command = command + self._thumb = thumb + self._thumb_hover = thumb_hover + self._first = 0.0 + self._last = 1.0 + self._drag_off = None + self._hover = False + self.bind("", lambda e: self._redraw()) + self.bind("", self._click) + self.bind("", self._drag) + self.bind("", lambda e: self._set_hover(True)) + self.bind("", lambda e: self._set_hover(False)) + + def set(self, first, last): + self._first, self._last = float(first), float(last) + self._redraw() + + def _set_hover(self, on): + self._hover = on + self._redraw() + + def _redraw(self): + self.delete("all") + if self._last - self._first >= 1.0: + return + h = self.winfo_height() + w = self.winfo_width() + y0 = int(self._first * h) + y1 = max(int(self._last * h), y0 + 24) + self.create_rectangle( + 2, y0, w - 2, y1, + fill=self._thumb_hover if self._hover else self._thumb, + outline="") + + def _click(self, e): + h = self.winfo_height() or 1 + y0 = self._first * h + y1 = self._last * h + self._drag_off = (e.y - y0) if y0 <= e.y <= y1 else (y1 - y0) / 2 + self._drag(e) + + def _drag(self, e): + h = self.winfo_height() or 1 + span = self._last - self._first + first = max(0.0, min((e.y - (self._drag_off or 0)) / h, 1.0 - span)) + if self.command: + self.command("moveto", first) + +class OctoUpdaterApp(tk.Tk): + def __init__(self): + super().__init__() + + # ── DPI scaling ──────────────────────────────────────────────────── + # With awareness declared (see _enable_dpi_awareness) Windows reports + # the real DPI. Derive one scale factor from it and drive everything + # off it: fonts via Tk's own point→pixel scaling, and every hard-coded + # pixel value via self._px(). At 100% the factor is 1.0 and _px() is an + # exact identity, so nothing changes; only high-DPI diverges. + self._sc = max(1.0, self.winfo_fpixels("1i") / 96.0) + # tk scaling is pixels-per-point; DPI/72 == _sc * 96/72. Setting it + # explicitly keeps point-fonts growing by exactly _sc regardless of + # what Tk defaulted to. + self.tk.call("tk", "scaling", self._sc * 96.0 / 72.0) + # Bake the factor into the shared design constants once, up front, so + # the ~40 WIN_W/WIN_H/FOOT_H references downstream need no changes. + global WIN_W, WIN_H, FOOT_H + WIN_W = self._px(WIN_W) + WIN_H = self._px(WIN_H) + FOOT_H = self._px(FOOT_H) + + # Keep the window hidden until it's positioned and fully built, so it + # never flashes at the default top-left corner before centering. + self.withdraw() + + # Move a pre-1.3 config/cache from beside the .exe into the per-user + # data dir before anything reads them, so first-run detection and + # load_config() below see the relocated files (see _relocate_legacy_data). + _relocate_legacy_data() + + # Detect first run before anything writes the config. + self._first_run = not os.path.exists(CONFIG_FILE) + # Set when the user adds a Defender exclusion via Settings; checked at + # the next reconcile to skip the auto-prompt (so a manual add isn't + # double-prompted), then reset — so each folder change offers one unless + # the user just added it themselves. + self._av_excluded = False + self._cfg = load_config() + # Pending reconcile mode (None = none): _offer_reconcile surfaces the + # Update button and the Update the user clicks runs an integrity pass + # instead of a routine size-based sync. "full" also writes a fresh + # Config.wtf (folder change / first run); "keep-config" leaves Config.wtf. + self._pending_reconcile = self._cfg.get("pending_reconcile") or None + self._running = False + # True only after a verify/update confirmed the client files are up + # to date. PLAY is gated on this AND on no mod being in an error state. + self._client_ready = False + self._worker: UpdateWorker | None = None + self._log_q: queue.Queue = queue.Queue() + self._prog_q: queue.Queue = queue.Queue() + # Session log lives in memory; the "Show logs" window renders it. + self._log_buffer: list = [] + self._logwin = None + self._logwin_text = None + self._settings_overlay = None + # Guards against triggering the default-mods / recommended-addons + # auto-install more than once per app session (e.g. verify firing + # twice in quick succession). + self._default_mods_install_started = False + self._default_addons_install_started = False + + # Game folder path — shared by the Settings modal. A change takes + # effect only once Settings is closed (see _close_settings), so no + # live trace fires mid-edit. + self._game_path = tk.StringVar( + value=os.path.normpath(self._cfg.get("out_dir", DEFAULT_GAME_DIR))) + + # Count of mods with an update available — shown as a badge on the + # MODS nav tab. + self._mod_updates_count = 0 + + # Addons state + self._addons_status = {"state": "idle", "addons": {}, "available": []} + self._addons_busy = False + # True only while addons are actually downloading/installing (not + # during a verify) — gates the PLAY button, like mods installs do. + self._addons_installing = False + self._addons_verified_ts = 0.0 + self._addon_updates_count = 0 + self._mpq_updates_count = 0 + self._mpq_busy = None + # {folder: {"error": msg, "git": url}} for failed installs/updates — + # survives the post-operation rescan so rows can show what went wrong. + self._addon_errors = {} + self._addon_sections_open = {"INSTALLED": True, "AVAILABLE": True} + + # News feed cache (featured post and announcements cached separately) + self._feat_ts = 0.0 + self._patch_ts = 0.0 + self._patch_items = None + self._featured = None + + # Scrollable list canvases that respond to the mouse wheel whenever + # the pointer is anywhere over them (not just over the scrollbar). + self._wheel_canvases: list = [] + self.bind_all("", self._on_mousewheel) + + self.title("Octo Updater") + self.resizable(False, False) + self.configure(bg=C_BG) + + # Center on screen up front. WIN_W/WIN_H are fixed, so we can position + # the window in a single geometry call (no need to map-then-measure). + sw = self.winfo_screenwidth() + sh = self.winfo_screenheight() + x = (sw - WIN_W) // 2 + y = (sh - WIN_H) // 2 + self.geometry(f"{WIN_W}x{WIN_H}+{x}+{y}") + + self._build() + + out_dir = self._cfg.get("out_dir", DEFAULT_GAME_DIR) + if not os.path.exists(out_dir): + def _wipe(c): + c.pop("mods", None) + c.pop("addons", None) + self._cfg = update_config(_wipe) + + live_ver = get_client_version(out_dir) + if live_ver: + self._client_ver_var.set(live_ver) + self.protocol("WM_DELETE_WINDOW", self._on_close) + self._poll() + # Run any one-time migrations for a freshly-installed updater version + # (and stamp octo_updater_version) before verifying. It runs + # synchronously, so a v1.3 re-patch is settled before verify reads + # WoW.exe, and the normal verify below still runs — an upgrading user + # never misses a pending client update. + self._new_version_installed() + # On first run, defer verification until Settings is closed (see + # _close_settings). If a reconcile was offered on a previous run but + # never applied, restore the Update button instead of a routine verify. + # Otherwise run the routine launch verify. + if self._first_run: + pass + elif self._pending_reconcile: + self.after(300, self._offer_reconcile) + else: + self.after(300, self._start_verify) + self.after(600, self._load_news) + # Check mod updates at launch too — but only once mods have actually + # been used (mod_release_cache exists). On a first run or right after a + # game-folder change nothing is installed yet, so there's nothing to + # check; the MODS tab will do it when opened. + if self._cfg.get("mod_release_cache"): + self.after(900, lambda: threading.Thread( + target=self._load_mods_state, daemon=True).start()) + # Same parity for addons: background verify at launch (feeds the + # ADDONS tab badge), but only once addons were initialized for this + # folder — never on a first run / fresh folder. + if self._cfg.get("addons") is not None: + self.after(1500, self._addons_verify) + # MPQ patches: check installed known patches for updates at launch so + # the tab badge shows without opening the tab (no-op if none installed). + if not self._first_run and self._game_path.get().strip(): + self.after(1700, self._mpq_check) + # Daily self-update check (cached), last so it never delays the rest. + self.after(2000, lambda: threading.Thread( + target=self._check_updater_update, daemon=True).start()) + # First launch: open Settings so the user sets the game folder etc. + if self._first_run: + self.after(500, self._open_settings) + + # Everything is positioned and built — reveal the centered window. + self.deiconify() + + def _px(self, *vals): + """Scale design pixels (authored at 96 DPI) to the current DPI. Returns + an int for one arg, a tuple for several — handy for canvas coordinate + lists. Exact identity when the scale factor is 1.0 (100% scaling).""" + if len(vals) == 1: + return round(vals[0] * self._sc) + return tuple(round(v * self._sc) for v in vals) + + # ── build ───────────────────────────────────────────────────────────────── + + def _on_close(self): + """Hide the window first so the close feels instant + Config/caches are already saved at write time, + and the worker threads are daemons, so nothing blocks the exit.""" + try: + self.withdraw() + except Exception: + pass + # A download's aria2c child isn't a daemon thread — kill it explicitly + # so it can't keep syncing headless after the window is gone. + stop_aria2c() + self.quit() + + def _add_tooltip(self, widget, text: str): + """Attach a small hover tooltip to a widget.""" + state = {"win": None} + + def show(_e=None): + if state["win"] is not None: + return + x = widget.winfo_rootx() + self._px(12) + y = widget.winfo_rooty() + widget.winfo_height() + self._px(4) + tw = tk.Toplevel(self) + tw.wm_overrideredirect(True) + tw.wm_geometry(f"+{x}+{y}") + tk.Label(tw, text=text, font=("Segoe UI", 9), + fg=C_TEXT, bg="#0f0b16", + highlightthickness=1, highlightbackground=C_PANEL_BDR, + padx=self._px(6), pady=self._px(2)).pack() + state["win"] = tw + + def hide(_e=None): + if state["win"] is not None: + state["win"].destroy() + state["win"] = None + + widget.bind("", show, add="+") + widget.bind("", hide, add="+") + + def _on_mousewheel(self, event): + # Panels are stacked, so several list canvases share the same screen + # region — only the one inside the active tab's panel should scroll. + active = getattr(self, "_active_panel", None) + active_prefix = (str(active) + ".") if active is not None else None + for cv in list(self._wheel_canvases): + try: + if not cv.winfo_ismapped(): + continue + if active_prefix and not str(cv).startswith(active_prefix): + continue + wx, wy = cv.winfo_rootx(), cv.winfo_rooty() + if (wx <= event.x_root <= wx + cv.winfo_width() + and wy <= event.y_root <= wy + cv.winfo_height()): + # When the content fits entirely in view, Tk would still + # happily shift it around — ignore the wheel instead. + first, last = cv.yview() + if last - first < 1.0: + cv.yview_scroll(int(-event.delta / 120), "units") + return + except tk.TclError: + self._wheel_canvases.remove(cv) + + def _draw_nav_tab(self, tab: str, hover: bool = False): + """Render a nav tab on the header canvas; the active tab's text gets + a soft glow (dim gold halo layers behind the bright text).""" + cv = self._hdr_canvas + tag = f"nav_{tab}" + cv.delete(tag) + cx, cy = self._nav_pos[tab], self._px(54) + font = ("Segoe UI", 11, "bold") + if tab == self._active_tab: + for r, col in ((self._px(2), "#42340f"), (self._px(1), "#7a5c1d")): + for dx, dy in ((-r, 0), (r, 0), (0, -r), (0, r), + (-r, -r), (r, -r), (-r, r), (r, r)): + cv.create_text(cx + dx, cy + dy, text=tab, + font=font, fill=col, tags=tag) + cv.create_text(cx, cy, text=tab, font=font, fill=C_GOLD_LT, + tags=tag) + else: + cv.create_text(cx, cy, text=tab, font=font, + fill=C_GOLD_LT if hover else C_TEXT, tags=tag) + + # Update-counter badges + count = 0 + if tab == "MODS": + count = self._mod_updates_count + elif tab == "ADDONS": + count = self._addon_updates_count + elif tab == "MPQ": + count = self._mpq_updates_count + if count: + bx = cx + self._nav_text_w[tab] // 2 + self._px(11) + by = cy - self._px(11) + # Canvas oval, not a ● glyph: the oval gives exact geometric + # control so the number always centers, consistently across OSes. + # (A filled-circle glyph antialiases nicely but its disk isn't + # centered in the glyph box — by a font-specific amount — so the + # number drifts off-centre and can't be corrected reliably.) + r8 = self._px(8) + cv.create_oval(bx - r8, by - r8, bx + r8, by + r8, + fill=C_GOLD, outline="", tags=tag) + cv.create_text(bx, by, text=str(count), + font=("Segoe UI", 8, "bold"), + fill="#1a1408", tags=tag) + + def _switch_tab(self, tab: str): + if tab == self._active_tab: + return + prev = self._active_tab + self._active_tab = tab + self._draw_nav_tab(prev) + self._draw_nav_tab(tab) + + PANEL_TOP = self._px(119) + PANEL_H = WIN_H - PANEL_TOP - FOOT_H - self._px(10) + + # Panels stay mapped and stacked; switching tabs only raises the + # active one. place_forget()/place() would unmap and remap the whole + # widget tree of a populated panel (hundreds of widgets) every + # switch — a visible synchronous stall. + panels = {"NEWS": self._news_panel, + "TWEAKS": self._tweaks_panel_frame, + "ADDONS": self._addons_panel_frame, + "MODS": self._mods_panel_frame, + "MPQ": self._mpq_panel_frame} + target = panels.get(tab, self._news_panel) + if not target.winfo_ismapped(): + target.place(x=self._px(40), y=PANEL_TOP, + width=WIN_W - self._px(80), height=PANEL_H) + target.tkraise() + self._active_panel = target + + if tab == "MODS": + threading.Thread(target=self._load_mods_state, daemon=True).start() + elif tab == "TWEAKS": + self._refresh_tweaks_panel() + elif tab == "ADDONS": + self._addons_verify() + elif tab == "MPQ": + self._mpq_check() + else: + self._load_news() + + def _build(self): + self._bg_canvas = tk.Canvas(self, width=WIN_W, height=WIN_H, + bg=C_BG, highlightthickness=0) + self._bg_canvas.place(x=0, y=0) + self._draw_bg() + + self._build_header() + self._build_panel() + self._build_footer() + + def _draw_bg(self): + c = self._bg_canvas + bloom_cx, bloom_cy = WIN_W - self._px(80), self._px(80) + for i in range(40, 0, -1): + r = self._px(i * 9) + alpha_frac = (40 - i) / 40 + r_val = int(0x12 + alpha_frac * (0x2e - 0x12)) + g_val = int(0x0e + alpha_frac * (0x18 - 0x0e)) + b_val = int(0x1a + alpha_frac * (0x50 - 0x1a)) + col = f"#{r_val:02x}{g_val:02x}{b_val:02x}" + c.create_oval(bloom_cx - r, bloom_cy - r, + bloom_cx + r, bloom_cy + r, + fill=col, outline="") + + c.create_line(0, WIN_H - self._px(1), WIN_W, WIN_H - self._px(1), + fill=C_PANEL_BDR) + + def _build_header(self): + HDR_H = self._px(108) + + hdr = tk.Canvas(self, width=WIN_W, height=HDR_H, + bg=C_BG, highlightthickness=0) + hdr.place(x=0, y=0, width=WIN_W, height=HDR_H) + self._hdr_canvas = hdr + + # Same corner bloom as the main background (identical coordinates + # and colors) so the header blends seamlessly with the body instead + # of sitting as a darker separated band. + bloom_cx, bloom_cy = WIN_W - self._px(80), self._px(80) + for i in range(40, 0, -1): + r = self._px(i * 9) + alpha_frac = (40 - i) / 40 + r_val = int(0x12 + alpha_frac * (0x2e - 0x12)) + g_val = int(0x0e + alpha_frac * (0x18 - 0x0e)) + b_val = int(0x1a + alpha_frac * (0x50 - 0x1a)) + col = f"#{r_val:02x}{g_val:02x}{b_val:02x}" + hdr.create_oval(bloom_cx - r, bloom_cy - r, + bloom_cx + r, bloom_cy + r, + fill=col, outline="") + + import tkinter.font as tkfont + + self._logo_y = HDR_H // 2 - self._px(6) + self._draw_logo() + + # Point-sized font: Tk scales it by tk-scaling, so measure() below + # already returns device pixels at the current DPI. + nav_font = tkfont.Font(family="Segoe UI", size=11, weight="bold") + tabs = ["NEWS", "TWEAKS", "ADDONS", "MODS", "MPQ"] + self._active_tab = "NEWS" + self._nav_pos = {} + self._nav_text_w = {} + self._hdr_regions = {} + x = self._px(240) + for tab in tabs: + w = nav_font.measure(tab) + self._px(36) + self._nav_pos[tab] = x + w // 2 + self._nav_text_w[tab] = nav_font.measure(tab) + self._hdr_regions[tab] = (x, 0, x + w, HDR_H) + x += w + self._draw_nav_tab(tab) + + self._hdr_regions["gear"] = (WIN_W - self._px(36), self._px(2), + WIN_W - self._px(2), self._px(34)) + self._draw_gear() + + # The wordmark is a clickable header element too (opens the repo). + lb = hdr.bbox("logo") + if lb: + self._hdr_regions["logo"] = (lb[0] - self._px(4), lb[1] - self._px(4), + lb[2] + self._px(6), lb[3] + self._px(4)) + self._logo_cx = (lb[0] + lb[2]) // 2 + else: + self._logo_cx = self._px(127) + # "Update available!" label under the wordmark, shown once the daily + # self-update check finds a newer release. + self._update_available = False + self._draw_update_label() + + self._hdr_hover = None + hdr.bind("", self._on_hdr_click) + hdr.bind("", self._on_hdr_motion) + hdr.bind("", lambda e: self._on_hdr_motion(None)) + + self._clear_wdb_var = tk.BooleanVar( + value=bool(self._cfg.get("clear_wdb_on_launch", False))) + self._close_on_launch_var = tk.BooleanVar( + value=bool(self._cfg.get("close_on_launch", False))) + self._auto_mods_var = tk.BooleanVar( + value=bool(self._cfg.get("auto_install_mods", True))) + self._auto_addons_var = tk.BooleanVar( + value=bool(self._cfg.get("auto_install_addons", True))) + self._ignore_speech_var = tk.BooleanVar( + value=bool(self._cfg.get("ignore_speech", False))) + # Deferred "install missing" pending from turning an auto-install + # option on in Settings — applied on close (see _close_settings). + self._auto_mods_retrigger = False + self._auto_addons_retrigger = False + + def _draw_logo(self, hover: bool = False): + cv = self._hdr_canvas + cv.delete("logo") + cv.create_text(self._px(24), self._logo_y, text="Octo Updater", + font=("Segoe UI", 24, "bold"), + fill="#b478d9" if hover else "#9a5cbf", + anchor="w", tags="logo") + + def _draw_update_label(self, hover: bool = False): + cv = self._hdr_canvas + cv.delete("upd_label") + self._hdr_regions.pop("update", None) + if not self._update_available: + return + cv.create_text(self._logo_cx, self._logo_y + self._px(26), + text="Update available!", + font=("Segoe UI", 10, "bold"), + fill=C_GOLD_LT if hover else C_GOLD, + anchor="n", tags="upd_label") + lb = cv.bbox("upd_label") + if lb: + self._hdr_regions["update"] = (lb[0] - self._px(4), lb[1] - self._px(2), + lb[2] + self._px(4), lb[3] + self._px(2)) + + def _draw_gear(self, hover: bool = False): + cv = self._hdr_canvas + cv.delete("gear_icon") + cv.create_text(WIN_W - self._px(10), self._px(8), text="⚙", + font=("Segoe UI", 13), + fill=C_GOLD if hover else C_TEXT_DIM, + anchor="ne", tags="gear_icon") + + def _hdr_hit(self, x, y): + for name, (x0, y0, x1, y1) in self._hdr_regions.items(): + if x0 <= x <= x1 and y0 <= y <= y1: + return name + return None + + def _on_hdr_motion(self, event): + name = self._hdr_hit(event.x, event.y) if event is not None else None + if name == self._hdr_hover: + return + prev = self._hdr_hover + self._hdr_hover = name + if prev in self._nav_pos: + self._draw_nav_tab(prev) + if name in self._nav_pos: + self._draw_nav_tab(name, hover=True) + if "gear" in (prev, name): + self._draw_gear(hover=(name == "gear")) + if "logo" in (prev, name): + self._draw_logo(hover=(name == "logo")) + if "update" in (prev, name): + self._draw_update_label(hover=(name == "update")) + self._hdr_canvas.configure(cursor="hand2" if name else "") + + def _on_hdr_click(self, event): + name = self._hdr_hit(event.x, event.y) + if name == "gear": + self._open_settings(event) + elif name in ("logo", "update"): + self._open_url("https://github.com/rebasedkon/octo-updater") + elif name in self._nav_pos: + self._switch_tab(name) + + def _check_updater_update(self): + """Background daily check of the updater's own GitHub releases; shows + the 'Update available!' header label if a newer version exists.""" + try: + tag = fetch_updater_latest_tag() + except Exception: + tag = None + if updater_update_available(tag): + def show(): + self._update_available = True + self._draw_update_label() + self.after(0, show) + + def _build_panel(self): + PANEL_TOP = self._px(109) + PANEL_BOT = FOOT_H + PANEL_H = WIN_H - PANEL_TOP - PANEL_BOT + PAD = self._px(40) + + panel = tk.Frame(self, bg=C_BG) + panel.place(x=PAD, y=PANEL_TOP + self._px(10), + width=WIN_W - PAD * 2, + height=PANEL_H - self._px(20)) + self._news_panel = panel + self._active_panel = panel # NEWS is the initial tab + + inner_w = WIN_W - PAD * 2 + self._news_left_w = int(inner_w * 0.60) + self._news_right_w = inner_w - self._news_left_w - self._px(12) + + # Latest announcement — parchment panel (left) + feat = tk.Frame(panel, bg=C_PARCH) + feat.place(x=0, y=0, width=self._news_left_w, relheight=1.0) + self._feat_frame = feat + + # Patch-notes list (right) + ann = tk.Frame(panel, bg=C_PANEL, + highlightthickness=1, + highlightbackground=C_PANEL_BDR) + ann.place(x=self._news_left_w + self._px(12), y=0, + width=self._news_right_w, relheight=1.0) + self._patch_frame = ann + + self._render_featured(None, loading=True) + self._render_patch_notes(None, loading=True) + + self._log_line("Octo Updater v" + UPDATER_VERSION + "\n", "acct") + self._log_line("─" * 60 + "\n", "dim") + self._build_mods_panel() + self._build_tweaks_panel() + self._build_addons_panel() + self._build_mpq_panel() + + # ── news panel ─────────────────────────────────────────────────────────── + + def _load_news(self, force=False): + self._load_featured(force) + self._load_patch_notes(force) + + def _load_featured(self, force=False): + now = time.time() + if (not force and self._featured is not None + and (now - self._feat_ts) < NEWS_CACHE_TTL): + return + self._render_featured(None, loading=True) + + def worker(): + feat, err = None, "" + try: + feat = fetch_featured_post() + except Exception: + err = "Couldn't reach the news feed." + + def apply(): + self._feat_ts = time.time() + self._featured = feat + self._render_featured(feat, error=err) + self.after(0, apply) + threading.Thread(target=worker, daemon=True).start() + + def _load_patch_notes(self, force=False): + now = time.time() + if (not force and self._patch_items is not None + and (now - self._patch_ts) < NEWS_CACHE_TTL): + return + self._render_patch_notes(None, loading=True) + + def worker(): + items, err = None, "" + try: + items = fetch_patch_notes() + except Exception: + err = "Couldn't reach the news feed." + + def apply(): + self._patch_ts = time.time() + self._patch_items = items + self._render_patch_notes(items, error=err) + self.after(0, apply) + threading.Thread(target=worker, daemon=True).start() + + def _render_featured(self, post, loading=False, error=""): + f = self._feat_frame + for w in f.winfo_children(): + w.destroy() + f.configure(highlightthickness=1, highlightbackground=C_PARCH_EDGE) + + title = (post or {}).get("title", "") + + # Title band — slightly darker parchment strip + band = tk.Frame(f, bg=C_PARCH_BAND) + band.pack(fill="x") + hdr = tk.Frame(band, bg=C_PARCH_BAND) + hdr.pack(fill="x", padx=self._px(20), pady=(self._px(16), self._px(12))) + tk.Label(hdr, + text=title.upper() if title else "NEWS", + font=("Segoe UI", 13, "bold"), + fg=C_PARCH_TITLE, bg=C_PARCH_BAND, + wraplength=self._news_left_w - self._px(100), + justify="left", anchor="w").pack(side="left", + fill="x", expand=True) + rf = tk.Label(hdr, text="⟳", font=("Segoe UI", 14), + fg=C_PARCH_DIM, bg=C_PARCH_BAND, cursor="hand2") + rf.pack(side="right") + rf.bind("", lambda e: self._load_featured(force=True)) + rf.bind("", lambda e: rf.configure(fg=C_PARCH_LINK)) + rf.bind("", lambda e: rf.configure(fg=C_PARCH_DIM)) + + if not post: + msg = error or ("Loading…" if loading + else "No news yet — check back later.") + tk.Label(f, text=msg, font=("Segoe UI", 10), + fg=C_PARCH_DIM, bg=C_PARCH).pack(padx=self._px(20), + pady=self._px(16), + anchor="w") + return + + byline = [] + if post.get("author"): + byline.append(f"by {post['author']}") + byline.append(_format_news_date(post.get("date", ""))) + bl = tk.Frame(f, bg=C_PARCH_BAND) + bl.pack(fill="x") + tk.Label(bl, text=" · ".join(byline), + font=("Segoe UI", 10, "italic"), + fg=C_PARCH_DIM, bg=C_PARCH_BAND, + anchor="w").pack(fill="x", padx=self._px(20), pady=self._px(10)) + tk.Frame(f, bg=C_PARCH_LINE, height=self._px(1)).pack(fill="x") + + # Pack the link first with side="bottom" so it's always reserved its + # space; the body Text (which defaults to 24 lines tall) then fills + # only the remaining area instead of clipping the link off the panel. + if post.get("url"): + link = tk.Label(f, text="⧉ Read full post on the forum", + font=("Segoe UI", 11), + fg=C_PARCH_LINK, bg=C_PARCH, + cursor="hand2", anchor="w") + link.pack(side="bottom", fill="x", padx=self._px(20), + pady=(self._px(4), self._px(16))) + link.bind("", + lambda e, u=post["url"]: self._open_url(u)) + link.bind("", lambda e: link.configure(fg=C_PARCH_TITLE)) + link.bind("", lambda e: link.configure(fg=C_PARCH_LINK)) + + body = _strip_html(post.get("html", "")) + txt = tk.Text(f, bg=C_PARCH, fg=C_PARCH_TEXT, relief="flat", + font=("Segoe UI", 11), wrap="word", height=1, + padx=self._px(2), pady=self._px(8), + spacing2=self._px(4), spacing3=self._px(4), + highlightthickness=0, cursor="arrow") + txt.insert("1.0", body) + txt.configure(state="disabled") + txt.pack(fill="both", expand=True, padx=self._px(20), + pady=(self._px(8), self._px(2))) + + def _render_patch_notes(self, items, loading=False, error=""): + f = self._patch_frame + for w in f.winfo_children(): + w.destroy() + + hdr = tk.Frame(f, bg=C_PANEL) + hdr.pack(fill="x", padx=self._px(14), pady=(self._px(16), self._px(10))) + tk.Label(hdr, text="PATCH NOTES", + font=("Segoe UI", 12, "bold"), + fg=C_GOLD, bg=C_PANEL).pack(side="left") + rf = tk.Label(hdr, text="⟳", font=("Segoe UI", 14), + fg=C_TEXT_DIM, bg=C_PANEL, cursor="hand2") + rf.pack(side="right") + rf.bind("", lambda e: self._load_patch_notes(force=True)) + rf.bind("", lambda e: rf.configure(fg=C_GOLD)) + rf.bind("", lambda e: rf.configure(fg=C_TEXT_DIM)) + + tk.Frame(f, bg=C_DIVIDER, height=self._px(1)).pack( + fill="x", padx=self._px(14)) + + if items is None or error: + msg = error or ("Loading…" if loading + else "Couldn't reach the news feed.") + tk.Label(f, text=msg, font=FONT_BODY, + fg=C_TEXT_DIM, bg=C_PANEL).pack(padx=self._px(14), + pady=self._px(12), + anchor="w") + return + if not items: + tk.Label(f, text="No news yet — check back later.", + font=FONT_BODY, fg=C_TEXT_DIM, + bg=C_PANEL).pack(padx=self._px(14), pady=self._px(12), + anchor="w") + return + + list_frame = tk.Frame(f, bg=C_PANEL) + list_frame.pack(fill="both", expand=True, padx=(self._px(14), self._px(4)), + pady=(0, self._px(10))) + canvas = tk.Canvas(list_frame, bg=C_PANEL, highlightthickness=0) + sb = SlimScrollbar(list_frame, command=canvas.yview, bg=C_PANEL, + width=self._px(10)) + self._wheel_canvases.append(canvas) + inner = tk.Frame(canvas, bg=C_PANEL) + inner.bind("", + lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) + canvas.create_window((0, 0), window=inner, anchor="nw", + width=self._news_right_w - self._px(40)) + canvas.configure(yscrollcommand=sb.set) + sb.pack(side="right", fill="y") + canvas.pack(side="left", fill="both", expand=True) + + wrap_w = self._news_right_w - self._px(50) + for item in items: + top = tk.Frame(inner, bg=C_PANEL) + top.pack(fill="x", pady=(self._px(12), 0)) + tk.Label(top, text=_format_news_date(item.get("date", "")), + font=("Segoe UI", 9), fg=C_TEXT_DIM, + bg=C_PANEL).pack(side="right", anchor="n") + tk.Label(top, text=item.get("title", ""), + font=("Segoe UI", 11, "bold"), + fg=C_GOLD, bg=C_PANEL, + wraplength=wrap_w - self._px(85), justify="left", + anchor="w").pack(side="left", fill="x", expand=True) + + if item.get("author"): + tk.Label(inner, text=f"by {item['author']}", + font=("Segoe UI", 10, "italic"), + fg=C_TEXT_DIM, bg=C_PANEL, + anchor="w").pack(fill="x", pady=(self._px(2), 0)) + + body = item.get("body", "").strip() + if len(body) > 260: + body = body[:260].rstrip() + "…" + if body: + tk.Label(inner, text=body, font=("Segoe UI", 10), + fg=C_TEXT, bg=C_PANEL, + wraplength=wrap_w, justify="left", + anchor="w").pack(fill="x", pady=(self._px(5), 0)) + + if item.get("url"): + lnk = tk.Label(inner, text="⧉ Read more", + font=("Segoe UI", 10), + fg=C_GOLD, bg=C_PANEL, + cursor="hand2", anchor="w") + lnk.pack(fill="x", pady=(self._px(5), 0)) + lnk.bind("", + lambda e, u=item["url"]: self._open_url(u)) + lnk.bind("", lambda e, w=lnk: w.configure(fg=C_GOLD_LT)) + lnk.bind("", lambda e, w=lnk: w.configure(fg=C_GOLD)) + + tk.Frame(inner, bg=C_DIVIDER, height=self._px(1)).pack( + fill="x", pady=(self._px(12), 0)) + + # ── tweaks panel ───────────────────────────────────────────────────────────── + + def _build_tweaks_panel(self): + outer = tk.Frame(self, bg=C_PANEL, + highlightthickness=1, + highlightbackground=C_PANEL_BDR, + highlightcolor=C_PANEL_BDR) + self._tweaks_panel_frame = outer + + self._tweaks_inner = tk.Frame(outer, bg=C_PANEL) + self._tweaks_inner.pack(fill="both", expand=True) + + bar = tk.Frame(outer, bg=C_PANEL) + bar.place(relx=1.0, x=-self._px(16), y=self._px(3), anchor="ne") + + apl = tk.Label(bar, text="Apply", font=("Segoe UI", 11), + fg=C_TEXT, bg=C_PANEL_BDR, cursor="hand2", + padx=self._px(16), pady=self._px(4)) + apl.bind("", lambda e: self._apply_tweaks()) + apl.bind("", lambda e: apl.configure(bg=C_GOLD, fg="#000")) + apl.bind("", lambda e: apl.configure(bg=C_PANEL_BDR, fg=C_TEXT)) + self._tweaks_apply_btn = apl + + rst = tk.Label(bar, text="Reset", font=("Segoe UI", 11), + fg=C_TEXT, bg=C_PANEL_BDR, cursor="hand2", + padx=self._px(16), pady=self._px(4)) + rst.bind("", lambda e: self._reset_tweaks()) + rst.bind("", lambda e: rst.configure(bg=C_GOLD, fg="#000")) + rst.bind("", lambda e: rst.configure(bg=C_PANEL_BDR, fg=C_TEXT)) + self._tweaks_reset_btn = rst + + self._tweak_widgets: dict = {} + self._tweak_vars: dict = {} + + self._build_tweaks_rows() + + def _build_tweaks_rows(self): + for w in self._tweaks_inner.winfo_children(): + w.destroy() + self._tweak_widgets = {} + self._tweak_vars = {} + + values = load_tweaks_config() + PAD_X = self._px(16) + + for (tid, label, kind, recommended, _, desc, mn, mx, step) in TWEAKS_ITEMS: + if kind == "section": + tk.Label(self._tweaks_inner, text=label, + font=("Segoe UI", 11, "bold"), + fg=C_GOLD, bg=C_PANEL, + anchor="w").pack(fill="x", padx=PAD_X, + pady=(self._px(10), self._px(2))) + tk.Frame(self._tweaks_inner, bg=C_DIVIDER, height=self._px(1)).pack( + fill="x", padx=PAD_X, pady=(0, self._px(4))) + continue + + row = tk.Frame(self._tweaks_inner, bg=C_PANEL) + row.pack(fill="x", padx=PAD_X, pady=self._px(3)) + + tk.Label(row, text=label, + font=("Segoe UI", 10, "bold"), + fg=C_TEXT, bg=C_PANEL, + width=22, anchor="w").pack(side="left") + + if kind == "checkbox": + var = tk.BooleanVar(value=values.get(tid, False)) + var.trace_add("write", self._refresh_tweaks_buttons) + tk.Checkbutton(row, variable=var, + bg=C_PANEL, activebackground=C_PANEL, + fg=C_TEXT, selectcolor=C_PANEL, + highlightthickness=0, bd=0, + relief="flat", cursor="hand2" + ).pack(side="left", padx=(self._px(4), self._px(12))) + self._tweak_vars[tid] = var + + elif kind == "number": + val = values.get(tid, mn or 0) + var = tk.StringVar(value=str(int(val))) + var.trace_add("write", self._refresh_tweaks_buttons) + entry = tk.Entry(row, textvariable=var, + bg="#18181e", fg=C_TEXT, + insertbackground=C_GOLD, + relief="flat", font=FONT_MONO, + width=7, + highlightthickness=1, + highlightbackground=C_PANEL_BDR, + highlightcolor=C_GOLD, + justify="center") + entry.pack(side="left", padx=(self._px(4), self._px(12)), + ipady=self._px(3)) + self._tweak_vars[tid] = var + self._tweak_widgets[tid] = entry + + def _clamp(e, t=tid, lo=mn, hi=mx): + try: + v = int(float(self._tweak_vars[t].get())) + if lo is not None: v = max(lo, v) + if hi is not None: v = min(hi, v) + self._tweak_vars[t].set(str(v)) + except ValueError: + self._tweak_vars[t].set(str(TWEAKS_DEFAULTS.get(t, lo or 0))) + entry.bind("", _clamp) + entry.bind("", _clamp) + + elif kind == "dropdown": + cur = values.get(tid, TWEAKS_DEFAULTS.get(tid)) + if cur not in LOCALES: + cur = DEFAULT_LOCALE + var = tk.StringVar(value=cur) # holds the locale code + var.trace_add("write", self._refresh_tweaks_buttons) + mb = tk.Menubutton( + row, text=LOCALES[cur][1], font=("Segoe UI", 10), + fg=C_TEXT, bg="#18181e", activebackground=C_PANEL_BDR, + activeforeground=C_TEXT, relief="flat", cursor="hand2", + width=15, anchor="w", padx=self._px(8), pady=self._px(2), + highlightthickness=1, highlightbackground=C_PANEL_BDR) + menu = tk.Menu(mb, tearoff=0, bg=C_PANEL, fg=C_TEXT, + activebackground=C_GOLD, activeforeground="#000") + for code, (_i, label) in LOCALES.items(): + menu.add_command( + label=label, + command=lambda c=code, v=var: v.set(c)) + mb["menu"] = menu + mb.pack(side="left", padx=(self._px(4), self._px(12))) + var.trace_add( + "write", + lambda *_a, m=mb, v=var: m.configure( + text=LOCALES.get(v.get(), (0, v.get()))[1])) + self._tweak_vars[tid] = var + + if desc: + tk.Label(row, text=desc, + font=("Segoe UI", 10), fg=C_TEXT_DIM, bg=C_PANEL, + wraplength=self._px(520), justify="left", anchor="w" + ).pack(side="left", fill="x", expand=True) + + self._refresh_tweaks_buttons() + + def _refresh_tweaks_buttons(self, *args): + """Show Apply only when the UI differs from the saved config and + Reset only when values are custom (differ from the defaults); paint + out-of-range number entries red.""" + if not getattr(self, "_tweak_vars", None): + return + + any_bad = False + for tid, entry in self._tweak_widgets.items(): + lo, hi = TWEAKS_LIMITS.get(tid, (None, None)) + try: + v = int(float(self._tweak_vars[tid].get())) + bad = ((lo is not None and v < lo) or + (hi is not None and v > hi)) + except ValueError: + bad = True + any_bad = any_bad or bad + entry.configure(fg=C_ERR if bad else C_TEXT) + + ui = self._get_tweaks_from_ui() + saved = load_tweaks_config() + defaults = dict(TWEAKS_DEFAULTS) + defaults["fieldOfView"] = fov_default_for_display() + + def norm(d): + def one(k): + default = TWEAKS_DEFAULTS.get(k) + if isinstance(default, bool): + return bool(d.get(k)) + if isinstance(default, str): + return str(d.get(k, default)) + return int(d.get(k, 0)) + return {k: one(k) for k in ui} + + # An out-of-range entry always counts as a change: _get_tweaks_from_ui + # clamps it, and the clamped value can coincide with the saved one + # (e.g. saved 180, typed 192 → clamps to 180), which would otherwise + # hide the buttons while the entry still shows an invalid number. + ui_n = norm(ui) + dirty = any_bad or ui_n != norm(saved) + custom = any_bad or ui_n != norm(defaults) + + self._tweaks_apply_btn.pack_forget() + self._tweaks_reset_btn.pack_forget() + if dirty: + self._tweaks_apply_btn.pack(side="left") + if custom: + self._tweaks_reset_btn.pack( + side="left", padx=(self._px(8), 0) if dirty else (0, 0)) + + def _refresh_tweaks_panel(self): + values = load_tweaks_config() + for tid, var in self._tweak_vars.items(): + v = values.get(tid, TWEAKS_DEFAULTS.get(tid)) + if isinstance(var, tk.BooleanVar): + var.set(bool(v)) + elif isinstance(TWEAKS_DEFAULTS.get(tid), str): + var.set(v if v in LOCALES else DEFAULT_LOCALE) + else: + var.set(str(int(v)) if v is not None else "") + + def _get_tweaks_from_ui(self) -> dict: + """Read tweak values from the UI, always clamped to their limits — + an out-of-range entry can never reach the config or the exe patch.""" + result = {} + for tid, var in self._tweak_vars.items(): + if isinstance(var, tk.BooleanVar): + result[tid] = var.get() + elif isinstance(TWEAKS_DEFAULTS.get(tid), str): + code = var.get() + result[tid] = code if code in LOCALES else DEFAULT_LOCALE + else: + try: + v = int(float(var.get())) + except ValueError: + v = TWEAKS_DEFAULTS.get(tid, 0) + lo, hi = TWEAKS_LIMITS.get(tid, (None, None)) + if lo is not None: + v = max(lo, v) + if hi is not None: + v = min(hi, v) + result[tid] = v + return result + + def _reset_tweaks(self): + defaults = dict(TWEAKS_DEFAULTS) + defaults["fieldOfView"] = fov_default_for_display() + save_tweaks_config(defaults) + self._refresh_tweaks_panel() + out = self._game_path.get().strip() + if out and os.path.exists(os.path.join(out, "WoW.exe")): + self._set_btn_busy("Patching…") + self._status_var.set("Applying tweaks…") + # Pass the same defaults that were saved + threading.Thread(target=self._apply_tweaks_worker, + args=(out, defaults), daemon=True).start() + + def _apply_tweaks(self): + values = self._get_tweaks_from_ui() + save_tweaks_config(values) + # Write the (possibly clamped) saved values back into the entries so + # the UI never keeps showing an out-of-range number after Apply. + self._refresh_tweaks_panel() + + out = self._game_path.get().strip() + if not out: + self._log_line("Game folder not set.\n", "err") + return + + exe = os.path.join(out, "WoW.exe") + if not os.path.exists(exe): + self._log_line("WoW.exe not found — run Update first.\n", "err") + return + + self._log_line("\nApplying tweaks to WoW.exe...\n", "acct") + self._set_btn_busy("Patching…") + self._status_var.set("Applying tweaks…") + threading.Thread(target=self._apply_tweaks_worker, + args=(out, values), daemon=True).start() + + def _apply_tweaks_worker(self, client_dir: str, tweaks: dict): + log_q = queue.Queue() + prog_q = queue.Queue() + worker = UpdateWorker(client_dir, log_q, prog_q) + + def drain(): + try: + while True: + msg, tag = log_q.get_nowait() + if msg not in ("__DONE__", "__ERROR__") and not msg.startswith("__"): + self.after(0, lambda m=msg, t=tag: self._log_line( + (m if m.endswith("\n") else m + "\n"), t)) + except queue.Empty: + pass + + try: + worker.patch_exe(tweaks) + drain() + update_config_wtf(client_dir, tweaks) + + self._log_line("\nTweaks applied.\n", "ok") + self.after(0, self._refresh_ready_state) + except Exception as e: + drain() + self._log_line(f"\n✗ Tweak patch failed: {e}\n", "err") + + def _fail_state(): + self._status_var.set("Tweaks failed — check the log") + self._set_btn_update() + self.after(0, _fail_state) + + # ── version migrations ───────────────────────────────────────────────────────── + + def _new_version_installed(self): + """One-time, version-gated migrations run at launch. + + The config key ``octo_updater_version`` records the updater version + whose migrations have already been applied to this install (absent on + installs predating this mechanism, < 1.3). When it already matches the + running ``UPDATER_VERSION`` there's nothing to do. + + Otherwise replay every migration step newer than the stored version, + then stamp the current one so each runs exactly once. Runs + synchronously (before the normal startup verify is scheduled), so any + WoW.exe re-patch is settled before verify reads the exe — no read/write + race, and the standard verify still runs and can surface a pending + client update. Future versions add their own ``if from_ver < (x, y): …`` + block below.""" + cfg = load_config() + if cfg.get("octo_updater_version") == UPDATER_VERSION: + return + + if not self._first_run: + from_ver = _parse_version(cfg.get("octo_updater_version", "")) + if from_ver < (1, 3): + self._migrate_1_3() + + self._cfg = update_config( + lambda c: c.__setitem__("octo_updater_version", UPDATER_VERSION)) + + def _migrate_1_3(self): + """1.3: raise the ground-clutter defaults for existing installs. + + frillDistance is a VanillaTweak baked into WoW.exe, so bumping the old + 70 → 120 default needs a re-patch (skipped for a user who already set a + higher value). frillDensity is a plain Config.wtf value; if it's still + below 128 we lift it to the new default in place.""" + out = self._game_path.get().strip() + + # frillDistance (VanillaTweak / WoW.exe). Raise to 120 if it's lower + stored = dict(load_config().get("tweaks", {})) + if int(stored.get("frillDistance", 70)) < 120: + stored["frillDistance"] = 120 + save_tweaks_config(stored) + self._log_line( + "Ground clutter distance raised to 120 (new default).\n", "acct") + if out and os.path.exists(os.path.join(out, "WoW.exe")): + self._log_line("Re-patching WoW.exe for v1.3…\n", "acct") + self._apply_tweaks_worker(out, load_tweaks_config()) + + # frillDensity (Config.wtf value). Raise to 128 if it's lower + cfg_path = os.path.join(out, "WTF", "Config.wtf") if out else "" + if cfg_path and os.path.exists(cfg_path): + try: + with open(cfg_path, "r", encoding="utf-8") as f: + lines = f.readlines() + changed = False + for i, line in enumerate(lines): + if not line.strip().lower().startswith("set frilldensity "): + continue + parts = line.split('"') + cur = (int(parts[1]) if len(parts) >= 2 + and parts[1].strip().lstrip("-").isdigit() else None) + if cur is not None and cur < 128: + lines[i] = 'SET frillDensity "128"\n' + changed = True + break + if changed: + with open(cfg_path, "w", encoding="utf-8") as f: + f.writelines(lines) + self._log_line( + "Ground clutter density raised to 128 (new default).\n", + "acct") + except Exception as e: + self._log_line( + f"Could not update frillDensity in Config.wtf: {e}\n", "err") + + # dlls.txt cleanup: earlier versions wrongly registered the loader + # (VfPatcher.dll) and dxvk here — neither is loaded via dlls.txt, so a + # loader that walks the list would try to load itself / a missing file. + dlls_path = _dlls_txt_path(out) if out else "" + if dlls_path and os.path.exists(dlls_path): + try: + present = {l.strip().lower() + for l in open(dlls_path).read().splitlines()} + stale = [n for n in ("VfPatcher.dll", "dxvk") + if n.lower() in present] + for n in stale: + remove_dll(out, n) + if stale: + self._log_line( + f"Removed stale dlls.txt entries: {', '.join(stale)}.\n", + "acct") + except Exception as e: + self._log_line(f"Could not clean dlls.txt: {e}\n", "err") + + # dxvk.conf: refresh an existing config to the current template + # (d3d9.presentInterval, no cursor / numBackBuffers). Only when DXVK is + # actually present — a missing dxvk.conf is left alone. + if out and os.path.exists(os.path.join(out, "dxvk.conf")): + _write_dxvk_conf(out) + self._log_line("dxvk.conf updated to the new defaults.\n", "acct") + + # Legacy game-hash cache: the torrent-based download no longer keeps a + # per-file SHA-1 cache. Delete the pre-1.3 cache file beside the app. + stale_cache = os.path.join(APP_DIR, "octo_updater_hash_cache.json") + try: + if os.path.exists(stale_cache): + os.remove(stale_cache) + except OSError: + pass + + # Drop the vestigial WoW.exe hash keys from config — unused since the + # download revamp (verify is size-based, patching reads the pristine + # base), so they'd otherwise linger in every migrated config. + self._cfg = update_config( + lambda c: (c.pop("expected_patched_wow_hash", None), + c.pop("original_server_wow_hash", None))) + + self._set_pending_reconcile("keep-config") + + # ── mods panel ─────────────────────────────────────────────────────────────── + + def _build_mods_panel(self): + PAD = self._px(18) + PANEL_TOP = self._px(119) + PANEL_H = WIN_H - PANEL_TOP - FOOT_H - self._px(10) + + outer = tk.Frame(self, bg=C_PANEL, + highlightthickness=1, + highlightbackground=C_PANEL_BDR) + self._mods_panel_frame = outer + + note = tk.Frame(outer, bg=C_PANEL) + note.pack(fill="x", padx=self._px(16), pady=(self._px(14), self._px(8)), + anchor="w") + tk.Label(note, text="Mods marked with ", + font=("Segoe UI", 10), fg=C_TEXT_DIM, + bg=C_PANEL).pack(side="left") + tk.Label(note, text="★", font=("Segoe UI", 10), + fg=C_GOLD, bg=C_PANEL).pack(side="left") + tk.Label(note, text=" are essential", + font=("Segoe UI", 10), fg=C_TEXT_DIM, + bg=C_PANEL).pack(side="left") + + tk.Frame(outer, bg=C_DIVIDER, height=self._px(1)).pack( + fill="x", padx=self._px(16), pady=(0, self._px(4))) + + list_frame = tk.Frame(outer, bg=C_PANEL) + list_frame.pack(fill="both", expand=True, padx=self._px(16)) + + canvas = tk.Canvas(list_frame, bg=C_PANEL, highlightthickness=0) + sb = SlimScrollbar(list_frame, command=canvas.yview, bg=C_PANEL, + width=self._px(10)) + self._wheel_canvases.append(canvas) + self._mods_inner = tk.Frame(canvas, bg=C_PANEL) + self._mods_inner.bind( + "", + lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) + mods_win = canvas.create_window((0, 0), window=self._mods_inner, + anchor="nw") + # Stretch rows to the full canvas width so right-side controls + # (Ignore updates) sit flush against the scrollbar. + canvas.bind("", + lambda e: canvas.itemconfigure(mods_win, width=e.width)) + canvas.configure(yscrollcommand=sb.set) + sb.pack(side="right", fill="y") + canvas.pack(side="left", fill="both", expand=True) + + tk.Frame(outer, bg=C_DIVIDER, height=self._px(1)).pack( + fill="x", padx=self._px(16), pady=(self._px(4), 0)) + foot = tk.Frame(outer, bg=C_PANEL) + foot.pack(fill="x", padx=self._px(16), pady=(self._px(6), self._px(10))) + + # Packed on demand by _refresh_apply_btn_visibility(): shown only + # when there are unapplied checkbox changes or a mod is in error. + self._apply_btn = tk.Label(foot, text="Apply", + font=("Segoe UI", 11), + fg=C_TEXT, bg=C_PANEL_BDR, + cursor="hand2", padx=self._px(16), + pady=self._px(4)) + self._apply_btn.bind("", lambda e: self._apply_mods()) + self._apply_btn.bind("", lambda e: self._apply_btn.configure(bg=C_GOLD, fg="#000")) + self._apply_btn.bind("", lambda e: self._apply_btn.configure(bg=C_PANEL_BDR, fg=C_TEXT)) + + self._mod_row_vars: dict = {} + self._mods_state: list = [] + self._mod_pending_state: dict = {} + self._render_mod_rows() + self._refresh_apply_btn_visibility() + + def _render_mod_rows(self): + cfg = load_config() + mods_cfg = cfg.get("mods", {}) + + # Essential mods first, then alphabetical within each group. + mods_sorted = sorted( + MODS_REGISTRY, + key=lambda m: (not m.get("essential", False), m["name"].lower())) + + if self._mod_row_vars: + for mod in mods_sorted: + mid = mod["id"] + state = mods_cfg.get(mid, {}) + live = next((m for m in self._mods_state if m["id"] == mid), None) + refs = self._mod_row_vars.get(mid, {}) + if not refs: + continue + + if live is not None and "ver_label" in refs: + # Installed mods show their installed version; others + # show the latest available. + ver = state.get("installed_version") or live.get("latest_version") or "unknown" + refs["ver_label"].configure(text=f" {ver}") + + # Checkbox always reflects config only — never a registry default. + # A mod only shows checked if it's actually recorded as installed. + if mid not in self._mod_pending_state: + if "enabled" in refs: + refs["enabled"].set(state.get("enabled", False)) + if "ignore" in refs: + refs["ignore"].set(state.get("ignore_updates", False)) + + has_error = bool(state.get("error")) + installed = bool(state.get("installed_version")) + if "name_label" in refs: + refs["name_label"].configure( + fg=C_ERR if has_error else (C_MOD_HL if installed else C_TEXT)) + if "desc_label" in refs: + refs["desc_label"].configure( + fg=C_TEXT if state.get("enabled", False) else C_TEXT_DIM) + if "error_label" in refs: + if has_error: + refs["error_label"].configure(text=f" \u26a0 {state['error']}") + refs["error_label"].pack(fill="x", pady=(0, self._px(4))) + else: + refs["error_label"].pack_forget() + + if "update_label" in refs: + self._style_mod_action_label(refs["update_label"], + mod, state, live) + return + + for w in self._mods_inner.winfo_children(): + w.destroy() + self._mod_row_vars = {} + + for mod in mods_sorted: + mid = mod["id"] + state = mods_cfg.get(mid, {}) + live = next((m for m in self._mods_state if m["id"] == mid), {}) + + # Installed mods show their installed version; others show latest. + latest_ver = state.get("installed_version") or live.get("latest_version") or "unknown" + # Checkbox reflects only what's actually recorded in config — never + # a registry default. Pending (not-yet-applied) UI changes still + # win so an in-progress toggle survives a background re-render. + enabled = self._mod_pending_state.get(mid, {}).get("enabled", state.get("enabled", False)) + ignore_upd = state.get("ignore_updates", False) + essential = mod.get("essential", False) + installed = bool(state.get("installed_version")) + # Installed mods are highlighted green; the name is neutral text + # otherwise (error state overrides to red in the refresh paths). + name_col = C_MOD_HL if installed else C_TEXT + + container = tk.Frame(self._mods_inner, bg=C_PANEL) + container.pack(fill="x") + + row = tk.Frame(container, bg=C_PANEL) + row.pack(fill="x", pady=self._px(5)) + + name_f = tk.Frame(row, bg=C_PANEL, width=self._px(210)) + name_f.pack(side="left", fill="y") + name_f.pack_propagate(False) + # Essential mods get a gold star badge; a fixed-width slot keeps + # the names aligned whether or not the star is present. + star = tk.Label(name_f, text="★" if essential else "", + font=("Segoe UI", 9), fg=C_GOLD, bg=C_PANEL, + width=2, anchor="w") + star.pack(side="left") + if essential: + self._add_tooltip(star, "Essential mod") + name_label = tk.Label(name_f, text=mod["name"], + font=("Segoe UI", 10, "bold"), + fg=name_col, bg=C_PANEL, anchor="w") + name_label.pack(side="left") + ver_label = tk.Label(name_f, text=f" {latest_ver}", + font=("Segoe UI", 9), fg=C_TEXT_DIM, bg=C_PANEL) + ver_label.pack(side="left") + + enabled_var = tk.BooleanVar(value=enabled) + tk.Checkbutton(row, variable=enabled_var, + bg=C_PANEL, activebackground=C_PANEL, + fg=C_TEXT, selectcolor=C_PANEL, + highlightthickness=0, bd=0, + relief="flat", cursor="hand2", + command=lambda m=mid, v=enabled_var: self._toggle_mod(m, v) + ).pack(side="left", padx=(self._px(4), self._px(8))) + + # Right-side widgets are packed first so they stay pinned to the + # panel's right edge; the description then fills the middle. + ignore_var = tk.BooleanVar(value=ignore_upd) + ig_f = tk.Frame(row, bg=C_PANEL) + ig_f.pack(side="right", padx=(self._px(8), 0)) + tk.Checkbutton(ig_f, variable=ignore_var, + bg=C_PANEL, activebackground=C_PANEL, + fg=C_TEXT, selectcolor=C_PANEL, + highlightthickness=0, bd=0, + relief="flat", cursor="hand2", + command=lambda m=mid, v=ignore_var: self._set_ignore(m, v) + ).pack(side="left") + tk.Label(ig_f, text="Ignore updates", + font=("Segoe UI", 9), fg=C_TEXT_DIM, bg=C_PANEL).pack(side="left") + + link = tk.Label(row, text="⧉", + font=("Segoe UI", 12), fg=C_TEXT_DIM, + bg=C_PANEL, cursor="hand2") + link.pack(side="right", padx=self._px(4)) + link.bind("", lambda e, u=mod["repo_url"]: self._open_url(u)) + link.bind("", lambda e, l=link: l.configure(fg=C_GOLD)) + link.bind("", lambda e, l=link: l.configure(fg=C_TEXT_DIM)) + + update_label = tk.Label(row, text="update", + font=("Segoe UI", 10, "bold"), + fg=C_GOLD, bg=C_PANEL, cursor="hand2") + update_label.bind("", lambda e, m=mid: self._update_mod(m)) + update_label.bind("", lambda e, l=update_label: + l.configure(fg=getattr(l, "_hover", C_GOLD_LT))) + update_label.bind("", lambda e, l=update_label: + l.configure(fg=getattr(l, "_base", C_GOLD))) + self._style_mod_action_label(update_label, mod, state, live) + + desc_label = tk.Label(row, text=mod["description"], + font=("Segoe UI", 10), + fg=(C_TEXT if enabled else C_TEXT_DIM), + bg=C_PANEL, wraplength=self._px(400), + justify="left", anchor="w") + desc_label.pack(side="left", fill="x", expand=True) + + existing_err = state.get("error") + error_label = tk.Label(container, text="", + font=("Segoe UI", 9), fg=C_ERR, + bg=C_PANEL, anchor="w", padx=self._px(16)) + if existing_err: + name_label.configure(fg=C_ERR) + error_label.configure(text=f" \u26a0 {existing_err}") + error_label.pack(fill="x", pady=(0, self._px(4))) + + divider = tk.Frame(self._mods_inner, bg=C_DIVIDER, height=self._px(1)) + divider.pack(fill="x", pady=(self._px(2), 0)) + + self._mod_row_vars[mid] = { + "enabled": enabled_var, + "ignore": ignore_var, + "ver_label": ver_label, + "name_label": name_label, + "desc_label": desc_label, + "error_label": error_label, + "update_label": update_label, + } + + def _load_mods_state(self): + state = [] + for mod in MODS_REGISTRY: + try: + v = fetch_mod_latest_version_cached(mod) + except Exception: + v = None + if v: + state.append({"id": mod["id"], "latest_version": v}) + self._mods_state = state + self.after(0, self._render_mod_rows) + self.after(0, self._refresh_mods_badge) + + def _count_mod_updates(self) -> int: + mods_cfg = load_config().get("mods", {}) + count = 0 + for mod in MODS_REGISTRY: + state = mods_cfg.get(mod["id"], {}) + live = next((m for m in self._mods_state + if m["id"] == mod["id"]), None) + if not state.get("error") and mod_update_available(mod, state, live): + count += 1 + return count + + def _refresh_mods_badge(self): + try: + count = self._count_mod_updates() + except Exception: + count = 0 + if count != self._mod_updates_count: + self._mod_updates_count = count + self._draw_nav_tab("MODS") + + # The two game-resolution fixes can't coexist: enabling one prompts to + # replace the other (see _confirm_res_fix_toggle). + _RES_FIX_CONFLICTS = {"no1600x1200": "VanillaMultiMonitorFix", + "VanillaMultiMonitorFix": "no1600x1200"} + + # Per-mod (title, message, confirm) shown when enabling either resolution + # fix. confirm=True asks Yes/No and only proceeds on Yes; confirm=False is + # an OK-only acknowledgement that always proceeds. + _RES_FIX_NOTICE = { + "no1600x1200": ( + "No1600x1200", + "No1600x1200 mod may cause the game to crash on launch on some " + "systems.\n\n" + "VanillaMultiMonitorFix is the recommended resolution fix. Use " + "No1600x1200 only if VanillaMultiMonitorFix doesn't solve your " + "problem.\n\n" + "Enable No1600x1200 anyway?", + True), + "VanillaMultiMonitorFix": ( + "VanillaMultiMonitorFix", + "Enable this mod only if the game doesn't detect your monitor's " + "native resolution.\n\n" + "VanillaMultiMonitorFix may interfere with correct resolution " + "detection. By default, it's recommended to first try launching the " + "game with DXVK, which may fix the resolution detection issue.\n\n" + "Enable VanillaMultiMonitorFix only if you're actually experiencing " + "resolution issues.", + False), + } + + def _toggle_mod(self, mod_id: str, var: tk.BooleanVar): + # Turning on one of the conflicting resolution fixes needs confirmation + # before it becomes a pending change. + if var.get() and mod_id in self._RES_FIX_CONFLICTS: + if not self._confirm_res_fix_toggle(mod_id, var): + return # backed out — var + pending already reset + self._mod_pending_state.setdefault(mod_id, {})["enabled"] = var.get() + self._refresh_apply_btn_visibility() + + def _mod_is_on(self, mod_id: str) -> bool: + """Whether a mod is currently on in the UI: a pending toggle wins, + otherwise its saved enabled state.""" + pending = self._mod_pending_state.get(mod_id, {}) + if "enabled" in pending: + return bool(pending["enabled"]) + return bool(load_config().get("mods", {}).get(mod_id, {}).get("enabled")) + + def _confirm_res_fix_toggle(self, mod_id: str, var: tk.BooleanVar) -> bool: + """Confirm enabling no1600x1200 / VanillaMultiMonitorFix. They conflict, + so first offer to replace the other one, then show that mod's own notice + (_RES_FIX_NOTICE). Returns True to proceed, or False if the user backed + out — in which case the checkbox and pending state are reset to 'off'.""" + from tkinter import messagebox + other = self._RES_FIX_CONFLICTS[mod_id] + name = next(m["name"] for m in MODS_REGISTRY if m["id"] == mod_id) + oname = next(m["name"] for m in MODS_REGISTRY if m["id"] == other) + + def _cancel(): + var.set(False) + self._mod_pending_state.pop(mod_id, None) + self._refresh_apply_btn_visibility() + + # 1) Conflict — only when the other fix is currently on. + replace_other = self._mod_is_on(other) + if replace_other and not messagebox.askyesno( + "Conflicting mods", + f"{name} and {oname} both affect the game's resolution detection " + f"and can't be used together — only one can be active at a time.\n\n" + f"VanillaMultiMonitorFix is recommended if you experience " + f"resolution issues in the game.\n\n" + f"Replace {oname} with {name}?", + parent=self): + _cancel() + return False + + # 2) That mod's own notice (crash risk / usage guidance). A confirm + # notice can still cancel the toggle; an OK-only one just informs. + title, message, confirm = self._RES_FIX_NOTICE[mod_id] + if confirm: + if not messagebox.askyesno(title, message, parent=self): + _cancel() + return False + else: + messagebox.showinfo(title, message, parent=self) + + # Confirmed — schedule the other fix for removal and untick its box. + if replace_other: + self._mod_pending_state.setdefault(other, {})["enabled"] = False + other_var = self._mod_row_vars.get(other, {}).get("enabled") + if other_var is not None: + other_var.set(False) + return True + + def _set_ignore(self, mod_id: str, var: tk.BooleanVar): + self._mod_pending_state.setdefault(mod_id, {})["ignore_updates"] = var.get() + self._refresh_apply_btn_visibility() + + def _refresh_apply_btn_visibility(self): + """Apply is offered only when there is something to apply: pending + checkbox changes, or a failed mod the user may want to retry.""" + has_error = any(bool(s.get("error")) + for s in load_config().get("mods", {}).values()) + if self._mod_pending_state or has_error: + if not self._apply_btn.winfo_ismapped(): + self._apply_btn.pack(side="left") + else: + self._apply_btn.pack_forget() + + def _open_url(self, url: str): + import webbrowser + webbrowser.open(url) + + def _style_mod_action_label(self, lbl, mod, state, live): + """Drive the per-mod action label: 'retry' (red) when the mod is in + an error state, 'update' (gold) when a newer version is available, + hidden otherwise. Both do the same thing — reinstall the mod.""" + if state.get("error"): + lbl._base, lbl._hover = C_GOLD, C_GOLD_LT + lbl.configure(text="retry", fg=C_GOLD) + lbl.pack(side="right", padx=(self._px(2), self._px(8))) + elif mod_update_available(mod, state, live): + lbl._base, lbl._hover = C_GOLD, C_GOLD_LT + lbl.configure(text="update", fg=C_GOLD) + lbl.pack(side="right", padx=(self._px(2), self._px(8))) + else: + lbl.pack_forget() + + def _update_mod(self, mod_id: str): + """Download and install the newest release of a single mod (the + per-row "update" label). Runs through the normal apply worker so + errors/versions are recorded exactly like a manual Apply.""" + out = self._game_path.get().strip() + if not out: + return + mod = next(m for m in MODS_REGISTRY if m["id"] == mod_id) + self._log_line(f"\nUpdating {mod['name']}...\n", "acct") + self._set_btn_busy("Installing…") + self._status_var.set("Downloading mods…") + threading.Thread(target=self._apply_mods_worker, + args=(out, mod_id), daemon=True).start() + + def _apply_mods(self): + out = self._game_path.get().strip() + if not out: + return + self._apply_btn.configure(text="Applying...", bg="#2a2a32", fg=C_TEXT_DIM) + self._set_btn_busy("Installing…") + self._status_var.set("Downloading mods…") + threading.Thread(target=self._apply_mods_worker, + args=(out,), daemon=True).start() + + def _maybe_install_default_addons(self): + """Auto-install the recommended addons the first time this game + folder is ready — the same one-shot mechanism as the default mods: + the absence of the "addons" key in config means "never initialized + for this folder", and a game-folder change wipes it to re-arm. + Runs after the default mods finished (chained from the mods apply + completion) or directly when mods were already initialized.""" + if self._default_addons_install_started: + return + if load_config().get("addons") is not None: + return # already initialized for this folder + + out = self._game_path.get().strip() + if not out or not os.path.exists(os.path.join(out, "WoW.exe")): + return # game isn't actually installed here yet + + self._default_addons_install_started = True + + # Mark this folder as initialized even if every install fails, so + # the batch doesn't re-fire on the next verify. + update_config(lambda c: c.setdefault("addons", {})) + + # "Install recommended addons" (Settings → General): when + # off, skip the batch install but still verify so the ADDONS tab lists + # them as available for manual install. + if not load_config().get("auto_install_addons", True): + self._addons_verify() + return + + ap = addons_path(out) + recs = [{"folder": name, "status": "available", "git": url, + "branch": None, "ref": None, "toc": {}, + "description": None, "error": None} + for name, url in RECOMMENDED_ADDONS.items() + if not os.path.isdir(os.path.join(ap, name))] + if not recs: + # Nothing to install (e.g. switched to a folder that already has + # the addons) — still run a verify so the ADDONS tab badge shows + # any available updates without the user opening the tab. + self._addons_verify() + return + self._log_line("\nInstalling recommended addons...\n", "acct") + self._addon_apply(recs) + + def _maybe_install_essential_mods(self): + """Auto-install every mod flagged essential the first time this game + folder is ready to use — i.e. on a brand-new install, or right after + the game folder was changed to a new location. Both cases wipe the + "mods" key from config, which is what this checks for, so once this + has run (successfully or not — failures are recorded per-mod) it + won't fire again until the folder changes. + + Reuses the normal apply worker so failures land in config exactly + like a manual Apply would (per-mod "error" field, mod left disabled, + installed_version cached the same way).""" + if self._default_mods_install_started: + return + + cfg = load_config() + if cfg.get("mods"): + return # already initialized for this folder + + out = self._game_path.get().strip() + if not out or not os.path.exists(os.path.join(out, "WoW.exe")): + return # game isn't actually installed here yet + + self._default_mods_install_started = True + + # "Install essential mods" (Settings → General) gates the + # full essential set. VanillaFixes is exempt — it's the loader the other + # mods depend on, so it's always auto-installed. + auto_mods = cfg.get("auto_install_mods", True) + for mod in MODS_REGISTRY: + if mod.get("essential", False) and (auto_mods + or mod["id"] == "VanillaFixes"): + self._mod_pending_state.setdefault(mod["id"], {})["enabled"] = True + + self._log_line("\nInstalling essential mods...\n", "acct") + self._set_btn_busy("Installing…") + self._status_var.set("Downloading mods…") + threading.Thread(target=self._apply_mods_worker, + args=(out,), daemon=True).start() + + def _apply_mods_worker(self, client_dir: str, only_mod_id: str | None = None): + # Work on a detached copy of the "mods" section; it's merged back into + # the live config atomically at the end (update_config). + mods_cfg = load_config().get("mods", {}) + + # Registry order is install order (VanillaFixes first). + ordered = MODS_REGISTRY + + pending = self._mod_pending_state + # Arm the one-time "DXVK first launch" notice if DXVK gets (re)installed + # this run — the new d3d9.dll invalidates the shader cache. + set_dxvk_notice = False + + for mod in ordered: + mid = mod["id"] + if only_mod_id is not None and mid != only_mod_id: + continue + state = mods_cfg.get(mid, {}) + + # Read enabled/ignore from pending UI changes first, fall back to + # saved config. No registry-default fallback here: a mod is only + # ever "enabled" because the user (or the one-time default-mods + # seed in _maybe_install_essential_mods) explicitly said so. + enabled = pending.get(mid, {}).get("enabled", state.get("enabled", False)) + ignore_upd = pending.get(mid, {}).get("ignore_updates", state.get("ignore_updates", False)) + + # A targeted single-mod update/retry always means "install this + # mod". Without this, retrying a failed install is a no-op: the + # error handler recorded enabled=False, so needs_install stays + # False and the mod is skipped (only its error gets cleared). + if only_mod_id is not None and mid == only_mod_id: + enabled = True + + installed_ver = state.get("installed_version") + is_installed = (bool(installed_ver) and + mod_installed_files_present(mod, client_dir)) + + needs_install = enabled and not is_installed + needs_uninstall = not enabled and is_installed + + needs_version_lookup = needs_install or (enabled and is_installed and not ignore_upd) + latest_ver = None + mod_release = None + if needs_version_lookup: + try: + if mod["source"]["kind"] in ("github_release", "codeberg_release"): + mod_release = _fetch_release_cached(mod) + latest_ver = _release_version(mod, mod_release) if mod_release else None + else: + latest_ver = fetch_mod_latest_version_cached(mod) + except Exception: + pass + + update_avail = (is_installed and + latest_ver is not None and + latest_ver != installed_ver and + not ignore_upd) + needs_update = enabled and update_avail + + if not (needs_install or needs_uninstall or needs_update): + if mid in pending: + mods_cfg.setdefault(mid, {}).update({ + "enabled": enabled, + "ignore_updates": ignore_upd, + }) + # A previously failed mod that the user leaves disabled on a + # later Apply counts as dismissed — clear the error so it + # stops blocking the PLAY button. + if not enabled and state.get("error"): + mods_cfg.setdefault(mid, {})["error"] = None + continue + + action = ("Installing" if needs_install else + "Updating" if needs_update else "Removing") + self.after(0, lambda a=action, n=mod["name"]: + self._status_var.set(f"{a} {n}…")) + + try: + if needs_install: + log(f"\nInstalling {mod['name']} {latest_ver}...") + written = install_mod(mod, client_dir, release=mod_release) + if mod.get("register_dll"): + add_dll(client_dir, mod["register_dll"]) + resolved_ver = mod.pop("_resolved_version", None) or latest_ver or "unknown" + mods_cfg[mid] = { + "enabled": True, + "installed_version": resolved_ver, + "installed_files": written, + "ignore_updates": ignore_upd, + "error": None, + } + if mid == "dxvk": + set_dxvk_notice = True + log(f" \u2713 {mod['name']} installed.") + + elif needs_uninstall: + log(f"\nUninstalling {mod['name']}...") + uninstall_mod(mod, client_dir) + if mod.get("register_dll"): + remove_dll(client_dir, mod["register_dll"]) + mods_cfg[mid] = { + "enabled": False, + "installed_version": None, + "installed_files": [], + "ignore_updates": ignore_upd, + "error": None, + } + log(f" \u2713 {mod['name']} uninstalled.") + + elif needs_update: + log(f"\nUpdating {mod['name']} {installed_ver} \u2192 {latest_ver}...") + uninstall_mod(mod, client_dir) + written = install_mod(mod, client_dir, release=mod_release) + if mod.get("register_dll"): + add_dll(client_dir, mod["register_dll"]) + mods_cfg[mid] = { + "enabled": True, + "installed_version": latest_ver, + "installed_files": written, + "ignore_updates": ignore_upd, + "error": None, + } + if mid == "dxvk": + set_dxvk_notice = True + log(f" \u2713 {mod['name']} updated.") + + except Exception as e: + err = describe_install_error(e) + log(f" \u2717 {mod['name']}: {err}") + mods_cfg[mid] = { + "enabled": False, + "installed_version": None, + "installed_files": [], + "ignore_updates": ignore_upd, + "error": err, + } + + # Merge just the "mods" key into the current on-disk config (which + # other threads may have written to during this long install), rather + # than saving our stale whole-config snapshot. + sorted_mods = dict(sorted(mods_cfg.items(), + key=lambda kv: kv[0].lower())) + def _merge(c): + c["mods"] = sorted_mods + if set_dxvk_notice: + c["dxvk_notice_pending"] = True + fresh_cfg = update_config(_merge) + fresh_mods = fresh_cfg.get("mods", {}) + # Keep pending checkbox toggles on a targeted single-mod update — + # they were never applied and would be lost otherwise. + if only_mod_id is None: + self._mod_pending_state = {} + + def _do_inplace_update(): + for mod in MODS_REGISTRY: + mid = mod["id"] + state = fresh_mods.get(mid, {}) + refs = self._mod_row_vars.get(mid, {}) + if not refs: + continue + has_error = bool(state.get("error")) + installed = bool(state.get("installed_version")) + + if mid not in self._mod_pending_state: + refs["enabled"].set(state.get("enabled", False)) + refs["ignore"].set(state.get("ignore_updates", False)) + + live = next((m for m in self._mods_state if m["id"] == mid), {}) + ver = state.get("installed_version") or live.get("latest_version") or "unknown" + if "ver_label" in refs: + refs["ver_label"].configure(text=f" {ver}") + + if "name_label" in refs: + refs["name_label"].configure( + fg=C_ERR if has_error else (C_MOD_HL if installed else C_TEXT)) + + if "desc_label" in refs: + refs["desc_label"].configure( + fg=C_TEXT if state.get("enabled", False) else C_TEXT_DIM) + + if "error_label" in refs: + if has_error: + refs["error_label"].configure( + text=f" \u26a0 {state['error']}") + refs["error_label"].pack(fill="x", pady=(0, self._px(4))) + else: + refs["error_label"].pack_forget() + + if "update_label" in refs: + self._style_mod_action_label(refs["update_label"], + mod, state, live) + + self._apply_btn.configure(text="Apply", bg=C_PANEL_BDR, fg=C_TEXT) + self._refresh_apply_btn_visibility() + self._refresh_mods_badge() + # Fresh setup chain: once the default mods finished installing, + # the recommended addons follow (no-op if already initialized). + self._maybe_install_default_addons() + + # Any mod in an error state (download blocked, API limit, AV + # deleted the archive, …) — bring the MODS tab up so the error + # is visible. PLAY stays disabled via _refresh_ready_state. + if any(bool(s.get("error")) for s in fresh_mods.values()): + self._switch_tab("MODS") + self._refresh_ready_state() + + self.after(0, _do_inplace_update) + + # ── addons panel ───────────────────────────────────────────────────────── + + def _build_addons_panel(self): + outer = tk.Frame(self, bg=C_PANEL, + highlightthickness=1, + highlightbackground=C_PANEL_BDR, + highlightcolor=C_PANEL_BDR) + self._addons_panel_frame = outer + + top = tk.Frame(outer, bg=C_PANEL) + top.pack(fill="x", padx=self._px(16), pady=(self._px(4), 0)) + self._addon_filter_var = tk.StringVar() + self._addon_filter_job = None + self._addon_filter_var.trace_add( + "write", self._on_addon_filter_changed) + ent = tk.Entry(top, textvariable=self._addon_filter_var, + bg="#2b2244", fg=C_TEXT, insertbackground=C_GOLD, + relief="flat", font=("Segoe UI", 10), width=24, + highlightthickness=1, + highlightbackground="#4a3c6e", + highlightcolor=C_GOLD) + tk.Label(top, text="⌕", font=("Segoe UI", 18), + fg=C_TEXT, bg=C_PANEL).pack(side="right") + ent.pack(side="right", ipady=self._px(4), padx=(0, self._px(6))) + + legend = tk.Frame(top, bg=C_PANEL) + legend.pack(side="left") + tk.Label(legend, text="Addons marked with ", + font=("Segoe UI", 10), fg=C_TEXT_DIM, + bg=C_PANEL).pack(side="left") + tk.Label(legend, text="★", font=("Segoe UI", 10), + fg=C_GOLD, bg=C_PANEL).pack(side="left") + tk.Label(legend, text=" are recommended", + font=("Segoe UI", 10), fg=C_TEXT_DIM, + bg=C_PANEL).pack(side="left") + + list_frame = tk.Frame(outer, bg=C_PANEL) + list_frame.pack(fill="both", expand=True, padx=(self._px(16), self._px(4))) + canvas = tk.Canvas(list_frame, bg=C_PANEL, highlightthickness=0) + sb = SlimScrollbar(list_frame, command=canvas.yview, bg=C_PANEL, + width=self._px(10)) + self._wheel_canvases.append(canvas) + self._addons_canvas = canvas + self._addons_win = None + canvas.bind("", + lambda e: self._addons_win is not None + and canvas.itemconfigure(self._addons_win, width=e.width)) + canvas.configure(yscrollcommand=sb.set) + sb.pack(side="right", fill="y") + canvas.pack(side="left", fill="both", expand=True) + self._reset_addons_inner() + + tk.Frame(outer, bg=C_DIVIDER, height=self._px(1)).pack( + fill="x", padx=self._px(16), pady=(self._px(4), 0)) + foot = tk.Frame(outer, bg=C_PANEL) + foot.pack(fill="x", padx=self._px(16), pady=(self._px(8), self._px(12))) + foot.columnconfigure(0, weight=1) + foot.columnconfigure(1, weight=1) + foot.columnconfigure(2, weight=1) + + chk = tk.Label(foot, text="⟳ Check for updates", + font=("Segoe UI", 10), fg=C_TEXT_DIM, bg=C_PANEL, + cursor="hand2") + chk.grid(row=0, column=0, sticky="w") + chk.bind("", lambda e: self._addons_verify(force=True)) + chk.bind("", lambda e: chk.configure(fg=C_GOLD)) + chk.bind("", lambda e: chk.configure(fg=C_TEXT_DIM)) + + add = tk.Label(foot, text="+ Add custom git addon", + font=("Segoe UI", 10, "bold"), fg="#d76f9e", + bg=C_PANEL, cursor="hand2") + add.grid(row=0, column=1) + add.bind("", lambda e: self._open_custom_addon_dialog()) + add.bind("", lambda e: add.configure(fg="#eb96ba")) + add.bind("", lambda e: add.configure(fg="#d76f9e")) + + self._addons_right_lbl = tk.Label(foot, text="", + font=("Segoe UI", 10, "bold"), + bg=C_PANEL, cursor="hand2") + self._addons_right_lbl.grid(row=0, column=2, sticky="e") + self._addons_right_lbl.bind("", + lambda e: self._addon_update_all()) + + self._render_addons() + + # ── MPQ panel ──────────────────────────────────────────────────────────── + + def _build_mpq_panel(self): + outer = tk.Frame(self, bg=C_PANEL, + highlightthickness=1, + highlightbackground=C_PANEL_BDR, + highlightcolor=C_PANEL_BDR) + self._mpq_panel_frame = outer + + list_frame = tk.Frame(outer, bg=C_PANEL) + list_frame.pack(fill="both", expand=True, + padx=(self._px(16), self._px(4)), + pady=(self._px(8), self._px(8))) + canvas = tk.Canvas(list_frame, bg=C_PANEL, highlightthickness=0) + sb = SlimScrollbar(list_frame, command=canvas.yview, bg=C_PANEL, + width=self._px(10)) + self._wheel_canvases.append(canvas) + self._mpq_canvas = canvas + self._mpq_win = None + canvas.bind("", + lambda e: self._mpq_win is not None + and canvas.itemconfigure(self._mpq_win, width=e.width)) + canvas.configure(yscrollcommand=sb.set) + sb.pack(side="right", fill="y") + canvas.pack(side="left", fill="both", expand=True) + + self._mpq_sections_open = {"INSTALLED": True, "AVAILABLE": True} + self._mpq_installed_rows = [] + self._mpq_available_rows = list(MPQ_PATCHES) + self._reset_mpq_inner() + self._render_mpq() + + def _reset_mpq_inner(self): + cv = self._mpq_canvas + old = getattr(self, "_mpq_inner", None) + if old is not None: + old.destroy() + inner = tk.Frame(cv, bg=C_PANEL) + inner.bind("", + lambda e: cv.configure(scrollregion=cv.bbox("all"))) + if self._mpq_win is None: + self._mpq_win = cv.create_window((0, 0), window=inner, anchor="nw", + width=cv.winfo_width() or 1) + else: + cv.itemconfigure(self._mpq_win, window=inner) + cv.yview_moveto(0) + self._mpq_inner = inner + + def _scan_mpq_patches(self) -> list: + """Lettered patch archives (patch-.mpq, case-insensitive) present in the + game's Data folder, e.g. Patch-A.mpq / patch-b.MPQ. Sorted by letter.""" + out = self._game_path.get().strip() + data_dir = os.path.join(out, "Data") if out else "" + found = [] + if data_dir and os.path.isdir(data_dir): + for name in os.listdir(data_dir): + if re.fullmatch(r"patch-[a-z]\.mpq", name, re.IGNORECASE): + found.append(name) + found.sort(key=str.lower) + return found + + def _mpq_check(self): + """Scan Data/ for lettered patches; for the ones we know (registry), + compare the on-disk SHA256 to the server's to flag updates. Feeds the + INSTALLED/AVAILABLE lists and the tab badge; the sha check runs in the + background so the list appears immediately.""" + installed_files = self._scan_mpq_patches() + installed_lc = {f.lower() for f in installed_files} + self._mpq_installed_rows = [ + {"file": f, "entry": mpq_patch_for(f), "update": False} + for f in installed_files] + self._mpq_available_rows = [ + e for e in MPQ_PATCHES if e["file"].lower() not in installed_lc] + self._render_mpq() + + out = self._game_path.get().strip() + data_dir = os.path.join(out, "Data") if out else "" + known = [r for r in self._mpq_installed_rows if r["entry"]] + if not (known and data_dir): + self._mpq_updates_count = 0 + self._draw_nav_tab("MPQ") + return + + def worker(): + results = {} + for row in known: + try: + server = fetch_mpq_sha256(row["entry"]["url"]) + local = sha256_file(os.path.join(data_dir, row["file"])) + results[row["file"]] = (server != local) + except Exception: + results[row["file"]] = False + + def apply(): + count = 0 + for row in self._mpq_installed_rows: + if row["file"] in results: + row["update"] = results[row["file"]] + count += bool(row["update"]) + self._mpq_updates_count = count + self._draw_nav_tab("MPQ") + self._render_mpq() + self.after(0, apply) + threading.Thread(target=worker, daemon=True).start() + + def _render_mpq(self): + if not hasattr(self, "_mpq_inner"): + return + self._reset_mpq_inner() + installed = getattr(self, "_mpq_installed_rows", []) + available = getattr(self, "_mpq_available_rows", []) + busy = getattr(self, "_mpq_busy", None) + + self._mpq_section_header("INSTALLED", installed) + if self._mpq_sections_open.get("INSTALLED", True): + for row in installed: + entry = row["entry"] + name = entry["name"] if entry else row["file"] + desc = entry["description"] if entry else "" + action = ("Update", entry) if (row["update"] and entry) else None + self._mpq_row(name, row["file"], desc, action, + busy == row["file"]) + + self._mpq_section_header("AVAILABLE", available) + if self._mpq_sections_open.get("AVAILABLE", True): + for entry in available: + self._mpq_row(entry["name"], entry["file"], + entry["description"], ("Install", entry), + busy == entry["file"]) + + def _mpq_section_header(self, title: str, rows: list): + f = self._mpq_inner + is_open = self._mpq_sections_open.get(title, True) + hdr = tk.Frame(f, bg=C_PANEL) + hdr.pack(fill="x", pady=(self._px(10), self._px(2))) + arrow = tk.Label(hdr, text="▾" if is_open else "▸", + font=("Segoe UI", 14, "bold"), + fg=C_GOLD, bg=C_PANEL, cursor="hand2", width=2) + arrow.pack(side="left") + lbl = tk.Label(hdr, text=title, font=("Segoe UI", 12, "bold"), + fg=C_GOLD, bg=C_PANEL, cursor="hand2") + lbl.pack(side="left") + tk.Label(hdr, text=f" {len(rows)}", font=("Segoe UI", 10), + fg=C_TEXT_DIM, bg=C_PANEL).pack(side="left") + + def toggle(_e=None, t=title): + self._mpq_sections_open[t] = \ + not self._mpq_sections_open.get(t, True) + self._render_mpq() + arrow.bind("", toggle) + lbl.bind("", toggle) + + if is_open and not rows: + tk.Label(f, text="Nothing here.", font=("Segoe UI", 10), + fg=C_TEXT_DIM, bg=C_PANEL).pack(anchor="w", + padx=self._px(8)) + + def _mpq_row(self, name, filename, description, action, busy): + """One patch row: name (and filename), an Install/Update button or a + 'Downloading…' label, and the description underneath. `action` is + (label, registry_entry) or None.""" + f = self._mpq_inner + box = tk.Frame(f, bg=C_PANEL) + box.pack(fill="x", pady=self._px(4)) + top = tk.Frame(box, bg=C_PANEL) + top.pack(fill="x") + tk.Label(top, text=name, font=("Segoe UI", 11, "bold"), + fg=C_TEXT, bg=C_PANEL, anchor="w").pack(side="left", + padx=self._px(8)) + if filename and filename.lower() != name.lower(): + tk.Label(top, text=f" {filename}", font=("Segoe UI", 9), + fg=C_TEXT_DIM, bg=C_PANEL).pack(side="left") + if busy: + tk.Label(top, text="Downloading…", font=("Segoe UI", 10), + fg=C_TEXT_DIM, bg=C_PANEL).pack(side="right", + padx=self._px(8)) + elif action: + label, entry = action + btn = tk.Label(top, text=label, font=("Segoe UI", 10, "bold"), + fg="#000", bg=C_GOLD, cursor="hand2", + padx=self._px(12), pady=self._px(2)) + btn.pack(side="right", padx=self._px(8)) + btn.bind("", lambda e, en=entry: self._mpq_install(en)) + btn.bind("", lambda e: btn.configure(bg=C_GOLD_LT)) + btn.bind("", lambda e: btn.configure(bg=C_GOLD)) + if description: + tk.Label(box, text=description, font=("Segoe UI", 10), + fg=C_TEXT_DIM, bg=C_PANEL, anchor="w", + wraplength=self._px(620), justify="left").pack( + fill="x", padx=self._px(8), pady=(self._px(2), 0)) + + def _mpq_install(self, entry): + if getattr(self, "_mpq_busy", None): + return # one download at a time + out = self._game_path.get().strip() + if not out: + self._log_line("Set the game folder first.\n", "err") + return + data_dir = os.path.join(out, "Data") + self._mpq_busy = entry["file"] + self._render_mpq() + self._log_line(f"\nDownloading {entry['name']}…\n", "acct") + + def worker(): + err = None + try: + download_mpq_patch(entry, data_dir) + except Exception as e: + err = str(e) + + def done(): + self._mpq_busy = None + if err: + self._log_line(f"✗ {entry['name']} failed: {err}\n", "err") + else: + self._log_line(f"✓ {entry['name']} installed.\n", "ok") + self._mpq_check() # re-scan + re-check + badge + self.after(0, done) + threading.Thread(target=worker, daemon=True).start() + + # ── addons engine (app side) ───────────────────────────────────────────── + + def _addons_verify(self, force=False, remote_checks=True): + """Scan Interface/AddOns, match against the catalog, and check every + tracked addon's remote commit sha (config-cached). With + remote_checks=False the scan is guaranteed network-free: shas come + from the cache only (used for post-install/update refreshes).""" + if self._addons_busy: + return + # A recent verify result is already rendered — plain tab switches + # within the TTL don't need a rescan or a rebuild at all. + if (not force and self._addons_status["state"] == "done" + and (time.time() - self._addons_verified_ts) + < ADDONS_VERIFY_TTL): + return + client = self._game_path.get().strip() + self._addons_busy = True + had_content = bool(self._addons_status["addons"] + or self._addons_status["available"]) + self._addons_status = {**self._addons_status, "state": "verifying"} + if had_content: + # Keep showing the existing list while checking in background. + self._refresh_addons_footer() + else: + self._render_addons() + + def worker(): + try: + catalog = fetch_addons_catalog(force=force) + except Exception: + # offline — fall back to whatever the config still holds + catalog = (load_config().get("addons_catalog_cache", {}) + .get("catalog") or []) + + available = [{"folder": a.get("name"), "status": "available", + "git": a.get("git"), "branch": a.get("branch"), + "ref": a.get("ref"), "toc": a.get("toc") or {}, + "description": a.get("description"), "error": None} + for a in catalog + if a.get("name") and a["name"] not in BLOCKED_ADDONS] + + # Curated recommendations: apply git-URL overrides on top of the + # catalog, and synthesize entries for recommended addons the + # catalog doesn't carry (or has renamed). Overridden forks may + # use a different default branch, so branch/ref are reset. + by_name = {a["folder"]: a for a in available} + for name, override in RECOMMENDED_ADDONS.items(): + rec = by_name.get(name) + if rec is None: + available.append( + {"folder": name, "status": "available", + "git": override, "branch": None, "ref": None, + "toc": {}, "description": None, "error": None}) + elif not _same_git_repo(rec.get("git"), override): + rec.update(git=override, branch=None, ref=None) + + addons = {} + records = load_config().get("addons", {}) + ap = addons_path(client) if client else "" + if ap and os.path.isdir(ap): + for name in sorted(os.listdir(ap)): + if name.startswith(("Blizzard_", "Turtle_")): + continue + dirp = os.path.join(ap, name) + if not os.path.isdir(dirp): + continue + rec = {"folder": name, "status": "unknown", "git": None, + "branch": None, "ref": None, "toc": {}, + "description": None, "error": None} + toc_path = os.path.join(dirp, f"{name}.toc") + if not os.path.exists(toc_path): + rec.update(status="invalid", + error="Missing .toc file") + addons[name] = rec + continue + rec["toc"] = read_toc_file(toc_path) + avail = next((a for a in available + if a["folder"] == name), None) + if avail: + rec["description"] = avail["description"] + saved = records.get(name) + override = RECOMMENDED_ADDONS.get(name) + if (saved and saved.get("git") and override + and not saved.get("custom") + and not _same_git_repo(saved["git"], override)): + # Installed from a different repo than the curated fork + # — offer an update that migrates to the fork. A + # user-added custom addon (saved["custom"]) is left as + # the user chose it, even when its name matches a + # recommended addon. + rec.update(git=override, branch=None, ref=None, + status="outOfDate") + elif saved and saved.get("git"): + rec.update(git=saved.get("git"), + branch=saved.get("branch"), + ref=saved.get("ref"), + custom=saved.get("custom", False)) + if remote_checks: + remote = addon_remote_sha( + rec["git"], rec["branch"], rec["ref"], + force=force) + else: + remote = addon_cached_sha( + rec["git"], rec["branch"], rec["ref"]) + if remote is None: + # no cached answer — assume current rather + # than hitting the network + remote = saved.get("sha") + if remote is None: + rec.update(status="invalid", + error="Failed to verify") + elif remote == saved.get("sha"): + rec["status"] = "upToDate" + else: + rec["status"] = "outOfDate" + elif avail: + # Known catalog addon installed outside the updater — + # offer to take it over via a fresh install. + rec.update(git=avail["git"], branch=avail["branch"], + ref=avail["ref"], status="outOfDate") + addons[name] = rec + + # Overlay install failures from this session: the rescan drops + # them (a failed install leaves no folder on disk), so re-attach + # errors to the matching available row — or synthesize one for a + # failed custom addon. Errors for now-installed folders are stale + # and dropped. + for folder in [f for f in self._addon_errors if f in addons]: + self._addon_errors.pop(folder, None) + by_name = {a["folder"]: a for a in available} + for folder, info in self._addon_errors.items(): + rec = by_name.get(folder) + if rec is None: + rec = {"folder": folder, "status": "available", + "git": info.get("git"), "branch": None, + "ref": None, "toc": {}, "description": None, + "error": None} + available.append(rec) + rec["error"] = info["error"] + + def done(): + changed = (addons != self._addons_status["addons"] + or available != self._addons_status["available"]) + self._addons_status = {"state": "done", "addons": addons, + "available": available} + self._addons_verified_ts = time.time() + self._addons_busy = False + self._refresh_addons_badge() + if changed: + self._render_addons() + else: + self._refresh_addons_footer() + self.after(0, done) + threading.Thread(target=worker, daemon=True).start() + + def _addon_apply(self, recs): + """Install/update the given addon records sequentially.""" + client = self._game_path.get().strip() + if not client or self._addons_busy or not recs: + return + self._addons_busy = True + self._addons_installing = True + for rec in recs: + rec["status"] = "downloading" + self._addons_status["addons"].setdefault(rec["folder"], rec) + self._render_addons() + # PLAY is inactive while addons download/install, same as for mods. + self._set_btn_busy("Installing…") + self._status_var.set("Downloading addons…") + + def worker(): + for rec in recs: + self.after(0, lambda n=rec["folder"]: + self._status_var.set(f"Installing {n}…")) + try: + if not rec.get("git") or not is_allowed_git_url(rec["git"]): + raise RuntimeError("Addon URL is not from an " + "allowed git host") + sha = addon_remote_sha(rec["git"], rec.get("branch"), + rec.get("ref"), raise_errors=True) + if not sha: + raise RuntimeError("Could not resolve remote commit") + install_addon_files(client, rec["folder"], rec["git"], sha) + if rec["folder"] == "pfUI": + patch_pfui_default_profile(client) + record = {"git": rec["git"], "branch": rec.get("branch"), + "ref": rec.get("ref"), "sha": sha} + if rec.get("custom"): + record["custom"] = True + update_config(lambda c, f=rec["folder"], r=record: + c.setdefault("addons", {}).__setitem__(f, r)) + self._addon_errors.pop(rec["folder"], None) + log(f" ✓ Addon {rec['folder']} installed.") + except Exception as e: + err = describe_install_error(e) + log(f" ✗ Addon {rec['folder']}: {err}") + rec.update(status="invalid", error=err) + self._addon_errors[rec["folder"]] = { + "error": err, "git": rec.get("git")} + + def done(): + self._addons_busy = False + self._addons_installing = False + self._addons_verified_ts = 0.0 # make the re-verify run + self._refresh_ready_state() # PLAY active again + # Cache-only refresh: the install itself already resolved and + # cached the shas — no further API requests are needed. + self._addons_verify(remote_checks=False) + self.after(0, done) + threading.Thread(target=worker, daemon=True).start() + + def _addon_update_all(self): + recs = [r for r in self._addons_status["addons"].values() + if r["status"] == "outOfDate"] + self._addon_apply(recs) + + def _addon_remove(self, folder: str): + from tkinter import messagebox + if not messagebox.askyesno( + "Remove addon", + f"Delete {folder} and all of its files?"): + return + client = self._game_path.get().strip() + if not client or self._addons_busy: + return + try: + dirp = os.path.join(addons_path(client), folder) + if os.path.isdir(dirp): + shutil.rmtree(dirp) + update_config(lambda c: c.get("addons", {}).pop(folder, None)) + self._log_line(f"Removed addon {folder}\n", "dim") + except Exception as e: + self._log_line(f"Failed to remove addon {folder}: {e}\n", "err") + self._addons_status["addons"].pop(folder, None) + self._addon_errors.pop(folder, None) + self._refresh_addons_badge() + self._render_addons() + + def _open_custom_addon_dialog(self): + if self._settings_overlay is not None: + return + ov = tk.Frame(self, bg="#0a0a0e") + ov.place(x=0, y=0, width=WIN_W, height=WIN_H) + ov.bind("", lambda e: self._close_settings()) + self._settings_overlay = ov + self.bind("", lambda e: self._close_settings()) + + # Same purple-dark theme as the Settings modal. + P_BG, P_HDR, P_BDR, P_INP = C_PANEL, C_HDR, C_PANEL_BDR, "#0f0b16" + MW, MH = self._px(560), self._px(230) + panel = tk.Frame(ov, bg=P_BG, highlightthickness=1, + highlightbackground=P_BDR, highlightcolor=P_BDR) + panel.place(x=(WIN_W - MW) // 2, y=(WIN_H - MH) // 2 - self._px(20), + width=MW, height=MH) + + hdr = tk.Frame(panel, bg=P_HDR, height=self._px(46)) + hdr.pack(fill="x") + hdr.pack_propagate(False) + tk.Label(hdr, text="ADD CUSTOM GIT ADDON", + font=("Segoe UI", 13, "bold"), + fg=C_PURPLE, bg=P_HDR).pack(side="left", padx=self._px(18)) + x_btn = tk.Label(hdr, text="✕", font=("Segoe UI", 12), + fg=C_TEXT_DIM, bg=P_HDR, cursor="hand2") + x_btn.pack(side="right", padx=self._px(16)) + x_btn.bind("", lambda e: self._close_settings()) + x_btn.bind("", lambda e: x_btn.configure(fg=C_TEXT)) + x_btn.bind("", lambda e: x_btn.configure(fg=C_TEXT_DIM)) + tk.Frame(panel, bg=P_BDR, height=self._px(1)).pack(fill="x") + + body = tk.Frame(panel, bg=P_BG) + body.pack(fill="both", expand=True, padx=self._px(22), + pady=(self._px(16), self._px(12))) + tk.Label(body, text="REPOSITORY URL", + font=("Segoe UI", 10, "bold"), + fg=C_GOLD, bg=P_BG).pack(anchor="w") + url_var = tk.StringVar() + tk.Entry(body, textvariable=url_var, bg=P_INP, fg=C_TEXT, + insertbackground=C_GOLD, relief="flat", font=FONT_MONO, + highlightthickness=1, highlightbackground=P_BDR, + highlightcolor=C_GOLD).pack(fill="x", ipady=self._px(7), + pady=(self._px(6), self._px(6))) + tk.Label(body, + text="Allowed hosts: " + ", ".join(ADDON_GIT_HOSTS), + font=("Segoe UI", 9), fg=C_TEXT_DIM, bg=P_BG).pack(anchor="w") + err = tk.Label(body, text="", font=("Segoe UI", 9), + fg=C_ERR, bg=P_BG) + err.pack(anchor="w") + + def submit(): + url = url_var.get().strip().rstrip("/") + if url.endswith(".git"): + url = url[:-4] + if not is_allowed_git_url(url): + err.configure(text="URL must be https from an allowed host.") + return + folder = url.rsplit("/", 1)[-1] + if not folder or folder in (".", "..") or "\\" in folder: + err.configure(text="Could not derive addon folder name.") + return + self._close_settings() + self._log_line(f"\nInstalling custom addon {folder}…\n", "acct") + self._addon_apply([{"folder": folder, "status": "available", + "git": url, "branch": None, "ref": None, + "toc": {}, "description": None, + "error": None, "custom": True}]) + + btn = tk.Label(body, text="Install", font=("Segoe UI", 11, "bold"), + fg=C_TEXT, bg=P_BDR, cursor="hand2", + padx=self._px(16), pady=self._px(7)) + btn.pack(anchor="e", pady=(self._px(8), 0)) + btn.bind("", lambda e: submit()) + btn.bind("", lambda e: btn.configure(bg=C_GOLD, fg="#000")) + btn.bind("", lambda e: btn.configure(bg=P_BDR, fg=C_TEXT)) + + # ── addons rendering ───────────────────────────────────────────────────── + + def _reset_addons_inner(self): + """Replace the whole rows container with a fresh frame. A single + destroy() tears the old subtree down inside Tk (C code) — far faster + than destroying hundreds of row widgets one by one from Python.""" + cv = self._addons_canvas + old = getattr(self, "_addons_inner", None) + if old is not None: + old.destroy() + inner = tk.Frame(cv, bg=C_PANEL) + inner.bind("", + lambda e: cv.configure(scrollregion=cv.bbox("all"))) + if self._addons_win is None: + self._addons_win = cv.create_window((0, 0), window=inner, + anchor="nw", + width=cv.winfo_width() or 1) + else: + cv.itemconfigure(self._addons_win, window=inner) + cv.yview_moveto(0) + self._addons_inner = inner + + def _on_addon_filter_changed(self, *_args): + """Debounce search input — re-render once typing pauses, not on + every keystroke.""" + if self._addon_filter_job is not None: + self.after_cancel(self._addon_filter_job) + self._addon_filter_job = self.after(250, self._apply_addon_filter) + + def _apply_addon_filter(self): + self._addon_filter_job = None + self._render_addons() + + def _render_addons(self): + """Rebuild the addons list. Rows are created in small batches on the + Tk event loop so a large catalog doesn't freeze the UI.""" + if not hasattr(self, "_addons_inner"): + return + self._addons_render_gen = getattr(self, "_addons_render_gen", 0) + 1 + gen = self._addons_render_gen + self._reset_addons_inner() + + st = self._addons_status + flt = self._addon_filter_var.get().strip().lower() + flt_compact = flt.replace(" ", "") + + def matches(rec): + if not flt: + return True + title = strip_wow_colors((rec.get("toc") or {}).get("Title") or "") + hay = f"{rec['folder']} {title}".lower() + # Space-insensitive both ways: "sell value" finds SellValue, + # "sellvalue" finds "Sell Value". + return flt in hay or flt_compact in hay.replace(" ", "") + + def keep(lst): + lst = sorted(lst, key=lambda r: r["folder"].lower()) + return [r for r in lst if matches(r)] + + installed = keep(st["addons"].values()) + # Recommended addons are mixed into Available and marked with a ★ + # badge. Sort recommended first so they surface at the top of the list. + available = keep(a for a in st["available"] + if a["folder"] not in st["addons"]) + available.sort(key=lambda a: (a["folder"] not in RECOMMENDED_ADDONS, + a["folder"].lower())) + + work = [] + for title, rows in (("INSTALLED", installed), + ("AVAILABLE", available)): + work.append(("header", title, rows)) + if self._addon_sections_open.get(title, True): + work.extend(("row", rec) for rec in rows) + self._addons_build_queue = work + self._refresh_addons_footer() + self._addons_build_step(gen) + + def _addons_build_step(self, gen: int): + """Create up to one batch of queued headers/rows, then yield to the + event loop; abandons the queue if a newer render has started.""" + if gen != self._addons_render_gen: + return + queue = self._addons_build_queue + built = 0 + while queue and built < 14: + item = queue.pop(0) + if item[0] == "header": + self._addon_section_header(item[1], item[2]) + else: + self._addon_row(item[1]) + built += 1 + if queue: + self.after(1, lambda: self._addons_build_step(gen)) + + def _addon_section_header(self, title: str, rows: list): + f = self._addons_inner + is_open = self._addon_sections_open.get(title, True) + + hdr = tk.Frame(f, bg=C_PANEL) + hdr.pack(fill="x", pady=(self._px(10), self._px(2))) + arrow = tk.Label(hdr, text="▾" if is_open else "▸", + font=("Segoe UI", 14, "bold"), + fg=C_GOLD, bg=C_PANEL, cursor="hand2", width=2) + arrow.pack(side="left") + lbl = tk.Label(hdr, text=title, + font=("Segoe UI", 12, "bold"), + fg=C_GOLD, bg=C_PANEL, cursor="hand2") + lbl.pack(side="left") + tk.Label(hdr, text=f" {len(rows)}", font=("Segoe UI", 10), + fg=C_TEXT_DIM, bg=C_PANEL).pack(side="left") + + def toggle(_e=None, t=title): + self._addon_sections_open[t] = \ + not self._addon_sections_open.get(t, True) + self._render_addons() + arrow.bind("", toggle) + lbl.bind("", toggle) + + if is_open and not rows: + msg = ("Verifying…" if self._addons_status["state"] == "verifying" + else "Nothing here.") + tk.Label(f, text=msg, font=("Segoe UI", 10), fg=C_TEXT_DIM, + bg=C_PANEL).pack(anchor="w", padx=self._px(8)) + + def _addon_row(self, rec: dict): + f = self._addons_inner + installed = rec["folder"] in self._addons_status["addons"] + toc = rec.get("toc") or {} + + warnings = [] + if toc.get("Interface") and toc["Interface"] != "11200": + warnings.append(f"Made for client {toc['Interface']}") + # pfUI bundles its own modules, so its .toc dependencies aren't real + # missing addons — never warn about them. + if installed and rec["folder"] != "pfUI": + deps = [d.strip() for d in + (toc.get("Dependencies") or "").replace(";", ",").split(",") + if d.strip()] + missing = [d for d in deps + if d not in self._addons_status["addons"]] + if missing: + warnings.append("Missing deps: " + ", ".join(missing)) + + row = tk.Frame(f, bg=C_PANEL) + row.pack(fill="x", pady=self._px(3)) + + + # right side first so it stays pinned to the edge + if installed: + # Trash can drawn as canvas shapes (handle, lid, tapered body + # with slats) — same fixed-size approach as the download arrow. + rm = tk.Canvas(row, width=self._px(20), height=self._px(18), + bg=C_PANEL, highlightthickness=0, cursor="hand2") + rm.pack(side="right", padx=(self._px(8), self._px(2))) + rm.create_rectangle(*self._px(8, 2, 12, 4), fill="#8a4a4a", + outline="", tags="trash") + rm.create_rectangle(*self._px(4, 4, 16, 6), fill="#8a4a4a", + outline="", tags="trash") + rm.create_polygon(*self._px(5, 8, 15, 8, 14, 16, 6, 16), + fill="#8a4a4a", outline="", tags="trash") + for x in (8, 10, 12): + rm.create_line(*self._px(x, 10, x, 14), fill=C_PANEL) + rm.bind("", + lambda e, n=rec["folder"]: self._addon_remove(n)) + rm.bind("", lambda e, c=rm: + c.itemconfigure("trash", fill=C_ERR)) + rm.bind("", lambda e, c=rm: + c.itemconfigure("trash", fill="#8a4a4a")) + else: + # Download arrow drawn as a polygon — exact size and centering, + # independent of any font, without inflating the row height. + dl = tk.Canvas(row, width=self._px(20), height=self._px(18), + bg=C_PANEL, highlightthickness=0, cursor="hand2") + dl.pack(side="right", padx=(self._px(8), self._px(2))) + dl_item = dl.create_polygon( + *self._px(8, 3, 12, 3, 12, 9, 16, 9, 10, 15, 4, 9, 8, 9), + fill=C_OK, outline="") + dl.bind("", + lambda e, r=rec: self._addon_apply([dict(r)])) + dl.bind("", lambda e, c=dl, i=dl_item: + c.itemconfigure(i, fill="#8fdf8e")) + dl.bind("", lambda e, c=dl, i=dl_item: + c.itemconfigure(i, fill=C_OK)) + + # Repo link on the right (like the Mods tab), between the status text + # and the install/remove icon. + if rec.get("git"): + repo_url = rec["git"][:-4] if rec["git"].endswith(".git") \ + else rec["git"] + lnk = tk.Label(row, text="⧉", font=("Segoe UI", 10), + fg=C_TEXT_DIM, bg=C_PANEL, cursor="hand2") + lnk.pack(side="right", padx=(self._px(4), self._px(2))) + lnk.bind("", lambda e, u=repo_url: self._open_url(u)) + lnk.bind("", lambda e, w=lnk: w.configure(fg=C_GOLD)) + lnk.bind("", lambda e, w=lnk: w.configure(fg=C_TEXT_DIM)) + + status = rec["status"] + if status == "downloading": + tk.Label(row, text="downloading…", font=("Segoe UI", 10), + fg=C_TEXT_DIM, bg=C_PANEL).pack(side="right", padx=self._px(4)) + elif status == "invalid" or rec.get("error"): + # Short marker on the right; the full reason gets its own line + # under the row (long messages would squeeze the description). + tk.Label(row, text="⛔ Addon error", font=("Segoe UI", 10), + fg=C_ERR, bg=C_PANEL).pack(side="right", padx=self._px(4)) + elif status == "outOfDate" and installed: + upd = tk.Label(row, text="Update", font=("Segoe UI", 10, "bold"), + fg=C_GOLD, bg=C_PANEL, cursor="hand2") + upd.pack(side="right", padx=self._px(4)) + upd.bind("", + lambda e, r=rec: self._addon_apply([r])) + upd.bind("", lambda e, w=upd: w.configure(fg=C_GOLD_LT)) + upd.bind("", lambda e, w=upd: w.configure(fg=C_GOLD)) + elif warnings: + tk.Label(row, text=f"⚠ {warnings[0]}", font=("Segoe UI", 10), + fg="#d4b43c", bg=C_PANEL).pack(side="right", padx=self._px(4)) + elif status == "upToDate": + tk.Label(row, text="Up to date", font=("Segoe UI", 10), + fg=C_TEXT_DIM, bg=C_PANEL).pack(side="right", padx=self._px(4)) + elif status == "unknown": + tk.Label(row, text="Not versioned", font=("Segoe UI", 10), + fg=C_TEXT_DIM, bg=C_PANEL).pack(side="right", padx=self._px(4)) + + # name (WoW colour codes honoured) + repo link + name_f = tk.Frame(row, bg=C_PANEL, width=self._px(250)) + name_f.pack(side="left", fill="y") + name_f.pack_propagate(False) + # Gold ★ badge for recommended addons; fixed-width slot keeps the + # titles aligned whether or not the star is present. + is_recommended = rec["folder"] in RECOMMENDED_ADDONS + star = tk.Label(name_f, text="★" if is_recommended else "", + font=("Segoe UI", 9), fg=C_GOLD, bg=C_PANEL, + width=2, anchor="w") + star.pack(side="left") + if is_recommended: + self._add_tooltip(star, "Recommended addon") + title = toc.get("Title") or rec["folder"] + for seg, col in parse_wow_colored(title)[:6]: + tk.Label(name_f, text=seg, font=("Segoe UI", 10, "bold"), + fg=col or C_TEXT, bg=C_PANEL).pack(side="left") + + desc = strip_wow_colors(toc.get("Notes") + or rec.get("description") or "") + tk.Label(row, text=desc, font=("Segoe UI", 10), fg=C_TEXT_DIM, + bg=C_PANEL, wraplength=self._px(430), justify="left", + anchor="w").pack(side="left", fill="x", expand=True) + + if rec.get("error"): + tk.Label(f, text=f" ⚠ {rec['error']}", + font=("Segoe UI", 9), fg=C_ERR, bg=C_PANEL, + wraplength=self._px(840), justify="left", + anchor="w").pack(fill="x", pady=(0, self._px(3))) + + tk.Frame(f, bg=C_DIVIDER, height=self._px(1)).pack( + fill="x", pady=(self._px(3), 0)) + + def _refresh_addons_badge(self): + count = sum(1 for r in self._addons_status["addons"].values() + if r["status"] == "outOfDate") + if count != self._addon_updates_count: + self._addon_updates_count = count + self._draw_nav_tab("ADDONS") + + def _refresh_addons_footer(self): + lbl = self._addons_right_lbl + st = self._addons_status + if st["state"] == "verifying" or self._addons_busy: + lbl.configure(text="Checking…", fg=C_TEXT_DIM, cursor="arrow") + elif any(r["status"] == "outOfDate" + for r in st["addons"].values()): + lbl.configure(text="Update all", fg=C_OK, cursor="hand2") + else: + lbl.configure(text="Everything up to date", fg=C_TEXT_DIM, + cursor="arrow") + + # ── footer ──────────────────────────────────────────────────────────────── + + def _build_footer(self): + foot = tk.Frame(self, bg=C_BG, height=FOOT_H) + foot.place(x=0, y=WIN_H - FOOT_H, width=WIN_W, height=FOOT_H) + + # Bottom-left column: status message on top, PLAY/UPDATE button in + # the middle, client version at the bottom (with a bottom margin so + # the content doesn't sit flush against the window edge). + left = tk.Frame(foot, bg=C_BG) + left.place(x=self._px(40), y=self._px(6)) + + self._status_var = tk.StringVar(value="Ready to update") + tk.Label(left, textvariable=self._status_var, + font=("Segoe UI", 10, "bold"), + fg=C_TEXT, bg=C_BG, width=26, anchor="w").pack(anchor="w") + + # Thin halo frame around the button gives a soft glow that follows + # the button state (gold for UPDATE, green for PLAY). + self._btn_mode = "update" + self._btn_glow = tk.Frame(left, bg="#4a3812") + self._btn_glow.pack(anchor="w", pady=(self._px(6), self._px(6))) + self._upd_btn = tk.Label(self._btn_glow, text="UPDATE", + font=("Segoe UI", 11, "bold"), + fg="#ffffff", bg=C_GOLD, + cursor="hand2", + width=14, pady=self._px(7), + anchor="center") + self._upd_btn.pack(padx=self._px(3), pady=self._px(3)) + self._upd_btn.bind("", lambda e: self._btn_click()) + self._upd_btn.bind("", lambda e: self._btn_hover(True)) + self._upd_btn.bind("", lambda e: self._btn_hover(False)) + + self._client_ver_var = tk.StringVar(value="") + tk.Label(left, textvariable=self._client_ver_var, + font=FONT_VER, fg=C_TEXT_DIM, bg=C_BG).pack( + anchor="w", pady=(0, self._px(36))) + + pb_frame = tk.Frame(foot, bg=C_BG) + pb_frame.place(x=self._px(250), y=0, + width=WIN_W - self._px(250) - self._px(40), height=FOOT_H) + + self._pb_canvas = tk.Canvas(pb_frame, + height=self._px(6), bg=C_BG, + highlightthickness=0) + self._pb_canvas.pack(fill="x", side="bottom", padx=0, + ipady=0, pady=(0, self._px(56))) + self._pb_width = WIN_W - self._px(250) - self._px(40) + self._pb_val = 0.0 + + self._prog_label_var = tk.StringVar(value="") + tk.Label(pb_frame, textvariable=self._prog_label_var, + font=("Segoe UI", 10), fg=C_TEXT, bg=C_BG).pack( + side="bottom", pady=(0, self._px(6))) + + tk.Label(foot, text=f"v{UPDATER_VERSION}", + font=("Courier New", 8), + fg="#555560", bg=C_BG).place(relx=1.0, rely=1.0, + x=self._px(-10), y=self._px(-6), + anchor="se") + + # Align the progress bar's bottom edge exactly with the PLAY/UPDATE + # button's bottom edge once real geometry is known. + def _align_pb(): + self.update_idletasks() + gap = (foot.winfo_rooty() + FOOT_H) - ( + self._btn_glow.winfo_rooty() + self._btn_glow.winfo_height()) + if gap > 0: + self._pb_canvas.pack_configure(pady=(0, gap)) + self.after(60, _align_pb) + + def _draw_progress(self, value: float): + self._pb_val = max(0.0, min(1.0, value)) + c = self._pb_canvas + w = self._pb_width + c.delete("all") + if not self._running: + return + c.create_rectangle(0, 0, w, self._px(6), fill="#1e1e26", outline="") + filled = int(w * self._pb_val) + if filled > 0: + for x in range(filled): + t = x / max(filled - 1, 1) + r_val = int(0xc8 + t * (0xe8 - 0xc8)) + g_val = int(0x92 + t * (0xb8 - 0x92)) + b_val = int(0x2a + t * (0x4b - 0x2a)) + col = f"#{r_val:02x}{g_val:02x}{b_val:02x}" + c.create_line(x, 0, x, self._px(6), fill=col) + + # ── helpers ─────────────────────────────────────────────────────────────── + + def _game_folder_changed(self, new_val: str): + """Wipe all state tied to the previous game folder — the folder-scoped + config (mods/addons install records) and session TTLs/badges — and + point everything at new_val. Called from _close_settings when the user + has switched to a different folder.""" + if os.path.exists(os.path.join(new_val, "WoW.exe")): + remove_wdb(new_val) + + # Wipe folder-scoped config and set the new path in one atomic merge. + # This also re-arms the default-mods / recommended-addons auto-install. + def _wipe(c): + c["out_dir"] = new_val + for k in ("mods", "addons"): + c.pop(k, None) + self._cfg = update_config(_wipe) + + self._mod_pending_state = {} + self._default_mods_install_started = False + self._default_addons_install_started = False + self._client_ready = False + + # Reset session-level state so nothing from the previous folder is + # served from memory: addons verify + rendered list, news timers, badges. + self._addons_verified_ts = 0.0 + self._addons_status = {"state": "idle", "addons": {}, "available": []} + self._addon_errors = {} + self._feat_ts = 0.0 + self._patch_ts = 0.0 + self._mod_updates_count = 0 + self._addon_updates_count = 0 + self._mpq_updates_count = 0 + self._mpq_installed_rows = [] + self._mpq_available_rows = list(MPQ_PATCHES) + self._draw_nav_tab("MODS") + self._draw_nav_tab("ADDONS") + self._draw_nav_tab("MPQ") + self._render_addons() + self._render_mpq() + + self._log_line("\nGame folder changed — state reset.\n", "acct") + + def _set_pending_reconcile(self, mode): + """Set the pending-reconcile mode (None / 'full' / 'keep-config') and + persist it, so a reconcile offered but not applied before quitting is + still pending next launch.""" + self._pending_reconcile = mode or None + self._cfg = update_config( + lambda c: c.__setitem__("pending_reconcile", self._pending_reconcile)) + + def _offer_reconcile(self): + """Surface the Update button for the pending reconcile (mode already + set) WITHOUT running a verify first. Clicking Update then reconciles: + an integrity check + WoW.exe re-patch (+ a fresh Config.wtf when the + mode is 'full') — meaningful even when the game files are already + intact.""" + self._running = False + self._client_ready = False + self._status_var.set("Update available!") + self._set_btn_update() + + def _prompt_av_exclusion(self): + """Ask whether to add the current game folder to Windows Defender + exclusions (some mods can be mistakenly flagged by antivirus).""" + from tkinter import messagebox + if messagebox.askyesno( + "Game folder changed", + "It is highly recommended to add the game folder to your " + "antivirus exclusions. Antivirus software may incorrectly " + "detect some mods as threats and prevent them from being " + "downloaded or installed properly.\n\n" + "Do you want to add the game folder to Defender exclusions?", + parent=self): + self._allow_through_antivirus() + + def _render_log(self, msg: str, tag: str = ""): + """Normalize a raw log message (ensure a trailing newline, auto-tag + when untagged) and append it to the log panel. Main thread only.""" + line = msg if msg.endswith("\n") else msg + "\n" + if not tag: + ml = line.lower() + if "✓" in line or "success" in ml or "complete" in ml or "up to date" in ml: + tag = "ok" + elif "✗" in line or "error" in ml or "fail" in ml or "mismatch" in ml: + tag = "err" + elif line.strip().startswith("["): + tag = "acct" + self._log_line(line, tag) + + def _log_line(self, text: str, tag: str = ""): + self._log_buffer.append((text, tag)) + txt = self._logwin_text + if txt is not None: + try: + txt.configure(state="normal") + if tag: + txt.insert("end", text, tag) + else: + txt.insert("end", text) + txt.see("end") + txt.configure(state="disabled") + except tk.TclError: + self._logwin_text = None + + # ── settings ───────────────────────────────────────────────────────────── + + def _open_settings(self, event=None): + if self._settings_overlay is not None: + return + + ov = tk.Frame(self, bg="#0a0a0e") + ov.place(x=0, y=0, width=WIN_W, height=WIN_H) + ov.bind("", lambda e: self._close_settings()) + self._settings_overlay = ov + + self.bind("", lambda e: self._close_settings()) + + P_BG, P_HDR, P_BDR, P_INP = C_PANEL, C_HDR, C_PANEL_BDR, "#0f0b16" + MW, MH = self._px(800), self._px(500) + panel = tk.Frame(ov, bg=P_BG, highlightthickness=1, + highlightbackground=P_BDR, highlightcolor=P_BDR) + panel.place(x=(WIN_W - MW) // 2, y=(WIN_H - MH) // 2 - self._px(20), + width=MW, height=MH) + + hdr = tk.Frame(panel, bg=P_HDR, height=self._px(46)) + hdr.pack(fill="x") + hdr.pack_propagate(False) + tk.Label(hdr, text="SETTINGS", font=("Segoe UI", 13, "bold"), + fg=C_PURPLE, bg=P_HDR).pack(side="left", padx=self._px(18)) + x_btn = tk.Label(hdr, text="✕", font=("Segoe UI", 12), + fg=C_TEXT_DIM, bg=P_HDR, cursor="hand2") + x_btn.pack(side="right", padx=self._px(16)) + x_btn.bind("", lambda e: self._close_settings()) + x_btn.bind("", lambda e: x_btn.configure(fg=C_TEXT)) + x_btn.bind("", lambda e: x_btn.configure(fg=C_TEXT_DIM)) + tk.Frame(panel, bg=P_BDR, height=self._px(1)).pack(fill="x") + + PADX = self._px(22) + body = tk.Frame(panel, bg=P_BG) + body.pack(fill="both", expand=True, padx=PADX, + pady=(self._px(16), self._px(12))) + + loc_row = tk.Frame(body, bg=P_BG) + loc_row.pack(fill="x") + tk.Label(loc_row, text="GAME FOLDER", + font=("Segoe UI", 10, "bold"), + fg=C_GOLD, bg=P_BG).pack(side="left") + opn = tk.Label(loc_row, text="Open folder", font=FONT_BODY, + fg=C_TEXT_DIM, bg=P_BG, cursor="hand2") + opn.pack(side="left", padx=(self._px(16), 0)) + opn.bind("", lambda e: self._open_client_folder()) + opn.bind("", lambda e: opn.configure(fg=C_GOLD)) + opn.bind("", lambda e: opn.configure(fg=C_TEXT_DIM)) + + # Same StringVar as the Update tab's Game folder entry — changing it + # here fires the exact same folder-change mechanics immediately. + path_row = tk.Frame(body, bg=P_BG) + path_row.pack(fill="x", pady=(self._px(8), 0)) + ent = tk.Entry(path_row, textvariable=self._game_path, + bg=P_INP, fg=C_TEXT, relief="flat", font=FONT_MONO, + state="readonly", readonlybackground=P_INP, + highlightthickness=1, highlightbackground=P_BDR, + highlightcolor=P_BDR) + ent.pack(side="left", fill="x", expand=True, ipady=self._px(7)) + chg = tk.Label(path_row, text="Change", + font=("Segoe UI", 10, "bold"), + fg=C_TEXT, bg=P_BDR, cursor="hand2", + padx=self._px(16), pady=self._px(7)) + chg.pack(side="left", padx=(self._px(8), 0)) + chg.bind("", lambda e: self._settings_change_dir()) + chg.bind("", lambda e: chg.configure(bg=C_GOLD, fg="#000")) + chg.bind("", lambda e: chg.configure(bg=P_BDR, fg=C_TEXT)) + + # Two equal-width columns via grid, so the right column keeps a fixed + # position and reaches toward the right edge — regardless of how wide + # the left column's text is. + cols = tk.Frame(body, bg=P_BG) + cols.pack(fill="x", pady=(self._px(20), 0)) + cols.columnconfigure(0, weight=3, uniform="s") + cols.columnconfigure(1, weight=2, uniform="s") + lcol = tk.Frame(cols, bg=P_BG) + lcol.grid(row=0, column=0, sticky="nw") + rcol = tk.Frame(cols, bg=P_BG) + rcol.grid(row=0, column=1, sticky="nw") + + tk.Label(lcol, text="DOWNLOAD MIRROR", + font=("Segoe UI", 10, "bold"), + fg=C_GOLD, bg=P_BG).pack(anchor="w", pady=(0, self._px(4))) + mir = tk.Frame(lcol, bg=P_BG) + mir.pack(anchor="w") + tk.Label(mir, text="●", font=("Segoe UI", 9), + fg=C_OK, bg=P_BG).pack(side="left") + tk.Label(mir, text=" Iceland", font=("Segoe UI", 10, "bold"), + fg=C_TEXT, bg=P_BG).pack(side="left") + self._mirror_status_lbl = tk.Label(mir, text="checking…", + font=("Segoe UI", 9), + fg=C_TEXT_DIM, bg=P_BG) + self._mirror_status_lbl.pack(side="left", padx=(self._px(8), 0)) + rf = tk.Label(mir, text="⟳", font=("Segoe UI", 11), + fg=C_TEXT_DIM, bg=P_BG, cursor="hand2") + rf.pack(side="left", padx=(self._px(6), 0)) + rf.bind("", lambda e: self._check_mirror_status()) + rf.bind("", lambda e: rf.configure(fg=C_GOLD)) + rf.bind("", lambda e: rf.configure(fg=C_TEXT_DIM)) + self._check_mirror_status() + + tk.Label(lcol, text="TROUBLESHOOTING", + font=("Segoe UI", 10, "bold"), + fg=C_GOLD, bg=P_BG).pack(anchor="w", pady=(self._px(22), 0)) + + def _titem(icon, text, cmd, icon_color=C_GOLD): + r = tk.Frame(lcol, bg=P_BG, cursor="hand2") + r.pack(anchor="w", pady=(self._px(12), 0)) + # Monochrome glyphs in a fixed-width slot so all icons line up + # and read at the same size (color emoji would render larger). + ic = tk.Label(r, text=icon, font=("Segoe UI Symbol", 11), + fg=icon_color, bg=P_BG, width=2, anchor="w") + ic.pack(side="left") + tl = tk.Label(r, text=text, font=("Segoe UI", 10), + fg=C_TEXT, bg=P_BG) + tl.pack(side="left") + for w in (r, ic, tl): + w.bind("", lambda e: cmd()) + w.bind("", lambda e: tl.configure(fg=C_GOLD)) + w.bind("", lambda e: tl.configure(fg=C_TEXT)) + + _titem("✓", "Verify game files", self._settings_verify) + _titem("☰", "Show logs", self._show_logs) + _titem("⛊", "Add game folder to Defender exclusions", + self._allow_through_antivirus) + + tk.Label(lcol, text="SUPPORT THE DEVELOPER", + font=("Segoe UI", 10, "bold"), + fg=C_GOLD, bg=P_BG).pack(anchor="w", pady=(self._px(22), 0)) + _titem("♥", "Ko-fi", + lambda: self._open_url("https://ko-fi.com/rebased"), + icon_color="#e8615f") + _titem("☕", "Buy Me a Coffee", + lambda: self._open_url("https://buymeacoffee.com/rebased"), + icon_color="#b5854f") + + tk.Label(rcol, text="GENERAL", + font=("Segoe UI", 10, "bold"), + fg=C_GOLD, bg=P_BG).pack(anchor="w") + tk.Checkbutton(rcol, text=" Clear WDB on game launch", + variable=self._clear_wdb_var, + command=self._toggle_clear_wdb, + font=("Segoe UI", 10), fg=C_TEXT, bg=P_BG, + activebackground=P_BG, activeforeground=C_TEXT, + selectcolor=P_INP, highlightthickness=0, bd=0, + cursor="hand2").pack(anchor="w", pady=(self._px(10), 0)) + tk.Checkbutton(rcol, text=" Minimize Octo Updater on game launch", + variable=self._close_on_launch_var, + command=self._toggle_close_on_launch, + font=("Segoe UI", 10), fg=C_TEXT, bg=P_BG, + activebackground=P_BG, activeforeground=C_TEXT, + selectcolor=P_INP, highlightthickness=0, bd=0, + cursor="hand2").pack(anchor="w", pady=(self._px(10), 0)) + cb_auto_mods = tk.Checkbutton( + rcol, text=" Install essential mods", + variable=self._auto_mods_var, command=self._toggle_auto_mods, + font=("Segoe UI", 10), fg=C_TEXT, bg=P_BG, + activebackground=P_BG, activeforeground=C_TEXT, + selectcolor=P_INP, highlightthickness=0, bd=0, cursor="hand2") + cb_auto_mods.pack(anchor="w", pady=(self._px(10), 0)) + self._add_tooltip( + cb_auto_mods, + "VanillaFixes will always be installed, even when this " + "option is turned off") + tk.Checkbutton(rcol, text=" Install recommended addons", + variable=self._auto_addons_var, + command=self._toggle_auto_addons, + font=("Segoe UI", 10), fg=C_TEXT, bg=P_BG, + activebackground=P_BG, activeforeground=C_TEXT, + selectcolor=P_INP, highlightthickness=0, bd=0, + cursor="hand2").pack(anchor="w", pady=(self._px(10), 0)) + cb_ignore_speech = tk.Checkbutton( + rcol, text=" Ignore speech.mpq", + variable=self._ignore_speech_var, + command=self._toggle_ignore_speech, + font=("Segoe UI", 10), fg=C_TEXT, bg=P_BG, + activebackground=P_BG, activeforeground=C_TEXT, + selectcolor=P_INP, highlightthickness=0, bd=0, cursor="hand2") + cb_ignore_speech.pack(anchor="w", pady=(self._px(10), 0)) + self._add_tooltip( + cb_ignore_speech, + "Ignores verification and updates for speech.mpq, allowing custom " + "speech sounds") + + def _close_settings(self): + self.unbind("") + if self._settings_overlay is not None: + self._settings_overlay.destroy() + self._settings_overlay = None + + # Closing Settings is the "Confirm" action. It needs a reconcile when + # this is the first run, or the chosen folder now differs from the saved + # one — the two cases are handled identically from here on. + new_path = os.path.normpath(self._game_path.get().strip()) + saved = os.path.normpath(self._cfg.get("out_dir", "")) + folder_changed = bool(new_path) and new_path != saved + needs_reconcile = self._first_run or folder_changed + self._first_run = False + + if folder_changed: + self._game_folder_changed(new_path) + if needs_reconcile: + self._set_pending_reconcile("full") + self._offer_reconcile() + # The reconcile installs mods/addons itself — drop any pending + # auto-install retriggers so they don't race it. + self._auto_mods_retrigger = self._auto_addons_retrigger = False + else: + # A settings change with no reconcile: apply an auto-install option + # the user turned on this session (idempotent — no-op if nothing + # missing). + if self._auto_mods_retrigger: + self._auto_mods_retrigger = False + self._install_missing_essential_mods() + if self._auto_addons_retrigger: + self._auto_addons_retrigger = False + self._install_missing_recommended_addons() + + # Offer a Defender exclusion at each reconcile, unless the user already + # added one via Settings since the last reconcile. Reset after, so a + # later reconcile (e.g. another folder change) offers it again. + if needs_reconcile: + if not self._av_excluded: + self._prompt_av_exclusion() + self._av_excluded = False + + def _open_client_folder(self): + import subprocess + path = os.path.normpath(self._game_path.get().strip()) + if os.path.isdir(path): + # Explicit explorer.exe, not os.startfile: ShellExecute resolves + # extensionless paths against PATHEXT/.lnk, so a Desktop shortcut + # named like the folder (e.g. "OctoWoW.lnk") gets *executed* + # instead of the folder being opened. + subprocess.Popen(["explorer.exe", path]) + self._log_line(f"Opened folder: {path}\n", "dim") + else: + self._log_line(f"Folder not found: {path}\n", "err") + + def _settings_change_dir(self): + cur = self._game_path.get() + initial = cur if os.path.isdir(cur) else os.path.expanduser("~") + chosen = filedialog.askdirectory( + title="Select game client folder", + initialdir=initial, mustexist=False) + if chosen: + # normpath → backslashes; fires the folder-change reset + self._game_path.set(os.path.normpath(chosen)) + + def _settings_verify(self): + self._close_settings() + self._verify_game_files() + + def _allow_through_antivirus(self): + """Add a Windows Defender exclusion for the game folder (asks for + admin elevation via UAC).""" + client_dir = os.path.normpath(self._game_path.get().strip()) + if not client_dir or client_dir == ".": + return + import ctypes + cmd = f"Add-MpPreference -ExclusionPath '{client_dir}'" + r = ctypes.windll.shell32.ShellExecuteW( + None, "runas", "powershell.exe", + f'-NoProfile -WindowStyle Hidden -Command "{cmd}"', None, 0) + if r > 32: + self._av_excluded = True + self._log_line( + f"Requested Defender exclusion for: {client_dir}\n", "ok") + else: + self._log_line("Antivirus exclusion cancelled.\n", "err") + + def _maybe_show_aria2_firewall_notice(self): + """One-time heads-up, shown before the first download, that the bundled + aria2c downloader may trigger a Windows Firewall prompt — so it doesn't + look sketchy to a non-technical user. Purely informational; tracked in + the config so it appears only once.""" + if self._cfg.get("aria2_firewall_notice_shown"): + return + from tkinter import messagebox + messagebox.showinfo( + "Downloading the game", + "Octo Updater downloads the game with aria2c, the same tool the " + "official launcher uses.\n\n" + "You may be asked to allow aria2c through the firewall. You can " + "safely reject the request, but download speeds may be slightly " + "lower.\n\n" + "This message is shown only once.", + parent=self) + self._cfg = update_config( + lambda c: c.__setitem__("aria2_firewall_notice_shown", True)) + + def _check_mirror_status(self): + lbl = self._mirror_status_lbl + lbl.configure(text="checking…", fg=C_TEXT_DIM) + + def worker(): + ok = False + try: + req = urllib.request.Request( + CLIENT_TORRENT_URL, headers={"User-Agent": UA}) + with secure_urlopen(req, timeout=6, + allowed_hosts=ALLOWED_DOWNLOAD_HOSTS): + ok = True + except Exception: + ok = False + + def upd(): + try: + lbl.configure(text="online" if ok else "offline", + fg=C_OK if ok else C_ERR) + except tk.TclError: + pass + self.after(0, upd) + threading.Thread(target=worker, daemon=True).start() + + def _toggle_clear_wdb(self): + val = self._clear_wdb_var.get() + self._cfg = update_config( + lambda c: c.__setitem__("clear_wdb_on_launch", val)) + + def _toggle_close_on_launch(self): + val = self._close_on_launch_var.get() + self._cfg = update_config( + lambda c: c.__setitem__("close_on_launch", val)) + + def _toggle_auto_mods(self): + val = self._auto_mods_var.get() + self._cfg = update_config( + lambda c: c.__setitem__("auto_install_mods", val)) + # Install the missing essential mods only when Settings is closed; + # turning it back off cancels the pending install. + self._auto_mods_retrigger = val + + def _toggle_auto_addons(self): + val = self._auto_addons_var.get() + self._cfg = update_config( + lambda c: c.__setitem__("auto_install_addons", val)) + self._auto_addons_retrigger = val + + def _toggle_ignore_speech(self): + self._cfg = update_config( + lambda c: c.__setitem__("ignore_speech", + self._ignore_speech_var.get())) + + def _install_missing_essential_mods(self): + """Install every essential mod not already present. Used when the user + turns 'Install essential mods' on after the fact.""" + if self._running: + return + out = self._game_path.get().strip() + if not out or not os.path.exists(os.path.join(out, "WoW.exe")): + return # no client yet — the fresh-folder auto-install handles it + mods_cfg = load_config().get("mods", {}) + pending = False + for mod in MODS_REGISTRY: + if not mod.get("essential", False): + continue + state = mods_cfg.get(mod["id"], {}) + if (state.get("installed_version") + and mod_installed_files_present(mod, out)): + continue # already installed + self._mod_pending_state.setdefault(mod["id"], {})["enabled"] = True + pending = True + if not pending: + return + self._log_line("\nInstalling essential mods...\n", "acct") + self._set_btn_busy("Installing…") + self._status_var.set("Downloading mods…") + threading.Thread(target=self._apply_mods_worker, + args=(out,), daemon=True).start() + + def _install_missing_recommended_addons(self): + """Install every recommended addon not already present. Used when the + user turns 'Install recommended addons' on afterwards.""" + if self._addons_busy: + return + out = self._game_path.get().strip() + if not out or not os.path.exists(os.path.join(out, "WoW.exe")): + return + ap = addons_path(out) + recs = [{"folder": name, "status": "available", "git": url, + "branch": None, "ref": None, "toc": {}, + "description": None, "error": None} + for name, url in RECOMMENDED_ADDONS.items() + if not os.path.isdir(os.path.join(ap, name))] + if not recs: + return + self._log_line("\nInstalling recommended addons...\n", "acct") + self._addon_apply(recs) + + def _verify_game_files(self): + """Full integrity pass: aria2 hash-checks every game file's pieces + against the torrent and re-downloads only the bad/missing ones, then + re-patches WoW.exe. Catches same-size corruption the routine size-based + sync can't. Installed mods are left alone.""" + if self._running: + return + self._client_ready = False + self._log_line( + "\nVerify game files — hash-checking every file against the torrent.\n", + "acct") + self._start_update(check_integrity=True) + + def _show_logs(self): + if self._logwin is not None: + try: + self._logwin.deiconify() + self._logwin.lift() + self._logwin.focus_force() + return + except tk.TclError: + self._logwin = None + self._logwin_text = None + + win = tk.Toplevel(self) + win.title("Octo Updater — Logs") + LW, LH = self._px(760), self._px(420) + sw, sh = win.winfo_screenwidth(), win.winfo_screenheight() + win.geometry(f"{LW}x{LH}+{(sw - LW) // 2}+{(sh - LH) // 2}") + win.configure(bg=C_BG) + + top = tk.Frame(win, bg=C_BG) + top.pack(fill="x", padx=self._px(12), pady=(self._px(10), self._px(4))) + tk.Label(top, text="SESSION LOG", font=("Segoe UI", 9, "bold"), + fg=C_GOLD, bg=C_BG).pack(side="left") + + outer = tk.Frame(win, bg=C_BG) + outer.pack(fill="both", expand=True, padx=self._px(12), + pady=(0, self._px(12))) + sb = SlimScrollbar(outer, bg=C_BG, width=self._px(10)) + sb.pack(side="right", fill="y") + txt = tk.Text(outer, bg=C_LOG_BG, fg=C_TEXT, + insertbackground=C_TEXT, relief="flat", + font=FONT_MONO, wrap="word", state="disabled", + padx=self._px(10), pady=self._px(8), yscrollcommand=sb.set, + cursor="arrow", selectbackground=C_PANEL_BDR) + txt.pack(side="left", fill="both", expand=True) + sb.command = txt.yview + for t, c in (("ok", C_OK), ("err", C_ERR), + ("dim", C_TEXT_DIM), ("acct", C_GOLD)): + txt.tag_config(t, foreground=c) + + txt.configure(state="normal") + for text, tag in self._log_buffer: + if tag: + txt.insert("end", text, tag) + else: + txt.insert("end", text) + txt.see("end") + txt.configure(state="disabled") + + def _close(): + self._logwin = None + self._logwin_text = None + win.destroy() + win.protocol("WM_DELETE_WINDOW", _close) + + self._logwin = win + self._logwin_text = txt + + # ── button helpers ─────────────────────────────────────────────────────────── + + def _set_btn_play(self): + self._btn_mode = "play" + self._upd_btn.configure(text="PLAY", bg=C_GREEN_BTN, fg="#ffffff") + self._btn_glow.configure(bg="#2b511d") + + def _set_btn_update(self): + self._btn_mode = "update" + self._upd_btn.configure(text="UPDATE", bg=C_GOLD, fg="#ffffff") + self._btn_glow.configure(bg="#4a3812") + + def _set_btn_busy(self, label="…"): + self._btn_mode = "busy" + self._upd_btn.configure(text=label, bg="#2a2434", fg=C_TEXT_DIM) + self._btn_glow.configure(bg="#211c2c") + + def _btn_hover(self, entering: bool): + if self._btn_mode == "busy": + return + if entering: + col = C_GREEN_HOV if self._btn_mode == "play" else C_GOLD_LT + glow = "#397024" if self._btn_mode == "play" else "#5c4a16" + else: + col = C_GREEN_BTN if self._btn_mode == "play" else C_GOLD + glow = "#2b511d" if self._btn_mode == "play" else "#4a3812" + self._upd_btn.configure(bg=col) + self._btn_glow.configure(bg=glow) + + def _mods_have_errors(self) -> bool: + return any(bool(s.get("error")) + for s in load_config().get("mods", {}).values()) + + def _refresh_ready_state(self): + """Recompute the footer status/button after an operation finishes. + PLAY is only offered when the client files are up to date AND no mod + is in an error state — otherwise the button stays grey and inactive.""" + # Never re-enable PLAY while addons are actively installing — this + # guards against a stray call during the mods→addons setup chain or a + # post-install verify flipping the button back on mid-download. + if self._addons_installing: + self._set_btn_busy("Installing…") + self._status_var.set("Downloading addons…") + return + if not self._client_ready: + self._status_var.set("Update available!") + self._set_btn_update() + return + if self._mods_have_errors(): + self._set_btn_busy("PLAY") + self._status_var.set("Mod errors — check MODS tab") + else: + self._set_btn_play() + self._status_var.set("Everything up to date!") + + def _btn_click(self): + if self._btn_mode == "play": + self._launch_game() + elif self._btn_mode == "update": + self._start_update() + + def _launch_game(self): + """Launch the game detached. + If VanillaFixes is installed use VanillaFixes.exe (it injects dlls then + starts WoW.exe itself). Otherwise fall back to WoW.exe directly.""" + import subprocess + client_dir = self._game_path.get().strip() + cfg = load_config() + vf_state = cfg.get("mods", {}).get("VanillaFixes", {}) + vf_installed = (vf_state.get("enabled") and + vf_state.get("installed_version") and + os.path.exists(os.path.join(client_dir, "VanillaFixes.exe"))) + + if vf_installed: + exe = os.path.join(client_dir, "VanillaFixes.exe") + exe_lbl = "VanillaFixes.exe" + else: + exe = os.path.join(client_dir, "WoW.exe") + exe_lbl = "WoW.exe" + + if not os.path.exists(exe): + self._log_line(f"{exe_lbl} not found at: {exe}\n", "err") + return + + # One-time DXVK first-launch notice (armed when DXVK was installed). + if cfg.get("dxvk_notice_pending"): + self._cfg = update_config( + lambda c: c.pop("dxvk_notice_pending", None)) + from tkinter import messagebox + messagebox.showinfo( + "DXVK mod first launch", + "Initial shader compilation may cause temporary in-game " + "stuttering during the first launch. This is a normal process " + "while the game builds its shader cache.\n\n" + "Users with AMD GPUs experiencing stability issues can switch " + "to DXVK 2.5.3", + parent=self) + + if self._cfg.get("clear_wdb_on_launch", False): + remove_wdb(client_dir) + + try: + flags = (getattr(subprocess, "DETACHED_PROCESS", 0) + | getattr(subprocess, "CREATE_BREAKAWAY_FROM_JOB", 0)) + try: + subprocess.Popen([exe], cwd=client_dir, + creationflags=flags, close_fds=True) + except OSError: + # The job object doesn't permit breakaway — retry without it. + flags &= ~getattr(subprocess, "CREATE_BREAKAWAY_FROM_JOB", 0) + subprocess.Popen([exe], cwd=client_dir, + creationflags=flags, close_fds=True) + self._log_line(f"Launched {exe_lbl}!\n", "ok") + # Briefly disable PLAY so a double-click can't spawn two clients. + self._set_btn_busy("PLAY") + self._status_var.set("Launching...") + # Optionally minimize (close) the updater to the taskbar shortly after launch + if self._cfg.get("close_on_launch", False): + self.after(1000, self.iconify) # self._on_close and return + self.after(5000, self._refresh_ready_state) + except Exception as e: + self._log_line(f"Failed to launch {exe_lbl}: {e}\n", "err") + + # ── verify lifecycle ────────────────────────────────────────────────────────── + + def _start_verify(self): + out = self._game_path.get().strip() + if not out: + self._set_btn_update() + return + # Cancel any verify already in flight before swapping the queues, so + # a stale worker can't keep writing to a queue we no longer poll. + prev = getattr(self, "_verify_worker", None) + if prev is not None: + prev.cancel() + self._running = True + self._set_btn_busy("Checking…") + self._status_var.set("Checking for updates…") + self._log_q = queue.Queue() + self._prog_q = queue.Queue() + worker = VerifyWorker(out, self._log_q, self._prog_q) + self._verify_worker = worker + threading.Thread(target=worker.run, daemon=True).start() + + # ── update lifecycle ────────────────────────────────────────────────────────── + + def _start_update(self, check_integrity: bool = False, + overwrite_config: bool = False): + if self._running: + return + if self._pending_reconcile: + check_integrity = True + overwrite_config = (self._pending_reconcile == "full") + self._set_pending_reconcile(None) + out = self._game_path.get().strip() + if not out: + self._log_line("✗ Please set the game folder first.\n", "err") + return + + self._cfg = update_config(lambda c: c.__setitem__("out_dir", out)) + + self._maybe_show_aria2_firewall_notice() + + self._log_line(f"\nGame folder: {out}\n", "dim") + + self._running = True + self._set_btn_busy("Verifying…" if check_integrity else "Updating…") + self._status_var.set("Verifying game files…" if check_integrity + else "Updating game files…") + self._draw_progress(0.0) + self._prog_label_var.set("") + + self._log_q = queue.Queue() + self._prog_q = queue.Queue() + + self._worker = UpdateWorker(out, self._log_q, self._prog_q, + check_integrity=check_integrity, + overwrite_config=overwrite_config) + + t = threading.Thread(target=self._worker.run, daemon=True) + t.start() + + def _finish(self, success: bool): + self._running = False + if success: + self._client_ready = True + self._draw_progress(1.0) + self._refresh_ready_state() + # Game files are confirmed present now — install essential mods + # if this is a fresh folder (first launch or folder just changed). + self._maybe_install_essential_mods() + # When mods were already initialized, the addons chain from + # _do_inplace_update never runs — trigger it directly. + if load_config().get("mods"): + self._maybe_install_default_addons() + else: + self._client_ready = False + self._status_var.set("Update failed — check the log") + self._draw_progress(0.0) + self._set_btn_update() + + # ── queue polling ───────────────────────────────────────────────────────── + + def _poll(self): + try: + while True: + msg, tag = self._log_q.get_nowait() + if msg == "__DONE__": + self._finish(True) + elif msg == "__ERROR__": + self._finish(False) + elif msg == "__UP_TO_DATE__": + self._running = False + self._client_ready = True + self._draw_progress(1.0) + self._refresh_ready_state() + # Files were already fine (no download needed) — still need + # to check whether essential mods should be auto-installed, + # e.g. right after switching to a folder that already has + # the client but has never had mods installed via this + # updater. + self._maybe_install_essential_mods() + if load_config().get("mods"): + self._maybe_install_default_addons() + elif msg == "__UPDATE_NEEDED__": + self._running = False + self._client_ready = False + self._status_var.set("Update available!") + self._draw_progress(0.0) + self._set_btn_update() + elif msg.startswith("__VERSION__"): + ver = msg[len("__VERSION__"):] + self._client_ver_var.set(ver) + else: + self._render_log(msg, tag) + except queue.Empty: + pass + + # Drain the global app-log queue that helper functions write to. + try: + while True: + msg, tag = _LOG_Q.get_nowait() + self._render_log(msg, tag) + except queue.Empty: + pass + + latest = None + try: + while True: + latest = self._prog_q.get_nowait() + except queue.Empty: + pass + if latest is not None: + val, lbl = latest[0], latest[1] + status = latest[2] if len(latest) > 2 else None + self._draw_progress(val) + self._prog_label_var.set(lbl) + if status: + self._status_var.set(status) + self._set_btn_busy("Verifying…" if status.startswith("Verifying") + else "Updating…") + + self.after(80, self._poll) + + +# ────────────────────────────────────────────────────────────────────────────── +# Entry point +# ────────────────────────────────────────────────────────────────────────────── + +def _enable_dpi_awareness(): + """Declare DPI awareness before any Tk window is created, so Windows + reports the true DPI (letting us scale crisply) instead of bitmap-scaling a + 96-DPI render. Prefers Per-Monitor-V2, falling back through older contexts. + Must run before the Tk root exists; a no-op off Windows / on old Windows.""" + try: + import ctypes + except Exception: + return + user32 = getattr(ctypes, "windll", None) and ctypes.windll.user32 + if user32 is not None: + # DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4, PER_MONITOR = -3. + for ctx in (-4, -3): + try: + if user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(ctx)): + return + except Exception: + pass + try: + ctypes.windll.shcore.SetProcessDpiAwareness(2) # per-monitor (8.1+) + return + except Exception: + pass + try: + ctypes.windll.user32.SetProcessDPIAware() # system-aware (Vista+) + except Exception: + pass + + +if __name__ == "__main__": + _enable_dpi_awareness() + app = OctoUpdaterApp() + app.mainloop() \ No newline at end of file diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..c9a8e7abe2ca326430c149f286c6f446032f5d16 GIT binary patch literal 74071 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 z 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 FO#m%wVoU%4 literal 0 HcmV?d00001