diff --git a/rare/components/tabs/games/game_info/game_info.py b/rare/components/tabs/games/game_info/game_info.py index b51afabc..c11f8f79 100644 --- a/rare/components/tabs/games/game_info/game_info.py +++ b/rare/components/tabs/games/game_info/game_info.py @@ -59,7 +59,6 @@ class GameInfo(QWidget): self.rgame: Optional[RareGame] = None self.game: Optional[Game] = None self.igame: Optional[InstalledGame] = None - self.verify_threads: Dict[str, QRunnable] = {} self.image = ImageWidget(self) self.image.setFixedSize(ImageSize.Display) @@ -127,99 +126,95 @@ class GameInfo(QWidget): @pyqtSlot() def repair(self): """ This function is to be called from the button only """ - repair_file = os.path.join(self.core.lgd.get_tmp_path(), f"{self.igame.app_name}.repair") + repair_file = os.path.join(self.core.lgd.get_tmp_path(), f"{self.rgame.app_name}.repair") if not os.path.exists(repair_file): QMessageBox.warning( self, - self.tr("Error - {}").format(self.igame.title), + self.tr("Error - {}").format(self.rgame.title), self.tr( "Repair file does not exist or game does not need a repair. Please verify game first" ), ) return - self.repair_game(self.igame) + self.repair_game(self.rgame) - def repair_game(self, igame: InstalledGame): - game = self.core.get_game(igame.app_name) + def repair_game(self, rgame: RareGame): + rgame.update_game() ans = False - if igame.version != game.app_version(igame.platform): + if rgame.has_update: ans = QMessageBox.question( self, self.tr("Repair and update?"), self.tr( "There is an update for {} from {} to {}. " "Do you want to update the game while repairing it?" - ).format(igame.title, igame.version, game.app_version(igame.platform)), + ).format(rgame.title, rgame.version, rgame.remote_version), ) == QMessageBox.Yes self.signals.game.install.emit( InstallOptionsModel( - app_name=igame.app_name, repair_mode=True, repair_and_update=ans, update=True + app_name=rgame.app_name, repair_mode=True, repair_and_update=ans, update=True ) ) @pyqtSlot() def verify(self): """ This function is to be called from the button only """ - if not os.path.exists(self.igame.install_path): - logger.error(f"Installation path {self.igame.install_path} for {self.igame.title} does not exist") + if not os.path.exists(self.rgame.igame.install_path): + logger.error(f"Installation path {self.rgame.igame.install_path} for {self.rgame.title} does not exist") QMessageBox.warning( self, - self.tr("Error - {}").format(self.igame.title), - self.tr("Installation path for {} does not exist. Cannot continue.").format(self.igame.title), + self.tr("Error - {}").format(self.rgame.title), + self.tr("Installation path for {} does not exist. Cannot continue.").format(self.rgame.title), ) return - self.verify_game(self.igame) + self.verify_game(self.rgame) - def verify_game(self, igame: InstalledGame): + def verify_game(self, rgame: RareGame): self.ui.verify_widget.setCurrentIndex(1) - verify_worker = VerifyWorker(igame.app_name) + verify_worker = VerifyWorker(rgame, self.core, self.args) verify_worker.signals.status.connect(self.verify_status) verify_worker.signals.result.connect(self.verify_result) verify_worker.signals.error.connect(self.verify_error) self.ui.verify_progress.setValue(0) - self.verify_threads[igame.app_name] = verify_worker + self.rgame.active_thread = verify_worker self.verify_pool.start(verify_worker) self.ui.move_button.setEnabled(False) - def verify_cleanup(self, app_name: str): + def verify_cleanup(self, rgame: RareGame): self.ui.verify_widget.setCurrentIndex(0) - self.verify_threads.pop(app_name) + rgame.active_thread = None self.ui.move_button.setEnabled(True) self.ui.verify_button.setEnabled(True) - @pyqtSlot(str, str) - def verify_error(self, app_name, message): - self.verify_cleanup(app_name) - igame = self.core.get_installed_game(app_name) + @pyqtSlot(RareGame, str) + def verify_error(self, rgame: RareGame, message): + self.verify_cleanup(rgame) QMessageBox.warning( self, - self.tr("Error - {}").format(igame.title), + self.tr("Error - {}").format(rgame.title), message ) - @pyqtSlot(str, int, int, float, float) - def verify_status(self, app_name, num, total, percentage, speed): - # checked, max, app_name - if app_name == self.game.app_name: - self.ui.verify_progress.setValue(num * 100 // total) + @pyqtSlot(RareGame, int, int, float, float) + def verify_status(self, rgame: RareGame, num, total, percentage, speed): + self.ui.verify_progress.setValue(num * 100 // total) - @pyqtSlot(str, bool, int, int) - def verify_result(self, app_name, success, failed, missing): - self.verify_cleanup(app_name) + @pyqtSlot(RareGame, bool, int, int) + def verify_result(self, rgame: RareGame, success, failed, missing): + self.verify_cleanup(rgame) self.ui.repair_button.setDisabled(success) - igame = self.core.get_installed_game(app_name) if success: QMessageBox.information( self, - self.tr("Summary - {}").format(igame.title), + self.tr("Summary - {}").format(rgame.title), self.tr("{} has been verified successfully. " - "No missing or corrupt files found").format(igame.title), + "No missing or corrupt files found").format(rgame.title), ) - self.verification_finished.emit(igame) + self.verification_finished.emit(rgame.igame) else: ans = QMessageBox.question( self, - self.tr("Summary - {}").format(igame.title), + self.tr("Summary - {}").format(rgame.title), self.tr( "Verification failed, {} file(s) corrupted, {} file(s) are missing. " "Do you want to repair them?" @@ -228,7 +223,7 @@ class GameInfo(QWidget): QMessageBox.Yes, ) if ans == QMessageBox.Yes: - self.repair_game(igame) + self.repair_game(rgame) @pyqtSlot(str) def move_game(self, dest_path): @@ -325,8 +320,22 @@ class GameInfo(QWidget): self.ui.move_button.showMenu() def update_game(self, rgame: RareGame): - # FIXME: Use RareGame for the rest of the code + if self.rgame is not None: + if worker:= self.rgame.active_thread is not None: + if isinstance(worker, VerifyWorker): + try: + worker.signals.status.disconnect(self.verify_status) + except TypeError as e: + logger.warning(f"{self.rgame.app_title} verify worker: {e}") self.rgame = rgame + if worker:= self.rgame.active_thread is not None: + if isinstance(worker, VerifyWorker): + self.ui.verify_widget.setCurrentIndex(1) + self.ui.verify_progress.setValue(self.rgame.progress) + worker.signals.status.connect(self.verify_status) + else: + self.ui.verify_widget.setCurrentIndex(0) + # FIXME: Use RareGame for the rest of the code self.game = rgame.game self.igame = rgame.igame self.title.setTitle(self.game.app_title) @@ -386,17 +395,17 @@ class GameInfo(QWidget): self.steam_worker.set_app_name(self.game.app_name) QThreadPool.globalInstance().start(self.steam_worker) - if len(self.verify_threads.keys()) == 0 or not self.verify_threads.get(self.game.app_name): - self.ui.verify_widget.setCurrentIndex(0) - elif self.verify_threads.get(self.game.app_name): - self.ui.verify_widget.setCurrentIndex(1) - self.ui.verify_progress.setValue( - int( - self.verify_threads[self.game.app_name].num - / self.verify_threads[self.game.app_name].total - * 100 - ) - ) + # if len(self.verify_threads.keys()) == 0 or not self.verify_threads.get(self.game.app_name): + # self.ui.verify_widget.setCurrentIndex(0) + # elif self.verify_threads.get(self.game.app_name): + # self.ui.verify_widget.setCurrentIndex(1) + # self.ui.verify_progress.setValue( + # int( + # self.verify_threads[self.game.app_name].num + # / self.verify_threads[self.game.app_name].total + # * 100 + # ) + # ) # If the game that is currently moving matches with the current app_name, we show the progressbar. # Otherwhise, we show the move tool button. @@ -409,7 +418,7 @@ class GameInfo(QWidget): self.ui.move_stack.setCurrentIndex(index) # If a game is verifying or moving, disable both verify and moving buttons. - if len(self.verify_threads): + if rgame.active_thread is not None: self.ui.verify_button.setEnabled(False) self.ui.move_button.setEnabled(False) if self.is_moving: diff --git a/rare/models/game.py b/rare/models/game.py index e1a8b9ad..c2b7bba3 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -1,6 +1,6 @@ import json import os -from dataclasses import dataclass +from dataclasses import dataclass, field from datetime import datetime from enum import IntEnum from logging import getLogger @@ -18,18 +18,21 @@ from rare.utils.paths import data_dir logger = getLogger("RareGame") -class RareGameState(IntEnum): - IDLE = 0, - RUNNING = 1, - - class RareGame(QObject): + class State(IntEnum): + IDLE = 0 + RUNNING = 1 + DOWNLOADING = 2 + VERIFYING = 3 + MOVING = 4 + @dataclass class Metadata: queued: bool = False queue_pos: Optional[int] = None last_played: Optional[datetime] = None + tags: List[str] = field(default_factory=list) @classmethod def from_dict(cls, data: Dict): @@ -37,6 +40,7 @@ class RareGame(QObject): queued=data.get("queued", False), queue_pos=data.get("queue_pos", None), last_played=datetime.strptime(data.get("last_played", "None"), "%Y-%m-%dT%H:%M:%S.%f"), + tags=data.get("tags", []), ) def as_dict(self): @@ -44,6 +48,7 @@ class RareGame(QObject): queued=self.queued, queue_pos=self.queue_pos, last_played=self.last_played.strftime("%Y-%m-%dT%H:%M:%S.%f"), + tags=self.tags, ) def __bool__(self): @@ -123,6 +128,18 @@ class RareGame(QObject): with open(os.path.join(data_dir(), "game_meta.json"), "w") as metadata_json: json.dump(metadata, metadata_json, indent=2) + def update_game(self): + self.game = self.core.get_game( + self.app_name, update_meta=True, platform=self.igame.platform if self.igame else "Windows" + ) + + def update_igame(self): + self.igame = self.core.get_installed_game(self.app_name) + + def update_rgame(self): + self.update_igame() + self.update_game() + @property def app_name(self) -> str: return self.igame.app_name if self.igame is not None else self.game.app_name @@ -346,11 +363,6 @@ class RareGame(QObject): """ return not self.game.asset_infos - def install(self): - self.signals.game.install.emit( - InstallOptionsModel(app_name=self.game.app_name) - ) - @property def is_origin(self) -> bool: return self.game.metadata.get("customAttributes", {}).get("ThirdPartyManagedApp", {}).get("value") == "Origin" @@ -387,3 +399,9 @@ class RareGame(QObject): def finish_progress(self, fail: bool, miss: int, app: str): self.set_installed(True) self.signals.progress.finish.emit(fail) + + def install(self): + self.signals.game.install.emit( + InstallOptionsModel(app_name=self.game.app_name) + ) + diff --git a/rare/shared/workers/verify.py b/rare/shared/workers/verify.py index dca52349..1a9bb1eb 100644 --- a/rare/shared/workers/verify.py +++ b/rare/shared/workers/verify.py @@ -1,41 +1,47 @@ import os +import sys +from argparse import Namespace from logging import getLogger from PyQt5.QtCore import pyqtSignal, QObject, QRunnable from rare.lgndr.cli import LegendaryCLI +from rare.lgndr.core import LegendaryCore from rare.lgndr.glue.arguments import LgndrVerifyGameArgs from rare.lgndr.glue.monkeys import LgndrIndirectStatus -from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton +from rare.models.game import RareGame logger = getLogger("VerificationWorker") class VerifyWorker(QRunnable): class Signals(QObject): - status = pyqtSignal(str, int, int, float, float) - result = pyqtSignal(str, bool, int, int) - error = pyqtSignal(str, str) + status = pyqtSignal(RareGame, int, int, float, float) + result = pyqtSignal(RareGame, bool, int, int) + error = pyqtSignal(RareGame, str) num: int = 0 total: int = 1 # set default to 1 to avoid DivisionByZero before it is initialized - def __init__(self, app_name): + def __init__(self, rgame: RareGame, core: LegendaryCore, args: Namespace): + sys.excepthook = sys.__excepthook__ super(VerifyWorker, self).__init__() self.signals = VerifyWorker.Signals() self.setAutoDelete(True) - self.core = LegendaryCoreSingleton() - self.args = ArgumentsSingleton() - self.app_name = app_name + self.core = core + self.args = args + self.rgame = rgame def status_callback(self, num: int, total: int, percentage: float, speed: float): - self.signals.status.emit(self.app_name, num, total, percentage, speed) + self.rgame.signals.progress.update.emit(num * 100 // total) + self.signals.status.emit(self.rgame, num, total, percentage, speed) def run(self): + self.rgame.signals.progress.start.emit() cli = LegendaryCLI(self.core) status = LgndrIndirectStatus() args = LgndrVerifyGameArgs( - app_name=self.app_name, indirect_status=status, verify_stdout=self.status_callback + app_name=self.rgame.app_name, indirect_status=status, verify_stdout=self.status_callback ) # lk: first pass, verify with the current manifest @@ -57,16 +63,18 @@ class VerifyWorker(QRunnable): if result is None: raise ValueError except ValueError: - self.signals.error.emit(self.app_name, status.message) + self.rgame.signals.progress.finish.emit(True) + self.signals.error.emit(self.rgame, status.message) return success = result is not None and not any(result) if success: # lk: if verification was successful we delete the repair file and run the clean procedure # lk: this could probably be cut down to what is relevant for this use-case and skip the `cli` call - igame = self.core.get_installed_game(self.app_name) - game = self.core.get_game(self.app_name, platform=igame.platform) - repair_file = os.path.join(self.core.lgd.get_tmp_path(), f"{self.app_name}.repair") - cli.install_game_cleanup(game=game, igame=igame, repair_mode=True, repair_file=repair_file) + repair_file = os.path.join(self.core.lgd.get_tmp_path(), f"{self.rgame.app_name}.repair") + cli.install_game_cleanup(game=self.rgame.game, igame=self.rgame.igame, repair_mode=True, repair_file=repair_file) + self.rgame.update_rgame() + + self.rgame.signals.progress.finish.emit(False) + self.signals.result.emit(self.rgame, success, *result) - self.signals.result.emit(self.app_name, success, *result)