Files
PixelLauncher/launcher/mrpack.py
T
kosta2014p ec6f13ddcc PixelLauncher 2.1.0 — стабильная сборка
Офлайн-лаунчер Minecraft: аккаунты без Microsoft, установка версий и лоадеров
(Fabric, Quilt, Forge, NeoForge), каталог модпаков Modrinth, поиск и обновление
модов, разбор вылетов, системный трей, автозагрузка, плагины, прокси и зеркала.
2026-10-18 22:44:19 +07:00

146 lines
5.4 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 -*-
"""Импорт модпаков .mrpack (формат Modrinth) и каталог модпаков.
mrpack — zip с modrinth.index.json: индекс описывает версию Minecraft,
загрузчик (fabric/quilt/forge/neoforge) и список файлов для скачивания;
плюс каталоги overrides/ (client-overrides/) — готовые файлы папки игры.
"""
import json
import os
import zipfile
from . import forge, http, mods
from .java_fetch import _safe_join
PACKS_BASE = "https://pixellauncher.ru/packs"
CATALOG_URL = PACKS_BASE + "/catalog.json"
def fetch_catalog(timeout=15):
"""Каталог модпаков с сайта: [{title, desc, url, mc, loader, size}, ...]."""
raw = http.get_bytes(CATALOG_URL, timeout=timeout)
data = json.loads(raw.decode("utf-8"))
if not isinstance(data, list):
raise ValueError("пустой каталог модпаков")
return [x for x in data
if isinstance(x, dict) and isinstance(x.get("url"), str)
and x["url"]]
def pack_url(entry):
"""Абсолютный URL пака: относительные пути тянутся с сайта."""
u = str(entry.get("url") or "").strip()
if u.startswith(("http://", "https://")):
return u
return PACKS_BASE + "/" + u.lstrip("/")
def read_index(path):
"""Читает индекс пака. Возвращает (индекс, mc_ver, loader, loader_ver)."""
with zipfile.ZipFile(path) as z:
with z.open("modrinth.index.json") as f:
data = json.loads(f.read().decode("utf-8"))
deps = data.get("dependencies") or {}
mc = deps.get("minecraft") or ""
loader, loader_ver = "", ""
if deps.get("fabric-loader"):
loader, loader_ver = "fabric", deps["fabric-loader"]
elif deps.get("quilt-loader"):
loader, loader_ver = "quilt", deps["quilt-loader"]
elif deps.get("neoforge"):
loader, loader_ver = "neoforge", deps["neoforge"]
elif deps.get("forge"):
loader, loader_ver = "forge", deps["forge"]
if not mc:
raise RuntimeError("в паке не указана версия Minecraft")
return data, mc, loader, loader_ver
def _files_from_index(index):
"""[(путь_относительно_игры, url, filename, size)] — клиентские файлы."""
out = []
for entry in index.get("files") or []:
env = entry.get("env") or {}
if env.get("client") == "unsupported":
continue
downloads = entry.get("downloads") or []
if not downloads:
continue
primary = next((d for d in downloads if d.get("primary")), downloads[0])
url = primary.get("url") or ""
if not url:
continue
rel = (entry.get("path") or "").replace("\\", "/")
if not rel or rel.endswith("/"):
continue
out.append((rel, url, primary.get("filename") or rel.rsplit("/", 1)[-1],
int(primary.get("size") or entry.get("fileSize") or 0)))
return out
def install(game_dir, pack_path, log=None, progress=None, cancel=None):
"""Ставит пак в game_dir. Возвращает (id_версии, скачано_файлов).
progress(fraction) и cancel — как у http.download_file.
"""
if log:
log(f"Пак: читаю {os.path.basename(pack_path)}…")
index, mc, loader, loader_ver = read_index(pack_path)
files = _files_from_index(index)
total = sum(max(1, f[3]) for f in files) or 1
done = 0
# лоадер
vid = None
if loader:
if log:
log(f"Пак: Minecraft {mc}, {loader} {loader_ver}")
if loader in ("fabric", "quilt"):
vid = mods.install(game_dir, loader, mc, loader_version=loader_ver)
else: # forge / neoforge — официальным инсталлятором
vid = forge.install(game_dir, loader, mc,
loader_version=loader_ver or None, log=log)
done += max(1, total // (len(files) + 2))
# скачивание файлов
got = 0
for rel, url, filename, size in files:
if cancel is not None and cancel.is_set():
raise http.Cancelled
dest = _safe_join(game_dir, rel)
os.makedirs(os.path.dirname(dest), exist_ok=True)
if log:
log(f"Пак: {rel}")
if http.download_file(url, dest, size=size, cancel=cancel):
got += 1
done += max(1, size or 1)
if progress:
progress(min(0.95, done / total))
# overrides
ov = 0
with zipfile.ZipFile(pack_path) as z:
for name in z.namelist():
target = None
for prefix in ("overrides/", "client-overrides/"):
if name.startswith(prefix) and not name.endswith("/"):
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 z.open(name) as src, open(dest, "wb") as out:
out.write(src.read())
ov += 1
if log and ov:
log(f"Пак: распаковано {ov} файлов overrides")
if progress:
progress(1.0)
if log:
log(f"Пак: готово — {got} файл(ов) скачано")
return vid, got