1
0
Fork 0
mirror of synced 2024-06-03 03:04:42 +12:00

Add communication system for game helper

This commit is contained in:
Dummerle 2022-06-08 21:14:48 +02:00
parent b3d7f5ba92
commit 0bb1d0ef7e
No known key found for this signature in database
GPG key ID: AB68CC59CA39F2F1
4 changed files with 242 additions and 85 deletions

View file

@ -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):

View file

@ -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)

View file

@ -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")
)
)

View 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"]
)