Finish asset gui logic and argument parse module.

This commit is contained in:
2026-07-13 01:19:11 +08:00
parent a14e2a5d54
commit ccbe2052b8
4 changed files with 311 additions and 16 deletions
+151
View File
@@ -0,0 +1,151 @@
import logging
import shlex
from core_lib.game.library import get_natives_platform_info
logger = logging.getLogger("Launcher.CoreLib")
def is_argument_allowed(argument: dict | str, allow_features: dict=dict) -> bool:
"""
Check if argument is allowed or not.
:param argument:
:param allow_features: Should be like {key: value}
:return:
allowed: bool
"""
if isinstance(argument, str):
return True
rules = argument.get('rules', {})
_, platform_rule_name, _ = get_natives_platform_info()
allowed = False
if not rules:
return True
for rule in rules:
rule_action = rule.get("action", None)
os_section = rule.get("os", {})
rule_os = os_section.get("name", None)
features_rule = rule.get("features", None)
if rule_os is not None and platform_rule_name != rule_os:
continue
matched = True
# 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
if rule_action == "allow":
allowed = True
elif rule_action == "disallow":
allowed = False
else:
logger.warning(f"Unknown action {rule_action}\n")
return allowed
def replace_placeholders(argument, placeholders: dict) -> str:
if argument:
for key, value in placeholders.items():
argument = argument.replace("${" + key + "}", "" if value is None else str(value))
return argument
class ArgumentMappings:
def __init__(self, player_name="Player", version_name="unknown",
game_directory=None, assets_root=None, assets_index_name=None,
auth_uuid="00000001-0002-0003-0004-000000000005",
auth_access_token="", clientid=None, auth_xuid=None,
user_type=None, version_type=None, natives_directory=None,
launcher_name=None, launcher_version=None,
classpath=None, classpath_separator=None, library_directory=None):
self.player_name = player_name
self.version_name = version_name
self.game_directory = game_directory
self.assets_root = assets_root
self.assets_index_name = assets_index_name
self.auth_uuid = auth_uuid
self.auth_access_token = auth_access_token
self.clientid = clientid
self.auth_xuid = auth_xuid
self.user_type = user_type
self.version_type = version_type
self.natives_directory = natives_directory
self.launcher_name = launcher_name
self.launcher_version = launcher_version
self.classpath = classpath
self.classpath_separator = classpath_separator
self.library_directory = library_directory
def generate_placeholders(self):
return {
"player_name": self.player_name,
"version_name": self.version_name,
"game_directory": self.game_directory,
"assets_root": self.assets_root,
"assets_index_name": self.assets_index_name,
"auth_uuid": self.auth_uuid,
"auth_access_token": self.auth_access_token,
"auth_player_name": self.player_name,
"clientid": self.clientid,
"auth_xuid": self.auth_xuid,
"user_type": self.user_type,
"version_type": self.version_type,
"natives_directory": self.natives_directory,
"launcher_name": self.launcher_name,
"launcher_version": self.launcher_version,
"classpath": self.classpath,
"classpath_separator": self.classpath_separator,
"library_directory": self.library_directory
}
def convert_argument_to_multiple_string(argument: str | dict) -> list[str]:
if isinstance(argument, str):
return [argument]
if isinstance(argument, dict):
value = argument.get("value", None)
if isinstance(value, list):
return [str(item) for item in value] # ensure all item are string
elif isinstance(value, str):
return [value]
raise TypeError("Argument must be of type str or dict")
def parse_and_generate_arguments(arguments: list | str, mappings: ArgumentMappings, features: dict=dict) -> list[str]:
parsed_arguments = []
if isinstance(arguments, str):
# Convert legacy argument into a list type
arguments = shlex.split(arguments)
placeholders = mappings.generate_placeholders()
for argument in arguments:
# Check rules
if not is_argument_allowed(argument, allow_features=features):
continue
# Each argument (section) may contain multiple items, convert it to list before replace placeholder
converted = convert_argument_to_multiple_string(argument)
for item in converted:
parsed = replace_placeholders(item, placeholders)
parsed_arguments.append(parsed)
return parsed_arguments
+12 -2
View File
@@ -13,6 +13,8 @@ from core_lib.game.version_info import find_useful_part_in_specific_version_mani
MOJANG_RESOURCE_ENDPOINT = "https://resources.download.minecraft.net/{beginning}/{hash}" MOJANG_RESOURCE_ENDPOINT = "https://resources.download.minecraft.net/{beginning}/{hash}"
ASSET_DOWNLOAD_MAX_WORKERS = 20
logger = logging.getLogger("Launcher.CoreLib") logger = logging.getLogger("Launcher.CoreLib")
def fetch_asset_manifest(target_version_manifest: dict, raw=False) -> tuple[dict | bytes, str]: def fetch_asset_manifest(target_version_manifest: dict, raw=False) -> tuple[dict | bytes, str]:
@@ -84,7 +86,7 @@ def fetch_and_save_asset_manifest(target_version_manifest: dict, destination: Pa
with file.path.open("wb") as f: with file.path.open("wb") as f:
f.write(data) f.write(data)
if sha1 is not None and not file.verify_sha1(sha1): if sha1 and not file.verify_sha1(sha1):
raise HashMismatchException( raise HashMismatchException(
"sha1", "sha1",
file.sha1, file.sha1,
@@ -94,6 +96,7 @@ def fetch_and_save_asset_manifest(target_version_manifest: dict, destination: Pa
return json.loads(data.decode("utf-8")) return json.loads(data.decode("utf-8"))
except Exception as e: except Exception as e:
logger.exception(e)
raise AssetManifestSaveException( raise AssetManifestSaveException(
"Error saving version manifest:\n{}".format(e) "Error saving version manifest:\n{}".format(e)
) )
@@ -164,7 +167,14 @@ def download_assets_and_copy(assets: list[FileObject], legacy_assets: list[dict]
files: list[FileObject] (files that download failed) files: list[FileObject] (files that download failed)
""" """
fails , _ = download_multiple_files(assets, progress_callback=progress_callback) fails , _ = download_multiple_files(assets, progress_callback=progress_callback,
max_workers=ASSET_DOWNLOAD_MAX_WORKERS)
if fails:
logger.warning(
"Download failed for assets: \n"
+"\n".join([asset.posix_path for asset in fails])
)
for asset in legacy_assets: for asset in legacy_assets:
target = asset.get("target") target = asset.get("target")
+1
View File
@@ -413,6 +413,7 @@ def fetch_and_save_version_manifest(dest: Path, target_version: str=None, root_m
return data return data
except Exception as e: except Exception as e:
logger.exception(e)
raise VersionManifestSaveException( raise VersionManifestSaveException(
"Error saving version manifest:\n{}".format(e) "Error saving version manifest:\n{}".format(e)
) )
+147 -14
View File
@@ -2,6 +2,7 @@ import json
import textwrap import textwrap
import threading import threading
import time import time
import traceback
from pathlib import Path from pathlib import Path
from PySide6.QtCore import Qt, QObject, Signal, Slot from PySide6.QtCore import Qt, QObject, Signal, Slot
@@ -11,7 +12,9 @@ from PySide6.QtWidgets import QPushButton, QVBoxLayout, QDialog, QPlainTextEdit,
from core_lib.common.common import download_file, FileObject, download_multiple_files from core_lib.common.common import download_file, FileObject, download_multiple_files
from core_lib.common.exception import VersionNotSelectedException, UnsupportedPlatformException, \ from core_lib.common.exception import VersionNotSelectedException, UnsupportedPlatformException, \
NativeResolveException, DownloadException, DependencyException, VersionManifestFetchException, \ NativeResolveException, DownloadException, DependencyException, VersionManifestFetchException, \
VersionManifestSaveException, NoSpecifiedVersionKeyException VersionManifestSaveException, NoSpecifiedVersionKeyException, AssetManifestFetchException, \
AssetManifestSaveException, AssetDataKeyNotFoundException
from core_lib.game.asset import fetch_and_save_asset_manifest, collect_assets_from_manifest, download_assets_and_copy
from core_lib.game.library import collect_native_artifacts, download_libraries, unzip_natives, collect_library_artifacts from core_lib.game.library import collect_native_artifacts, download_libraries, unzip_natives, collect_library_artifacts
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,
@@ -48,6 +51,10 @@ class TestDialog(QDialog):
self.button_4.setObjectName("ActionButton") self.button_4.setObjectName("ActionButton")
self.button_4.clicked.connect(self.load_all_version) self.button_4.clicked.connect(self.load_all_version)
self.button_5 = QPushButton("Download assets")
self.button_5.setObjectName("ActionButton")
self.button_5.clicked.connect(self.download_assets)
self.ver_dropdown = QComboBox() self.ver_dropdown = QComboBox()
self.ver_dropdown.setObjectName("Dropdown") self.ver_dropdown.setObjectName("Dropdown")
@@ -70,6 +77,7 @@ class TestDialog(QDialog):
self.right_layout.addWidget(self.button) self.right_layout.addWidget(self.button)
self.right_layout.addWidget(self.button_2) self.right_layout.addWidget(self.button_2)
self.right_layout.addWidget(self.button_3) self.right_layout.addWidget(self.button_3)
self.right_layout.addWidget(self.button_5)
self.right_layout.addStretch() self.right_layout.addStretch()
self.right_layout.addWidget(self.ver_type_dropdown) self.right_layout.addWidget(self.ver_type_dropdown)
self.right_layout.addWidget(self.ver_dropdown) self.right_layout.addWidget(self.ver_dropdown)
@@ -131,6 +139,10 @@ class TestDialog(QDialog):
) )
) )
self.widget_signal.show_and_print_error_message.connect(
self.show_and_print_error
)
self.widget_signal.show_warning_message.connect( self.widget_signal.show_warning_message.connect(
lambda msg: messagebox.warning( lambda msg: messagebox.warning(
self.parent, self.parent,
@@ -185,6 +197,7 @@ class TestDialog(QDialog):
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_error_message = Signal(str)
show_and_print_error_message = Signal(dict)
show_warning_message = Signal(str) show_warning_message = Signal(str)
show_info_message = Signal(str) show_info_message = Signal(str)
enable_widget = Signal(object) enable_widget = Signal(object)
@@ -227,6 +240,25 @@ class TestDialog(QDialog):
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
@@ -326,8 +358,12 @@ class TestDialog(QDialog):
""") """)
self.output.setPlainText(content) self.output.setPlainText(content)
except Exception as e: except Exception as e:
self.widget_signal.show_error_message.emit( self.widget_signal.show_and_print_error_message.emit(
"Failed to run test. Details: {}".format(e) {
"error": traceback.format_exception(e),
"title": "Test Error",
"message": "Failed to run test."
}
) )
finally: finally:
self.button.setDisabled(False) self.button.setDisabled(False)
@@ -362,7 +398,25 @@ class TestDialog(QDialog):
return return
thread = threading.Thread( thread = threading.Thread(
target=self._download_libraries_process, target=self._download_libraries,
daemon=True,
args=(ver,)
)
thread.start()
def download_assets(self):
self.button_5.setDisabled(True)
self.output.clear()
ver = self.get_selected_version()
if not ver:
self.widget_signal.enable_widget.emit(self.button_5)
return
thread = threading.Thread(
target=self._download_assets,
daemon=True, daemon=True,
args=(ver,) args=(ver,)
) )
@@ -444,8 +498,12 @@ class TestDialog(QDialog):
"Unable to save version manifest: {}".format(e.user_message), "Unable to save version manifest: {}".format(e.user_message),
) )
except Exception as e: except Exception as e:
self.widget_signal.show_error_message.emit( self.widget_signal.show_and_print_error_message.emit(
"An unexpected error occurred while caching version manifest: {}".format(e), {
"error": traceback.format_exception(e),
"title": "Cache Error",
"message": "An unexpected error occurred while caching version manifest."
}
) )
return True if data else False, data return True if data else False, data
@@ -462,8 +520,12 @@ class TestDialog(QDialog):
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_error_message.emit( self.widget_signal.show_and_print_error_message.emit(
"Unable to read version manifest:\n{}".format(e), {
"error": traceback.format_exception(e),
"title": "Cache Error",
"message": "Unable to read version manifest."
}
) )
return None return None
@@ -542,13 +604,17 @@ class TestDialog(QDialog):
"Unable to download client while downloading it: {}".format(e.user_message), "Unable to download client while downloading it: {}".format(e.user_message),
) )
except Exception as e: except Exception as e:
self.widget_signal.show_error_message.emit( self.widget_signal.show_and_print_error_message.emit(
"Unable to download client: {}".format(e), {
"error": traceback.format_exception(e),
"title": "Download Error",
"message": "Unable to download client, an unexpected error occurred."
}
) )
finally: finally:
self.widget_signal.enable_widget.emit(self.button_2) self.widget_signal.enable_widget.emit(self.button_2)
def _download_libraries_process(self, ver: str): def _download_libraries(self, ver: str):
output_dir = Path(self.app.work_dir, "libraries") output_dir = Path(self.app.work_dir, "libraries")
output_dir.mkdir(parents=True, exist_ok=True) output_dir.mkdir(parents=True, exist_ok=True)
@@ -610,13 +676,80 @@ class TestDialog(QDialog):
f"{e.user_message}" f"{e.user_message}"
) )
except Exception as e: except Exception as e:
self.widget_signal.show_error_message.emit( self.widget_signal.show_and_print_error_message.emit(
"Unable to collect library artifacts. An unknown error occurred:\n" {
f"{str(e) + f"\n{type(e)}"}" "error": traceback.format_exception(e),
"title": "Download Error",
"message": "Unable to collect library artifacts. An unknown error occurred."
}
) )
finally: finally:
self.widget_signal.enable_widget.emit(self.button_3) self.widget_signal.enable_widget.emit(self.button_3)
def _download_assets(self, ver: str):
try:
self.widget_signal.clean_output.emit()
ver_data = self.get_cached_version_manifest(ver)
if not ver_data:
return
_, asset_index, _, _, _, _, _ = find_useful_part_in_specific_version_manifest(ver_data)
asset_id = asset_index.get("id")
assets_dir = Path(self.app.work_dir, "assets")
index_path = Path(assets_dir, "indexes", f"{asset_id}.json")
# Download and save asset manifest
asset_manifest = fetch_and_save_asset_manifest(
ver_data,
index_path,
)
# Download assets files
assets, legacy_assets = collect_assets_from_manifest(
asset_id,
asset_manifest,
assets_dir,
)
fails = download_assets_and_copy(
assets,
legacy_assets,
progress_callback=self.download_signal.progress.emit,
)
if fails:
self.widget_signal.show_error_message.emit(
"Some assets failed to download:\n"
+ "\n".join(file.posix_path for file in fails)
)
else:
self.download_signal.log.emit("All necessary assets were downloaded.")
except AssetManifestFetchException as e:
self.widget_signal.show_error_message.emit(
f"Unable to fetch asset manifest.\n{e.user_message}"
)
except AssetManifestSaveException as e:
self.widget_signal.show_error_message.emit(
f"Unable to save asset manifest.\n{e.user_message}"
)
except AssetDataKeyNotFoundException as e:
self.widget_signal.show_error_message.emit(
f"Unable to collect assets.\n{e.user_message}"
)
except Exception as e:
self.widget_signal.show_and_print_error_message.emit(
{
"error": traceback.format_exception(e),
"title": "Download Error",
"message": "Unable to download assets. An unknown error occurred."
}
)
finally:
self.widget_signal.enable_widget.emit(self.button_5)
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:
return True, [] return True, []