From 8ac9a1c41f8eb818606a9594eb5df589fd517566 Mon Sep 17 00:00:00 2001 From: Dummerle <44114474+Dummerle@users.noreply.github.com> Date: Sun, 27 Mar 2022 21:52:32 +0200 Subject: [PATCH] Rewrite game launch code; Fix override_exe; auto renew session --- rare/__init__.py | 2 +- rare/app.py | 16 +- .../tabs/games/game_info/game_settings.py | 7 +- rare/components/tabs/games/game_utils.py | 310 +++++++++--------- 4 files changed, 179 insertions(+), 156 deletions(-) diff --git a/rare/__init__.py b/rare/__init__.py index 2e00ef69..533cac3f 100644 --- a/rare/__init__.py +++ b/rare/__init__.py @@ -1,3 +1,3 @@ -__version__ = "1.8.7" +__version__ = "1.8.8" code_name = "Stellula Kakopo" diff --git a/rare/app.py b/rare/app.py index 8fbb639f..13491017 100644 --- a/rare/app.py +++ b/rare/app.py @@ -7,8 +7,9 @@ import sys import time import traceback from argparse import Namespace +from datetime import datetime -from PyQt5.QtCore import Qt, QThreadPool, QSettings, QTranslator +from PyQt5.QtCore import Qt, QThreadPool, QSettings, QTranslator, QTimer from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QMessageBox from requests import HTTPError @@ -159,6 +160,19 @@ class App(QApplication): self.launch_dialog.login() + dt_exp = datetime.fromisoformat(self.core.lgd.userdata['expires_at'][:-1]) + dt_now = datetime.utcnow() + td = abs(dt_exp - dt_now) + self.timer = QTimer() + self.timer.timeout.connect(self.re_login) + self.timer.start(int(td.total_seconds() - 60)) + + def re_login(self): + logger.info("Session expires shortly. Renew session") + self.core.login() + self.timer.stop() + self.timer.deleteLater() + def show_mainwindow(self): if self.window_launched: self.mainwindow.show() diff --git a/rare/components/tabs/games/game_info/game_settings.py b/rare/components/tabs/games/game_info/game_settings.py index 7cdc2032..d3b0be90 100644 --- a/rare/components/tabs/games/game_info/game_settings.py +++ b/rare/components/tabs/games/game_info/game_settings.py @@ -57,16 +57,15 @@ class GameSettings(DefaultGameSettings): f"{self.game.app_name}/auto_sync_cloud", self.cloud_sync.isChecked() ) ) + self.override_exe_edit.textChanged.connect( + lambda text: self.save_line_edit("override_exe", text) + ) self.launch_params.textChanged.connect( lambda x: self.save_line_edit("start_params", x) ) self.game_settings_layout.setAlignment(Qt.AlignTop) - self.linux_settings.mangohud.set_wrapper_activated.connect( - lambda active: self.wrapper_settings.add_wrapper("mangohud") - if active else self.wrapper_settings.delete_wrapper("mangohud")) - def compute_save_path(self): if ( self.core.is_installed(self.game.app_name) diff --git a/rare/components/tabs/games/game_utils.py b/rare/components/tabs/games/game_utils.py index c77ba2f1..97deba27 100644 --- a/rare/components/tabs/games/game_utils.py +++ b/rare/components/tabs/games/game_utils.py @@ -6,14 +6,15 @@ import webbrowser from dataclasses import dataclass from logging import getLogger -from PyQt5.QtCore import QObject, QSettings, QProcess, QProcessEnvironment, pyqtSignal +from PyQt5.QtCore import QObject, QSettings, QProcess, QProcessEnvironment, pyqtSignal, QUrl +from PyQt5.QtGui import QDesktopServices from PyQt5.QtWidgets import QMessageBox, QPushButton +from legendary.models.game import LaunchParameters, InstalledGame -from legendary.models.game import LaunchParameters -from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton from rare.components.dialogs.uninstall_dialog import UninstallDialog from rare.components.extra.console import ConsoleWindow from rare.components.tabs.games import CloudSaveUtils +from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton from rare.utils import legendary_utils from rare.utils.meta import RareGameMeta @@ -168,156 +169,24 @@ class GameUtils(QObject): ) return - process = GameProcess(app_name) - process.setProcessChannelMode(GameProcess.MergedChannels) + def _launch_real(): + process = self._get_process(app_name, env) - if game.third_party_store != "Origin": - if not offline: - if not skip_update_check and not self.core.is_noupdate_game(app_name): - # check updates - try: - latest = self.core.get_asset( - app_name, igame.platform, update=False - ) - except ValueError: - self.finished.emit(app_name, self.tr("Metadata doesn't exist")) - return - else: - if latest.build_version != igame.version: - self.finished.emit(app_name, self.tr("Please update game")) - return + if game.third_party_store != "Origin": + self._launch_game(igame, process, offline, skip_update_check, ask_always_sync) + else: + self._launch_origin(igame.app_name, process) - params: LaunchParameters = self.core.get_launch_parameters( - app_name=app_name, offline=offline, wine_bin=wine_bin, wine_pfx=wine_pfx - ) - - full_params = list() - - if os.environ.get("container") == "flatpak": - full_params.extend(["flatpak-spawn", "--host"]) - - full_params.extend(params.launch_command) - full_params.append( - os.path.join(params.game_directory, params.game_executable) - ) - full_params.extend(params.game_parameters) - full_params.extend(params.egl_parameters) - full_params.extend(params.user_parameters) - - process.setWorkingDirectory(params.working_directory) - environment = QProcessEnvironment() - full_env = os.environ.copy() - full_env.update(params.environment) - for env, value in full_env.items(): - environment.insert(env, value) - - if platform.system() != "Windows": - # wine prefixes - for env in ["STEAM_COMPAT_DATA_PATH", "WINEPREFIX"]: - if val := full_env.get(env): - if not os.path.exists(val): - try: - os.makedirs(val) - except PermissionError as e: - logger.error(str(e)) - QMessageBox.warning( - None, - "Error", - self.tr( - "Error while launching {}. No permission to create {} for {}" - ).format(game.app_title, val, env), - ) - process.deleteLater() - return - # check wine executable - if shutil.which(full_params[0]) is None: - # wine binary does not exist - QMessageBox.warning( - None, - "Warning", - self.tr( - "'{}' does not exist. Please change it in Settings" - ).format(full_params[0]), - ) - process.deleteLater() - return - - if shutil.which(full_params[0]) is None: - QMessageBox.warning(None, "Warning", self.tr("'{}' does not exist").format(full_params[0])) + env = self.core.get_app_environment(app_name, wine_pfx=wine_pfx) + pre_cmd, wait = self.core.get_pre_launch_command(app_name) + if pre_cmd: + pre_cmd = pre_cmd.split() + pre_proc = self._launch_pre_command(env) + pre_proc.start(pre_cmd[0], pre_cmd[1:]) + if wait: + pre_proc.finished.connect(_launch_real) return - - process.setProcessEnvironment(environment) - process.game_finished.connect(self.game_finished) - running_game = RunningGameModel( - process=process, app_name=app_name, always_ask_sync=ask_always_sync - ) - - process.start(full_params[0], full_params[1:]) - self.game_launched.emit(app_name) - self.signals.set_discord_rpc.emit(app_name) - logger.info(f"{game.app_title} launched") - - self.running_games[game.app_name] = running_game - - else: - origin_uri = self.core.get_origin_uri(game.app_name, self.args.offline) - logger.info("Launch Origin Game: ") - if platform.system() == "Windows": - webbrowser.open(origin_uri) - self.finished.emit(app_name, "") - return - wine_pfx = self.core.lgd.config.get( - game.app_name, "wine_prefix", fallback=os.path.expanduser("~/.wine") - ) - if not wine_bin: - wine_bin = self.core.lgd.config.get( - game.app_name, "wine_executable", fallback="/usr/bin/wine" - ) - - if shutil.which(wine_bin) is None: - # wine binary does not exist - QMessageBox.warning( - None, - "Warning", - self.tr( - "Wine executable '{}' does not exist. Please change it in Settings" - ).format(wine_bin), - ) - process.deleteLater() - return - - env = self.core.get_app_environment(game.app_name, wine_pfx=wine_pfx) - - if not env.get("WINEPREFIX") and not os.path.exists("/usr/bin/wine"): - logger.error( - f"In order to launch Origin correctly you must specify the wine binary and prefix " - f"to use in the configuration file or command line. See the README for details." - ) - self.finished.emit( - app_name, - self.tr("No wine executable selected. Please set it in settings"), - ) - return - - environment = QProcessEnvironment() - for e in env: - environment.insert(e, env[e]) - process.setProcessEnvironment(environment) - process.finished.connect(lambda x: self.game_finished(x, game.app_name)) - process.start(wine_bin, [origin_uri]) - - if QSettings().value("show_console", False, bool): - self.console.show() - process.readyReadStandardOutput.connect( - lambda: self.console.log( - str(process.readAllStandardOutput().data(), "utf-8", "ignore") - ) - ) - process.readyReadStandardError.connect( - lambda: self.console.error( - str(process.readAllStandardError().data(), "utf-8", "ignore") - ) - ) + _launch_real() def game_finished(self, exit_code, app_name): logger.info(f"Game exited with exit code: {exit_code}") @@ -370,6 +239,147 @@ class GameUtils(QObject): return self.cloud_save_utils.game_finished(app_name, game.always_ask_sync) + def _launch_pre_command(self, env: dict): + proc = QProcess() + proc.setProcessChannelMode(QProcess.MergedChannels) + environment = QProcessEnvironment() + for e in env: + environment.insert(e, env[e]) + proc.setProcessEnvironment(environment) + + proc.readyReadStandardOutput.connect( + lambda: self.console.log( + str(proc.readAllStandardOutput().data(), "utf-8", "ignore") + ) + ) + proc.readyReadStandardError.connect( + lambda: self.console.error( + str(proc.readAllStandardError().data(), "utf-8", "ignore") + ) + ) + return proc + + def _get_process(self, app_name, env): + process = GameProcess(app_name) + process.setProcessChannelMode(GameProcess.MergedChannels) + + environment = QProcessEnvironment() + for e in env: + environment.insert(e, env[e]) + process.setProcessEnvironment(environment) + + process.readyReadStandardOutput.connect( + lambda: self.console.log( + str(process.readAllStandardOutput().data(), "utf-8", "ignore") + ) + ) + process.readyReadStandardError.connect( + lambda: self.console.error( + str(process.readAllStandardError().data(), "utf-8", "ignore") + ) + ) + process.finished.connect(lambda x: self.game_finished(x, app_name)) + process.stateChanged.connect( + lambda state: self.console.show() + if (state == QProcess.Running + and QSettings().value("show_console", False, bool)) + else None + ) + return process + + def _launch_origin(self, app_name, process: QProcess): + origin_uri = self.core.get_origin_uri(app_name, self.args.offline) + logger.info("Launch Origin Game: ") + if platform.system() == "Windows": + QDesktopServices.openUrl(QUrl(origin_uri)) + self.finished.emit(app_name, "") + return + + command = self.core.get_app_launch_command(app_name) + + if not os.path.exists(command[0]) and shutil.which(command[0]) is None: + # wine binary does not exist + QMessageBox.warning( + None, "Warning", + self.tr( + "'{}' does not exist. Please change it in Settings" + ).format(command[0]), + ) + process.deleteLater() + return + + process.start(command[0], command[1:]) + + def _launch_game(self, igame: InstalledGame, process: QProcess, offline: bool, + skip_update_check: bool, ask_always_sync: bool): + if not offline: # skip for update + if not skip_update_check and not self.core.is_noupdate_game(igame.app_name): + # check updates + try: + latest = self.core.get_asset( + igame.app_name, igame.platform, update=False + ) + except ValueError: + self.finished.emit(igame.app_name, self.tr("Metadata doesn't exist")) + return + else: + if latest.build_version != igame.version: + self.finished.emit(igame.app_name, self.tr("Please update game")) + return + + params: LaunchParameters = self.core.get_launch_parameters( + app_name=igame.app_name, offline=offline + ) + + full_params = list() + + if os.environ.get("container") == "flatpak": + full_params.extend(["flatpak-spawn", "--host"]) + + full_params.extend(params.launch_command) + full_params.append( + os.path.join(params.game_directory, params.game_executable) + ) + full_params.extend(params.game_parameters) + full_params.extend(params.egl_parameters) + full_params.extend(params.user_parameters) + + process.setWorkingDirectory(params.working_directory) + + if platform.system() != "Windows": + # wine prefixes + for env in ["STEAM_COMPAT_DATA_PATH", "WINEPREFIX"]: + if val := process.processEnvironment().value(env, ""): + if not os.path.exists(val): + try: + os.makedirs(val) + except PermissionError as e: + logger.error(str(e)) + QMessageBox.warning( + None, + "Error", + self.tr( + "Error while launching {}. No permission to create {} for {}" + ).format(igame.title, val, env), + ) + process.deleteLater() + return + # check wine executable + + if shutil.which(full_params[0]) is None: + QMessageBox.warning(None, "Warning", self.tr("'{}' does not exist").format(full_params[0])) + return + running_game = RunningGameModel( + process=process, app_name=igame.app_name, always_ask_sync=ask_always_sync + ) + process.start(full_params[0], full_params[1:]) + + self.game_launched.emit(igame.app_name) + self.signals.set_discord_rpc.emit(igame.app_name) + logger.info(f"{igame.title} launched") + + self.running_games[igame.app_name] = running_game + def sync_finished(self, app_name): if app_name in self.launch_queue.keys(): self.cloud_save_finished.emit(app_name)