189 lines
7.0 KiB
Python
189 lines
7.0 KiB
Python
# -*- 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 _primary_download(downloads):
|
||
"""Достаёт основной файл из "downloads".
|
||
|
||
Формат в индексе Modrinth встречается в трёх видах:
|
||
["https://..."] — просто список ссылок,
|
||
[{"hashes": {...}, "url": "...", ...}] — старый формат с объектами,
|
||
{"primary": "https://...", "alternate": [...]} — новый формат.
|
||
"""
|
||
if isinstance(downloads, dict):
|
||
downloads = ([downloads.get("primary")] +
|
||
list(downloads.get("alternate") or []))
|
||
if not downloads:
|
||
return None
|
||
first = downloads[0]
|
||
if isinstance(first, str):
|
||
return {"url": first, "filename": "", "size": 0}
|
||
primary = next((d for d in downloads
|
||
if isinstance(d, dict) and d.get("primary")), first)
|
||
if not isinstance(primary, dict):
|
||
return None
|
||
return primary
|
||
|
||
|
||
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 = _primary_download(downloads)
|
||
if not primary:
|
||
continue
|
||
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")
|
||
return vid, got
|
||
|
||
|
||
def install_from_url(game_dir, url, log=None, progress=None, cancel=None, filename_hint="pack.mrpack"):
|
||
"""Скачивает модпак по прямой ссылке и устанавливает."""
|
||
import tempfile
|
||
dest = os.path.join(tempfile.gettempdir(), "PixelLauncher",
|
||
filename_hint.replace("/", "_"))
|
||
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||
|
||
def prog(done_b, total_b):
|
||
if progress and total_b:
|
||
progress(min(0.4, (done_b / total_b) * 0.4))
|
||
|
||
http.download_file(url, dest, progress=prog, cancel=cancel)
|
||
def p2(f):
|
||
if progress:
|
||
progress(0.4 + f * 0.6)
|
||
vid, got = install(game_dir, dest, log=log, progress=p2, cancel=cancel)
|
||
try:
|
||
os.unlink(dest)
|
||
except OSError:
|
||
pass
|
||
return vid, got
|