Now vanilla (or with Fabric loader) profile creation is implemented.

This commit is contained in:
wei
2026-07-31 22:14:34 +08:00
parent 8d865c92a9
commit b5df1ec1ca
7 changed files with 668 additions and 127 deletions
+2
View File
@@ -1 +1,3 @@
# TestLauncher # TestLauncher
# There's no stable branch because this project is still under development.
+13 -4
View File
@@ -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_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 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]: def fetch_fabric_version_list(version_manifest_url: str=FABRIC_META_VERSION_ENDPOINT_V2, timeout: int=5) -> list[dict]:
try: try:
r = requests.get(version_manifest_url, timeout=timeout) 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]: -> list[dict]:
try: try:
r = requests.get(loader_version_url.format(target_version), timeout=timeout) r = requests.get(loader_version_url.format(target_version), timeout=timeout)
r.raise_for_status()
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
raise VersionManifestFetchException( raise VersionManifestFetchException(
"Timeout while fetch from {}".format(loader_version_url) "Timeout while fetch from {}".format(loader_version_url)
@@ -75,6 +79,12 @@ def get_version_support_loader_list(target_version: str, loader_version_url: str
"Connection error while fetch from {}: {}".format(loader_version_url, e) "Connection error while fetch from {}: {}".format(loader_version_url, e)
) )
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
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( raise VersionManifestFetchException(
"HTTP error while fetch from {}: HTTP Status: {}".format(loader_version_url, e.response.status_code) "HTTP error while fetch from {}: HTTP Status: {}".format(loader_version_url, e.response.status_code)
) )
@@ -102,7 +112,8 @@ def get_version_support_loader_list(target_version: str, loader_version_url: str
if not loaders: if not loaders:
raise VersionManifestException( 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 return loaders
@@ -121,6 +132,7 @@ def get_loader_manifest_from_loader_data(target_version: str, loader_data: dict,
try: try:
r = requests.get(url, timeout=5) r = requests.get(url, timeout=5)
r.raise_for_status()
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
raise VersionManifestFetchException( raise VersionManifestFetchException(
"Timeout while fetch from {}".format(url) "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) "Unknown error while fetch from {}: {}".format(url, e)
) )
+43 -33
View File
@@ -400,6 +400,48 @@ def get_core_file_download_url_and_hash(version_manifest: dict, is_server: bool=
return download_url, sha1 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: 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 data = root_manifest
sha1 = None sha1 = None
file = FileObject( return save_version_manifest(data, dest, sha1)
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)
)
def download_core_file(manifest: dict, destination: Path, is_server=False, raise_for_null_hash=False, def download_core_file(manifest: dict, destination: Path, is_server=False, raise_for_null_hash=False,
progress_callback: Callable[[str, float], None]=None) -> FileObject: progress_callback: Callable[[str, float], None]=None) -> FileObject:
+1 -2
View File
@@ -136,13 +136,12 @@ def find_java_installations(extra_install_patterns: list | None=None) -> tuple[l
try: try:
ver, raw = get_java_version(converted_path) ver, raw = get_java_version(converted_path)
except JavaRuntimeParseException: except JavaRuntimeParseException:
logger.debug(f"Unable to parse java version: {converted_path}") logger.debug(f"Unable to parse java version: {converted_path}")
except JavaRuntimeException: except JavaRuntimeException:
logger.debug(f"Unable to run java executable: {converted_path}") logger.debug(f"Unable to run java executable: {converted_path}")
except Exception as e: except Exception as e:
logger.exception(f"Unhandled exception: {e} (For path: {converted_path})") logger.exception(f"Unhandled exception: {e} (For path: {converted_path})")
finally:
if not ver: if not ver:
errors.append(converted_path) errors.append(converted_path)
continue continue
+71 -2
View File
@@ -1,6 +1,7 @@
import json import json
import time import time
import traceback import traceback
from collections.abc import Callable
from pathlib import Path from pathlib import Path
from uuid import uuid4 from uuid import uuid4
from PySide6.QtCore import QObject 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.common import FileObject
from core_lib.common.exception import VersionManifestFetchException, VersionManifestSaveException, \ from core_lib.common.exception import VersionManifestFetchException, VersionManifestSaveException, \
NoSpecifiedVersionKeyException 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: class ManifestManager:
@@ -59,6 +61,52 @@ class ManifestManager:
return True if data else False, data 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: 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():
@@ -80,7 +128,8 @@ class ManifestManager:
) )
return None 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) path = self.switch_between_specific_path(specified_ver)
file = FileObject(path) file = FileObject(path)
sha1 = None sha1 = None
@@ -122,6 +171,26 @@ class ManifestManager:
return data 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): def create_download_progress_callback(self, progress_title: str):
task_id = uuid4().hex task_id = uuid4().hex
signals = self.widget_mgr.download_signal signals = self.widget_mgr.download_signal
+528 -78
View File
@@ -4,23 +4,16 @@ from pathlib import Path
from PySide6.QtCore import QSize, Qt, QObject, Signal, Slot from PySide6.QtCore import QSize, Qt, QObject, Signal, Slot
from PySide6.QtWidgets import QWidget, QVBoxLayout, QComboBox, QListWidget, QPushButton, QLabel, QHBoxLayout, \ 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 core_lib.game.version_info import get_all_versions, get_available_version_types
from manager.game import ManifestManager, GameManager from manager.game import ManifestManager, GameManager
from manager.profile import CreateProfileRequest
from manager.widgets import SignalTransferItem, WidgetManager from manager.widgets import SignalTransferItem, WidgetManager
LOADER_LIST = {
"fabric": {
"type": "modded",
"loadVersionMethod": None,
},
"Don't Install": {
"type": "vanilla",
"loadVersionMethod": "official",
}
}
class CreateProfilePage(QWidget): class CreateProfilePage(QWidget):
def __init__(self, app, parent=None): def __init__(self, app, parent=None):
super(CreateProfilePage, self).__init__(parent) super(CreateProfilePage, self).__init__(parent)
@@ -73,8 +66,8 @@ class CreateProfilePage(QWidget):
self.version_list.setObjectName("ListWidget") self.version_list.setObjectName("ListWidget")
# Button # Button
self.create_profile = QPushButton("Create Profile") self.create_profile_button = QPushButton("Create Profile")
self.create_profile.setObjectName("Button") self.create_profile_button.setObjectName("Button")
self.refresh_button = QPushButton("Refresh") self.refresh_button = QPushButton("Refresh")
self.refresh_button.setObjectName("Button") self.refresh_button.setObjectName("Button")
@@ -85,7 +78,7 @@ class CreateProfilePage(QWidget):
self.operate_layout.addWidget(self.version_list_type_label) self.operate_layout.addWidget(self.version_list_type_label)
self.operate_layout.addStretch() self.operate_layout.addStretch()
self.operate_layout.addWidget(self.refresh_button) 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_config_fields_layout.addWidget(
self.mod_loader_label self.mod_loader_label
@@ -143,15 +136,38 @@ class CreateProfilePage(QWidget):
# Signals # Signals
self.signals = self.PageSignal() self.signals = self.PageSignal()
self.signals.get_selected_type.connect(self.get_selected_type) self.signals.get_selected_item.connect(self.get_selected_item)
self.signals.get_selected_version.connect(self.get_selected_version) self.signals.update_list.connect(self.update_list)
self.signals.update_type_dropdown.connect(self.update_type_dropdown) self.signals.clean_list.connect(self.clean_list)
self.signals.update_version_dropdown.connect(self.update_version_dropdown)
# events # Button event
self.type_list_loading = threading.Event() self.create_profile_button.clicked.connect(self.create_profile)
self.version_list_loading = threading.Event()
# 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() self.init_all()
class PageSignal(QObject): class PageSignal(QObject):
@@ -159,19 +175,75 @@ class CreateProfilePage(QWidget):
load_version_list = Signal() load_version_list = Signal()
# widget # widget
update_list = Signal(object)
clean_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_icon_label = Signal(object)
update_mod_loader_dropdown = Signal(list)
update_mod_loader_version_list = Signal(list)
get_selected_version = Signal(object) get_selected_item = Signal(object)
get_selected_type = Signal(object)
# #
# Signal Handler # 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 @staticmethod
def clean_list(target_list: QListWidget | QComboBox): def clean_list(target_list: QListWidget | QComboBox):
if not isinstance(target_list, QListWidget | QComboBox): if not isinstance(target_list, QListWidget | QComboBox):
@@ -179,33 +251,44 @@ class CreateProfilePage(QWidget):
target_list.clear() target_list.clear()
@Slot(dict) class GetSelectedItem(SignalTransferItem):
def get_selected_type(self, item: SignalTransferItem): def __init__(self, target_list: QListWidget | QComboBox):
if not isinstance(item, SignalTransferItem): SignalTransferItem.__init__(self)
raise TypeError("Item is not a SignalTransferItem") 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: self.display_text = display_text
item.result_queue.put(None) self.raw_value = raw_value
else:
item.result_queue.put(selected_type)
item.wait_event.set() self.setText(self.display_text)
@Slot(object) @Slot(object)
def get_selected_version(self, item: SignalTransferItem): def get_selected_item(self, request: GetSelectedItem):
if not isinstance(item, SignalTransferItem): if not isinstance(request, self.GetSelectedItem):
raise TypeError("Item is not a SignalTransferItem") raise TypeError("Item is not a GetSelectedItem")
selected_item = self.version_list.currentItem() target = request.target_list
if selected_item is None: if isinstance(target, QComboBox):
item.result_queue.put(None) selected_text = target.currentText()
elif isinstance(target, QListWidget):
item = target.currentItem()
if isinstance(item, self.DisplayListItem):
selected_text = item.raw_value
else: else:
item.result_queue.put(selected_item.text()) selected_text = None
else:
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): def update_type_dropdown(self, types: list):
# Move release to top # Move release to top
@@ -218,63 +301,328 @@ class CreateProfilePage(QWidget):
if len(self.version_type_dropdown.currentText()) == 0: if len(self.version_type_dropdown.currentText()) == 0:
self.version_type_dropdown.setCurrentIndex(0) self.version_type_dropdown.setCurrentIndex(0)
def update_version_dropdown(self, versions: list):
ids = []
for version in versions: def update_version_list(self, request: UpdateListRequest):
ver_id = version.get("id", None) version_list = request.target_list
if ver_id is not None:
ids.append(ver_id)
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: if not ver_id:
self.version_list.setCurrentRow(0) 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 # Thread Starter
# #
def init_all(self): 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): def start_load_type_list(self):
thread = threading.Thread(target=self.load_type_list, args=(callback,)) thread = threading.Thread(target=self.load_type_list)
thread.start() thread.start()
def start_load_version_list(self): def start_load_version_list(self):
thread = threading.Thread(target=self.load_version_list) thread = threading.Thread(target=self.load_version_list)
thread.start() thread.start()
def start_load_mod_loader_list_and_version(self): def start_load_mod_loader_list(self):
pass 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 # Background Thread
# #
def load_type_list(self, callback: Callable[None]=None): def load_type_list(self):
# Cleanup
self.signals.clean_list.emit(self.version_type_dropdown)
manifest = self.manifest_mgr.get_cached_version_manifest() manifest = self.manifest_mgr.get_cached_version_manifest()
if not manifest: if not manifest:
return return
ver_types = get_available_version_types(manifest) 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) request = self.UpdateListRequest(
target_list=self.version_type_dropdown,
if callable(callback): items=ver_types,
callback() current_index=release_index,
clean=True,
def load_version_list(self, callback: Callable[Callable[None] | None]=None): block_signals=True,
# Cleanup complete_callback=self.start_load_version_list,
self.version_list.clear() )
self.signals.update_list.emit(request)
def load_version_list(self):
# Get selected version type # Get selected version type
request = SignalTransferItem() request = self.GetSelectedItem(
self.signals.get_selected_type.emit(request) target_list=self.version_type_dropdown
)
self.signals.get_selected_item.emit(request)
if not request.wait(10): if not request.wait(10):
self.widget_mgr.signal.show_error_message.emit( self.widget_mgr.signal.show_error_message.emit(
@@ -292,13 +640,115 @@ class CreateProfilePage(QWidget):
# Fetch (or read) manifest # Fetch (or read) manifest
manifest = self.manifest_mgr.get_cached_version_manifest() manifest = self.manifest_mgr.get_cached_version_manifest()
if not manifest:
return
versions = get_all_versions( versions = get_all_versions(
manifest, manifest,
specified_type=selected_type specified_type=selected_type
) )
# Update list # 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): def load_vanilla_version_list(self):
callback(None) """
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."
)
+3 -1
View File
@@ -1,8 +1,10 @@
# Copyright announce for *Launcher* icon resources # 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: ## 2. Below icons are from other source that may contain copyright:
- profile_default.png : [Source](https://www.pixilart.com/art/minecraft-crafting-table-8aa9e1b9aee1a62) - 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) ## 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)