Files
Modernization-Tool/setup_tool.py
T

2096 lines
90 KiB
Python

import os
import sys
import json
import shutil
import subprocess
import math
import re
import stat
import struct
import tkinter as tk
import webbrowser
from tkinter import ttk, filedialog, messagebox
import remote_packages
def get_base_path():
"""Gets the correct directory whether running as a script or a compiled .exe"""
if getattr(sys, 'frozen', False):
# Running as a compiled PyInstaller executable
return sys._MEIPASS
# Running as a normal Python script
return os.path.dirname(os.path.abspath(__file__))
class ToolTip:
"""Creates a hover-tooltip for a given tkinter widget."""
def __init__(self, widget, text):
self.widget = widget
self.text = text
self.tw = None
self.id = None
self.widget.bind("<Enter>", self.enter)
self.widget.bind("<Leave>", self.leave)
def enter(self, event=None):
self.schedule()
def schedule(self):
self.unschedule()
self.id = self.widget.after(400, self.showtip) # 400ms delay before showing
def unschedule(self):
if self.id:
self.widget.after_cancel(self.id)
self.id = None
def showtip(self, event=None):
x, y, cx, cy = self.widget.bbox("insert")
x += self.widget.winfo_rootx() + 25
y += self.widget.winfo_rooty() + 20
self.tw = tk.Toplevel(self.widget)
self.tw.wm_overrideredirect(True)
self.tw.wm_geometry(f"+{x}+{y}")
label = tk.Label(self.tw, text=self.text, justify='left',
background="#ffffe0", relief='solid', borderwidth=1,
font=("Segoe UI", "9", "normal"), wraplength=350)
label.pack(ipadx=6, ipady=4)
def leave(self, event=None):
self.unschedule()
if self.tw:
self.tw.destroy()
self.tw = None
class WowSetupTool:
def __init__(self, root):
self.root = root
self.root.title("WoW Vanilla 1.12 Modernization Tool")
# Lock window size exactly as requested
self.root.geometry("680x660")
self.root.resizable(False, False)
icon_path = os.path.join(get_base_path(), "PurpleWowLogo.ico")
if os.path.exists(icon_path):
self.root.iconbitmap(icon_path)
# Dictionary containing all tooltip explanations
self.descriptions = {
# Setup & General
"autologin": "Automates the authentication process. Bypasses the standard login screen by securely passing your credentials so you drop directly into the character selection screen.",
"render_directx9": "Uses VanillaFixes with the game's native DirectX 9 renderer. Choose this if you do not want DXVK/Vulkan.",
"render_dxvk": "Uses VanillaFixes with DXVK, translating DirectX 9 to Vulkan for smoother frame pacing on many modern systems.",
# Core DLLs
"nampower.dll": "Improves spell input responsiveness by adding modern spell queueing and latency compensation to the Vanilla client.",
"no1600x1200.dll": "Removes the hardcoded 1600x1200 resolution limit. Natively unlocks widescreen and ultrawidescreen resolutions in the game settings.",
"perf_boost.dll": "Provides dynamic render distance controls (culling). Stabilizes framerates in crowded environments by lowering the rendering priority of non-essential entities.",
"UnitXP_SP3.dll": "Engine-level optimizations replacing legacy assembly. Introduces improved network handling, better Tab-targeting, true line-of-sight checks via Lua, and modern nameplate support.",
"VanillaHelpers.dll": "Expands engine limits. Raises the client memory allocator from 2GB to 4GB to prevent Out-of-Memory crashes and introduces modern high-resolution texture support.",
"SuperWoWhook.dll": "Injects a massively expanded Lua API. Increases macro limit, enables castbars on nameplates, and provides hooks for modern UI addons.",
"transmogfix.dll": "Eliminates FPS drops caused by rapid equipment visual updates when transmogged items lose durability.",
"weirdperformance.dll": "Engine-level optimizations: SIMD math replacements, faster data decompression (modern zlib), MPQ file caching, timer calibration, and Lua runtime GC improvements.",
# WeirdUtils
"bigcursor.dll": "Upscales the hardware cursor for improved visibility on modern resolutions without losing sharpness (up to 4.0x scale via CVar).",
"customassets.dll": "Enables loading loose game asset files from the Data/ directory mirroring internal paths without needing to repack MPQ archives.",
"logsessions.dll": "Organizes combat and chat logs into clean, per-character, per-day files automatically upon login.",
"minimapicons.dll": "Adds TBC/WotLK-style minimap tracking icons for NPCs and objects, combined into a new native tracking dropdown.",
"pngscreenshots.dll": "Saves screenshots as compressed PNG files on a background thread to completely eliminate frame drops when taking pictures.",
"worldmarkers.dll": "Place up to 5 animated colored markers (Cataclysm style) in the world for raid positioning. Syncs automatically with other users.",
# Tweaks Tab
"fov": "Calculates horizontal Field of View mathematically scaled to maintain vertical aspect space based on your screen ratio.",
"farclip": "Increases the maximum terrain render distance. Vanilla default is 777. Tweaks default is 1500.",
"frill": "Changes the ground clutter (grass) render distance. Vanilla default is 70. Tweaks default is 300.",
"nameplate": "Increases the distance at which enemy nameplates become visible. Vanilla default is 20. Tweaks default is 41.",
"cam": "Increases the maximum camera zoom-out distance. Vanilla default is 50. Max safe limit is 100.",
"sound": "Increases the maximum number of simultaneous audio channels. Values above 64 may cause crashes.",
"loot": "Reverses the auto-loot behavior so you always auto-loot, and hold Shift for manual looting.",
"bg_sound": "Allows game sounds to continue playing while the game is minimized or in the background.",
"laa": "Patches the executable to be Large Address Aware, allowing the 32-bit client to utilize up to 4GB of RAM (Essential for HD Mods).",
"cam_fix": "Fixes a bug where right-clicking and dragging to rotate the camera occasionally snaps your view in a random direction.",
"dep_fix": "Disables Data Execution Prevention (DEP) and EmulateAtlThunks for WoW_Modernized.exe. Prevents Windows from force-closing the game due to memory hooks. (Prompts for Admin Privileges).",
"script_memory": "Sets WoW's AddOn Script Memory to 0 (unlimited). When disabled, the tool leaves your existing Config.wtf value unchanged.",
"crossfaction_res": "Allows the client to attempt resurrection of released cross-faction players.",
"custom_glues": "Enables custom GlueXML frames and XML on the login and character-selection screens.",
"bluemoon": "Restores the rare blue moon visual effect that appears around 1 AM on some nights. Installed as a patch inside WoW_Modernized.exe by vanilla-tweaks (no MPQ).",
"clear_wdb": "Automatically clears the current WDB cache and prevents stale WDB data from rebuilding by placing an empty WDB blocker file in the game folder. Unchecking this option removes the blocker so WoW can create its WDB folder normally.",
"darker_nights": "Installs Project Reforged Patch-N for darker, more atmospheric nights. Installed file: Data\\patch-N.mpq. Project Reforged recommends VanillaHelpers.",
"pretty_night_sky": "Replaces the Vanilla night sky with a more detailed starry sky. Installed file: Data\\patch-Z.mpq. The original hosted patch-9 name is deliberately changed so the official numeric patch-9.mpq is never overwritten.",
"epoch_water": "Replaces Vanilla water textures with the Epoch Water visual pack. Installed file: Data\\patch-W.mpq.",
"fog_pushback": "Pushes environmental fog farther back for a clearer long-distance view. Installed file: Data\\patch-Y.mpq. Works best together with an increased Farclip value.",
"pink_herbs": "Turns most herb-node textures bright pink/purple to make gathering nodes easier to spot. Installed file: Data\\patch-H.mpq.",
"no_error_sounds": "Installs the complete NoErrorSounds pack: muted spell fizzle sounds plus its included muted interface sounds. Installed as loose WAV files under Sound\\Spells\\Fizzle and Sound\\interface (no MPQ).",
"fish_ping": "Replaces the fishing bite sound with a much more noticeable ping. Installed file: Sound\\Spells\\Tradeskills\\FishBite.wav (no MPQ). Designed specifically for WoW Vanilla 1.12.1.",
"warlock_muted_demons": "Mutes the repeated voice lines from Warlock demons using Vanilla-compatible loose sound replacements. Installed as loose WAV files under Data\\Sound\\Creature (no MPQ)."
}
# Basic Setup Variables
self.wow_dir = tk.StringVar()
self.rendering_mode = tk.StringVar(value="directx9")
self.install_autologin = tk.BooleanVar(value=True)
self.screen_w = self.root.winfo_screenwidth()
self.screen_h = self.root.winfo_screenheight()
self.detected_ratio = self.screen_w / self.screen_h
self.ratio_options = {
f"Auto-detected ({self.screen_w}x{self.screen_h})": self.detected_ratio,
"4:3 (Standard)": 4.0/3.0,
"16:9 (Widescreen)": 16.0/9.0,
"16:10 (Widescreen)": 16.0/10.0,
"21:9 (Ultrawide)": 21.0/9.0,
"32:9 (Super Ultrawide)": 32.0/9.0
}
# Recommended core plugins, kept in the same order as the UI.
self.core_plugins = {
"nampower.dll": tk.BooleanVar(value=True),
"UnitXP_SP3.dll": tk.BooleanVar(value=True),
"SuperWoWhook.dll": tk.BooleanVar(value=True),
"transmogfix.dll": tk.BooleanVar(value=True),
"perf_boost.dll": tk.BooleanVar(value=True),
"weirdperformance.dll": tk.BooleanVar(value=True),
"VanillaHelpers.dll": tk.BooleanVar(value=True)
}
# Optional client fixes and WeirdUtils.
self.optional_plugins = {
"no1600x1200.dll": tk.BooleanVar(value=False),
"bigcursor.dll": tk.BooleanVar(value=False),
"customassets.dll": tk.BooleanVar(value=False),
"logsessions.dll": tk.BooleanVar(value=False),
"minimapicons.dll": tk.BooleanVar(value=False),
"pngscreenshots.dll": tk.BooleanVar(value=False),
"worldmarkers.dll": tk.BooleanVar(value=False)
}
self.addon_dependencies = {
"nampower.dll": "nampowersettings",
"perf_boost.dll": "perfboostsettings",
"UnitXP_SP3.dll": "UnitXP_SP3_Addon",
"SuperWoWhook.dll": "SuperAPI"
}
# Vanilla Tweaks Variables
self.vt_fov = tk.DoubleVar()
self.ratio_var = tk.StringVar(value=list(self.ratio_options.keys())[0])
self.vt_farclip = tk.IntVar(value=777)
self.vt_frill = tk.IntVar(value=300)
self.vt_nameplate = tk.IntVar(value=41)
self.vt_soundchan = tk.IntVar(value=64)
self.vt_maxcam = tk.IntVar(value=100)
# Tweak Toggles
self.vt_quickloot = tk.BooleanVar(value=True)
self.vt_bg_sound = tk.BooleanVar(value=True)
self.vt_laa = tk.BooleanVar(value=True)
self.vt_cam_fix = tk.BooleanVar(value=True)
self.vt_dep_fix = tk.BooleanVar(value=True)
self.vt_script_memory = tk.BooleanVar(value=True)
self.vt_crossfaction_res = tk.BooleanVar(value=False)
self.vt_custom_glues = tk.BooleanVar(value=True)
self.vt_bluemoon = tk.BooleanVar(value=False)
self.vt_clear_wdb = tk.BooleanVar(value=True)
# Optional visual and audio mods.
self.visual_mods = {
"darker_nights": tk.BooleanVar(value=False),
"pretty_night_sky": tk.BooleanVar(value=False),
"epoch_water": tk.BooleanVar(value=False),
"fog_pushback": tk.BooleanVar(value=False),
"pink_herbs": tk.BooleanVar(value=False),
}
self.audio_mods = {
"no_error_sounds": tk.BooleanVar(value=False),
"fish_ping": tk.BooleanVar(value=False),
"warlock_muted_demons": tk.BooleanVar(value=False),
}
# Safety Limit Toggle
self.safety_override = tk.BooleanVar(value=False)
self.slider_widgets =[]
self._loaded_settings_dir = None
self._loading_settings = False
self._install_in_progress = False
self.on_ratio_change()
self.build_ui()
# Snapshot the clean application defaults after every variable and
# subclass-provided option exists. Each WoW folder is loaded from this
# baseline so settings from one installation cannot leak into another.
self._default_settings = self._collect_settings()
# Settings are stored per WoW installation. Loading on path selection
# means a newer copy of the tool can immediately restore the choices
# made by an older copy.
self.wow_dir.trace_add("write", self._on_wow_dir_changed)
def on_ratio_change(self, event=None):
selection = self.ratio_var.get()
ratio = self.ratio_options.get(selection, 4.0/3.0)
default_ar, default_fov = 4.0 / 3.0, 1.570796
fov = 2 * math.atan((ratio / default_ar) * math.tan(default_fov / 2))
self.vt_fov.set(round(fov, 4))
def toggle_safety_limits(self):
override = self.safety_override.get()
for scale, safe_max, extreme_max, var in self.slider_widgets:
if override:
scale.configure(to=extreme_max)
else:
scale.configure(to=safe_max)
try:
if var.get() > safe_max:
var.set(safe_max)
except tk.TclError:
pass
def create_slider_row(self, parent, row, label_text, var, min_val, safe_max, extreme_max, desc_key):
lbl = ttk.Label(parent, text=label_text)
lbl.grid(row=row, column=0, sticky='w', pady=5)
ToolTip(lbl, self.descriptions[desc_key])
current_max = extreme_max if self.safety_override.get() else safe_max
scale = ttk.Scale(parent, from_=min_val, to=current_max, orient='horizontal', variable=var,
command=lambda s, v=var: v.set(int(float(s))))
scale.grid(row=row, column=1, sticky='ew', padx=15, pady=5)
entry = ttk.Entry(parent, textvariable=var, width=8)
entry.grid(row=row, column=2, sticky='e', pady=5)
self.slider_widgets.append((scale, safe_max, extreme_max, var))
return lbl, scale, entry
def create_mod_option_row(self, parent, text, variable, attribution, tooltip):
row = ttk.Frame(parent)
row.pack(fill="x", padx=10, pady=4)
cb = ttk.Checkbutton(row, text=text, variable=variable)
cb.pack(side="left")
ToolTip(cb, tooltip)
if attribution:
ttk.Label(
row,
text=attribution,
font=("Segoe UI", 7, "italic"),
).pack(side="right", padx=(6, 0))
return cb
def _settings_path(self, target_dir):
return os.path.join(target_dir, ".modernization_tool", "settings.json")
def _looks_like_managed_install(self, target_dir):
return any(
os.path.exists(os.path.join(target_dir, rel))
for rel in (
"WoW_Modernized.exe",
"Play Modernized WoW.lnk",
".modernization_tool",
)
)
def _collect_settings(self):
live_plugins = {}
for name in (
"classicapi_enabled",
"auction_throttle_enabled",
"vmmfix_enabled",
"interact_enabled",
):
var = getattr(self, name, None)
if var is not None:
live_plugins[name] = bool(var.get())
return {
"version": 1,
"rendering_mode": self.rendering_mode.get(),
"install_autologin": bool(self.install_autologin.get()),
"core_plugins": {
name: bool(var.get()) for name, var in self.core_plugins.items()
},
"optional_plugins": {
name: bool(var.get()) for name, var in self.optional_plugins.items()
},
"live_plugins": live_plugins,
"vanilla_tweaks": {
"ratio": self.ratio_var.get(),
"fov": float(self.vt_fov.get()),
"farclip": int(self.vt_farclip.get()),
"frill": int(self.vt_frill.get()),
"nameplate": int(self.vt_nameplate.get()),
"sound_channels": int(self.vt_soundchan.get()),
"max_camera": int(self.vt_maxcam.get()),
"quickloot": bool(self.vt_quickloot.get()),
"background_sound": bool(self.vt_bg_sound.get()),
"laa": bool(self.vt_laa.get()),
"camera_fix": bool(self.vt_cam_fix.get()),
"dep_fix": bool(self.vt_dep_fix.get()),
"script_memory": bool(self.vt_script_memory.get()),
"crossfaction_res": bool(self.vt_crossfaction_res.get()),
"custom_glues": bool(self.vt_custom_glues.get()),
"bluemoon": bool(self.vt_bluemoon.get()),
"clear_wdb": bool(self.vt_clear_wdb.get()),
"safety_override": bool(self.safety_override.get()),
},
"visual_mods": {
name: bool(var.get()) for name, var in self.visual_mods.items()
},
"audio_mods": {
name: bool(var.get()) for name, var in self.audio_mods.items()
},
}
def save_settings(self, target_dir):
settings_path = self._settings_path(target_dir)
os.makedirs(os.path.dirname(settings_path), exist_ok=True)
temp_path = settings_path + ".new"
with open(temp_path, "w", encoding="utf-8") as handle:
json.dump(self._collect_settings(), handle, indent=2, sort_keys=True)
os.replace(temp_path, settings_path)
self._loaded_settings_dir = os.path.normcase(os.path.abspath(target_dir))
def _set_bool_mapping(self, saved, variables):
if not isinstance(saved, dict):
return
for name, value in saved.items():
var = variables.get(name)
if var is not None and isinstance(value, bool):
var.set(value)
def _load_legacy_install_state(self, target_dir):
"""Best-effort migration for installs made before settings.json existed."""
if not self._looks_like_managed_install(target_dir):
return
if (
os.path.isfile(os.path.join(target_dir, "d3d9.dll"))
and os.path.isfile(os.path.join(target_dir, "dxvk.conf"))
):
self.rendering_mode.set("dxvk")
else:
self.rendering_mode.set("directx9")
# Once this tool has clearly been used on the folder, on-disk presence
# is the safest migration signal for plugin checkboxes.
for filename, var in self.core_plugins.items():
var.set(os.path.isfile(os.path.join(target_dir, filename)))
for filename, var in self.optional_plugins.items():
var.set(os.path.isfile(os.path.join(target_dir, filename)))
live_files = {
"classicapi_enabled": "ClassicAPI.dll",
"auction_throttle_enabled": "AuctionQueryThrottle.dll",
"vmmfix_enabled": "VanillaMultiMonitorFix.dll",
"interact_enabled": "Interact.dll",
}
for attr, filename in live_files.items():
var = getattr(self, attr, None)
if var is not None:
var.set(os.path.isfile(os.path.join(target_dir, filename)))
autologin_lua = os.path.join(
target_dir, "Data", "Interface", "GlueXML", "AutoLogin.lua"
)
self.install_autologin.set(os.path.isfile(autologin_lua))
managed_ids = {
"darker_nights": "visual_darker_nights",
"pretty_night_sky": "visual_pretty_night_sky",
"epoch_water": "visual_epoch_water",
"fog_pushback": "visual_fog_pushback",
"pink_herbs": "visual_pink_herbs",
}
for key, managed_id in managed_ids.items():
self.visual_mods[key].set(
remote_packages.managed_mod_is_installed(target_dir, managed_id)
)
managed_audio = {
"no_error_sounds": "audio_no_error_sounds",
"fish_ping": "audio_fish_ping",
"warlock_muted_demons": "audio_warlock_muted_demons",
}
for key, managed_id in managed_audio.items():
self.audio_mods[key].set(
remote_packages.managed_mod_is_installed(target_dir, managed_id)
)
wdb_path = os.path.join(target_dir, "WDB")
if os.path.isfile(wdb_path):
self.vt_clear_wdb.set(True)
elif os.path.isdir(wdb_path):
self.vt_clear_wdb.set(False)
def _apply_settings_dict(self, saved):
if not isinstance(saved, dict):
raise ValueError("settings root must be a JSON object")
rendering = saved.get("rendering_mode")
if rendering in ("directx9", "dxvk"):
self.rendering_mode.set(rendering)
if isinstance(saved.get("install_autologin"), bool):
self.install_autologin.set(saved["install_autologin"])
self._set_bool_mapping(saved.get("core_plugins"), self.core_plugins)
self._set_bool_mapping(saved.get("optional_plugins"), self.optional_plugins)
self._set_bool_mapping(saved.get("visual_mods"), self.visual_mods)
self._set_bool_mapping(saved.get("audio_mods"), self.audio_mods)
live = saved.get("live_plugins")
if isinstance(live, dict):
for name, value in live.items():
var = getattr(self, name, None)
if var is not None and isinstance(value, bool):
var.set(value)
tweaks = saved.get("vanilla_tweaks")
if isinstance(tweaks, dict):
ratio = tweaks.get("ratio")
if isinstance(ratio, str) and ratio in self.ratio_options:
self.ratio_var.set(ratio)
numeric = (
("fov", self.vt_fov, float),
("farclip", self.vt_farclip, int),
("frill", self.vt_frill, int),
("nameplate", self.vt_nameplate, int),
("sound_channels", self.vt_soundchan, int),
("max_camera", self.vt_maxcam, int),
)
for key, var, converter in numeric:
value = tweaks.get(key)
if isinstance(value, (int, float)) and not isinstance(value, bool):
var.set(converter(value))
boolean = (
("quickloot", self.vt_quickloot),
("background_sound", self.vt_bg_sound),
("laa", self.vt_laa),
("camera_fix", self.vt_cam_fix),
("dep_fix", self.vt_dep_fix),
("script_memory", self.vt_script_memory),
("crossfaction_res", self.vt_crossfaction_res),
("custom_glues", self.vt_custom_glues),
("bluemoon", self.vt_bluemoon),
("clear_wdb", self.vt_clear_wdb),
("safety_override", self.safety_override),
)
for key, var in boolean:
value = tweaks.get(key)
if isinstance(value, bool):
var.set(value)
def _reset_settings_to_defaults(self):
defaults = getattr(self, "_default_settings", None)
if isinstance(defaults, dict):
self._apply_settings_dict(defaults)
def _normalize_plugin_conflicts(self):
"""Resolve impossible states created by old/corrupt settings files."""
no1600 = self.optional_plugins.get("no1600x1200.dll")
vmmfix = getattr(self, "vmmfix_enabled", None)
if no1600 is not None and vmmfix is not None and no1600.get() and vmmfix.get():
# VMMFix is the more complete monitor/resolution fix. Prefer it when
# legacy state incorrectly claims both mutually exclusive options.
no1600.set(False)
return True
return False
def validate_plugin_conflicts(self):
"""Last-line safety check independent of checkbox callbacks/settings."""
no1600 = self.optional_plugins.get("no1600x1200.dll")
vmmfix = getattr(self, "vmmfix_enabled", None)
if no1600 is not None and vmmfix is not None and no1600.get() and vmmfix.get():
messagebox.showerror(
"Plugin conflict",
"no1600x1200 and VanillaMultiMonitorFix cannot be enabled together.\n\n"
"Disable one of them before applying the setup.",
)
return False
return True
def load_settings(self, target_dir):
settings_path = self._settings_path(target_dir)
self._loading_settings = True
recovered_from_damage = False
try:
# Always start from clean defaults. This prevents values from a
# previously selected WoW directory leaking into this installation.
self._reset_settings_to_defaults()
if not os.path.isfile(settings_path):
self._load_legacy_install_state(target_dir)
self._normalize_plugin_conflicts()
self.toggle_safety_limits()
self.update_superwow_managed_controls()
return False
try:
with open(settings_path, "r", encoding="utf-8") as handle:
saved = json.load(handle)
if not isinstance(saved, dict):
raise ValueError("settings root must be a JSON object")
self._apply_settings_dict(saved)
except (OSError, json.JSONDecodeError, ValueError, TypeError, tk.TclError):
# Keep the damaged file untouched for manual recovery/debugging.
# Recover only what can safely be inferred from the WoW folder.
self._reset_settings_to_defaults()
self._load_legacy_install_state(target_dir)
recovered_from_damage = True
self._normalize_plugin_conflicts()
self.toggle_safety_limits()
self.update_superwow_managed_controls()
if recovered_from_damage:
messagebox.showwarning(
"Settings recovery",
"The saved Modernization Tool settings for this WoW folder could not be read.\n\n"
"The file was left untouched. Installed components were detected where possible, "
"and defaults were used for options that cannot be inferred. Review the choices "
"before clicking Apply.",
)
return False
return True
finally:
self._loading_settings = False
def _on_wow_dir_changed(self, *_args):
if self._loading_settings:
return
target_dir = self.wow_dir.get().strip()
if not target_dir:
return
normalized = os.path.normcase(os.path.abspath(target_dir))
if normalized == self._loaded_settings_dir:
return
# Do not pop validation errors while the user is typing a path.
if not (
os.path.isfile(os.path.join(target_dir, "WoW.exe"))
and os.path.isdir(os.path.join(target_dir, "Data"))
):
return
self.load_settings(target_dir)
self._loaded_settings_dir = normalized
def build_ui(self):
help_banner = tk.Label(
self.root,
text="💡 HOVER FOR HELP — Move your mouse over any setting or plugin to see what it does.",
background="#EAF4FF",
foreground="#005A9E",
font=("Segoe UI", 10, "bold"),
relief="solid",
borderwidth=1,
padx=10,
pady=6,
)
help_banner.pack(fill="x", padx=10, pady=(8, 2))
notebook = ttk.Notebook(self.root)
notebook.pack(fill='both', expand=True, padx=10, pady=(5, 10))
tab_main = ttk.Frame(notebook)
tab_plugins = ttk.Frame(notebook)
tab_tweaks = ttk.Frame(notebook)
tab_visual_audio = ttk.Frame(notebook)
tab_credits = ttk.Frame(notebook)
notebook.add(tab_main, text="Setup & Rendering")
notebook.add(tab_plugins, text="Plugins")
notebook.add(tab_tweaks, text="Vanilla Tweaks")
notebook.add(tab_visual_audio, text="Visual & Audio")
notebook.add(tab_credits, text="Credits & Sources")
self.build_main_tab(tab_main)
self.build_plugins_tab(tab_plugins)
self.build_tweaks_tab(tab_tweaks)
self.build_visual_audio_tab(tab_visual_audio)
self.build_credits_tab(tab_credits)
self.apply_button = ttk.Button(
self.root,
text="Apply Setup & Tweaks",
command=self.run_installation,
style="Accent.TButton",
)
self.apply_button.pack(pady=10, fill='x', padx=20)
def build_main_tab(self, parent):
ttk.Label(parent, text="Vanilla 1.12 Installation Directory:").pack(anchor='w', pady=(10, 0), padx=10)
dir_frame = ttk.Frame(parent)
dir_frame.pack(fill='x', padx=10, pady=5)
ttk.Entry(dir_frame, textvariable=self.wow_dir).pack(side='left', fill='x', expand=True)
ttk.Button(dir_frame, text="Browse...", command=lambda: self.wow_dir.set(filedialog.askdirectory())).pack(side='left', padx=(5,0))
ttk.Label(parent, text="Rendering Mode:").pack(anchor='w', pady=(20, 0), padx=10)
rb_directx9 = ttk.Radiobutton(
parent,
text="VanillaFixes (DirectX 9)",
variable=self.rendering_mode,
value="directx9"
)
rb_directx9.pack(anchor='w', padx=20, pady=2)
ToolTip(rb_directx9, self.descriptions["render_directx9"])
rb_dxvk = ttk.Radiobutton(
parent,
text="VanillaFixes + DXVK (Vulkan)",
variable=self.rendering_mode,
value="dxvk"
)
rb_dxvk.pack(anchor='w', padx=20, pady=2)
ToolTip(rb_dxvk, self.descriptions["render_dxvk"])
ttk.Label(parent, text="Optional Mods:").pack(anchor='w', pady=(20, 0), padx=10)
cb_login = ttk.Checkbutton(parent, text="Install Auto Login Mod (Data/Interface/GlueXML)", variable=self.install_autologin)
cb_login.pack(anchor='w', padx=20, pady=2)
ToolTip(cb_login, self.descriptions["autologin"])
def build_plugins_tab(self, parent):
container = ttk.Frame(parent)
container.pack(fill='both', expand=True, padx=10, pady=10)
left_frame = ttk.LabelFrame(container, text="Recommended Core")
left_frame.pack(side='left', fill='both', expand=True, padx=(0, 5))
ttk.Label(left_frame, text="These are highly recommended for performance\nand client stability.", font=("", 8, "italic")).pack(anchor='w', padx=10, pady=10)
for dll, var in self.core_plugins.items():
cb = ttk.Checkbutton(left_frame, text=dll, variable=var)
cb.pack(anchor='w', padx=10, pady=4)
ToolTip(cb, self.descriptions.get(dll, ""))
right_frame = ttk.LabelFrame(container, text="Optional")
right_frame.pack(side='right', fill='both', expand=True, padx=(5, 0))
ttk.Label(right_frame, text="Optional client-side fixes and quality-of-life enhancements.", font=("", 8, "italic")).pack(anchor='w', padx=10, pady=10)
for dll, var in self.optional_plugins.items():
cb = ttk.Checkbutton(right_frame, text=dll, variable=var)
cb.pack(anchor='w', padx=10, pady=4)
ToolTip(cb, self.descriptions.get(dll, ""))
def _superwow_enabled(self):
var = self.core_plugins.get("SuperWoWhook.dll")
return bool(var is not None and var.get())
def update_superwow_managed_controls(self):
"""Disable vanilla-tweaks controls that SuperWoW already handles."""
active = self._superwow_enabled()
if hasattr(self, "superwow_notice"):
if active:
self.superwow_notice.configure(
text="✓ SuperWoW enabled — FoV, Sound Channels, Auto-loot and Background sounds are handled by SuperWoW. Their vanilla-tweaks patches are skipped.",
background="#EAF4FF",
foreground="#005A9E",
)
else:
self.superwow_notice.configure(
text="SuperWoW disabled — FoV, Sound Channels, Auto-loot and Background sounds are controlled by vanilla-tweaks.",
background="#F4F4F4",
foreground="#444444",
)
if hasattr(self, "fov_ratio_combo"):
self.fov_ratio_combo.configure(state="disabled" if active else "readonly")
if hasattr(self, "fov_entry"):
self.fov_entry.configure(state="disabled" if active else "normal")
if hasattr(self, "sound_scale"):
self.sound_scale.configure(state="disabled" if active else "normal")
if hasattr(self, "sound_entry"):
self.sound_entry.configure(state="disabled" if active else "normal")
if hasattr(self, "cb_loot"):
self.cb_loot.configure(state="disabled" if active else "normal")
if hasattr(self, "cb_bg"):
self.cb_bg.configure(state="disabled" if active else "normal")
def build_tweaks_tab(self, parent):
self.superwow_notice = tk.Label(
parent,
text="",
background="#EAF4FF",
foreground="#005A9E",
font=("Segoe UI", 9, "bold"),
relief="solid",
borderwidth=1,
padx=8,
pady=4,
anchor="w",
justify="left",
wraplength=620,
)
self.superwow_notice.pack(fill="x", padx=10, pady=(5, 2))
fov_frame = ttk.LabelFrame(parent, text="Field of View (FoV) Calculator")
fov_frame.pack(fill='x', padx=10, pady=5)
ttk.Label(fov_frame, text="Screen Aspect Ratio:").grid(row=0, column=0, padx=5, pady=5, sticky='w')
self.fov_ratio_combo = ttk.Combobox(
fov_frame,
textvariable=self.ratio_var,
values=list(self.ratio_options.keys()),
state="readonly",
width=28
)
self.fov_ratio_combo.grid(row=0, column=1, padx=5, pady=5, sticky='w')
self.fov_ratio_combo.bind("<<ComboboxSelected>>", self.on_ratio_change)
fov_lbl = ttk.Label(fov_frame, text="Calculated FoV (Radians) [Safe Max: 2.268]:")
fov_lbl.grid(row=1, column=0, padx=5, pady=5, sticky='w')
ToolTip(fov_lbl, self.descriptions["fov"])
self.fov_entry = ttk.Entry(fov_frame, textvariable=self.vt_fov, width=15)
self.fov_entry.grid(row=1, column=1, padx=5, pady=5, sticky='w')
style = ttk.Style()
style.configure("Warning.TCheckbutton", foreground="red")
ttk.Checkbutton(parent, text="Disable Safety Limits (Warning: Exceeding max limits may cause game instability)",
variable=self.safety_override, style="Warning.TCheckbutton",
command=self.toggle_safety_limits).pack(anchor='w', padx=15, pady=(0, 5))
frame_nums = ttk.Frame(parent)
frame_nums.pack(fill='x', padx=15, pady=0)
frame_nums.columnconfigure(1, weight=1)
self.create_slider_row(frame_nums, 0, "Render distance (Farclip) [Safe Max: 1500]:", self.vt_farclip, 777, 1500, 10000, "farclip")
self.create_slider_row(frame_nums, 1, "Ground clutter (Frilldistance) [Safe Max: 300]:", self.vt_frill, 70, 300, 1000, "frill")
self.create_slider_row(frame_nums, 2, "Nameplate range [Safe Max: 41]:", self.vt_nameplate, 20, 41, 150, "nameplate")
self.create_slider_row(frame_nums, 3, "Camera distance [Safe Max: 100]:", self.vt_maxcam, 50, 100, 250, "cam")
_, self.sound_scale, self.sound_entry = self.create_slider_row(
frame_nums, 4, "Sound Channels [Safe Max: 64]:",
self.vt_soundchan, 12, 64, 128, "sound"
)
ttk.Label(parent, text="Patch Toggles:").pack(anchor='w', pady=(5,0), padx=10)
toggles_frame = ttk.Frame(parent)
toggles_frame.pack(fill='x', padx=10)
self.cb_loot = ttk.Checkbutton(toggles_frame, text="Always auto-loot", variable=self.vt_quickloot)
self.cb_loot.grid(row=0, column=0, sticky='w', padx=10, pady=2)
ToolTip(self.cb_loot, self.descriptions["loot"])
self.cb_bg = ttk.Checkbutton(toggles_frame, text="Background sounds", variable=self.vt_bg_sound)
self.cb_bg.grid(row=0, column=1, sticky='w', padx=10, pady=2)
ToolTip(self.cb_bg, self.descriptions["bg_sound"])
cb_laa = ttk.Checkbutton(toggles_frame, text="Large Address Aware (LAA)", variable=self.vt_laa)
cb_laa.grid(row=1, column=0, sticky='w', padx=10, pady=2)
ToolTip(cb_laa, self.descriptions["laa"])
cb_cam = ttk.Checkbutton(toggles_frame, text="Fix Camera Skip Glitch", variable=self.vt_cam_fix)
cb_cam.grid(row=1, column=1, sticky='w', padx=10, pady=2)
ToolTip(cb_cam, self.descriptions["cam_fix"])
cb_dep = ttk.Checkbutton(toggles_frame, text="Disable DEP (Requires Admin)", variable=self.vt_dep_fix)
cb_dep.grid(row=2, column=0, sticky='w', padx=10, pady=2)
ToolTip(cb_dep, self.descriptions["dep_fix"])
cb_script_memory = ttk.Checkbutton(
toggles_frame,
text="Unlimited AddOn Script Memory",
variable=self.vt_script_memory
)
cb_script_memory.grid(row=2, column=1, sticky='w', padx=10, pady=2)
ToolTip(cb_script_memory, self.descriptions["script_memory"])
cb_crossfaction = ttk.Checkbutton(
toggles_frame,
text="Cross-faction Res Fix",
variable=self.vt_crossfaction_res
)
cb_crossfaction.grid(row=3, column=0, sticky='w', padx=10, pady=2)
ToolTip(cb_crossfaction, self.descriptions["crossfaction_res"])
cb_custom_glues = ttk.Checkbutton(
toggles_frame,
text="Custom Glues Patch",
variable=self.vt_custom_glues
)
cb_custom_glues.grid(row=3, column=1, sticky='w', padx=10, pady=2)
ToolTip(cb_custom_glues, self.descriptions["custom_glues"])
cb_clear_wdb = ttk.Checkbutton(
toggles_frame,
text="Automatically Clear WDB",
variable=self.vt_clear_wdb
)
cb_clear_wdb.grid(row=4, column=0, sticky='w', padx=10, pady=2)
ToolTip(cb_clear_wdb, self.descriptions["clear_wdb"])
self.update_superwow_managed_controls()
def build_visual_audio_tab(self, parent):
ttk.Label(
parent,
text="Optional appearance and sound replacements. Everything here is disabled by default.",
font=("Segoe UI", 9, "italic"),
).pack(anchor="w", padx=12, pady=(10, 6))
container = ttk.Frame(parent)
container.pack(fill="both", expand=True, padx=10, pady=(0, 10))
visual_frame = ttk.LabelFrame(container, text="Visual Mods")
visual_frame.pack(side="left", fill="both", expand=True, padx=(0, 5))
audio_frame = ttk.LabelFrame(container, text="Audio Mods")
audio_frame.pack(side="right", fill="both", expand=True, padx=(5, 0))
self.create_mod_option_row(
visual_frame,
"Bluemoon Patch",
self.vt_bluemoon,
"via vanilla-tweaks",
self.descriptions["bluemoon"],
)
self.create_mod_option_row(
visual_frame,
"Darker Nights",
self.visual_mods["darker_nights"],
"by Project Reforged",
self.descriptions["darker_nights"],
)
self.create_mod_option_row(
visual_frame,
"Pretty Night Sky",
self.visual_mods["pretty_night_sky"],
"source: RetroCro",
self.descriptions["pretty_night_sky"],
)
self.create_mod_option_row(
visual_frame,
"Epoch Water",
self.visual_mods["epoch_water"],
"source: RetroCro",
self.descriptions["epoch_water"],
)
self.create_mod_option_row(
visual_frame,
"Fog Pushback",
self.visual_mods["fog_pushback"],
"source: RetroCro",
self.descriptions["fog_pushback"],
)
self.create_mod_option_row(
visual_frame,
"Pink Herbs",
self.visual_mods["pink_herbs"],
"by seacrabsam",
self.descriptions["pink_herbs"],
)
self.create_mod_option_row(
audio_frame,
"NoErrorSounds",
self.audio_mods["no_error_sounds"],
"by Macumbafeh",
self.descriptions["no_error_sounds"],
)
self.create_mod_option_row(
audio_frame,
"FishPing",
self.audio_mods["fish_ping"],
"by notsureawake",
self.descriptions["fish_ping"],
)
self.create_mod_option_row(
audio_frame,
"Warlock Muted Demons",
self.audio_mods["warlock_muted_demons"],
"by spzilyk",
self.descriptions["warlock_muted_demons"],
)
def build_credits_tab(self, parent):
# Create a frame with a scrollbar
frame = ttk.Frame(parent)
frame.pack(fill='both', expand=True, padx=10, pady=10)
scrollbar = ttk.Scrollbar(frame)
scrollbar.pack(side='right', fill='y')
bg_color = self.root.cget('bg')
text_area = tk.Text(
frame,
wrap='word',
yscrollcommand=scrollbar.set,
bg=bg_color,
relief='flat',
font=("Segoe UI", 9)
)
text_area.pack(side='left', fill='both', expand=True)
scrollbar.config(command=text_area.yview)
# Configure Text Tags for formatting
text_area.tag_configure(
"header",
font=("Segoe UI", 10, "bold"),
spacing1=10,
spacing3=5,
foreground="#333333"
)
text_area.tag_configure("bold", font=("Segoe UI", 9, "bold"))
# Helper function to create clickable links
def insert_link(text, url):
tag_name = f"link_{url}"
text_area.tag_configure(tag_name, foreground="#005A9E", underline=True)
text_area.tag_bind(tag_name, "<Button-1>", lambda e, u=url: webbrowser.open_new(u))
text_area.tag_bind(tag_name, "<Enter>", lambda e: text_area.config(cursor="hand2"))
text_area.tag_bind(tag_name, "<Leave>", lambda e: text_area.config(cursor="arrow"))
text_area.insert("end", text, tag_name)
text_area.insert(
"end",
"This modernization tool brings together work from several community projects and developers. "
"Use the links below to view the original sources, documentation and releases.\n",
""
)
# Rendering & executable patching
text_area.insert("end", "\nRendering & Client Patching\n", "header")
text_area.insert("end", "• VanillaFixes: ", "bold")
insert_link("Source Repository", "https://github.com/hannesmann/vanillafixes")
text_area.insert("end", "\n• DXVK: ", "bold")
insert_link("Source Repository", "https://github.com/doitsujin/dxvk")
text_area.insert("end", "\n• Vanilla Tweaks: ", "bold")
insert_link("Source Repository", "https://github.com/tubtubs/vanilla-tweaks")
# Core engine & API plugins
text_area.insert("end", "\n\nCore Engine & API Plugins\n", "header")
text_area.insert("end", "• VanillaHelpers: ", "bold")
insert_link("Source Repository", "https://github.com/isfir/VanillaHelpers")
text_area.insert("end", "\n• PerfBoost: ", "bold")
text_area.insert("end", "by avitasia | ")
insert_link("Addon Source", "https://gitea.com/avitasia/PerfBoostSettings")
text_area.insert("end", "\n• UnitXP_SP3: ", "bold")
insert_link("Source Repository", "https://github.com/brues-code/UnitXP_SP3")
text_area.insert("end", " (DLL + UnitXP_SP3_Addon are distributed together in releases)")
text_area.insert("end", "\n• SuperWoW: ", "bold")
insert_link("Mod Source", "https://github.com/balakethelock/SuperWoW")
text_area.insert("end", " | ")
insert_link("SuperAPI Addon", "https://github.com/balakethelock/SuperAPI")
text_area.insert("end", "\n• ClassicAPI: ", "bold")
insert_link("Source Repository", "https://github.com/brues-code/ClassicAPI")
text_area.insert("end", "\n• AuctionQueryThrottle: ", "bold")
insert_link("Source Repository", "https://github.com/brues-code/AuctionQueryThrottle")
text_area.insert("end", "\n• Nampower: ", "bold")
insert_link("Mod Source", "https://github.com/brues-code/nampower")
text_area.insert("end", " | ")
insert_link("Addon Source", "https://github.com/brues-code/NampowerSettings")
text_area.insert("end", "\n• No1600x1200: ", "bold")
insert_link("Source / Backup", "https://github.com/RetroCro/TurtleWoW-Mods#no1600x1200")
text_area.insert("end", "\n• VanillaMultiMonitorFix: ", "bold")
insert_link("Source Repository", "https://github.com/Mates1500/VanillaMultiMonitorFix")
text_area.insert("end", "\n• Interact: ", "bold")
insert_link("Source Repository", "https://github.com/lookino/Interact")
# Visual & audio mods
text_area.insert("end", "\n\nVisual & Audio Mods\n", "header")
text_area.insert("end", "• Bluemoon Patch: ", "bold")
insert_link("via vanilla-tweaks", "https://github.com/tubtubs/vanilla-tweaks")
text_area.insert("end", "\n• Darker Nights: ", "bold")
insert_link("Project Reforged", "https://projectreforged.github.io/vanilla/downloads/turtle/")
text_area.insert("end", "\n• Pretty Night Sky / Epoch Water / Fog Pushback: ", "bold")
insert_link("RetroCro TurtleWoW Mods", "https://github.com/RetroCro/TurtleWoW-Mods")
text_area.insert("end", "\n• Pink Herbs: ", "bold")
insert_link("seacrabsam/patch-herb", "https://github.com/seacrabsam/patch-herb")
text_area.insert("end", "\n• NoErrorSounds: ", "bold")
insert_link("Macumbafeh/NoErrorSounds", "https://github.com/Macumbafeh/NoErrorSounds")
text_area.insert("end", "\n• FishPing: ", "bold")
insert_link("notsureawake/FishPing", "https://github.com/notsureawake/FishPing")
text_area.insert("end", "\n• Warlock Muted Demons: ", "bold")
insert_link("spzilyk/Warlock-Muted-Demons", "https://github.com/spzilyk/Warlock-Muted-Demons")
text_area.insert("end", "\n• Automatically Clear WDB: ", "bold")
insert_link("RetroCro guide", "https://github.com/RetroCro/TurtleWoW-Mods#automatically-clear-wdb-folder-every-time-you-launch-turtle-wow")
# Other bundled enhancements
text_area.insert("end", "\n\nOther Bundled Enhancements\n", "header")
text_area.insert("end", "• Vanilla-Autologin: ", "bold")
insert_link("Source Repository", "https://github.com/MarcelineVQ/turtle-autologin")
# WeirdUtils
text_area.insert("end", "\n\nWeirdUtils Suite\n", "header")
text_area.insert(
"end",
"The tool bundles or exposes these WeirdUtils modules: "
"weirdperformance.dll, transmogfix.dll, bigcursor.dll, customassets.dll, "
"logsessions.dll, minimapicons.dll, pngscreenshots.dll and worldmarkers.dll.\n"
)
text_area.insert(
"end",
"Some WeirdUtils components are distributed as pre-compiled binaries; see the project page "
"for documentation and releases.\n\n"
)
text_area.insert("end", "• WeirdUtils Documentation & Releases: ", "bold")
insert_link("Project Repository", "https://codeberg.org/MarcelineVQ/WeirdUtils")
# This tool
text_area.insert("end", "\n\nModernization Tool\n", "header")
text_area.insert("end", "• WoW Modernization Tool: ", "bold")
insert_link("Project Repository", "https://github.com/Dusk-92/Modernization-Tool")
text_area.insert("end", "\n")
# Lock text area to prevent editing
text_area.config(state='disabled')
def _inspect_wow_executable(self, wow_exe):
"""Validate the PE architecture without relying on a version-specific hash."""
try:
file_size = os.path.getsize(wow_exe)
if file_size < 1024 * 1024:
return False, "WoW.exe is unexpectedly small."
with open(wow_exe, "rb") as handle:
dos_header = handle.read(64)
if len(dos_header) < 64 or dos_header[:2] != b"MZ":
return False, "WoW.exe is not a valid Windows executable."
pe_offset = struct.unpack_from("<I", dos_header, 0x3C)[0]
if pe_offset < 64 or pe_offset > file_size - 26:
return False, "WoW.exe has an invalid PE header."
handle.seek(pe_offset)
if handle.read(4) != b"PE\0\0":
return False, "WoW.exe has an invalid PE signature."
coff = handle.read(20)
if len(coff) != 20:
return False, "WoW.exe has a truncated PE header."
machine = struct.unpack_from("<H", coff, 0)[0]
optional_size = struct.unpack_from("<H", coff, 16)[0]
if optional_size < 2:
return False, "WoW.exe has no valid optional PE header."
optional_magic = struct.unpack("<H", handle.read(2))[0]
# Vanilla 1.12/Turtle uses the 32-bit x86 PE format.
if machine != 0x014C or optional_magic != 0x010B:
return (
False,
"WoW.exe is not a 32-bit x86 client. Modernization Tool is "
"intended for Vanilla 1.12/Turtle-compatible clients only.",
)
return True, ""
except (OSError, struct.error) as exc:
return False, f"WoW.exe could not be inspected: {exc}"
def _find_data_file_case_insensitive(self, data_dir, wanted):
try:
entries = {name.casefold(): name for name in os.listdir(data_dir)}
except OSError:
return None
actual = entries.get(wanted.casefold())
return os.path.join(data_dir, actual) if actual else None
def validate_installation_dir(self, target_dir):
"""Validate a Vanilla/Turtle-compatible 32-bit client before patching."""
if not target_dir:
messagebox.showerror(
"Directory Error",
"Please select a Vanilla 1.12 installation directory.",
)
return False
target_dir = os.path.abspath(target_dir)
wow_exe = os.path.join(target_dir, "WoW.exe")
data_dir = os.path.join(target_dir, "Data")
interface_dir = os.path.join(target_dir, "Interface")
if (
not os.path.isfile(wow_exe)
or not os.path.isdir(data_dir)
or not os.path.isdir(interface_dir)
):
messagebox.showerror(
"Invalid Directory",
"This does not look like a valid Vanilla 1.12 installation directory.\n\n"
"Select the game directory that directly contains WoW.exe, Data and Interface.",
)
return False
valid_pe, pe_reason = self._inspect_wow_executable(wow_exe)
if not valid_pe:
messagebox.showerror(
"Unsupported WoW Client",
pe_reason
+ "\n\nNo files were changed. Select a Vanilla 1.12/Turtle-compatible client.",
)
return False
# These are core archives in the Vanilla/Turtle data layout. Requiring
# them rejects most later-expansion/modern clients without tying the
# tool to one exact Turtle WoW.exe hash.
required_archives = ("base.MPQ", "dbc.MPQ", "interface.MPQ")
missing = [
name
for name in required_archives
if self._find_data_file_case_insensitive(data_dir, name) is None
]
if missing:
messagebox.showerror(
"Unsupported WoW Client",
"The selected client does not have the expected Vanilla/Turtle data layout.\n\n"
"Missing core archive(s): "
+ ", ".join(missing)
+ "\n\nNo files were changed.",
)
return False
return True
def validate_limits(self):
if self.safety_override.get():
return True
try:
if self.vt_farclip.get() > 1500:
messagebox.showerror("Limit Exceeded", "Render distance (Farclip) exceeds the safe limit of 1500.\n\nPlease lower it, or check 'Disable Safety Limits' to bypass.")
return False
if self.vt_frill.get() > 300:
messagebox.showerror("Limit Exceeded", "Ground clutter (Frilldistance) exceeds the safe limit of 300.\n\nPlease lower it, or check 'Disable Safety Limits' to bypass.")
return False
if self.vt_nameplate.get() > 41:
messagebox.showerror("Limit Exceeded", "Nameplate range exceeds the safe limit of 41.\n\nPlease lower it, or check 'Disable Safety Limits' to bypass.")
return False
if self.vt_maxcam.get() > 100:
messagebox.showerror("Limit Exceeded", "Camera distance exceeds the safe limit of 100.\n\nPlease lower it, or check 'Disable Safety Limits' to bypass.")
return False
if self.vt_soundchan.get() > 64:
messagebox.showerror("Limit Exceeded", "Sound Channels exceed the safe limit of 64.\n\nPlease lower it, or check 'Disable Safety Limits' to bypass.")
return False
if self.vt_fov.get() > 2.2689:
messagebox.showerror("Limit Exceeded", "Field of View exceeds the safe limit of 130 degrees (2.2689 radians).\n\nPlease lower it, or check 'Disable Safety Limits' to bypass.")
return False
except tk.TclError:
messagebox.showerror("Input Error", "Please ensure all Tweak fields contain valid numbers.")
return False
return True
def clean_unselected_files(self, target):
"""Removes managed files/folders if they were explicitly unselected by the user."""
# 1. Clean AutoLogin files if unselected
if not self.install_autologin.get():
glue_dir = os.path.join(target, "Data", "Interface", "GlueXML")
for file_name in ["AutoLogin.lua", "AutoLogin.xml", "GlueXML.toc"]:
file_path = os.path.join(glue_dir, file_name)
if os.path.exists(file_path):
try: os.remove(file_path)
except: pass
if os.path.exists(glue_dir) and not os.listdir(glue_dir):
try: os.rmdir(glue_dir)
except: pass
# 2. Clean unselected Core Plugins and their dependent AddOns
for dll_name, var in self.core_plugins.items():
if not var.get():
dll_path = os.path.join(target, dll_name)
if os.path.exists(dll_path):
try: os.remove(dll_path)
except: pass
addon_folder = self.addon_dependencies.get(dll_name)
if addon_folder:
addon_path = os.path.join(target, "Interface", "AddOns", addon_folder)
if os.path.exists(addon_path):
shutil.rmtree(addon_path, ignore_errors=True)
# 3. Clean unselected Optional plugins
for dll_name, var in self.optional_plugins.items():
if not var.get():
dll_path = os.path.join(target, dll_name)
if os.path.exists(dll_path):
try: os.remove(dll_path)
except: pass
def configure_script_memory(self, target):
"""Optionally set AddOn Script Memory to 0 (unlimited) in WTF/Config.wtf."""
if not self.vt_script_memory.get():
return
wtf_dir = os.path.join(target, "WTF")
config_path = os.path.join(wtf_dir, "Config.wtf")
original_mode = None
restore_readonly = False
try:
os.makedirs(wtf_dir, exist_ok=True)
if os.path.exists(config_path):
original_mode = os.stat(config_path).st_mode
if not (original_mode & stat.S_IWRITE):
os.chmod(config_path, original_mode | stat.S_IWRITE)
restore_readonly = True
existing = ""
if os.path.exists(config_path):
with open(config_path, "r", encoding="utf-8", errors="ignore") as f:
existing = f.read()
setting = 'SET scriptMemory "0"'
pattern = re.compile(
r'^\s*SET\s+scriptMemory\s+"[^"]*"\s*$',
re.IGNORECASE | re.MULTILINE
)
if pattern.search(existing):
updated = pattern.sub(setting, existing)
else:
if existing and not existing.endswith(("\n", "\r")):
existing += "\n"
updated = existing + setting + "\n"
with open(config_path, "w", encoding="utf-8", newline="") as f:
f.write(updated)
except PermissionError as exc:
raise RuntimeError(
"Windows denied access to WTF\\Config.wtf. Close WoW and any "
"program using the file. If your WoW folder is protected, run "
"the Modernization Tool as administrator and try again."
) from exc
finally:
if restore_readonly and original_mode is not None and os.path.exists(config_path):
try:
os.chmod(config_path, original_mode)
except OSError:
pass
def configure_wdb_cache(self, target):
"""Apply the RetroCro WDB blocker method when enabled."""
wdb_path = os.path.join(target, "WDB")
if self.vt_clear_wdb.get():
if os.path.islink(wdb_path):
raise RuntimeError("WDB is a symbolic link. Remove it manually before enabling Automatically Clear WDB.")
if os.path.isdir(wdb_path):
shutil.rmtree(wdb_path)
elif os.path.exists(wdb_path) and not os.path.isfile(wdb_path):
raise RuntimeError("The existing WDB path is not a normal file or directory.")
if not os.path.exists(wdb_path):
with open(wdb_path, "wb"):
pass
else:
# Only remove the blocker file. A real WDB cache directory is left
# untouched so WoW can manage it normally.
if os.path.isfile(wdb_path):
try:
os.chmod(wdb_path, os.stat(wdb_path).st_mode | stat.S_IWRITE)
except OSError:
pass
os.remove(wdb_path)
def configure_visual_audio(self, target):
progress = getattr(self, "_report_download_progress", None)
close_progress = getattr(self, "_close_download_progress", None)
warnings = []
visual_defs = [
(
"darker_nights",
"visual_darker_nights",
"Darker Nights",
remote_packages.install_darker_nights,
),
(
"pretty_night_sky",
"visual_pretty_night_sky",
"Pretty Night Sky",
remote_packages.install_pretty_night_sky,
),
(
"epoch_water",
"visual_epoch_water",
"Epoch Water",
remote_packages.install_epoch_water,
),
(
"fog_pushback",
"visual_fog_pushback",
"Fog Pushback",
remote_packages.install_fog_pushback,
),
(
"pink_herbs",
"visual_pink_herbs",
"Pink Herbs",
remote_packages.install_pink_herbs,
),
]
audio_defs = [
(
"no_error_sounds",
"audio_no_error_sounds",
"NoErrorSounds",
remote_packages.install_no_error_sounds,
os.path.join("Audio", "NoErrorSounds", "Sound"),
"Sound",
),
(
"fish_ping",
"audio_fish_ping",
"FishPing",
remote_packages.install_fish_ping,
os.path.join("Audio", "FishPing", "Sound"),
"Sound",
),
(
"warlock_muted_demons",
"audio_warlock_muted_demons",
"Warlock Muted Demons",
remote_packages.install_warlock_muted_demons,
os.path.join("Audio", "WarlockMutedDemons", "Data"),
"Data",
),
]
try:
# Large visual MPQs deliberately stay online-only, but they are not
# downloaded again on every Apply. A revision bump in remote_packages
# is enough to trigger one fresh download when a source/version changes.
for key, managed_id, display_name, installer in visual_defs:
if self.visual_mods[key].get():
revision = remote_packages.VISUAL_MOD_REVISIONS[managed_id]
if remote_packages.managed_mpq_is_current(
target,
managed_id,
revision,
):
continue
try:
installer(target, progress=progress)
except Exception as exc:
if remote_packages.managed_mod_is_installed(target, managed_id):
warnings.append(
f"{display_name}: update source unavailable; existing installed copy kept."
)
else:
raise RuntimeError(
f"{display_name} installation failed and no bundled MPQ backup is provided:\n{exc}"
) from exc
else:
remote_packages.remove_managed_mod(target, managed_id)
fallback_root = os.path.join(get_base_path(), "Payload", "Fallback")
for (
key,
managed_id,
display_name,
installer,
fallback_rel,
destination_prefix,
) in audio_defs:
if self.audio_mods[key].get():
try:
installer(target, progress=progress)
except Exception as exc:
if remote_packages.managed_mod_is_installed(target, managed_id):
warnings.append(
f"{display_name}: online source unavailable; existing installed copy kept."
)
continue
bundled = os.path.join(fallback_root, fallback_rel)
if not os.path.isdir(bundled):
raise RuntimeError(
f"{display_name} installation failed and its bundled backup is missing:\n{exc}"
) from exc
remote_packages.install_bundled_tree(
target,
managed_id,
bundled,
destination_prefix,
)
warnings.append(
f"{display_name}: online source unavailable; bundled backup installed."
)
else:
remote_packages.remove_managed_mod(target, managed_id)
finally:
if callable(close_progress):
close_progress()
if warnings:
messagebox.showwarning(
"Some online sources were unavailable",
"\n\n".join(warnings),
)
def _dep_marker_path(self, target_dir):
return os.path.join(
target_dir,
".modernization_tool",
"dep_override.json",
)
def _previous_settings_used_dep_override(self, target_dir):
"""Use the last successful settings file as migration/ownership evidence."""
settings_path = self._settings_path(target_dir)
if not os.path.isfile(settings_path):
return False
try:
with open(settings_path, "r", encoding="utf-8") as handle:
saved = json.load(handle)
tweaks = saved.get("vanilla_tweaks", {})
return isinstance(tweaks, dict) and tweaks.get("dep_fix") is True
except (OSError, json.JSONDecodeError, ValueError, TypeError):
return False
def _dep_override_owned_by_tool(self, target_dir):
return (
os.path.isfile(self._dep_marker_path(target_dir))
or self._previous_settings_used_dep_override(target_dir)
)
def _run_elevated_powershell(self, target_dir, script_text, action_name):
"""Run one small PowerShell script elevated and verify its exit code."""
support_dir = os.path.join(target_dir, ".modernization_tool")
os.makedirs(support_dir, exist_ok=True)
script_path = os.path.join(support_dir, "process_mitigation.ps1")
with open(script_path, "w", encoding="utf-8", newline="\n") as handle:
handle.write("$ErrorActionPreference = 'Stop'\n")
handle.write(script_text.rstrip() + "\n")
handle.write("exit 0\n")
escaped_script = script_path.replace("'", "''")
launcher = (
"$ErrorActionPreference='Stop'; "
"try { "
"$p = Start-Process -FilePath 'powershell.exe' -Verb RunAs "
"-Wait -PassThru -ArgumentList @("
"'-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass',"
f"'-File','{escaped_script}'"
"); "
"exit $p.ExitCode "
"} catch { Write-Error $_; exit 1 }"
)
try:
result = subprocess.run(
[
"powershell",
"-NoProfile",
"-NonInteractive",
"-Command",
launcher,
],
creationflags=0x08000000,
capture_output=True,
text=True,
)
except FileNotFoundError as exc:
raise RuntimeError(
f"{action_name} could not run because Windows PowerShell was not found."
) from exc
finally:
try:
os.remove(script_path)
except OSError:
pass
if result.returncode != 0:
details = (result.stderr or result.stdout or "").strip()
if len(details) > 800:
details = details[-800:]
suffix = f"\n\nDetails: {details}" if details else ""
raise RuntimeError(
f"{action_name} was not completed. The Administrator/UAC prompt may "
f"have been declined, or Windows rejected the mitigation change.{suffix}"
)
def apply_process_mitigations(self, target_dir):
"""Apply or restore the per-app DEP override with verified UAC completion."""
marker_path = self._dep_marker_path(target_dir)
if self.vt_dep_fix.get():
self._run_elevated_powershell(
target_dir,
"Set-ProcessMitigation -Name 'WoW_Modernized.exe' "
"-Disable DEP, EmulateAtlThunks",
"Disabling DEP for WoW_Modernized.exe",
)
# Keep explicit ownership evidence so disabling this option later
# restores only a setting that Modernization Tool applied.
try:
os.makedirs(os.path.dirname(marker_path), exist_ok=True)
temp_marker = marker_path + ".new"
with open(temp_marker, "w", encoding="utf-8") as handle:
json.dump(
{
"target": "WoW_Modernized.exe",
"disabled": ["DEP", "EmulateAtlThunks"],
},
handle,
indent=2,
)
os.replace(temp_marker, marker_path)
except OSError:
# settings.json is also ownership evidence after a successful
# Apply, so a marker write failure must not invalidate DEP.
pass
return
if not self._dep_override_owned_by_tool(target_dir):
return
# Microsoft documents -Remove together with -Disable for restoring an
# app-specific mitigation override back to the system default.
self._run_elevated_powershell(
target_dir,
"Set-ProcessMitigation -Name 'WoW_Modernized.exe' "
"-Remove -Disable DEP, EmulateAtlThunks",
"Restoring the system DEP defaults for WoW_Modernized.exe",
)
try:
os.remove(marker_path)
except OSError:
pass
def _set_install_busy(self, busy):
self._install_in_progress = bool(busy)
button = getattr(self, "apply_button", None)
if button is not None:
try:
button.configure(
state="disabled" if busy else "normal",
text="Applying Setup..." if busy else "Apply Setup & Tweaks",
)
except tk.TclError:
pass
try:
self.root.configure(cursor="wait" if busy else "")
self.root.update_idletasks()
except tk.TclError:
pass
def run_installation(self):
# Progress callbacks intentionally process Tk events. This explicit
# guard prevents a second Apply event from entering the installer while
# the first one is still running.
if self._install_in_progress:
return
self._set_install_busy(True)
try:
target_dir = self.wow_dir.get().strip()
# 1. Validate Directory / client architecture.
if not self.validate_installation_dir(target_dir):
return
# 2. Validate Bounds.
if not self.validate_limits():
return
# 3. Validate plugin combinations independently of UI callbacks.
if not self.validate_plugin_conflicts():
return
try:
self.clean_unselected_files(target_dir)
self.copy_base_files(target_dir)
self.configure_dxvk(target_dir)
self.configure_plugins(target_dir)
self.configure_visual_audio(target_dir)
self.run_vanilla_tweaks(target_dir)
self.configure_script_memory(target_dir)
self.configure_wdb_cache(target_dir)
self.apply_process_mitigations(target_dir)
# Generate the seamless launcher shortcut.
self.create_launcher_shortcut(target_dir)
self.cleanup_legacy_outputs(target_dir)
settings_warning = ""
try:
self.save_settings(target_dir)
except OSError as exc:
settings_warning = (
"\n\nWarning: your choices could not be saved for the next run. "
f"Details: {exc}"
)
messagebox.showinfo(
"Success",
"Installation and patching complete!\n\n"
"Use the new 'Play Modernized WoW' shortcut in your directory to launch the game."
+ settings_warning,
)
except PermissionError as exc:
messagebox.showerror(
"Permission Error",
"Windows denied access to a file or folder in the selected WoW directory.\n\n"
"Close WoW and any program using the files. If the folder is protected, "
"run the Modernization Tool as administrator and try again.\n\n"
f"Details: {exc}",
)
except Exception as exc:
messagebox.showerror("Installation Error", str(exc))
finally:
# Never leave the UI locked after validation errors, network
# failures, cancelled UAC prompts or installation exceptions.
close_progress = getattr(self, "_close_download_progress", None)
if callable(close_progress):
close_progress()
self._set_install_busy(False)
def cleanup_legacy_outputs(self, target_dir):
"""Remove only known leftovers from older Modernization Tool builds."""
modernized = os.path.join(target_dir, "WoW_Modernized.exe")
if not os.path.exists(modernized):
return
# These names were generated by the old WoW_Tweaked.exe workflow and
# are no longer used after switching to WoW_Modernized.exe.
legacy_names = (
"WoW_Tweaked.exe",
"WoW_Tweaked.dxvk-cache",
"WoW_Tweaked_d3d9.log",
)
for filename in legacy_names:
path = os.path.join(target_dir, filename)
if os.path.isfile(path):
try:
os.remove(path)
except OSError:
# Cleanup is best-effort and must not turn a successful
# installation into an error because a stale log is locked.
pass
# New shortcuts keep the icon in the tool's own support directory.
# Remove the old root copy only after the replacement exists.
managed_icon = os.path.join(
target_dir,
".modernization_tool",
"PurpleWowLogo.ico",
)
root_icon = os.path.join(target_dir, "PurpleWowLogo.ico")
if os.path.isfile(managed_icon) and os.path.isfile(root_icon):
try:
os.remove(root_icon)
except OSError:
pass
def create_launcher_shortcut(self, target_dir):
"""Create the launcher transactionally and verify cscript succeeded."""
shortcut_path = os.path.join(target_dir, "Play Modernized WoW.lnk")
staged_shortcut = shortcut_path + ".modernization-new.lnk"
vanilla_fixes_exe = os.path.join(target_dir, "VanillaFixes.exe")
modernized_exe = os.path.join(target_dir, "WoW_Modernized.exe")
if not os.path.isfile(vanilla_fixes_exe):
raise RuntimeError(
"VanillaFixes.exe is missing, so the launcher shortcut cannot be created."
)
if not os.path.isfile(modernized_exe):
raise RuntimeError(
"WoW_Modernized.exe is missing, so the launcher shortcut cannot be created."
)
support_dir = os.path.join(target_dir, ".modernization_tool")
os.makedirs(support_dir, exist_ok=True)
source_icon = os.path.join(get_base_path(), "PurpleWowLogo.ico")
target_icon = os.path.join(support_dir, "PurpleWowLogo.ico")
icon_vbs_line = ""
if os.path.isfile(source_icon):
try:
remote_packages._atomic_replace_file(source_icon, target_icon)
escaped_icon = (target_icon + ", 0").replace('"', '""')
icon_vbs_line = f'oLink.IconLocation = "{escaped_icon}"'
except Exception:
# A custom icon is cosmetic; shortcut creation may continue.
icon_vbs_line = ""
def vbs_escape(value):
return str(value).replace('"', '""')
vbs_script = f"""
Set oWS = WScript.CreateObject("WScript.Shell")
sLinkFile = "{vbs_escape(staged_shortcut)}"
Set oLink = oWS.CreateShortcut(sLinkFile)
oLink.TargetPath = "{vbs_escape(vanilla_fixes_exe)}"
oLink.Arguments = "WoW_Modernized.exe"
oLink.WorkingDirectory = "{vbs_escape(target_dir)}"
oLink.Description = "Launch Vanilla WoW with VanillaFixes and Tweaks"
{icon_vbs_line}
oLink.Save
"""
vbs_path = os.path.join(support_dir, "create_shortcut.vbs")
if os.path.exists(staged_shortcut):
try:
os.remove(staged_shortcut)
except OSError as exc:
raise RuntimeError(
f"Could not prepare the launcher shortcut update: {exc}"
) from exc
try:
with open(vbs_path, "w", encoding="utf-8", newline="\r\n") as handle:
handle.write(vbs_script)
try:
result = subprocess.run(
["cscript", "//nologo", vbs_path],
creationflags=0x08000000,
capture_output=True,
text=True,
)
except FileNotFoundError as exc:
raise RuntimeError(
"Windows Script Host (cscript.exe) was not found, so the "
"launcher shortcut could not be created."
) from exc
if result.returncode != 0:
details = (result.stderr or result.stdout or "").strip()
if len(details) > 800:
details = details[-800:]
suffix = f"\n\nDetails: {details}" if details else ""
raise RuntimeError(
"Windows Script Host failed to create the launcher shortcut."
+ suffix
)
if (
not os.path.isfile(staged_shortcut)
or os.path.getsize(staged_shortcut) <= 0
):
raise RuntimeError(
"Windows reported successful shortcut creation, but the new "
"Play Modernized WoW shortcut was not produced."
)
# Only replace the user's current shortcut after the new one has
# been created and verified, so a cscript failure leaves the old
# shortcut intact.
os.replace(staged_shortcut, shortcut_path)
finally:
for temp_path in (vbs_path, staged_shortcut):
if os.path.exists(temp_path):
try:
os.remove(temp_path)
except OSError:
pass
def copy_base_files(self, target):
payload_dir = os.path.join(get_base_path(), "Payload")
if not os.path.exists(payload_dir): return
if self.install_autologin.get() and os.path.exists(os.path.join(payload_dir, "Data")):
shutil.copytree(os.path.join(payload_dir, "Data"), os.path.join(target, "Data"), dirs_exist_ok=True)
if os.path.exists(os.path.join(payload_dir, "Interface")):
shutil.copytree(os.path.join(payload_dir, "Interface"), os.path.join(target, "Interface"), dirs_exist_ok=True)
# Removed SuperWoWhook.dll from this list!
for file in ["VanillaFixes.exe", "VfPatcher.dll"]:
source_file = os.path.join(payload_dir, file)
if os.path.exists(source_file): shutil.copy2(source_file, target)
def configure_dxvk(self, target):
payload_dir = os.path.join(get_base_path(), "Payload")
d3d9_target = os.path.join(target, "d3d9.dll")
conf_target = os.path.join(target, "dxvk.conf")
if self.rendering_mode.get() == "dxvk":
d3d9_src = os.path.join(payload_dir, "DXVK_Standard", "d3d9.dll")
conf_src = os.path.join(payload_dir, "dxvk.conf")
if os.path.exists(d3d9_src):
shutil.copy2(d3d9_src, d3d9_target)
if os.path.exists(conf_src):
shutil.copy2(conf_src, conf_target)
else:
for path in (d3d9_target, conf_target):
if os.path.exists(path):
os.remove(path)
def _managed_dll_entries(self):
"""Return every dlls.txt entry owned by this tool."""
managed = {"dxvk"}
managed.update(name.casefold() for name in self.core_plugins)
managed.update(name.casefold() for name in self.optional_plugins)
managed.update(
name.casefold()
for name in (
"ClassicAPI.dll",
"AuctionQueryThrottle.dll",
"VanillaMultiMonitorFix.dll",
"Interact.dll",
)
)
return managed
def _write_dlls_file(self, target, active_managed_lines):
"""Rewrite only tool-owned dlls.txt entries and preserve user entries."""
dlls_path = os.path.join(target, "dlls.txt")
managed_names = self._managed_dll_entries()
preserved = []
seen_preserved = set()
if os.path.isfile(dlls_path):
try:
with open(dlls_path, "r", encoding="utf-8", errors="ignore") as handle:
existing_lines = handle.read().splitlines()
except OSError as exc:
raise RuntimeError(f"Could not read existing dlls.txt: {exc}") from exc
for raw_line in existing_lines:
stripped = raw_line.strip()
if not stripped:
continue
# Preserve comments exactly; they may document manual plugins.
if stripped.startswith(("#", ";")):
preserved.append(raw_line.rstrip())
continue
if stripped.casefold() in managed_names:
continue
# Preserve unknown/manual entries once, case-insensitively.
key = stripped.casefold()
if key not in seen_preserved:
preserved.append(raw_line.rstrip())
seen_preserved.add(key)
active = []
seen_active = set()
for line in active_managed_lines:
stripped = str(line).strip()
if not stripped:
continue
key = stripped.casefold()
if key not in seen_active:
active.append(stripped)
seen_active.add(key)
output_lines = list(active)
if output_lines and preserved:
output_lines.append("")
output_lines.extend(preserved)
staged = dlls_path + ".modernization-new"
try:
with open(staged, "w", encoding="utf-8", newline="\n") as handle:
if output_lines:
handle.write("\n".join(output_lines) + "\n")
os.replace(staged, dlls_path)
except OSError as exc:
raise RuntimeError(f"Could not update dlls.txt: {exc}") from exc
finally:
if os.path.exists(staged):
try:
os.remove(staged)
except OSError:
pass
def configure_plugins(self, target):
payload_base = os.path.join(get_base_path(), "Payload")
payload_weirdu = os.path.join(payload_base, "WeirdUtils")
dlls_text_lines = []
if self.rendering_mode.get() == "dxvk":
dlls_text_lines.append("dxvk")
# Process Core Plugins
for dll_name, var in self.core_plugins.items():
if var.get():
source_dll = os.path.join(payload_base, dll_name)
if os.path.exists(source_dll):
shutil.copy2(source_dll, target)
dlls_text_lines.append(dll_name)
# Clean up corresponding addons if their core DLL was UNCHECKED
for dll_name, addon_folder in self.addon_dependencies.items():
if not self.core_plugins.get(dll_name, tk.BooleanVar(value=True)).get():
addon_path = os.path.join(target, "Interface", "AddOns", addon_folder)
if os.path.exists(addon_path):
shutil.rmtree(addon_path, ignore_errors=True)
# Process Optional Plugins - installed to the game root.
for dll_name, var in self.optional_plugins.items():
if var.get():
source_base = payload_base if dll_name == "no1600x1200.dll" else payload_weirdu
source_dll = os.path.join(source_base, dll_name)
if os.path.exists(source_dll):
shutil.copy2(source_dll, target)
dlls_text_lines.append(dll_name)
# Update only entries owned by this tool. Unknown/manual lines are kept.
self._write_dlls_file(target, dlls_text_lines)
def run_vanilla_tweaks(self, target, tweaks_exe=None, modern_cli=False):
"""Patch a copy of WoW.exe while preserving the original executable."""
wow_exe = os.path.join(target, "WoW.exe")
if tweaks_exe is None:
tweaks_exe = os.path.join(get_base_path(), "vanilla-tweaks.exe")
if not os.path.exists(tweaks_exe):
raise FileNotFoundError("vanilla-tweaks.exe was not found.")
args = [tweaks_exe]
superwow_active = self._superwow_enabled()
if modern_cli:
# tubtubs/vanilla-tweaks keeps these four patches opt-in. When
# SuperWoW is active, deliberately leave them unpatched.
if not superwow_active and abs(self.vt_fov.get() - 1.5708) >= 0.0001:
args.extend(["--fov", str(self.vt_fov.get()), "--fov-patch"])
if self.vt_farclip.get() == 777:
args.append("--no-farclip")
else:
args.extend(["--farclip", str(self.vt_farclip.get())])
if self.vt_frill.get() == 70:
args.append("--no-frilldistance")
else:
args.extend(["--frilldistance", str(self.vt_frill.get())])
if self.vt_nameplate.get() == 20:
args.append("--no-nameplatedistance")
else:
args.extend(["--nameplatedistance", str(self.vt_nameplate.get())])
if not superwow_active and self.vt_soundchan.get() != 12:
args.extend([
"--soundchannels",
str(self.vt_soundchan.get()),
"--soundchannels-patch",
])
if self.vt_maxcam.get() != 50:
args.extend(["--maxcameradistance", str(self.vt_maxcam.get())])
if not superwow_active and self.vt_quickloot.get():
args.append("--quickloot")
if not superwow_active and self.vt_bg_sound.get():
args.append("--sound-in-background")
if not self.vt_laa.get():
args.append("--no-largeaddressaware")
if not self.vt_cam_fix.get():
args.append("--no-cameraskipfix")
if self.vt_crossfaction_res.get():
args.append("--crossfactionresfix")
if not self.vt_custom_glues.get():
args.append("--no-customgluespatch")
if not self.vt_bluemoon.get():
args.append("--no-bluemoonpatch")
else:
# Legacy bundled brndd patcher enables these older patches by
# default, so explicitly disable all four when SuperWoW handles them.
if superwow_active:
args.extend([
"--no-fov",
"--no-soundchannels",
"--no-quickloot",
"--no-sound-in-background",
])
else:
if abs(self.vt_fov.get() - 1.5708) < 0.0001:
args.append("--no-fov")
else:
args.extend(["--fov", str(self.vt_fov.get())])
if self.vt_soundchan.get() == 12:
args.append("--no-soundchannels")
else:
args.extend(["--soundchannels", str(self.vt_soundchan.get())])
if not self.vt_quickloot.get():
args.append("--no-quickloot")
if not self.vt_bg_sound.get():
args.append("--no-sound-in-background")
if self.vt_farclip.get() == 777:
args.append("--no-farclip")
else:
args.extend(["--farclip", str(self.vt_farclip.get())])
if self.vt_frill.get() == 70:
args.append("--no-frilldistance")
else:
args.extend(["--frilldistance", str(self.vt_frill.get())])
if self.vt_nameplate.get() == 20:
args.append("--no-nameplatedistance")
else:
args.extend(["--nameplatedistance", str(self.vt_nameplate.get())])
if self.vt_maxcam.get() != 50:
args.extend(["--maxcameradistance", str(self.vt_maxcam.get())])
if not self.vt_laa.get():
args.append("--no-largeaddressaware")
if not self.vt_cam_fix.get():
args.append("--no-cameraskipfix")
args.extend(["-o", os.path.join(target, "WoW_Modernized.exe")])
args.append(wow_exe)
subprocess.run(args, check=True)
if __name__ == "__main__":
root = tk.Tk()
app = WowSetupTool(root)
root.mainloop()