PixelLauncher 2.1.0 — стабильная сборка
Офлайн-лаунчер Minecraft: аккаунты без Microsoft, установка версий и лоадеров (Fabric, Quilt, Forge, NeoForge), каталог модпаков Modrinth, поиск и обновление модов, разбор вылетов, системный трей, автозагрузка, плагины, прокси и зеркала.
This commit is contained in:
commit
ec6f13ddcc
33 files changed
+10445
No files matched your search
@@ -0,0 +1,145 @@
|
||||
# -*- 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
|
||||
Reference in new issue
Block a user