2022-05-15 11:25:13 +12:00
|
|
|
import json
|
2021-11-02 10:53:04 +13:00
|
|
|
import os
|
|
|
|
import platform
|
2021-12-06 08:11:11 +13:00
|
|
|
import shutil
|
2021-11-02 10:53:04 +13:00
|
|
|
from dataclasses import dataclass
|
|
|
|
from logging import getLogger
|
|
|
|
|
2022-05-15 11:25:13 +12:00
|
|
|
from PyQt5.QtCore import QObject, QSettings, QProcess, QProcessEnvironment, pyqtSignal, QUrl, QTimer, pyqtSlot
|
2022-03-28 08:52:32 +13:00
|
|
|
from PyQt5.QtGui import QDesktopServices
|
2022-05-15 11:25:13 +12:00
|
|
|
from PyQt5.QtNetwork import QLocalSocket
|
2021-11-02 10:53:04 +13:00
|
|
|
from PyQt5.QtWidgets import QMessageBox, QPushButton
|
2022-03-28 08:52:32 +13:00
|
|
|
from legendary.models.game import LaunchParameters, InstalledGame
|
2021-11-02 10:53:04 +13:00
|
|
|
|
|
|
|
from rare.components.dialogs.uninstall_dialog import UninstallDialog
|
2022-05-05 08:44:53 +12:00
|
|
|
from rare.components.extra.console import Console
|
2021-11-02 10:53:04 +13:00
|
|
|
from rare.components.tabs.games import CloudSaveUtils
|
2022-03-28 08:52:32 +13:00
|
|
|
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
|
2021-11-02 10:53:04 +13:00
|
|
|
from rare.utils import legendary_utils
|
2022-05-15 11:25:13 +12:00
|
|
|
from rare.utils import utils
|
2022-01-03 10:52:43 +13:00
|
|
|
from rare.utils.meta import RareGameMeta
|
2021-11-02 10:53:04 +13:00
|
|
|
|
|
|
|
logger = getLogger("GameUtils")
|
|
|
|
|
|
|
|
|
2022-05-15 11:25:13 +12:00
|
|
|
class GameProcess(QObject):
|
|
|
|
@dataclass
|
|
|
|
class FinishedModel:
|
|
|
|
action: str
|
|
|
|
app_name: str
|
|
|
|
exit_code: int
|
|
|
|
playtime: int # seconds
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_json(cls, data):
|
|
|
|
return cls(
|
|
|
|
action=data["action"],
|
|
|
|
app_name=data["app_name"],
|
|
|
|
exit_code=data["exit_code"],
|
|
|
|
playtime=data["playtime"],
|
|
|
|
)
|
|
|
|
|
2021-11-02 10:53:04 +13:00
|
|
|
game_finished = pyqtSignal(int, str)
|
|
|
|
|
2022-05-15 11:25:13 +12:00
|
|
|
def __init__(self, app_name: str):
|
2021-11-02 10:53:04 +13:00
|
|
|
super(GameProcess, self).__init__()
|
|
|
|
self.app_name = app_name
|
2022-05-15 11:25:13 +12:00
|
|
|
self.socket = QLocalSocket()
|
|
|
|
self.socket.connected.connect(self._socket_connected)
|
|
|
|
self.socket.errorOccurred.connect(self._error_occured)
|
|
|
|
self.socket.readyRead.connect(self._message_available)
|
|
|
|
self.socket.disconnected.connect(lambda: self.socket.close())
|
2022-05-19 09:41:48 +12:00
|
|
|
self.timer = QTimer()
|
2022-05-15 11:25:13 +12:00
|
|
|
# wait a short time for process started
|
2022-05-19 09:41:48 +12:00
|
|
|
self.timer.timeout.connect(self.connect_to_server)
|
|
|
|
self.timer.start(200)
|
2022-05-15 11:25:13 +12:00
|
|
|
|
|
|
|
def connect_to_server(self):
|
2022-05-19 09:41:48 +12:00
|
|
|
self.socket.connectToServer(f"rare_{self.app_name}")
|
2022-05-15 11:25:13 +12:00
|
|
|
|
|
|
|
def _message_available(self):
|
|
|
|
message = self.socket.readAll().data()
|
|
|
|
if not message.startswith(b"{"):
|
|
|
|
logger.error(f"Received unsupported message{message.decode()}")
|
|
|
|
return
|
2022-01-07 11:46:26 +13:00
|
|
|
try:
|
2022-05-15 11:25:13 +12:00
|
|
|
data = json.loads(message)
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
logger.error("Could not load json data")
|
|
|
|
return
|
|
|
|
|
|
|
|
if data.get("action", "") == "finished":
|
|
|
|
logger.info(f"{self.app_name} finished")
|
|
|
|
resp = self.FinishedModel.from_json(data)
|
|
|
|
self._game_finished(resp.exit_code)
|
2021-11-02 10:53:04 +13:00
|
|
|
|
2022-05-15 11:25:13 +12:00
|
|
|
def _socket_connected(self):
|
2022-05-19 09:41:48 +12:00
|
|
|
self.timer.stop()
|
|
|
|
self.timer.deleteLater()
|
2022-05-15 11:25:13 +12:00
|
|
|
logger.info(f"Connection established for {self.app_name}")
|
|
|
|
|
|
|
|
def _error_occured(self, _):
|
|
|
|
logger.error(self.socket.errorString())
|
|
|
|
|
|
|
|
def _game_finished(self, exit_code):
|
|
|
|
self.game_finished.emit(exit_code, self.app_name)
|
2021-11-02 10:53:04 +13:00
|
|
|
|
2022-04-05 09:12:21 +12:00
|
|
|
|
2021-11-02 10:53:04 +13:00
|
|
|
@dataclass
|
|
|
|
class RunningGameModel:
|
|
|
|
process: GameProcess
|
|
|
|
app_name: str
|
2021-11-17 10:54:23 +13:00
|
|
|
always_ask_sync: bool = False
|
2021-11-02 10:53:04 +13:00
|
|
|
|
|
|
|
|
|
|
|
class GameUtils(QObject):
|
|
|
|
running_games = dict()
|
2021-11-17 10:54:23 +13:00
|
|
|
finished = pyqtSignal(str, str) # app_name, error
|
2021-11-02 10:53:04 +13:00
|
|
|
cloud_save_finished = pyqtSignal(str)
|
|
|
|
launch_queue = dict()
|
2021-11-14 11:56:07 +13:00
|
|
|
game_launched = pyqtSignal(str)
|
2021-11-17 10:54:23 +13:00
|
|
|
update_list = pyqtSignal(str)
|
2021-11-02 10:53:04 +13:00
|
|
|
|
|
|
|
def __init__(self, parent=None):
|
|
|
|
super(GameUtils, self).__init__(parent=parent)
|
2022-02-26 06:43:27 +13:00
|
|
|
self.core = LegendaryCoreSingleton()
|
|
|
|
self.signals = GlobalSignalsSingleton()
|
|
|
|
self.args = ArgumentsSingleton()
|
2021-11-02 10:53:04 +13:00
|
|
|
|
2022-05-05 08:44:53 +12:00
|
|
|
self.console = Console()
|
2021-11-02 10:53:04 +13:00
|
|
|
self.cloud_save_utils = CloudSaveUtils()
|
|
|
|
self.cloud_save_utils.sync_finished.connect(self.sync_finished)
|
2022-01-03 10:52:43 +13:00
|
|
|
self.game_meta = RareGameMeta()
|
2021-11-02 10:53:04 +13:00
|
|
|
|
|
|
|
def uninstall_game(self, app_name) -> bool:
|
2021-11-17 10:54:23 +13:00
|
|
|
# returns if uninstalled
|
2021-11-02 10:53:04 +13:00
|
|
|
game = self.core.get_game(app_name)
|
2021-11-17 10:54:23 +13:00
|
|
|
igame = self.core.get_installed_game(app_name)
|
|
|
|
if not os.path.exists(igame.install_path):
|
2021-12-24 22:09:50 +13:00
|
|
|
if QMessageBox.Yes == QMessageBox.question(
|
2022-01-03 10:52:43 +13:00
|
|
|
None,
|
|
|
|
"Uninstall",
|
|
|
|
self.tr(
|
|
|
|
"Game files of {} do not exist. Remove it from installed games?"
|
|
|
|
).format(igame.title),
|
|
|
|
QMessageBox.Yes | QMessageBox.No,
|
|
|
|
QMessageBox.Yes,
|
2021-12-24 22:09:50 +13:00
|
|
|
):
|
2021-11-17 10:54:23 +13:00
|
|
|
self.core.lgd.remove_installed_game(app_name)
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
2021-11-02 10:53:04 +13:00
|
|
|
infos = UninstallDialog(game).get_information()
|
|
|
|
if infos == 0:
|
|
|
|
return False
|
|
|
|
legendary_utils.uninstall(game.app_name, self.core, infos)
|
2022-02-26 06:43:27 +13:00
|
|
|
self.signals.game_uninstalled.emit(app_name)
|
2021-11-02 10:53:04 +13:00
|
|
|
return True
|
|
|
|
|
2021-12-24 22:09:50 +13:00
|
|
|
def prepare_launch(
|
2022-01-03 10:52:43 +13:00
|
|
|
self, app_name, offline: bool = False, skip_update_check: bool = False
|
2021-12-24 22:09:50 +13:00
|
|
|
):
|
2021-11-02 10:53:04 +13:00
|
|
|
game = self.core.get_game(app_name)
|
2021-11-17 10:54:23 +13:00
|
|
|
dont_sync_after_finish = False
|
|
|
|
|
2022-04-26 09:30:52 +12:00
|
|
|
if game.supports_cloud_saves and not offline:
|
2021-11-17 10:54:23 +13:00
|
|
|
try:
|
|
|
|
sync = self.cloud_save_utils.sync_before_launch_game(app_name)
|
|
|
|
except ValueError:
|
|
|
|
logger.info("Cancel startup")
|
|
|
|
self.sync_finished(app_name)
|
2022-03-23 08:40:23 +13:00
|
|
|
return
|
2021-11-17 10:54:23 +13:00
|
|
|
except AssertionError:
|
|
|
|
dont_sync_after_finish = True
|
|
|
|
else:
|
|
|
|
if sync:
|
|
|
|
self.launch_queue[app_name] = (app_name, skip_update_check, offline)
|
|
|
|
return
|
2021-11-14 11:56:07 +13:00
|
|
|
self.sync_finished(app_name)
|
|
|
|
|
2021-12-24 22:09:50 +13:00
|
|
|
self.launch_game(
|
|
|
|
app_name, offline, skip_update_check, ask_always_sync=dont_sync_after_finish
|
|
|
|
)
|
|
|
|
|
|
|
|
def launch_game(
|
2022-01-03 10:52:43 +13:00
|
|
|
self,
|
|
|
|
app_name: str,
|
|
|
|
offline: bool = False,
|
|
|
|
skip_update_check: bool = False,
|
|
|
|
wine_bin: str = None,
|
|
|
|
wine_pfx: str = None,
|
|
|
|
ask_always_sync: bool = False,
|
2021-12-24 22:09:50 +13:00
|
|
|
):
|
2022-05-15 11:25:13 +12:00
|
|
|
executable = utils.get_rare_executable()
|
2022-05-19 09:41:48 +12:00
|
|
|
executable, args = executable[0], executable[1:]
|
2022-05-15 11:25:13 +12:00
|
|
|
args.extend([
|
|
|
|
"start", app_name
|
|
|
|
])
|
2022-05-19 09:41:48 +12:00
|
|
|
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")
|
|
|
|
|
|
|
|
QProcess.startDetached(executable, args)
|
|
|
|
logger.info(f"Start new Process: ({executable} {' '.join(args)})")
|
2022-05-15 11:25:13 +12:00
|
|
|
game_process = GameProcess(app_name)
|
|
|
|
game_process.game_finished.connect(self.game_finished)
|
|
|
|
self.running_games[app_name] = game_process
|
2021-11-02 10:53:04 +13:00
|
|
|
|
|
|
|
def game_finished(self, exit_code, app_name):
|
2022-02-02 10:29:34 +13:00
|
|
|
logger.info(f"Game exited with exit code: {exit_code}")
|
2022-03-29 08:11:22 +13:00
|
|
|
self.console.log(f"Game exited with code: {exit_code}")
|
2022-03-10 10:16:45 +13:00
|
|
|
self.signals.set_discord_rpc.emit("")
|
2021-11-28 12:50:02 +13:00
|
|
|
is_origin = self.core.get_game(app_name).third_party_store == "Origin"
|
2022-03-28 10:03:48 +13:00
|
|
|
if exit_code == 1 and is_origin:
|
2021-11-02 10:53:04 +13:00
|
|
|
msg_box = QMessageBox()
|
2021-12-24 22:09:50 +13:00
|
|
|
msg_box.setText(
|
|
|
|
self.tr(
|
|
|
|
"Origin is not installed. Do you want to download installer file? "
|
|
|
|
)
|
|
|
|
)
|
2021-11-02 10:53:04 +13:00
|
|
|
msg_box.addButton(QPushButton("Download"), QMessageBox.YesRole)
|
|
|
|
msg_box.addButton(QPushButton("Cancel"), QMessageBox.RejectRole)
|
|
|
|
resp = msg_box.exec()
|
|
|
|
# click install button
|
|
|
|
if resp == 0:
|
2022-03-28 10:03:48 +13:00
|
|
|
QDesktopServices.openUrl(QUrl("https://www.dm.origin.com/download"))
|
|
|
|
return
|
2022-03-10 09:45:02 +13:00
|
|
|
if exit_code != 0:
|
2021-12-24 22:09:50 +13:00
|
|
|
QMessageBox.warning(
|
|
|
|
None,
|
|
|
|
"Warning",
|
2022-03-10 09:45:02 +13:00
|
|
|
self.tr("Failed to launch {}. Check logs to find error").format(
|
2021-12-24 22:09:50 +13:00
|
|
|
self.core.get_game(app_name).app_title
|
|
|
|
),
|
|
|
|
)
|
2022-03-10 09:45:02 +13:00
|
|
|
# show console on error, even if disabled
|
|
|
|
self.console.show()
|
2021-11-02 10:53:04 +13:00
|
|
|
|
2021-11-17 10:54:23 +13:00
|
|
|
game: RunningGameModel = self.running_games.get(app_name, None)
|
2021-11-28 12:50:02 +13:00
|
|
|
if app_name in self.running_games.keys():
|
|
|
|
self.running_games.pop(app_name)
|
2021-11-03 09:57:19 +13:00
|
|
|
self.finished.emit(app_name, "")
|
2021-11-02 10:53:04 +13:00
|
|
|
|
|
|
|
if self.core.get_game(app_name).supports_cloud_saves:
|
|
|
|
if exit_code != 0:
|
2021-12-24 22:09:50 +13:00
|
|
|
r = QMessageBox.question(
|
|
|
|
None,
|
|
|
|
"Question",
|
|
|
|
self.tr(
|
2022-03-29 08:11:22 +13:00
|
|
|
"Game exited with code {}, which is not a normal code. "
|
|
|
|
"It could be caused by a crash. Do you want to sync cloud saves"
|
2021-12-24 22:09:50 +13:00
|
|
|
).format(exit_code),
|
|
|
|
buttons=QMessageBox.Yes | QMessageBox.No,
|
|
|
|
defaultButton=QMessageBox.Yes,
|
|
|
|
)
|
2021-11-02 10:53:04 +13:00
|
|
|
if r != QMessageBox.Yes:
|
|
|
|
return
|
2021-11-17 10:54:23 +13:00
|
|
|
self.cloud_save_utils.game_finished(app_name, game.always_ask_sync)
|
2021-11-02 10:53:04 +13:00
|
|
|
|
2022-03-28 08:52:32 +13:00
|
|
|
def _launch_pre_command(self, env: dict):
|
|
|
|
proc = QProcess()
|
2022-05-05 08:11:41 +12:00
|
|
|
environment = QProcessEnvironment().systemEnvironment()
|
2022-03-28 08:52:32 +13:00
|
|
|
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")
|
|
|
|
)
|
|
|
|
)
|
2022-05-05 08:11:41 +12:00
|
|
|
self.console.set_env(environment)
|
2022-03-28 08:52:32 +13:00
|
|
|
return proc
|
|
|
|
|
|
|
|
def _get_process(self, app_name, env):
|
|
|
|
process = GameProcess(app_name)
|
|
|
|
|
2022-05-05 08:11:41 +12:00
|
|
|
environment = QProcessEnvironment().systemEnvironment()
|
2022-03-28 08:52:32 +13:00
|
|
|
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
|
|
|
|
)
|
2022-05-05 08:11:41 +12:00
|
|
|
self.console.set_env(environment)
|
2022-03-28 08:52:32 +13:00
|
|
|
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
|
2022-03-28 10:03:48 +13:00
|
|
|
command.append(origin_uri)
|
2022-03-28 08:52:32 +13:00
|
|
|
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
|
|
|
|
|
2021-11-02 10:53:04 +13:00
|
|
|
def sync_finished(self, app_name):
|
|
|
|
if app_name in self.launch_queue.keys():
|
|
|
|
self.cloud_save_finished.emit(app_name)
|
|
|
|
params = self.launch_queue[app_name]
|
|
|
|
self.launch_queue.pop(app_name)
|
|
|
|
self.launch_game(*params)
|
|
|
|
else:
|
|
|
|
self.cloud_save_finished.emit(app_name)
|