1
0
Fork 0
mirror of synced 2024-06-02 10:44:40 +12:00

GameProcess: Make it a persistent member of RareGame

The `GameProcess` class now acts as a persistent member of `RareGame`
that can be re-used for launching games. Its signals are handled and
repeated by `RareGame`.

Implements launching directly into `RareGame`

Signed-off-by: loathingKernel <142770+loathingKernel@users.noreply.github.com>
This commit is contained in:
loathingKernel 2023-01-12 17:32:03 +02:00
parent 08c7ae89e9
commit 0d4b75d399
7 changed files with 134 additions and 100 deletions

View file

@ -57,7 +57,7 @@ def main():
launch_minimal_parser.add_argument('--wine-prefix', dest='wine_pfx', action='store', metavar='<wine pfx path>',
default=os.environ.get('LGDRY_WINE_PREFIX', None),
help='Set WINE prefix to use')
launch_minimal_parser.add_argument("--ask-alyways-sync", help="Ask for cloud saves",
launch_minimal_parser.add_argument("--ask-sync-saves", help="Ask to sync cloud saves",
action="store_true")
launch_minimal_parser.add_argument("--skip-update-check", help="Do not check for updates",
action="store_true")

View file

@ -206,7 +206,6 @@ class GamesTab(QStackedWidget):
self.icon_view.layout().addWidget(icon_widget)
self.list_view.layout().addWidget(list_widget)
rgame.set_pixmap()
self.game_utils.check_running(rgame)
self.filter_games(self.active_filter)
def add_library_widget(self, rgame: RareGame):

View file

@ -252,8 +252,8 @@ class GameWidget(LibraryWidget):
if self.rgame.game.supports_cloud_saves and not offline:
self.syncing_cloud_saves = True
self.game_utils.prepare_launch(
self.rgame, offline, skip_version_check
self.rgame.launch(
offline=offline, skip_update_check=skip_version_check
)
def sync_finished(self, app_name):

View file

@ -6,14 +6,17 @@ from enum import IntEnum
from logging import getLogger
from typing import List, Optional, Dict
from PyQt5.QtCore import QObject, pyqtSignal, QRunnable
from PyQt5.QtCore import QObject, pyqtSignal, QRunnable, pyqtSlot, QProcess
from PyQt5.QtGui import QPixmap
from legendary.models.game import Game, InstalledGame, SaveGameFile
from rare.lgndr.core import LegendaryCore
from rare.models.install import InstallOptionsModel
from rare.shared.image_manager import ImageManager
from rare.shared.game_process import GameProcess
from rare.utils.paths import data_dir
from rare.utils.misc import get_rare_executable
logger = getLogger("RareGame")
@ -92,7 +95,7 @@ class RareGame(QObject):
self.pixmap: QPixmap = QPixmap()
self.metadata: RareGame.Metadata = RareGame.Metadata()
self.load_metadata()
self.__load_metadata()
self.owned_dlcs: List[RareGame] = []
self.saves: List[SaveGameFile] = []
@ -103,7 +106,29 @@ class RareGame(QObject):
self.progress: int = 0
self.active_worker: Optional[QRunnable] = None
self.game_running = False
self.state = RareGame.State.IDLE
self.game_process = GameProcess(self)
self.game_process.launched.connect(self.__game_launched)
self.game_process.finished.connect(self.__game_finished)
if self.is_installed and not self.is_dlc:
self.game_process.connect(on_startup=True)
@pyqtSlot(int)
def __game_launched(self, code: int):
self.state = RareGame.State.RUNNING
if code == GameProcess.Code.ON_STARTUP:
return
self.metadata.last_played = datetime.now()
self.__save_metadata()
self.signals.game.launched.emit()
@pyqtSlot(int)
def __game_finished(self, exit_code: int):
self.state = RareGame.State.IDLE
if exit_code == GameProcess.Code.ON_STARTUP:
return
self.signals.game.finished.emit()
__metadata_json: Optional[Dict] = None
@ -122,12 +147,12 @@ class RareGame(QObject):
RareGame.__metadata_json = metadata
return RareGame.__metadata_json
def load_metadata(self):
def __load_metadata(self):
metadata = self.__load_metadata_json()
if self.app_name in metadata:
self.metadata = RareGame.Metadata.from_dict(metadata[self.app_name])
def save_metadata(self):
def __save_metadata(self):
metadata = self.__load_metadata_json()
metadata[self.app_name] = self.metadata.as_dict()
with open(os.path.join(data_dir(), "game_meta.json"), "w") as metadata_json:
@ -367,6 +392,15 @@ class RareGame(QObject):
@property
def is_origin(self) -> bool:
"""!
@brief Property to report if a Game is an Origin game
Legendary and by extenstion Rare can't launch Origin games directly,
it just launches the Origin client and thus requires a bit of a special
handling to let the user know.
@return bool If the game is an Origin game
"""
return self.game.metadata.get("customAttributes", {}).get("ThirdPartyManagedApp", {}).get("value") == "Origin"
@property
@ -374,12 +408,10 @@ class RareGame(QObject):
if self.is_installed:
if self.is_non_asset:
return True
elif self.game_running or self.needs_verification:
if self.state == RareGame.State.RUNNING or self.needs_verification:
return False
else:
return True
else:
return False
return True
return False
def set_pixmap(self):
self.pixmap = self.image_manager.get_pixmap(self.app_name, self.is_installed)
@ -412,4 +444,33 @@ class RareGame(QObject):
InstallOptionsModel(
app_name=self.app_name, repair_mode=True, repair_and_update=repair_and_update, update=True
)
)
)
def launch(
self,
offline: bool = False,
skip_update_check: bool = False,
wine_bin: str = None,
wine_pfx: str = None,
ask_sync_saves: bool = False,
):
executable = get_rare_executable()
executable, args = executable[0], executable[1:]
args.extend([
"start", self.app_name
])
if offline:
args.append("--offline")
if skip_update_check:
args.append("--skip-update-check")
if wine_bin:
args.extend(["--wine-bin", wine_bin])
if wine_pfx:
args.extend(["--wine-prefix", wine_pfx])
if ask_sync_saves:
args.extend("--ask-sync-saves")
# kill me, if I don't change it before commit
QProcess.startDetached(executable, args)
logger.info(f"Start new Process: ({executable} {' '.join(args)})")
self.game_process.connect(on_startup=False)

