270 lines
11 KiB
Python
270 lines
11 KiB
Python
"""Разбор 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)
|