diff --git a/setup_tool_dynamic.py b/setup_tool_dynamic.py new file mode 100644 index 0000000..cdc589d --- /dev/null +++ b/setup_tool_dynamic.py @@ -0,0 +1,203 @@ +import os +import shutil +import tkinter as tk +from tkinter import ttk, messagebox + +import remote_packages +from setup_tool import WowSetupTool, ToolTip, get_base_path + + +class ModernWowSetupTool(WowSetupTool): + """Feature-branch entry point adding live GitHub package updates.""" + + def __init__(self, root): + # Define this before WowSetupTool.__init__ because the parent constructor + # calls self.build_ui(), which dispatches to our overridden Plugins tab. + self.client_tweak = tk.StringVar(master=root, value="none") + super().__init__(root) + + def build_plugins_tab(self, parent): + container = ttk.Frame(parent) + container.pack(fill="both", expand=True, padx=10, pady=10) + + top = ttk.Frame(container) + top.pack(fill="both", expand=True) + + left_frame = ttk.LabelFrame(top, 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(top, text="Optional WeirdUtils") + right_frame.pack(side="right", fill="both", expand=True, padx=(5, 0)) + + ttk.Label( + right_frame, + text="Additional quality-of-life adjustments.", + 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, "")) + + tweaks_frame = ttk.LabelFrame(container, text="Tweaks") + tweaks_frame.pack(fill="x", pady=(10, 0)) + + ttk.Label( + tweaks_frame, + text="Choose one. ClassicAPI and Auction Query Throttle are mutually exclusive.", + font=("", 8, "italic"), + ).pack(anchor="w", padx=10, pady=(7, 3)) + + choices = ttk.Frame(tweaks_frame) + choices.pack(fill="x", padx=10, pady=(0, 8)) + + rb_none = ttk.Radiobutton( + choices, text="None", variable=self.client_tweak, value="none" + ) + rb_none.pack(side="left", padx=(0, 16)) + + rb_classicapi = ttk.Radiobutton( + choices, + text="ClassicAPI", + variable=self.client_tweak, + value="classicapi", + ) + rb_classicapi.pack(side="left", padx=(0, 16)) + ToolTip( + rb_classicapi, + "Downloads and installs the latest stable ClassicAPI.dll release from " + "brues-code/ClassicAPI when setup is applied.", + ) + + rb_auction = ttk.Radiobutton( + choices, + text="Auction Query Throttle", + variable=self.client_tweak, + value="auction", + ) + rb_auction.pack(side="left") + ToolTip( + rb_auction, + "Downloads and installs the latest stable AuctionQueryThrottle.dll release " + "from brues-code/AuctionQueryThrottle when setup is applied.", + ) + + def clean_unselected_files(self, target): + super().clean_unselected_files(target) + + choice = self.client_tweak.get() + managed = { + "classicapi": "ClassicAPI.dll", + "auction": "AuctionQueryThrottle.dll", + } + + # Always remove the non-selected alternative. Selecting None removes both. + for key, filename in managed.items(): + if choice != key: + path = os.path.join(target, filename) + if os.path.exists(path): + try: + os.remove(path) + except OSError: + pass + + def _fallback_core_dll(self, payload_base, target, dll_name, error): + source_dll = os.path.join(payload_base, dll_name) + if not os.path.exists(source_dll): + raise RuntimeError( + f"Could not download the latest {dll_name}, and no bundled fallback exists.\n\n{error}" + ) from error + + shutil.copy2(source_dll, target) + messagebox.showwarning( + "Online update unavailable", + f"Could not download the latest {dll_name}.\n\n" + f"The bundled version will be used instead.\n\nDetails: {error}", + ) + + 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.gpu_type.get() != "AMD": + dlls_text_lines.append("dxvk") + + # Core plugins. UnitXP and SuperWoW are refreshed from their upstream + # sources every time setup is applied. Their bundled copies remain an + # offline fallback so this feature does not reduce current reliability. + for dll_name, var in self.core_plugins.items(): + if not var.get(): + continue + + if dll_name == "UnitXP_SP3.dll": + try: + remote_packages.install_unitxp(target) + except Exception as exc: + self._fallback_core_dll(payload_base, target, dll_name, exc) + + elif dll_name == "SuperWoWhook.dll": + try: + remote_packages.install_superwow(target) + except Exception as exc: + self._fallback_core_dll(payload_base, target, dll_name, exc) + + else: + 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 dependent addons when the matching core DLL is disabled. + for dll_name, addon_folder in self.addon_dependencies.items(): + core_var = self.core_plugins.get(dll_name) + if core_var is not None and not core_var.get(): + addon_path = os.path.join(target, "Interface", "AddOns", addon_folder) + if os.path.exists(addon_path): + shutil.rmtree(addon_path, ignore_errors=True) + + # Optional WeirdUtils. + for dll_name, var in self.optional_plugins.items(): + if var.get(): + source_dll = os.path.join(payload_weirdu, dll_name) + if os.path.exists(source_dll): + shutil.copy2(source_dll, target) + dlls_text_lines.append(dll_name) + + # Mutually-exclusive live Tweaks. + choice = self.client_tweak.get() + if choice == "classicapi": + try: + remote_packages.install_classicapi(target) + except Exception as exc: + raise RuntimeError(f"ClassicAPI update failed:\n{exc}") from exc + dlls_text_lines.append("ClassicAPI.dll") + + elif choice == "auction": + try: + remote_packages.install_auction_query_throttle(target) + except Exception as exc: + raise RuntimeError(f"Auction Query Throttle update failed:\n{exc}") from exc + dlls_text_lines.append("AuctionQueryThrottle.dll") + + with open(os.path.join(target, "dlls.txt"), "w") as f: + f.write("\n".join(dlls_text_lines)) + + +if __name__ == "__main__": + root = tk.Tk() + app = ModernWowSetupTool(root) + root.mainloop()