Now launch game process no longer blocking main thread.
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
LAUNCHER_VERSION = "0.0.4-alpha"
|
||||
LAUNCHER_NAME = "TestLauncher (Aka TapLauncher)"
|
||||
LAUNCHER_DIR_NAME = "TestLauncher"
|
||||
MAIN_REPO_URL = "https://repo.weispace.net/wei/Launcher/"
|
||||
|
||||
CLIENT_ID = "30cbb657-edab-4d06-9fb7-cf5734a40088"
|
||||
MICROSOFT_VERIFY_ENDPOINT = "https://www.microsoft.com/link"
|
||||
LAUNCHER_USER_AGENT = f"{LAUNCHER_NAME}/{LAUNCHER_VERSION} (Powered by request module)"
|
||||
|
||||
@@ -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:
|
||||
if not check_natives_exists(manifest, libraries_dir, natives_dir) or not check_libraries_exists(manifest, libraries_dir):
|
||||
return False
|
||||
natives_exist = check_natives_exists(
|
||||
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):
|
||||
|
||||
@@ -18,12 +18,23 @@ import argparse
|
||||
import logging
|
||||
|
||||
from PySide6.QtWidgets import QApplication, QMainWindow
|
||||
from platformdirs import (
|
||||
user_data_dir,
|
||||
)
|
||||
|
||||
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.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():
|
||||
root_logger = logging.getLogger()
|
||||
logger = logging.getLogger("Launcher.Main")
|
||||
@@ -32,10 +43,10 @@ def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
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). "
|
||||
"TUI is not implemented yet.", required=False)
|
||||
"TUI is not implemented yet.", required=False, deprecated=True)
|
||||
parser.add_argument("-d", "--debug", action="store_true",
|
||||
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()
|
||||
for arg in unknown:
|
||||
@@ -43,21 +54,31 @@ def main():
|
||||
|
||||
debug = args.debug
|
||||
|
||||
# check workdir
|
||||
if not os.path.exists(args.work_dir) or not os.path.isdir(args.work_dir):
|
||||
app = QApplication(sys.argv)
|
||||
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()
|
||||
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
|
||||
logger.error("Work directory '%s' does not exist." % work_dir)
|
||||
show_error(f"Specified work directory \"{work_dir}\" does not exist or is not a directory.")
|
||||
sys.exit(-1)
|
||||
|
||||
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
|
||||
level = logging.DEBUG if debug else logging.INFO
|
||||
|
||||
+321
-29
@@ -1,17 +1,20 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
import types
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
from typing import Literal
|
||||
from uuid import uuid4
|
||||
from PySide6.QtCore import QObject
|
||||
from PySide6.QtWidgets import QInputDialog
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
|
||||
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, \
|
||||
NoSpecifiedVersionKeyException
|
||||
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 manager.account import AccountManager, LaunchAccount
|
||||
from manager.profile import LaunchProfile
|
||||
from manager.widgets import SelectPathRequest, AskForRequest
|
||||
from manager.widgets import SelectPathRequest, AskForRequest, SignalTransferItem
|
||||
|
||||
MAX_DOWNLOAD_ATTEMPTS = 3
|
||||
|
||||
class ManifestManager:
|
||||
def __init__(self, app):
|
||||
@@ -226,7 +230,7 @@ class ManifestManager:
|
||||
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
|
||||
|
||||
if version_id in visited:
|
||||
@@ -259,7 +263,7 @@ class ManifestManager:
|
||||
)
|
||||
|
||||
# Get inherits if needed
|
||||
parent = self.resolve_launch_manifest(parent_id, visited)
|
||||
parent = self.resolve_manifest(parent_id, visited)
|
||||
|
||||
if parent is None:
|
||||
return None
|
||||
@@ -396,6 +400,251 @@ class JavaManager:
|
||||
|
||||
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
|
||||
class LaunchResult:
|
||||
process: subprocess.Popen | None = None
|
||||
@@ -414,13 +663,19 @@ class LaunchResult:
|
||||
self.warnings.append(message)
|
||||
|
||||
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.manifest_mgr = manifest_manager
|
||||
self.widget_mgr = app.widget_manager
|
||||
self.java_mgr = java_manager
|
||||
self.game_file_mgr = game_file_manager
|
||||
self.account_mgr = account_manager
|
||||
self.launched_profiles: list[str] = []
|
||||
self.signal = self.LaunchSignal()
|
||||
|
||||
class LaunchSignal(QObject):
|
||||
finished = Signal(object, object) # LaunchProfile, LaunchResult
|
||||
|
||||
def launch(self, profile: LaunchProfile) -> LaunchResult:
|
||||
result = LaunchResult()
|
||||
@@ -428,8 +683,11 @@ class LaunchManager:
|
||||
version_id = profile.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
|
||||
resolved = self.manifest_mgr.resolve_launch_manifest(version_id)
|
||||
resolved = self.manifest_mgr.resolve_manifest(version_id)
|
||||
|
||||
if resolved is None:
|
||||
return result.fail(
|
||||
@@ -461,11 +719,37 @@ class LaunchManager:
|
||||
|
||||
# Check client
|
||||
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
|
||||
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()
|
||||
|
||||
@@ -479,27 +763,20 @@ class LaunchManager:
|
||||
# Asset
|
||||
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):
|
||||
callback, task_id = self.manifest_mgr.create_download_progress_callback(
|
||||
"Downloading missing assets"
|
||||
request = GameFileDownloadRequest(
|
||||
"download_assets",
|
||||
(version_id, default_assets_dir, version_manifest)
|
||||
)
|
||||
self.game_file_mgr.start_task(request)
|
||||
|
||||
try:
|
||||
download_assets(
|
||||
version_manifest,
|
||||
default_assets_dir,
|
||||
no_hash_check=True,
|
||||
progress_callback=callback,
|
||||
launcher_root=self.app.work_dir,
|
||||
request.wait(None)
|
||||
|
||||
asset_result = request.result()
|
||||
|
||||
if asset_result is not True:
|
||||
return result.fail(
|
||||
"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)
|
||||
|
||||
@@ -602,6 +879,20 @@ class LaunchManager:
|
||||
|
||||
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):
|
||||
return profile_id in self.launched_profiles
|
||||
|
||||
@@ -623,4 +914,5 @@ class GameManager(QObject):
|
||||
self.widget_mgr = app.widget_manager
|
||||
self.manifest = ManifestManager(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)
|
||||
+101
-52
@@ -1,13 +1,13 @@
|
||||
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.QtWidgets import (QWidget, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QScrollArea, QFrame,
|
||||
QSizePolicy, QApplication)
|
||||
|
||||
from manager.game import LaunchManager
|
||||
from manager.game import LaunchManager, LaunchResult
|
||||
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 pages.process_log import ProcessLogWindow
|
||||
|
||||
@@ -161,6 +161,9 @@ class ProfilePopup(QFrame):
|
||||
)
|
||||
|
||||
def load_profiles(self, profiles: list[ProfileSummary]) -> None:
|
||||
# Clean old profiles
|
||||
self.clean_profiles()
|
||||
|
||||
for profile in profiles:
|
||||
icon_path = profile.icon
|
||||
|
||||
@@ -176,6 +179,14 @@ class ProfilePopup(QFrame):
|
||||
item.set_icon(icon_path)
|
||||
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:
|
||||
self.closed.emit()
|
||||
@@ -187,7 +198,7 @@ class LaunchProfilePage(QWidget):
|
||||
super(LaunchProfilePage, self).__init__(parent)
|
||||
self.app = app
|
||||
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.launch_manager: LaunchManager = self.game_manager.launch
|
||||
self.process_log_windows: list[ProcessLogWindow] = []
|
||||
@@ -271,7 +282,15 @@ class LaunchProfilePage(QWidget):
|
||||
|
||||
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):
|
||||
def __init__(self, image_path, parent=None):
|
||||
@@ -294,6 +313,51 @@ class LaunchProfilePage(QWidget):
|
||||
y = (self.height() - scaled.height()) // 2
|
||||
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):
|
||||
profiles: list[ProfileSummary] = self.profile_manager.list_profiles()
|
||||
self.profile_popup.load_profiles(profiles)
|
||||
@@ -363,54 +427,39 @@ class LaunchProfilePage(QWidget):
|
||||
self.profile_button.set_popup_open(False)
|
||||
|
||||
def launch_profile(self) -> None:
|
||||
try:
|
||||
profile_id = self.profile_button.property("profile_id")
|
||||
self.status_label.setText("Preparing to launch...")
|
||||
launch_profile = self.app.profile_manager.get_launch_profile(profile_id)
|
||||
profile_id = self.profile_button.property("profile_id")
|
||||
|
||||
if launch_profile is None:
|
||||
self.status_label.setText("Unable to load the selected profile.")
|
||||
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...")
|
||||
launch_profile = self.app.profile_manager.get_launch_profile(profile_id)
|
||||
|
||||
if launch_profile is None:
|
||||
self.status_label.setText("Unable to load the selected profile.")
|
||||
return
|
||||
|
||||
if self.launch_manager.is_profile_running(launch_profile.profile_id):
|
||||
request = AskForRequest(
|
||||
"Launch Request",
|
||||
"Target profile {} is running. Are you sure you want to launch it again?".format(
|
||||
launch_profile.display_name
|
||||
),
|
||||
)
|
||||
self.widget_mgr.signal.askyesno.emit(request)
|
||||
|
||||
if not request.wait(60):
|
||||
self.widget_mgr.signal.show_error_message.emit(
|
||||
"Time out waiting for request for launch."
|
||||
)
|
||||
return
|
||||
|
||||
if self.launch_manager.is_profile_running(launch_profile.profile_id):
|
||||
request = AskForRequest(
|
||||
"Launch Request",
|
||||
"Target profile {} is running. Are you sure you want to launch it again?".format(
|
||||
launch_profile.display_name
|
||||
),
|
||||
)
|
||||
self.widget_mgr.signal.askyesno.emit(request)
|
||||
if not request.result() is True:
|
||||
return
|
||||
|
||||
if not request.wait(60):
|
||||
self.widget_mgr.signal.show_error_message.emit(
|
||||
"Time out waiting for request for launch."
|
||||
)
|
||||
return
|
||||
|
||||
if not request.result() is True:
|
||||
return
|
||||
|
||||
result = self.launch_manager.launch(launch_profile)
|
||||
|
||||
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")
|
||||
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!
|
||||
|
||||
Reference in New Issue
Block a user