View file

@ -1,62 +1,82 @@
import datetime
import json
import logging
from enum import IntEnum
from typing import TYPE_CHECKING
from PyQt5.QtCore import QObject, pyqtSignal, QTimer, pyqtSlot
from PyQt5.QtNetwork import QLocalSocket
from PyQt5.QtWidgets import QMessageBox
from rare.models.game import RareGame
from rare.models.launcher import ErrorModel, Actions, FinishedModel, StateChangedModel
logger = logging.getLogger("GameProcess")
if TYPE_CHECKING:
from rare.models.game import RareGame
class GameProcess(QObject):
# str: app_name, int: exit_code
game_finished = pyqtSignal(RareGame, int)
# str: app_name
game_launched = pyqtSignal(RareGame)
# int: code
launched = pyqtSignal(int)
# int: code
finished = pyqtSignal(int)
def __init__(self, rgame: RareGame, on_startup=False, always_ask_sync: bool = False):
class Code(IntEnum):
SUCCESS = 0
TIMEOUT = -1
ON_STARTUP = -1234
def __init__(self, rgame: 'RareGame'):
super(GameProcess, self).__init__()
self.rgame = rgame
self.on_startup = on_startup
self.on_startup = False
self.tried_connections = 0
self.timer = QTimer()
self.timer.timeout.connect(self.__connect)
self.socket = QLocalSocket()
self.socket_name = f"rare_{self.rgame.app_name}"
self.socket.connected.connect(self._socket_connected)
# if hasattr(self.socket, "errorOccured"):
# self.socket.errorOccurred.connect(self._error_occurred)
# else:
# QTimer.singleShot(
# 100,
# lambda: self._error_occurred(QLocalSocket.UnknownSocketError) if self.socket.error() else None
# )
# logger.warning("Do not handle errors on QLocalSocket, because of an old qt version")
try:
self.socket.errorOccurred.connect(self._error_occurred)
except AttributeError:
QTimer.singleShot(100, lambda: self._error_occurred(QLocalSocket.UnknownSocketError) if self.socket.error() else None)
# QTimer.singleShot(
# 100,
# lambda: self._error_occurred(QLocalSocket.UnknownSocketError) if self.socket.error() else None
# )
logger.warning("Do not handle errors on QLocalSocket, because of an old qt version")
self.socket.readyRead.connect(self._message_available)
self.always_ask_sync = always_ask_sync
self.socket.disconnected.connect(self.__close)
def close_socket():
try:
self.socket.close()
except RuntimeError:
pass
def connect(self, on_startup: bool):
self.on_startup = on_startup
self.timer.start(200)
self.socket.disconnected.connect(close_socket)
self.timer = QTimer()
if not on_startup:
# wait a short time for process started
self.timer.timeout.connect(self.connect_to_server)
self.timer.start(200)
else:
# nothing happens, if no server available
self.connect_to_server()
@pyqtSlot()
def __close(self):
try:
self.socket.close()
except RuntimeError:
pass
def connect_to_server(self):
self.socket.connectToServer(f"rare_{self.rgame.app_name}")
@pyqtSlot()
def __connect(self):
self.socket.connectToServer(self.socket_name)
self.tried_connections += 1
if self.tried_connections > 50: # 10 seconds
QMessageBox.warning(None, "Error", self.tr("Connection to game process failed (Timeout)"))
self.timer.stop()
self.game_finished.emit(self.rgame, 1)
self.finished.emit(GameProcess.Code.TIMEOUT)
@pyqtSlot()
def _message_available(self):
@ -71,9 +91,9 @@ class GameProcess(QObject):
logger.error("Could not load json data")
return
action = data.get("action", -1)
action = data.get("action", False)
if action == -1:
if not action:
logger.error("Got unexpected action")
elif action == Actions.finished:
logger.info(f"{self.rgame.app_name} {self.rgame.app_title} finished")
@ -86,35 +106,29 @@ class GameProcess(QObject):
self.socket.close()
self._game_finished(1)
QMessageBox.warning(None, "Error", self.tr(
"Error in game {}: \n{}").format(self.rgame.app_title, model.error_string))
"Error in game {}:\n{}").format(self.rgame.app_title, model.error_string))
elif action == Actions.state_update:
model = StateChangedModel.from_json(data)
if model.new_state == StateChangedModel.States.started:
logger.info("Launched Game")
self.game_launched.emit(self.rgame)
meta_data = self.rgame.metadata
meta_data.last_played = datetime.datetime.now()
self.rgame.save_metadata()
self.launched.emit(GameProcess.Code.SUCCESS)
@pyqtSlot()
def _socket_connected(self):
self.timer.stop()
self.timer.deleteLater()
logger.info(f"Connection established for {self.rgame.app_name} ({self.rgame.app_title})")
if self.on_startup:
logger.info(f"Found {self.rgame.app_name} ({self.rgame.app_title}) running at startup")
# FIXME run this after startup, widgets do not exist at this time
QTimer.singleShot(1000, lambda: self.game_launched.emit(self.rgame))
self.launched.emit(GameProcess.Code.ON_STARTUP)
@pyqtSlot(QLocalSocket.LocalSocketError)
def _error_occurred(self, _: QLocalSocket.LocalSocketError):
if self.on_startup:
self.timer.stop()
self.socket.close()
self._game_finished(-1234) # 1234 is exit code for startup
self._game_finished(GameProcess.Code.ON_STARTUP) # 1234 is exit code for startup
logger.error(f"{self.rgame.app_name} ({self.rgame.app_title}): {self.socket.errorString()}")
def _game_finished(self, exit_code: int):
self.deleteLater()
self.game_finished.emit(self.rgame, exit_code)
self.finished.emit(exit_code)

