Add download progress dialog and some mod loader parse function.
This commit is contained in:
@@ -5,20 +5,15 @@ from typing import Callable
|
|||||||
from PySide6 import QtWidgets
|
from PySide6 import QtWidgets
|
||||||
from PySide6.QtCore import QSize
|
from PySide6.QtCore import QSize
|
||||||
from PySide6.QtGui import QIcon, Qt, QPixmap
|
from PySide6.QtGui import QIcon, Qt, QPixmap
|
||||||
from PySide6.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QWidget, QStyle, QToolButton, \
|
from PySide6.QtWidgets import QApplication, QWidget, QStyle, QToolButton
|
||||||
QStackedWidget
|
|
||||||
|
|
||||||
|
from constant import LAUNCHER_VERSION
|
||||||
from core_lib.qt.hack import move_window_to_center, is_light_theme, \
|
from core_lib.qt.hack import move_window_to_center, is_light_theme, \
|
||||||
recolor_icon
|
recolor_icon
|
||||||
from core_lib.qt import messagebox
|
from core_lib.qt import messagebox
|
||||||
from core_lib.launcher.object import Launcher as LauncherObject
|
from core_lib.launcher.object import Launcher as LauncherObject
|
||||||
|
from manager.widgets import WidgetManager
|
||||||
from pages.account import AccountPage
|
from window import LauncherMainWindow
|
||||||
from pages.create_profile import CreateProfile
|
|
||||||
from pages.test import TestPage
|
|
||||||
|
|
||||||
LAUNCHER_VERSION = "alpha_0.0.2"
|
|
||||||
MAIN_REPO_URL = "https://repo.weispace.net/wei/Launcher/"
|
|
||||||
|
|
||||||
class ToolBar(QtWidgets.QToolBar):
|
class ToolBar(QtWidgets.QToolBar):
|
||||||
def __init__(self, app: QApplication, parent=None):
|
def __init__(self, app: QApplication, parent=None):
|
||||||
@@ -100,141 +95,38 @@ class ToolBar(QtWidgets.QToolBar):
|
|||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
class Launcher(QApplication, LauncherObject):
|
class Launcher(QApplication, LauncherObject):
|
||||||
def __init__(self):
|
def __init__(self, logger):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
# Window config
|
|
||||||
self.setApplicationName("TestLauncher")
|
|
||||||
self.main_window = QMainWindow()
|
|
||||||
self.main_window.setGeometry(0, 0, 800, 600)
|
|
||||||
move_window_to_center(self.main_window)
|
|
||||||
|
|
||||||
# logger
|
# logger
|
||||||
self.logger = logging.getLogger("Launcher.MainWindow")
|
self.logger = logger
|
||||||
|
|
||||||
# dirs
|
# dirs
|
||||||
self.program_dir = Path(__file__).parent
|
self.program_dir = Path(__file__).parent
|
||||||
self.work_dir = Path.cwd()
|
self.work_dir = Path.cwd()
|
||||||
|
|
||||||
|
# Widget (Signal, Event)
|
||||||
|
self.widget_manager = WidgetManager()
|
||||||
|
|
||||||
|
# Window config
|
||||||
|
self.setApplicationName("TestLauncher")
|
||||||
|
self.main_window = LauncherMainWindow(
|
||||||
|
app=self,
|
||||||
|
widget_manager=self.widget_manager,
|
||||||
|
)
|
||||||
|
self.main_window.setGeometry(0, 0, 800, 600)
|
||||||
|
move_window_to_center(self.main_window)
|
||||||
|
|
||||||
# Set icon
|
# Set icon
|
||||||
if self.icon_path.exists():
|
if self.icon_path.exists():
|
||||||
self.setWindowIcon(QIcon(self.icon_path.as_posix()))
|
self.setWindowIcon(QIcon(self.icon_path.as_posix()))
|
||||||
else:
|
else:
|
||||||
self.logger.error("No icon found.")
|
self.logger.error("No icon found.")
|
||||||
|
|
||||||
# Main layout
|
|
||||||
self.central = QStackedWidget()
|
|
||||||
self.toolbar = ToolBar(self, self.main_window)
|
|
||||||
|
|
||||||
# center message (will be deleted if launcher finished development)
|
|
||||||
content = """Still digging!"""
|
|
||||||
message = "If you found that something might be a issue. You can report it to the main repo! Any suggestion are welcome."
|
|
||||||
self.icon_label = QLabel()
|
|
||||||
self.icon_label.setObjectName("icon_label")
|
|
||||||
self.dev_info_box = QLabel(content)
|
|
||||||
self.dev_info_box.setObjectName("dev_info_box")
|
|
||||||
self.dev_info_2 = QLabel(message)
|
|
||||||
self.dev_info_2.setObjectName("dev_info_message")
|
|
||||||
self.version_label = QLabel("Current version: {}".format(self.launcher_version))
|
|
||||||
self.version_label.setObjectName("version_label")
|
|
||||||
self.repo_url_label = QLabel(f"<a href=\"{MAIN_REPO_URL}\">Main repo</a>")
|
|
||||||
self.repo_url_label.setObjectName("repo_url_label")
|
|
||||||
self.repo_url_label.setOpenExternalLinks(True)
|
|
||||||
|
|
||||||
if Path(self.resources_dir, "pictures", "in_progress.png").exists():
|
|
||||||
image = QPixmap(Path(self.resources_dir, "pictures", "in_progress.png"))
|
|
||||||
self.icon_label.setPixmap(image.scaled(300, 300))
|
|
||||||
else:
|
|
||||||
self.icon_label.setText("Ouch. The icon is missing.")
|
|
||||||
|
|
||||||
# Homepage
|
|
||||||
self.home_page = QWidget()
|
|
||||||
self.home_layout = QVBoxLayout(self.home_page)
|
|
||||||
self.home_layout.addStretch()
|
|
||||||
self.home_layout.addWidget(self.icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
|
|
||||||
self.home_layout.addWidget(self.dev_info_box, alignment=Qt.AlignmentFlag.AlignHCenter)
|
|
||||||
self.home_layout.addWidget(self.dev_info_2, alignment=Qt.AlignmentFlag.AlignHCenter)
|
|
||||||
self.home_layout.addWidget(self.version_label, alignment=Qt.AlignmentFlag.AlignHCenter)
|
|
||||||
self.home_layout.addWidget(self.repo_url_label, alignment=Qt.AlignmentFlag.AlignHCenter)
|
|
||||||
self.home_layout.addStretch()
|
|
||||||
|
|
||||||
# test page
|
|
||||||
self.test_page = TestPage(self, self.main_window)
|
|
||||||
|
|
||||||
# Account page
|
|
||||||
self.account_page = AccountPage(self, self.main_window)
|
|
||||||
|
|
||||||
# Profile page
|
|
||||||
self.create_profile_page = CreateProfile(self, self.main_window)
|
|
||||||
|
|
||||||
# Add page
|
|
||||||
self.central.addWidget(self.home_page)
|
|
||||||
self.central.addWidget(self.test_page)
|
|
||||||
self.central.addWidget(self.account_page)
|
|
||||||
self.central.addWidget(self.create_profile_page)
|
|
||||||
|
|
||||||
# Bind toolbar and center widget
|
|
||||||
self.main_window.addToolBar(Qt.ToolBarArea.LeftToolBarArea, self.toolbar)
|
|
||||||
self.main_window.setCentralWidget(self.central)
|
|
||||||
|
|
||||||
# Apply qss
|
|
||||||
self.apply_qss(Path("main.qss"), self.setStyleSheet)
|
|
||||||
self.apply_qss(Path("toolbar.qss"), self.toolbar.setStyleSheet)
|
|
||||||
|
|
||||||
# Toolbar
|
|
||||||
|
|
||||||
# Test Window btn
|
|
||||||
self.open_test_page = self.toolbar.add_button(" Tests", icon=self.get_icon(Path(self.icon_dir, "debug.png")))
|
|
||||||
self.open_test_page.setObjectName("open_test_page")
|
|
||||||
|
|
||||||
# Home btn
|
|
||||||
self.home_button = self.toolbar.add_button(icon=self.get_icon(Path(self.icon_dir, "home.png")))
|
|
||||||
|
|
||||||
# Account btn (will be moved to the bottom (or last one item) of the toolbar
|
|
||||||
self.open_account_page = self.toolbar.add_button(icon=self.get_icon(Path(self.icon_dir, "head.png")))
|
|
||||||
|
|
||||||
# Profile btn
|
|
||||||
self.open_create_profile_page = self.toolbar.add_button(icon=self.get_icon(Path(self.icon_dir, "add_temp.png")))
|
|
||||||
|
|
||||||
# Bind tool buttons
|
|
||||||
self.toolbar.addWidget(self.home_button)
|
|
||||||
self.toolbar.addWidget(self.open_test_page)
|
|
||||||
self.toolbar.addWidget(self.open_create_profile_page)
|
|
||||||
self.toolbar.setMovable(True)
|
|
||||||
self.toolbar.setFloatable(False)
|
|
||||||
self.toolbar.setAllowedAreas(Qt.ToolBarArea.AllToolBarAreas)
|
|
||||||
|
|
||||||
self.toolbar.addWidget(self.toolbar.spacer)
|
|
||||||
self.toolbar.addSeparator()
|
|
||||||
self.toolbar.addWidget(self.open_account_page)
|
|
||||||
|
|
||||||
# Bind page-related button click event
|
|
||||||
self.home_button.clicked.connect(
|
|
||||||
lambda: self.set_current_page(self.home_page, self.home_button)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.open_test_page.clicked.connect(
|
|
||||||
lambda: self.set_current_page(self.test_page, self.open_test_page)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.open_account_page.clicked.connect(
|
|
||||||
lambda: self.set_current_page(self.account_page, self.open_account_page)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.open_create_profile_page.clicked.connect(
|
|
||||||
lambda: self.set_current_page(self.create_profile_page, self.open_create_profile_page)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.set_current_page(self.home_page, self.home_button)
|
|
||||||
|
|
||||||
def exec(self):
|
def exec(self):
|
||||||
self.main_window.show()
|
self.main_window.show()
|
||||||
self.toolbar.update_all()
|
self.main_window.toolbar.update_all()
|
||||||
self.exec_()
|
self.exec_()
|
||||||
|
|
||||||
def set_current_page(self, page: QWidget, related_button: QToolButton):
|
|
||||||
self.central.setCurrentWidget(page)
|
|
||||||
related_button.setFocus()
|
|
||||||
|
|
||||||
def get_icon(self, path: Path):
|
def get_icon(self, path: Path):
|
||||||
icon = self.style().standardIcon(QStyle.StandardPixmap.SP_MessageBoxWarning)
|
icon = self.style().standardIcon(QStyle.StandardPixmap.SP_MessageBoxWarning)
|
||||||
if not path.exists() or not path.is_file():
|
if not path.exists() or not path.is_file():
|
||||||
@@ -342,6 +234,5 @@ class Launcher(QApplication, LauncherObject):
|
|||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
def get_main_window(self):
|
||||||
|
return self.main_window
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
LAUNCHER_VERSION = "alpha_0.0.2"
|
||||||
|
MAIN_REPO_URL = "https://repo.weispace.net/wei/Launcher/"
|
||||||
@@ -180,6 +180,7 @@ def download_file(file: FileObject, overwrite=True, progress_callback: Callable[
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
with requests.get(file.url, stream=True) as r:
|
with requests.get(file.url, stream=True) as r:
|
||||||
|
r.raise_for_status()
|
||||||
current_chunk = 0
|
current_chunk = 0
|
||||||
total_bytes = r.headers.get("content-length", None)
|
total_bytes = r.headers.get("content-length", None)
|
||||||
logger.debug("URL: {}".format(file.url))
|
logger.debug("URL: {}".format(file.url))
|
||||||
@@ -211,7 +212,7 @@ def download_file(file: FileObject, overwrite=True, progress_callback: Callable[
|
|||||||
if any([file.expected_sha1, file.expected_md5, file.expected_sha256]):
|
if any([file.expected_sha1, file.expected_md5, file.expected_sha256]):
|
||||||
logger.debug("File hash is match as the expected hash.")
|
logger.debug("File hash is match as the expected hash.")
|
||||||
else:
|
else:
|
||||||
logger.warning("File hash is not checked.")
|
logger.warning("Hash is not checked for file: {}".format(file.posix_path))
|
||||||
|
|
||||||
return file
|
return file
|
||||||
|
|
||||||
@@ -299,3 +300,8 @@ def get_platform_info():
|
|||||||
|
|
||||||
return platform_name, arch, os_version
|
return platform_name, arch, os_version
|
||||||
|
|
||||||
|
def is_valid_zipfile(path: Path) -> bool:
|
||||||
|
try:
|
||||||
|
return zipfile.is_zipfile(path)
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
+89
-38
@@ -1,11 +1,14 @@
|
|||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
|
|
||||||
|
from core_lib.common.common import get_platform_info
|
||||||
from core_lib.game.library import get_natives_platform_info
|
from core_lib.game.library import get_natives_platform_info
|
||||||
|
|
||||||
logger = logging.getLogger("Launcher.CoreLib")
|
logger = logging.getLogger("Launcher.CoreLib")
|
||||||
|
|
||||||
|
|
||||||
def is_argument_allowed(argument: dict | str, allow_features: dict | None = None) -> bool:
|
def is_argument_allowed(argument: dict | str, allow_features: dict | None = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if argument is allowed or not.
|
Check if argument is allowed or not.
|
||||||
@@ -22,7 +25,8 @@ def is_argument_allowed(argument: dict | str, allow_features: dict | None=None)
|
|||||||
|
|
||||||
rules = argument.get('rules', {})
|
rules = argument.get('rules', {})
|
||||||
|
|
||||||
_, platform_rule_name, _ = get_natives_platform_info()
|
_, platform_rule_name, platform_arch_type = get_natives_platform_info()
|
||||||
|
_, _, platform_version = get_platform_info()
|
||||||
|
|
||||||
allowed = False
|
allowed = False
|
||||||
|
|
||||||
@@ -30,33 +34,52 @@ def is_argument_allowed(argument: dict | str, allow_features: dict | None=None)
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
for rule in rules:
|
for rule in rules:
|
||||||
rule_action = rule.get("action", None)
|
if not isinstance(rule, dict):
|
||||||
os_section = rule.get("os", {})
|
logger.warning("Ignoring invalid argument rule: {}".format(rule))
|
||||||
rule_os = os_section.get("name", None)
|
continue
|
||||||
features_rule = rule.get("features", None)
|
|
||||||
|
|
||||||
|
rule_action = rule.get("action", None)
|
||||||
|
os_rule = rule.get("os") or {}
|
||||||
|
rule_os = os_rule.get("name", None)
|
||||||
|
feature_rule = rule.get("features") or {}
|
||||||
|
rule_arch = os_rule.get("arch")
|
||||||
|
rule_version = os_rule.get("version")
|
||||||
|
|
||||||
|
# Check platform
|
||||||
if rule_os is not None and platform_rule_name != rule_os:
|
if rule_os is not None and platform_rule_name != rule_os:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
matched = True
|
# Check architecture
|
||||||
|
if rule_arch is not None and rule_arch != platform_arch_type:
|
||||||
# Check feature dict
|
|
||||||
if features_rule:
|
|
||||||
for key, expected in features_rule.items():
|
|
||||||
if allow_features.get(key, False) != expected:
|
|
||||||
matched = False
|
|
||||||
break
|
|
||||||
|
|
||||||
if not matched:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Check platform version
|
||||||
|
if rule_version is not None:
|
||||||
|
try:
|
||||||
|
if re.search(
|
||||||
|
rule_version,
|
||||||
|
str(platform_version),
|
||||||
|
) is None:
|
||||||
|
continue
|
||||||
|
except re.error as exc:
|
||||||
|
logger.warning(
|
||||||
|
"Unsupported argument OS version regex %r: %s",
|
||||||
|
rule_version,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
# Check feature rule
|
||||||
|
if any(allow_features.get(key, False) != expected for key, expected in feature_rule.items()):
|
||||||
|
continue
|
||||||
|
|
||||||
if rule_action == "allow":
|
if rule_action == "allow":
|
||||||
allowed = True
|
allowed = True
|
||||||
elif rule_action == "disallow":
|
elif rule_action == "disallow":
|
||||||
allowed = False
|
allowed = False
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Unknown action {rule_action}\n")
|
logger.warning(f"Unknown argument rule action {rule_action}\n")
|
||||||
|
|
||||||
return allowed
|
return allowed
|
||||||
|
|
||||||
@@ -67,6 +90,7 @@ def replace_placeholders(argument, placeholders: dict) -> str:
|
|||||||
|
|
||||||
return argument
|
return argument
|
||||||
|
|
||||||
|
|
||||||
class ArgumentMappings:
|
class ArgumentMappings:
|
||||||
def __init__(self, player_name="Player", version_name="unknown",
|
def __init__(self, player_name="Player", version_name="unknown",
|
||||||
game_directory=None, assets_root=None, legacy_assets_root=None, assets_index_name=None,
|
game_directory=None, assets_root=None, legacy_assets_root=None, assets_index_name=None,
|
||||||
@@ -87,8 +111,15 @@ class ArgumentMappings:
|
|||||||
self.auth_xuid = auth_xuid
|
self.auth_xuid = auth_xuid
|
||||||
self.user_type = user_type
|
self.user_type = user_type
|
||||||
self.user_properties = user_properties
|
self.user_properties = user_properties
|
||||||
if not isinstance(user_properties, dict):
|
if isinstance(user_properties, dict):
|
||||||
self.user_properties = {}
|
self.user_properties = json.dumps(
|
||||||
|
user_properties,
|
||||||
|
separators=(",", ":"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if user_properties is not None:
|
||||||
|
logger.warning(f"User properties should be a dictionary.")
|
||||||
|
self.user_properties = "{}"
|
||||||
self.version_type = version_type
|
self.version_type = version_type
|
||||||
self.natives_directory = natives_directory
|
self.natives_directory = natives_directory
|
||||||
self.launcher_name = launcher_name
|
self.launcher_name = launcher_name
|
||||||
@@ -122,6 +153,7 @@ class ArgumentMappings:
|
|||||||
"library_directory": self.library_directory
|
"library_directory": self.library_directory
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def convert_argument_to_multiple_string(argument: str | dict) -> list[str]:
|
def convert_argument_to_multiple_string(argument: str | dict) -> list[str]:
|
||||||
if isinstance(argument, str):
|
if isinstance(argument, str):
|
||||||
return [argument]
|
return [argument]
|
||||||
@@ -136,6 +168,7 @@ def convert_argument_to_multiple_string(argument: str | dict) -> list[str]:
|
|||||||
|
|
||||||
raise TypeError("Argument must be of type str or dict")
|
raise TypeError("Argument must be of type str or dict")
|
||||||
|
|
||||||
|
|
||||||
def find_game_and_jvm_args(arguments: dict | str) -> tuple[list, list]:
|
def find_game_and_jvm_args(arguments: dict | str) -> tuple[list, list]:
|
||||||
game_args = []
|
game_args = []
|
||||||
jvm_args = []
|
jvm_args = []
|
||||||
@@ -160,11 +193,13 @@ def find_game_and_jvm_args(arguments: dict | str) -> tuple[list, list]:
|
|||||||
|
|
||||||
return game_args, jvm_args
|
return game_args, jvm_args
|
||||||
|
|
||||||
|
|
||||||
REMAPPING_ARGUMENTS = {
|
REMAPPING_ARGUMENTS = {
|
||||||
# For some (I don't know) reason, in 26.2, Mojang include some
|
# For some (I don't know) reason, in 26.2, Mojang include some
|
||||||
"-Djava.library.path=${natives_directory}/java": "-Djava.library.path=${natives_directory}"
|
"-Djava.library.path=${natives_directory}/java": "-Djava.library.path=${natives_directory}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def hard_remapping_special_arguments(argument: str) -> str:
|
def hard_remapping_special_arguments(argument: str) -> str:
|
||||||
for arg in REMAPPING_ARGUMENTS:
|
for arg in REMAPPING_ARGUMENTS:
|
||||||
new_value = REMAPPING_ARGUMENTS[arg]
|
new_value = REMAPPING_ARGUMENTS[arg]
|
||||||
@@ -174,7 +209,9 @@ def hard_remapping_special_arguments(argument: str) -> str:
|
|||||||
|
|
||||||
return argument
|
return argument
|
||||||
|
|
||||||
def parse_and_generate_arguments(arguments: list | str, mappings: ArgumentMappings, features: dict | None=None) -> list[str]:
|
|
||||||
|
def parse_and_generate_arguments(arguments: list | str, mappings: ArgumentMappings, features: dict | None = None) -> \
|
||||||
|
list[str]:
|
||||||
if features is None:
|
if features is None:
|
||||||
features = {}
|
features = {}
|
||||||
|
|
||||||
@@ -204,38 +241,59 @@ def parse_and_generate_arguments(arguments: list | str, mappings: ArgumentMappin
|
|||||||
return parsed_arguments
|
return parsed_arguments
|
||||||
|
|
||||||
|
|
||||||
|
def append_missing_necessary_jvm_arguments(arguments: list, arg_maps: ArgumentMappings) -> list[str]:
|
||||||
|
result = []
|
||||||
|
has_classpath = any(
|
||||||
|
argument in ("-cp", "-classpath", "--class-path")
|
||||||
|
for argument in arguments
|
||||||
|
)
|
||||||
|
|
||||||
|
has_native_path = any(
|
||||||
|
argument.startswith("-Djava.library.path=")
|
||||||
|
for argument in arguments
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_native_path:
|
||||||
|
result.append(f"-Djava.library.path={arg_maps.natives_directory}")
|
||||||
|
|
||||||
|
if not has_classpath:
|
||||||
|
result.extend(["-cp", arg_maps.classpath])
|
||||||
|
|
||||||
|
result.extend(arguments)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def generate_launch_command(arg_maps: ArgumentMappings, main_class: str, java_path: str, full_args: dict | str
|
def generate_launch_command(arg_maps: ArgumentMappings, main_class: str, java_path: str, full_args: dict | str
|
||||||
) -> list[str]:
|
, features: dict | None = None) -> list[str]:
|
||||||
cmd = [java_path]
|
cmd = [java_path]
|
||||||
game_args, jvm_args = find_game_and_jvm_args(full_args)
|
game_args, jvm_args = find_game_and_jvm_args(full_args)
|
||||||
parsed_jvm_args = parse_and_generate_arguments(
|
parsed_jvm_args = parse_and_generate_arguments(
|
||||||
jvm_args,
|
jvm_args,
|
||||||
arg_maps
|
arg_maps,
|
||||||
|
features=features
|
||||||
)
|
)
|
||||||
parsed_game_args = parse_and_generate_arguments(
|
parsed_game_args = parse_and_generate_arguments(
|
||||||
game_args,
|
game_args,
|
||||||
arg_maps
|
arg_maps,
|
||||||
|
features=features
|
||||||
)
|
)
|
||||||
|
|
||||||
# Legacy Minecraft don't have jvm arguments exist in the version library. We need to generate by ourselves
|
# Legacy Minecraft don't have jvm arguments exist in the version library. We need to generate by ourselves
|
||||||
if len(parsed_jvm_args) == 0:
|
parsed_jvm_args = append_missing_necessary_jvm_arguments(parsed_jvm_args, arg_maps)
|
||||||
cmd.extend([
|
|
||||||
f"-Djava.library.path={arg_maps.natives_directory}",
|
|
||||||
"-cp",
|
|
||||||
arg_maps.classpath,
|
|
||||||
])
|
|
||||||
else:
|
|
||||||
cmd.extend(parsed_jvm_args)
|
|
||||||
|
|
||||||
|
cmd.extend(parsed_jvm_args)
|
||||||
cmd.append(main_class)
|
cmd.append(main_class)
|
||||||
cmd.extend(parsed_game_args)
|
cmd.extend(parsed_game_args)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
def is_a_argument(arg: str, startswith="--"):
|
def is_a_argument(arg: str, startswith="--"):
|
||||||
pattern = re.compile(rf"{startswith}(.+)")
|
pattern = re.compile(rf"{startswith}(.+)")
|
||||||
return pattern.match(arg) is not None
|
return pattern.match(arg) is not None
|
||||||
|
|
||||||
|
|
||||||
def generate_argument_details(arguments: list[str | dict], startswith="--") -> list[dict]:
|
def generate_argument_details(arguments: list[str | dict], startswith="--") -> list[dict]:
|
||||||
items = []
|
items = []
|
||||||
|
|
||||||
@@ -271,13 +329,6 @@ def generate_argument_details(arguments: list[str | dict], startswith="--") -> l
|
|||||||
|
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
def is_same_argument(item: dict, another: dict) -> bool:
|
def is_same_argument(item: dict, another: dict) -> bool:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-553
@@ -1,22 +1,17 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import zipfile
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Callable
|
|
||||||
from urllib.parse import urlsplit, unquote
|
from urllib.parse import urlsplit, unquote
|
||||||
|
|
||||||
from ..common.common import get_platform_info, FileObject, download_multiple_files, unzip
|
from ..common.common import get_platform_info
|
||||||
from .version_info import (
|
from .version_info import (
|
||||||
MOJANG_LIBRARIES_ENDPOINT,
|
MOJANG_LIBRARIES_ENDPOINT,
|
||||||
NATIVE_OS_KEY_MAP,
|
NATIVE_OS_KEY_MAP,
|
||||||
NATIVE_OS_RULE_MAP,
|
NATIVE_OS_RULE_MAP,
|
||||||
NATIVE_ARCH_KEY_MAP,
|
NATIVE_ARCH_KEY_MAP,
|
||||||
NATIVE_NEW_TO_OLD_ARCH,
|
NATIVE_NEW_TO_OLD_ARCH,
|
||||||
NATIVE_OLD_TO_NEW_ARCH, find_useful_part_in_specific_version_manifest,
|
NATIVE_OLD_TO_NEW_ARCH
|
||||||
)
|
)
|
||||||
from ..common.exception import UnsupportedPlatformException, NativeResolveException, DependencyException, \
|
|
||||||
VersionDataKeyNotFoundException, DownloadException
|
|
||||||
|
|
||||||
logger = logging.getLogger("Launcher.CoreLib")
|
logger = logging.getLogger("Launcher.CoreLib")
|
||||||
|
|
||||||
@@ -32,9 +27,6 @@ def parse_maven_coordinate(value: str, default_extension="jar"):
|
|||||||
group, artifact, version = parts[:3]
|
group, artifact, version = parts[:3]
|
||||||
classifier = parts[3] if len(parts) == 4 else None
|
classifier = parts[3] if len(parts) == 4 else None
|
||||||
|
|
||||||
if version.count("-") > 0:
|
|
||||||
version, extra = version.split("-", 1)
|
|
||||||
|
|
||||||
if not group or not artifact or not version:
|
if not group or not artifact or not version:
|
||||||
raise ValueError(f"Coordinate key can not be empty: {value!r}")
|
raise ValueError(f"Coordinate key can not be empty: {value!r}")
|
||||||
|
|
||||||
@@ -45,7 +37,6 @@ def parse_maven_coordinate(value: str, default_extension="jar"):
|
|||||||
|
|
||||||
def convert_maven_version(version: str, failback=None) -> tuple[int, int, int]:
|
def convert_maven_version(version: str, failback=None) -> tuple[int, int, int]:
|
||||||
try:
|
try:
|
||||||
print(version)
|
|
||||||
items = version.split(".")
|
items = version.split(".")
|
||||||
if len(items) >= 3:
|
if len(items) >= 3:
|
||||||
major, minor, patch = items[0], items[1], items[2]
|
major, minor, patch = items[0], items[1], items[2]
|
||||||
@@ -369,545 +360,3 @@ def generate_formatted_artifact_with_name(lib_name, lib_artifact, libraries_endp
|
|||||||
artifact_new.update(extra)
|
artifact_new.update(extra)
|
||||||
|
|
||||||
return artifact_new
|
return artifact_new
|
||||||
|
|
||||||
|
|
||||||
def collect_library_artifacts(specific_version_manifest):
|
|
||||||
_, _, _, _, libraries, _, _ = find_useful_part_in_specific_version_manifest(specific_version_manifest)
|
|
||||||
|
|
||||||
items = []
|
|
||||||
|
|
||||||
# Same as natives
|
|
||||||
_, _, os_version = get_platform_info()
|
|
||||||
platform_key_name, platform_rule_name, platform_arch_type = get_natives_platform_info()
|
|
||||||
|
|
||||||
for library in libraries:
|
|
||||||
name = library.get("name", "")
|
|
||||||
artifact_raw = library.get("downloads", {}).get("artifact", {})
|
|
||||||
rules = library.get("rules", [])
|
|
||||||
classifiers = library.get("downloads", {}).get("classifiers", {})
|
|
||||||
natives = library.get("natives", {})
|
|
||||||
|
|
||||||
# Ignore natives
|
|
||||||
if (len(natives) > 0 or bool(re.search(r":natives-[^:]+$", name)) or
|
|
||||||
any(native.startswith("natives-") for native in classifiers)):
|
|
||||||
logger.info(f"Skipping {name} because its a native library.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not is_library_allowed(
|
|
||||||
rules,
|
|
||||||
platform_rule_name,
|
|
||||||
platform_arch_type,
|
|
||||||
os_version,
|
|
||||||
):
|
|
||||||
logger.debug(f"Library {name} can't pass library rules.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
artifact = generate_formatted_artifact_with_name(
|
|
||||||
name,
|
|
||||||
artifact_raw
|
|
||||||
)
|
|
||||||
|
|
||||||
if artifact:
|
|
||||||
items.append(artifact)
|
|
||||||
|
|
||||||
if not items:
|
|
||||||
raise DependencyException(
|
|
||||||
"Could not find any artifact in any of the libraries."
|
|
||||||
)
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
def collect_native_artifacts(specific_version_manifest):
|
|
||||||
"""
|
|
||||||
Collects native artifacts that support current platform
|
|
||||||
:param specific_version_manifest:
|
|
||||||
:return:
|
|
||||||
filtered_natives_artifacts: list[dict]
|
|
||||||
"""
|
|
||||||
filtered = []
|
|
||||||
|
|
||||||
# Convert current platform and architecture type to same formula
|
|
||||||
_, _, os_version = get_platform_info()
|
|
||||||
platform_key_name, platform_rule_name, platform_arch_type = get_natives_platform_info()
|
|
||||||
|
|
||||||
if platform_key_name is None or platform_arch_type is None or platform_rule_name is None:
|
|
||||||
raise UnsupportedPlatformException(
|
|
||||||
f"Unsupported platform: key={platform_key_name}, "
|
|
||||||
f"rule={platform_rule_name}, arch={platform_arch_type}"
|
|
||||||
)
|
|
||||||
|
|
||||||
_, _, _, _, libraries, _, _ = find_useful_part_in_specific_version_manifest(specific_version_manifest)
|
|
||||||
|
|
||||||
for library in libraries:
|
|
||||||
name = library.get("name", "")
|
|
||||||
artifact = library.get("downloads", {}).get("artifact", {})
|
|
||||||
# For newer Minecraft (VER>1.7.10)
|
|
||||||
rules = library.get("rules", [])
|
|
||||||
# For legacy version (VER<1.8)
|
|
||||||
classifiers = library.get("downloads", {}).get("classifiers", {})
|
|
||||||
natives = library.get("natives", {})
|
|
||||||
exclude = library.get("extract", {}).get("exclude", ["META-INF/"]) # For extract use
|
|
||||||
|
|
||||||
is_natives = False
|
|
||||||
|
|
||||||
# Check if library is a native
|
|
||||||
if (len(natives) > 0 or bool(re.search(r":natives-[^:]+$", name)) or
|
|
||||||
any(native.startswith("natives-") for native in classifiers)):
|
|
||||||
is_natives = True
|
|
||||||
|
|
||||||
if not is_natives:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if rules and classifiers:
|
|
||||||
logger.debug(f"Found newer and legacy (classifier) native rules for {name}")
|
|
||||||
elif rules:
|
|
||||||
logger.debug(f"Found newer native rules for {name}")
|
|
||||||
elif classifiers:
|
|
||||||
logger.debug(f"Found legacy native rules (classifier) for {name}")
|
|
||||||
|
|
||||||
# =============== New rules ===============
|
|
||||||
|
|
||||||
# Parse action section
|
|
||||||
if not is_library_allowed(
|
|
||||||
rules,
|
|
||||||
platform_rule_name,
|
|
||||||
platform_arch_type,
|
|
||||||
os_version,
|
|
||||||
):
|
|
||||||
logger.debug(f"Library {name} can't pass library rules.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if is_native_allowed(
|
|
||||||
name,
|
|
||||||
platform_key_name,
|
|
||||||
platform_arch_type,
|
|
||||||
):
|
|
||||||
result_artifact = generate_formatted_artifact_with_name(name, artifact, extra={"exclude": exclude,
|
|
||||||
"flatten": True})
|
|
||||||
if result_artifact:
|
|
||||||
filtered.append(result_artifact)
|
|
||||||
# For some reason, we still need to continue process because some version use multiple type of
|
|
||||||
# classifier. (Such as 1.14.2)
|
|
||||||
else:
|
|
||||||
logger.debug(f"Library {name} can't pass native rules.")
|
|
||||||
|
|
||||||
# =============== Old rules ===============
|
|
||||||
logger.debug(
|
|
||||||
"Library {} doesn't have native key. This library may use old rule.".format(name)
|
|
||||||
)
|
|
||||||
|
|
||||||
result, result_artifact, classifier = is_legacy_native_allowed(
|
|
||||||
natives,
|
|
||||||
classifiers,
|
|
||||||
platform_rule_name,
|
|
||||||
platform_arch_type,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not result:
|
|
||||||
logger.debug(f"Library {name} can't pass legacy native rules.")
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
result_artifact = generate_formatted_artifact_with_name(name,
|
|
||||||
result_artifact,
|
|
||||||
extra={"exclude": exclude, "flatten": True},
|
|
||||||
relative_args={
|
|
||||||
"extra": classifier
|
|
||||||
})
|
|
||||||
if result_artifact:
|
|
||||||
filtered.append(result_artifact)
|
|
||||||
|
|
||||||
# Remove duplicate items
|
|
||||||
items = remove_duplicate_artifacts(filtered)
|
|
||||||
|
|
||||||
if not filtered:
|
|
||||||
raise NativeResolveException(
|
|
||||||
"No compatible native artifact found.",
|
|
||||||
user_message="No compatible native library was found for this platform.",
|
|
||||||
)
|
|
||||||
elif not items:
|
|
||||||
raise NativeResolveException(
|
|
||||||
"No compatible native artifact found. Report developer because this may be a issue"
|
|
||||||
" at function \"remove_duplicate_artifacts\"",
|
|
||||||
)
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
def download_libraries(artifacts, output_dir, progress_callback: Callable[[str, float], None] = None) \
|
|
||||||
-> tuple[list[FileObject], list[FileObject]]:
|
|
||||||
"""
|
|
||||||
Download libraries from a list that contains artifacts
|
|
||||||
:param artifacts:
|
|
||||||
:param output_dir:
|
|
||||||
:param progress_callback:
|
|
||||||
:return:
|
|
||||||
fails: list (List of artifacts that download failed)
|
|
||||||
successes: list (artifacts that were downloaded)
|
|
||||||
"""
|
|
||||||
files = []
|
|
||||||
successes = []
|
|
||||||
|
|
||||||
for artifact in artifacts:
|
|
||||||
sha1 = artifact.get("sha1")
|
|
||||||
relative_path = artifact.get("path")
|
|
||||||
url = artifact.get("url")
|
|
||||||
|
|
||||||
if not relative_path or not url:
|
|
||||||
logger.warning(
|
|
||||||
"Artifact {} has no path or url. Skipping.".format(artifact["name"])
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
full_path = output_dir / Path(relative_path)
|
|
||||||
|
|
||||||
file = FileObject(
|
|
||||||
full_path,
|
|
||||||
url=url,
|
|
||||||
sha1=sha1,
|
|
||||||
)
|
|
||||||
|
|
||||||
if sha1 is None:
|
|
||||||
logger.warning(f"File {full_path} has no SHA1 hash.")
|
|
||||||
|
|
||||||
if file.exists and (sha1 is None or file.verify("sha1", sha1)):
|
|
||||||
logger.debug(f"File {full_path} exists.")
|
|
||||||
successes.append(file)
|
|
||||||
continue
|
|
||||||
|
|
||||||
files.append(file)
|
|
||||||
|
|
||||||
fails, finished = download_multiple_files(files, progress_callback=progress_callback)
|
|
||||||
successes.extend(finished)
|
|
||||||
|
|
||||||
return fails, successes
|
|
||||||
|
|
||||||
|
|
||||||
def unzip_natives(filtered, libraries_dir, destination: Path):
|
|
||||||
"""
|
|
||||||
Unzip native libraries from a list that contains artifacts
|
|
||||||
:param filtered:
|
|
||||||
:param libraries_dir:
|
|
||||||
:param destination:
|
|
||||||
:return:
|
|
||||||
|
|
||||||
IMPORTANT:
|
|
||||||
Please put "exclude" key inside the artifact (from the version data). If not, some unnecessary file will be also
|
|
||||||
included when unzipping the native library.
|
|
||||||
"""
|
|
||||||
destination.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
for result_artifact in filtered:
|
|
||||||
exclude = result_artifact.get("exclude", [])
|
|
||||||
flatten = result_artifact.get("flatten", False)
|
|
||||||
relative_path = result_artifact.get("path")
|
|
||||||
|
|
||||||
if not relative_path:
|
|
||||||
logger.warning(
|
|
||||||
"Artifact {} has no relative path. Skipping.".format(result_artifact["name"])
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
full_path = libraries_dir / Path(relative_path)
|
|
||||||
|
|
||||||
unzip(full_path, destination, exclude=exclude, flatten=flatten)
|
|
||||||
|
|
||||||
logger.debug(f"Unzipped {full_path}.")
|
|
||||||
|
|
||||||
|
|
||||||
def check_libraries_exists(manifest: dict, libraries_dir: Path):
|
|
||||||
"""
|
|
||||||
Check if libraries are present in the libraries directory
|
|
||||||
:param manifest:
|
|
||||||
:param libraries_dir:
|
|
||||||
:return:
|
|
||||||
not_found: list
|
|
||||||
"""
|
|
||||||
libraries = manifest.get("libraries", [])
|
|
||||||
|
|
||||||
if not libraries:
|
|
||||||
raise VersionDataKeyNotFoundException(
|
|
||||||
"Libraries not found in manifest.",
|
|
||||||
)
|
|
||||||
|
|
||||||
not_found = []
|
|
||||||
|
|
||||||
# Same as natives
|
|
||||||
_, _, os_version = get_platform_info()
|
|
||||||
platform_key_name, platform_rule_name, platform_arch_type = get_natives_platform_info()
|
|
||||||
|
|
||||||
for library in libraries:
|
|
||||||
name = library.get("name", "")
|
|
||||||
artifact_raw = library.get("downloads", {}).get("artifact", {})
|
|
||||||
rules = library.get("rules", [])
|
|
||||||
classifiers = library.get("downloads", {}).get("classifiers", {})
|
|
||||||
natives = library.get("natives", {})
|
|
||||||
|
|
||||||
# Ignore natives
|
|
||||||
if (len(natives) > 0 or bool(re.search(r":natives-[^:]+$", name)) or
|
|
||||||
any(native.startswith("natives-") for native in classifiers)):
|
|
||||||
logger.info(f"Skipping {name} because its a native library.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not is_library_allowed(
|
|
||||||
rules,
|
|
||||||
platform_rule_name,
|
|
||||||
platform_arch_type,
|
|
||||||
os_version,
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
|
|
||||||
artifact = generate_formatted_artifact_with_name(
|
|
||||||
name,
|
|
||||||
artifact_raw
|
|
||||||
)
|
|
||||||
|
|
||||||
if not artifact:
|
|
||||||
logger.warning(f"Artifact {name} are not valid. Skipping...")
|
|
||||||
continue
|
|
||||||
|
|
||||||
relative_path = artifact.get("path")
|
|
||||||
full_path = libraries_dir / Path(relative_path)
|
|
||||||
|
|
||||||
if not full_path.is_file() or not full_path.exists():
|
|
||||||
logger.debug(f"Artifact {name} missing.")
|
|
||||||
not_found.append(full_path)
|
|
||||||
|
|
||||||
return not_found
|
|
||||||
|
|
||||||
|
|
||||||
def check_natives_exists(manifest: dict, libraries_dir: Path, natives_dir: Path):
|
|
||||||
filtered = []
|
|
||||||
|
|
||||||
# Convert current platform and architecture type to same formula
|
|
||||||
_, _, os_version = get_platform_info()
|
|
||||||
platform_key_name, platform_rule_name, platform_arch_type = get_natives_platform_info()
|
|
||||||
|
|
||||||
if platform_key_name is None or platform_arch_type is None or platform_rule_name is None:
|
|
||||||
raise UnsupportedPlatformException(
|
|
||||||
f"Unsupported platform: key={platform_key_name}, "
|
|
||||||
f"rule={platform_rule_name}, arch={platform_arch_type}"
|
|
||||||
)
|
|
||||||
|
|
||||||
_, _, _, _, libraries, _, _ = find_useful_part_in_specific_version_manifest(manifest)
|
|
||||||
|
|
||||||
for library in libraries:
|
|
||||||
name = library.get("name", "")
|
|
||||||
artifact = library.get("downloads", {}).get("artifact", {})
|
|
||||||
# For newer Minecraft (VER>1.7.10)
|
|
||||||
rules = library.get("rules", [])
|
|
||||||
# For legacy version (VER<1.8)
|
|
||||||
classifiers = library.get("downloads", {}).get("classifiers", {})
|
|
||||||
natives = library.get("natives", {})
|
|
||||||
exclude = library.get("extract", {}).get("exclude", ["META-INF/"]) # For extract use
|
|
||||||
|
|
||||||
is_natives = False
|
|
||||||
|
|
||||||
# Check if library is a native
|
|
||||||
if (len(natives) > 0 or bool(re.search(r":natives-[^:]+$", name)) or
|
|
||||||
any(native.startswith("natives-") for native in classifiers)):
|
|
||||||
is_natives = True
|
|
||||||
|
|
||||||
if not is_natives:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# =============== New rules ===============
|
|
||||||
|
|
||||||
# Parse action section
|
|
||||||
if not is_library_allowed(
|
|
||||||
rules,
|
|
||||||
platform_rule_name,
|
|
||||||
platform_arch_type,
|
|
||||||
os_version,
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if is_native_allowed(
|
|
||||||
name,
|
|
||||||
platform_key_name,
|
|
||||||
platform_arch_type,
|
|
||||||
):
|
|
||||||
result_artifact = generate_formatted_artifact_with_name(name, artifact, extra={"exclude": exclude,
|
|
||||||
"flatten": True})
|
|
||||||
if result_artifact:
|
|
||||||
filtered.append(result_artifact)
|
|
||||||
|
|
||||||
# =============== Old rules ===============
|
|
||||||
result, result_artifact, classifier = is_legacy_native_allowed(
|
|
||||||
natives,
|
|
||||||
classifiers,
|
|
||||||
platform_rule_name,
|
|
||||||
platform_arch_type,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not result:
|
|
||||||
continue
|
|
||||||
|
|
||||||
result_artifact = generate_formatted_artifact_with_name(name,
|
|
||||||
result_artifact,
|
|
||||||
extra={"exclude": exclude, "flatten": True},
|
|
||||||
relative_args={
|
|
||||||
"extra": classifier
|
|
||||||
})
|
|
||||||
|
|
||||||
if result_artifact:
|
|
||||||
filtered.append(result_artifact)
|
|
||||||
|
|
||||||
# Remove duplicate items
|
|
||||||
items = remove_duplicate_artifacts(filtered)
|
|
||||||
|
|
||||||
if not filtered:
|
|
||||||
raise NativeResolveException(
|
|
||||||
"No compatible native artifact found.",
|
|
||||||
user_message="No compatible native library was found for this platform.",
|
|
||||||
)
|
|
||||||
elif not items:
|
|
||||||
raise NativeResolveException(
|
|
||||||
"No compatible native artifact found. Report developer because this may be a issue"
|
|
||||||
" at function \"remove_duplicate_artifacts\"",
|
|
||||||
)
|
|
||||||
|
|
||||||
expected = set()
|
|
||||||
|
|
||||||
for result_artifact in items:
|
|
||||||
full_path = libraries_dir / Path(result_artifact["path"])
|
|
||||||
exclude = result_artifact.get("exclude", []) or []
|
|
||||||
flatten = result_artifact.get("flatten", False)
|
|
||||||
|
|
||||||
if not full_path.is_file() or not full_path.exists():
|
|
||||||
return False
|
|
||||||
|
|
||||||
with zipfile.ZipFile(full_path) as zf:
|
|
||||||
for member in zf.namelist():
|
|
||||||
if member.endswith("/"):
|
|
||||||
continue
|
|
||||||
if any(member.startswith(item) for item in exclude):
|
|
||||||
continue
|
|
||||||
|
|
||||||
target = Path(member).name if flatten else member
|
|
||||||
if target:
|
|
||||||
expected.add(target)
|
|
||||||
|
|
||||||
if not expected:
|
|
||||||
return False
|
|
||||||
|
|
||||||
for target in expected:
|
|
||||||
if not (natives_dir / target).exists():
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def check_full_libraries_exists(manifest: dict, libraries_dir: Path, natives_dir: Path) -> bool:
|
|
||||||
if not check_natives_exists(manifest, libraries_dir, natives_dir) or not check_libraries_exists(manifest, libraries_dir):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def generate_classpath(manifest: dict, libraries_dir: Path, core_file_path: Path):
|
|
||||||
"""
|
|
||||||
Generate classpath by using specific version's data
|
|
||||||
:param manifest:
|
|
||||||
:param libraries_dir:
|
|
||||||
:param core_file_path:
|
|
||||||
:return:
|
|
||||||
not_found: list
|
|
||||||
"""
|
|
||||||
libraries = manifest.get("libraries", [])
|
|
||||||
|
|
||||||
if not libraries:
|
|
||||||
raise VersionDataKeyNotFoundException(
|
|
||||||
"Libraries not found in manifest.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Same as natives
|
|
||||||
_, _, os_version = get_platform_info()
|
|
||||||
platform_key_name, platform_rule_name, platform_arch_type = get_natives_platform_info()
|
|
||||||
|
|
||||||
separator = ";" if platform_key_name == "windows" else ":"
|
|
||||||
classes = []
|
|
||||||
|
|
||||||
for library in libraries:
|
|
||||||
name = library.get("name", "")
|
|
||||||
artifact_raw = library.get("downloads", {}).get("artifact", {})
|
|
||||||
rules = library.get("rules", [])
|
|
||||||
classifiers = library.get("downloads", {}).get("classifiers", {})
|
|
||||||
natives = library.get("natives", {})
|
|
||||||
|
|
||||||
# Ignore natives
|
|
||||||
if (len(natives) > 0 or bool(re.search(r":natives-[^:]+$", name)) or
|
|
||||||
any(native.startswith("natives-") for native in classifiers)):
|
|
||||||
logger.info(f"Skipping {name} because its a native library.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not is_library_allowed(
|
|
||||||
rules,
|
|
||||||
platform_rule_name,
|
|
||||||
platform_arch_type,
|
|
||||||
os_version,
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
|
|
||||||
artifact = generate_formatted_artifact_with_name(
|
|
||||||
name,
|
|
||||||
artifact_raw
|
|
||||||
)
|
|
||||||
|
|
||||||
if not artifact:
|
|
||||||
logger.warning(f"Artifact {name} are not valid. Skipping...")
|
|
||||||
continue
|
|
||||||
|
|
||||||
relative_path = artifact.get("path")
|
|
||||||
full_path = libraries_dir / Path(relative_path)
|
|
||||||
|
|
||||||
if not full_path.is_file() or not full_path.exists():
|
|
||||||
raise FileNotFoundError(f"Artifact {name} missing.")
|
|
||||||
|
|
||||||
classes.append(full_path.as_posix())
|
|
||||||
|
|
||||||
# Append core file
|
|
||||||
classes.append(core_file_path.as_posix())
|
|
||||||
|
|
||||||
return f"{separator}".join(classes)
|
|
||||||
|
|
||||||
|
|
||||||
def download_full_libraries(manifest: dict, libraries_dir: Path, natives_dir: Path,
|
|
||||||
progress_callback: Callable[[str, float], None] = None,
|
|
||||||
redownload_callback: Callable[
|
|
||||||
[list[FileObject]], tuple[bool, list[FileObject]]] = None) -> None:
|
|
||||||
# libraries
|
|
||||||
libraries = collect_library_artifacts(manifest)
|
|
||||||
|
|
||||||
# natives
|
|
||||||
natives = collect_native_artifacts(manifest)
|
|
||||||
|
|
||||||
# all
|
|
||||||
full = list(libraries)
|
|
||||||
full.extend(natives)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
"Supported native count: {}".format(len(natives))
|
|
||||||
)
|
|
||||||
|
|
||||||
# Start download
|
|
||||||
fails, successes = download_libraries(full, libraries_dir, progress_callback=progress_callback)
|
|
||||||
|
|
||||||
download_ok = len(fails) == 0
|
|
||||||
|
|
||||||
if redownload_callback and not download_ok:
|
|
||||||
download_ok, fails = redownload_callback(fails)
|
|
||||||
|
|
||||||
if not download_ok:
|
|
||||||
raise DownloadException(
|
|
||||||
"Unable to continue process due to certain libraries download failed:\n"
|
|
||||||
+ "\n".join(file.posix_path for file in fails)
|
|
||||||
)
|
|
||||||
|
|
||||||
if download_ok:
|
|
||||||
# Start unzip natives
|
|
||||||
unzip_natives(natives, libraries_dir, natives_dir)
|
|
||||||
else:
|
|
||||||
raise DependencyException(
|
|
||||||
"Certain libraries download failed. Unable to continue unzip process.\n"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -0,0 +1,589 @@
|
|||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from .library import get_natives_platform_info, is_library_allowed, generate_formatted_artifact_with_name, \
|
||||||
|
is_native_allowed, is_legacy_native_allowed, remove_duplicate_artifacts
|
||||||
|
from .libraryx import normalize_library_to_common_struct
|
||||||
|
from ..common.common import get_platform_info, FileObject, download_multiple_files, unzip, is_valid_zipfile
|
||||||
|
from .version_info import find_useful_part_in_specific_version_manifest
|
||||||
|
from ..common.exception import UnsupportedPlatformException, NativeResolveException, DependencyException, \
|
||||||
|
VersionDataKeyNotFoundException, DownloadException
|
||||||
|
|
||||||
|
logger = logging.getLogger("Launcher.CoreLib")
|
||||||
|
|
||||||
|
def collect_library_artifacts(specific_version_manifest):
|
||||||
|
_, _, _, _, libraries, _, _ = find_useful_part_in_specific_version_manifest(specific_version_manifest)
|
||||||
|
|
||||||
|
items = []
|
||||||
|
|
||||||
|
# Same as natives
|
||||||
|
_, _, os_version = get_platform_info()
|
||||||
|
platform_key_name, platform_rule_name, platform_arch_type = get_natives_platform_info()
|
||||||
|
|
||||||
|
for library_raw in libraries:
|
||||||
|
library = normalize_library_to_common_struct(
|
||||||
|
library_raw
|
||||||
|
)
|
||||||
|
|
||||||
|
name = library.get("name", "")
|
||||||
|
artifact_raw = library.get("downloads", {}).get("artifact", {})
|
||||||
|
rules = library.get("rules", [])
|
||||||
|
classifiers = library.get("downloads", {}).get("classifiers", {})
|
||||||
|
natives = library.get("natives", {})
|
||||||
|
|
||||||
|
# Ignore natives
|
||||||
|
if (len(natives) > 0 or bool(re.search(r":natives-[^:]+$", name)) or
|
||||||
|
any(native.startswith("natives-") for native in classifiers)):
|
||||||
|
logger.info(f"Skipping {name} because its a native library.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not is_library_allowed(
|
||||||
|
rules,
|
||||||
|
platform_rule_name,
|
||||||
|
platform_arch_type,
|
||||||
|
os_version,
|
||||||
|
):
|
||||||
|
logger.debug(f"Library {name} can't pass library rules.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
artifact = generate_formatted_artifact_with_name(
|
||||||
|
name,
|
||||||
|
artifact_raw
|
||||||
|
)
|
||||||
|
|
||||||
|
if artifact:
|
||||||
|
items.append(artifact)
|
||||||
|
|
||||||
|
if not items:
|
||||||
|
raise DependencyException(
|
||||||
|
"Could not find any artifact in any of the libraries."
|
||||||
|
)
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def collect_native_artifacts(specific_version_manifest):
|
||||||
|
"""
|
||||||
|
Collects native artifacts that support current platform
|
||||||
|
:param specific_version_manifest:
|
||||||
|
:return:
|
||||||
|
filtered_natives_artifacts: list[dict]
|
||||||
|
"""
|
||||||
|
filtered = []
|
||||||
|
|
||||||
|
# Convert current platform and architecture type to same formula
|
||||||
|
_, _, os_version = get_platform_info()
|
||||||
|
platform_key_name, platform_rule_name, platform_arch_type = get_natives_platform_info()
|
||||||
|
|
||||||
|
if platform_key_name is None or platform_arch_type is None or platform_rule_name is None:
|
||||||
|
raise UnsupportedPlatformException(
|
||||||
|
f"Unsupported platform: key={platform_key_name}, "
|
||||||
|
f"rule={platform_rule_name}, arch={platform_arch_type}"
|
||||||
|
)
|
||||||
|
|
||||||
|
_, _, _, _, libraries, _, _ = find_useful_part_in_specific_version_manifest(specific_version_manifest)
|
||||||
|
|
||||||
|
for library_raw in libraries:
|
||||||
|
library = normalize_library_to_common_struct(
|
||||||
|
library_raw
|
||||||
|
)
|
||||||
|
|
||||||
|
name = library.get("name", "")
|
||||||
|
artifact = library.get("downloads", {}).get("artifact", {})
|
||||||
|
# For newer Minecraft (VER>1.7.10)
|
||||||
|
rules = library.get("rules", [])
|
||||||
|
# For legacy version (VER<1.8)
|
||||||
|
classifiers = library.get("downloads", {}).get("classifiers", {})
|
||||||
|
natives = library.get("natives", {})
|
||||||
|
exclude = library.get("extract", {}).get("exclude", ["META-INF/"]) # For extract use
|
||||||
|
|
||||||
|
is_natives = False
|
||||||
|
|
||||||
|
# Check if library is a native
|
||||||
|
if (len(natives) > 0 or bool(re.search(r":natives-[^:]+$", name)) or
|
||||||
|
any(native.startswith("natives-") for native in classifiers)):
|
||||||
|
is_natives = True
|
||||||
|
|
||||||
|
if not is_natives:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if rules and classifiers:
|
||||||
|
logger.debug(f"Found newer and legacy (classifier) native rules for {name}")
|
||||||
|
elif rules:
|
||||||
|
logger.debug(f"Found newer native rules for {name}")
|
||||||
|
elif classifiers:
|
||||||
|
logger.debug(f"Found legacy native rules (classifier) for {name}")
|
||||||
|
|
||||||
|
# =============== New rules ===============
|
||||||
|
|
||||||
|
# Parse action section
|
||||||
|
if not is_library_allowed(
|
||||||
|
rules,
|
||||||
|
platform_rule_name,
|
||||||
|
platform_arch_type,
|
||||||
|
os_version,
|
||||||
|
):
|
||||||
|
logger.debug(f"Library {name} can't pass library rules.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if is_native_allowed(
|
||||||
|
name,
|
||||||
|
platform_key_name,
|
||||||
|
platform_arch_type,
|
||||||
|
):
|
||||||
|
result_artifact = generate_formatted_artifact_with_name(name, artifact, extra={"exclude": exclude,
|
||||||
|
"flatten": True})
|
||||||
|
if result_artifact:
|
||||||
|
filtered.append(result_artifact)
|
||||||
|
# For some reason, we still need to continue process because some version use multiple type of
|
||||||
|
# classifier. (Such as 1.14.2)
|
||||||
|
else:
|
||||||
|
logger.debug(f"Library {name} can't pass native rules.")
|
||||||
|
|
||||||
|
# =============== Old rules ===============
|
||||||
|
logger.debug(
|
||||||
|
"Library {} doesn't have native key. This library may use old rule.".format(name)
|
||||||
|
)
|
||||||
|
|
||||||
|
result, result_artifact, classifier = is_legacy_native_allowed(
|
||||||
|
natives,
|
||||||
|
classifiers,
|
||||||
|
platform_rule_name,
|
||||||
|
platform_arch_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
logger.debug(f"Library {name} can't pass legacy native rules.")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
result_artifact = generate_formatted_artifact_with_name(name,
|
||||||
|
result_artifact,
|
||||||
|
extra={"exclude": exclude, "flatten": True},
|
||||||
|
relative_args={
|
||||||
|
"extra": classifier
|
||||||
|
})
|
||||||
|
if result_artifact:
|
||||||
|
filtered.append(result_artifact)
|
||||||
|
|
||||||
|
# Remove duplicate items
|
||||||
|
items = remove_duplicate_artifacts(filtered)
|
||||||
|
|
||||||
|
if not filtered:
|
||||||
|
raise NativeResolveException(
|
||||||
|
"No compatible native artifact found.",
|
||||||
|
user_message="No compatible native library was found for this platform.",
|
||||||
|
)
|
||||||
|
elif not items:
|
||||||
|
raise NativeResolveException(
|
||||||
|
"No compatible native artifact found. Report developer because this may be a issue"
|
||||||
|
" at function \"remove_duplicate_artifacts\"",
|
||||||
|
)
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
def download_libraries(artifacts, output_dir, progress_callback: Callable[[str, float], None] = None) \
|
||||||
|
-> tuple[list[FileObject], list[FileObject]]:
|
||||||
|
"""
|
||||||
|
Download libraries from a list that contains artifacts
|
||||||
|
:param artifacts:
|
||||||
|
:param output_dir:
|
||||||
|
:param progress_callback:
|
||||||
|
:return:
|
||||||
|
fails: list (List of artifacts that download failed)
|
||||||
|
successes: list (artifacts that were downloaded)
|
||||||
|
"""
|
||||||
|
files = []
|
||||||
|
successes = []
|
||||||
|
|
||||||
|
for artifact in artifacts:
|
||||||
|
sha1 = artifact.get("sha1")
|
||||||
|
sha256 = artifact.get("sha256")
|
||||||
|
sha512 = artifact.get("sha512")
|
||||||
|
relative_path = artifact.get("path")
|
||||||
|
url = artifact.get("url")
|
||||||
|
|
||||||
|
if not relative_path or not url:
|
||||||
|
logger.warning(
|
||||||
|
"Artifact {} has no path or url. Skipping.".format(artifact["name"])
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
full_path = output_dir / Path(relative_path)
|
||||||
|
|
||||||
|
file = FileObject(
|
||||||
|
full_path,
|
||||||
|
url=url,
|
||||||
|
sha1=sha1,
|
||||||
|
)
|
||||||
|
|
||||||
|
if sha1 is None:
|
||||||
|
logger.warning(f"File {full_path} has no SHA1 hash.")
|
||||||
|
|
||||||
|
if file.exists:
|
||||||
|
if sha1 is not None:
|
||||||
|
valid = file.verify("sha1", sha1)
|
||||||
|
elif sha256 is not None:
|
||||||
|
valid = file.verify("sha256", sha256)
|
||||||
|
elif sha512 is not None:
|
||||||
|
valid = file.verify("sha512", sha512)
|
||||||
|
elif full_path.suffix.lower() == ".jar":
|
||||||
|
valid = is_valid_zipfile(file.path)
|
||||||
|
else:
|
||||||
|
valid = full_path.stat().st_size > 0
|
||||||
|
|
||||||
|
if valid:
|
||||||
|
successes.append(file)
|
||||||
|
continue
|
||||||
|
|
||||||
|
files.append(file)
|
||||||
|
|
||||||
|
fails, finished = download_multiple_files(files, progress_callback=progress_callback)
|
||||||
|
successes.extend(finished)
|
||||||
|
|
||||||
|
return fails, successes
|
||||||
|
|
||||||
|
|
||||||
|
def unzip_natives(filtered, libraries_dir, destination: Path):
|
||||||
|
"""
|
||||||
|
Unzip native libraries from a list that contains artifacts
|
||||||
|
:param filtered:
|
||||||
|
:param libraries_dir:
|
||||||
|
:param destination:
|
||||||
|
:return:
|
||||||
|
|
||||||
|
IMPORTANT:
|
||||||
|
Please put "exclude" key inside the artifact (from the version data). If not, some unnecessary file will be also
|
||||||
|
included when unzipping the native library.
|
||||||
|
"""
|
||||||
|
destination.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
for result_artifact in filtered:
|
||||||
|
exclude = result_artifact.get("exclude", [])
|
||||||
|
flatten = result_artifact.get("flatten", False)
|
||||||
|
relative_path = result_artifact.get("path")
|
||||||
|
|
||||||
|
if not relative_path:
|
||||||
|
logger.warning(
|
||||||
|
"Artifact {} has no relative path. Skipping.".format(result_artifact["name"])
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
full_path = libraries_dir / Path(relative_path)
|
||||||
|
|
||||||
|
unzip(full_path, destination, exclude=exclude, flatten=flatten)
|
||||||
|
|
||||||
|
logger.debug(f"Unzipped {full_path}.")
|
||||||
|
|
||||||
|
|
||||||
|
def check_libraries_exists(manifest: dict, libraries_dir: Path):
|
||||||
|
"""
|
||||||
|
Check if libraries are present in the libraries directory
|
||||||
|
:param manifest:
|
||||||
|
:param libraries_dir:
|
||||||
|
:return:
|
||||||
|
not_found: list
|
||||||
|
"""
|
||||||
|
libraries = manifest.get("libraries", [])
|
||||||
|
|
||||||
|
if not libraries:
|
||||||
|
raise VersionDataKeyNotFoundException(
|
||||||
|
"Libraries not found in manifest.",
|
||||||
|
)
|
||||||
|
|
||||||
|
not_found = []
|
||||||
|
|
||||||
|
# Same as natives
|
||||||
|
_, _, os_version = get_platform_info()
|
||||||
|
platform_key_name, platform_rule_name, platform_arch_type = get_natives_platform_info()
|
||||||
|
|
||||||
|
for library_raw in libraries:
|
||||||
|
library = normalize_library_to_common_struct(
|
||||||
|
library_raw
|
||||||
|
)
|
||||||
|
|
||||||
|
name = library.get("name", "")
|
||||||
|
artifact_raw = library.get("downloads", {}).get("artifact", {})
|
||||||
|
rules = library.get("rules", [])
|
||||||
|
classifiers = library.get("downloads", {}).get("classifiers", {})
|
||||||
|
natives = library.get("natives", {})
|
||||||
|
|
||||||
|
# Ignore natives
|
||||||
|
if (len(natives) > 0 or bool(re.search(r":natives-[^:]+$", name)) or
|
||||||
|
any(native.startswith("natives-") for native in classifiers)):
|
||||||
|
logger.info(f"Skipping {name} because its a native library.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not is_library_allowed(
|
||||||
|
rules,
|
||||||
|
platform_rule_name,
|
||||||
|
platform_arch_type,
|
||||||
|
os_version,
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
artifact = generate_formatted_artifact_with_name(
|
||||||
|
name,
|
||||||
|
artifact_raw
|
||||||
|
)
|
||||||
|
|
||||||
|
if not artifact:
|
||||||
|
logger.warning(f"Artifact {name} are not valid. Skipping...")
|
||||||
|
continue
|
||||||
|
|
||||||
|
relative_path = artifact.get("path")
|
||||||
|
full_path = libraries_dir / Path(relative_path)
|
||||||
|
|
||||||
|
if not full_path.is_file() or not full_path.exists():
|
||||||
|
logger.debug(f"Artifact {name} missing.")
|
||||||
|
not_found.append(full_path)
|
||||||
|
|
||||||
|
return not_found
|
||||||
|
|
||||||
|
|
||||||
|
def check_natives_exists(manifest: dict, libraries_dir: Path, natives_dir: Path):
|
||||||
|
filtered = []
|
||||||
|
|
||||||
|
# Convert current platform and architecture type to same formula
|
||||||
|
_, _, os_version = get_platform_info()
|
||||||
|
platform_key_name, platform_rule_name, platform_arch_type = get_natives_platform_info()
|
||||||
|
|
||||||
|
if platform_key_name is None or platform_arch_type is None or platform_rule_name is None:
|
||||||
|
raise UnsupportedPlatformException(
|
||||||
|
f"Unsupported platform: key={platform_key_name}, "
|
||||||
|
f"rule={platform_rule_name}, arch={platform_arch_type}"
|
||||||
|
)
|
||||||
|
|
||||||
|
_, _, _, _, libraries, _, _ = find_useful_part_in_specific_version_manifest(manifest)
|
||||||
|
|
||||||
|
for library_raw in libraries:
|
||||||
|
library = normalize_library_to_common_struct(
|
||||||
|
library_raw
|
||||||
|
)
|
||||||
|
|
||||||
|
name = library.get("name", "")
|
||||||
|
artifact = library.get("downloads", {}).get("artifact", {})
|
||||||
|
# For newer Minecraft (VER>1.7.10)
|
||||||
|
rules = library.get("rules", [])
|
||||||
|
# For legacy version (VER<1.8)
|
||||||
|
classifiers = library.get("downloads", {}).get("classifiers", {})
|
||||||
|
natives = library.get("natives", {})
|
||||||
|
exclude = library.get("extract", {}).get("exclude", ["META-INF/"]) # For extract use
|
||||||
|
|
||||||
|
is_natives = False
|
||||||
|
|
||||||
|
# Check if library is a native
|
||||||
|
if (len(natives) > 0 or bool(re.search(r":natives-[^:]+$", name)) or
|
||||||
|
any(native.startswith("natives-") for native in classifiers)):
|
||||||
|
is_natives = True
|
||||||
|
|
||||||
|
if not is_natives:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# =============== New rules ===============
|
||||||
|
|
||||||
|
# Parse action section
|
||||||
|
if not is_library_allowed(
|
||||||
|
rules,
|
||||||
|
platform_rule_name,
|
||||||
|
platform_arch_type,
|
||||||
|
os_version,
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if is_native_allowed(
|
||||||
|
name,
|
||||||
|
platform_key_name,
|
||||||
|
platform_arch_type,
|
||||||
|
):
|
||||||
|
result_artifact = generate_formatted_artifact_with_name(name, artifact, extra={"exclude": exclude,
|
||||||
|
"flatten": True})
|
||||||
|
if result_artifact:
|
||||||
|
filtered.append(result_artifact)
|
||||||
|
|
||||||
|
# =============== Old rules ===============
|
||||||
|
result, result_artifact, classifier = is_legacy_native_allowed(
|
||||||
|
natives,
|
||||||
|
classifiers,
|
||||||
|
platform_rule_name,
|
||||||
|
platform_arch_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
continue
|
||||||
|
|
||||||
|
result_artifact = generate_formatted_artifact_with_name(name,
|
||||||
|
result_artifact,
|
||||||
|
extra={"exclude": exclude, "flatten": True},
|
||||||
|
relative_args={
|
||||||
|
"extra": classifier
|
||||||
|
})
|
||||||
|
|
||||||
|
if result_artifact:
|
||||||
|
filtered.append(result_artifact)
|
||||||
|
|
||||||
|
# Remove duplicate items
|
||||||
|
items = remove_duplicate_artifacts(filtered)
|
||||||
|
|
||||||
|
if not filtered:
|
||||||
|
raise NativeResolveException(
|
||||||
|
"No compatible native artifact found.",
|
||||||
|
user_message="No compatible native library was found for this platform.",
|
||||||
|
)
|
||||||
|
elif not items:
|
||||||
|
raise NativeResolveException(
|
||||||
|
"No compatible native artifact found. Report developer because this may be a issue"
|
||||||
|
" at function \"remove_duplicate_artifacts\"",
|
||||||
|
)
|
||||||
|
|
||||||
|
expected = set()
|
||||||
|
|
||||||
|
for result_artifact in items:
|
||||||
|
full_path = libraries_dir / Path(result_artifact["path"])
|
||||||
|
exclude = result_artifact.get("exclude", []) or []
|
||||||
|
flatten = result_artifact.get("flatten", False)
|
||||||
|
|
||||||
|
if not full_path.is_file() or not full_path.exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
with zipfile.ZipFile(full_path) as zf:
|
||||||
|
for member in zf.namelist():
|
||||||
|
if member.endswith("/"):
|
||||||
|
continue
|
||||||
|
if any(member.startswith(item) for item in exclude):
|
||||||
|
continue
|
||||||
|
|
||||||
|
target = Path(member).name if flatten else member
|
||||||
|
if target:
|
||||||
|
expected.add(target)
|
||||||
|
|
||||||
|
if not expected:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for target in expected:
|
||||||
|
if not (natives_dir / target).exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_full_libraries_exists(manifest: dict, libraries_dir: Path, natives_dir: Path) -> bool:
|
||||||
|
if not check_natives_exists(manifest, libraries_dir, natives_dir) or not check_libraries_exists(manifest, libraries_dir):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def generate_classpath(manifest: dict, libraries_dir: Path, core_file_path: Path):
|
||||||
|
"""
|
||||||
|
Generate classpath by using specific version's data
|
||||||
|
:param manifest:
|
||||||
|
:param libraries_dir:
|
||||||
|
:param core_file_path:
|
||||||
|
:return:
|
||||||
|
not_found: list
|
||||||
|
"""
|
||||||
|
libraries = manifest.get("libraries", [])
|
||||||
|
|
||||||
|
if not libraries:
|
||||||
|
raise VersionDataKeyNotFoundException(
|
||||||
|
"Libraries not found in manifest.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Same as natives
|
||||||
|
_, _, os_version = get_platform_info()
|
||||||
|
platform_key_name, platform_rule_name, platform_arch_type = get_natives_platform_info()
|
||||||
|
|
||||||
|
separator = ";" if platform_key_name == "windows" else ":"
|
||||||
|
classes = []
|
||||||
|
|
||||||
|
for library_raw in libraries:
|
||||||
|
library = normalize_library_to_common_struct(
|
||||||
|
library_raw
|
||||||
|
)
|
||||||
|
|
||||||
|
name = library.get("name", "")
|
||||||
|
artifact_raw = library.get("downloads", {}).get("artifact", {})
|
||||||
|
rules = library.get("rules", [])
|
||||||
|
classifiers = library.get("downloads", {}).get("classifiers", {})
|
||||||
|
natives = library.get("natives", {})
|
||||||
|
|
||||||
|
# Ignore natives
|
||||||
|
if (len(natives) > 0 or bool(re.search(r":natives-[^:]+$", name)) or
|
||||||
|
any(native.startswith("natives-") for native in classifiers)):
|
||||||
|
logger.info(f"Skipping {name} because its a native library.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not is_library_allowed(
|
||||||
|
rules,
|
||||||
|
platform_rule_name,
|
||||||
|
platform_arch_type,
|
||||||
|
os_version,
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
artifact = generate_formatted_artifact_with_name(
|
||||||
|
name,
|
||||||
|
artifact_raw
|
||||||
|
)
|
||||||
|
|
||||||
|
if not artifact:
|
||||||
|
logger.warning(f"Artifact {name} are not valid. Skipping...")
|
||||||
|
continue
|
||||||
|
|
||||||
|
relative_path = artifact.get("path")
|
||||||
|
full_path = libraries_dir / Path(relative_path)
|
||||||
|
|
||||||
|
if not full_path.is_file() or not full_path.exists():
|
||||||
|
raise FileNotFoundError(f"Artifact {name} missing.")
|
||||||
|
|
||||||
|
classes.append(full_path.as_posix())
|
||||||
|
|
||||||
|
# Append core file
|
||||||
|
classes.append(core_file_path.as_posix())
|
||||||
|
|
||||||
|
return f"{separator}".join(classes)
|
||||||
|
|
||||||
|
|
||||||
|
def download_full_libraries(manifest: dict, libraries_dir: Path, natives_dir: Path,
|
||||||
|
progress_callback: Callable[[str, float], None] = None,
|
||||||
|
redownload_callback: Callable[
|
||||||
|
[list[FileObject]], tuple[bool, list[FileObject]]] = None) -> None:
|
||||||
|
# libraries
|
||||||
|
libraries = collect_library_artifacts(manifest)
|
||||||
|
|
||||||
|
# natives
|
||||||
|
natives = collect_native_artifacts(manifest)
|
||||||
|
|
||||||
|
# all
|
||||||
|
full = list(libraries)
|
||||||
|
full.extend(natives)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Supported native count: {}".format(len(natives))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start download
|
||||||
|
fails, successes = download_libraries(full, libraries_dir, progress_callback=progress_callback)
|
||||||
|
|
||||||
|
download_ok = len(fails) == 0
|
||||||
|
|
||||||
|
if redownload_callback and not download_ok:
|
||||||
|
download_ok, fails = redownload_callback(fails)
|
||||||
|
|
||||||
|
if not download_ok:
|
||||||
|
raise DownloadException(
|
||||||
|
"Unable to continue process due to certain libraries download failed:\n"
|
||||||
|
+ "\n".join(file.posix_path for file in fails)
|
||||||
|
)
|
||||||
|
|
||||||
|
if download_ok:
|
||||||
|
# Start unzip natives
|
||||||
|
unzip_natives(natives, libraries_dir, natives_dir)
|
||||||
|
else:
|
||||||
|
raise DependencyException(
|
||||||
|
"Certain libraries download failed. Unable to continue unzip process.\n"
|
||||||
|
)
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
"""
|
||||||
|
This module provide some advanced function for library parse/download process use.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from .library import (convert_maven_version, parse_maven_coordinate, generate_relative_path_by_maven_coordinate,
|
||||||
|
is_complete_maven_url, generate_repo_url)
|
||||||
|
from .version_info import MOJANG_LIBRARIES_ENDPOINT
|
||||||
|
from ..common.exception import VersionDataKeyNotFoundException, DependencyException
|
||||||
|
|
||||||
|
logger = logging.getLogger("Launcher.CoreLib")
|
||||||
|
|
||||||
|
|
||||||
|
def convert_maven_version_with_extra(version: str, failback=None) -> tuple[tuple[int, int, int] | None, str | None]:
|
||||||
|
match = re.fullmatch(
|
||||||
|
r"(\d+(?:\.\d+){1,2})(.*)",
|
||||||
|
version,
|
||||||
|
)
|
||||||
|
|
||||||
|
if match is None:
|
||||||
|
return failback, None
|
||||||
|
|
||||||
|
base_ver = match.group(1)
|
||||||
|
extra_ver = match.group(2) or None
|
||||||
|
|
||||||
|
base_ver_converted = convert_maven_version(base_ver, failback=None)
|
||||||
|
|
||||||
|
if base_ver_converted is None:
|
||||||
|
return failback, extra_ver
|
||||||
|
|
||||||
|
return base_ver_converted, extra_ver
|
||||||
|
|
||||||
|
def generate_unique_library_key(group: str, artifact: str, classifier: str | None=None, extension: str | None=None, version: str | None=None) -> str:
|
||||||
|
key = f"{group}:{artifact}"
|
||||||
|
if version is not None:
|
||||||
|
key += f":{version}"
|
||||||
|
|
||||||
|
if classifier is not None:
|
||||||
|
key += f":{classifier}"
|
||||||
|
|
||||||
|
if extension is not None:
|
||||||
|
key += f":{extension}"
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_library_to_common_struct(library: dict, default_maven_url: str = MOJANG_LIBRARIES_ENDPOINT) -> dict:
|
||||||
|
result = deepcopy(library)
|
||||||
|
name: str = library.get("name")
|
||||||
|
|
||||||
|
if name is None:
|
||||||
|
raise VersionDataKeyNotFoundException(
|
||||||
|
"Provided mod loader version manifest library does not contain key \"name\"."
|
||||||
|
)
|
||||||
|
|
||||||
|
group, artifact, version, classifier, extension = parse_maven_coordinate(name)
|
||||||
|
|
||||||
|
downloads_raw = result.setdefault("downloads", {})
|
||||||
|
|
||||||
|
if isinstance(downloads_raw, dict):
|
||||||
|
downloads = downloads_raw
|
||||||
|
else:
|
||||||
|
downloads = {}
|
||||||
|
|
||||||
|
artifact_raw = dict(downloads.get("artifact") or {})
|
||||||
|
|
||||||
|
# Generate new relative path and put back
|
||||||
|
if not artifact_raw.get("path"):
|
||||||
|
path = generate_relative_path_by_maven_coordinate(name)
|
||||||
|
if not path:
|
||||||
|
raise DependencyException(
|
||||||
|
f"Unable to generate artifact path for {name}"
|
||||||
|
)
|
||||||
|
artifact_raw["path"] = path
|
||||||
|
|
||||||
|
if not artifact_raw.get("url"):
|
||||||
|
top_level_url = result.get("url")
|
||||||
|
|
||||||
|
if top_level_url:
|
||||||
|
if is_complete_maven_url(
|
||||||
|
top_level_url,
|
||||||
|
group,
|
||||||
|
artifact,
|
||||||
|
version,
|
||||||
|
classifier,
|
||||||
|
extension,
|
||||||
|
):
|
||||||
|
# Put existing url to downloads.artifact
|
||||||
|
artifact_raw["url"] = top_level_url
|
||||||
|
else:
|
||||||
|
# Replace broken url
|
||||||
|
artifact_raw["url"] = generate_repo_url(
|
||||||
|
top_level_url,
|
||||||
|
group,
|
||||||
|
artifact,
|
||||||
|
version,
|
||||||
|
classifier,
|
||||||
|
extension,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Generate missing url
|
||||||
|
artifact_raw["url"] = generate_repo_url(
|
||||||
|
default_maven_url,
|
||||||
|
group,
|
||||||
|
artifact,
|
||||||
|
version,
|
||||||
|
classifier,
|
||||||
|
extension,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Put hash information to artifact
|
||||||
|
for key in (
|
||||||
|
"sha1",
|
||||||
|
"sha256",
|
||||||
|
"sha512",
|
||||||
|
"md5",
|
||||||
|
"size",
|
||||||
|
):
|
||||||
|
if artifact_raw.get(key) is None:
|
||||||
|
value = result.get(key)
|
||||||
|
if value is not None:
|
||||||
|
artifact_raw[key] = value
|
||||||
|
|
||||||
|
# Put updated data back to result
|
||||||
|
downloads["artifact"] = artifact_raw
|
||||||
|
result["downloads"] = downloads
|
||||||
|
|
||||||
|
return result
|
||||||
@@ -9,8 +9,8 @@ from core_lib.common.exception import VersionManifestFetchException, VersionData
|
|||||||
logger = logging.getLogger("Launcher.CoreLib")
|
logger = logging.getLogger("Launcher.CoreLib")
|
||||||
|
|
||||||
FABRIC_META_VERSION_ENDPOINT_V2 = "https://meta.fabricmc.net/v2/versions"
|
FABRIC_META_VERSION_ENDPOINT_V2 = "https://meta.fabricmc.net/v2/versions"
|
||||||
FABRIC_META_LOADER_VERSION_ENDPOINT_V2 = "https://meta.fabricmc.net/v1/versions/loader/{}/"
|
FABRIC_META_LOADER_VERSION_ENDPOINT_V2 = "https://meta.fabricmc.net/v2/versions/loader/{}/"
|
||||||
FABRIC_META_LOADER_MANIFEST_ENDPOINT_V2 = "https://meta.fabricmc.net//v2/versions/loader/{}/{}/profile/json"
|
FABRIC_META_LOADER_MANIFEST_ENDPOINT_V2 = "https://meta.fabricmc.net/v2/versions/loader/{}/{}/profile/json"
|
||||||
FABRIC_MAVEN_URL = "https://maven.fabricmc.net/"
|
FABRIC_MAVEN_URL = "https://maven.fabricmc.net/"
|
||||||
|
|
||||||
def fetch_fabric_version_list(version_manifest_url: str=FABRIC_META_VERSION_ENDPOINT_V2, timeout: int=5) -> list[dict]:
|
def fetch_fabric_version_list(version_manifest_url: str=FABRIC_META_VERSION_ENDPOINT_V2, timeout: int=5) -> list[dict]:
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ from copy import deepcopy
|
|||||||
import shlex
|
import shlex
|
||||||
|
|
||||||
from core_lib.common.exception import VersionDataKeyNotFoundException, DependencyException
|
from core_lib.common.exception import VersionDataKeyNotFoundException, DependencyException
|
||||||
from core_lib.game.library import parse_maven_coordinate, convert_maven_version, generate_repo_url, \
|
from core_lib.game.library import parse_maven_coordinate, generate_repo_url, \
|
||||||
is_complete_maven_url
|
is_complete_maven_url
|
||||||
|
from core_lib.game.libraryx import convert_maven_version_with_extra, generate_unique_library_key, \
|
||||||
|
normalize_library_to_common_struct
|
||||||
from core_lib.game.version_info import MOJANG_LIBRARIES_ENDPOINT
|
from core_lib.game.version_info import MOJANG_LIBRARIES_ENDPOINT
|
||||||
|
|
||||||
logger = logging.getLogger("Launcher.CoreLib")
|
logger = logging.getLogger("Launcher.CoreLib")
|
||||||
@@ -58,10 +60,12 @@ def find_useful_part_in_mod_version_manifest(mod_version_manifest: dict):
|
|||||||
"mainClass": main_class,
|
"mainClass": main_class,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def merge_necessary_dependencies(inherits_libraries: list, mod_libraries: list, mod_maven_url=None,
|
def merge_necessary_dependencies(inherits_libraries: list, mod_libraries: list, mod_maven_url=None,
|
||||||
inherits_maven_url=MOJANG_LIBRARIES_ENDPOINT, default_extension="jar") -> list[dict]:
|
inherits_maven_url=MOJANG_LIBRARIES_ENDPOINT, default_extension="jar") -> list[dict]:
|
||||||
|
|
||||||
inherits = {}
|
inherits = {}
|
||||||
|
result_pre = []
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
# ensure below step won't modify original data
|
# ensure below step won't modify original data
|
||||||
@@ -73,7 +77,7 @@ def merge_necessary_dependencies(inherits_libraries: list, mod_libraries: list,
|
|||||||
source_from = lib["sourceFrom"]
|
source_from = lib["sourceFrom"]
|
||||||
lib_group=lib["parsed"]["group"]
|
lib_group=lib["parsed"]["group"]
|
||||||
lib_artifact=lib["parsed"]["artifact"]
|
lib_artifact=lib["parsed"]["artifact"]
|
||||||
lib_version=lib["parsed"]["version"]
|
lib_version=lib["parsed"]["versionRaw"]
|
||||||
lib_classifier=lib["parsed"]["classifier"]
|
lib_classifier=lib["parsed"]["classifier"]
|
||||||
lib_extension=lib["parsed"]["extension"]
|
lib_extension=lib["parsed"]["extension"]
|
||||||
|
|
||||||
@@ -111,73 +115,119 @@ def merge_necessary_dependencies(inherits_libraries: list, mod_libraries: list,
|
|||||||
"Provided inherited library does not contain key \"name\"."
|
"Provided inherited library does not contain key \"name\"."
|
||||||
)
|
)
|
||||||
|
|
||||||
group, artifact, version, classifier, extension = parse_maven_coordinate(name,
|
group, artifact, version_raw, classifier, extension = parse_maven_coordinate(name, default_extension=default_extension)
|
||||||
default_extension=default_extension)
|
ver_converted, extra = convert_maven_version_with_extra(version_raw)
|
||||||
|
|
||||||
|
if extra:
|
||||||
|
logger.debug("Found extra version \"{}\" for library {}".format(extra, name))
|
||||||
|
|
||||||
|
if not ver_converted:
|
||||||
|
raise DependencyException(
|
||||||
|
f"Unable to convert provided inherited library name {name}'s version {version_raw} to tuple."
|
||||||
|
)
|
||||||
|
|
||||||
|
lib_key = generate_unique_library_key(
|
||||||
|
group=group,
|
||||||
|
artifact=artifact,
|
||||||
|
classifier=classifier,
|
||||||
|
extension=extension,
|
||||||
|
)
|
||||||
|
|
||||||
# Save into dict for next step to check if library is existing
|
# Save into dict for next step to check if library is existing
|
||||||
if not f"{group}:{artifact}" in inherits:
|
if not lib_key in inherits:
|
||||||
inherits[f"{group}:{artifact}"] = library
|
inherits[lib_key] = library
|
||||||
inherits[f"{group}:{artifact}"]["parsed"] = {
|
inherits[lib_key]["parsed"] = {
|
||||||
"group": group,
|
"group": group,
|
||||||
"artifact": artifact,
|
"artifact": artifact,
|
||||||
"version": version,
|
"version": ver_converted,
|
||||||
|
"versionRaw": version_raw,
|
||||||
|
"versionExtra": extra,
|
||||||
"classifier": classifier,
|
"classifier": classifier,
|
||||||
"extension": extension,
|
"extension": extension,
|
||||||
|
"endpoint": inherits_maven_url
|
||||||
}
|
}
|
||||||
inherits[f"{group}:{artifact}"]["sourceFrom"] = "official"
|
inherits[lib_key]["sourceFrom"] = "official"
|
||||||
else:
|
else:
|
||||||
logger.warning("Duplicate library: {}".format(name))
|
logger.warning("Duplicate library: {}".format(name))
|
||||||
|
|
||||||
for mod_library in mod_libraries:
|
for mod_library in mod_libraries:
|
||||||
name = mod_library.get("name", None)
|
name = mod_library.get("name", None)
|
||||||
group, artifact, version, classifier, extension = parse_maven_coordinate(name, default_extension=default_extension)
|
|
||||||
|
|
||||||
ver_converted = convert_maven_version(version)
|
|
||||||
|
|
||||||
if not name:
|
if not name:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
"Provided mod library does not contain key \"name\"."
|
"Provided mod library does not contain key \"name\"."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
group, artifact, version_raw, classifier, extension = parse_maven_coordinate(name, default_extension=default_extension)
|
||||||
|
|
||||||
|
ver_converted, extra = convert_maven_version_with_extra(version_raw)
|
||||||
|
|
||||||
|
if extra:
|
||||||
|
logger.debug("Found extra version \"{}\" for library {}".format(extra, name))
|
||||||
|
|
||||||
if ver_converted is None:
|
if ver_converted is None:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
"Unable to convert inherits library version {} to tuple".format(version),
|
"Unable to convert mod library version {} to tuple".format(version_raw),
|
||||||
)
|
)
|
||||||
|
|
||||||
group, artifact, version, classifier, extension = parse_maven_coordinate(name, default_extension=default_extension)
|
group, artifact, version_raw, classifier, extension = parse_maven_coordinate(name, default_extension=default_extension)
|
||||||
|
lib_key = generate_unique_library_key(
|
||||||
|
group=group,
|
||||||
|
artifact=artifact,
|
||||||
|
classifier=classifier,
|
||||||
|
extension=extension,
|
||||||
|
)
|
||||||
|
|
||||||
mod_library["sourceFrom"] = "mod"
|
mod_library["sourceFrom"] = "mod"
|
||||||
mod_library["parsed"] = {
|
mod_library["parsed"] = {
|
||||||
"group": group,
|
"group": group,
|
||||||
"artifact": artifact,
|
"artifact": artifact,
|
||||||
"version": version,
|
# "version": {"raw": version, "converted": ver_converted, "extra": extra},
|
||||||
|
"version": ver_converted,
|
||||||
|
"versionRaw": version_raw,
|
||||||
|
"versionExtra": extra,
|
||||||
"classifier": classifier,
|
"classifier": classifier,
|
||||||
"extension": extension,
|
"extension": extension,
|
||||||
|
"endpoint": mod_maven_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
if f"{group}:{artifact}" in inherits:
|
if lib_key in inherits:
|
||||||
found_lib = inherits.pop(f"{group}:{artifact}")
|
found_lib = inherits.pop(lib_key)
|
||||||
found_lib_ver = convert_maven_version(found_lib["parsed"]["version"], None)
|
found_lib_ver = found_lib["parsed"]["version"]
|
||||||
|
found_lib_ver_extra = found_lib["parsed"]["versionExtra"]
|
||||||
|
|
||||||
if found_lib_ver is None:
|
if found_lib_ver is None:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
"Unable to convert mod library version {} to tuple".format(found_lib["version"])
|
"Unable to convert mod library version {} to tuple".format(found_lib["versionRaw"])
|
||||||
)
|
)
|
||||||
|
|
||||||
if found_lib_ver > ver_converted or found_lib_ver == ver_converted:
|
if found_lib_ver > ver_converted:
|
||||||
result.append(found_lib)
|
result_pre.append(found_lib)
|
||||||
elif found_lib_ver < ver_converted:
|
elif found_lib_ver < ver_converted:
|
||||||
result.append(mod_library)
|
result_pre.append(mod_library)
|
||||||
else:
|
else:
|
||||||
result.append(mod_library)
|
# Use mod loader library if extra version is existing
|
||||||
|
if found_lib_ver_extra == mod_library["parsed"]["versionExtra"]:
|
||||||
|
result_pre.append(found_lib)
|
||||||
|
else:
|
||||||
|
result_pre.append(mod_library)
|
||||||
|
else:
|
||||||
|
result_pre.append(mod_library)
|
||||||
|
|
||||||
# Add the remaining dependencies back to result
|
# Add the remaining dependencies back to result
|
||||||
for key in inherits:
|
for key in inherits:
|
||||||
value = inherits[key]
|
value = inherits[key]
|
||||||
result.append(value)
|
result_pre.append(value)
|
||||||
|
|
||||||
for library in result:
|
for library in result_pre:
|
||||||
|
# Cleanup
|
||||||
check_lib(library)
|
check_lib(library)
|
||||||
|
library_normalized = normalize_library_to_common_struct(library, default_maven_url=library["parsed"]["endpoint"])
|
||||||
|
|
||||||
|
library_normalized.pop("parsed", None)
|
||||||
|
library_normalized.pop("sourceFrom", None)
|
||||||
|
library_normalized.pop("endpoint", None)
|
||||||
|
result.append(library_normalized)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -240,4 +290,14 @@ def merge_manifest(inherits_version_manifest: dict, mod_version_manifest: dict,
|
|||||||
|
|
||||||
merged_manifest["arguments"] = merge_game_and_jvm_args(inherits_arguments, mod_arguments)
|
merged_manifest["arguments"] = merge_game_and_jvm_args(inherits_arguments, mod_arguments)
|
||||||
|
|
||||||
|
# Update version number
|
||||||
|
mod_id = mod_version_manifest.get("id")
|
||||||
|
inherits_id = inherits_version_manifest.get("id")
|
||||||
|
|
||||||
|
if mod_id:
|
||||||
|
merged_manifest["id"] = mod_id
|
||||||
|
|
||||||
|
if inherits_id:
|
||||||
|
merged_manifest["inheritsFrom"] = inherits_id
|
||||||
|
|
||||||
return merged_manifest
|
return merged_manifest
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
from PySide6.QtCore import Qt, Slot, QTimer
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QDialog,
|
||||||
|
QLabel,
|
||||||
|
QProgressBar,
|
||||||
|
QPushButton,
|
||||||
|
QScrollArea,
|
||||||
|
QSizePolicy,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressRow(QWidget):
|
||||||
|
def __init__(self, name: str, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.setSizePolicy(
|
||||||
|
QSizePolicy.Policy.Expanding,
|
||||||
|
QSizePolicy.Policy.Fixed,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.name_label = QLabel(name, self)
|
||||||
|
self.name_label.setWordWrap(True)
|
||||||
|
self.name_label.setTextInteractionFlags(
|
||||||
|
Qt.TextInteractionFlag.TextSelectableByMouse
|
||||||
|
)
|
||||||
|
|
||||||
|
self.status_label = QLabel("Preparing...", self)
|
||||||
|
self.status_label.setWordWrap(True)
|
||||||
|
self.status_label.setTextInteractionFlags(
|
||||||
|
Qt.TextInteractionFlag.TextSelectableByMouse
|
||||||
|
)
|
||||||
|
|
||||||
|
self.progress_bar = QProgressBar(self)
|
||||||
|
self.progress_bar.setRange(0, 100)
|
||||||
|
self.progress_bar.setValue(0)
|
||||||
|
|
||||||
|
# Flags
|
||||||
|
self.finished = False
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(8, 8, 8, 8)
|
||||||
|
layout.setSpacing(4)
|
||||||
|
layout.addWidget(self.name_label)
|
||||||
|
layout.addWidget(self.status_label)
|
||||||
|
layout.addWidget(self.progress_bar)
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressDialog(QDialog):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.setWindowTitle("Downloads")
|
||||||
|
self.resize(500, 300)
|
||||||
|
self.setMinimumSize(360, 220)
|
||||||
|
|
||||||
|
self.rows: dict[str, ProgressRow] = {}
|
||||||
|
|
||||||
|
self.scroll_area = QScrollArea(self)
|
||||||
|
self.download_widget = QWidget(self.scroll_area)
|
||||||
|
self.download_layout = QVBoxLayout(self.download_widget)
|
||||||
|
self.download_layout.setContentsMargins(4, 4, 4, 4)
|
||||||
|
self.download_layout.setSpacing(8)
|
||||||
|
self.download_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||||
|
|
||||||
|
self.scroll_area.setWidget(self.download_widget)
|
||||||
|
self.scroll_area.setWidgetResizable(True)
|
||||||
|
self.scroll_area.setHorizontalScrollBarPolicy(
|
||||||
|
Qt.ScrollBarPolicy.ScrollBarAlwaysOff
|
||||||
|
)
|
||||||
|
|
||||||
|
self.close_button = QPushButton("Close", self)
|
||||||
|
self.close_button.clicked.connect(self.hide)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(8, 8, 8, 8)
|
||||||
|
layout.setSpacing(8)
|
||||||
|
layout.addWidget(self.scroll_area, 1)
|
||||||
|
layout.addWidget(
|
||||||
|
self.close_button,
|
||||||
|
0,
|
||||||
|
Qt.AlignmentFlag.AlignRight,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.close_timer = QTimer(self)
|
||||||
|
self.close_timer.setSingleShot(True)
|
||||||
|
self.close_timer.timeout.connect(self.cleanup)
|
||||||
|
|
||||||
|
@Slot(str, str)
|
||||||
|
def add_task(self, task_id: str, name: str):
|
||||||
|
if task_id in self.rows:
|
||||||
|
return
|
||||||
|
|
||||||
|
row = ProgressRow(name, self.download_widget)
|
||||||
|
self.rows[task_id] = row
|
||||||
|
|
||||||
|
self.download_layout.addWidget(row)
|
||||||
|
|
||||||
|
if not self.isVisible():
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
@Slot(str, str, float)
|
||||||
|
def update_task(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
status: str,
|
||||||
|
percent: float,
|
||||||
|
):
|
||||||
|
row = self.rows.get(task_id)
|
||||||
|
if row is None:
|
||||||
|
# Prevent started、progress missing due to timing issues
|
||||||
|
self.add_task(task_id, task_id)
|
||||||
|
row = self.rows[task_id]
|
||||||
|
|
||||||
|
value = max(0, min(100, round(percent)))
|
||||||
|
row.status_label.setText(status)
|
||||||
|
row.progress_bar.setValue(value)
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
|
def finish_task(self, task_id: str):
|
||||||
|
row = self.rows.get(task_id)
|
||||||
|
if row is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
row.status_label.setText("Completed")
|
||||||
|
row.progress_bar.setValue(100)
|
||||||
|
row.finished = True
|
||||||
|
|
||||||
|
if self.all_tasks_finished():
|
||||||
|
self.close_timer.start(2000)
|
||||||
|
|
||||||
|
def all_tasks_finished(self):
|
||||||
|
return all(row.finished == True for task_id, row in self.rows.items())
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""
|
||||||
|
If you want to use this function with a timer. use self.close_timer.start(TIMEOUT)
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
QTimer.singleShot(2000, self.cleanup)
|
||||||
|
for task_id in list(self.rows):
|
||||||
|
self.remove_task(task_id)
|
||||||
|
|
||||||
|
self.hide()
|
||||||
|
|
||||||
|
@Slot(str, str)
|
||||||
|
def fail_task(self, task_id: str, error: str):
|
||||||
|
row = self.rows.get(task_id)
|
||||||
|
if row is None:
|
||||||
|
self.add_task(task_id, task_id)
|
||||||
|
row = self.rows[task_id]
|
||||||
|
|
||||||
|
row.status_label.setText(f"Failed: {error}")
|
||||||
|
row.progress_bar.setStyleSheet(
|
||||||
|
"QProgressBar::chunk { background-color: #c62828; }"
|
||||||
|
)
|
||||||
|
# Move to top
|
||||||
|
self.download_layout.removeWidget(row)
|
||||||
|
self.download_layout.insertWidget(0, row)
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
|
def remove_task(self, task_id: str):
|
||||||
|
row = self.rows.pop(task_id, None)
|
||||||
|
if row is not None:
|
||||||
|
self.download_layout.removeWidget(row)
|
||||||
|
row.setParent(None)
|
||||||
|
row.deleteLater()
|
||||||
@@ -50,7 +50,7 @@ def main():
|
|||||||
logging.basicConfig(format=DEFAULT_FORMAT, datefmt=DEFAULT_DATE_FORMAT, level=level)
|
logging.basicConfig(format=DEFAULT_FORMAT, datefmt=DEFAULT_DATE_FORMAT, level=level)
|
||||||
|
|
||||||
# Start (the real) launcher
|
# Start (the real) launcher
|
||||||
app = Launcher()
|
app = Launcher(logger)
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from PySide6.QtCore import QObject
|
||||||
|
|
||||||
|
|
||||||
|
class RocketLauncher(QObject):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
"""
|
||||||
|
IMPORTANT: Still under construction!
|
||||||
|
"""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PySide6.QtCore import QObject, Signal
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
|
from core_lib.common.exception import ProfileException
|
||||||
|
from core_lib.game.profile import read_profile_file
|
||||||
|
|
||||||
|
DEFAuLT_PROFILE_RELATIVE_PATH = Path("launcher_profiles.json")
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ProfileSummary:
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
version_id: str
|
||||||
|
version_type: str
|
||||||
|
icon: str
|
||||||
|
last_used: datetime | None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ProfileDetail(ProfileSummary):
|
||||||
|
game_dir: Path | None
|
||||||
|
java_args: str
|
||||||
|
resolution: tuple[int, int] | None
|
||||||
|
created: datetime
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CreateProfileRequest:
|
||||||
|
name: str
|
||||||
|
version_id: str
|
||||||
|
version_type: str = "custom"
|
||||||
|
game_dir: Path | None = None
|
||||||
|
java_args: str = ""
|
||||||
|
icon: str = ""
|
||||||
|
resolution: tuple[int, int] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class UpdateProfileRequest:
|
||||||
|
name: str | None = None
|
||||||
|
version_id: str | None = None
|
||||||
|
version_type: str | None = None
|
||||||
|
game_dir: Path | None = None
|
||||||
|
java_args: str | None = None
|
||||||
|
icon: str | None = None
|
||||||
|
resolution: tuple[int, int] | None = None
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class LaunchProfile:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ValidationResult:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ProfileManager(QObject):
|
||||||
|
profiles_changed = Signal()
|
||||||
|
profile_created = Signal(str)
|
||||||
|
profile_updated = Signal(str)
|
||||||
|
profile_removed = Signal(str)
|
||||||
|
current_profile_changed = Signal(object) # str | None
|
||||||
|
error_occurred = Signal(str, str) # error_code, message
|
||||||
|
replace_relative_path = Signal(str)
|
||||||
|
|
||||||
|
def __init__(self, app):
|
||||||
|
QObject.__init__(self, app)
|
||||||
|
self.app = app
|
||||||
|
self.profiles = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def profile_path(self):
|
||||||
|
return Path(self.app.work_dir, DEFAuLT_PROFILE_RELATIVE_PATH)
|
||||||
|
|
||||||
|
# File lifetime
|
||||||
|
def load(self, path: Path=None, return_value=False) -> None | dict:
|
||||||
|
if path is None:
|
||||||
|
path = self.profile_path
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = read_profile_file(path)
|
||||||
|
except ProfileException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# def load(self, path: str | Path) -> None: ...
|
||||||
|
def save(self) -> None: ...
|
||||||
|
def save_as(self, path: str | Path) -> None: ...
|
||||||
|
def reload(self) -> None: ...
|
||||||
|
|
||||||
|
# Profile search
|
||||||
|
def list_profiles(self) -> list[ProfileSummary]: ...
|
||||||
|
def get_profile(self, profile_id: str) -> ProfileDetail: ...
|
||||||
|
def contains(self, profile_id: str) -> bool: ...
|
||||||
|
|
||||||
|
# CRUD
|
||||||
|
def create_profile(self, request: CreateProfileRequest) -> str: ...
|
||||||
|
def update_profile(
|
||||||
|
self, profile_id: str, changes: UpdateProfileRequest
|
||||||
|
) -> None: ...
|
||||||
|
def remove_profile(self, profile_id: str) -> None: ...
|
||||||
|
def duplicate_profile(
|
||||||
|
self, profile_id: str, new_name: str | None = None
|
||||||
|
) -> str: ...
|
||||||
|
|
||||||
|
# Profile switch
|
||||||
|
def set_current_profile(self, profile_id: str | None) -> None: ...
|
||||||
|
def get_current_profile_id(self) -> str | None: ...
|
||||||
|
def get_launch_profile(self, profile_id: str) -> LaunchProfile: ...
|
||||||
|
|
||||||
|
# Validation
|
||||||
|
def validate(self, request: CreateProfileRequest) -> ValidationResult: ...
|
||||||
|
|
||||||
|
# Property
|
||||||
|
@property
|
||||||
|
def path(self) -> Path | None: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_loaded(self) -> bool: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_dirty(self) -> bool: ...
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import queue
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from PySide6.QtCore import QObject, Signal, Slot
|
||||||
|
from PySide6.QtWidgets import QProgressDialog
|
||||||
|
|
||||||
|
from core_lib.qt import messagebox
|
||||||
|
|
||||||
|
|
||||||
|
class AskForRequest:
|
||||||
|
def __init__(self, title, message):
|
||||||
|
self.title = title
|
||||||
|
self.message = message
|
||||||
|
self.result_queue = queue.Queue(maxsize=1)
|
||||||
|
self.wait_event = threading.Event()
|
||||||
|
|
||||||
|
self.details = None # Set this if flag use_plain_text is enabled
|
||||||
|
|
||||||
|
# flags
|
||||||
|
self.use_plain_text = True
|
||||||
|
|
||||||
|
def wait(self, timeout: int | None = 10):
|
||||||
|
return self.wait_event.wait(timeout)
|
||||||
|
|
||||||
|
def result(self):
|
||||||
|
try:
|
||||||
|
return self.result_queue.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
return None
|
||||||
|
|
||||||
|
class SelectPathRequest(AskForRequest):
|
||||||
|
def __init__(self, title, message, filter_raw=None):
|
||||||
|
super().__init__(title, message)
|
||||||
|
self.filter = filter_raw
|
||||||
|
|
||||||
|
|
||||||
|
class WidgetManager(QObject):
|
||||||
|
def __init__(self):
|
||||||
|
super(WidgetManager, self).__init__()
|
||||||
|
|
||||||
|
self.signal = self.WidgetSignal(self)
|
||||||
|
self.download_signal = self.DownloadSignal(self)
|
||||||
|
|
||||||
|
class DownloadSignal(QObject):
|
||||||
|
started = Signal(str, str) # task_id, display_name
|
||||||
|
progress = Signal(str, str, float) # task_id, status, percent
|
||||||
|
finished = Signal(str) # task_id
|
||||||
|
failed = Signal(str, str) # task_id, error
|
||||||
|
removed = Signal(str) # task_id
|
||||||
|
log = Signal(str)
|
||||||
|
|
||||||
|
class WidgetSignal(QObject):
|
||||||
|
show_error_message = Signal(str)
|
||||||
|
show_and_print_error_message = Signal(dict)
|
||||||
|
show_warning_message = Signal(str)
|
||||||
|
show_info_message = Signal(str)
|
||||||
|
enable_widget = Signal(object)
|
||||||
|
disable_widget = Signal(object)
|
||||||
|
askyesno = Signal(object)
|
||||||
|
ask_for_path = Signal(object)
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6 import QtGui
|
|
||||||
from PySide6.QtCore import QSize
|
from PySide6.QtCore import QSize
|
||||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QComboBox, QListWidget, QPushButton, QLabel, QHBoxLayout
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QComboBox, QListWidget, QPushButton, QLabel, QHBoxLayout
|
||||||
|
|
||||||
from core_lib.launcher.object import Launcher as LauncherObject
|
from core_lib.launcher.object import Launcher as LauncherObject
|
||||||
|
|
||||||
class CreateProfile(QWidget):
|
class CreateProfile(QWidget):
|
||||||
def __init__(self, launcher: "LauncherObject",parent=None):
|
def __init__(self, app, parent=None):
|
||||||
super(CreateProfile, self).__init__(parent)
|
super(CreateProfile, self).__init__(parent)
|
||||||
self.app = launcher
|
self.app = app
|
||||||
|
|
||||||
self.layout = QHBoxLayout()
|
self.layout = QHBoxLayout()
|
||||||
|
|
||||||
|
|||||||
+212
-269
@@ -1,15 +1,15 @@
|
|||||||
import json
|
import json
|
||||||
import queue
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import textwrap
|
import textwrap
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, QObject, Signal, Slot
|
from PySide6.QtCore import Qt, QObject, Signal, Slot
|
||||||
from PySide6.QtWidgets import QPushButton, QVBoxLayout, QPlainTextEdit, \
|
from PySide6.QtWidgets import QPushButton, QVBoxLayout, QPlainTextEdit, \
|
||||||
QHBoxLayout, QComboBox, QMessageBox, QFileDialog, QWidget
|
QHBoxLayout, QComboBox, QWidget
|
||||||
|
|
||||||
from core_lib.common.common import FileObject, download_multiple_files
|
from core_lib.common.common import FileObject, download_multiple_files
|
||||||
from core_lib.common.exception import VersionNotSelectedException, UnsupportedPlatformException, \
|
from core_lib.common.exception import VersionNotSelectedException, UnsupportedPlatformException, \
|
||||||
@@ -19,52 +19,28 @@ from core_lib.common.exception import VersionNotSelectedException, UnsupportedPl
|
|||||||
from core_lib.game.argument import ArgumentMappings, parse_and_generate_arguments, find_game_and_jvm_args, \
|
from core_lib.game.argument import ArgumentMappings, parse_and_generate_arguments, find_game_and_jvm_args, \
|
||||||
generate_launch_command
|
generate_launch_command
|
||||||
from core_lib.game.asset import download_assets, check_assets_exist, find_correct_assets_dir
|
from core_lib.game.asset import download_assets, check_assets_exist, find_correct_assets_dir
|
||||||
from core_lib.game.library import generate_classpath, download_full_libraries, check_full_libraries_exists
|
from core_lib.game.library_core import generate_classpath, download_full_libraries, check_full_libraries_exists
|
||||||
|
|
||||||
from core_lib.game.version_info import (find_useful_part_in_specific_version_manifest, \
|
from core_lib.game.version_info import (find_useful_part_in_specific_version_manifest, \
|
||||||
get_all_versions, get_available_version_types, \
|
get_all_versions, get_available_version_types, \
|
||||||
fetch_and_save_version_manifest, get_specific_version_manifest_url_and_hash,
|
fetch_and_save_version_manifest, get_specific_version_manifest_url_and_hash,
|
||||||
download_core_file)
|
download_core_file)
|
||||||
from core_lib.qt import messagebox
|
|
||||||
from core_lib.qt.hack import is_main_thread
|
from core_lib.qt.hack import is_main_thread
|
||||||
from core_lib.qt.messagebox import ask_yes_no_with_plain_text
|
from core_lib.qt.progress_dialog import ProgressDialog
|
||||||
from core_lib.runtime.java import find_java_installations
|
from core_lib.runtime.java import find_java_installations
|
||||||
|
from manager.widgets import SelectPathRequest, AskForRequest
|
||||||
|
|
||||||
MAX_DOWNLOAD_ATTEMPTS = 3
|
MAX_DOWNLOAD_ATTEMPTS = 3
|
||||||
|
|
||||||
|
|
||||||
class AskForRequest:
|
|
||||||
def __init__(self, title, message):
|
|
||||||
self.title = title
|
|
||||||
self.message = message
|
|
||||||
self.result_queue = queue.Queue(maxsize=1)
|
|
||||||
self.wait_event = threading.Event()
|
|
||||||
|
|
||||||
self.details = None # Set this if flag use_plain_text is enabled
|
|
||||||
|
|
||||||
# flags
|
|
||||||
self.use_plain_text = True
|
|
||||||
|
|
||||||
def wait(self, timeout: int | None = 10):
|
|
||||||
return self.wait_event.wait(timeout)
|
|
||||||
|
|
||||||
def result(self):
|
|
||||||
try:
|
|
||||||
return self.result_queue.get_nowait()
|
|
||||||
except queue.Empty:
|
|
||||||
return None
|
|
||||||
|
|
||||||
class SelectPathRequest(AskForRequest):
|
|
||||||
def __init__(self, title, message, filter_raw=None):
|
|
||||||
super().__init__(title, message)
|
|
||||||
self.filter = filter_raw
|
|
||||||
|
|
||||||
|
|
||||||
class TestPage(QWidget):
|
class TestPage(QWidget):
|
||||||
def __init__(self, app, parent):
|
def __init__(self, /, app, parent, widget_manager):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.app = app
|
self.app = app
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
|
self.widget_mgr = widget_manager
|
||||||
|
|
||||||
self.resize(800, 600)
|
self.resize(800, 600)
|
||||||
self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowMaximizeButtonHint)
|
self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowMaximizeButtonHint)
|
||||||
|
|
||||||
@@ -138,188 +114,64 @@ class TestPage(QWidget):
|
|||||||
}
|
}
|
||||||
"""))
|
"""))
|
||||||
|
|
||||||
# Bind signal functions
|
self.signals = self.PageSignals(self)
|
||||||
self.download_signal = self.DownloadSignal(self)
|
|
||||||
self.widget_signal = self.WidgetSignal(self)
|
|
||||||
|
|
||||||
self.download_signal.log.connect(
|
# Bind signal functions
|
||||||
|
self.widget_mgr.download_signal.log.connect(
|
||||||
self.output.appendPlainText
|
self.output.appendPlainText
|
||||||
)
|
)
|
||||||
|
|
||||||
self.download_signal.finished.connect(
|
# self.widget_mgr.download_signal.finished.connect(
|
||||||
lambda count: self.output.appendPlainText(
|
# lambda count: self.output.appendPlainText(
|
||||||
f"Download finished, failed: {count}"
|
# f"Download finished, failed: {count}"
|
||||||
)
|
# )
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
|
# self.widget_mgr.download_signal.progress.connect(
|
||||||
|
# lambda name, progress: self.output.setPlainText(
|
||||||
|
# f"Downloading {name}: {self.get_progress(progress)}"
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
|
||||||
self.download_signal.progress.connect(
|
self.signals.clean_ver_types_dropdown.connect(
|
||||||
lambda name, progress: self.output.setPlainText(
|
|
||||||
f"Downloading {name}: {self.get_progress(progress)}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.widget_signal.clean_ver_types_dropdown.connect(
|
|
||||||
self.ver_type_dropdown.clear
|
self.ver_type_dropdown.clear
|
||||||
)
|
)
|
||||||
|
|
||||||
self.widget_signal.update_types_dropdown.connect(
|
self.signals.update_types_dropdown.connect(
|
||||||
lambda types: self.update_type_dropdown(types)
|
lambda types: self.update_type_dropdown(types)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.widget_signal.clean_vers_dropdown.connect(
|
self.signals.clean_vers_dropdown.connect(
|
||||||
self.ver_dropdown.clear
|
self.ver_dropdown.clear
|
||||||
)
|
)
|
||||||
|
|
||||||
self.widget_signal.update_vers_dropdown.connect(
|
self.signals.update_vers_dropdown.connect(
|
||||||
lambda vers: self.update_version_dropdown(vers)
|
lambda vers: self.update_version_dropdown(vers)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.widget_signal.show_error_message.connect(
|
self.signals.get_selected_ver.connect(
|
||||||
lambda msg: messagebox.error(
|
|
||||||
self.parent,
|
|
||||||
"Error",
|
|
||||||
message=msg
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.widget_signal.show_and_print_error_message.connect(
|
|
||||||
self.show_and_print_error
|
|
||||||
)
|
|
||||||
|
|
||||||
self.widget_signal.show_warning_message.connect(
|
|
||||||
lambda msg: messagebox.warning(
|
|
||||||
self.parent,
|
|
||||||
"Warning",
|
|
||||||
message=msg
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.widget_signal.show_info_message.connect(
|
|
||||||
lambda msg: messagebox.info(
|
|
||||||
self.parent,
|
|
||||||
"Info",
|
|
||||||
message=msg
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.widget_signal.enable_widget.connect(
|
|
||||||
lambda widget: widget.setEnabled(True)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.widget_signal.disable_widget.connect(
|
|
||||||
lambda widget: widget.setDisabled(True)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.widget_signal.askyesno.connect(
|
|
||||||
self.ask_request
|
|
||||||
)
|
|
||||||
|
|
||||||
self.widget_signal.ask_for_path.connect(
|
|
||||||
self.ask_for_path
|
|
||||||
)
|
|
||||||
|
|
||||||
self.widget_signal.get_selected_ver.connect(
|
|
||||||
self.get_selected_ver
|
self.get_selected_ver
|
||||||
)
|
)
|
||||||
|
|
||||||
self.widget_signal.get_selected_type.connect(
|
self.signals.get_selected_type.connect(
|
||||||
self.get_selected_type
|
self.get_selected_type
|
||||||
)
|
)
|
||||||
|
|
||||||
self.widget_signal.clean_output.connect(
|
self.signals.clean_output.connect(
|
||||||
lambda : self.output.clear()
|
lambda : self.output.clear()
|
||||||
)
|
)
|
||||||
|
|
||||||
self.load_all_types()
|
self.load_all_types()
|
||||||
|
|
||||||
class DownloadSignal(QObject):
|
class PageSignals(QObject):
|
||||||
log = Signal(str)
|
|
||||||
finished = Signal(int)
|
|
||||||
failed = Signal(str)
|
|
||||||
progress = Signal(str, float)
|
|
||||||
|
|
||||||
class WidgetSignal(QObject):
|
|
||||||
clean_ver_types_dropdown = Signal()
|
clean_ver_types_dropdown = Signal()
|
||||||
clean_vers_dropdown = Signal()
|
clean_vers_dropdown = Signal()
|
||||||
update_vers_dropdown = Signal(list)
|
update_vers_dropdown = Signal(list)
|
||||||
update_types_dropdown = Signal(list)
|
update_types_dropdown = Signal(list)
|
||||||
show_error_message = Signal(str)
|
|
||||||
show_and_print_error_message = Signal(dict)
|
|
||||||
show_warning_message = Signal(str)
|
|
||||||
show_info_message = Signal(str)
|
|
||||||
enable_widget = Signal(object)
|
|
||||||
disable_widget = Signal(object)
|
|
||||||
askyesno = Signal(object)
|
|
||||||
ask_for_path = Signal(object)
|
|
||||||
get_selected_ver = Signal(dict)
|
get_selected_ver = Signal(dict)
|
||||||
get_selected_type = Signal(dict)
|
get_selected_type = Signal(dict)
|
||||||
clean_output = Signal()
|
clean_output = Signal()
|
||||||
|
|
||||||
@Slot(object)
|
|
||||||
def ask_request(self, context: AskForRequest):
|
|
||||||
if not context.use_plain_text:
|
|
||||||
msg = QMessageBox.question(
|
|
||||||
self,
|
|
||||||
context.title,
|
|
||||||
context.message,
|
|
||||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
||||||
QMessageBox.StandardButton.Yes,
|
|
||||||
)
|
|
||||||
result = msg == QMessageBox.StandardButton.Yes
|
|
||||||
else:
|
|
||||||
result = ask_yes_no_with_plain_text(
|
|
||||||
self,
|
|
||||||
context.title,
|
|
||||||
context.message,
|
|
||||||
context.details,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result:
|
|
||||||
context.result_queue.put(True)
|
|
||||||
else:
|
|
||||||
context.result_queue.put(False)
|
|
||||||
|
|
||||||
context.wait_event.set()
|
|
||||||
|
|
||||||
@Slot(object)
|
|
||||||
def ask_request(self, context: AskForRequest):
|
|
||||||
if not context.use_plain_text:
|
|
||||||
msg = QMessageBox.question(
|
|
||||||
self,
|
|
||||||
context.title,
|
|
||||||
context.message,
|
|
||||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
||||||
QMessageBox.StandardButton.Yes,
|
|
||||||
)
|
|
||||||
result = msg == QMessageBox.StandardButton.Yes
|
|
||||||
else:
|
|
||||||
result = ask_yes_no_with_plain_text(
|
|
||||||
self,
|
|
||||||
context.title,
|
|
||||||
context.message,
|
|
||||||
context.details,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result:
|
|
||||||
context.result_queue.put(True)
|
|
||||||
else:
|
|
||||||
context.result_queue.put(False)
|
|
||||||
|
|
||||||
context.wait_event.set()
|
|
||||||
|
|
||||||
@Slot(object)
|
|
||||||
def ask_for_path(self, context: SelectPathRequest):
|
|
||||||
path, _ = QFileDialog.getOpenFileName(
|
|
||||||
self,
|
|
||||||
context.title,
|
|
||||||
"",
|
|
||||||
context.filter,
|
|
||||||
)
|
|
||||||
|
|
||||||
context.result_queue.put(path)
|
|
||||||
|
|
||||||
context.wait_event.set()
|
|
||||||
|
|
||||||
@Slot(dict)
|
@Slot(dict)
|
||||||
def get_selected_ver(self, context: dict):
|
def get_selected_ver(self, context: dict):
|
||||||
context["version"] = self.ver_dropdown.currentText()
|
context["version"] = self.ver_dropdown.currentText()
|
||||||
@@ -334,25 +186,6 @@ class TestPage(QWidget):
|
|||||||
if isinstance(event, threading.Event):
|
if isinstance(event, threading.Event):
|
||||||
event.set()
|
event.set()
|
||||||
|
|
||||||
@Slot(dict)
|
|
||||||
def show_and_print_error(self, context: dict):
|
|
||||||
error = context.get("error", None)
|
|
||||||
title = context.get("title", "Error")
|
|
||||||
message = context.get("message", "No content")
|
|
||||||
|
|
||||||
if isinstance(error, list):
|
|
||||||
self.app.logger.error("".join(error))
|
|
||||||
elif isinstance(error, str):
|
|
||||||
self.app.logger.error(error)
|
|
||||||
elif isinstance(error, Exception):
|
|
||||||
self.app.logger.exception(error)
|
|
||||||
|
|
||||||
messagebox.error(
|
|
||||||
self.parent,
|
|
||||||
title,
|
|
||||||
message=message
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_progress(value):
|
def get_progress(value):
|
||||||
width = 30
|
width = 30
|
||||||
@@ -393,7 +226,7 @@ class TestPage(QWidget):
|
|||||||
context = {
|
context = {
|
||||||
"event": event,
|
"event": event,
|
||||||
}
|
}
|
||||||
self.widget_signal.get_selected_ver.emit(context)
|
self.signals.get_selected_ver.emit(context)
|
||||||
|
|
||||||
if not event.wait(10):
|
if not event.wait(10):
|
||||||
raise VersionNotSelectedException(
|
raise VersionNotSelectedException(
|
||||||
@@ -413,7 +246,7 @@ class TestPage(QWidget):
|
|||||||
ver = self._get_selected_version_unsafe()
|
ver = self._get_selected_version_unsafe()
|
||||||
return ver
|
return ver
|
||||||
except VersionNotSelectedException as e:
|
except VersionNotSelectedException as e:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
"Select a version before continuing.\n"
|
"Select a version before continuing.\n"
|
||||||
"Details: {}".format(e.user_message)
|
"Details: {}".format(e.user_message)
|
||||||
)
|
)
|
||||||
@@ -452,7 +285,7 @@ class TestPage(QWidget):
|
|||||||
""")
|
""")
|
||||||
self.output.setPlainText(content)
|
self.output.setPlainText(content)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.widget_signal.show_and_print_error_message.emit(
|
self.widget_mgr.signal.show_and_print_error_message.emit(
|
||||||
{
|
{
|
||||||
"error": traceback.format_exception(e),
|
"error": traceback.format_exception(e),
|
||||||
"title": "Test Error",
|
"title": "Test Error",
|
||||||
@@ -470,7 +303,7 @@ class TestPage(QWidget):
|
|||||||
ver = self.get_selected_version()
|
ver = self.get_selected_version()
|
||||||
|
|
||||||
if not ver:
|
if not ver:
|
||||||
self.widget_signal.enable_widget.emit(self.button_2)
|
self.widget_mgr.signal.enable_widget.emit(self.button_2)
|
||||||
return
|
return
|
||||||
|
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
@@ -488,7 +321,7 @@ class TestPage(QWidget):
|
|||||||
ver = self.get_selected_version()
|
ver = self.get_selected_version()
|
||||||
|
|
||||||
if not ver:
|
if not ver:
|
||||||
self.widget_signal.enable_widget.emit(self.button_3)
|
self.widget_mgr.signal.enable_widget.emit(self.button_3)
|
||||||
return
|
return
|
||||||
|
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
@@ -506,7 +339,7 @@ class TestPage(QWidget):
|
|||||||
ver = self.get_selected_version()
|
ver = self.get_selected_version()
|
||||||
|
|
||||||
if not ver:
|
if not ver:
|
||||||
self.widget_signal.enable_widget.emit(self.button_5)
|
self.widget_mgr.signal.enable_widget.emit(self.button_5)
|
||||||
return
|
return
|
||||||
|
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
@@ -524,7 +357,7 @@ class TestPage(QWidget):
|
|||||||
ver = self.get_selected_version()
|
ver = self.get_selected_version()
|
||||||
|
|
||||||
if not ver:
|
if not ver:
|
||||||
self.widget_signal.enable_widget.emit(self.button_6)
|
self.widget_mgr.signal.enable_widget.emit(self.button_6)
|
||||||
return
|
return
|
||||||
|
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
@@ -542,7 +375,7 @@ class TestPage(QWidget):
|
|||||||
ver = self.get_selected_version()
|
ver = self.get_selected_version()
|
||||||
|
|
||||||
if not ver:
|
if not ver:
|
||||||
self.widget_signal.enable_widget.emit(self.button_7)
|
self.widget_mgr.signal.enable_widget.emit(self.button_7)
|
||||||
return
|
return
|
||||||
|
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
@@ -561,7 +394,7 @@ class TestPage(QWidget):
|
|||||||
args=(ver_type,)
|
args=(ver_type,)
|
||||||
)
|
)
|
||||||
thread.start()
|
thread.start()
|
||||||
self.download_signal.log.emit("Loading all versions...")
|
self.widget_mgr.download_signal.log.emit("Loading all versions...")
|
||||||
|
|
||||||
def load_all_types(self):
|
def load_all_types(self):
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
@@ -569,21 +402,21 @@ class TestPage(QWidget):
|
|||||||
daemon=True
|
daemon=True
|
||||||
)
|
)
|
||||||
thread.start()
|
thread.start()
|
||||||
self.download_signal.log.emit("Loading all types...")
|
self.widget_mgr.download_signal.log.emit("Loading all types...")
|
||||||
|
|
||||||
# All functions below that must be run in background thread
|
# All functions below that must be run in background thread
|
||||||
def _load_all_types(self):
|
def _load_all_types(self):
|
||||||
self.widget_signal.clean_ver_types_dropdown.emit()
|
self.signals.clean_ver_types_dropdown.emit()
|
||||||
|
|
||||||
data = self.get_cached_version_manifest()
|
data = self.get_cached_version_manifest()
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
|
|
||||||
ver_types = get_available_version_types(data)
|
ver_types = get_available_version_types(data)
|
||||||
self.widget_signal.update_types_dropdown.emit(ver_types)
|
self.signals.update_types_dropdown.emit(ver_types)
|
||||||
|
|
||||||
def _load_all_version(self, ver_type):
|
def _load_all_version(self, ver_type):
|
||||||
self.widget_signal.clean_vers_dropdown.emit()
|
self.signals.clean_vers_dropdown.emit()
|
||||||
|
|
||||||
data = self.get_cached_version_manifest()
|
data = self.get_cached_version_manifest()
|
||||||
if not data:
|
if not data:
|
||||||
@@ -593,7 +426,7 @@ class TestPage(QWidget):
|
|||||||
data,
|
data,
|
||||||
specified_type=ver_type
|
specified_type=ver_type
|
||||||
)
|
)
|
||||||
self.widget_signal.update_vers_dropdown.emit(vers)
|
self.signals.update_vers_dropdown.emit(vers)
|
||||||
|
|
||||||
#
|
#
|
||||||
# ============ Version Data Cache ============
|
# ============ Version Data Cache ============
|
||||||
@@ -620,15 +453,15 @@ class TestPage(QWidget):
|
|||||||
try:
|
try:
|
||||||
data = fetch_and_save_version_manifest(path, target_version=specified_ver, root_manifest=root_manifest)
|
data = fetch_and_save_version_manifest(path, target_version=specified_ver, root_manifest=root_manifest)
|
||||||
except VersionManifestFetchException as e:
|
except VersionManifestFetchException as e:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
e.user_message,
|
e.user_message,
|
||||||
)
|
)
|
||||||
except VersionManifestSaveException as e:
|
except VersionManifestSaveException as e:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
e.user_message,
|
e.user_message,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.widget_signal.show_and_print_error_message.emit(
|
self.widget_mgr.signal.show_and_print_error_message.emit(
|
||||||
{
|
{
|
||||||
"error": traceback.format_exception(e),
|
"error": traceback.format_exception(e),
|
||||||
"title": "Cache Error",
|
"title": "Cache Error",
|
||||||
@@ -641,7 +474,7 @@ class TestPage(QWidget):
|
|||||||
def _read_cached_version_manifest(self, specified_ver: str = None) -> dict | None:
|
def _read_cached_version_manifest(self, specified_ver: str = None) -> dict | None:
|
||||||
path = self.switch_between_specific_path(specified_ver)
|
path = self.switch_between_specific_path(specified_ver)
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
"Version manifest missing after check. Please try again later.",
|
"Version manifest missing after check. Please try again later.",
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
@@ -650,7 +483,7 @@ class TestPage(QWidget):
|
|||||||
version_manifest = json.loads(path.read_text())
|
version_manifest = json.loads(path.read_text())
|
||||||
return version_manifest
|
return version_manifest
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.widget_signal.show_and_print_error_message.emit(
|
self.widget_mgr.signal.show_and_print_error_message.emit(
|
||||||
{
|
{
|
||||||
"error": traceback.format_exception(e),
|
"error": traceback.format_exception(e),
|
||||||
"title": "Cache Error",
|
"title": "Cache Error",
|
||||||
@@ -674,7 +507,7 @@ class TestPage(QWidget):
|
|||||||
try:
|
try:
|
||||||
_, sha1 = get_specific_version_manifest_url_and_hash(root_manifest, specified_ver)
|
_, sha1 = get_specific_version_manifest_url_and_hash(root_manifest, specified_ver)
|
||||||
except NoSpecifiedVersionKeyException as e:
|
except NoSpecifiedVersionKeyException as e:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
f"Version {specified_ver} does not exist in version manifest.\n{e.user_message}"
|
f"Version {specified_ver} does not exist in version manifest.\n{e.user_message}"
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
@@ -701,27 +534,59 @@ class TestPage(QWidget):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def create_download_progress_callback(self, progress_title: str):
|
||||||
|
task_id = uuid4().hex
|
||||||
|
signals = self.widget_mgr.download_signal
|
||||||
|
|
||||||
|
signals.started.emit(task_id, progress_title)
|
||||||
|
|
||||||
|
def callback(name, percent):
|
||||||
|
signals.progress.emit(
|
||||||
|
task_id,
|
||||||
|
f"Downloading {name}",
|
||||||
|
percent,
|
||||||
|
)
|
||||||
|
|
||||||
|
return callback, task_id
|
||||||
|
|
||||||
#
|
#
|
||||||
# ============ Game Files ============
|
# ============ Game Files ============
|
||||||
#
|
#
|
||||||
# INFO: All are not in main thread
|
# INFO: All are not in main thread
|
||||||
def _download_client(self, ver: str):
|
def _download_client(self, ver: str):
|
||||||
|
callback, task_id = self.create_download_progress_callback(f"Downloading {ver} client")
|
||||||
try:
|
try:
|
||||||
ver_data = self.get_cached_version_manifest(ver)
|
ver_data = self.get_cached_version_manifest(ver)
|
||||||
|
|
||||||
if not ver_data:
|
if not ver_data:
|
||||||
|
self.widget_mgr.download_signal.failed.emit(
|
||||||
|
task_id,
|
||||||
|
"Version manifest is unavailable.",
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
client_dest = Path(self.app.work_dir, "versions", ver, f"{ver}.jar")
|
client_dest = Path(self.app.work_dir, "versions", ver, f"{ver}.jar")
|
||||||
download_core_file(ver_data, client_dest, progress_callback=self.download_signal.progress.emit,
|
download_core_file(
|
||||||
raise_for_null_hash=True)
|
ver_data,
|
||||||
self.download_signal.log.emit(f"Downloaded {ver} client to {client_dest}")
|
client_dest,
|
||||||
|
progress_callback=callback,
|
||||||
|
raise_for_null_hash=True,
|
||||||
|
)
|
||||||
|
self.widget_mgr.download_signal.log.emit(f"Downloaded {ver} client to {client_dest}")
|
||||||
except DownloadException as e:
|
except DownloadException as e:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_mgr.download_signal.failed.emit(
|
||||||
|
task_id,
|
||||||
|
e.user_message,
|
||||||
|
)
|
||||||
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
"Unable to download client while downloading it: {}".format(e.user_message),
|
"Unable to download client while downloading it: {}".format(e.user_message),
|
||||||
)
|
)
|
||||||
except VersionDataKeyNotFoundException as e:
|
except VersionDataKeyNotFoundException as e:
|
||||||
self.widget_signal.show_and_print_error_message.emit(
|
self.widget_mgr.download_signal.failed.emit(
|
||||||
|
task_id,
|
||||||
|
e.user_message,
|
||||||
|
)
|
||||||
|
self.widget_mgr.signal.show_and_print_error_message.emit(
|
||||||
{
|
{
|
||||||
"error": traceback.format_exception(e),
|
"error": traceback.format_exception(e),
|
||||||
"title": "Fetch Error",
|
"title": "Fetch Error",
|
||||||
@@ -729,23 +594,34 @@ class TestPage(QWidget):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.widget_signal.show_and_print_error_message.emit(
|
self.widget_mgr.download_signal.failed.emit(
|
||||||
|
task_id,
|
||||||
|
str(e),
|
||||||
|
)
|
||||||
|
self.widget_mgr.signal.show_and_print_error_message.emit(
|
||||||
{
|
{
|
||||||
"error": traceback.format_exception(e),
|
"error": traceback.format_exception(e),
|
||||||
"title": "Download Error",
|
"title": "Download Error",
|
||||||
"message": "Unable to download client, an unexpected error occurred."
|
"message": "Unable to download client, an unexpected error occurred."
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
self.widget_mgr.download_signal.finished.emit(task_id)
|
||||||
finally:
|
finally:
|
||||||
self.widget_signal.enable_widget.emit(self.button_2)
|
self.widget_mgr.signal.enable_widget.emit(self.button_2)
|
||||||
|
|
||||||
def _download_libraries(self, ver: str):
|
def _download_libraries(self, ver: str):
|
||||||
|
callback, task_id = self.create_download_progress_callback(f"Downloading libraries for version {ver}")
|
||||||
try:
|
try:
|
||||||
self.widget_signal.clean_output.emit()
|
self.signals.clean_output.emit()
|
||||||
|
|
||||||
ver_data = self.get_cached_version_manifest(ver)
|
ver_data = self.get_cached_version_manifest(ver)
|
||||||
|
|
||||||
if not ver_data:
|
if not ver_data:
|
||||||
|
self.widget_mgr.download_signal.failed.emit(
|
||||||
|
task_id,
|
||||||
|
"Version manifest is unavailable.",
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
libraries_dir = Path(self.app.work_dir, "libraries")
|
libraries_dir = Path(self.app.work_dir, "libraries")
|
||||||
@@ -757,90 +633,136 @@ class TestPage(QWidget):
|
|||||||
ver_data,
|
ver_data,
|
||||||
libraries_dir,
|
libraries_dir,
|
||||||
natives_dest,
|
natives_dest,
|
||||||
progress_callback=self.download_signal.progress.emit,
|
progress_callback=callback,
|
||||||
redownload_callback=self.redownload_files
|
redownload_callback=self.redownload_files
|
||||||
)
|
)
|
||||||
self.download_signal.log.emit(
|
self.widget_mgr.download_signal.log.emit(
|
||||||
"All libraries downloaded successfully."
|
"All libraries downloaded successfully."
|
||||||
)
|
)
|
||||||
except DependencyException as e:
|
except DependencyException as e:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_mgr.download_signal.failed.emit(
|
||||||
|
task_id,
|
||||||
|
e.user_message,
|
||||||
|
)
|
||||||
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
"Unable to collect library artifacts.\n"
|
"Unable to collect library artifacts.\n"
|
||||||
f"{e.user_message}"
|
f"{e.user_message}"
|
||||||
)
|
)
|
||||||
except DownloadException as e:
|
except DownloadException as e:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_mgr.download_signal.failed.emit(
|
||||||
|
task_id,
|
||||||
|
e.user_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
"Unable to collect library artifacts. An download error occurred:\n"
|
"Unable to collect library artifacts. An download error occurred:\n"
|
||||||
f"{e.user_message}"
|
f"{e.user_message}"
|
||||||
)
|
)
|
||||||
except UnsupportedPlatformException as e:
|
except UnsupportedPlatformException as e:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_mgr.download_signal.failed.emit(
|
||||||
|
task_id,
|
||||||
|
e.user_message,
|
||||||
|
)
|
||||||
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
"Unable to collect native artifacts.\n"
|
"Unable to collect native artifacts.\n"
|
||||||
f"{e.user_message}"
|
f"{e.user_message}"
|
||||||
)
|
)
|
||||||
except NativeResolveException as e:
|
except NativeResolveException as e:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_mgr.download_signal.failed.emit(
|
||||||
|
task_id,
|
||||||
|
e.user_message,
|
||||||
|
)
|
||||||
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
"Unable to collect native artifacts.\n"
|
"Unable to collect native artifacts.\n"
|
||||||
f"{e.user_message}"
|
f"{e.user_message}"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.widget_signal.show_and_print_error_message.emit(
|
self.widget_mgr.download_signal.failed.emit(
|
||||||
|
task_id,
|
||||||
|
str(e),
|
||||||
|
)
|
||||||
|
self.widget_mgr.signal.show_and_print_error_message.emit(
|
||||||
{
|
{
|
||||||
"error": traceback.format_exception(e),
|
"error": traceback.format_exception(e),
|
||||||
"title": "Download Error",
|
"title": "Download Error",
|
||||||
"message": "Unable to collect library artifacts. An unknown error occurred."
|
"message": "Unable to collect library artifacts. An unknown error occurred."
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
self.widget_mgr.download_signal.finished.emit(task_id)
|
||||||
finally:
|
finally:
|
||||||
self.widget_signal.enable_widget.emit(self.button_3)
|
self.widget_mgr.signal.enable_widget.emit(self.button_3)
|
||||||
|
|
||||||
def _download_assets(self, ver: str):
|
def _download_assets(self, ver: str):
|
||||||
|
callback, task_id = self.create_download_progress_callback(f"Downloading assets for version {ver}")
|
||||||
try:
|
try:
|
||||||
self.widget_signal.clean_output.emit()
|
self.signals.clean_output.emit()
|
||||||
|
|
||||||
ver_data = self.get_cached_version_manifest(ver)
|
ver_data = self.get_cached_version_manifest(ver)
|
||||||
|
|
||||||
if not ver_data:
|
if not ver_data:
|
||||||
|
self.widget_mgr.download_signal.failed.emit(
|
||||||
|
task_id,
|
||||||
|
"Version manifest is unavailable.",
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
assets_dir = Path(self.app.work_dir, "assets")
|
assets_dir = Path(self.app.work_dir, "assets")
|
||||||
|
|
||||||
fails = download_assets(ver_data, assets_dir,
|
fails = download_assets(ver_data, assets_dir,
|
||||||
no_hash_check=True,
|
no_hash_check=True,
|
||||||
progress_callback=self.download_signal.progress.emit,
|
progress_callback=callback,
|
||||||
launcher_root=self.app.work_dir)
|
launcher_root=self.app.work_dir)
|
||||||
|
|
||||||
if fails:
|
if fails:
|
||||||
self.widget_signal.show_warning_message.emit(
|
self.widget_mgr.download_signal.failed.emit(
|
||||||
|
task_id,
|
||||||
|
f"{len(fails)} asset files failed to download.",
|
||||||
|
)
|
||||||
|
self.widget_mgr.signal.show_warning_message.emit(
|
||||||
"Some assets were not downloaded. Although is not necessary for launching game."
|
"Some assets were not downloaded. Although is not necessary for launching game."
|
||||||
" But it may cause some texture issue when you playing. (Such as missing textures)"
|
" But it may cause some texture issue when you playing. (Such as missing textures)"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.download_signal.log.emit(
|
self.widget_mgr.download_signal.log.emit(
|
||||||
"All assets downloaded successfully."
|
"All assets downloaded successfully."
|
||||||
)
|
)
|
||||||
except AssetManifestFetchException as e:
|
except AssetManifestFetchException as e:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_mgr.download_signal.failed.emit(
|
||||||
|
task_id,
|
||||||
|
e.user_message,
|
||||||
|
)
|
||||||
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
f"Unable to fetch asset manifest.\n{e.user_message}"
|
f"Unable to fetch asset manifest.\n{e.user_message}"
|
||||||
)
|
)
|
||||||
except AssetManifestSaveException as e:
|
except AssetManifestSaveException as e:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_mgr.download_signal.failed.emit(
|
||||||
|
task_id,
|
||||||
|
e.user_message,
|
||||||
|
)
|
||||||
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
f"Unable to save asset manifest.\n{e.user_message}"
|
f"Unable to save asset manifest.\n{e.user_message}"
|
||||||
)
|
)
|
||||||
except AssetDataKeyNotFoundException as e:
|
except AssetDataKeyNotFoundException as e:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_mgr.download_signal.failed.emit(
|
||||||
|
task_id,
|
||||||
|
e.user_message,
|
||||||
|
)
|
||||||
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
f"Unable to collect assets.\n{e.user_message}"
|
f"Unable to collect assets.\n{e.user_message}"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.widget_signal.show_and_print_error_message.emit(
|
self.widget_mgr.signal.show_and_print_error_message.emit(
|
||||||
{
|
{
|
||||||
"error": traceback.format_exception(e),
|
"error": traceback.format_exception(e),
|
||||||
"title": "Download Error",
|
"title": "Download Error",
|
||||||
"message": "Unable to download assets. An unknown error occurred."
|
"message": "Unable to download assets. An unknown error occurred."
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
self.widget_mgr.download_signal.finished.emit(task_id)
|
||||||
finally:
|
finally:
|
||||||
self.widget_signal.enable_widget.emit(self.button_5)
|
self.widget_mgr.signal.enable_widget.emit(self.button_5)
|
||||||
|
|
||||||
def _parse_argument(self, ver):
|
def _parse_argument(self, ver):
|
||||||
try:
|
try:
|
||||||
@@ -854,7 +776,7 @@ class TestPage(QWidget):
|
|||||||
target_args = new_args if new_args else old_args
|
target_args = new_args if new_args else old_args
|
||||||
|
|
||||||
if not target_args:
|
if not target_args:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
"Unable to parse arguments because data are invalid.\n"
|
"Unable to parse arguments because data are invalid.\n"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -869,10 +791,10 @@ class TestPage(QWidget):
|
|||||||
game_args,
|
game_args,
|
||||||
arg_maps
|
arg_maps
|
||||||
)
|
)
|
||||||
self.download_signal.log.emit(f"Parsed jvm arguments: \n{parsed_jvm_args}\n{" ".join(parsed_jvm_args)}")
|
self.widget_mgr.download_signal.log.emit(f"Parsed jvm arguments: \n{parsed_jvm_args}\n{" ".join(parsed_jvm_args)}")
|
||||||
self.download_signal.log.emit(f"Parsed game arguments: \n{parsed_game_args}\n{" ".join(parsed_game_args)}")
|
self.widget_mgr.download_signal.log.emit(f"Parsed game arguments: \n{parsed_game_args}\n{" ".join(parsed_game_args)}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.widget_signal.show_and_print_error_message.emit(
|
self.widget_mgr.signal.show_and_print_error_message.emit(
|
||||||
{
|
{
|
||||||
"error": traceback.format_exception(e),
|
"error": traceback.format_exception(e),
|
||||||
"title": "Parse Error",
|
"title": "Parse Error",
|
||||||
@@ -880,7 +802,7 @@ class TestPage(QWidget):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
self.widget_signal.enable_widget.emit(self.button_6)
|
self.widget_mgr.signal.enable_widget.emit(self.button_6)
|
||||||
|
|
||||||
def _launch_offline_game(self, ver):
|
def _launch_offline_game(self, ver):
|
||||||
"""
|
"""
|
||||||
@@ -927,10 +849,23 @@ class TestPage(QWidget):
|
|||||||
current_status = 3
|
current_status = 3
|
||||||
default_assets_dir = Path(self.app.work_dir, "assets")
|
default_assets_dir = Path(self.app.work_dir, "assets")
|
||||||
if not check_assets_exist(ver_data, default_assets_dir, launcher_root=self.app.work_dir):
|
if not check_assets_exist(ver_data, default_assets_dir, launcher_root=self.app.work_dir):
|
||||||
download_assets(ver_data, default_assets_dir,
|
callback, task_id = self.create_download_progress_callback(
|
||||||
|
"Downloading missing assets"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
download_assets(
|
||||||
|
ver_data,
|
||||||
|
default_assets_dir,
|
||||||
no_hash_check=True,
|
no_hash_check=True,
|
||||||
progress_callback=self.download_signal.progress.emit,
|
progress_callback=callback,
|
||||||
launcher_root=self.app.work_dir)
|
launcher_root=self.app.work_dir,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.widget_mgr.download_signal.failed.emit(task_id, str(e))
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
self.widget_mgr.download_signal.finished.emit(task_id)
|
||||||
|
|
||||||
assets_dir = find_correct_assets_dir(asset_id, default_assets_dir, self.app.work_dir)
|
assets_dir = find_correct_assets_dir(asset_id, default_assets_dir, self.app.work_dir)
|
||||||
|
|
||||||
@@ -964,7 +899,7 @@ class TestPage(QWidget):
|
|||||||
target_args = arguments if arguments else old_args
|
target_args = arguments if arguments else old_args
|
||||||
|
|
||||||
if not target_args:
|
if not target_args:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
"Unable to parse arguments because data are invalid.\n"
|
"Unable to parse arguments because data are invalid.\n"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -983,10 +918,10 @@ class TestPage(QWidget):
|
|||||||
filter_raw="Java executable (java.exe javaw.exe);;All files (*)"
|
filter_raw="Java executable (java.exe javaw.exe);;All files (*)"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.widget_signal.ask_for_path.emit(request)
|
self.widget_mgr.signal.ask_for_path.emit(request)
|
||||||
|
|
||||||
if not request.wait(120):
|
if not request.wait(120):
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
"Timed out while waiting for Java executable selection."
|
"Timed out while waiting for Java executable selection."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -994,9 +929,9 @@ class TestPage(QWidget):
|
|||||||
java_path = request.result()
|
java_path = request.result()
|
||||||
|
|
||||||
if java_path:
|
if java_path:
|
||||||
self.download_signal.log.emit(f"Use specified java executable path: {java_path}")
|
self.widget_mgr.download_signal.log.emit(f"Use specified java executable path: {java_path}")
|
||||||
else:
|
else:
|
||||||
self.download_signal.log.emit("Use system environment java.")
|
self.widget_mgr.download_signal.log.emit("Use system environment java.")
|
||||||
java_path = "java"
|
java_path = "java"
|
||||||
|
|
||||||
# Finally, generate launch command
|
# Finally, generate launch command
|
||||||
@@ -1007,7 +942,7 @@ class TestPage(QWidget):
|
|||||||
full_args=target_args,
|
full_args=target_args,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.download_signal.log.emit(f"Launch command: {cmd}")
|
self.widget_mgr.download_signal.log.emit(f"Launch command: {cmd}")
|
||||||
|
|
||||||
# run it
|
# run it
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
@@ -1021,9 +956,9 @@ class TestPage(QWidget):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for line in process.stdout:
|
for line in process.stdout:
|
||||||
self.download_signal.log.emit(line.rstrip())
|
self.widget_mgr.download_signal.log.emit(line.rstrip())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.widget_signal.show_and_print_error_message.emit(
|
self.widget_mgr.signal.show_and_print_error_message.emit(
|
||||||
{
|
{
|
||||||
"error": traceback.format_exception(e),
|
"error": traceback.format_exception(e),
|
||||||
"title": "Launch Error",
|
"title": "Launch Error",
|
||||||
@@ -1031,7 +966,7 @@ class TestPage(QWidget):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
self.widget_signal.enable_widget.emit(self.button_7)
|
self.widget_mgr.signal.enable_widget.emit(self.button_7)
|
||||||
|
|
||||||
def redownload_files(self, files: list[FileObject]) -> tuple[bool, list[FileObject]]:
|
def redownload_files(self, files: list[FileObject]) -> tuple[bool, list[FileObject]]:
|
||||||
if not files:
|
if not files:
|
||||||
@@ -1045,10 +980,10 @@ class TestPage(QWidget):
|
|||||||
request = AskForRequest("Redownload", message)
|
request = AskForRequest("Redownload", message)
|
||||||
request.details = "\n".join(file.posix_path for file in files)
|
request.details = "\n".join(file.posix_path for file in files)
|
||||||
|
|
||||||
self.widget_signal.askyesno.emit(request)
|
self.widget_mgr.signal.askyesno.emit(request)
|
||||||
|
|
||||||
if not request.wait(60):
|
if not request.wait(60):
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
"Timed out while waiting for confirmation. Please try again."
|
"Timed out while waiting for confirmation. Please try again."
|
||||||
)
|
)
|
||||||
return False, files
|
return False, files
|
||||||
@@ -1061,26 +996,34 @@ class TestPage(QWidget):
|
|||||||
# copy
|
# copy
|
||||||
pending = list(files)
|
pending = list(files)
|
||||||
|
|
||||||
|
callback, task_id = self.create_download_progress_callback(f"Redownloading {len(files)} files.")
|
||||||
|
|
||||||
for retry_time in range(1, MAX_DOWNLOAD_ATTEMPTS + 1):
|
for retry_time in range(1, MAX_DOWNLOAD_ATTEMPTS + 1):
|
||||||
if not pending:
|
if not pending:
|
||||||
return True, []
|
return True, []
|
||||||
|
|
||||||
self.download_signal.log.emit(f"Attempt {retry_time}/{len(pending)}")
|
self.widget_mgr.download_signal.log.emit(f"Attempt {retry_time}/{len(pending)}")
|
||||||
|
|
||||||
fails, successes = download_multiple_files(pending, progress_callback=self.download_signal.progress.emit)
|
fails, successes = download_multiple_files(pending,
|
||||||
|
progress_callback=callback)
|
||||||
|
|
||||||
for file in successes:
|
for file in successes:
|
||||||
self.download_signal.log.emit(f"Downloaded {file.posix_path}.")
|
self.widget_mgr.download_signal.log.emit(f"Downloaded {file.posix_path}.")
|
||||||
|
|
||||||
if fails:
|
if fails:
|
||||||
pending = fails
|
pending = fails
|
||||||
else:
|
else:
|
||||||
|
self.widget_mgr.download_signal.finished.emit(task_id)
|
||||||
pending = []
|
pending = []
|
||||||
|
|
||||||
if not pending:
|
if not pending:
|
||||||
return True, []
|
return True, []
|
||||||
|
|
||||||
failed_list = "\n".join(file.posix_path for file in pending)
|
failed_list = "\n".join(file.posix_path for file in pending)
|
||||||
|
self.widget_mgr.download_signal.failed.emit(
|
||||||
|
task_id,
|
||||||
|
f"{len(pending)} files still failed after retries.",
|
||||||
|
)
|
||||||
self.app.logger.warning(
|
self.app.logger.warning(
|
||||||
"Unable to download these files:\n{}".format(failed_list)
|
"Unable to download these files:\n{}".format(failed_list)
|
||||||
)
|
)
|
||||||
@@ -1097,7 +1040,7 @@ class TestPage(QWidget):
|
|||||||
def _find_and_save_jvm_search_result(self):
|
def _find_and_save_jvm_search_result(self):
|
||||||
installations, errors = find_java_installations()
|
installations, errors = find_java_installations()
|
||||||
if not installations:
|
if not installations:
|
||||||
self.widget_signal.show_warning_message.emit(
|
self.widget_mgr.signal.show_warning_message.emit(
|
||||||
"There are no Java installations available in you system.\n"
|
"There are no Java installations available in you system.\n"
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
@@ -1109,7 +1052,7 @@ class TestPage(QWidget):
|
|||||||
request.details = "\n".join(path.as_posix() for path in errors)
|
request.details = "\n".join(path.as_posix() for path in errors)
|
||||||
request.use_plain_text = True
|
request.use_plain_text = True
|
||||||
|
|
||||||
self.widget_signal.askyesno.emit(request)
|
self.widget_mgr.signal.askyesno.emit(request)
|
||||||
|
|
||||||
self.jvm_settings_path.parent.mkdir(parents=True, exist_ok=True)
|
self.jvm_settings_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
@@ -1119,7 +1062,7 @@ class TestPage(QWidget):
|
|||||||
|
|
||||||
return installations
|
return installations
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.widget_signal.show_and_print_error_message.emit(
|
self.widget_mgr.signal.show_and_print_error_message.emit(
|
||||||
{
|
{
|
||||||
"error": traceback.format_exception(e),
|
"error": traceback.format_exception(e),
|
||||||
"title": "Settings Error",
|
"title": "Settings Error",
|
||||||
@@ -1131,7 +1074,7 @@ class TestPage(QWidget):
|
|||||||
try:
|
try:
|
||||||
return json.loads(self.jvm_settings_path.read_text())
|
return json.loads(self.jvm_settings_path.read_text())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.widget_signal.show_and_print_error_message.emit(
|
self.widget_mgr.signal.show_and_print_error_message.emit(
|
||||||
{
|
{
|
||||||
"error": traceback.format_exception(e),
|
"error": traceback.format_exception(e),
|
||||||
"title": "Settings Error",
|
"title": "Settings Error",
|
||||||
@@ -1171,7 +1114,7 @@ class TestPage(QWidget):
|
|||||||
return executable
|
return executable
|
||||||
|
|
||||||
if not no_error:
|
if not no_error:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
"No Java executable found for major version {}.".format(major_version)
|
"No Java executable found for major version {}.".format(major_version)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,366 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PySide6 import QtWidgets
|
||||||
|
from PySide6.QtCore import QSize
|
||||||
|
from PySide6.QtGui import QIcon, Qt, QPixmap
|
||||||
|
from PySide6.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QWidget, QToolButton, \
|
||||||
|
QStackedWidget
|
||||||
|
from PySide6.QtCore import Slot
|
||||||
|
from PySide6.QtWidgets import QMessageBox, QFileDialog
|
||||||
|
|
||||||
|
from core_lib.qt import messagebox
|
||||||
|
from core_lib.qt.progress_dialog import ProgressDialog
|
||||||
|
from core_lib.qt.messagebox import ask_yes_no_with_plain_text
|
||||||
|
from constant import MAIN_REPO_URL
|
||||||
|
from manager.profile import ProfileManager
|
||||||
|
from manager.widgets import AskForRequest, SelectPathRequest
|
||||||
|
from pages.account import AccountPage
|
||||||
|
from pages.create_profile import CreateProfile
|
||||||
|
from pages.test import TestPage
|
||||||
|
|
||||||
|
class ToolBar(QtWidgets.QToolBar):
|
||||||
|
def __init__(self, app: QApplication, parent=None):
|
||||||
|
super(ToolBar, self).__init__(parent)
|
||||||
|
self.app = app
|
||||||
|
self.spacer = QWidget()
|
||||||
|
self.orientationChanged.connect(self._update_widget_size)
|
||||||
|
|
||||||
|
def _update_widget_size(self, orientation):
|
||||||
|
vertical = orientation == Qt.Orientation.Vertical
|
||||||
|
|
||||||
|
if vertical:
|
||||||
|
self.spacer.setSizePolicy(
|
||||||
|
QtWidgets.QSizePolicy.Policy.Preferred,
|
||||||
|
QtWidgets.QSizePolicy.Policy.Expanding,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.spacer.setSizePolicy(
|
||||||
|
QtWidgets.QSizePolicy.Policy.Expanding,
|
||||||
|
QtWidgets.QSizePolicy.Policy.Preferred,
|
||||||
|
)
|
||||||
|
|
||||||
|
for button in self.findChildren(QToolButton):
|
||||||
|
if vertical:
|
||||||
|
button.setToolButtonStyle(
|
||||||
|
Qt.ToolButtonStyle.ToolButtonIconOnly
|
||||||
|
)
|
||||||
|
button.setFixedSize(60, 60)
|
||||||
|
continue
|
||||||
|
|
||||||
|
button.setMinimumSize(0, 0)
|
||||||
|
button.setMaximumSize(16777215, 16777215)
|
||||||
|
|
||||||
|
if not button.icon().isNull() and not button.text():
|
||||||
|
button.setToolButtonStyle(
|
||||||
|
Qt.ToolButtonStyle.ToolButtonIconOnly
|
||||||
|
)
|
||||||
|
button.setFixedSize(60, 60)
|
||||||
|
elif button.icon().isNull():
|
||||||
|
button.setToolButtonStyle(
|
||||||
|
Qt.ToolButtonStyle.ToolButtonTextOnly
|
||||||
|
)
|
||||||
|
button.setFixedWidth(150)
|
||||||
|
else:
|
||||||
|
button.setToolButtonStyle(
|
||||||
|
Qt.ToolButtonStyle.ToolButtonTextBesideIcon
|
||||||
|
)
|
||||||
|
button.setMinimumWidth(60)
|
||||||
|
|
||||||
|
def add_button(self, text=None, icon: QIcon=None) -> QToolButton:
|
||||||
|
button = QtWidgets.QToolButton(self)
|
||||||
|
|
||||||
|
if text:
|
||||||
|
text = " "+text
|
||||||
|
button.setText(text)
|
||||||
|
# else:
|
||||||
|
# button.setText("\u00a0")
|
||||||
|
|
||||||
|
if icon:
|
||||||
|
button.setIcon(icon)
|
||||||
|
button.setIconSize(QSize(32, 32))
|
||||||
|
|
||||||
|
if text:
|
||||||
|
button.setToolButtonStyle(
|
||||||
|
Qt.ToolButtonStyle.ToolButtonTextBesideIcon
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
button.setToolButtonStyle(
|
||||||
|
Qt.ToolButtonStyle.ToolButtonIconOnly
|
||||||
|
)
|
||||||
|
|
||||||
|
button.setAutoRaise(True)
|
||||||
|
button.setFixedHeight(60)
|
||||||
|
|
||||||
|
return button
|
||||||
|
|
||||||
|
def update_all(self):
|
||||||
|
self._update_widget_size(self.orientation())
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
class LauncherMainWindow(QMainWindow):
|
||||||
|
def __init__(self, / ,app, widget_manager, parent: QWidget | None=None):
|
||||||
|
super(LauncherMainWindow, self).__init__(parent)
|
||||||
|
self.app = app
|
||||||
|
self.widget_manager = widget_manager
|
||||||
|
|
||||||
|
# Main layout
|
||||||
|
self.central = QStackedWidget()
|
||||||
|
self.toolbar = ToolBar(self.app, self)
|
||||||
|
|
||||||
|
# center message (will be deleted if launcher finished development)
|
||||||
|
content = """Still digging!"""
|
||||||
|
message = "If you found that something might be a issue. You can report it to the main repo! Any suggestion are welcome."
|
||||||
|
self.icon_label = QLabel()
|
||||||
|
self.icon_label.setObjectName("icon_label")
|
||||||
|
self.dev_info_box = QLabel(content)
|
||||||
|
self.dev_info_box.setObjectName("dev_info_box")
|
||||||
|
self.dev_info_2 = QLabel(message)
|
||||||
|
self.dev_info_2.setObjectName("dev_info_message")
|
||||||
|
self.version_label = QLabel("Current version: {}".format(self.app.launcher_version))
|
||||||
|
self.version_label.setObjectName("version_label")
|
||||||
|
self.repo_url_label = QLabel(f"<a href=\"{MAIN_REPO_URL}\">Main repo</a>")
|
||||||
|
self.repo_url_label.setObjectName("repo_url_label")
|
||||||
|
self.repo_url_label.setOpenExternalLinks(True)
|
||||||
|
|
||||||
|
if Path(self.app.resources_dir, "pictures", "in_progress.png").exists():
|
||||||
|
image = QPixmap(Path(self.app.resources_dir, "pictures", "in_progress.png"))
|
||||||
|
self.icon_label.setPixmap(image.scaled(300, 300))
|
||||||
|
else:
|
||||||
|
self.icon_label.setText("Ouch. The icon is missing.")
|
||||||
|
|
||||||
|
# Manager
|
||||||
|
self.profile_manager = ProfileManager(self)
|
||||||
|
|
||||||
|
# Dialog
|
||||||
|
self.download_dialog = ProgressDialog(self)
|
||||||
|
|
||||||
|
# Homepage
|
||||||
|
self.home_page = QWidget()
|
||||||
|
self.home_layout = QVBoxLayout(self.home_page)
|
||||||
|
self.home_layout.addStretch()
|
||||||
|
self.home_layout.addWidget(self.icon_label, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self.home_layout.addWidget(self.dev_info_box, alignment=Qt.AlignmentFlag.AlignHCenter)
|
||||||
|
self.home_layout.addWidget(self.dev_info_2, alignment=Qt.AlignmentFlag.AlignHCenter)
|
||||||
|
self.home_layout.addWidget(self.version_label, alignment=Qt.AlignmentFlag.AlignHCenter)
|
||||||
|
self.home_layout.addWidget(self.repo_url_label, alignment=Qt.AlignmentFlag.AlignHCenter)
|
||||||
|
self.home_layout.addStretch()
|
||||||
|
|
||||||
|
# test page
|
||||||
|
self.test_page = TestPage(
|
||||||
|
app=self.app,
|
||||||
|
parent=self,
|
||||||
|
widget_manager=self.app.widget_manager
|
||||||
|
)
|
||||||
|
|
||||||
|
# Account page
|
||||||
|
self.account_page = AccountPage(self.app, self)
|
||||||
|
|
||||||
|
# Profile page
|
||||||
|
self.create_profile_page = CreateProfile(self.app, self)
|
||||||
|
|
||||||
|
# Add page
|
||||||
|
self.central.addWidget(self.home_page)
|
||||||
|
self.central.addWidget(self.test_page)
|
||||||
|
self.central.addWidget(self.account_page)
|
||||||
|
self.central.addWidget(self.create_profile_page)
|
||||||
|
|
||||||
|
# Bind toolbar and center widget
|
||||||
|
self.addToolBar(Qt.ToolBarArea.LeftToolBarArea, self.toolbar)
|
||||||
|
self.setCentralWidget(self.central)
|
||||||
|
|
||||||
|
# Apply qss
|
||||||
|
self.app.apply_qss(Path("main.qss"), self.setStyleSheet)
|
||||||
|
self.app.apply_qss(Path("toolbar.qss"), self.toolbar.setStyleSheet)
|
||||||
|
|
||||||
|
# Toolbar
|
||||||
|
|
||||||
|
# Test Window btn
|
||||||
|
self.open_test_page = self.toolbar.add_button(" Tests", icon=self.app.get_icon(Path(self.app.icon_dir, "debug.png")))
|
||||||
|
self.open_test_page.setObjectName("open_test_page")
|
||||||
|
|
||||||
|
# Home btn
|
||||||
|
self.home_button = self.toolbar.add_button(icon=self.app.get_icon(Path(self.app.icon_dir, "home.png")))
|
||||||
|
|
||||||
|
# Account btn (will be moved to the bottom (or last one item) of the toolbar
|
||||||
|
self.open_account_page = self.toolbar.add_button(icon=self.app.get_icon(Path(self.app.icon_dir, "head.png")))
|
||||||
|
|
||||||
|
# Profile btn
|
||||||
|
self.open_create_profile_page = self.toolbar.add_button(icon=self.app.get_icon(Path(self.app.icon_dir, "add_temp.png")))
|
||||||
|
|
||||||
|
# Bind tool buttons
|
||||||
|
self.toolbar.addWidget(self.home_button)
|
||||||
|
self.toolbar.addWidget(self.open_test_page)
|
||||||
|
self.toolbar.addWidget(self.open_create_profile_page)
|
||||||
|
self.toolbar.setMovable(True)
|
||||||
|
self.toolbar.setFloatable(False)
|
||||||
|
self.toolbar.setAllowedAreas(Qt.ToolBarArea.AllToolBarAreas)
|
||||||
|
|
||||||
|
self.toolbar.addWidget(self.toolbar.spacer)
|
||||||
|
self.toolbar.addSeparator()
|
||||||
|
self.toolbar.addWidget(self.open_account_page)
|
||||||
|
|
||||||
|
# Bind page-related button click event
|
||||||
|
self.home_button.clicked.connect(
|
||||||
|
lambda: self.set_current_page(self.home_page, self.home_button)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.open_test_page.clicked.connect(
|
||||||
|
lambda: self.set_current_page(self.test_page, self.open_test_page)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.open_account_page.clicked.connect(
|
||||||
|
lambda: self.set_current_page(self.account_page, self.open_account_page)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.open_create_profile_page.clicked.connect(
|
||||||
|
lambda: self.set_current_page(self.create_profile_page, self.open_create_profile_page)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bind signal event
|
||||||
|
self.bind_event()
|
||||||
|
|
||||||
|
self.set_current_page(self.home_page, self.home_button)
|
||||||
|
|
||||||
|
@Slot(object)
|
||||||
|
def ask_request(self, context: AskForRequest):
|
||||||
|
if not context.use_plain_text:
|
||||||
|
msg = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
context.title,
|
||||||
|
context.message,
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
|
QMessageBox.StandardButton.Yes,
|
||||||
|
)
|
||||||
|
result = msg == QMessageBox.StandardButton.Yes
|
||||||
|
else:
|
||||||
|
result = ask_yes_no_with_plain_text(
|
||||||
|
self,
|
||||||
|
context.title,
|
||||||
|
context.message,
|
||||||
|
context.details,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
context.result_queue.put(True)
|
||||||
|
else:
|
||||||
|
context.result_queue.put(False)
|
||||||
|
|
||||||
|
context.wait_event.set()
|
||||||
|
|
||||||
|
@Slot(object)
|
||||||
|
def ask_for_path(self, context: SelectPathRequest):
|
||||||
|
path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self,
|
||||||
|
context.title,
|
||||||
|
"",
|
||||||
|
context.filter,
|
||||||
|
)
|
||||||
|
|
||||||
|
context.result_queue.put(path)
|
||||||
|
|
||||||
|
context.wait_event.set()
|
||||||
|
|
||||||
|
@Slot(dict)
|
||||||
|
def show_and_print_error(self, context: dict):
|
||||||
|
error = context.get("error", None)
|
||||||
|
title = context.get("title", "Error")
|
||||||
|
message = context.get("message", "No content")
|
||||||
|
|
||||||
|
if isinstance(error, list):
|
||||||
|
self.app.logger.error("".join(error))
|
||||||
|
elif isinstance(error, str):
|
||||||
|
self.app.logger.error(error)
|
||||||
|
elif isinstance(error, Exception):
|
||||||
|
self.app.logger.exception(error)
|
||||||
|
|
||||||
|
messagebox.error(
|
||||||
|
self,
|
||||||
|
title,
|
||||||
|
message=message
|
||||||
|
)
|
||||||
|
|
||||||
|
@Slot(object)
|
||||||
|
def enable_widget(self, widget):
|
||||||
|
widget.setEnabled(True)
|
||||||
|
|
||||||
|
@Slot(object)
|
||||||
|
def disable_widget(self, widget):
|
||||||
|
widget.setDisabled(True)
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
|
def show_error(self, message):
|
||||||
|
messagebox.error(self, "Error", message=message)
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
|
def show_warning(self, widget):
|
||||||
|
messagebox.warning(self, "Warning", message=widget)
|
||||||
|
|
||||||
|
@Slot(str)
|
||||||
|
def show_info(self, widget):
|
||||||
|
messagebox.info(self, "Info", message=widget)
|
||||||
|
|
||||||
|
def set_current_page(self, page: QWidget, related_button: QToolButton):
|
||||||
|
self.central.setCurrentWidget(page)
|
||||||
|
related_button.setFocus()
|
||||||
|
|
||||||
|
def bind_event(self):
|
||||||
|
download_signal = self.widget_manager.download_signal
|
||||||
|
widget_signal = self.widget_manager.signal
|
||||||
|
|
||||||
|
download_signal.started.connect(
|
||||||
|
self.download_dialog.add_task,
|
||||||
|
Qt.ConnectionType.QueuedConnection,
|
||||||
|
)
|
||||||
|
download_signal.progress.connect(
|
||||||
|
self.download_dialog.update_task,
|
||||||
|
Qt.ConnectionType.QueuedConnection,
|
||||||
|
)
|
||||||
|
download_signal.finished.connect(
|
||||||
|
self.download_dialog.finish_task,
|
||||||
|
Qt.ConnectionType.QueuedConnection,
|
||||||
|
)
|
||||||
|
download_signal.failed.connect(
|
||||||
|
self.download_dialog.fail_task,
|
||||||
|
Qt.ConnectionType.QueuedConnection,
|
||||||
|
)
|
||||||
|
download_signal.removed.connect(
|
||||||
|
self.download_dialog.remove_task,
|
||||||
|
Qt.ConnectionType.QueuedConnection,
|
||||||
|
)
|
||||||
|
|
||||||
|
widget_signal.show_error_message.connect(
|
||||||
|
self.show_error,
|
||||||
|
Qt.ConnectionType.QueuedConnection,
|
||||||
|
)
|
||||||
|
|
||||||
|
widget_signal.show_warning_message.connect(
|
||||||
|
self.show_warning,
|
||||||
|
Qt.ConnectionType.QueuedConnection,
|
||||||
|
)
|
||||||
|
|
||||||
|
widget_signal.show_info_message.connect(
|
||||||
|
self.show_info,
|
||||||
|
Qt.ConnectionType.QueuedConnection,
|
||||||
|
)
|
||||||
|
|
||||||
|
widget_signal.show_and_print_error_message.connect(
|
||||||
|
self.show_and_print_error
|
||||||
|
)
|
||||||
|
|
||||||
|
widget_signal.enable_widget.connect(
|
||||||
|
self.enable_widget,
|
||||||
|
Qt.ConnectionType.QueuedConnection,
|
||||||
|
)
|
||||||
|
|
||||||
|
widget_signal.disable_widget.connect(
|
||||||
|
self.disable_widget,
|
||||||
|
Qt.ConnectionType.QueuedConnection,
|
||||||
|
)
|
||||||
|
|
||||||
|
widget_signal.askyesno.connect(
|
||||||
|
self.ask_request
|
||||||
|
)
|
||||||
|
|
||||||
|
widget_signal.ask_for_path.connect(
|
||||||
|
self.ask_for_path
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user