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,198 @@
|
||||
"""Установка модлоадеров 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
|
||||
Reference in new issue
Block a user