Офлайн-лаунчер Minecraft: аккаунты без Microsoft, установка версий и лоадеров (Fabric, Quilt, Forge, NeoForge), каталог модпаков Modrinth, поиск и обновление модов, разбор вылетов, системный трей, автозагрузка, плагины, прокси и зеркала.
100 lines
3.3 KiB
Python
100 lines
3.3 KiB
Python
"""Запуск игры (offline-mode)."""
|
|
|
|
import hashlib
|
|
import os
|
|
import subprocess
|
|
import threading
|
|
import time
|
|
|
|
from .i18n import T
|
|
|
|
CREATE_NO_WINDOW = 0x08000000 if os.name == "nt" else 0
|
|
|
|
|
|
def offline_uuid(username):
|
|
"""UUID, который Mojang генерирует для оффлайн-игроков."""
|
|
h = hashlib.md5(("OfflinePlayer:" + username).encode("utf-8")).digest()
|
|
b = bytearray(h)
|
|
b[6] = (b[6] & 0x0F) | 0x30 # version 3
|
|
b[8] = (b[8] & 0x3F) | 0x80 # RFC 4122 variant
|
|
return "".join(f"{x:02x}" for x in b)
|
|
|
|
|
|
def launch(game_dir, cmd, log=None, log_path=None, on_line=None, on_exit=None):
|
|
"""Запускает процесс игры. Возвращает Popen.
|
|
|
|
log_path — файл для полного вывода игры;
|
|
on_line — вызывается на каждую строку вывода (живой лог);
|
|
on_exit — вызывается после завершения процесса (после чтения вывода).
|
|
"""
|
|
if log:
|
|
log("Команда: " + " ".join(cmd))
|
|
|
|
stream = on_line is not None or log_path is not None
|
|
f = None
|
|
if log_path:
|
|
try:
|
|
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
|
f = open(log_path, "ab")
|
|
f.write(("\n=== PixelLauncher: " +
|
|
time.strftime("%Y-%m-%d %H:%M:%S") + " ===\n" +
|
|
"Команда: " + " ".join(cmd) + "\n").encode("utf-8",
|
|
"replace"))
|
|
except OSError:
|
|
f = None
|
|
|
|
proc = subprocess.Popen(
|
|
cmd,
|
|
cwd=game_dir,
|
|
stdout=subprocess.PIPE if stream else subprocess.DEVNULL,
|
|
stderr=subprocess.STDOUT if stream else subprocess.DEVNULL,
|
|
text=True, encoding="utf-8", errors="replace", bufsize=1,
|
|
creationflags=CREATE_NO_WINDOW,
|
|
)
|
|
|
|
def _pump():
|
|
"""Читает вывод игры: пишет в файл и отдаёт строки построчно."""
|
|
try:
|
|
for line in proc.stdout:
|
|
if f is not None:
|
|
try:
|
|
f.write(line.encode("utf-8", "replace"))
|
|
except ValueError:
|
|
pass
|
|
if on_line:
|
|
try:
|
|
on_line(line.rstrip("\r\n"))
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
except (ValueError, OSError):
|
|
pass
|
|
finally:
|
|
if f is not None:
|
|
try:
|
|
f.close()
|
|
except OSError:
|
|
pass
|
|
|
|
pump = None
|
|
if stream:
|
|
pump = threading.Thread(target=_pump, daemon=True)
|
|
pump.start()
|
|
elif f is not None:
|
|
f.close()
|
|
|
|
def _wait():
|
|
code = proc.wait()
|
|
if pump is not None: # дать выводу дочитаться
|
|
pump.join(timeout=10)
|
|
if on_exit:
|
|
try:
|
|
on_exit(code)
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
|
|
threading.Thread(target=_wait, daemon=True).start()
|
|
|
|
if log:
|
|
log(T("Игра запущена, PID ") + str(proc.pid) + ". " + T("Приятной игры!"))
|
|
return proc
|