313 lines
14 KiB
YAML
313 lines
14 KiB
YAML
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
|