From 0ff3b156ea25b54b05b6ff3e0c717848b0f6a511 Mon Sep 17 00:00:00 2001 From: Dusk-92 Date: Thu, 27 Aug 2026 15:07:56 +0200 Subject: [PATCH] ci: add one-time bundled fallback refresh --- .../workflows/refresh-bundled-fallbacks.yml | 312 ++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 .github/workflows/refresh-bundled-fallbacks.yml diff --git a/.github/workflows/refresh-bundled-fallbacks.yml b/.github/workflows/refresh-bundled-fallbacks.yml new file mode 100644 index 0000000..a282532 --- /dev/null +++ b/.github/workflows/refresh-bundled-fallbacks.yml @@ -0,0 +1,312 @@ +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