PixelLauncher 2.1.0 — стабильная сборка
Офлайн-лаунчер Minecraft: аккаунты без Microsoft, установка версий и лоадеров (Fabric, Quilt, Forge, NeoForge), каталог модпаков Modrinth, поиск и обновление модов, разбор вылетов, системный трей, автозагрузка, плагины, прокси и зеркала.
This commit is contained in:
commit
ec6f13ddcc
33 files changed
+10445
No files matched your search
@@ -0,0 +1,265 @@
|
||||
"""Разбор 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=""):
|
||||
gd = self.game_dir
|
||||
subst = {
|
||||
"auth_player_name": username,
|
||||
"version_name": self.id,
|
||||
"game_directory": 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": "2.1.0",
|
||||
"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)
|
||||
Reference in new issue
Block a user