mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add cross-platform Python asset pipeline GUI
This commit is contained in:
parent
aaab2115d1
commit
20dd5ed63b
3 changed files with 842 additions and 0 deletions
782
tools/asset_pipeline_gui.py
Normal file
782
tools/asset_pipeline_gui.py
Normal file
|
|
@ -0,0 +1,782 @@
|
|||
#!/usr/bin/env python3
|
||||
"""WoWee Asset Pipeline GUI.
|
||||
|
||||
Cross-platform Tkinter app for running asset extraction and managing texture packs
|
||||
that are merged into Data/override in deterministic order.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import platform
|
||||
import queue
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import zipfile
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
from tkinter.scrolledtext import ScrolledText
|
||||
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parents[1]
|
||||
PIPELINE_DIR = ROOT_DIR / "asset_pipeline"
|
||||
STATE_FILE = PIPELINE_DIR / "state.json"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PackInfo:
|
||||
pack_id: str
|
||||
name: str
|
||||
source: str
|
||||
installed_dir: str
|
||||
installed_at: str
|
||||
file_count: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppState:
|
||||
wow_data_dir: str = ""
|
||||
output_data_dir: str = str(ROOT_DIR / "Data")
|
||||
extractor_path: str = ""
|
||||
expansion: str = "auto"
|
||||
locale: str = "auto"
|
||||
skip_dbc: bool = False
|
||||
dbc_csv: bool = False
|
||||
verify: bool = False
|
||||
verbose: bool = False
|
||||
threads: int = 0
|
||||
packs: list[PackInfo] = field(default_factory=list)
|
||||
active_pack_ids: list[str] = field(default_factory=list)
|
||||
last_extract_at: str = ""
|
||||
last_extract_ok: bool = False
|
||||
last_extract_command: str = ""
|
||||
last_override_build_at: str = ""
|
||||
|
||||
|
||||
class PipelineManager:
|
||||
def __init__(self) -> None:
|
||||
PIPELINE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
(PIPELINE_DIR / "packs").mkdir(parents=True, exist_ok=True)
|
||||
self.state = self._load_state()
|
||||
|
||||
def _default_state(self) -> AppState:
|
||||
return AppState()
|
||||
|
||||
def _load_state(self) -> AppState:
|
||||
if not STATE_FILE.exists():
|
||||
return self._default_state()
|
||||
try:
|
||||
doc = json.loads(STATE_FILE.read_text(encoding="utf-8"))
|
||||
packs = [PackInfo(**item) for item in doc.get("packs", [])]
|
||||
doc["packs"] = packs
|
||||
state = AppState(**doc)
|
||||
return state
|
||||
except (OSError, ValueError, TypeError):
|
||||
return self._default_state()
|
||||
|
||||
def save_state(self) -> None:
|
||||
serializable = asdict(self.state)
|
||||
STATE_FILE.write_text(json.dumps(serializable, indent=2), encoding="utf-8")
|
||||
|
||||
def now_str(self) -> str:
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
def _normalize_id(self, name: str) -> str:
|
||||
raw = "".join(ch.lower() if ch.isalnum() else "-" for ch in name).strip("-")
|
||||
base = raw or "pack"
|
||||
return f"{base}-{int(time.time())}"
|
||||
|
||||
def _pack_dir(self, pack_id: str) -> Path:
|
||||
return PIPELINE_DIR / "packs" / pack_id
|
||||
|
||||
def _looks_like_data_root(self, path: Path) -> bool:
|
||||
markers = {"interface", "world", "character", "textures", "sound"}
|
||||
names = {p.name.lower() for p in path.iterdir() if p.is_dir()} if path.is_dir() else set()
|
||||
return bool(markers.intersection(names))
|
||||
|
||||
def find_data_root(self, pack_path: Path) -> Path:
|
||||
direct_data = pack_path / "Data"
|
||||
if direct_data.is_dir():
|
||||
return direct_data
|
||||
|
||||
lower_data = pack_path / "data"
|
||||
if lower_data.is_dir():
|
||||
return lower_data
|
||||
|
||||
if self._looks_like_data_root(pack_path):
|
||||
return pack_path
|
||||
|
||||
# Common zip layout: one wrapper directory.
|
||||
children = [p for p in pack_path.iterdir() if p.is_dir()] if pack_path.is_dir() else []
|
||||
if len(children) == 1:
|
||||
child = children[0]
|
||||
child_data = child / "Data"
|
||||
if child_data.is_dir():
|
||||
return child_data
|
||||
if self._looks_like_data_root(child):
|
||||
return child
|
||||
|
||||
return pack_path
|
||||
|
||||
def _count_files(self, root: Path) -> int:
|
||||
if not root.exists():
|
||||
return 0
|
||||
return sum(1 for p in root.rglob("*") if p.is_file())
|
||||
|
||||
def install_pack_from_zip(self, zip_path: Path) -> PackInfo:
|
||||
pack_name = zip_path.stem
|
||||
pack_id = self._normalize_id(pack_name)
|
||||
target = self._pack_dir(pack_id)
|
||||
target.mkdir(parents=True, exist_ok=False)
|
||||
|
||||
with zipfile.ZipFile(zip_path, "r") as zf:
|
||||
zf.extractall(target)
|
||||
|
||||
data_root = self.find_data_root(target)
|
||||
info = PackInfo(
|
||||
pack_id=pack_id,
|
||||
name=pack_name,
|
||||
source=str(zip_path),
|
||||
installed_dir=str(target),
|
||||
installed_at=self.now_str(),
|
||||
file_count=self._count_files(data_root),
|
||||
)
|
||||
self.state.packs.append(info)
|
||||
self.save_state()
|
||||
return info
|
||||
|
||||
def install_pack_from_folder(self, folder_path: Path) -> PackInfo:
|
||||
pack_name = folder_path.name
|
||||
pack_id = self._normalize_id(pack_name)
|
||||
target = self._pack_dir(pack_id)
|
||||
shutil.copytree(folder_path, target)
|
||||
|
||||
data_root = self.find_data_root(target)
|
||||
info = PackInfo(
|
||||
pack_id=pack_id,
|
||||
name=pack_name,
|
||||
source=str(folder_path),
|
||||
installed_dir=str(target),
|
||||
installed_at=self.now_str(),
|
||||
file_count=self._count_files(data_root),
|
||||
)
|
||||
self.state.packs.append(info)
|
||||
self.save_state()
|
||||
return info
|
||||
|
||||
def uninstall_pack(self, pack_id: str) -> None:
|
||||
self.state.packs = [p for p in self.state.packs if p.pack_id != pack_id]
|
||||
self.state.active_pack_ids = [pid for pid in self.state.active_pack_ids if pid != pack_id]
|
||||
target = self._pack_dir(pack_id)
|
||||
if target.exists():
|
||||
shutil.rmtree(target)
|
||||
self.save_state()
|
||||
|
||||
def set_pack_active(self, pack_id: str, active: bool) -> None:
|
||||
if active:
|
||||
if pack_id not in self.state.active_pack_ids:
|
||||
self.state.active_pack_ids.append(pack_id)
|
||||
else:
|
||||
self.state.active_pack_ids = [pid for pid in self.state.active_pack_ids if pid != pack_id]
|
||||
self.save_state()
|
||||
|
||||
def move_active_pack(self, pack_id: str, delta: int) -> None:
|
||||
ids = self.state.active_pack_ids
|
||||
if pack_id not in ids:
|
||||
return
|
||||
idx = ids.index(pack_id)
|
||||
nidx = idx + delta
|
||||
if nidx < 0 or nidx >= len(ids):
|
||||
return
|
||||
ids[idx], ids[nidx] = ids[nidx], ids[idx]
|
||||
self.state.active_pack_ids = ids
|
||||
self.save_state()
|
||||
|
||||
def rebuild_override(self) -> dict[str, int]:
|
||||
out_dir = Path(self.state.output_data_dir)
|
||||
override_dir = out_dir / "override"
|
||||
override_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if override_dir.exists():
|
||||
shutil.rmtree(override_dir)
|
||||
override_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
copied = 0
|
||||
replaced = 0
|
||||
|
||||
active_map = {p.pack_id: p for p in self.state.packs}
|
||||
for pack_id in self.state.active_pack_ids:
|
||||
info = active_map.get(pack_id)
|
||||
if info is None:
|
||||
continue
|
||||
pack_dir = Path(info.installed_dir)
|
||||
if not pack_dir.exists():
|
||||
continue
|
||||
|
||||
data_root = self.find_data_root(pack_dir)
|
||||
for source in data_root.rglob("*"):
|
||||
if not source.is_file():
|
||||
continue
|
||||
rel = source.relative_to(data_root)
|
||||
target = override_dir / rel
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
if target.exists():
|
||||
replaced += 1
|
||||
shutil.copy2(source, target)
|
||||
copied += 1
|
||||
|
||||
self.state.last_override_build_at = self.now_str()
|
||||
self.save_state()
|
||||
return {"copied": copied, "replaced": replaced}
|
||||
|
||||
def _resolve_extractor(self) -> list[str] | None:
|
||||
configured = self.state.extractor_path.strip()
|
||||
if configured:
|
||||
path = Path(configured)
|
||||
if path.exists() and path.is_file():
|
||||
return [str(path)]
|
||||
|
||||
ext = ".exe" if platform.system().lower().startswith("win") else ""
|
||||
for candidate in [
|
||||
ROOT_DIR / "build" / "bin" / f"asset_extract{ext}",
|
||||
ROOT_DIR / "bin" / f"asset_extract{ext}",
|
||||
]:
|
||||
if candidate.exists():
|
||||
return [str(candidate)]
|
||||
|
||||
if platform.system().lower().startswith("win"):
|
||||
return None
|
||||
|
||||
shell_script = ROOT_DIR / "extract_assets.sh"
|
||||
if shell_script.exists():
|
||||
return ["bash", str(shell_script)]
|
||||
|
||||
return None
|
||||
|
||||
def build_extract_command(self) -> list[str]:
|
||||
mpq_dir = self.state.wow_data_dir.strip()
|
||||
output_dir = self.state.output_data_dir.strip()
|
||||
if not mpq_dir or not output_dir:
|
||||
raise ValueError("Both WoW Data directory and output directory are required.")
|
||||
|
||||
extractor = self._resolve_extractor()
|
||||
if extractor is None:
|
||||
raise ValueError(
|
||||
"No extractor found. Build asset_extract first or set the extractor path in Configuration."
|
||||
)
|
||||
|
||||
if extractor[0].endswith("extract_assets.sh") or extractor[-1].endswith("extract_assets.sh"):
|
||||
cmd = [*extractor, mpq_dir]
|
||||
if self.state.expansion and self.state.expansion != "auto":
|
||||
cmd.append(self.state.expansion)
|
||||
return cmd
|
||||
|
||||
cmd = [*extractor, "--mpq-dir", mpq_dir, "--output", output_dir]
|
||||
if self.state.expansion and self.state.expansion != "auto":
|
||||
cmd.extend(["--expansion", self.state.expansion])
|
||||
if self.state.locale and self.state.locale != "auto":
|
||||
cmd.extend(["--locale", self.state.locale])
|
||||
if self.state.skip_dbc:
|
||||
cmd.append("--skip-dbc")
|
||||
if self.state.dbc_csv:
|
||||
cmd.append("--dbc-csv")
|
||||
if self.state.verify:
|
||||
cmd.append("--verify")
|
||||
if self.state.verbose:
|
||||
cmd.append("--verbose")
|
||||
if self.state.threads > 0:
|
||||
cmd.extend(["--threads", str(self.state.threads)])
|
||||
return cmd
|
||||
|
||||
def summarize_state(self) -> dict[str, Any]:
|
||||
output_dir = Path(self.state.output_data_dir)
|
||||
manifest_path = output_dir / "manifest.json"
|
||||
override_dir = output_dir / "override"
|
||||
|
||||
summary: dict[str, Any] = {
|
||||
"output_dir": str(output_dir),
|
||||
"output_exists": output_dir.exists(),
|
||||
"manifest_exists": manifest_path.exists(),
|
||||
"manifest_entries": 0,
|
||||
"override_exists": override_dir.exists(),
|
||||
"override_files": self._count_files(override_dir),
|
||||
"packs_installed": len(self.state.packs),
|
||||
"packs_active": len(self.state.active_pack_ids),
|
||||
"last_extract_at": self.state.last_extract_at or "never",
|
||||
"last_extract_ok": self.state.last_extract_ok,
|
||||
"last_override_build_at": self.state.last_override_build_at or "never",
|
||||
}
|
||||
|
||||
if manifest_path.exists():
|
||||
try:
|
||||
doc = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
entries = doc.get("entries", {})
|
||||
if isinstance(entries, dict):
|
||||
summary["manifest_entries"] = len(entries)
|
||||
except (OSError, ValueError, TypeError):
|
||||
summary["manifest_entries"] = -1
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
class AssetPipelineGUI:
|
||||
def __init__(self, root: tk.Tk) -> None:
|
||||
self.root = root
|
||||
self.manager = PipelineManager()
|
||||
|
||||
self.log_queue: queue.Queue[str] = queue.Queue()
|
||||
self.proc_thread: threading.Thread | None = None
|
||||
self.proc_running = False
|
||||
|
||||
self.root.title("WoWee Asset Pipeline")
|
||||
self.root.geometry("1120x760")
|
||||
|
||||
self.status_var = tk.StringVar(value="Ready")
|
||||
self._build_ui()
|
||||
self._load_vars_from_state()
|
||||
self.refresh_pack_list()
|
||||
self.refresh_state_view()
|
||||
self.root.after(120, self._poll_logs)
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
top = ttk.Frame(self.root, padding=10)
|
||||
top.pack(fill="both", expand=True)
|
||||
|
||||
status = ttk.Label(top, textvariable=self.status_var, anchor="w")
|
||||
status.pack(fill="x", pady=(0, 8))
|
||||
|
||||
self.notebook = ttk.Notebook(top)
|
||||
self.notebook.pack(fill="both", expand=True)
|
||||
|
||||
self.cfg_tab = ttk.Frame(self.notebook, padding=10)
|
||||
self.packs_tab = ttk.Frame(self.notebook, padding=10)
|
||||
self.state_tab = ttk.Frame(self.notebook, padding=10)
|
||||
self.logs_tab = ttk.Frame(self.notebook, padding=10)
|
||||
|
||||
self.notebook.add(self.cfg_tab, text="Configuration")
|
||||
self.notebook.add(self.packs_tab, text="Texture Packs")
|
||||
self.notebook.add(self.state_tab, text="Current State")
|
||||
self.notebook.add(self.logs_tab, text="Logs")
|
||||
|
||||
self._build_config_tab()
|
||||
self._build_packs_tab()
|
||||
self._build_state_tab()
|
||||
self._build_logs_tab()
|
||||
|
||||
def _build_config_tab(self) -> None:
|
||||
self.var_wow_data = tk.StringVar()
|
||||
self.var_output_data = tk.StringVar()
|
||||
self.var_extractor = tk.StringVar()
|
||||
self.var_expansion = tk.StringVar(value="auto")
|
||||
self.var_locale = tk.StringVar(value="auto")
|
||||
self.var_skip_dbc = tk.BooleanVar(value=False)
|
||||
self.var_dbc_csv = tk.BooleanVar(value=False)
|
||||
self.var_verify = tk.BooleanVar(value=False)
|
||||
self.var_verbose = tk.BooleanVar(value=False)
|
||||
self.var_threads = tk.IntVar(value=0)
|
||||
|
||||
frame = self.cfg_tab
|
||||
|
||||
self._path_row(frame, 0, "WoW Data (MPQ source)", self.var_wow_data, self._pick_wow_data_dir)
|
||||
self._path_row(frame, 1, "Output Data directory", self.var_output_data, self._pick_output_dir)
|
||||
self._path_row(frame, 2, "Extractor binary/script (optional)", self.var_extractor, self._pick_extractor)
|
||||
|
||||
ttk.Label(frame, text="Expansion").grid(row=3, column=0, sticky="w", pady=6)
|
||||
exp_combo = ttk.Combobox(
|
||||
frame,
|
||||
textvariable=self.var_expansion,
|
||||
values=["auto", "classic", "turtle", "tbc", "wotlk"],
|
||||
state="readonly",
|
||||
width=18,
|
||||
)
|
||||
exp_combo.grid(row=3, column=1, sticky="w", pady=6)
|
||||
|
||||
ttk.Label(frame, text="Locale").grid(row=3, column=2, sticky="w", pady=6)
|
||||
loc_combo = ttk.Combobox(
|
||||
frame,
|
||||
textvariable=self.var_locale,
|
||||
values=["auto", "enUS", "enGB", "deDE", "frFR", "esES", "esMX", "ruRU", "koKR", "zhCN", "zhTW"],
|
||||
state="normal",
|
||||
width=12,
|
||||
)
|
||||
loc_combo.grid(row=3, column=3, sticky="w", pady=6)
|
||||
|
||||
ttk.Label(frame, text="Threads (0 = auto)").grid(row=4, column=0, sticky="w", pady=6)
|
||||
ttk.Spinbox(frame, from_=0, to=256, textvariable=self.var_threads, width=8).grid(
|
||||
row=4, column=1, sticky="w", pady=6
|
||||
)
|
||||
|
||||
opts = ttk.Frame(frame)
|
||||
opts.grid(row=5, column=0, columnspan=4, sticky="w", pady=6)
|
||||
ttk.Checkbutton(opts, text="Skip DBC extraction", variable=self.var_skip_dbc).pack(side="left", padx=(0, 12))
|
||||
ttk.Checkbutton(opts, text="Generate DBC CSV", variable=self.var_dbc_csv).pack(side="left", padx=(0, 12))
|
||||
ttk.Checkbutton(opts, text="Verify CRC", variable=self.var_verify).pack(side="left", padx=(0, 12))
|
||||
ttk.Checkbutton(opts, text="Verbose output", variable=self.var_verbose).pack(side="left", padx=(0, 12))
|
||||
|
||||
buttons = ttk.Frame(frame)
|
||||
buttons.grid(row=6, column=0, columnspan=4, sticky="w", pady=12)
|
||||
ttk.Button(buttons, text="Save Configuration", command=self.save_config).pack(side="left", padx=(0, 8))
|
||||
ttk.Button(buttons, text="Run Extraction", command=self.run_extraction).pack(side="left", padx=(0, 8))
|
||||
ttk.Button(buttons, text="Refresh State", command=self.refresh_state_view).pack(side="left")
|
||||
|
||||
tip = (
|
||||
"Texture packs are merged into <Output Data>/override in active order. "
|
||||
"Later packs override earlier packs file-by-file."
|
||||
)
|
||||
ttk.Label(frame, text=tip, foreground="#444").grid(row=7, column=0, columnspan=4, sticky="w", pady=(8, 0))
|
||||
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
def _build_packs_tab(self) -> None:
|
||||
left = ttk.Frame(self.packs_tab)
|
||||
left.pack(side="left", fill="both", expand=True)
|
||||
|
||||
right = ttk.Frame(self.packs_tab)
|
||||
right.pack(side="right", fill="y", padx=(12, 0))
|
||||
|
||||
self.pack_list = tk.Listbox(left, height=22)
|
||||
self.pack_list.pack(fill="both", expand=True)
|
||||
self.pack_list.bind("<<ListboxSelect>>", lambda _evt: self._refresh_pack_detail())
|
||||
|
||||
self.pack_detail = ScrolledText(left, height=10, wrap="word", state="disabled")
|
||||
self.pack_detail.pack(fill="both", expand=False, pady=(10, 0))
|
||||
|
||||
ttk.Button(right, text="Install ZIP", width=22, command=self.install_zip).pack(pady=4)
|
||||
ttk.Button(right, text="Install Folder", width=22, command=self.install_folder).pack(pady=4)
|
||||
ttk.Separator(right, orient="horizontal").pack(fill="x", pady=8)
|
||||
ttk.Button(right, text="Activate", width=22, command=self.activate_selected_pack).pack(pady=4)
|
||||
ttk.Button(right, text="Deactivate", width=22, command=self.deactivate_selected_pack).pack(pady=4)
|
||||
ttk.Button(right, text="Move Up", width=22, command=lambda: self.move_selected_pack(-1)).pack(pady=4)
|
||||
ttk.Button(right, text="Move Down", width=22, command=lambda: self.move_selected_pack(1)).pack(pady=4)
|
||||
ttk.Separator(right, orient="horizontal").pack(fill="x", pady=8)
|
||||
ttk.Button(right, text="Rebuild Override", width=22, command=self.rebuild_override).pack(pady=4)
|
||||
ttk.Button(right, text="Uninstall", width=22, command=self.uninstall_selected_pack).pack(pady=4)
|
||||
|
||||
def _build_state_tab(self) -> None:
|
||||
actions = ttk.Frame(self.state_tab)
|
||||
actions.pack(fill="x")
|
||||
ttk.Button(actions, text="Refresh", command=self.refresh_state_view).pack(side="left")
|
||||
|
||||
self.state_text = ScrolledText(self.state_tab, wrap="word", state="disabled")
|
||||
self.state_text.pack(fill="both", expand=True, pady=(10, 0))
|
||||
|
||||
def _build_logs_tab(self) -> None:
|
||||
actions = ttk.Frame(self.logs_tab)
|
||||
actions.pack(fill="x")
|
||||
ttk.Button(actions, text="Clear Logs", command=self.clear_logs).pack(side="left")
|
||||
|
||||
self.log_text = ScrolledText(self.logs_tab, wrap="none", state="disabled")
|
||||
self.log_text.pack(fill="both", expand=True, pady=(10, 0))
|
||||
|
||||
def _path_row(self, frame: ttk.Frame, row: int, label: str, variable: tk.StringVar, browse_cmd) -> None:
|
||||
ttk.Label(frame, text=label).grid(row=row, column=0, sticky="w", pady=6)
|
||||
ttk.Entry(frame, textvariable=variable).grid(row=row, column=1, columnspan=2, sticky="ew", pady=6)
|
||||
ttk.Button(frame, text="Browse", command=browse_cmd).grid(row=row, column=3, sticky="e", pady=6)
|
||||
|
||||
def _pick_wow_data_dir(self) -> None:
|
||||
picked = filedialog.askdirectory(title="Select WoW Data directory")
|
||||
if picked:
|
||||
self.var_wow_data.set(picked)
|
||||
|
||||
def _pick_output_dir(self) -> None:
|
||||
picked = filedialog.askdirectory(title="Select output Data directory")
|
||||
if picked:
|
||||
self.var_output_data.set(picked)
|
||||
|
||||
def _pick_extractor(self) -> None:
|
||||
picked = filedialog.askopenfilename(title="Select extractor binary or script")
|
||||
if picked:
|
||||
self.var_extractor.set(picked)
|
||||
|
||||
def _load_vars_from_state(self) -> None:
|
||||
st = self.manager.state
|
||||
self.var_wow_data.set(st.wow_data_dir)
|
||||
self.var_output_data.set(st.output_data_dir)
|
||||
self.var_extractor.set(st.extractor_path)
|
||||
self.var_expansion.set(st.expansion)
|
||||
self.var_locale.set(st.locale)
|
||||
self.var_skip_dbc.set(st.skip_dbc)
|
||||
self.var_dbc_csv.set(st.dbc_csv)
|
||||
self.var_verify.set(st.verify)
|
||||
self.var_verbose.set(st.verbose)
|
||||
self.var_threads.set(st.threads)
|
||||
|
||||
def save_config(self) -> None:
|
||||
st = self.manager.state
|
||||
st.wow_data_dir = self.var_wow_data.get().strip()
|
||||
st.output_data_dir = self.var_output_data.get().strip()
|
||||
st.extractor_path = self.var_extractor.get().strip()
|
||||
st.expansion = self.var_expansion.get().strip() or "auto"
|
||||
st.locale = self.var_locale.get().strip() or "auto"
|
||||
st.skip_dbc = bool(self.var_skip_dbc.get())
|
||||
st.dbc_csv = bool(self.var_dbc_csv.get())
|
||||
st.verify = bool(self.var_verify.get())
|
||||
st.verbose = bool(self.var_verbose.get())
|
||||
st.threads = int(self.var_threads.get())
|
||||
self.manager.save_state()
|
||||
self.status_var.set("Configuration saved")
|
||||
|
||||
def _selected_pack(self) -> PackInfo | None:
|
||||
sel = self.pack_list.curselection()
|
||||
if not sel:
|
||||
return None
|
||||
idx = int(sel[0])
|
||||
if idx < 0 or idx >= len(self.manager.state.packs):
|
||||
return None
|
||||
return self.manager.state.packs[idx]
|
||||
|
||||
def refresh_pack_list(self) -> None:
|
||||
active = self.manager.state.active_pack_ids
|
||||
self.pack_list.delete(0, tk.END)
|
||||
for pack in self.manager.state.packs:
|
||||
marker = ""
|
||||
if pack.pack_id in active:
|
||||
marker = f"[active #{active.index(pack.pack_id) + 1}] "
|
||||
self.pack_list.insert(tk.END, f"{marker}{pack.name}")
|
||||
self._refresh_pack_detail()
|
||||
|
||||
def _refresh_pack_detail(self) -> None:
|
||||
pack = self._selected_pack()
|
||||
self.pack_detail.configure(state="normal")
|
||||
self.pack_detail.delete("1.0", tk.END)
|
||||
if pack is None:
|
||||
self.pack_detail.insert(tk.END, "Select a texture pack to see details.")
|
||||
self.pack_detail.configure(state="disabled")
|
||||
return
|
||||
|
||||
active = "yes" if pack.pack_id in self.manager.state.active_pack_ids else "no"
|
||||
order = "-"
|
||||
if pack.pack_id in self.manager.state.active_pack_ids:
|
||||
order = str(self.manager.state.active_pack_ids.index(pack.pack_id) + 1)
|
||||
lines = [
|
||||
f"Name: {pack.name}",
|
||||
f"Active: {active}",
|
||||
f"Order: {order}",
|
||||
f"Files: {pack.file_count}",
|
||||
f"Installed at: {pack.installed_at}",
|
||||
f"Installed dir: {pack.installed_dir}",
|
||||
f"Source: {pack.source}",
|
||||
]
|
||||
self.pack_detail.insert(tk.END, "\n".join(lines))
|
||||
self.pack_detail.configure(state="disabled")
|
||||
|
||||
def install_zip(self) -> None:
|
||||
zip_path = filedialog.askopenfilename(
|
||||
title="Choose texture pack ZIP",
|
||||
filetypes=[("ZIP archives", "*.zip"), ("All files", "*.*")],
|
||||
)
|
||||
if not zip_path:
|
||||
return
|
||||
try:
|
||||
info = self.manager.install_pack_from_zip(Path(zip_path))
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
messagebox.showerror("Install failed", str(exc))
|
||||
return
|
||||
|
||||
self.refresh_pack_list()
|
||||
self.refresh_state_view()
|
||||
self.status_var.set(f"Installed pack: {info.name}")
|
||||
|
||||
def install_folder(self) -> None:
|
||||
folder = filedialog.askdirectory(title="Choose texture pack folder")
|
||||
if not folder:
|
||||
return
|
||||
try:
|
||||
info = self.manager.install_pack_from_folder(Path(folder))
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
messagebox.showerror("Install failed", str(exc))
|
||||
return
|
||||
|
||||
self.refresh_pack_list()
|
||||
self.refresh_state_view()
|
||||
self.status_var.set(f"Installed pack: {info.name}")
|
||||
|
||||
def activate_selected_pack(self) -> None:
|
||||
pack = self._selected_pack()
|
||||
if pack is None:
|
||||
return
|
||||
self.manager.set_pack_active(pack.pack_id, True)
|
||||
self.refresh_pack_list()
|
||||
self.refresh_state_view()
|
||||
self.status_var.set(f"Activated pack: {pack.name}")
|
||||
|
||||
def deactivate_selected_pack(self) -> None:
|
||||
pack = self._selected_pack()
|
||||
if pack is None:
|
||||
return
|
||||
self.manager.set_pack_active(pack.pack_id, False)
|
||||
self.refresh_pack_list()
|
||||
self.refresh_state_view()
|
||||
self.status_var.set(f"Deactivated pack: {pack.name}")
|
||||
|
||||
def move_selected_pack(self, delta: int) -> None:
|
||||
pack = self._selected_pack()
|
||||
if pack is None:
|
||||
return
|
||||
self.manager.move_active_pack(pack.pack_id, delta)
|
||||
self.refresh_pack_list()
|
||||
self.refresh_state_view()
|
||||
self.status_var.set(f"Reordered active pack: {pack.name}")
|
||||
|
||||
def uninstall_selected_pack(self) -> None:
|
||||
pack = self._selected_pack()
|
||||
if pack is None:
|
||||
return
|
||||
ok = messagebox.askyesno("Confirm uninstall", f"Uninstall texture pack '{pack.name}'?")
|
||||
if not ok:
|
||||
return
|
||||
self.manager.uninstall_pack(pack.pack_id)
|
||||
self.refresh_pack_list()
|
||||
self.refresh_state_view()
|
||||
self.status_var.set(f"Uninstalled pack: {pack.name}")
|
||||
|
||||
def rebuild_override(self) -> None:
|
||||
try:
|
||||
report = self.manager.rebuild_override()
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
messagebox.showerror("Override rebuild failed", str(exc))
|
||||
return
|
||||
self.refresh_state_view()
|
||||
self.status_var.set(
|
||||
f"Override rebuilt: {report['copied']} files copied, {report['replaced']} replaced"
|
||||
)
|
||||
self._append_log(
|
||||
f"[{self.manager.now_str()}] Override rebuild complete: {report['copied']} copied, {report['replaced']} replaced"
|
||||
)
|
||||
|
||||
def clear_logs(self) -> None:
|
||||
self.log_text.configure(state="normal")
|
||||
self.log_text.delete("1.0", tk.END)
|
||||
self.log_text.configure(state="disabled")
|
||||
|
||||
def _append_log(self, line: str) -> None:
|
||||
self.log_text.configure(state="normal")
|
||||
self.log_text.insert(tk.END, line + "\n")
|
||||
self.log_text.see(tk.END)
|
||||
self.log_text.configure(state="disabled")
|
||||
|
||||
def _poll_logs(self) -> None:
|
||||
while True:
|
||||
try:
|
||||
line = self.log_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
break
|
||||
self._append_log(line)
|
||||
self.root.after(120, self._poll_logs)
|
||||
|
||||
def run_extraction(self) -> None:
|
||||
if self.proc_running:
|
||||
messagebox.showinfo("Extraction running", "An extraction is already running.")
|
||||
return
|
||||
|
||||
self.save_config()
|
||||
|
||||
try:
|
||||
cmd = self.manager.build_extract_command()
|
||||
except ValueError as exc:
|
||||
messagebox.showerror("Cannot run extraction", str(exc))
|
||||
return
|
||||
|
||||
def worker() -> None:
|
||||
self.proc_running = True
|
||||
started = self.manager.now_str()
|
||||
self.log_queue.put(f"[{started}] Running: {' '.join(cmd)}")
|
||||
self.root.after(0, lambda: self.status_var.set("Extraction running..."))
|
||||
|
||||
ok = False
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=str(ROOT_DIR),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
)
|
||||
assert process.stdout is not None
|
||||
for line in process.stdout:
|
||||
self.log_queue.put(line.rstrip())
|
||||
rc = process.wait()
|
||||
ok = rc == 0
|
||||
if not ok:
|
||||
self.log_queue.put(f"Extractor exited with status {rc}")
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
self.log_queue.put(f"Extraction error: {exc}")
|
||||
finally:
|
||||
self.manager.state.last_extract_at = self.manager.now_str()
|
||||
self.manager.state.last_extract_ok = ok
|
||||
self.manager.state.last_extract_command = " ".join(cmd)
|
||||
self.manager.save_state()
|
||||
self.proc_running = False
|
||||
self.root.after(0, self.refresh_state_view)
|
||||
self.root.after(
|
||||
0, lambda: self.status_var.set("Extraction complete" if ok else "Extraction failed")
|
||||
)
|
||||
|
||||
self.proc_thread = threading.Thread(target=worker, daemon=True)
|
||||
self.proc_thread.start()
|
||||
|
||||
def refresh_state_view(self) -> None:
|
||||
summary = self.manager.summarize_state()
|
||||
|
||||
active_names = []
|
||||
pack_map = {p.pack_id: p.name for p in self.manager.state.packs}
|
||||
for pid in self.manager.state.active_pack_ids:
|
||||
active_names.append(pack_map.get(pid, f"<missing {pid}>"))
|
||||
|
||||
lines = [
|
||||
"WoWee Asset Pipeline State",
|
||||
"",
|
||||
f"Output directory: {summary['output_dir']}",
|
||||
f"Output exists: {summary['output_exists']}",
|
||||
f"manifest.json present: {summary['manifest_exists']}",
|
||||
f"Manifest entries: {summary['manifest_entries']}",
|
||||
"",
|
||||
f"Override folder present: {summary['override_exists']}",
|
||||
f"Override file count: {summary['override_files']}",
|
||||
f"Last override build: {summary['last_override_build_at']}",
|
||||
"",
|
||||
f"Installed texture packs: {summary['packs_installed']}",
|
||||
f"Active texture packs: {summary['packs_active']}",
|
||||
"Active order:",
|
||||
]
|
||||
if active_names:
|
||||
for i, name in enumerate(active_names, start=1):
|
||||
lines.append(f" {i}. {name}")
|
||||
else:
|
||||
lines.append(" (none)")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
f"Last extraction time: {summary['last_extract_at']}",
|
||||
f"Last extraction success: {summary['last_extract_ok']}",
|
||||
f"Last extraction command: {self.manager.state.last_extract_command or '(none)'}",
|
||||
"",
|
||||
"Pipeline files:",
|
||||
f" State file: {STATE_FILE}",
|
||||
f" Packs dir: {PIPELINE_DIR / 'packs'}",
|
||||
]
|
||||
)
|
||||
|
||||
self.state_text.configure(state="normal")
|
||||
self.state_text.delete("1.0", tk.END)
|
||||
self.state_text.insert(tk.END, "\n".join(lines))
|
||||
self.state_text.configure(state="disabled")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
root = tk.Tk()
|
||||
AssetPipelineGUI(root)
|
||||
root.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue