PixelLauncher 2.1.3 — инстансы профилей, старт развёрнутым, аккуратные иконки сборок
This commit is contained in:
1 parent
39cf752b7b
commit
a04a586038
13 files changed
+1187
-87
No files matched your search
+3
-3
@@ -1,11 +1,11 @@
|
|||||||
; PixelLauncher — Inno Setup 6 script
|
; PixelLauncher — Inno Setup 6 script
|
||||||
; Сборка: python make_installer.py (или: ISCC.exe installer.iss)
|
; Сборка: python make_installer.py (или: ISCC.exe installer.iss)
|
||||||
; Результат: PixelLauncherSetup2.1.2.exe в папке проекта.
|
; Результат: PixelLauncherSetup2.1.3.exe в папке проекта.
|
||||||
; AppId должен оставаться неизменным, иначе обновление появится
|
; AppId должен оставаться неизменным, иначе обновление появится
|
||||||
; как отдельная программа.
|
; как отдельная программа.
|
||||||
|
|
||||||
#define MyAppName "PixelLauncher"
|
#define MyAppName "PixelLauncher"
|
||||||
#define MyAppVersion "2.1.2"
|
#define MyAppVersion "2.1.3"
|
||||||
#define MyAppPublisher "SuperBoys"
|
#define MyAppPublisher "SuperBoys"
|
||||||
#define MyAppURL "https://pixellauncher.ru"
|
#define MyAppURL "https://pixellauncher.ru"
|
||||||
#define StageDir "build\stage"
|
#define StageDir "build\stage"
|
||||||
@@ -32,7 +32,7 @@ ArchitecturesAllowed=x64compatible
|
|||||||
SetupIconFile={#MyIcon}
|
SetupIconFile={#MyIcon}
|
||||||
UninstallDisplayIcon={app}\launcher.ico
|
UninstallDisplayIcon={app}\launcher.ico
|
||||||
UninstallDisplayName={#MyAppName}
|
UninstallDisplayName={#MyAppName}
|
||||||
VersionInfoVersion=2.1.2.0
|
VersionInfoVersion=2.1.3.0
|
||||||
VersionInfoProductVersion={#MyAppVersion}
|
VersionInfoProductVersion={#MyAppVersion}
|
||||||
LZMANumBlockThreads=2
|
LZMANumBlockThreads=2
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""PixelLauncher — lightweight Minecraft launcher (offline mode)."""
|
"""PixelLauncher — lightweight Minecraft launcher (offline mode)."""
|
||||||
|
|
||||||
LAUNCHER_NAME = "PixelLauncher"
|
LAUNCHER_NAME = "PixelLauncher"
|
||||||
LAUNCHER_VERSION = "2.1.2"
|
LAUNCHER_VERSION = "2.1.3"
|
||||||
+7
-5
@@ -32,10 +32,12 @@ def _split_loader(lid):
|
|||||||
def read_manifest(path):
|
def read_manifest(path):
|
||||||
"""Читает manifest.json. Возвращает (данные, mc, loader, loader_ver)."""
|
"""Читает manifest.json. Возвращает (данные, mc, loader, loader_ver)."""
|
||||||
with zipfile.ZipFile(path) as z:
|
with zipfile.ZipFile(path) as z:
|
||||||
names = set(z.namelist())
|
return _read_manifest(z)
|
||||||
if "manifest.json" not in names:
|
|
||||||
raise RuntimeError("в архиве нет manifest.json")
|
|
||||||
with z.open("manifest.json") as f:
|
def _read_manifest(zf):
|
||||||
|
"""То же, но из уже открытого архива."""
|
||||||
|
with zf.open("manifest.json") as f:
|
||||||
data = json.loads(f.read().decode("utf-8"))
|
data = json.loads(f.read().decode("utf-8"))
|
||||||
minecraft = data.get("minecraft") or {}
|
minecraft = data.get("minecraft") or {}
|
||||||
mc = minecraft.get("version") or ""
|
mc = minecraft.get("version") or ""
|
||||||
@@ -123,7 +125,7 @@ def install(game_dir, pack_path, log=None, progress=None, cancel=None):
|
|||||||
with zipfile.ZipFile(pack_path) as zf:
|
with zipfile.ZipFile(pack_path) as zf:
|
||||||
names = set(zf.namelist())
|
names = set(zf.namelist())
|
||||||
if "manifest.json" in names:
|
if "manifest.json" in names:
|
||||||
data, mc, loader, loader_ver = read_manifest(pack_path)
|
data, mc, loader, loader_ver = _read_manifest(zf)
|
||||||
mod_list = []
|
mod_list = []
|
||||||
for entry in (data.get("files") or []):
|
for entry in (data.get("files") or []):
|
||||||
if entry.get("required") is False:
|
if entry.get("required") is False:
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ DEFAULTS = {
|
|||||||
# зеркала скачивания (пустые списки = официальные серверы Mojang)
|
# зеркала скачивания (пустые списки = официальные серверы Mojang)
|
||||||
"asset_mirrors": [],
|
"asset_mirrors": [],
|
||||||
"lib_mirrors": [],
|
"lib_mirrors": [],
|
||||||
|
# ключ CurseForge API (необязателен: есть зеркало без ключа)
|
||||||
|
"cf_api_key": "",
|
||||||
# третья волна
|
# третья волна
|
||||||
"fav_versions": [], # закреплённые версии (id)
|
"fav_versions": [], # закреплённые версии (id)
|
||||||
"profiles": [], # профили запуска (см. ui._open_profiles)
|
"profiles": [], # профили запуска (см. ui._open_profiles)
|
||||||
@@ -87,6 +89,8 @@ def _migrate(s):
|
|||||||
s[key] = []
|
s[key] = []
|
||||||
if not isinstance(s.get("accent"), str) or not s["accent"].startswith("#"):
|
if not isinstance(s.get("accent"), str) or not s["accent"].startswith("#"):
|
||||||
s["accent"] = DEFAULTS["accent"]
|
s["accent"] = DEFAULTS["accent"]
|
||||||
|
if not isinstance(s.get("cf_api_key"), str):
|
||||||
|
s["cf_api_key"] = ""
|
||||||
if s.get("theme") not in ("dark", "light"):
|
if s.get("theme") not in ("dark", "light"):
|
||||||
s["theme"] = "dark"
|
s["theme"] = "dark"
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -13,7 +13,9 @@ import struct
|
|||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
USER_AGENT = "PixelLauncher/2.1.2 (lightweight Minecraft launcher)"
|
from . import LAUNCHER_VERSION
|
||||||
|
|
||||||
|
USER_AGENT = "PixelLauncher/%s (lightweight Minecraft launcher)" % LAUNCHER_VERSION
|
||||||
ATTEMPTS = 4
|
ATTEMPTS = 4
|
||||||
|
|
||||||
_opener = None # настроенный opener (прокси) или None — напрямую в сеть
|
_opener = None # настроенный opener (прокси) или None — напрямую в сеть
|
||||||
|
|||||||
@@ -492,6 +492,9 @@ _TABLE = {
|
|||||||
"Загрузка…": "Loading…",
|
"Загрузка…": "Loading…",
|
||||||
"КБ": "KB",
|
"КБ": "KB",
|
||||||
"Б": "B",
|
"Б": "B",
|
||||||
|
"Ключ CurseForge API (необязательно):": "CurseForge API key (optional):",
|
||||||
|
"Без ключа сборки берутся с зеркала CurseForge.":
|
||||||
|
"Without a key, modpacks come from the CurseForge mirror.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,293 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Инстансы (как Prism Launcher): отдельная папка на каждый профиль.
|
||||||
|
|
||||||
|
Инстанс = папка instances/<name>/ со своим .minecraft-подобным содержимым
|
||||||
|
(mods, saves, resourcepacks, shaderpacks, config, options.txt и пр.)
|
||||||
|
плюс instance.json с метаданными (имя, версия, лоадер, Java, ОЗУ, иконка).
|
||||||
|
|
||||||
|
Общие ресурсы (versions, assets, libraries) хранятся в общем .minecraft
|
||||||
|
и переиспользуются всеми инстансами — экономия места.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
|
|
||||||
|
from . import config
|
||||||
|
|
||||||
|
INSTANCES_DIR = os.path.join(config.APP_DIR, "instances")
|
||||||
|
|
||||||
|
# Подпапки инстанса (как в Prism)
|
||||||
|
INSTANCE_DIRS = ("mods", "saves", "resourcepacks", "shaderpacks", "config",
|
||||||
|
"screenshots", "datapacks")
|
||||||
|
|
||||||
|
# Метаданные по умолчанию
|
||||||
|
META_DEFAULTS = {
|
||||||
|
"name": "",
|
||||||
|
"version": "", # id версии MC (например "1.21.4")
|
||||||
|
"loader": "", # "vanilla" | "fabric" | "forge" | "neoforge" | "quilt"
|
||||||
|
"java_path": "", # пусто = авто
|
||||||
|
"ram": 2048, # 0 = авто
|
||||||
|
"jvm_args": "",
|
||||||
|
"win_w": 0,
|
||||||
|
"win_h": 0,
|
||||||
|
"fullscreen": False,
|
||||||
|
"icon": "", # путь к PNG-иконке (пусто = дефолт)
|
||||||
|
"last_played": 0, # timestamp
|
||||||
|
"created": 0,
|
||||||
|
"notes": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _meta_path(inst_dir):
|
||||||
|
return os.path.join(inst_dir, "instance.json")
|
||||||
|
|
||||||
|
|
||||||
|
def _game_dir(inst_dir):
|
||||||
|
return os.path.join(inst_dir, ".minecraft")
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_dirs(inst_dir):
|
||||||
|
"""Создаёт структуру папок инстанса."""
|
||||||
|
os.makedirs(inst_dir, exist_ok=True)
|
||||||
|
gd = _game_dir(inst_dir)
|
||||||
|
os.makedirs(gd, exist_ok=True)
|
||||||
|
for sub in INSTANCE_DIRS:
|
||||||
|
os.makedirs(os.path.join(gd, sub), exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _sanitize_name(name):
|
||||||
|
"""Делает имя безопасным для пути."""
|
||||||
|
bad = '<>:"/\\|?*'
|
||||||
|
out = "".join("_" if c in bad else c for c in name).strip()
|
||||||
|
out = out.rstrip(". ")
|
||||||
|
return out or "Instance"
|
||||||
|
|
||||||
|
|
||||||
|
def create(name, version="", loader="vanilla", **overrides):
|
||||||
|
"""Создаёт новый инстанс. Возвращает путь к его папке."""
|
||||||
|
name = _sanitize_name(name)
|
||||||
|
inst_dir = os.path.join(INSTANCES_DIR, name)
|
||||||
|
# если уже есть — добавляем суффикс
|
||||||
|
n = 1
|
||||||
|
base = name
|
||||||
|
while os.path.exists(inst_dir):
|
||||||
|
n += 1
|
||||||
|
name = f"{base} ({n})"
|
||||||
|
inst_dir = os.path.join(INSTANCES_DIR, name)
|
||||||
|
_ensure_dirs(inst_dir)
|
||||||
|
|
||||||
|
meta = dict(META_DEFAULTS)
|
||||||
|
meta["name"] = name
|
||||||
|
meta["version"] = version
|
||||||
|
meta["loader"] = loader
|
||||||
|
meta["created"] = int(time.time())
|
||||||
|
meta.update(overrides)
|
||||||
|
save_meta(inst_dir, meta)
|
||||||
|
return inst_dir
|
||||||
|
|
||||||
|
|
||||||
|
def load_meta(inst_dir):
|
||||||
|
"""Читает instance.json (или дефолт, если нет/битый)."""
|
||||||
|
try:
|
||||||
|
with open(_meta_path(inst_dir), encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
data = {}
|
||||||
|
meta = dict(META_DEFAULTS)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
meta.update(data)
|
||||||
|
if not meta.get("name"):
|
||||||
|
meta["name"] = os.path.basename(inst_dir)
|
||||||
|
return meta
|
||||||
|
|
||||||
|
|
||||||
|
def save_meta(inst_dir, meta):
|
||||||
|
"""Записывает instance.json."""
|
||||||
|
try:
|
||||||
|
with open(_meta_path(inst_dir), "w", encoding="utf-8") as f:
|
||||||
|
json.dump(meta, f, indent=2, ensure_ascii=False)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def list_instances():
|
||||||
|
"""Список всех инстансов: [{dir, meta}, ...] отсортированный по имени."""
|
||||||
|
if not os.path.isdir(INSTANCES_DIR):
|
||||||
|
return []
|
||||||
|
out = []
|
||||||
|
for name in sorted(os.listdir(INSTANCES_DIR)):
|
||||||
|
d = os.path.join(INSTANCES_DIR, name)
|
||||||
|
if not os.path.isdir(d):
|
||||||
|
continue
|
||||||
|
if not os.path.isfile(_meta_path(d)):
|
||||||
|
# возможно, это старый .minecraft без instance.json — пропускаем
|
||||||
|
continue
|
||||||
|
out.append({"dir": d, "meta": load_meta(d)})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def game_dir(inst_dir):
|
||||||
|
"""Путь к .minecraft внутри инстанса."""
|
||||||
|
return _game_dir(inst_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def shared_dir():
|
||||||
|
"""Общий каталог (versions/assets/libraries) — тот же, что и раньше."""
|
||||||
|
return config.load_settings().get("game_dir", config.DEFAULT_GAME_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
def delete(inst_dir, keep_saves=False):
|
||||||
|
"""Удаляет инстанс. keep_saves=True — сохраняет папку saves."""
|
||||||
|
if not os.path.isdir(inst_dir):
|
||||||
|
return False
|
||||||
|
if keep_saves:
|
||||||
|
# перемещаем saves во временное место
|
||||||
|
saves = os.path.join(_game_dir(inst_dir), "saves")
|
||||||
|
tmp = inst_dir + ".deleted"
|
||||||
|
try:
|
||||||
|
os.makedirs(tmp, exist_ok=True)
|
||||||
|
if os.path.isdir(saves):
|
||||||
|
shutil.move(saves, os.path.join(tmp, "saves"))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
shutil.rmtree(inst_dir, ignore_errors=True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def rename(inst_dir, new_name):
|
||||||
|
"""Переименовывает инстанс (папку и meta.name)."""
|
||||||
|
new_name = _sanitize_name(new_name)
|
||||||
|
parent = os.path.dirname(inst_dir)
|
||||||
|
new_dir = os.path.join(parent, new_name)
|
||||||
|
if os.path.exists(new_dir):
|
||||||
|
return None
|
||||||
|
os.rename(inst_dir, new_dir)
|
||||||
|
meta = load_meta(new_dir)
|
||||||
|
meta["name"] = new_name
|
||||||
|
save_meta(new_dir, meta)
|
||||||
|
return new_dir
|
||||||
|
|
||||||
|
|
||||||
|
def duplicate(inst_dir):
|
||||||
|
"""Дублирует инстанс (без saves)."""
|
||||||
|
meta = load_meta(inst_dir)
|
||||||
|
name = meta.get("name") + " (copy)"
|
||||||
|
new_dir = create(name, meta.get("version", ""), meta.get("loader", "vanilla"))
|
||||||
|
# копируем mods, resourcepacks, config
|
||||||
|
src_gd = _game_dir(inst_dir)
|
||||||
|
dst_gd = _game_dir(new_dir)
|
||||||
|
for sub in ("mods", "resourcepacks", "shaderpacks", "config"):
|
||||||
|
s = os.path.join(src_gd, sub)
|
||||||
|
d = os.path.join(dst_gd, sub)
|
||||||
|
if os.path.isdir(s):
|
||||||
|
try:
|
||||||
|
shutil.copytree(s, d, dirs_exist_ok=True)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return new_dir
|
||||||
|
|
||||||
|
|
||||||
|
def export_zip(inst_dir, dest_path):
|
||||||
|
"""Экспорт инстанса в zip (без saves). Возвращает путь."""
|
||||||
|
import zipfile
|
||||||
|
meta = load_meta(inst_dir)
|
||||||
|
name = _sanitize_name(meta.get("name", "instance"))
|
||||||
|
if not dest_path:
|
||||||
|
dest_path = os.path.join(os.path.expanduser("~"), "Desktop",
|
||||||
|
name + ".pixelinst.zip")
|
||||||
|
src_gd = _game_dir(inst_dir)
|
||||||
|
with zipfile.ZipFile(dest_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
zf.writestr("instance.json", json.dumps(meta, indent=2,
|
||||||
|
ensure_ascii=False))
|
||||||
|
for root, _dirs, files in os.walk(src_gd):
|
||||||
|
rel_root = os.path.relpath(root, src_gd)
|
||||||
|
# пропускаем saves
|
||||||
|
if rel_root == "saves" or rel_root.startswith("saves" + os.sep):
|
||||||
|
continue
|
||||||
|
for fn in files:
|
||||||
|
full = os.path.join(root, fn)
|
||||||
|
arc = os.path.join(".minecraft", rel_root, fn)
|
||||||
|
zf.write(full, arc)
|
||||||
|
return dest_path
|
||||||
|
|
||||||
|
|
||||||
|
def import_zip(zip_path):
|
||||||
|
"""Импорт инстанса из .zip/.mrpack. Возвращает путь к новому инстансу."""
|
||||||
|
import zipfile
|
||||||
|
with zipfile.ZipFile(zip_path, "r") as zf:
|
||||||
|
names = zf.namelist()
|
||||||
|
# ищем instance.json
|
||||||
|
meta_raw = None
|
||||||
|
if "instance.json" in names:
|
||||||
|
meta_raw = json.loads(zf.read("instance.json").decode("utf-8"))
|
||||||
|
# mrpack: modrinth.index.json
|
||||||
|
elif "modrinth.index.json" in names:
|
||||||
|
idx = json.loads(zf.read("modrinth.index.json").decode("utf-8"))
|
||||||
|
meta_raw = {
|
||||||
|
"name": idx.get("name", "Modrinth Pack"),
|
||||||
|
"version": idx.get("versionId", ""),
|
||||||
|
"loader": _detect_loader(idx),
|
||||||
|
}
|
||||||
|
if not meta_raw:
|
||||||
|
raise ValueError("неизвестный формат архива")
|
||||||
|
|
||||||
|
name = _sanitize_name(meta_raw.get("name", "Imported"))
|
||||||
|
inst_dir = create(name, meta_raw.get("version", ""),
|
||||||
|
meta_raw.get("loader", "vanilla"))
|
||||||
|
dst_gd = _game_dir(inst_dir)
|
||||||
|
# распаковываем
|
||||||
|
for n in names:
|
||||||
|
if n.endswith("/"):
|
||||||
|
continue
|
||||||
|
if n == "instance.json" or n == "modrinth.index.json":
|
||||||
|
continue
|
||||||
|
target = n
|
||||||
|
if n.startswith(".minecraft/"):
|
||||||
|
target = n[len(".minecraft/"):]
|
||||||
|
# пропускаем saves при импорте
|
||||||
|
if target == "saves" or target.startswith("saves/"):
|
||||||
|
continue
|
||||||
|
out = os.path.join(dst_gd, target)
|
||||||
|
os.makedirs(os.path.dirname(out), exist_ok=True)
|
||||||
|
with zf.open(n) as src, open(out, "wb") as dst:
|
||||||
|
shutil.copyfileobj(src, dst)
|
||||||
|
# обновляем meta
|
||||||
|
meta = load_meta(inst_dir)
|
||||||
|
meta.update(meta_raw)
|
||||||
|
save_meta(inst_dir, meta)
|
||||||
|
return inst_dir
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_loader(idx):
|
||||||
|
"""Определяет лоадер из modrinth.index.json."""
|
||||||
|
for dep in (idx.get("dependencies") or {}):
|
||||||
|
if dep in ("fabric-loader", "quilt-loader"):
|
||||||
|
return dep.split("-")[0]
|
||||||
|
if dep == "forge":
|
||||||
|
return "forge"
|
||||||
|
if dep == "neoforge":
|
||||||
|
return "neoforge"
|
||||||
|
return "vanilla"
|
||||||
|
|
||||||
|
|
||||||
|
def mark_played(inst_dir):
|
||||||
|
"""Обновляет last_played."""
|
||||||
|
meta = load_meta(inst_dir)
|
||||||
|
meta["last_played"] = int(time.time())
|
||||||
|
save_meta(inst_dir, meta)
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_last_played(ts):
|
||||||
|
"""Человекочитаемое время последнего запуска."""
|
||||||
|
if not ts:
|
||||||
|
return "—"
|
||||||
|
delta = int(time.time()) - ts
|
||||||
|
if delta < 3600:
|
||||||
|
return f"{max(1, delta // 60)} мин назад"
|
||||||
|
if delta < 86400:
|
||||||
|
return f"{delta // 3600} ч назад"
|
||||||
|
if delta < 86400 * 7:
|
||||||
|
return f"{delta // 86400} дн назад"
|
||||||
|
return time.strftime("%d.%m.%Y", time.localtime(ts))
|
||||||
+1
-5
@@ -1,12 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Утилиты для работы с модпаками Modrinth/CurseForge."""
|
"""Утилиты для работы с модпаками Modrinth/CurseForge."""
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
from . import forge, http, mods
|
from . import curseforge, modrinth
|
||||||
from .java_fetch import _safe_join
|
|
||||||
from . import modrinth, curseforge
|
|
||||||
|
|
||||||
|
|
||||||
def install_pack_mrpack_url(game_dir, url, log=None, progress=None, cancel=None):
|
def install_pack_mrpack_url(game_dir, url, log=None, progress=None, cancel=None):
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"""Автообновление лаунчера: проверка манифеста, скачивание установщика.
|
"""Автообновление лаунчера: проверка манифеста, скачивание установщика.
|
||||||
|
|
||||||
Манифест на сервере — /update.json:
|
Манифест на сервере — /update.json:
|
||||||
{"version": "2.1.2", "url": "/download/PixelLauncherSetup2.1.2.exe",
|
{"version": "2.1.3", "url": "/download/PixelLauncherSetup2.1.3.exe",
|
||||||
"sha256": "…", "size": 15418378, "notes": "…"}
|
"sha256": "…", "size": 15418378, "notes": "…"}
|
||||||
url относительный (база — UPDATE_BASE), sha256/size необязательны,
|
url относительный (база — UPDATE_BASE), sha256/size необязательны,
|
||||||
но sha256 проверяется, если указан.
|
но sha256 проверяется, если указан.
|
||||||
|
|||||||
+825
-67
File diff suppressed because it is too large.
Load diff
+7
-3
@@ -183,12 +183,16 @@ class Version:
|
|||||||
return os.pathsep.join(entries)
|
return os.pathsep.join(entries)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
def build_command(self, java, username, uuid_hex, ram_mb, extra_jvm=""):
|
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
|
gd = self.game_dir
|
||||||
subst = {
|
subst = {
|
||||||
"auth_player_name": username,
|
"auth_player_name": username,
|
||||||
"version_name": self.id,
|
"version_name": self.id,
|
||||||
"game_directory": gd,
|
"game_directory": game_directory or gd,
|
||||||
"assets_root": self.assets_dir(),
|
"assets_root": self.assets_dir(),
|
||||||
"assets_index_name": self.asset_index.get("id", ""),
|
"assets_index_name": self.asset_index.get("id", ""),
|
||||||
"auth_uuid": uuid_hex,
|
"auth_uuid": uuid_hex,
|
||||||
@@ -198,7 +202,7 @@ class Version:
|
|||||||
"version_type": self.type,
|
"version_type": self.type,
|
||||||
"natives_directory": self.natives_dir(),
|
"natives_directory": self.natives_dir(),
|
||||||
"launcher_name": "PixelLauncher",
|
"launcher_name": "PixelLauncher",
|
||||||
"launcher_version": "2.1.2",
|
"launcher_version": sys.modules[__package__].LAUNCHER_VERSION,
|
||||||
"classpath": self.classpath_str(),
|
"classpath": self.classpath_str(),
|
||||||
"classpath_separator": os.pathsep,
|
"classpath_separator": os.pathsep,
|
||||||
"library_directory": self.libraries_dir(),
|
"library_directory": self.libraries_dir(),
|
||||||
|
|||||||
+36
-1
@@ -26,7 +26,7 @@ STAGE = os.path.join(BUILD, "stage")
|
|||||||
ISCC = r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
|
ISCC = r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
|
||||||
|
|
||||||
PYVER = "%d.%d.%d" % sys.version_info[:3]
|
PYVER = "%d.%d.%d" % sys.version_info[:3]
|
||||||
APP_VERSION = "2.1.2" # должен совпадать с LAUNCHER_VERSION в launcher/__init__.py
|
APP_VERSION = "2.1.3" # должен совпадать с LAUNCHER_VERSION в launcher/__init__.py
|
||||||
SETUP_NAME = "PixelLauncherSetup%s.exe" % APP_VERSION
|
SETUP_NAME = "PixelLauncherSetup%s.exe" % APP_VERSION
|
||||||
|
|
||||||
|
|
||||||
@@ -83,6 +83,38 @@ def build_stage():
|
|||||||
copied.append("tkinter/")
|
copied.append("tkinter/")
|
||||||
log("tkinter/Tcl: " + ", ".join(copied) + " + tcl/")
|
log("tkinter/Tcl: " + ", ".join(copied) + " + tcl/")
|
||||||
|
|
||||||
|
# --- Pillow (опционально) — сглаженные иконки сборок; если её нет,
|
||||||
|
# лаунчер переключается на штатный Tk-путь
|
||||||
|
sites = [os.path.join(base, "Lib", "site-packages")]
|
||||||
|
try:
|
||||||
|
import site as _site
|
||||||
|
us = _site.getusersitepackages()
|
||||||
|
sites.extend(us if isinstance(us, (list, tuple)) else [us])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
staged = []
|
||||||
|
for site_dir in sites:
|
||||||
|
if not os.path.isdir(site_dir):
|
||||||
|
continue
|
||||||
|
for name in sorted(os.listdir(site_dir)):
|
||||||
|
if name != "PIL" and not name.lower().startswith("pillow"):
|
||||||
|
continue
|
||||||
|
src = os.path.join(site_dir, name)
|
||||||
|
dst = os.path.join(STAGE, name)
|
||||||
|
if os.path.isdir(src):
|
||||||
|
if os.path.isdir(dst):
|
||||||
|
shutil.rmtree(dst, ignore_errors=True)
|
||||||
|
shutil.copytree(src, dst,
|
||||||
|
ignore=shutil.ignore_patterns("__pycache__",
|
||||||
|
"*.pyc"))
|
||||||
|
elif os.path.isfile(src):
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
staged.append(name)
|
||||||
|
if staged:
|
||||||
|
break
|
||||||
|
log("Pillow: " + (", ".join(staged) if staged
|
||||||
|
else "не найдена — иконки без сглаживания"))
|
||||||
|
|
||||||
# ._pth: гарантируем точку в пути (иначе import launcher не найдёт)
|
# ._pth: гарантируем точку в пути (иначе import launcher не найдёт)
|
||||||
pth = [f for f in os.listdir(STAGE) if f.endswith("._pth")][0]
|
pth = [f for f in os.listdir(STAGE) if f.endswith("._pth")][0]
|
||||||
pth_path = os.path.join(STAGE, pth)
|
pth_path = os.path.join(STAGE, pth)
|
||||||
@@ -151,6 +183,9 @@ if __name__ == "__main__":
|
|||||||
"import tkinter,ssl,urllib.request,json,zipfile;"
|
"import tkinter,ssl,urllib.request,json,zipfile;"
|
||||||
"r=tkinter.Tk(); r.withdraw(); r.destroy();"
|
"r=tkinter.Tk(); r.withdraw(); r.destroy();"
|
||||||
"import launcher.config, launcher.http, launcher.manifest;"
|
"import launcher.config, launcher.http, launcher.manifest;"
|
||||||
|
"import importlib,importlib.util;"
|
||||||
|
"importlib.import_module('PIL.Image')"
|
||||||
|
" if importlib.util.find_spec('PIL') else None;"
|
||||||
"print('stage ok')"],
|
"print('stage ok')"],
|
||||||
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
||||||
cwd=STAGE)
|
cwd=STAGE)
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
# Внешние зависимости не требуются.
|
# Внешние зависимости не требуются.
|
||||||
# Достаточно Python 3.8+ (всё реализовано на стандартной библиотеке:
|
# Достаточно Python 3.8+ (всё реализовано на стандартной библиотеке:
|
||||||
# tkinter, urllib, json, zipfile, concurrent.futures).
|
# tkinter, urllib, json, zipfile, concurrent.futures).
|
||||||
|
# Если доступна Pillow, лаунчер использует её для сглаженных иконок сборок
|
||||||
|
# (установщик PixelLauncher Pillow включает автоматически); без Pillow
|
||||||
|
# иконки рисуются штатным Tk — работает в любом случае.
|
||||||
Reference in new issue
Block a user