diff --git a/README.md b/README.md index 5e9ce2e..ce63a83 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# TestLauncher \ No newline at end of file +# TestLauncher + +# There's no stable branch because this project is still under development. \ No newline at end of file diff --git a/core_lib/game/mod/fabric.py b/core_lib/game/mod/fabric.py index 3260503..cbf1a17 100644 --- a/core_lib/game/mod/fabric.py +++ b/core_lib/game/mod/fabric.py @@ -13,6 +13,9 @@ FABRIC_META_LOADER_VERSION_ENDPOINT_V2 = "https://meta.fabricmc.net/v2/versions/ FABRIC_META_LOADER_MANIFEST_ENDPOINT_V2 = "https://meta.fabricmc.net/v2/versions/loader/{}/{}/profile/json" FABRIC_MAVEN_URL = "https://maven.fabricmc.net/" +def get_full_version_string(version: str, loader_version: str) -> str: + return f"fabric-loader-{loader_version}-{version}" + def fetch_fabric_version_list(version_manifest_url: str=FABRIC_META_VERSION_ENDPOINT_V2, timeout: int=5) -> list[dict]: try: r = requests.get(version_manifest_url, timeout=timeout) @@ -66,6 +69,7 @@ def get_version_support_loader_list(target_version: str, loader_version_url: str -> list[dict]: try: r = requests.get(loader_version_url.format(target_version), timeout=timeout) + r.raise_for_status() except requests.exceptions.Timeout: raise VersionManifestFetchException( "Timeout while fetch from {}".format(loader_version_url) @@ -75,9 +79,15 @@ def get_version_support_loader_list(target_version: str, loader_version_url: str "Connection error while fetch from {}: {}".format(loader_version_url, e) ) except requests.exceptions.HTTPError as e: - raise VersionManifestFetchException( - "HTTP error while fetch from {}: HTTP Status: {}".format(loader_version_url, e.response.status_code) - ) + if e.response.status_code == 404 or e.response.status_code == 400: + raise VersionManifestFetchException( + "HTTP error while fetch from {}: {}".format(loader_version_url, e), + user_message="Target version {} no fabric version found".format(target_version) + ) + else: + raise VersionManifestFetchException( + "HTTP error while fetch from {}: HTTP Status: {}".format(loader_version_url, e.response.status_code) + ) except Exception as e: raise VersionManifestFetchException( "Unknown error while fetch from {}: {}".format(loader_version_url, e) @@ -102,7 +112,8 @@ def get_version_support_loader_list(target_version: str, loader_version_url: str if not loaders: raise VersionManifestException( - "No available loader found in {}".format(loader_version_url) + "No available loader found in {}".format(loader_version_url), + user_message="Target version {} no fabric version found".format(target_version) ) return loaders @@ -121,6 +132,7 @@ def get_loader_manifest_from_loader_data(target_version: str, loader_data: dict, try: r = requests.get(url, timeout=5) + r.raise_for_status() except requests.exceptions.Timeout: raise VersionManifestFetchException( "Timeout while fetch from {}".format(url) @@ -150,6 +162,3 @@ def get_loader_manifest_from_loader_data(target_version: str, loader_data: dict, "Unknown error while fetch from {}: {}".format(url, e) ) - - - diff --git a/core_lib/game/version_info.py b/core_lib/game/version_info.py index 7694f86..5a552df 100644 --- a/core_lib/game/version_info.py +++ b/core_lib/game/version_info.py @@ -400,6 +400,48 @@ def get_core_file_download_url_and_hash(version_manifest: dict, is_server: bool= return download_url, sha1 +def save_version_manifest(manifest: dict, dest: Path, sha1: str | None=None) -> dict: + """ + Save version manifest with hash verify + :param manifest: + :param dest: + :param sha1: + :return: + data: dict + """ + file = FileObject( + path=dest, + sha1=sha1, + ) + + dest.parent.mkdir(parents=True, exist_ok=True) + + try: + method = "wb" if isinstance(manifest, bytes) else "w" + with dest.open(method) as f: + if isinstance(manifest, bytes): + f.write(manifest) + elif isinstance(manifest, dict): + json.dump(manifest, f) + + if sha1 is not None and not file.verify_sha1(sha1): + raise HashMismatchException( + "sha1", + file.sha1, + sha1, + file.posix_path + ) + + if isinstance(manifest, bytes): + # Convert the data back to dict + return json.loads(manifest.decode("utf-8")) + else: + return manifest + except Exception as e: + logger.exception(e) + raise VersionManifestSaveException( + "Error saving version manifest:\n{}".format(e) + ) def fetch_and_save_version_manifest(dest: Path, target_version: str=None, root_manifest=None) -> dict | None: """ @@ -423,39 +465,7 @@ def fetch_and_save_version_manifest(dest: Path, target_version: str=None, root_m data = root_manifest sha1 = None - file = FileObject( - path=dest, - sha1=sha1, - ) - - dest.parent.mkdir(parents=True, exist_ok=True) - - try: - method = "wb" if isinstance(data, bytes) else "w" - with dest.open(method) as f: - if isinstance(data, bytes): - f.write(data) - elif isinstance(data, dict): - json.dump(data, f) - - if sha1 is not None and not file.verify_sha1(sha1): - raise HashMismatchException( - "sha1", - file.sha1, - sha1, - file.posix_path - ) - - if isinstance(data, bytes): - # Convert the data back to dict - return json.loads(data.decode("utf-8")) - else: - return data - except Exception as e: - logger.exception(e) - raise VersionManifestSaveException( - "Error saving version manifest:\n{}".format(e) - ) + return save_version_manifest(data, dest, sha1) def download_core_file(manifest: dict, destination: Path, is_server=False, raise_for_null_hash=False, progress_callback: Callable[[str, float], None]=None) -> FileObject: diff --git a/core_lib/runtime/java.py b/core_lib/runtime/java.py index e35e5c5..ab85695 100644 --- a/core_lib/runtime/java.py +++ b/core_lib/runtime/java.py @@ -136,16 +136,15 @@ def find_java_installations(extra_install_patterns: list | None=None) -> tuple[l try: ver, raw = get_java_version(converted_path) except JavaRuntimeParseException: - logger.debug(f"Unable to parse java version: {converted_path}") except JavaRuntimeException: logger.debug(f"Unable to run java executable: {converted_path}") except Exception as e: logger.exception(f"Unhandled exception: {e} (For path: {converted_path})") - finally: - if not ver: - errors.append(converted_path) - continue + + if not ver: + errors.append(converted_path) + continue logger.debug(f"Detected java version {ver} for path: {converted_path}") diff --git a/manager/game.py b/manager/game.py index 8ab9d85..02b898f 100644 --- a/manager/game.py +++ b/manager/game.py @@ -1,6 +1,7 @@ import json import time import traceback +from collections.abc import Callable from pathlib import Path from uuid import uuid4 from PySide6.QtCore import QObject @@ -8,7 +9,8 @@ from PySide6.QtCore import QObject from core_lib.common.common import FileObject from core_lib.common.exception import VersionManifestFetchException, VersionManifestSaveException, \ NoSpecifiedVersionKeyException -from core_lib.game.version_info import fetch_and_save_version_manifest, get_specific_version_manifest_url_and_hash +from core_lib.game.version_info import fetch_and_save_version_manifest, get_specific_version_manifest_url_and_hash, \ + save_version_manifest class ManifestManager: @@ -59,6 +61,52 @@ class ManifestManager: return True if data else False, data + def _cache_custom_version_manifest(self, version_id: str, fetch_manifest_handler: Callable[[str], tuple[dict, str | None]]): + data = None + manifest_sha1 = None + path = self.get_specific_version_manifest_path(version_id) + + # Fetch data + try: + data, manifest_sha1 = fetch_manifest_handler(version_id) + except VersionManifestFetchException as e: + self.widget_mgr.signal.show_error_message.emit( + e.user_message, + ) + except VersionManifestSaveException as e: + self.widget_mgr.signal.show_error_message.emit( + e.user_message, + ) + except Exception as e: + self.widget_mgr.signal.show_and_print_error_message.emit( + { + "error": traceback.format_exception(e), + "title": "Cache Error", + "message": "An unexpected error occurred while caching version manifest." + } + ) + + # Save data + if data: + try: + save_version_manifest(data, path, sha1=manifest_sha1) + except VersionManifestSaveException as e: + self.widget_mgr.signal.show_error_message.emit( + e.user_message, + ) + return False, None + except Exception as e: + self.widget_mgr.signal.show_and_print_error_message.emit( + { + "error": traceback.format_exception(e), + "title": "Cache Error", + "message": "An unexpected error occurred while caching version manifest." + } + ) + return False, None + + return True if data else False, data + def _read_cached_version_manifest(self, specified_ver: str = None) -> dict | None: path = self.switch_between_specific_path(specified_ver) if not path.exists(): @@ -80,7 +128,8 @@ class ManifestManager: ) return None - def get_cached_version_manifest(self, specified_ver: str = None, max_age: int = 3600) -> dict | None: + def get_cached_version_manifest(self, specified_ver: str = None, + max_age: int = 3600) -> dict | None: path = self.switch_between_specific_path(specified_ver) file = FileObject(path) sha1 = None @@ -122,6 +171,26 @@ class ManifestManager: return data + def get_cached_custom_version_manifest(self, specified_ver: str, + fetch_manifest_handler: Callable[[str], tuple[dict, str | None]], + max_age=1260000) -> dict | None: + path = self.get_specific_version_manifest_path(specified_ver) + file = FileObject(path) + + if file.exists: + age = time.time() - path.stat().st_mtime + + # Check file stat + if age < max_age: + return self._read_cached_version_manifest(specified_ver) + + result, data = self._cache_custom_version_manifest(specified_ver, fetch_manifest_handler=fetch_manifest_handler) + + if not result: + return None + + return data + def create_download_progress_callback(self, progress_title: str): task_id = uuid4().hex signals = self.widget_mgr.download_signal diff --git a/pages/create_profile.py b/pages/create_profile.py index 87836f0..0f17ed0 100644 --- a/pages/create_profile.py +++ b/pages/create_profile.py @@ -4,23 +4,16 @@ from pathlib import Path from PySide6.QtCore import QSize, Qt, QObject, Signal, Slot from PySide6.QtWidgets import QWidget, QVBoxLayout, QComboBox, QListWidget, QPushButton, QLabel, QHBoxLayout, \ - QSizePolicy + QSizePolicy, QListWidgetItem, QInputDialog +from core_lib.common.exception import VersionManifestFetchException, VersionManifestException +from core_lib.game.mod import fabric +from core_lib.game.mod.mod_core import find_useful_part_in_mod_version_manifest from core_lib.game.version_info import get_all_versions, get_available_version_types from manager.game import ManifestManager, GameManager +from manager.profile import CreateProfileRequest from manager.widgets import SignalTransferItem, WidgetManager -LOADER_LIST = { - "fabric": { - "type": "modded", - "loadVersionMethod": None, - }, - "Don't Install": { - "type": "vanilla", - "loadVersionMethod": "official", - } -} - class CreateProfilePage(QWidget): def __init__(self, app, parent=None): super(CreateProfilePage, self).__init__(parent) @@ -73,8 +66,8 @@ class CreateProfilePage(QWidget): self.version_list.setObjectName("ListWidget") # Button - self.create_profile = QPushButton("Create Profile") - self.create_profile.setObjectName("Button") + self.create_profile_button = QPushButton("Create Profile") + self.create_profile_button.setObjectName("Button") self.refresh_button = QPushButton("Refresh") self.refresh_button.setObjectName("Button") @@ -85,7 +78,7 @@ class CreateProfilePage(QWidget): self.operate_layout.addWidget(self.version_list_type_label) self.operate_layout.addStretch() self.operate_layout.addWidget(self.refresh_button) - self.operate_layout.addWidget(self.create_profile) + self.operate_layout.addWidget(self.create_profile_button) self.mod_config_fields_layout.addWidget( self.mod_loader_label @@ -143,15 +136,38 @@ class CreateProfilePage(QWidget): # Signals self.signals = self.PageSignal() - self.signals.get_selected_type.connect(self.get_selected_type) - self.signals.get_selected_version.connect(self.get_selected_version) - self.signals.update_type_dropdown.connect(self.update_type_dropdown) - self.signals.update_version_dropdown.connect(self.update_version_dropdown) + self.signals.get_selected_item.connect(self.get_selected_item) + self.signals.update_list.connect(self.update_list) + self.signals.clean_list.connect(self.clean_list) - # events - self.type_list_loading = threading.Event() - self.version_list_loading = threading.Event() + # Button event + self.create_profile_button.clicked.connect(self.create_profile) + # Update version list while current type is changed + self.version_type_dropdown.currentTextChanged.connect( + self.start_load_version_list + ) + + self.version_list.currentTextChanged.connect( + self.start_load_mod_loader_version + ) + + self.mod_loader_dropdown.currentTextChanged.connect( + self.start_load_mod_loader_version + ) + + self.loader_list = { + "Don't Install": { + "type": "vanilla", + "loadVersionMethod": self.load_vanilla_version_list, + "profileCreateHandler": self.handle_vanilla_profile_create, + }, + "Fabric": { + "type": "modded", + "loadVersionMethod": getattr(self, "load_fabric_version_list", None), + "profileCreateHandler": self.handle_fabric_profile_create, + } + } self.init_all() class PageSignal(QObject): @@ -159,19 +175,75 @@ class CreateProfilePage(QWidget): load_version_list = Signal() # widget + update_list = Signal(object) clean_list = Signal(object) - update_version_dropdown = Signal(list) - update_type_dropdown = Signal(list) update_mod_loader_icon_label = Signal(object) - update_mod_loader_dropdown = Signal(list) - update_mod_loader_version_list = Signal(list) - get_selected_version = Signal(object) - get_selected_type = Signal(object) + get_selected_item = Signal(object) # # Signal Handler # + class UpdateListRequest(object): + def __init__(self, target_list: QListWidget | QComboBox, items: list[str], clean: bool=True, + current_index: int | None = None, custom_handler: Callable = None, + block_signals: bool = False, complete_callback: Callable = None): + self.target_list = target_list + self.items: list[str | dict] = items + self.clean = clean + self.current_index = current_index + self.custom_handler = custom_handler + self.block_signals = block_signals + self.complete_callback = complete_callback + + @Slot(object) + def update_list(self, request: UpdateListRequest): + target_list = request.target_list + items = request.items + + if not isinstance(target_list, QListWidget | QComboBox): + raise TypeError( + "Target list must be a QListWidget or QComboBox." + ) + + if request.block_signals: + target_list.blockSignals(True) + + # Handle update + try: + if request.clean: + target_list.clear() + + if callable(request.custom_handler): + handler = request.custom_handler + handler(request) + else: + target_list.addItems(items) + + index = request.current_index + if index is None: + return + + # Check and set current index + if 0 <= request.current_index < target_list.count(): + if isinstance(target_list, QComboBox): + target_list.setCurrentIndex(index) + elif isinstance(target_list, QListWidget): + target_list.setCurrentRow(index) + else: + self.app.logger.warning( + "Target list index out of range. Current index is {}.".format( + request.current_index + ) + ) + finally: + if request.block_signals: + target_list.blockSignals(False) + + if callable(request.complete_callback): + request.complete_callback() + + @staticmethod def clean_list(target_list: QListWidget | QComboBox): if not isinstance(target_list, QListWidget | QComboBox): @@ -179,33 +251,44 @@ class CreateProfilePage(QWidget): target_list.clear() - @Slot(dict) - def get_selected_type(self, item: SignalTransferItem): - if not isinstance(item, SignalTransferItem): - raise TypeError("Item is not a SignalTransferItem") + class GetSelectedItem(SignalTransferItem): + def __init__(self, target_list: QListWidget | QComboBox): + SignalTransferItem.__init__(self) + self.target_list = target_list - selected_type = self.version_type_dropdown.currentText() + class DisplayListItem(QListWidgetItem): + def __init__(self, parent: QListWidget=None, display_text: str=None, raw_value: str=None): + QListWidgetItem.__init__(self, parent) - if not selected_type: - item.result_queue.put(None) - else: - item.result_queue.put(selected_type) + self.display_text = display_text + self.raw_value = raw_value - item.wait_event.set() + self.setText(self.display_text) @Slot(object) - def get_selected_version(self, item: SignalTransferItem): - if not isinstance(item, SignalTransferItem): - raise TypeError("Item is not a SignalTransferItem") + def get_selected_item(self, request: GetSelectedItem): + if not isinstance(request, self.GetSelectedItem): + raise TypeError("Item is not a GetSelectedItem") - selected_item = self.version_list.currentItem() + target = request.target_list - if selected_item is None: - item.result_queue.put(None) + if isinstance(target, QComboBox): + selected_text = target.currentText() + elif isinstance(target, QListWidget): + item = target.currentItem() + if isinstance(item, self.DisplayListItem): + selected_text = item.raw_value + else: + selected_text = None else: - item.result_queue.put(selected_item.text()) + raise TypeError("Target list is not a QListWidget or QComboBox") - item.wait_event.set() + if not selected_text: + request.result_queue.put(None) + else: + request.result_queue.put(selected_text) + + request.wait_event.set() def update_type_dropdown(self, types: list): # Move release to top @@ -218,63 +301,328 @@ class CreateProfilePage(QWidget): if len(self.version_type_dropdown.currentText()) == 0: self.version_type_dropdown.setCurrentIndex(0) - def update_version_dropdown(self, versions: list): - ids = [] - for version in versions: - ver_id = version.get("id", None) - if ver_id is not None: - ids.append(ver_id) + def update_version_list(self, request: UpdateListRequest): + version_list = request.target_list - self.version_list.addItems(ids) + if not isinstance(version_list, QListWidget | QComboBox): + raise TypeError("Target list is not a QListWidget or QComboBox") - selected_item = self.version_list.currentItem() + for version in request.items: + ver_id = version.get("id", None) or version.get("version", None) + stable = version.get("stable", None) - if selected_item is None and len(ids) > 0: - self.version_list.setCurrentRow(0) + if not ver_id: + self.app.logger.warning("Provided version {} missing ID.".format(version)) + + display_text = ver_id + + if stable is not None and isinstance(stable, bool): + display_text = f"{ver_id}"+(" (stable)" if stable else "") + + item = self.DisplayListItem( + parent=request.target_list, + display_text=display_text, + raw_value=ver_id, + ) + + version_list.addItem(item) + + def update_mod_loader_list(self, loaders: dict): + for loader_name, value in loaders.items(): + self.mod_loader_version_list.addItem(loader_name) # # Thread Starter # def init_all(self): - self.start_load_type_list(callback=self.start_load_version_list) + self.start_load_type_list() + self.start_load_mod_loader_list() - def start_load_type_list(self, callback=None): - thread = threading.Thread(target=self.load_type_list, args=(callback,)) + def start_load_type_list(self): + thread = threading.Thread(target=self.load_type_list) thread.start() def start_load_version_list(self): thread = threading.Thread(target=self.load_version_list) thread.start() - def start_load_mod_loader_list_and_version(self): - pass + def start_load_mod_loader_list(self): + thread = threading.Thread(target=self.load_mod_loader_list) + thread.start() + + def start_load_mod_loader_version(self): + thread = threading.Thread(target=self.load_mod_loader_version) + thread.start() + + # + # Processor + # + def create_profile(self): + request = self.GetSelectedItem( + target_list=self.version_list, + ) + self.signals.get_selected_item.emit(request) + + if not request.wait(10): + self.widget_mgr.signal.show_error_message.emit( + "Timeout while waiting for selected version.", + ) + return + + minecraft_version = request.result() + + if not minecraft_version: + self.widget_mgr.signal.show_warning_message.emit( + "Selected a version before creating profile.", + ) + return + + loader_request = self.GetSelectedItem( + target_list=self.mod_loader_dropdown, + ) + self.signals.get_selected_item.emit(loader_request) + + if not loader_request.wait(10): + self.widget_mgr.signal.show_error_message.emit( + "Timeout while waiting for selected mod loader.", + ) + return + + loader_name = loader_request.result() + + if loader_name not in self.loader_list: + self.widget_mgr.signal.show_error_message.emit( + "Loader {} not found.".format(loader_name), + ) + return + + loader = self.loader_list[loader_name] + profile_create_handler: Callable[[str, str, str | None], None] = loader.get("profileCreateHandler", None) + + if profile_create_handler is None: + self.widget_mgr.signal.show_warning_message.emit( + "Profile creation for loader {} is not implemented.".format(loader_name), + ) + return + + loader_version_request = self.GetSelectedItem( + target_list=self.mod_loader_version_list, + ) + self.signals.get_selected_item.emit(loader_version_request) + + if not loader_version_request.wait(10): + self.widget_mgr.signal.show_error_message.emit( + "Timeout while waiting for selected mod loader version.", + ) + return + + loader_version = loader_version_request.result() + + if not loader_version and loader_version is not None: + loader_version = None + + profile_name, accepted = QInputDialog.getText( + self, + "Create Profile", + "Profile name:", + text=f"Minecraft {minecraft_version}", + ) + + if not accepted: + return + + profile_name = profile_name.strip() + if not profile_name: + self.widget_mgr.signal.show_warning_message.emit( + "Profile name cannot be empty.", + ) + return + + # Call handler + if callable(profile_create_handler): + profile_create_handler(profile_name, minecraft_version, loader_version) + + def handle_vanilla_profile_create(self, profile_name: str, version: str, loader_version: None=None, switch_to_current=False): + """ + Handle vanilla profile creation. + :param profile_name: + :param version: + :param loader_version: + :param switch_to_current: Switch new profile to current profile. + :return: + """ + manifest = self.manifest_mgr.get_cached_version_manifest(version) + + if not manifest: + self.widget_mgr.signal.show_warning_message.emit( + "Unable to create profile due to manifest fetch failure.", + ) + return + + version_id: str = manifest.get("id") + version_type = manifest.get( + "type", + self.version_type_dropdown.currentText() or "custom", + ) + + if version_id is None: + self.widget_mgr.signal.show_error_message.emit( + "Unable to create profile due to version ID is missing in manifest.", + ) + return + + request = CreateProfileRequest( + name=profile_name, + version_id=version_id, + version_type=version_type, + ) + + # Switch to current profile and save + profile_id = self.app.profile_manager.create_profile(request) + if profile_id is None: + return + + if switch_to_current: + if not self.app.profile_manager.set_current_profile(profile_id): + return + + if not self.app.profile_manager.save(): + return + + self.widget_mgr.signal.show_info_message.emit( + f"Profile {profile_id} successfully created.", + ) + + def handle_fabric_profile_create(self, profile_name: str, minecraft_version: str, loader_version: str | None, switch_to_current=False): + """ + Handle fabric profile creation. + :param profile_name: + :param minecraft_version: + :param loader_version: + :param switch_to_current: + :return: + """ + if not loader_version: + self.widget_mgr.signal.show_warning_message.emit( + "Selected a fabric loader version before creating profile.", + ) + return + + # Create vanilla manifest + vanilla_manifest = self.manifest_mgr.get_cached_version_manifest( + minecraft_version + ) + if not vanilla_manifest: + self.widget_mgr.signal.show_error_message.emit( + "Unable to create profile due to vanilla manifest fetch failure.", + ) + return + + version_id = fabric.get_full_version_string(version=minecraft_version, loader_version=loader_version) + + def fetch_loader_manifest(_version_id: str, ) -> tuple[dict, None]: + manifest = fabric.get_loader_manifest_from_loader_data( + minecraft_version, + {"version": loader_version}, + ) + return manifest, None + + loader_manifest = self.manifest_mgr.get_cached_custom_version_manifest( + version_id, fetch_manifest_handler=fetch_loader_manifest,) + + if not loader_manifest: + self.widget_mgr.signal.show_error_message.emit( + "Unable to create profile due to mod loader manifest fetch failure.", + ) + return + + loader_info = find_useful_part_in_mod_version_manifest( + loader_manifest + ) + + # Check mod info + if loader_info["id"] != version_id: + self.app.logger.error(f"Fabric manifest ID mismatch: expected {version_id}, got {loader_info["id"]}.") + self.widget_mgr.signal.show_error_message.emit( + "Unable to create profile. Exising loader version ID mismatch.", + ) + return + + if loader_info["inheritsFrom"] != minecraft_version: + self.app.logger.error( + f"Fabric manifest inherits from " + f'{loader_info["inheritsFrom"]}, not {minecraft_version}.' + ) + self.widget_mgr.signal.show_error_message.emit( + "Unable to create profile. Inherits version is mismatched as expected.", + ) + return + + request = CreateProfileRequest( + name=profile_name, + version_id=version_id, + version_type=loader_info["type"] or vanilla_manifest["type"] or "custom", + ) + + profile_id = self.app.profile_manager.create_profile( + request + ) + + if profile_id is None: + return + + if switch_to_current: + if not self.app.profile_manager.set_current_profile(profile_id): + return + + if not self.app.profile_manager.save(): + return + + self.widget_mgr.signal.show_info_message.emit( + f"Profile {profile_id} with fabric loader successfully created.", + ) + + + def handle_forge_profile_create(self, profile_name: str, minecraft_version: str, loader_version: str | None): + if not loader_version: + self.widget_mgr.signal.show_warning_message.emit( + "Selected a forge loader version before creating profile.", + ) + + def handle_neoforge_profile_create(self, profile_name: str, minecraft_version: str, loader_version: str | None): + if not loader_version: + self.widget_mgr.signal.show_warning_message.emit( + "Selected a neoforge loader version before creating profile.", + ) # # Background Thread # - def load_type_list(self, callback: Callable[None]=None): - # Cleanup - self.signals.clean_list.emit(self.version_type_dropdown) - + def load_type_list(self): manifest = self.manifest_mgr.get_cached_version_manifest() if not manifest: return ver_types = get_available_version_types(manifest) + release_index = ver_types.index("release") if "release" in ver_types else None - self.signals.update_type_dropdown.emit(ver_types) - - if callable(callback): - callback() - - def load_version_list(self, callback: Callable[Callable[None] | None]=None): - # Cleanup - self.version_list.clear() + request = self.UpdateListRequest( + target_list=self.version_type_dropdown, + items=ver_types, + current_index=release_index, + clean=True, + block_signals=True, + complete_callback=self.start_load_version_list, + ) + self.signals.update_list.emit(request) + def load_version_list(self): # Get selected version type - request = SignalTransferItem() - self.signals.get_selected_type.emit(request) + request = self.GetSelectedItem( + target_list=self.version_type_dropdown + ) + self.signals.get_selected_item.emit(request) if not request.wait(10): self.widget_mgr.signal.show_error_message.emit( @@ -292,13 +640,115 @@ class CreateProfilePage(QWidget): # Fetch (or read) manifest manifest = self.manifest_mgr.get_cached_version_manifest() + + if not manifest: + return + versions = get_all_versions( manifest, specified_type=selected_type ) # Update list - self.update_version_dropdown(versions) + request = self.UpdateListRequest( + target_list=self.version_list, + items=versions, + clean=True, + current_index=0, + custom_handler=self.update_version_list + ) + self.signals.update_list.emit(request) - if callable(callback): - callback(None) + def load_vanilla_version_list(self): + """ + A dummy port for load_mod_loader_version + :return: + """ + self.clean_list(self.mod_loader_version_list) + + def load_fabric_version_list(self): + self.signals.clean_list.emit(self.mod_loader_version_list) + request = self.GetSelectedItem( + target_list=self.version_list + ) + self.signals.get_selected_item.emit(request) + + if not request.wait(10): + self.widget_mgr.signal.show_error_message.emit( + "Timeout while waiting for selected version.", + ) + + selected_version = request.result() + + if not selected_version: + self.widget_mgr.signal.show_warning_message.emit( + "No version selected.", + ) + + try: + loaders = fabric.get_version_support_loader_list( + selected_version + ) + except (VersionManifestFetchException, VersionManifestException) as e: + self.widget_mgr.signal.show_error_message.emit( + "Failed to fetch version manifest: {}".format(e.user_message), + ) + return + except Exception as e: + self.widget_mgr.signal.show_error_message.emit( + "An unexpected error occurred while fetching version manifest: {}".format(e), + ) + return + + request = self.UpdateListRequest( + target_list=self.mod_loader_version_list, + items=loaders, + current_index=0, + custom_handler=self.update_version_list, + block_signals=True, + ) + self.signals.update_list.emit(request) + + def load_mod_loader_list(self): + loaders = [loader for loader in self.loader_list.keys()] + + request = self.UpdateListRequest( + target_list=self.mod_loader_dropdown, + items=loaders, + current_index=0, + clean=True + ) + self.signals.update_list.emit(request) + + def load_mod_loader_version(self): + request = self.GetSelectedItem( + target_list=self.mod_loader_dropdown + ) + self.signals.get_selected_item.emit(request) + + if not request.wait(10): + self.widget_mgr.signal.show_error_message.emit( + "Timeout while waiting for selected loader.", + ) + return + + loader_name = request.result() + + if loader_name not in self.loader_list: + raise KeyError("Target loader {} not found".format(loader_name)) + + loader = self.loader_list[loader_name] + version_handler = loader.get("loadVersionMethod", None) + + if not version_handler: + self.widget_mgr.signal.show_warning_message.emit( + "Selected loader \"{}\" is not implemented".format(loader_name), + ) + return + + if callable(version_handler): + version_handler() + else: + self.app.logger.warning( + "Select mod loader \"loadVersionMethod\" is not callable." + ) \ No newline at end of file diff --git a/resources/icons/COPYRIGHT.md b/resources/icons/COPYRIGHT.md index db9365b..2425a47 100644 --- a/resources/icons/COPYRIGHT.md +++ b/resources/icons/COPYRIGHT.md @@ -1,8 +1,10 @@ # Copyright announce for *Launcher* icon resources -## 1. You can do anything you want to these icons. (Except some icons that said maybe have other peoples copyright announce) +## 1. You can do anything you want to these icons. (However, you may need other people's accept to use below that maybe have other peoples copyright announce) ## 2. Below icons are from other source that may contain copyright: - profile_default.png : [Source](https://www.pixilart.com/art/minecraft-crafting-table-8aa9e1b9aee1a62) +- modLoader/fabric.png : [Source](https://fabricmc.net/assets/logo.png) +- modLoader/neoforged.png : [Source](https://raw.githubusercontent.com/neoforged/.github/refs/heads/main/art/neoforged.ico) ## For the icon owner: If you want to take down icon that infringing upon your copyright. Contact us with this [link](https://repo.weispace.net/wei/Launcher) \ No newline at end of file