name: One-time bundled fallback refresh on: push: branches: - feature/client-tweaks paths: - .github/workflows/refresh-bundled-fallbacks.yml permissions: contents: write jobs: refresh: runs-on: ubuntu-latest steps: - name: Checkout feature branch uses: actions/checkout@v4 with: ref: feature/client-tweaks fetch-depth: 0 - name: Refresh bundled files from upstream shell: bash run: | set -euo pipefail python - <<'PY' import hashlib import io import json import os import shutil import tempfile import urllib.request import zipfile from pathlib import Path ROOT = Path.cwd() PAYLOAD = ROOT / "Payload" FALLBACK = PAYLOAD / "Fallback" UA = {"User-Agent": "Dusk-92-Modernization-Tool-Fallback-Refresh"} metadata = {"schema": 1, "components": {}} def get_bytes(url): req = urllib.request.Request(url, headers=UA) with urllib.request.urlopen(req, timeout=180) as r: return r.read() def get_json(url): return json.loads(get_bytes(url).decode("utf-8")) def latest(repo): data = get_json(f"https://api.github.com/repos/{repo}/releases/latest") if data.get("draft") or data.get("prerelease"): raise RuntimeError(f"{repo}: latest release is not stable") return data def asset(release, exact=None, predicate=None): for a in release.get("assets", []): name = a.get("name", "") if exact and name.lower() == exact.lower(): return a if predicate and predicate(name): return a raise RuntimeError(f"Asset not found in {release.get('html_url')}") def write_bytes(path, data): path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(data) def validate_pe(path): data = Path(path).read_bytes() if len(data) < 1024 or data[:2] != b"MZ": raise RuntimeError(f"Invalid PE binary: {path}") def extract_zip(data): temp = Path(tempfile.mkdtemp(prefix="modernization_refresh_")) with zipfile.ZipFile(io.BytesIO(data)) as z: for info in z.infolist(): dest = (temp / info.filename).resolve() if temp.resolve() not in dest.parents and dest != temp.resolve(): raise RuntimeError(f"Unsafe zip path: {info.filename}") z.extractall(temp) return temp def find_file(root, name): matches = [p for p in Path(root).rglob("*") if p.is_file() and p.name.lower() == name.lower()] if not matches: raise RuntimeError(f"{name} not found") return matches[0] def find_dir(root, name): matches = [p for p in Path(root).rglob("*") if p.is_dir() and p.name.lower() == name.lower()] if not matches: raise RuntimeError(f"{name} directory not found") return matches[0] def replace_dir(src, dest): dest = Path(dest) if dest.exists(): shutil.rmtree(dest) dest.parent.mkdir(parents=True, exist_ok=True) shutil.copytree(src, dest) def sha256(path): h = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(1024 * 1024), b""): h.update(chunk) return h.hexdigest() def record(name, source, version, paths): metadata["components"][name] = { "source": source, "version": version, "files": [ { "path": str(Path(p).relative_to(ROOT)).replace("\\", "/"), "sha256": sha256(p), "size": Path(p).stat().st_size, } for p in paths if Path(p).is_file() ], } # Nampower DLL + settings addon rel = latest("brues-code/nampower") a = asset(rel, predicate=lambda n: n.lower().startswith("nampower-") and n.lower().endswith(".zip")) tmp = extract_zip(get_bytes(a["browser_download_url"])) try: dll = find_file(tmp, "nampower.dll") addon = find_dir(tmp, "NampowerSettings") write_bytes(PAYLOAD / "nampower.dll", dll.read_bytes()) replace_dir(addon, PAYLOAD / "Interface" / "Addons" / "nampowersettings") finally: shutil.rmtree(tmp, ignore_errors=True) validate_pe(PAYLOAD / "nampower.dll") record("Nampower", a["browser_download_url"], rel["tag_name"], [PAYLOAD / "nampower.dll"]) # UnitXP SP3 DLL + addon rel = latest("brues-code/UnitXP_SP3") a = asset(rel, predicate=lambda n: n.lower().startswith("unitxp_sp3") and n.lower().endswith(".zip")) tmp = extract_zip(get_bytes(a["browser_download_url"])) try: dll = find_file(tmp, "UnitXP_SP3.dll") addon = find_dir(tmp, "UnitXP_SP3_Addon") write_bytes(PAYLOAD / "UnitXP_SP3.dll", dll.read_bytes()) replace_dir(addon, PAYLOAD / "Interface" / "Addons" / "UnitXP_SP3_Addon") finally: shutil.rmtree(tmp, ignore_errors=True) validate_pe(PAYLOAD / "UnitXP_SP3.dll") record("UnitXP_SP3", a["browser_download_url"], rel["tag_name"], [PAYLOAD / "UnitXP_SP3.dll"]) # SuperWoW DLL + current SuperAPI master rel = latest("balakethelock/SuperWoW") a = asset(rel, predicate=lambda n: n.lower().startswith("superwow") and n.lower().endswith(".zip")) tmp = extract_zip(get_bytes(a["browser_download_url"])) try: dll = find_file(tmp, "SuperWoWhook.dll") write_bytes(PAYLOAD / "SuperWoWhook.dll", dll.read_bytes()) finally: shutil.rmtree(tmp, ignore_errors=True) validate_pe(PAYLOAD / "SuperWoWhook.dll") superapi_url = "https://codeload.github.com/balakethelock/SuperAPI/zip/refs/heads/master" tmp = extract_zip(get_bytes(superapi_url)) try: addon = find_dir(tmp, "SuperAPI-master") replace_dir(addon, PAYLOAD / "Interface" / "Addons" / "SuperAPI") finally: shutil.rmtree(tmp, ignore_errors=True) record("SuperWoW", a["browser_download_url"], rel["tag_name"], [PAYLOAD / "SuperWoWhook.dll"]) # VanillaHelpers rel = latest("isfir/VanillaHelpers") a = asset(rel, exact="VanillaHelpers.dll") write_bytes(PAYLOAD / "VanillaHelpers.dll", get_bytes(a["browser_download_url"])) validate_pe(PAYLOAD / "VanillaHelpers.dll") record("VanillaHelpers", a["browser_download_url"], rel["tag_name"], [PAYLOAD / "VanillaHelpers.dll"]) # no1600x1200 current RetroCro backup url = "https://raw.githubusercontent.com/RetroCro/TurtleWoW-Mods/refs/heads/main/Archive/DLL%20BACKUP/no1600x1200.dll" write_bytes(PAYLOAD / "no1600x1200.dll", get_bytes(url)) validate_pe(PAYLOAD / "no1600x1200.dll") record("no1600x1200", url, "RetroCro main", [PAYLOAD / "no1600x1200.dll"]) # PerfBoost current RetroCro binary backup (addon remains the existing avitasia bundle). url = "https://raw.githubusercontent.com/RetroCro/TurtleWoW-Mods/refs/heads/main/Archive/DLL%20BACKUP/perf_boost.dll" write_bytes(PAYLOAD / "perf_boost.dll", get_bytes(url)) validate_pe(PAYLOAD / "perf_boost.dll") record("PerfBoost", url, "RetroCro main", [PAYLOAD / "perf_boost.dll"]) # User-selected WeirdPerformance 0.7.3 build url = "https://codeberg.org/Dusk92/WeirdUtils/releases/download/0.7.3/weirdperformance.dll" write_bytes(PAYLOAD / "weirdperformance.dll", get_bytes(url)) validate_pe(PAYLOAD / "weirdperformance.dll") record("WeirdPerformance", url, "0.7.3", [PAYLOAD / "weirdperformance.dll"]) # ClassicAPI fallback rel = latest("brues-code/ClassicAPI") a = asset(rel, exact="ClassicAPI.dll") write_bytes(FALLBACK / "ClassicAPI.dll", get_bytes(a["browser_download_url"])) validate_pe(FALLBACK / "ClassicAPI.dll") record("ClassicAPI fallback", a["browser_download_url"], rel["tag_name"], [FALLBACK / "ClassicAPI.dll"]) # AuctionQueryThrottle fallback rel = latest("brues-code/AuctionQueryThrottle") a = asset(rel, exact="AuctionQueryThrottle.dll") write_bytes(FALLBACK / "AuctionQueryThrottle.dll", get_bytes(a["browser_download_url"])) validate_pe(FALLBACK / "AuctionQueryThrottle.dll") record("AuctionQueryThrottle fallback", a["browser_download_url"], rel["tag_name"], [FALLBACK / "AuctionQueryThrottle.dll"]) # VanillaMultiMonitorFix fallback rel = latest("Mates1500/VanillaMultiMonitorFix") a = asset(rel, exact="release.zip") tmp = extract_zip(get_bytes(a["browser_download_url"])) try: dll = find_file(tmp, "VanillaMultiMonitorFix.dll") cfg = find_file(tmp, "VMMFix_preferred_monitor.txt") out = FALLBACK / "VanillaMultiMonitorFix" if out.exists(): shutil.rmtree(out) out.mkdir(parents=True) write_bytes(out / "VanillaMultiMonitorFix.dll", dll.read_bytes()) write_bytes(out / "VMMFix_preferred_monitor.txt", cfg.read_bytes()) finally: shutil.rmtree(tmp, ignore_errors=True) validate_pe(FALLBACK / "VanillaMultiMonitorFix" / "VanillaMultiMonitorFix.dll") record( "VanillaMultiMonitorFix fallback", a["browser_download_url"], rel["tag_name"], [ FALLBACK / "VanillaMultiMonitorFix" / "VanillaMultiMonitorFix.dll", FALLBACK / "VanillaMultiMonitorFix" / "VMMFix_preferred_monitor.txt", ], ) # Interact fallback (DLL + addon) rel = latest("lookino/Interact") a = asset(rel, exact="Interact.zip") tmp = extract_zip(get_bytes(a["browser_download_url"])) try: dll = find_file(tmp, "Interact.dll") toc = find_file(tmp, "Interact.toc") addon = toc.parent out = FALLBACK / "Interact" if out.exists(): shutil.rmtree(out) out.mkdir(parents=True) write_bytes(out / "Interact.dll", dll.read_bytes()) replace_dir(addon, out / "Addon") finally: shutil.rmtree(tmp, ignore_errors=True) validate_pe(FALLBACK / "Interact" / "Interact.dll") record("Interact fallback", a["browser_download_url"], rel["tag_name"], [FALLBACK / "Interact" / "Interact.dll"]) # Tiny sound packs: store complete source trees as offline fallbacks. sound_specs = [ ("NoErrorSounds", "Macumbafeh/NoErrorSounds", "main", "Sound", FALLBACK / "Audio" / "NoErrorSounds" / "Sound"), ("FishPing", "notsureawake/FishPing", "master", "Sound", FALLBACK / "Audio" / "FishPing" / "Sound"), ("WarlockMutedDemons", "spzilyk/Warlock-Muted-Demons", "main", "Data", FALLBACK / "Audio" / "WarlockMutedDemons" / "Data"), ] for name, repo, branch, source_dir_name, dest in sound_specs: url = f"https://codeload.github.com/{repo}/zip/refs/heads/{branch}" tmp = extract_zip(get_bytes(url)) try: src = find_dir(tmp, source_dir_name) replace_dir(src, dest) finally: shutil.rmtree(tmp, ignore_errors=True) files = [p for p in dest.rglob("*") if p.is_file()] record(f"{name} fallback", url, branch, files) # Keep a modern tubtubs vanilla-tweaks build offline as well. rel = latest("tubtubs/vanilla-tweaks") a = asset( rel, predicate=lambda n: n.lower().endswith(".zip") and "windows" in n.lower() and not n.lower().endswith(".sha256sum"), ) tmp = extract_zip(get_bytes(a["browser_download_url"])) try: exe = find_file(tmp, "vanilla-tweaks.exe") write_bytes(ROOT / "vanilla-tweaks.exe", exe.read_bytes()) finally: shutil.rmtree(tmp, ignore_errors=True) validate_pe(ROOT / "vanilla-tweaks.exe") record("vanilla-tweaks fallback", a["browser_download_url"], rel["tag_name"], [ROOT / "vanilla-tweaks.exe"]) FALLBACK.mkdir(parents=True, exist_ok=True) (FALLBACK / "versions.json").write_text( json.dumps(metadata, indent=2, sort_keys=True) + "\n", encoding="utf-8", ) PY - name: Commit refreshed bundled fallbacks shell: bash run: | set -euo pipefail if git diff --quiet; then echo "Bundled files already match upstream." exit 0 fi git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add Payload vanilla-tweaks.exe git commit -m "payload: refresh bundled known-good fallbacks [skip ci]" git push origin HEAD:feature/client-tweaks