# -*- 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: names = set(z.namelist()) if "manifest.json" not in names: raise RuntimeError("в архиве нет manifest.json") with z.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(pack_path) 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