PixelLauncher 2.1.2 — секции настроек, панель быстрых настроек, сборки Modrinth/CurseForge

This commit is contained in:
kosta committed 2026-10-20 17:36:35 +07:00
1 parent 80dc1826a1
commit 39cf752b7b
13 files changed
+972 -49

No files matched your search

+218
View File
@@ -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