PixelLauncher 2.1.0 — стабильная сборка

Офлайн-лаунчер Minecraft: аккаунты без Microsoft, установка версий и лоадеров
(Fabric, Quilt, Forge, NeoForge), каталог модпаков Modrinth, поиск и обновление
модов, разбор вылетов, системный трей, автозагрузка, плагины, прокси и зеркала.
This commit is contained in:
kosta2014p committed 2026-10-18 22:44:19 +07:00
commit ec6f13ddcc
33 files changed
+10445

No files matched your search

+265
View File
@@ -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)