1
0
Fork 0
mirror of synced 2024-06-02 18:54:41 +12:00

Lgndr: Create argument models for consistent function arguments and cleaner implementation.

Lgndr: Prefix files that aren't part of legendary with 'api_'
Lgndr: Return statistics from 'verify_game' instead of parsing the exception
VerifyWorker: Add 'error' signal for exceptions.
This commit is contained in:
loathingKernel 2022-07-10 19:32:42 +03:00
parent d7aba41aa4
commit 3aae3887f6
10 changed files with 126 additions and 101 deletions

View file

@ -8,6 +8,7 @@ from PyQt5.QtCore import Qt, QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSl
from PyQt5.QtGui import QCloseEvent, QKeyEvent from PyQt5.QtGui import QCloseEvent, QKeyEvent
from PyQt5.QtWidgets import QDialog, QFileDialog, QCheckBox from PyQt5.QtWidgets import QDialog, QFileDialog, QCheckBox
from rare.lgndr.api_arguments import LgndrInstallGameArgs
from rare.lgndr.cli import LegendaryCLI from rare.lgndr.cli import LegendaryCLI
from rare.lgndr.core import LegendaryCore from rare.lgndr.core import LegendaryCore
from legendary.models.downloading import ConditionCheckResult from legendary.models.downloading import ConditionCheckResult
@ -311,7 +312,7 @@ class InstallInfoWorker(QRunnable):
cli = LegendaryCLISingleton() cli = LegendaryCLISingleton()
download = InstallDownloadModel( download = InstallDownloadModel(
# *self.core.prepare_download( # *self.core.prepare_download(
*cli.prepare_install( *cli.prepare_install(LgndrInstallGameArgs(
app_name=self.dl_item.options.app_name, app_name=self.dl_item.options.app_name,
base_path=self.dl_item.options.base_path, base_path=self.dl_item.options.base_path,
force=self.dl_item.options.force, force=self.dl_item.options.force,
@ -336,7 +337,7 @@ class InstallInfoWorker(QRunnable):
# disable_delta=, # disable_delta=,
# override_delta_manifest=, # override_delta_manifest=,
# reset_sdl=, # reset_sdl=,
sdl_prompt=lambda app_name, title: self.dl_item.options.sdl_list, sdl_prompt=lambda app_name, title: self.dl_item.options.sdl_list,)
) )
) )

View file

