PixelLauncher 2.1.2 — секции настроек, панель быстрых настроек, сборки Modrinth/CurseForge
This commit is contained in:
1 parent
80dc1826a1
commit
39cf752b7b
13 files changed
+972
-49
No files matched your search
@@ -0,0 +1,218 @@
|
||||
# -*- 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
|
||||
Reference in new issue
Block a user