Files
PixelLauncher/launcher/cfpack.py
T

221 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""Установка модпаков CurseForge (zip с manifest.json).
Формат CurseForge (minecraftModpack): в архиве лежит manifest.json, где
указана версия Minecraft, загрузчик и список модов (projectID + fileID),
плюс папка overrides/ с готовыми файлами игры.
Старые сборки (minecraftInstance) содержат minecraftinstance.json — там
моды перечислены с готовыми downloadUrl. Поддерживаются оба формата.
"""
import json
import os
import tempfile
import zipfile
from . import curseforge, forge, http, mods
from .java_fetch import _safe_join
LOADERS = ("fabric", "quilt", "forge", "neoforge")
def _split_loader(lid):
"""'forge-47.2.0' -> ('forge', '47.2.0')."""
lid = str(lid or "").strip()
if "-" in lid:
name, ver = lid.split("-", 1)
return name.lower(), ver
return lid.lower(), ""
def read_manifest(path):
"""Читает manifest.json. Возвращает (данные, mc, loader, loader_ver)."""
with zipfile.ZipFile(path) as z:
return _read_manifest(z)
def _read_manifest(zf):
"""То же, но из уже открытого архива."""
with zf.open("manifest.json") as f:
data = json.loads(f.read().decode("utf-8"))
minecraft = data.get("minecraft") or {}
mc = minecraft.get("version") or ""
loader, loader_ver = "", ""
for ml in (minecraft.get("modLoaders") or []):
name, ver = _split_loader(ml.get("id"))
if name not in LOADERS:
continue
if not loader or ml.get("primary"):
loader, loader_ver = name, ver
if ml.get("primary"):
break
if not mc:
raise RuntimeError("в паке не указана версия Minecraft")
return data, mc, loader, loader_ver
def _legacy_instance(zf):
"""Старый формат minecraftinstance.json -> (mods, mc, loader, loader_ver)."""
with zf.open("minecraftinstance.json") as f:
data = json.loads(f.read().decode("utf-8"))
mc = data.get("minecraftVersion") or ""
loader, loader_ver = "", ""
fv = data.get("forgeVersion") or ""
if fv:
loader, loader_ver = "forge", fv
mods_list = []
for m in (data.get("installedMods") or []) + (data.get("addons") or []):
url = m.get("downloadUrl") or ""
name = m.get("fileName") or ""
if not url or not name:
continue
sha1 = ""
for h in (m.get("hashes") or []):
if h.get("algo") == 1:
sha1 = h.get("value") or ""
mods_list.append({"url": url, "name": name, "sha1": sha1,
"size": int(m.get("fileSize") or 0)})
return data, mc, loader, loader_ver, mods_list
def _install_loader(game_dir, loader, loader_ver, mc, log):
"""Ставит загрузчик. Возвращает id версии или None."""
if not loader:
return None
if loader in ("fabric", "quilt"):
if log:
log("Пак: %s %s" % (loader, loader_ver or ""))
return mods.install(game_dir, loader, mc, loader_version=loader_ver or None)
if loader in ("forge", "neoforge"):
if log:
log("Пак: %s %s" % (loader, loader_ver or ""))
return forge.install(game_dir, loader, mc,
loader_version=loader_ver or None, log=log)
return None
def _extract_overrides(zf, game_dir, prefixes=("overrides/", "client-overrides/"),
log=None):
"""Распаковывает overrides/ в игровую папку. Возвращает число файлов."""
count = 0
for name in zf.namelist():
if name.endswith("/"):
continue
target = None
for prefix in prefixes:
if name.startswith(prefix):
target = name[len(prefix):]
break
if not target:
continue
dest = _safe_join(game_dir, target)
os.makedirs(os.path.dirname(dest), exist_ok=True)
with zf.open(name) as src, open(dest, "wb") as out:
out.write(src.read())
count += 1
if log and count:
log("Пак: распаковано %d файлов overrides" % count)
return count
def install(game_dir, pack_path, log=None, progress=None, cancel=None):
"""Ставит сборку CurseForge в game_dir. Возвращает (id_версии, файлов)."""
log = log or (lambda m, t=None: None)
with zipfile.ZipFile(pack_path) as zf:
names = set(zf.namelist())
if "manifest.json" in names:
data, mc, loader, loader_ver = _read_manifest(zf)
mod_list = []
for entry in (data.get("files") or []):
if entry.get("required") is False:
continue
pid = entry.get("projectID")
fid = entry.get("fileID")
if not pid or not fid:
continue
mod_list.append({"project": pid, "file": fid})
elif "minecraftinstance.json" in names:
data, mc, loader, loader_ver, mod_list = _legacy_instance(zf)
else:
raise RuntimeError("в архиве нет manifest.json")
if not mc:
raise RuntimeError("в паке не указана версия Minecraft")
log("Пак: Minecraft %s%s" % (mc, (", %s %s" % (loader, loader_ver))
if loader else ""))
vid = _install_loader(game_dir, loader, loader_ver, mc, log)
if progress:
progress(0.15)
# моды
mods_dir = os.path.join(game_dir, "mods")
os.makedirs(mods_dir, exist_ok=True)
total = len(mod_list) or 1
got = 0
for i, m in enumerate(mod_list):
if cancel is not None and cancel.is_set():
raise http.Cancelled
if "url" in m: # старый формат
url, name, sha1, size = (m["url"], m["name"], m["sha1"],
m.get("size") or 0)
else:
info = curseforge.get_file(m["project"], m["file"])
url = info.get("download_url") or ""
name = info.get("file_name") or ""
sha1 = info.get("sha1") or ""
size = info.get("size") or 0
if not url or not name:
continue
name = os.path.basename(name.replace("\\", "/"))
dest = _safe_join(mods_dir, name)
log("Пак: %s" % name)
try:
http.download_file(url, dest, size=size or None, sha1=sha1 or None,
progress=None, cancel=cancel)
got += 1
except http.Cancelled:
raise
except Exception as e: # noqa: BLE001
log("Пак: не скачан %s (%s)" % (name, e), "warn")
if progress:
progress(0.15 + 0.75 * (i + 1) / total)
# overrides
prefixes = ("overrides/", "client-overrides/")
ov_dir = (data.get("overrides") or "overrides")
if ov_dir and ov_dir != "overrides":
prefixes = (ov_dir.rstrip("/") + "/",) + prefixes
_extract_overrides(zf, game_dir, prefixes, log=log)
if progress:
progress(0.97)
return vid, got
def install_from_url(game_dir, url, log=None, progress=None, cancel=None,
filename_hint="pack.zip"):
"""Скачивает сборку CurseForge по прямой ссылке и устанавливает."""
dest = os.path.join(tempfile.gettempdir(), "PixelLauncher",
os.path.basename(filename_hint.replace("\\", "/"))
or "pack.zip")
os.makedirs(os.path.dirname(dest), exist_ok=True)
def prog(done_b, total_b):
if progress and total_b:
progress(min(0.15, (done_b / total_b) * 0.15))
http.download_file(url, dest, progress=prog, cancel=cancel)
def p2(f):
if progress:
progress(0.15 + f * 0.85)
vid, got = install(game_dir, dest, log=log, progress=p2, cancel=cancel)
try:
os.unlink(dest)
except OSError:
pass
return vid, got