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

+225
View File
@@ -0,0 +1,225 @@
"""Скачивание файлов игры: client jar, библиотеки, нативы, ассеты."""
import json
import os
import shutil
import zipfile
from concurrent.futures import as_completed, ThreadPoolExecutor
from . import http
LIB_WORKERS = 16
ASSET_WORKERS = 32
ASSET_BASE = "https://resources.download.minecraft.net/"
def _mirror_urls(mirrors, default_base, url):
"""Кандидаты для скачивания: зеркала (если подходят) + официальный URL."""
urls = []
for m in mirrors or []:
m = (m or "").strip().rstrip("/")
if m and url.startswith(default_base):
urls.append(m + "/" + url[len(default_base):])
urls.append(url)
return urls
def _download_many(tasks, workers=LIB_WORKERS, progress=None, verify=False,
cancel=None):
"""tasks: список (urls, path, sha1, size) — urls по приоритету (зеркала).
progress(done, total). verify=True — полная проверка SHA-1.
cancel — threading.Event: досрочная оставка (http.Cancelled).
"""
total = len(tasks)
errors = []
if not tasks:
return errors
def work(task):
urls, path, sha1, size = task
last = None
for url in urls:
if cancel is not None and cancel.is_set():
raise http.Cancelled()
try:
return http.download_file(url, path, sha1, size, verify,
cancel=cancel)
except http.Cancelled:
raise
except Exception as e: # noqa: BLE001
last = e
return RuntimeError(f"{urls[-1] if urls else '?'}: {last}")
with ThreadPoolExecutor(max_workers=workers) as ex:
futs = [ex.submit(work, t) for t in tasks]
done = 0
for fut in as_completed(futs):
if cancel is not None and cancel.is_set():
for f2 in futs:
f2.cancel()
raise http.Cancelled()
done += 1
try:
res = fut.result()
if isinstance(res, Exception):
errors.append(str(res))
except http.Cancelled:
raise
except Exception as e: # noqa: BLE001
errors.append(str(e))
if progress:
progress(done, total)
return errors
def extract_natives(version, libs_dir):
"""Распаковывает natives-jar'ы в папку нативов версии.
Если набор jar'ов не менялся (сигнатура в .stamp), распаковка
пропускается — на повторных запусках это экономит время.
"""
dest = version.natives_dir()
os.makedirs(dest, exist_ok=True)
stamp_path = os.path.join(dest, ".stamp")
jars, sig_parts = [], []
for rel in version.natives_jars:
jar = os.path.join(libs_dir, *rel.split("/"))
if not os.path.isfile(jar):
continue
jars.append(jar)
st = os.stat(jar)
sig_parts.append(f"{rel}:{st.st_size}:{int(st.st_mtime)}")
sig = ";".join(sig_parts)
try:
with open(stamp_path, encoding="utf-8") as f:
if f.read() == sig:
return dest
except OSError:
pass
for jar in jars:
try:
with zipfile.ZipFile(jar) as z:
for info in z.infolist():
name = info.filename
if name.endswith("/") or name.upper().startswith("META-INF/"):
continue
target = os.path.join(dest, *name.split("/"))
os.makedirs(os.path.dirname(target), exist_ok=True)
with z.open(info) as src, open(target, "wb") as dst:
shutil.copyfileobj(src, dst)
except (zipfile.BadZipFile, OSError):
continue
try:
with open(stamp_path, "w", encoding="utf-8") as f:
f.write(sig)
except OSError:
pass
return dest
def _ensure_assets(version, progress=None, verify=False, cancel=None,
mirrors=None):
ai = version.asset_index
if not ai.get("url"):
return
assets = version.assets_dir()
idx_id = ai.get("id", "legacy")
idx_path = os.path.join(assets, "indexes", idx_id + ".json")
http.download_file(ai["url"], idx_path, ai.get("sha1"), ai.get("size"),
cancel=cancel)
with open(idx_path, encoding="utf-8") as f:
index = json.load(f)
base = ASSET_BASE
tasks = []
objects = index.get("objects") or {}
for name, obj in objects.items():
h = obj["hash"]
url = base + h[:2] + "/" + h
path = os.path.join(assets, "objects", h[:2], h)
tasks.append((_mirror_urls(mirrors, base, url), path, h,
obj.get("size")))
errors = _download_many(tasks, workers=ASSET_WORKERS, progress=progress,
verify=verify, cancel=cancel)
# Виртуальные ассеты для старых версий (1.6 и раньше: virtual / map_to_resources)
if index.get("virtual"):
vdir = os.path.join(assets, "virtual", idx_id)
for name, obj in objects.items():
src = os.path.join(assets, "objects", obj["hash"][:2], obj["hash"])
dst = os.path.join(vdir, *name.split("/"))
os.makedirs(os.path.dirname(dst), exist_ok=True)
if not os.path.isfile(dst):
try:
shutil.copy2(src, dst)
except OSError:
pass
if index.get("map_to_resources"):
rdir = os.path.join(version.game_dir, "resources")
for name, obj in objects.items():
src = os.path.join(assets, "objects", obj["hash"][:2], obj["hash"])
dst = os.path.join(rdir, *name.split("/"))
os.makedirs(os.path.dirname(dst), exist_ok=True)
if not os.path.isfile(dst):
try:
shutil.copy2(src, dst)
except OSError:
pass
return errors
def ensure(version, progress=None, asset_progress=None, log=None, verify=False,
cancel=None, asset_mirrors=None, lib_mirrors=None):
"""Скачивает всё необходимое для запуска. Возвращает (errors, natives_dir).
verify=True — полная проверка SHA-1 уже скачанных файлов
(иначе достаточно совпадения размера).
cancel — threading.Event: досрочная остановка (бросает http.Cancelled).
asset_mirrors / lib_mirrors — списки зеркал (пусто = официальные серверы).
"""
libs_dir = version.libraries_dir()
errors = []
lib_base = "https://libraries.minecraft.net/"
tasks = []
if version.client_url:
tasks.append(([version.client_url], version.client_jar_path(),
version.client_sha1, version.client_size))
for lib in version.libraries:
path = os.path.join(libs_dir, *lib["path"].split("/"))
tasks.append((_mirror_urls(lib_mirrors, lib_base, lib["url"]), path,
lib.get("sha1"), lib.get("size")))
if log:
log(f"Скачивание библиотек ({len(tasks)} шт.)...")
errors += _download_many(tasks, progress=progress, verify=verify,
cancel=cancel)
natives = extract_natives(version, libs_dir)
lf = version.logging.get("file") or {}
if lf.get("url") and lf.get("id"):
cfg_dir = os.path.join(version.assets_dir(), "log_configs")
try:
http.download_file(lf["url"], os.path.join(cfg_dir, lf["id"]),
lf.get("sha1"), lf.get("size"), cancel=cancel)
except http.Cancelled:
raise
except Exception as e: # noqa: BLE001
errors.append(f"log config: {e}")
if version.asset_index.get("url"):
if log:
log("Скачивание ассетов...")
errors += _ensure_assets(version, progress=asset_progress,
verify=verify, cancel=cancel,
mirrors=asset_mirrors) or []
return errors, natives