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

+132
View File
@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
"""История запусков и статистика игры.
Всё хранится в .launcher/stats.json рядом с кэшем Java:
plays — {версия: {count, seconds, last, crashes}}
launches — последние запуски (кап 300): {ts, ver, ok, code, seconds}
"""
import json
import os
import threading
import time
_FILE = None # путь к stats.json (задаётся set_path)
_LOCK = threading.Lock()
_MAX_LAUNCHES = 300
def set_path(launcher_dir):
"""Каталог .launcher — тут лежат stats.json и java_cache.json."""
global _FILE
_FILE = os.path.join(str(launcher_dir), "stats.json")
def _load():
try:
with open(_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, dict):
raise ValueError
return data
except (OSError, ValueError):
return {"plays": {}, "launches": []}
def _save(data):
tmp = _FILE + ".tmp"
try:
with open(tmp, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False)
os.replace(tmp, _FILE)
except OSError:
pass
def record_start(version):
"""Вызывается при запуске игры: +1 к счётчику запусков версии."""
if not _FILE:
return
with _LOCK:
data = _load()
plays = data.setdefault("plays", {})
rec = plays.setdefault(version, {"count": 0, "seconds": 0,
"last": 0.0, "crashes": 0})
rec["count"] += 1
rec["last"] = time.time()
_save(data)
def record_finish(version, seconds, ok, code):
"""Вызывается при завершении игры: длительность, код, вылет."""
if not _FILE:
return
with _LOCK:
data = _load()
plays = data.setdefault("plays", {})
rec = plays.setdefault(version, {"count": 0, "seconds": 0,
"last": 0.0, "crashes": 0})
rec["seconds"] = max(0, int(seconds))
if not ok:
rec["crashes"] = rec.get("crashes", 0) + 1
launches = data.setdefault("launches", [])
launches.append({"ts": time.time(), "ver": version, "ok": bool(ok),
"code": code, "seconds": max(0, int(seconds))})
del launches[:-_MAX_LAUNCHES]
_save(data)
def totals():
"""Агрегаты для окна статистики."""
if not _FILE:
return {"total_seconds": 0, "total_launches": 0,
"per_version": [], "recent": []}
with _LOCK:
data = _load()
plays = data.get("plays") or {}
per = []
total_s = 0
for ver, rec in plays.items():
sec = int(rec.get("seconds") or 0)
total_s += sec
per.append({"ver": ver, "count": int(rec.get("count") or 0),
"seconds": sec, "last": float(rec.get("last") or 0),
"crashes": int(rec.get("crashes") or 0)})
per.sort(key=lambda r: r["last"], reverse=True)
recent = list(data.get("launches") or [])[-20:]
recent.reverse()
return {"total_seconds": total_s,
"total_launches": sum(r["count"] for r in per),
"per_version": per, "recent": recent}
def fmt_duration(seconds):
"""1800 → «30 мин»; 30 → «30 с»; 3700 → «1 ч 1 мин»."""
seconds = int(seconds or 0)
if seconds < 60:
return f"{seconds} с"
if seconds < 3600:
return f"{seconds // 60} мин"
h, m = seconds // 3600, (seconds % 3600) // 60
return f"{h} ч {m} мин" if m else f"{h} ч"
def fmt_time(ts):
"""Unix-время → «дд.мм.гггг чч:мм»."""
if not ts:
return "—"
try:
return time.strftime("%d.%m.%Y %H:%M", time.localtime(ts))
except (ValueError, OSError):
return "—"
def clear():
"""Полная очистка истории и статистики."""
if not _FILE:
return
with _LOCK:
try:
os.remove(_FILE)
except OSError:
pass