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

199 lines
6.9 KiB
Python

"""Установка модлоадеров Fabric и Quilt.
Ставит JSON профиля версии (с inheritsFrom на игровую версию) — лаунчер уже
умеет собирать classpath для таких версий, а файлы лоадера докачиваются
обычным механизмом при первом запуске.
"""
import json
import os
from . import http
LOADERS = ("fabric", "quilt")
META = {
"fabric": "https://meta.fabricmc.net/v2",
"quilt": "https://meta.quiltmc.org/v3",
}
_cache = {} # loader -> {"game": [(id, stable)], "loader": [ver]}
def _base(loader):
return META.get(loader) or META["fabric"]
def game_versions(loader):
"""[(id, stable)] игровых версий, поддерживаемых лоадером (свежие первые)."""
if loader not in _cache:
data = json.loads(http.get_bytes(_base(loader) + "/versions/game",
timeout=20))
games, loads = [], []
for v in data or []:
vid = v.get("version")
if vid:
games.append((vid, bool(v.get("stable"))))
_cache[loader] = {"game": games, "loader": loads}
return _cache[loader]["game"]
def loader_versions(loader):
"""Список версий лоадера (свежая первая)."""
if loader not in _cache:
game_versions(loader) # прогревает кэш
if not _cache[loader]["loader"]:
data = json.loads(http.get_bytes(_base(loader) + "/versions/loader",
timeout=20))
_cache[loader]["loader"] = [v["version"] for v in (data or [])
if v.get("version")]
return _cache[loader]["loader"]
def install(game_dir, loader, game_version, loader_version=None):
"""Скачивает JSON профиля лоадера. Возвращает id установленной версии."""
base = _base(loader)
if not loader_version:
vs = loader_versions(loader)
if not vs:
raise RuntimeError(f"нет версий лоадера {loader}")
loader_version = vs[0]
url = (f"{base}/versions/loader/{game_version}/{loader_version}/"
"profile/json")
raw = http.get_bytes(url, timeout=30)
data = json.loads(raw)
vid = data.get("id") or f"{loader}-loader-{loader_version}-{game_version}"
path = os.path.join(game_dir, "versions", vid, vid + ".json")
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as f:
f.write(raw)
return vid
# ------------------------------------------------------- менеджер модов (UI)
def mods_dir(game_dir):
d = os.path.join(game_dir, "mods")
os.makedirs(d, exist_ok=True)
return d
def list_mods(game_dir):
"""Файлы в mods/: [{file, name, enabled, size}] (имя без .jar/.disabled)."""
out = []
d = mods_dir(game_dir)
for name in sorted(os.listdir(d), key=str.lower):
low = name.lower()
if low.endswith(".jar.disabled"):
enabled, base = False, name[:-len(".disabled")]
elif low.endswith(".jar"):
enabled, base = True, name[:-4]
else:
continue
try:
size = os.path.getsize(os.path.join(d, name))
except OSError:
size = 0
out.append({"file": name, "name": base, "enabled": enabled,
"size": size})
return out
def set_enabled(game_dir, filename, on):
"""Вкл/выкл мода переименованием (.jar ↔ .jar.disabled). Новое имя/None."""
d = mods_dir(game_dir)
low = filename.lower()
if on:
if not low.endswith(".jar.disabled"):
return None
dst_name = filename[:-len(".disabled")]
else:
if not low.endswith(".jar"):
return None
dst_name = filename + ".disabled"
if os.path.exists(os.path.join(d, dst_name)):
return None
os.rename(os.path.join(d, filename), os.path.join(d, dst_name))
return dst_name
def remove_mod(game_dir, filename):
"""Удаляет файл мода. True при успехе."""
try:
os.remove(os.path.join(mods_dir(game_dir), filename))
return True
except OSError:
return False
def fmt_size(n):
"""3145728 → «3,0 МБ»."""
n = float(n or 0)
for unit in ("Б", "КБ", "МБ", "ГБ"):
if n < 1024 or unit == "ГБ":
return f"{n:.1f} {unit}".replace(".", ",") if unit != "Б" \
else f"{int(n)} {unit}"
n /= 1024
return f"{int(n)} Б"
# ------------------------------------------------------- обновления модов
def check_updates(game_dir, game_version, loader="", log=None):
"""Сверяет включённые моды с Modrinth по SHA-1.
Возвращает список обновлений:
[{file, name, cur, new_ver, new_file, url, size, sha1, project_id}].
"""
from . import modrinth
enabled = [m for m in list_mods(game_dir) if m["enabled"]]
if not enabled:
return []
if log:
log(f"Проверка {len(enabled)} модов по хэшу…")
hashes = {}
for m in enabled:
try:
h = http.sha1_file(os.path.join(mods_dir(game_dir), m["file"]))
except OSError:
continue
if h:
hashes[h] = m
if not hashes:
return []
found = modrinth.lookup_files(list(hashes), "sha1")
out = []
ver_cache = {}
for h, ver in found.items():
m = hashes.get(h)
pid = ver.get("project_id") or ""
if not m or not pid:
continue
if pid not in ver_cache:
try:
ver_cache[pid] = modrinth.versions(pid, game_version, loader)
except Exception: # noqa: BLE001
ver_cache[pid] = []
vs = ver_cache[pid]
# свежая версия первая: если её хэш совпадает с файлом — мы актуальны
if not vs or not vs[0]["sha1"] or vs[0]["sha1"] == h:
continue
nv = vs[0]
out.append({"file": m["file"], "name": m["name"],
"cur": (ver.get("version_number") or "").strip(),
"new_ver": nv["name"], "new_file": nv["filename"],
"url": nv["url"], "size": nv["size"],
"sha1": nv["sha1"], "project_id": pid})
out.sort(key=lambda u: u["name"].lower())
return out
def apply_update(game_dir, u):
"""Скачивает новую версию мода и отключает старый файл. True при успехе."""
dest = os.path.join(mods_dir(game_dir), u["new_file"])
ok = http.download_file(u["url"], dest, size=u["size"])
if not ok:
return False
if u["file"] != u["new_file"]:
set_enabled(game_dir, u["file"], False)
return True