View file

@ -2,7 +2,7 @@ import os
import platform
from logging import getLogger
from PyQt5.QtCore import QObject, QProcess, pyqtSignal, QUrl, pyqtSlot
from PyQt5.QtCore import QObject, pyqtSignal, QUrl, pyqtSlot
from PyQt5.QtCore import QStandardPaths
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import QMessageBox, QPushButton
@ -14,8 +14,7 @@ from rare.lgndr.glue.arguments import LgndrUninstallGameArgs
from rare.lgndr.glue.monkeys import LgndrIndirectStatus
from rare.models.game import RareGame
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
from rare.shared.game_process import GameProcess
from rare.utils import config_helper, misc
from rare.utils import config_helper
from .cloud_save_utils import CloudSaveUtils
logger = getLogger("GameUtils")
@ -83,13 +82,6 @@ class GameUtils(QObject):
self.cloud_save_utils = CloudSaveUtils()
self.cloud_save_utils.sync_finished.connect(self.sync_finished)
def check_running(self, rgame: RareGame):
if rgame.is_installed:
game_process = GameProcess(rgame, on_startup=True)
game_process.game_finished.connect(self.game_finished)
game_process.game_launched.connect(self.game_launched.emit)
self.running_games[rgame.app_name] = game_process
def uninstall_game(self, rgame: RareGame) -> bool:
# returns if uninstalled
if not os.path.exists(rgame.igame.install_path):
@ -138,42 +130,9 @@ class GameUtils(QObject):
self.sync_finished(rgame)
self.launch_game(
rgame, offline, skip_update_check, ask_always_sync=dont_sync_after_finish
rgame, offline, skip_update_check, ask_sync_saves=dont_sync_after_finish
)
def launch_game(
self,
rgame: RareGame,
offline: bool = False,
skip_update_check: bool = False,
wine_bin: str = None,
wine_pfx: str = None,
ask_always_sync: bool = False,
):
executable = misc.get_rare_executable()
executable, args = executable[0], executable[1:]
args.extend([
"start", rgame.app_name
])
if offline:
args.append("--offline")
if skip_update_check:
args.append("--skip-update-check")
if wine_bin:
args.extend(["--wine-bin", wine_bin])
if wine_pfx:
args.extend(["--wine-prefix", wine_pfx])
if ask_always_sync:
args.extend("--ask-always-sync")
# kill me, if I don't change it before commit
QProcess.startDetached(executable, args)
logger.info(f"Start new Process: ({executable} {' '.join(args)})")
game_process = GameProcess(rgame, ask_always_sync)
game_process.game_finished.connect(self.game_finished)
game_process.game_launched.connect(self.game_launched.emit)
self.running_games[rgame.app_name] = game_process
@pyqtSlot(RareGame, int)
def game_finished(self, rgame: RareGame, exit_code):
if self.running_games.get(rgame.app_name):

View file

@ -142,6 +142,7 @@ class RareCore(QObject):
def add_game(self, rgame: RareGame) -> None:
rgame.signals.game.install.connect(self._signals.game.install)
rgame.signals.game.finished.connect(self._signals.application.update_tray)
rgame.signals.game.finished.connect(lambda: self._signals.discord_rpc.set_title.emit(""))
self.__games[rgame.app_name] = rgame
def __filter_games(self, condition: Callable[[RareGame], bool]) -> Iterator[RareGame]: