Files
PixelLauncher/launcher/version.py
T

270 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Разбор JSON версии Minecraft: библиотеки, правила, аргументы, classpath."""
import os
import re
import shlex
import sys
from . import manifest as manifest_mod
LIB_BASE = "https://libraries.minecraft.net/"
def os_name():
if sys.platform.startswith("win"):
return "windows"
if sys.platform == "darwin":
return "osx"
return "linux"
def _arch():
import struct
return "x86_64" if struct.calcsize("P") * 8 == 64 else "x86"
def _rule_matches(osblock, cur_os, cur_arch, os_version):
if not osblock:
return True
name = osblock.get("name")
if name and name != cur_os:
return False
arch = osblock.get("arch")
if arch and arch != cur_arch:
return False
ver = osblock.get("version")
if ver and cur_os == "osx":
try:
if not re.search(ver, os_version):
return False
except re.error:
pass
return True
def rules_allow(rules, cur_os=None, cur_arch=None, os_version=""):
"""Правила Mojang: без правил — разрешено; иначе последнее подходящее."""
if not rules:
return True
cur_os = cur_os or os_name()
cur_arch = cur_arch or _arch()
allowed = False
for rule in rules:
if _rule_matches(rule.get("os"), cur_os, cur_arch, os_version):
if rule.get("features"): # demo и пр. — не поддерживаем
continue
allowed = rule.get("action") == "allow"
return allowed
def maven_to_path(name):
"""org.lwjgl:lwjgl:2.9.4-nightly -> org/lwjgl/lwjgl/2.9.4-nightly/lwjgl-2.9.4-nightly.jar"""
parts = name.split(":")
if len(parts) < 3:
return ""
group, artifact, version = parts[0], parts[1], parts[2]
classifier = parts[3] if len(parts) > 3 else None
fname = f"{artifact}-{version}" + (f"-{classifier}" if classifier else "") + ".jar"
return "/".join(group.split(".") + [artifact, version, fname])
def _flatten_args(items, cur_os):
out = []
for it in items or []:
if isinstance(it, str):
out.append(it)
elif isinstance(it, dict) and rules_allow(it.get("rules"), cur_os):
v = it.get("value")
if isinstance(v, list):
out.extend(v)
elif v:
out.append(v)
return out
class Version:
def __init__(self, game_dir, data):
self.game_dir = game_dir
self.id = data.get("id", "unknown")
self.type = data.get("type", "release")
self.main_class = data.get("mainClass", "")
self.java_major = (data.get("javaVersion") or {}).get("majorVersion", 8)
self.asset_index = data.get("assetIndex") or {}
self.logging = (data.get("logging") or {}).get("client") or {}
client = (data.get("downloads") or {}).get("client") or {}
self.client_url = client.get("url")
self.client_sha1 = client.get("sha1")
self.client_size = client.get("size")
cur_os = os_name()
self.libraries = [] # {"path", "url", "sha1", "size"} — всё что скачать
self.natives_jars = [] # пути (rel) к jar'ам с нативами
self.classpath = [] # rel-пути для -cp (включая natives-jar'ы)
self._parse_libraries(data.get("libraries") or [], cur_os)
if "arguments" in data:
self.jvm_args = _flatten_args((data.get("arguments") or {}).get("jvm"), cur_os)
self.game_args = _flatten_args((data.get("arguments") or {}).get("game"), cur_os)
else:
self.jvm_args = ["-Djava.library.path=${natives_directory}", "-cp", "${classpath}"]
legacy = data.get("minecraftArguments") or ""
self.game_args = shlex.split(legacy) if legacy else []
# ------------------------------------------------------------------
def _parse_libraries(self, libs, cur_os):
seen_paths = set()
for lib in libs:
if not rules_allow(lib.get("rules"), cur_os):
continue
name = lib.get("name", "")
dl = lib.get("downloads") or {}
artifact = dl.get("artifact")
if artifact and artifact.get("path"):
self._add_lib(artifact["path"],
artifact.get("url") or LIB_BASE + artifact["path"],
artifact.get("sha1"), seen_paths,
artifact.get("size"))
elif name and not lib.get("natives"):
# Старый стиль (без downloads): собираем путь из maven-имени
rel = maven_to_path(name)
if rel:
base = lib.get("url") or LIB_BASE
self._add_lib(rel, base + rel, None, seen_paths)
nats = lib.get("natives") or {}
cls_key = nats.get(cur_os)
if cls_key:
cls_key = cls_key.replace("${arch}", "64" if _arch() == "x86_64" else "32")
classifiers = dl.get("classifiers") or {}
cj = classifiers.get(cls_key)
if cj and cj.get("path"):
self._add_lib(cj["path"],
cj.get("url") or LIB_BASE + cj["path"],
cj.get("sha1"), seen_paths, cj.get("size"))
self.natives_jars.append(cj["path"])
else:
# Нет downloads.classifiers — собираем путь вручную
if name and cls_key:
parts = name.split(":")
if len(parts) == 3:
rel = maven_to_path(name + ":" + cls_key)
if rel:
self._add_lib(rel, LIB_BASE + rel, None, seen_paths)
self.natives_jars.append(rel)
def _add_lib(self, rel, url, sha1, seen, size=None):
if rel in seen:
return
seen.add(rel)
self.libraries.append({"path": rel, "url": url, "sha1": sha1,
"size": size})
self.classpath.append(rel)
# ------------------------------------------------------------------
def libraries_dir(self):
return os.path.join(self.game_dir, "libraries")
def natives_dir(self):
return os.path.join(self.game_dir, "versions", self.id, "natives-" + os_name())
def client_jar_path(self):
return os.path.join(self.game_dir, "versions", self.id, self.id + ".jar")
def assets_dir(self):
return os.path.join(self.game_dir, "assets")
def classpath_str(self):
libs = self.libraries_dir()
entries = [os.path.join(libs, *p.split("/")) for p in self.classpath]
entries.append(self.client_jar_path())
return os.pathsep.join(entries)
# ------------------------------------------------------------------
def build_command(self, java, username, uuid_hex, ram_mb, extra_jvm="",
game_directory=None):
# Общие ресурсы (libraries/assets/versions) берём из self.game_dir,
# а каталог игры (mods/saves/config) можно переопределить — так
# работают инстансы: общий движок, разные папки контента.
gd = self.game_dir
subst = {
"auth_player_name": username,
"version_name": self.id,
"game_directory": game_directory or gd,
"assets_root": self.assets_dir(),
"assets_index_name": self.asset_index.get("id", ""),
"auth_uuid": uuid_hex,
"auth_access_token": "0",
"auth_session": "0",
"user_type": "msa",
"version_type": self.type,
"natives_directory": self.natives_dir(),
"launcher_name": "PixelLauncher",
"launcher_version": sys.modules[__package__].LAUNCHER_VERSION,
"classpath": self.classpath_str(),
"classpath_separator": os.pathsep,
"library_directory": self.libraries_dir(),
"user_properties": "{}",
"clientid": "0",
"auth_xuid": "0",
"game_assets": os.path.join(self.assets_dir(), "virtual",
self.asset_index.get("id", "")),
"auth_local_player_name": username,
}
def sub(s):
return re.sub(r"\$\{(\w+)\}", lambda m: subst.get(m.group(1), m.group(0)), s)
cmd = [java]
cmd.append(f"-Xmx{ram_mb}M")
if ram_mb >= 1024:
cmd.append(f"-Xms{min(512, ram_mb // 2)}M")
if self.logging.get("file", {}).get("id") and self.logging.get("argument"):
cfg = os.path.join(self.assets_dir(), "log_configs",
self.logging["file"]["id"])
if os.path.isfile(cfg):
cmd.append(self.logging["argument"].replace("${path}", cfg))
cmd.append("-Dminecraft.launcher.brand=PixelLauncher")
if extra_jvm:
cmd.extend(shlex.split(extra_jvm))
cmd.extend(sub(a) for a in self.jvm_args)
cmd.append(self.main_class)
cmd.extend(sub(a) for a in self.game_args)
return cmd
def load(game_dir, vid, net=True):
"""Загружает версию, учитывая цепочку inheritsFrom."""
chain = []
cur = vid
seen = set()
while cur and cur not in seen:
seen.add(cur)
data = manifest_mod.fetch_version_json(game_dir, cur, net=net)
chain.append(data)
cur = data.get("inheritsFrom")
# Родитель -> ребёнок: поля ребёнка перекрывают родительские
merged = {}
libs = []
for data in reversed(chain):
for k, v in data.items():
if k == "libraries":
continue
merged[k] = v
# Библиотеки: сначала родительские, потом свои (ребёнка) — но чтобы
# библиотеки ребёнка имели приоритет, добавляем их первыми при итерации
# от ребёнка к родителю, см. ниже.
libs.append(data.get("libraries") or [])
# Собираем: сначала все библиотеки ребёнка (последняя группа), затем родителей
ordered = []
for group in reversed(libs):
ordered.extend(group)
merged["libraries"] = ordered
merged["id"] = vid
return Version(game_dir, merged)