Now launch game process no longer blocking main thread.

This commit is contained in:
wei
2026-08-03 18:04:40 +08:00
parent e657a9642e
commit ed9d25023d
5 changed files with 468 additions and 97 deletions
+2
View File
@@ -1,6 +1,8 @@
LAUNCHER_VERSION = "0.0.4-alpha" LAUNCHER_VERSION = "0.0.4-alpha"
LAUNCHER_NAME = "TestLauncher (Aka TapLauncher)" LAUNCHER_NAME = "TestLauncher (Aka TapLauncher)"
LAUNCHER_DIR_NAME = "TestLauncher"
MAIN_REPO_URL = "https://repo.weispace.net/wei/Launcher/" MAIN_REPO_URL = "https://repo.weispace.net/wei/Launcher/"
CLIENT_ID = "30cbb657-edab-4d06-9fb7-cf5734a40088" CLIENT_ID = "30cbb657-edab-4d06-9fb7-cf5734a40088"
MICROSOFT_VERIFY_ENDPOINT = "https://www.microsoft.com/link" MICROSOFT_VERIFY_ENDPOINT = "https://www.microsoft.com/link"
LAUNCHER_USER_AGENT = f"{LAUNCHER_NAME}/{LAUNCHER_VERSION} (Powered by request module)" LAUNCHER_USER_AGENT = f"{LAUNCHER_NAME}/{LAUNCHER_VERSION} (Powered by request module)"
+10 -3
View File
@@ -471,10 +471,17 @@ def check_natives_exists(manifest: dict, libraries_dir: Path, natives_dir: Path)
def check_full_libraries_exists(manifest: dict, libraries_dir: Path, natives_dir: Path) -> bool: 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): natives_exist = check_natives_exists(
return False manifest,
libraries_dir,
natives_dir,
)
missing_libraries = check_libraries_exists(
manifest,
libraries_dir,
)
return True return natives_exist and not missing_libraries
def generate_classpath(manifest: dict, libraries_dir: Path, core_file_path: Path): def generate_classpath(manifest: dict, libraries_dir: Path, core_file_path: Path):
+33 -12
View File
@@ -18,12 +18,23 @@ import argparse
import logging import logging
from PySide6.QtWidgets import QApplication, QMainWindow from PySide6.QtWidgets import QApplication, QMainWindow
from platformdirs import (
user_data_dir,
)
from app import Launcher from app import Launcher
from constant import LAUNCHER_DIR_NAME
from core_lib.log.default import DEFAULT_FORMAT, DEFAULT_DATE_FORMAT, get_colorful_handler from core_lib.log.default import DEFAULT_FORMAT, DEFAULT_DATE_FORMAT, get_colorful_handler
from core_lib.qt import messagebox from core_lib.qt import messagebox
def show_error(message):
app = QApplication(sys.argv)
app.setApplicationName("Launcher Bootstrapper")
window = QMainWindow()
messagebox.error(window, "Error", message)
app.exit()
def main(): def main():
root_logger = logging.getLogger() root_logger = logging.getLogger()
logger = logging.getLogger("Launcher.Main") logger = logging.getLogger("Launcher.Main")
@@ -32,10 +43,10 @@ def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("-m", "--mode", choices=['lightweight', 'full', 'choice'], default='choice', parser.add_argument("-m", "--mode", choices=['lightweight', 'full', 'choice'], default='choice',
help="Select a mode (lightweight for TUI, full for GUI, or choice to open a mode-selection window). " help="Select a mode (lightweight for TUI, full for GUI, or choice to open a mode-selection window). "
"TUI is not implemented yet.", required=False) "TUI is not implemented yet.", required=False, deprecated=True)
parser.add_argument("-d", "--debug", action="store_true", parser.add_argument("-d", "--debug", action="store_true",
help="Enable debug mode", default=False) help="Enable debug mode", default=False)
parser.add_argument("-wd", "--work-dir", default=os.getcwd(), help="Work directory") parser.add_argument("-wd", "--work-dir", default=None, help="Work directory")
args, unknown = parser.parse_known_args() args, unknown = parser.parse_known_args()
for arg in unknown: for arg in unknown:
@@ -43,21 +54,31 @@ def main():
debug = args.debug debug = args.debug
work_dir = args.work_dir
if work_dir is None:
if debug:
# Use working directory if launcher is in debug mode
work_dir = os.getcwd()
else:
try:
# Use user application data directory
work_dir = user_data_dir(LAUNCHER_DIR_NAME, appauthor=False)
os.makedirs(work_dir, exist_ok=True)
except Exception as e:
logger.warning("Failed to get application directory: %s", e)
show_error("Unable to get application directory. Try specifying 'work_dir' argument.")
sys.exit(-1)
else:
# check workdir # check workdir
if not os.path.exists(args.work_dir) or not os.path.isdir(args.work_dir): logger.error("Work directory '%s' does not exist." % work_dir)
app = QApplication(sys.argv) show_error(f"Specified work directory \"{work_dir}\" does not exist or is not a directory.")
app.setApplicationName("Launcher Bootstrapper")
window = QMainWindow()
messagebox.error(window, "Error", f"Specified work directory \"{args.work_dir}\" does not exist or"
" is not a directory.")
logger.error("Work directory '%s' does not exist. Exiting..." % args.work_dir)
app.exit()
sys.exit(-1) sys.exit(-1)
if os.getcwd() != args.work_dir: if os.getcwd() != args.work_dir:
os.chdir(args.work_dir) os.chdir(work_dir)
logger.info("Current working directory is {}".format(args.work_dir)) logger.info("Current working directory is {}".format(work_dir))
# Init lib # Init lib
level = logging.DEBUG if debug else logging.INFO level = logging.DEBUG if debug else logging.INFO
+321 -29
View File
@@ -1,17 +1,20 @@
import json import json
import os import os
import subprocess import subprocess
import threading
import time import time
import traceback import traceback
import types
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from threading import Thread
from typing import Literal
from uuid import uuid4 from uuid import uuid4
from PySide6.QtCore import QObject from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import QInputDialog
from constant import CLIENT_ID, LAUNCHER_VERSION from constant import CLIENT_ID, LAUNCHER_VERSION
from core_lib.common.common import FileObject from core_lib.common.common import FileObject, download_multiple_files
from core_lib.common.exception import VersionManifestFetchException, VersionManifestSaveException, \ from core_lib.common.exception import VersionManifestFetchException, VersionManifestSaveException, \
NoSpecifiedVersionKeyException NoSpecifiedVersionKeyException
from core_lib.game.argument import ArgumentMappings, generate_launch_command from core_lib.game.argument import ArgumentMappings, generate_launch_command
@@ -24,8 +27,9 @@ from core_lib.game.version_info import fetch_and_save_version_manifest, get_spec
from core_lib.runtime.java import find_java_installations from core_lib.runtime.java import find_java_installations
from manager.account import AccountManager, LaunchAccount from manager.account import AccountManager, LaunchAccount
from manager.profile import LaunchProfile from manager.profile import LaunchProfile
from manager.widgets import SelectPathRequest, AskForRequest from manager.widgets import SelectPathRequest, AskForRequest, SignalTransferItem
MAX_DOWNLOAD_ATTEMPTS = 3
class ManifestManager: class ManifestManager:
def __init__(self, app): def __init__(self, app):
@@ -226,7 +230,7 @@ class ManifestManager:
version_id: str version_id: str
client_version_id: str client_version_id: str
def resolve_launch_manifest(self, version_id: str, visited: set[str] | None = None) -> ResolvedVersionManifest | None: def resolve_manifest(self, version_id: str, visited: set[str] | None = None) -> ResolvedVersionManifest | None:
visited = set() if visited is None else visited visited = set() if visited is None else visited
if version_id in visited: if version_id in visited:
@@ -259,7 +263,7 @@ class ManifestManager:
) )
# Get inherits if needed # Get inherits if needed
parent = self.resolve_launch_manifest(parent_id, visited) parent = self.resolve_manifest(parent_id, visited)
if parent is None: if parent is None:
return None return None
@@ -396,6 +400,251 @@ class JavaManager:
return None return None
class GameFileDownloadRequest(SignalTransferItem):
def __init__(self, job_name: Literal["download_client", "download_libraries", "download_assets"],
args: tuple):
SignalTransferItem.__init__(self)
self.job_name: str = job_name
self.args: tuple = args
class GameFileManager:
def __init__(self, app, manifest_manager: ManifestManager):
self.app = app
self.widget_mgr = app.widget_manager
self.manifest_mgr = manifest_manager
self.jobs = {
"download_client": self.download_client,
"download_libraries": self.download_libraries,
"download_assets": self.download_assets,
}
def get_manifest(self, target_version: str,
manifest: dict | None, failed_message: str,
vanilla: bool=False) -> dict | None:
if manifest is None:
# Version manifest
if not vanilla:
resolved = self.manifest_mgr.resolve_manifest(target_version)
else:
resolved = self.manifest_mgr.get_cached_version_manifest(target_version)
if resolved is None:
self.widget_mgr.signal.show_error_message.emit(
failed_message,
)
return None
if isinstance(resolved, self.manifest_mgr.ResolvedVersionManifest):
manifest = resolved.manifest
else:
manifest = resolved
return manifest
def download_client(self, target_version: str, core_file_path, manifest: dict | None=None) -> bool:
"""
Download client
:param target_version: THIS should be the vanilla version id!
:param core_file_path:
:param manifest:
:return:
"""
manifest = self.get_manifest(target_version, manifest,
"Unable to download client for version {}, failed to fetch version manifest".format(target_version),
vanilla=True)
if manifest is None:
return False
callback, task_id = self.manifest_mgr.create_download_progress_callback(
"Downloading dependencies..."
)
try:
download_core_file(manifest, core_file_path,
raise_for_null_hash=True,
progress_callback=callback)
except Exception as e:
self.widget_mgr.download_signal.failed.emit(task_id, str(e))
self.widget_mgr.signal.show_error_message.emit(
"Unable to download client for version {}".format(target_version)
)
return False
else:
self.widget_mgr.download_signal.finished.emit(task_id)
return True
def download_libraries(self, target_version: str, libraries_dir: Path, natives_dir: Path, manifest: dict | None=None) -> bool:
manifest = self.get_manifest(target_version, manifest,
"Unable to download dependencies for version {}, failed to fetch version manifest.".format(
target_version)
)
if manifest is None:
return False
callback, task_id = self.manifest_mgr.create_download_progress_callback(
"Downloading dependencies..."
)
try:
download_full_libraries(manifest, libraries_dir, natives_dir,
progress_callback=callback,
redownload_callback=self.redownload_files)
except Exception as e:
self.widget_mgr.download_signal.failed.emit(task_id, str(e))
self.widget_mgr.signal.show_error_message.emit(
"Unable to download dependencies for version {}".format(target_version)
)
return False
else:
self.widget_mgr.download_signal.finished.emit(task_id)
return True
def download_assets(self, target_version: str, assets_dir: Path, manifest: dict | None = None,
allow_missing: bool = True) -> bool:
"""
Download assets
:param target_version:
:param assets_dir:
:param manifest:
:param allow_missing:
:return:
"""
manifest = self.get_manifest(target_version, manifest,
"Unable to download assets for version {}, failed to fetch version manifest.".format(target_version))
if manifest is None:
return False
callback, task_id = self.manifest_mgr.create_download_progress_callback(
"Downloading assets"
)
try:
fails = download_assets(
manifest,
assets_dir,
no_hash_check=True,
progress_callback=callback,
launcher_root=self.app.work_dir,
)
result, fails = self.redownload_files(fails)
if not result:
if allow_missing:
self.widget_mgr.signal.show_warning_message.emit(
"Some assets could not be downloaded. They are not required to launch the game,"
" but missing assets may cause texture issues while you play."
" (Allow missing assets flag is enabled)"
)
else:
self.widget_mgr.signal.show_error_message.emit(
"Some assets could not be downloaded. They are not required to launch the game,"
" but missing assets may cause texture issues while you play."
" Try relaunching to download them automatically."
)
self.widget_mgr.download_signal.failed.emit(task_id, "Some assets could not be downloaded.")
return False
except Exception as e:
self.widget_mgr.download_signal.failed.emit(task_id, str(e))
self.widget_mgr.signal.show_error_message.emit(
"Unable to download assets for version {}".format(target_version)
)
return False
else:
self.widget_mgr.download_signal.finished.emit(task_id)
return True
def start_task(self, request: GameFileDownloadRequest) -> Thread:
job_name = request.job_name
args = request.args
# Check target job function is existing
if not job_name in self.jobs:
raise NotImplementedError("Job {} not implemented".format(job_name))
def worker():
try:
result = self.jobs[request.job_name](*request.args)
request.result_queue.put(result)
except Exception as exc:
self.app.logger.exception("Game file task failed")
request.result_queue.put(exc)
finally:
request.wait_event.set()
thread = Thread(target=worker, daemon=True)
thread.start()
return thread
def redownload_files(self, files: list[FileObject]) -> tuple[bool, list[FileObject]]:
if not files:
return True, []
message = (
"Some files failed to download.\n"
"Would you like to retry downloading them?\n\n"
)
request = AskForRequest("Redownload", message)
request.details = "\n".join(file.posix_path for file in files)
self.widget_mgr.signal.askyesno.emit(request)
if not request.wait(60):
self.widget_mgr.signal.show_error_message.emit(
"Timed out while waiting for confirmation. Please try again."
)
return False, files
result = request.result()
if not result:
return False, files
# copy
pending = list(files)
callback, task_id = self.manifest_mgr.create_download_progress_callback(f"Redownloading {len(files)} files.")
for retry_time in range(1, MAX_DOWNLOAD_ATTEMPTS + 1):
if not pending:
return True, []
self.widget_mgr.download_signal.log.emit(f"Attempt {retry_time}/{MAX_DOWNLOAD_ATTEMPTS}")
fails, successes = download_multiple_files(pending,
progress_callback=callback)
for file in successes:
self.widget_mgr.download_signal.log.emit(f"Downloaded {file.posix_path}.")
if fails:
pending = fails
else:
self.widget_mgr.download_signal.finished.emit(task_id)
pending = []
if not pending:
return True, []
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(
"Unable to download these files:\n{}".format(failed_list)
)
return False, pending
@dataclass @dataclass
class LaunchResult: class LaunchResult:
process: subprocess.Popen | None = None process: subprocess.Popen | None = None
@@ -414,13 +663,19 @@ class LaunchResult:
self.warnings.append(message) self.warnings.append(message)
class LaunchManager: class LaunchManager:
def __init__(self, app, manifest_manager: ManifestManager, java_manager: JavaManager, account_manager: AccountManager): def __init__(self, app, manifest_manager: ManifestManager, java_manager: JavaManager, game_file_manager: GameFileManager,
account_manager: AccountManager):
self.app = app self.app = app
self.manifest_mgr = manifest_manager self.manifest_mgr = manifest_manager
self.widget_mgr = app.widget_manager self.widget_mgr = app.widget_manager
self.java_mgr = java_manager self.java_mgr = java_manager
self.game_file_mgr = game_file_manager
self.account_mgr = account_manager self.account_mgr = account_manager
self.launched_profiles: list[str] = [] self.launched_profiles: list[str] = []
self.signal = self.LaunchSignal()
class LaunchSignal(QObject):
finished = Signal(object, object) # LaunchProfile, LaunchResult
def launch(self, profile: LaunchProfile) -> LaunchResult: def launch(self, profile: LaunchProfile) -> LaunchResult:
result = LaunchResult() result = LaunchResult()
@@ -428,8 +683,11 @@ class LaunchManager:
version_id = profile.version_id version_id = profile.version_id
version_id = self.manifest_mgr.resolve_version_alias(version_id) version_id = self.manifest_mgr.resolve_version_alias(version_id)
if version_id is None:
return result.fail("Unable to resolve the correct version id of the profile.")
# Version manifest # Version manifest
resolved = self.manifest_mgr.resolve_launch_manifest(version_id) resolved = self.manifest_mgr.resolve_manifest(version_id)
if resolved is None: if resolved is None:
return result.fail( return result.fail(
@@ -461,11 +719,37 @@ class LaunchManager:
# Check client # Check client
if not core_file_path.exists(): if not core_file_path.exists():
download_core_file(version_manifest, core_file_path, raise_for_null_hash=True) request = GameFileDownloadRequest(
"download_client",
(client_version_id, core_file_path, version_manifest,)
)
self.game_file_mgr.start_task(request)
request.wait(None)
client_result = request.result()
if client_result is not True:
return result.fail(
f"Unable to launch game. Client download failed."
)
# Libraries # Libraries
if not check_full_libraries_exists(version_manifest, libraries_dir, natives_dir): if not check_full_libraries_exists(version_manifest, libraries_dir, natives_dir):
download_full_libraries(version_manifest, libraries_dir, natives_dir) request = GameFileDownloadRequest(
"download_libraries",
(version_id, libraries_dir, natives_dir, version_manifest)
)
self.game_file_mgr.start_task(request)
request.wait(None)
lib_result = request.result()
if lib_result is not True:
return result.fail(
"Unable to launch game due to certain dependencies download failure."
)
current_account_id = self.account_mgr.get_current_account_id() current_account_id = self.account_mgr.get_current_account_id()
@@ -479,27 +763,20 @@ class LaunchManager:
# Asset # Asset
default_assets_dir = Path(self.app.work_dir, "assets") default_assets_dir = Path(self.app.work_dir, "assets")
if not check_assets_exist(version_manifest, default_assets_dir, launcher_root=self.app.work_dir): if not check_assets_exist(version_manifest, default_assets_dir, launcher_root=self.app.work_dir):
callback, task_id = self.manifest_mgr.create_download_progress_callback( request = GameFileDownloadRequest(
"Downloading missing assets" "download_assets",
(version_id, default_assets_dir, version_manifest)
) )
self.game_file_mgr.start_task(request)
try: request.wait(None)
download_assets(
version_manifest, asset_result = request.result()
default_assets_dir,
no_hash_check=True, if asset_result is not True:
progress_callback=callback, return result.fail(
launcher_root=self.app.work_dir, "Unable to launch game due to certain assets download failure."
) )
except Exception as e:
self.widget_mgr.download_signal.failed.emit(task_id, str(e))
result.add_warning(
"Some assets could not be downloaded. They are not required to launch the game,"
" but missing assets may cause texture issues while you play."
" Try relaunching to download them automatically."
)
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)
@@ -602,6 +879,20 @@ class LaunchManager:
return result return result
def start_launch_task(self, profile: LaunchProfile) -> Thread:
def worker():
result = self.launch(profile)
self.signal.finished.emit(profile, result)
return result
thread = threading.Thread(
target=worker,
name=f"launch-{profile.profile_id}",
daemon=True,
)
thread.start()
return thread
def is_profile_running(self, profile_id): def is_profile_running(self, profile_id):
return profile_id in self.launched_profiles return profile_id in self.launched_profiles
@@ -623,4 +914,5 @@ class GameManager(QObject):
self.widget_mgr = app.widget_manager self.widget_mgr = app.widget_manager
self.manifest = ManifestManager(self.app) self.manifest = ManifestManager(self.app)
self.java = JavaManager(self.app) self.java = JavaManager(self.app)
self.launch = LaunchManager(self.app, self.manifest, self.java, self.account_manager) self.game_file = GameFileManager(self.app, manifest_manager=self.manifest)
self.launch = LaunchManager(self.app, self.manifest, self.java, self.game_file, self.account_manager)
+79 -30
View File
@@ -1,13 +1,13 @@
from pathlib import Path from pathlib import Path
from PySide6.QtCore import Qt, Signal from PySide6.QtCore import Qt, Signal, Slot, QTimer
from PySide6.QtGui import QIcon, QMouseEvent, QHideEvent, QPixmap, QPainter from PySide6.QtGui import QIcon, QMouseEvent, QHideEvent, QPixmap, QPainter
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QScrollArea, QFrame, from PySide6.QtWidgets import (QWidget, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QScrollArea, QFrame,
QSizePolicy, QApplication) QSizePolicy, QApplication)
from manager.game import LaunchManager from manager.game import LaunchManager, LaunchResult
from core_lib.game.version_info import LATEST_VERSION_ALIASES from core_lib.game.version_info import LATEST_VERSION_ALIASES
from manager.profile import ProfileSummary from manager.profile import ProfileSummary, LaunchProfile, ProfileManager
from manager.widgets import AskForRequest, WidgetManager from manager.widgets import AskForRequest, WidgetManager
from pages.process_log import ProcessLogWindow from pages.process_log import ProcessLogWindow
@@ -161,6 +161,9 @@ class ProfilePopup(QFrame):
) )
def load_profiles(self, profiles: list[ProfileSummary]) -> None: def load_profiles(self, profiles: list[ProfileSummary]) -> None:
# Clean old profiles
self.clean_profiles()
for profile in profiles: for profile in profiles:
icon_path = profile.icon icon_path = profile.icon
@@ -176,6 +179,14 @@ class ProfilePopup(QFrame):
item.set_icon(icon_path) item.set_icon(icon_path)
self.content_layout.addWidget(item) self.content_layout.addWidget(item)
def clean_profiles(self) -> None:
while self.content_layout.count():
layout_item = self.content_layout.takeAt(0)
widget = layout_item.widget()
if widget is not None:
widget.hide()
widget.deleteLater()
def hideEvent(self, event: QHideEvent) -> None: def hideEvent(self, event: QHideEvent) -> None:
self.closed.emit() self.closed.emit()
@@ -187,7 +198,7 @@ class LaunchProfilePage(QWidget):
super(LaunchProfilePage, self).__init__(parent) super(LaunchProfilePage, self).__init__(parent)
self.app = app self.app = app
self.widget_mgr: WidgetManager = app.widget_manager self.widget_mgr: WidgetManager = app.widget_manager
self.profile_manager = app.profile_manager self.profile_manager: ProfileManager = app.profile_manager
self.game_manager = app.game_manager self.game_manager = app.game_manager
self.launch_manager: LaunchManager = self.game_manager.launch self.launch_manager: LaunchManager = self.game_manager.launch
self.process_log_windows: list[ProcessLogWindow] = [] self.process_log_windows: list[ProcessLogWindow] = []
@@ -271,7 +282,15 @@ class LaunchProfilePage(QWidget):
self.app.apply_qss(Path("profile.qss"), self.setStyleSheet) self.app.apply_qss(Path("profile.qss"), self.setStyleSheet)
self.load_profiles() # Handle launch() result
self.launch_manager.signal.finished.connect(
self.launch_finished,
Qt.ConnectionType.QueuedConnection,
)
self.profile_manager.profiles_changed.connect(
self.load_profiles,
)
class ProfileBackgroundFrame(QFrame): class ProfileBackgroundFrame(QFrame):
def __init__(self, image_path, parent=None): def __init__(self, image_path, parent=None):
@@ -294,6 +313,51 @@ class LaunchProfilePage(QWidget):
y = (self.height() - scaled.height()) // 2 y = (self.height() - scaled.height()) // 2
painter.drawPixmap(x, y, scaled) painter.drawPixmap(x, y, scaled)
@Slot(object, object)
def launch_finished(self, launch_profile: LaunchProfile, result: LaunchResult) -> None:
"""
Handle launch finished signal
:param launch_profile:
:param result:
:return:
"""
if not result.success:
self.status_label.setText("Launch Failed")
self.widget_mgr.signal.show_error_message.emit(
f"Unable to launch profile "
f"{launch_profile.display_name}: "
f"{result.error_message}"
)
return
# Create log view
log_window = ProcessLogWindow(
result.process,
launch_profile.profile_id,
title=(
f"Minecraft Log - {launch_profile.display_name} "
f"({launch_profile.version_id})"
),
parent=None,
finished_callback=self.launch_manager.profile_finished,
)
for warning in result.warnings:
log_window.append_message(
f"[launcher warning] {warning}"
)
self.process_log_windows.append(log_window)
log_window.show()
self.status_label.setText("Launched!")
# Cleanup for status label
timer = QTimer(self)
timer.setSingleShot(True)
timer.timeout.connect(lambda: self.status_label.setText("Select a profile to launch"))
timer.start(3000)
def load_profiles(self): def load_profiles(self):
profiles: list[ProfileSummary] = self.profile_manager.list_profiles() profiles: list[ProfileSummary] = self.profile_manager.list_profiles()
self.profile_popup.load_profiles(profiles) self.profile_popup.load_profiles(profiles)
@@ -363,8 +427,15 @@ class LaunchProfilePage(QWidget):
self.profile_button.set_popup_open(False) self.profile_button.set_popup_open(False)
def launch_profile(self) -> None: def launch_profile(self) -> None:
try:
profile_id = self.profile_button.property("profile_id") profile_id = self.profile_button.property("profile_id")
if profile_id is None:
self.widget_mgr.signal.show_warning_message.emit(
"There is no profile is selected."
" Please create one first."
)
return
self.status_label.setText("Preparing to launch...") self.status_label.setText("Preparing to launch...")
launch_profile = self.app.profile_manager.get_launch_profile(profile_id) launch_profile = self.app.profile_manager.get_launch_profile(profile_id)
@@ -390,27 +461,5 @@ class LaunchProfilePage(QWidget):
if not request.result() is True: if not request.result() is True:
return return
result = self.launch_manager.launch(launch_profile) self.launch_manager.start_launch_task(launch_profile)
# The log view window creation process has been moved to the method self.launch_finished, move to there!
if result.success:
self.status_label.setText("Launched!")
log_window = ProcessLogWindow(
result.process,
launch_profile.profile_id,
title=f"Minecraft Log - {launch_profile.display_name} ({launch_profile.version_id})",
parent=None,
finished_callback=self.launch_manager.profile_finished,
)
for warning in result.warnings:
log_window.append_message(f"[launcher warning] {warning}")
self.process_log_windows.append(log_window)
log_window.show()
else:
self.widget_mgr.signal.show_error_message.emit(
"Unable to launch profile {}: {}".format(
launch_profile.display_name,
result.error_message,
)
)
finally:
self.status_label.setText("Select a profile to launch")