Add communication system for game helper
This commit is contained in:
parent
b3d7f5ba92
commit
0bb1d0ef7e
|
@ -5,7 +5,7 @@ import shutil
|
|||
from dataclasses import dataclass
|
||||
from logging import getLogger
|
||||
|
||||
from PyQt5.QtCore import QObject, QSettings, QProcess, QProcessEnvironment, pyqtSignal, QUrl, QTimer, pyqtSlot
|
||||
from PyQt5.QtCore import QObject, QSettings, QProcess, QProcessEnvironment, pyqtSignal, QUrl, QTimer
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtNetwork import QLocalSocket
|
||||
from PyQt5.QtWidgets import QMessageBox, QPushButton
|
||||
|
@ -18,37 +18,31 @@ from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, Argument
|
|||
from rare.utils import legendary_utils
|
||||
from rare.utils import utils
|
||||
from rare.utils.meta import RareGameMeta
|
||||
from rare.game_launch_helper import message_models
|
||||
|
||||
logger = getLogger("GameUtils")
|
||||
|
||||
|
||||
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"],
|
||||
)
|
||||
|
||||
game_finished = pyqtSignal(int, str)
|
||||
game_finished = pyqtSignal(int, str) # exit_code, appname
|
||||
tried_connections = 0
|
||||
|
||||
def __init__(self, app_name: str):
|
||||
super(GameProcess, self).__init__()
|
||||
self.app_name = app_name
|
||||
self.game = LegendaryCoreSingleton().get_game(app_name)
|
||||
self.socket = QLocalSocket()
|
||||
self.socket.connected.connect(self._socket_connected)
|
||||
self.socket.errorOccurred.connect(self._error_occured)
|
||||
self.socket.errorOccurred.connect(self._error_occurred)
|
||||
self.socket.readyRead.connect(self._message_available)
|
||||
self.socket.disconnected.connect(lambda: self.socket.close())
|
||||
|
||||
def close_socket():
|
||||
try:
|
||||
self.socket.close()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
self.socket.disconnected.connect(close_socket)
|
||||
self.timer = QTimer()
|
||||
# wait a short time for process started
|
||||
self.timer.timeout.connect(self.connect_to_server)
|
||||
|
@ -56,29 +50,50 @@ class GameProcess(QObject):
|
|||
|
||||
def connect_to_server(self):
|
||||
self.socket.connectToServer(f"rare_{self.app_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(1, self.app_name)
|
||||
|
||||
def _message_available(self):
|
||||
message = self.socket.readAll().data()
|
||||
if not message.startswith(b"{"):
|
||||
logger.error(f"Received unsupported message{message.decode()}")
|
||||
logger.error(f"Received unsupported message: {message.decode('utf-8')}")
|
||||
return
|
||||
try:
|
||||
data = json.loads(message)
|
||||
except json.JSONDecodeError:
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(e)
|
||||
logger.error("Could not load json data")
|
||||
return
|
||||
|
||||
if data.get("action", "") == "finished":
|
||||
action = data.get("action", -1)
|
||||
|
||||
if action == -1:
|
||||
logger.error("Got unexpected action")
|
||||
elif action == message_models.Actions.finished:
|
||||
logger.info(f"{self.app_name} finished")
|
||||
resp = self.FinishedModel.from_json(data)
|
||||
self._game_finished(resp.exit_code)
|
||||
model = message_models.FinishedModel.base_from_json(data)
|
||||
self._game_finished(model)
|
||||
elif action == message_models.Actions.error:
|
||||
model = message_models.ErrorModel.from_json(data)
|
||||
logger.error(f"Error in game {self.game.app_title}: {model.error_string}")
|
||||
QMessageBox.warning(None, "Error", self.tr(
|
||||
"Error in game {}: \n{}").format(self.game.app_title, model.error_string))
|
||||
|
||||
elif action == message_models.Actions.state_update:
|
||||
model = message_models.StateChangedModel.from_json(data)
|
||||
if model.new_state == message_models.StateChangedModel.States.started:
|
||||
logger.info("Launched Game")
|
||||
|
||||
def _socket_connected(self):
|
||||
self.timer.stop()
|
||||
self.timer.deleteLater()
|
||||
logger.info(f"Connection established for {self.app_name}")
|
||||
|
||||
def _error_occured(self, _):
|
||||
def _error_occurred(self, _):
|
||||
logger.error(self.socket.errorString())
|
||||
|
||||
def _game_finished(self, exit_code):
|
||||
|
|
|
@ -1,17 +1,58 @@
|
|||
import json
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from argparse import Namespace
|
||||
from logging import getLogger
|
||||
from typing import Union
|
||||
|
||||
from PyQt5.QtCore import QObject, QProcess, pyqtSignal, QUrl
|
||||
from PyQt5.QtCore import QObject, QProcess, pyqtSignal, QUrl, QRunnable, QThreadPool
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtNetwork import QLocalServer, QLocalSocket
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from legendary.core import LegendaryCore
|
||||
|
||||
from .lgd_helper import get_launch_args, InitArgs, get_configured_process, LaunchArgs
|
||||
from .lgd_helper import get_launch_args, InitArgs, get_configured_process, LaunchArgs, GameArgsError
|
||||
from .message_models import ErrorModel, Actions, FinishedModel, BaseModel, StateChangedModel
|
||||
from ..shared import LegendaryCoreSingleton
|
||||
|
||||
|
||||
class PreLaunchThread(QRunnable):
|
||||
class Signals(QObject):
|
||||
ready_to_launch = pyqtSignal(LaunchArgs)
|
||||
started_pre_launch_command = pyqtSignal()
|
||||
pre_launch_command_finished = pyqtSignal(int) # exit_code
|
||||
error_occurred = pyqtSignal(str)
|
||||
|
||||
def __init__(self, args: InitArgs):
|
||||
super(PreLaunchThread, self).__init__()
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.app_name = args.app_name
|
||||
self.signals = self.Signals()
|
||||
|
||||
def run(self) -> None:
|
||||
args = self.prepare_launch(self.app_name)
|
||||
if not args:
|
||||
return
|
||||
self.signals.ready_to_launch.emit(args)
|
||||
|
||||
def prepare_launch(self, app_name) -> Union[LaunchArgs, None]:
|
||||
try:
|
||||
args = get_launch_args(self.core, InitArgs(app_name))
|
||||
except GameArgsError as e:
|
||||
self.signals.error_occurred.emit(str(e))
|
||||
return None
|
||||
if not args:
|
||||
return None
|
||||
|
||||
if args.pre_launch_command:
|
||||
proc = get_configured_process()
|
||||
proc.setProcessEnvironment(args.env)
|
||||
self.signals.started_pre_launch_command.emit()
|
||||
proc.start(args.pre_launch_command[0], args.pre_launch_command[1:])
|
||||
if args.pre_launch_wait:
|
||||
proc.waitForFinished(-1)
|
||||
return args
|
||||
|
||||
|
||||
class GameProcessHelper(QObject):
|
||||
|
@ -26,13 +67,13 @@ class GameProcessHelper(QObject):
|
|||
self.game_process = QProcess()
|
||||
self.app_name = app_name
|
||||
self.logger = getLogger(self.app_name)
|
||||
self.core = LegendaryCore()
|
||||
self.core = LegendaryCoreSingleton(True)
|
||||
|
||||
self.server = QLocalServer()
|
||||
ret = self.server.listen(f"rare_{self.app_name}")
|
||||
if not ret:
|
||||
self.logger.info(self.server.errorString())
|
||||
print("Server is running")
|
||||
self.logger.error(self.server.errorString())
|
||||
self.logger.info("Server is running")
|
||||
self.server.close()
|
||||
self.success = False
|
||||
return
|
||||
|
@ -41,22 +82,6 @@ class GameProcessHelper(QObject):
|
|||
self.game_process.finished.connect(self.game_finished)
|
||||
self.start_time = time.time()
|
||||
|
||||
def prepare_launch(self, app_name) -> Union[LaunchArgs, None]:
|
||||
args = get_launch_args(self.core, InitArgs(app_name))
|
||||
if not args:
|
||||
return None
|
||||
|
||||
self.game_process.setProcessEnvironment(args.env)
|
||||
|
||||
if args.pre_launch_command:
|
||||
proc = get_configured_process()
|
||||
proc.setProcessEnvironment(args.env)
|
||||
proc.start(args.pre_launch_command[0], args.pre_launch_command[1:])
|
||||
if args.pre_launch_wait:
|
||||
proc.waitForFinished(-1)
|
||||
|
||||
return args
|
||||
|
||||
def new_server_connection(self):
|
||||
if self.socket is not None:
|
||||
self.socket.disconnectFromServer()
|
||||
|
@ -65,28 +90,58 @@ class GameProcessHelper(QObject):
|
|||
self.socket.disconnected.connect(self.socket.deleteLater)
|
||||
self.socket.flush()
|
||||
|
||||
def send_message(self, message: Union[bytes, str]):
|
||||
if isinstance(message, str):
|
||||
message = message.encode("utf-8")
|
||||
def send_message(self, message: BaseModel):
|
||||
if self.socket:
|
||||
self.socket.write(message)
|
||||
self.socket.write(json.dumps(message.__dict__).encode("utf-8"))
|
||||
self.socket.flush()
|
||||
else:
|
||||
print("Can't send message")
|
||||
self.logger.error("Can't send message")
|
||||
|
||||
def game_finished(self, exit_code):
|
||||
print("game finished")
|
||||
self.logger.info("game finished")
|
||||
self.send_message(
|
||||
json.dumps({
|
||||
"action": "finished",
|
||||
"app_name": self.app_name,
|
||||
"exit_code": exit_code,
|
||||
"playtime": int(time.time() - self.start_time)
|
||||
})
|
||||
FinishedModel(
|
||||
action=Actions.finished,
|
||||
app_name=self.app_name,
|
||||
exit_code=exit_code,
|
||||
playtime=int(time.time() - self.start_time)
|
||||
)
|
||||
|
||||
)
|
||||
self.exit_app.emit()
|
||||
|
||||
def start(self, args: Namespace):
|
||||
def launch_game(self, args: LaunchArgs):
|
||||
# should never happen
|
||||
if not args:
|
||||
self.stop()
|
||||
return
|
||||
|
||||
self.start_time = time.time()
|
||||
|
||||
if args.is_origin_game:
|
||||
QDesktopServices.openUrl(QUrl(args.executable))
|
||||
return
|
||||
|
||||
self.game_process.finished.connect(self.game_finished)
|
||||
self.game_process.errorOccurred.connect(
|
||||
lambda err: self.error_occurred(self.game_process.errorString()))
|
||||
self.game_process.start(args.executable, args.args)
|
||||
self.send_message(
|
||||
StateChangedModel(
|
||||
action=Actions.state_update, app_name=self.app_name,
|
||||
new_state=StateChangedModel.States.started
|
||||
)
|
||||
)
|
||||
|
||||
def error_occurred(self, error_str: str):
|
||||
self.logger.warning(error_str)
|
||||
self.send_message(ErrorModel(
|
||||
error_string=error_str, app_name=self.app_name,
|
||||
action=Actions.error)
|
||||
)
|
||||
self.stop()
|
||||
|
||||
def start(self, args: InitArgs):
|
||||
if not args.offline:
|
||||
try:
|
||||
if not self.core.login():
|
||||
|
@ -94,25 +149,43 @@ class GameProcessHelper(QObject):
|
|||
except ValueError:
|
||||
# automatically launch offline if available
|
||||
self.logger.error("Not logged in. Try to launch game offline")
|
||||
# offline = True
|
||||
args.offline = True
|
||||
|
||||
launch_args = self.prepare_launch(args.app_name)
|
||||
if not launch_args:
|
||||
self.server.close()
|
||||
self.server.deleteLater()
|
||||
return
|
||||
if launch_args.is_origin_game:
|
||||
# origin game on Windows
|
||||
QDesktopServices.openUrl(QUrl(args[2]))
|
||||
return
|
||||
self.game_process.start(launch_args.executable, launch_args.args)
|
||||
self.start_time = time.time()
|
||||
worker = PreLaunchThread(args)
|
||||
worker.signals.ready_to_launch.connect(self.launch_game)
|
||||
worker.signals.error_occurred.connect(self.error_occurred)
|
||||
# worker.signals.started_pre_launch_command(None)
|
||||
|
||||
QThreadPool.globalInstance().start(worker)
|
||||
|
||||
def stop(self):
|
||||
self.server.close()
|
||||
self.server.deleteLater()
|
||||
self.exit_app.emit()
|
||||
|
||||
|
||||
def start_game(args: Namespace):
|
||||
args = InitArgs.from_argparse(args)
|
||||
logging.basicConfig(
|
||||
format="[%(name)s] %(levelname)s: %(message)s",
|
||||
level=logging.INFO,
|
||||
)
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
helper = GameProcessHelper(args.app_name)
|
||||
|
||||
def excepthook(exc_type, exc_value, exc_tb):
|
||||
tb = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
|
||||
helper.logger.fatal(tb)
|
||||
helper.send_message(ErrorModel(
|
||||
app_name=args.app_name,
|
||||
action=Actions.error,
|
||||
error_string=tb
|
||||
))
|
||||
helper.stop()
|
||||
|
||||
sys.excepthook = excepthook
|
||||
if not helper.success:
|
||||
return
|
||||
helper.start(args)
|
||||
|
|
|
@ -12,6 +12,10 @@ from legendary.models.game import InstalledGame, LaunchParameters
|
|||
logger = getLogger("Helper")
|
||||
|
||||
|
||||
class GameArgsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class InitArgs:
|
||||
app_name: str
|
||||
|
@ -79,11 +83,11 @@ def get_game_params(core: LegendaryCore, igame: InstalledGame, args: InitArgs,
|
|||
igame.app_name, igame.platform, update=False
|
||||
)
|
||||
except ValueError:
|
||||
logger.error("Metadata doesn't exist")
|
||||
return launch_args
|
||||
raise GameArgsError("Metadata doesn't exist")
|
||||
else:
|
||||
if latest.build_version != igame.version:
|
||||
return launch_args
|
||||
raise GameArgsError("Game is not up to date. Please update first")
|
||||
|
||||
params: LaunchParameters = core.get_launch_parameters(
|
||||
app_name=igame.app_name, offline=args.offline
|
||||
)
|
||||
|
@ -118,21 +122,18 @@ def get_launch_args(core: LegendaryCore, args: InitArgs = None) -> LaunchArgs:
|
|||
resp = LaunchArgs()
|
||||
|
||||
if not game:
|
||||
return resp
|
||||
raise GameArgsError(f"Could not find metadata for ")
|
||||
|
||||
if game.third_party_store == "Origin":
|
||||
args.offline = False
|
||||
else:
|
||||
if not igame:
|
||||
logger.error("Game is not installed or has unsupported format")
|
||||
return resp
|
||||
raise GameArgsError("Game is not installed or has unsupported format")
|
||||
|
||||
if game.is_dlc:
|
||||
logger.error("Game is dlc")
|
||||
return resp
|
||||
raise GameArgsError("Game is a DLC")
|
||||
if not os.path.exists(igame.install_path):
|
||||
logger.error("Game path does not exist")
|
||||
return resp
|
||||
raise GameArgsError("Game path does not exist")
|
||||
|
||||
if game.third_party_store == "Origin":
|
||||
resp = get_origin_params(core, args.app_name, args.offline, resp)
|
||||
|
@ -147,12 +148,12 @@ def get_launch_args(core: LegendaryCore, args: InitArgs = None) -> LaunchArgs:
|
|||
def get_configured_process(env: dict = None):
|
||||
proc = QProcess()
|
||||
proc.readyReadStandardOutput.connect(
|
||||
lambda: print(
|
||||
lambda: logger.info(
|
||||
str(proc.readAllStandardOutput().data(), "utf-8", "ignore")
|
||||
)
|
||||
)
|
||||
proc.readyReadStandardError.connect(
|
||||
lambda: print(
|
||||
lambda: logger.info(
|
||||
str(proc.readAllStandardError().data(), "utf-8", "ignore")
|
||||
)
|
||||
)
|
||||
|
|
68
rare/game_launch_helper/message_models.py
Normal file
68
rare/game_launch_helper/message_models.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
import enum
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class Actions:
|
||||
finished = 0
|
||||
error = 1
|
||||
message = 2
|
||||
state_update = 3
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseModel:
|
||||
action: int
|
||||
app_name: str
|
||||
|
||||
@staticmethod
|
||||
def base_from_json(data: dict) -> dict:
|
||||
return dict(
|
||||
action=data["action"],
|
||||
app_name=data["app_name"]
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FinishedModel(BaseModel):
|
||||
exit_code: int
|
||||
playtime: int # seconds
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data):
|
||||
return cls(
|
||||
**super().base_from_json(data),
|
||||
exit_code=data["exit_code"],
|
||||
playtime=data["playtime"],
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StateChangedModel(BaseModel):
|
||||
class States:
|
||||
started = 0
|
||||
|
||||
# for future
|
||||
syncing_cloud = 1
|
||||
cloud_sync_finished = 2
|
||||
|
||||
new_state: int
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data):
|
||||
return cls(
|
||||
action=data["action"],
|
||||
app_name=data["app_name"],
|
||||
new_state=data["new_state"]
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ErrorModel(BaseModel):
|
||||
error_string: str
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data):
|
||||
return cls(
|
||||
**super().base_from_json(data),
|
||||
error_string=data["error_string"]
|
||||
)
|
Loading…
Reference in a new issue