@ -143,20 +143,27 @@ class GameInfo(QWidget, Ui_GameInfo):
return return
self.verify_widget.setCurrentIndex(1) self.verify_widget.setCurrentIndex(1)
verify_worker = VerifyWorker(self.game.app_name) verify_worker = VerifyWorker(self.game.app_name)
verify_worker.signals.status.connect(self.verify_statistics) verify_worker.signals.status.connect(self.verify_status)
verify_worker.signals.summary.connect(self.finish_verify) verify_worker.signals.result.connect(self.verify_result)
verify_worker.signals.error.connect(self.verify_error)
self.verify_progress.setValue(0) self.verify_progress.setValue(0)
self.verify_threads[self.game.app_name] = verify_worker self.verify_threads[self.game.app_name] = verify_worker
self.verify_pool.start(verify_worker) self.verify_pool.start(verify_worker)
self.move_button.setEnabled(False) self.move_button.setEnabled(False)
def verify_statistics(self, num, total, app_name): @pyqtSlot(str, str)
def verify_error(self, app_name, message):
pass
@pyqtSlot(str, int, int, float, float)
def verify_status(self, app_name, num, total, percentage, speed):
# checked, max, app_name # checked, max, app_name
if app_name == self.game.app_name: if app_name == self.game.app_name:
self.verify_progress.setValue(num * 100 // total) self.verify_progress.setValue(num * 100 // total)
def finish_verify(self, failed, missing, app_name): @pyqtSlot(str, bool, int, int)
if failed == missing == 0: def verify_result(self, app_name, success, failed, missing):
if success:
QMessageBox.information( QMessageBox.information(
self, self,
"Summary", "Summary",

View file

@ -6,7 +6,7 @@ from typing import Tuple, Iterable, List
from PyQt5.QtCore import Qt, QThreadPool, QRunnable, pyqtSlot, pyqtSignal from PyQt5.QtCore import Qt, QThreadPool, QRunnable, pyqtSlot, pyqtSignal
from PyQt5.QtWidgets import QGroupBox, QListWidgetItem, QFileDialog, QMessageBox from PyQt5.QtWidgets import QGroupBox, QListWidgetItem, QFileDialog, QMessageBox
from rare.lgndr.exception import LgndrException from rare.lgndr.api_exception import LgndrException
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
from rare.ui.components.tabs.games.import_sync.egl_sync_group import Ui_EGLSyncGroup from rare.ui.components.tabs.games.import_sync.egl_sync_group import Ui_EGLSyncGroup
from rare.ui.components.tabs.games.import_sync.egl_sync_list_group import ( from rare.ui.components.tabs.games.import_sync.egl_sync_list_group import (

View file

@ -11,7 +11,8 @@ from PyQt5.QtCore import Qt, QModelIndex, pyqtSignal, QRunnable, QObject, QThrea
from PyQt5.QtGui import QStandardItemModel from PyQt5.QtGui import QStandardItemModel
from PyQt5.QtWidgets import QFileDialog, QGroupBox, QCompleter, QTreeView, QHeaderView, qApp, QMessageBox from PyQt5.QtWidgets import QFileDialog, QGroupBox, QCompleter, QTreeView, QHeaderView, qApp, QMessageBox
from rare.lgndr.exception import LgndrException from lgndr.api_arguments import LgndrImportGameArgs
from rare.lgndr.api_exception import LgndrException
from rare.shared import LegendaryCLISingleton, LegendaryCoreSingleton, GlobalSignalsSingleton, ApiResultsSingleton from rare.shared import LegendaryCLISingleton, LegendaryCoreSingleton, GlobalSignalsSingleton, ApiResultsSingleton
from rare.ui.components.tabs.games.import_sync.import_group import Ui_ImportGroup from rare.ui.components.tabs.games.import_sync.import_group import Ui_ImportGroup
from rare.utils import legendary_utils from rare.utils import legendary_utils
@ -105,23 +106,12 @@ class ImportWorker(QRunnable):
# else: # else:
# return err # return err
# TODO: This should be moved into RareCore and wrap import_game
def import_game_args(self, app_path: str, app_name: str, platfrom: str = "Windows",
disable_check: bool = False, skip_dlcs: bool = False, with_dlcs: bool = False, yes: bool = False):
args = Namespace(
app_path=app_path,
app_name=app_name,
platform=platfrom,
disable_check=disable_check,
skip_dlcs=skip_dlcs,
with_dlcs=with_dlcs,
yes=yes,
)
return args
def __import_game(self, app_name: str, path: Path): def __import_game(self, app_name: str, path: Path):
cli = LegendaryCLISingleton() cli = LegendaryCLISingleton()
args = self.import_game_args(str(path), app_name) args = LgndrImportGameArgs(
app_path=str(path),
app_name=app_name,
)
try: try:
cli.import_game(args) cli.import_game(args)
return "" return ""

View file

@ -0,0 +1,5 @@
"""
Module that overloads and monkeypatches legendary's classes/methods to work with Rare
Files with the 'api_' prefix are not part of legendary's source, and contain facilities relating to Rare.
"""

View file

@ -0,0 +1,71 @@
from dataclasses import dataclass
from multiprocessing import Queue
from typing import Callable, List
@dataclass(kw_only=True)
class LgndrCommonArgs:
# keep this here for future reference
# when we move to 3.10 we can use 'kw_only' to do dataclass inheritance
app_name: str
platform: str = "Windows"
yes: bool = False
@dataclass
class LgndrImportGameArgs:
app_path: str
app_name: str
platform: str = "Windows"
disable_check: bool = False
skip_dlcs: bool = False
with_dlcs: bool = False
yes: bool = False
@dataclass
class LgndrVerifyGameArgs:
app_name: str
# Rare's extra arguments
verify_stdout: Callable[[int, int, float, float], None] = lambda a0, a1, a2, a3: print(
f"Verification progress: {a0}/{a1} ({a2:.01f}%) [{a3:.1f} MiB/s]\t\r"
)
@dataclass
class LgndrInstallGameArgs:
app_name: str
base_path: str = ""
status_q: Queue = None
shared_memory: int = 0
max_workers: int = 0
force: bool = False
disable_patching: bool = False
game_folder: str = ""
override_manifest: str = ""
override_old_manifest: str = ""
override_base_url: str = ""
platform: str = "Windows"
file_prefix: List = None
file_exclude_prefix: List = None
install_tag: List = None
order_opt: bool = False
dl_timeout: int = 10
repair_mode: bool = False
repair_and_update: bool = False
disable_delta: bool = False
override_delta_manifest: str = ""
egl_guid: str = ""
preferred_cdn: str = None
no_install: bool = False
ignore_space: bool = False
disable_sdl: bool = False
reset_sdl: bool = False
skip_sdl: bool = False
disable_https: bool = False
yes: bool = True
# Rare's extra arguments
sdl_prompt: Callable[[str, str], List[str]] = lambda a0, a1: []
verify_stdout: Callable[[int, int, float, float], None] = lambda a0, a1, a2, a3: print(
f"Verification progress: {a0}/{a1} ({a2:.01f}%) [{a3:.1f} MiB/s]\t\r"
)

View file

@ -1,9 +1,7 @@
import os import os
import logging import logging
import time import time
from argparse import Namespace from typing import Optional, Union
from multiprocessing import Queue
from typing import Optional, Callable, List
import legendary.cli import legendary.cli
from PyQt5.QtWidgets import QLabel, QMessageBox from PyQt5.QtWidgets import QLabel, QMessageBox
@ -14,8 +12,9 @@ from legendary.utils.lfs import validate_files
from legendary.utils.selective_dl import get_sdl_appname from legendary.utils.selective_dl import get_sdl_appname
from .core import LegendaryCore from .core import LegendaryCore
from .exception import LgndrException, LgndrLogHandler
from .manager import DLManager from .manager import DLManager
from .api_arguments import LgndrInstallGameArgs, LgndrImportGameArgs, LgndrVerifyGameArgs
from .api_exception import LgndrException, LgndrLogHandler
def get_boolean_choice(message): def get_boolean_choice(message):
@ -40,49 +39,7 @@ class LegendaryCLI(legendary.cli.LegendaryCLI):
self.handler = LgndrLogHandler() self.handler = LgndrLogHandler()
self.logger.addHandler(self.handler) self.logger.addHandler(self.handler)
# def __init__(self, core: LegendaryCore): def prepare_install(self, args: LgndrInstallGameArgs) -> (DLManager, AnalysisResult, InstalledGame, Game, bool, Optional[str], ConditionCheckResult):
# self.core = core
# self.logger = logging.getLogger('cli')
# self.logging_queue = None
# self.handler = LgndrLogHandler()
# self.logger.addHandler(self.handler)
# app_name, repair, repair_mode, no_install, repair_and_update, file_prefix, file_exclude_prefix, platform
# sdl_prompt, status_q
def prepare_install(self, app_name: str, base_path: str = '',
status_q: Queue = None, shared_memory: int = 0, max_workers: int = 0,
force: bool = False, disable_patching: bool = False,
game_folder: str = '', override_manifest: str = '',
override_old_manifest: str = '', override_base_url: str = '',
platform: str = 'Windows', file_prefix: list = None,
file_exclude_prefix: list = None, install_tag: list = None,
order_opt: bool = False, dl_timeout: int = 10,
repair_mode: bool = False, repair_and_update: bool = False,
disable_delta: bool = False, override_delta_manifest: str = '',
egl_guid: str = '', preferred_cdn: str = None,
no_install: bool = False, ignore_space: bool = False,
disable_sdl: bool = False, reset_sdl: bool = False, skip_sdl: bool = False,
sdl_prompt: Callable[[str, str], List[str]] = list,
yes: bool = True, verify_callback: Callable[[int, int, float, float], None] = None,
disable_https: bool = False) -> (DLManager, AnalysisResult, InstalledGame, Game, bool, Optional[str], ConditionCheckResult):
args = Namespace(
app_name=app_name, base_path=base_path,
status_q=status_q, shared_memory=shared_memory, max_workers=max_workers,
force=force, disable_patching=disable_patching,
game_folder=game_folder, override_manifest=override_manifest,
override_old_manifest=override_old_manifest, override_base_url=override_base_url,
platform=platform, file_prefix=file_prefix,
file_exclude_prefix=file_exclude_prefix, install_tag=install_tag,
order_opt=order_opt, dl_timeout=dl_timeout,
repair_mode=repair_mode, repair_and_update=repair_and_update,
disable_delta=disable_delta, override_delta_manifest=override_delta_manifest,
preferred_cdn=preferred_cdn,
no_install=no_install, ignore_space=ignore_space,
disable_sdl=disable_sdl, reset_sdl=reset_sdl, skip_sdl=skip_sdl,
sdl_prompt=sdl_prompt,
yes=yes, callback=verify_callback,
disable_https=disable_https
)
old_choice = legendary.cli.get_boolean_choice old_choice = legendary.cli.get_boolean_choice
legendary.cli.get_boolean_choice = get_boolean_choice legendary.cli.get_boolean_choice = get_boolean_choice
try: try:
@ -92,7 +49,7 @@ class LegendaryCLI(legendary.cli.LegendaryCLI):
finally: finally:
legendary.cli.get_boolean_choice = old_choice legendary.cli.get_boolean_choice = old_choice
def install_game(self, args): def install_game(self, args: LgndrInstallGameArgs) -> (DLManager, AnalysisResult, InstalledGame, Game, bool, Optional[str], ConditionCheckResult):
args.app_name = self._resolve_aliases(args.app_name) args.app_name = self._resolve_aliases(args.app_name)
if self.core.is_installed(args.app_name): if self.core.is_installed(args.app_name):
igame = self.core.get_installed_game(args.app_name) igame = self.core.get_installed_game(args.app_name)
@ -267,10 +224,7 @@ class LegendaryCLI(legendary.cli.LegendaryCLI):
def uninstall_game(self, args): def uninstall_game(self, args):
super(LegendaryCLI, self).uninstall_game(args) super(LegendaryCLI, self).uninstall_game(args)
def verify_game(self, args, print_command=True, repair_mode=False, repair_online=False): def verify_game(self, args: Union[LgndrVerifyGameArgs, LgndrInstallGameArgs], print_command=True, repair_mode=False, repair_online=False):
if not hasattr(args, 'callback') or args.callback is None:
args.callback = print
args.app_name = self._resolve_aliases(args.app_name) args.app_name = self._resolve_aliases(args.app_name)
if not self.core.is_installed(args.app_name): if not self.core.is_installed(args.app_name):
logger.error(f'Game "{args.app_name}" is not installed') logger.error(f'Game "{args.app_name}" is not installed')
@ -339,8 +293,8 @@ class LegendaryCLI(legendary.cli.LegendaryCLI):
speed = (processed - last_processed) / 1024 / 1024 / delta speed = (processed - last_processed) / 1024 / 1024 / delta
last_processed = processed last_processed = processed
if args.callback: if args.verify_stdout:
args.callback(num, total, percentage, speed) args.verify_stdout(num, total, percentage, speed)
if result == VerifyResult.HASH_MATCH: if result == VerifyResult.HASH_MATCH:
repair_file.append(f'{result_hash}:{path}') repair_file.append(f'{result_hash}:{path}')
@ -356,8 +310,8 @@ class LegendaryCLI(legendary.cli.LegendaryCLI):
logger.info(f'Other failure (see log), treating file as missing: "{path}"') logger.info(f'Other failure (see log), treating file as missing: "{path}"')
missing.append(path) missing.append(path)
if args.callback: if args.verify_stdout:
args.callback(num, total, percentage, speed) args.verify_stdout(num, total, percentage, speed)
# always write repair file, even if all match # always write repair file, even if all match
if repair_file: if repair_file:
@ -368,12 +322,14 @@ class LegendaryCLI(legendary.cli.LegendaryCLI):
if not missing and not failed: if not missing and not failed:
logger.info('Verification finished successfully.') logger.info('Verification finished successfully.')
return True, 0, 0
else: else:
logger.error(f'Verification failed, {len(failed)} file(s) corrupted, {len(missing)} file(s) are missing.') logger.warning(f'Verification failed, {len(failed)} file(s) corrupted, {len(missing)} file(s) are missing.')
if print_command: if print_command:
logger.info(f'Run "legendary repair {args.app_name}" to repair your game installation.') logger.info(f'Run "legendary repair {args.app_name}" to repair your game installation.')
return False, len(failed), len(missing)
def import_game(self, args): def import_game(self, args: LgndrImportGameArgs):
old_choice = legendary.cli.get_boolean_choice old_choice = legendary.cli.get_boolean_choice
legendary.cli.get_boolean_choice = get_boolean_choice legendary.cli.get_boolean_choice = get_boolean_choice
try: try:

View file

@ -5,7 +5,7 @@ from legendary.models.downloading import AnalysisResult
from legendary.models.game import Game from legendary.models.game import Game
from legendary.models.manifest import ManifestMeta from legendary.models.manifest import ManifestMeta
from .exception import LgndrException, LgndrLogHandler from .api_exception import LgndrException, LgndrLogHandler
from .manager import DLManager from .manager import DLManager

View file

@ -1,17 +1,14 @@
import os import os
import platform import platform
from argparse import Namespace
from logging import getLogger from logging import getLogger
from PyQt5.QtCore import pyqtSignal, QCoreApplication, QObject, QRunnable, QStandardPaths from PyQt5.QtCore import pyqtSignal, QCoreApplication, QObject, QRunnable, QStandardPaths
from legendary.core import LegendaryCore from legendary.core import LegendaryCore
from legendary.models.game import VerifyResult
from legendary.utils.lfs import validate_files
from parse import parse
from rare.lgndr.api_arguments import LgndrVerifyGameArgs
from rare.lgndr.api_exception import LgndrException
from rare.shared import LegendaryCLISingleton, LegendaryCoreSingleton from rare.shared import LegendaryCLISingleton, LegendaryCoreSingleton
from rare.utils import config_helper from rare.utils import config_helper
from rare.lgndr.exception import LgndrException
logger = getLogger("Legendary Utils") logger = getLogger("Legendary Utils")
@ -88,8 +85,9 @@ def update_manifest(app_name: str, core: LegendaryCore):
class VerifyWorker(QRunnable): class VerifyWorker(QRunnable):
class Signals(QObject): class Signals(QObject):
status = pyqtSignal(int, int, str) status = pyqtSignal(str, int, int, float, float)
summary = pyqtSignal(int, int, str) result = pyqtSignal(str, bool, int, int)
error = pyqtSignal(str, str)
num: int = 0 num: int = 0
total: int = 1 # set default to 1 to avoid DivisionByZero before it is initialized total: int = 1 # set default to 1 to avoid DivisionByZero before it is initialized
@ -103,25 +101,22 @@ class VerifyWorker(QRunnable):
self.app_name = app_name self.app_name = app_name
def status_callback(self, num: int, total: int, percentage: float, speed: float): def status_callback(self, num: int, total: int, percentage: float, speed: float):
self.signals.status.emit(num, total, self.app_name) self.signals.status.emit(self.app_name, num, total, percentage, speed)
def run(self): def run(self):
args = Namespace(app_name=self.app_name, args = LgndrVerifyGameArgs(app_name=self.app_name,
callback=self.status_callback) verify_stdout=self.status_callback)
try: try:
# TODO: offer this as an alternative when manifest doesn't exist # TODO: offer this as an alternative when manifest doesn't exist
# TODO: requires the client to be online. To do it this way, we need to # TODO: requires the client to be online. To do it this way, we need to
# TODO: somehow detect the error and offer a dialog in which case `verify_games` is # TODO: somehow detect the error and offer a dialog in which case `verify_games` is
# TODO: re-run with `repair_mode` and `repair_online` # TODO: re-run with `repair_mode` and `repair_online`
self.cli.verify_game(args, print_command=False, repair_mode=True, repair_online=True) success, failed, missing = self.cli.verify_game(
# self.cli.verify_game(args, print_command=False) args, print_command=False, repair_mode=True, repair_online=True)
self.signals.summary.emit(0, 0, self.app_name) # success, failed, missing = self.cli.verify_game(args, print_command=False)
self.signals.result.emit(self.app_name, success, failed, missing)
except LgndrException as ret: except LgndrException as ret:
r = parse('Verification failed, {:d} file(s) corrupted, {:d} file(s) are missing.', ret.message) self.signals.error.emit(self.app_name, ret.message)
if r is None:
raise ret
else:
self.signals.summary.emit(r[0], r[1], self.app_name)
# FIXME: lk: ah ef me sideways, we can't even import this thing properly # FIXME: lk: ah ef me sideways, we can't even import this thing properly