"""Разбор 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)