"""Установка модлоадеров 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