Офлайн-лаунчер Minecraft: аккаунты без Microsoft, установка версий и лоадеров (Fabric, Quilt, Forge, NeoForge), каталог модпаков Modrinth, поиск и обновление модов, разбор вылетов, системный трей, автозагрузка, плагины, прокси и зеркала.
112 lines
3.6 KiB
Python
112 lines
3.6 KiB
Python
"""Работа с манифестом версий Mojang (piston-meta)."""
|
|
|
|
import json
|
|
import os
|
|
import time
|
|
|
|
from . import http
|
|
|
|
MANIFEST_URL = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"
|
|
MANIFEST_MAX_AGE = 3600 # свежий кэш не перекачиваем (час)
|
|
|
|
_memory_cache = None
|
|
_skip_ttl_once = False
|
|
|
|
|
|
def _cache_dir(game_dir):
|
|
d = os.path.join(game_dir, ".launcher")
|
|
os.makedirs(d, exist_ok=True)
|
|
return d
|
|
|
|
|
|
def invalidate():
|
|
"""Сбросить кэш: следующий get_manifest скачает манифест заново."""
|
|
global _memory_cache, _skip_ttl_once
|
|
_memory_cache = None
|
|
_skip_ttl_once = True
|
|
|
|
|
|
def get_manifest(game_dir, net=True, max_age=MANIFEST_MAX_AGE):
|
|
"""Возвращает манифест версий.
|
|
|
|
Свежий кэш на диске используется сразу, без обращения к сети.
|
|
Сеть -> кэш на диске -> ошибка.
|
|
"""
|
|
global _memory_cache, _skip_ttl_once
|
|
if _memory_cache is not None:
|
|
return _memory_cache
|
|
|
|
cache = os.path.join(_cache_dir(game_dir), "version_manifest.json")
|
|
force = _skip_ttl_once
|
|
_skip_ttl_once = False
|
|
|
|
fresh = False
|
|
if os.path.isfile(cache):
|
|
try:
|
|
fresh = (time.time() - os.path.getmtime(cache)) < max_age
|
|
except OSError:
|
|
fresh = False
|
|
|
|
if net and (force or not fresh):
|
|
try:
|
|
data = json.loads(http.get_bytes(MANIFEST_URL, timeout=20))
|
|
with open(cache, "w", encoding="utf-8") as f:
|
|
json.dump(data, f)
|
|
_memory_cache = data
|
|
return data
|
|
except Exception:
|
|
pass
|
|
if os.path.isfile(cache):
|
|
with open(cache, encoding="utf-8") as f:
|
|
_memory_cache = json.load(f)
|
|
return _memory_cache
|
|
raise RuntimeError("Нет доступа к сети и нет кэша манифеста версий")
|
|
|
|
|
|
def version_url(manifest, vid):
|
|
for v in manifest.get("versions", []):
|
|
if v.get("id") == vid:
|
|
return v.get("url")
|
|
return None
|
|
|
|
|
|
def local_versions(game_dir):
|
|
"""Идентификаторы версий, установленных локально (Fabric, Quilt и т.п.)."""
|
|
root = os.path.join(game_dir, "versions")
|
|
out = []
|
|
try:
|
|
for name in sorted(os.listdir(root)):
|
|
if os.path.isfile(os.path.join(root, name, name + ".json")):
|
|
out.append(name)
|
|
except OSError:
|
|
pass
|
|
return out
|
|
|
|
|
|
def fetch_version_json(game_dir, vid, net=True, max_age=21600):
|
|
"""JSON конкретной версии: свежий кэш -> сеть -> versions/{id}/{id}.json."""
|
|
path = os.path.join(game_dir, "versions", vid, vid + ".json")
|
|
fresh = False
|
|
if os.path.isfile(path):
|
|
try:
|
|
fresh = (time.time() - os.path.getmtime(path)) < max_age
|
|
except OSError:
|
|
fresh = False
|
|
if net and not fresh:
|
|
try:
|
|
manifest = get_manifest(game_dir, net=True)
|
|
url = version_url(manifest, vid)
|
|
if not url:
|
|
raise RuntimeError(f"Версия {vid} не найдена в манифесте")
|
|
raw = http.get_bytes(url, timeout=20)
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
with open(path, "wb") as f:
|
|
f.write(raw)
|
|
return json.loads(raw)
|
|
except Exception:
|
|
pass
|
|
if os.path.isfile(path):
|
|
with open(path, encoding="utf-8") as f:
|
|
return json.load(f)
|
|
raise RuntimeError(f"Не удалось получить JSON версии {vid}")
|