PixelLauncher 2.1.0 — стабильная сборка

Офлайн-лаунчер Minecraft: аккаунты без Microsoft, установка версий и лоадеров
(Fabric, Quilt, Forge, NeoForge), каталог модпаков Modrinth, поиск и обновление
модов, разбор вылетов, системный трей, автозагрузка, плагины, прокси и зеркала.
This commit is contained in:
kosta2014p committed 2026-10-18 22:44:19 +07:00
commit ec6f13ddcc
33 files changed
+10445

No files matched your search

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