Merge pull request #197 from loathingKernel/apes_together_strong
Implement shim legendary classes with overloaded/modified functions
This commit is contained in:
commit
04cd397a2f
4
.gitmodules
vendored
4
.gitmodules
vendored
|
@ -1,4 +0,0 @@
|
|||
[submodule "legendary"]
|
||||
path = rare/legendary
|
||||
url = https://github.com/dummerle/legendary
|
||||
branch = rare
|
|
@ -23,3 +23,13 @@ To contribute fork the repository and clone **your** repo: `git clone https://gi
|
|||
and upload it to GitHub with `git commit -m "message"` and `git push`. Some IDEs like PyCharm can do this automatically.
|
||||
|
||||
If you uploaded your changes, create a pull request
|
||||
|
||||
# Code Style Guidelines
|
||||
|
||||
## Signals and threads
|
||||
|
||||
## Function naming
|
||||
|
||||
## UI Classes
|
||||
|
||||
### Widget and Layout naming
|
|
@ -34,3 +34,13 @@ start = "rare.__main__:main"
|
|||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42", "wheel", "nuitka", "toml"]
|
||||
build-backend = "nuitka.distutils.Build"
|
||||
|
||||
[nuitka]
|
||||
show-scons = true
|
||||
enable-plugin = pyqt5,anti-bloat
|
||||
show-anti-bloat-changes = true
|
||||
nofollow-import-to = ["*.tests", "*.distutils"]
|
||||
|
|
|
@ -13,8 +13,8 @@ def main():
|
|||
multiprocessing.freeze_support()
|
||||
|
||||
# insert legendary for installed via pip/setup.py submodule to path
|
||||
if not __name__ == "__main__":
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "legendary"))
|
||||
# if not __name__ == "__main__":
|
||||
# sys.path.insert(0, os.path.join(os.path.dirname(__file__), "legendary"))
|
||||
|
||||
# CLI Options
|
||||
parser = ArgumentParser()
|
||||
|
@ -70,20 +70,18 @@ def main():
|
|||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.desktop_shortcut:
|
||||
from rare.utils import utils
|
||||
if args.desktop_shortcut or args.startmenu_shortcut:
|
||||
from rare.utils.misc import create_desktop_link
|
||||
|
||||
if args.desktop_shortcut:
|
||||
create_desktop_link(type_of_link="desktop", for_rare=True)
|
||||
|
||||
if args.startmenu_shortcut:
|
||||
create_desktop_link(type_of_link="start_menu", for_rare=True)
|
||||
|
||||
utils.create_desktop_link(type_of_link="desktop", for_rare=True)
|
||||
print("Link created")
|
||||
return
|
||||
|
||||
if args.startmenu_shortcut:
|
||||
from rare.utils import utils
|
||||
|
||||
utils.create_desktop_link(type_of_link="start_menu", for_rare=True)
|
||||
print("link created")
|
||||
return
|
||||
|
||||
if args.version:
|
||||
from rare import __version__, code_name
|
||||
|
||||
|
@ -123,10 +121,10 @@ def main():
|
|||
if __name__ == "__main__":
|
||||
# run from source
|
||||
# insert raw legendary submodule
|
||||
sys.path.insert(
|
||||
0, os.path.join(pathlib.Path(__file__).parent.absolute(), "legendary")
|
||||
)
|
||||
# sys.path.insert(
|
||||
# 0, os.path.join(pathlib.Path(__file__).parent.absolute(), "legendary")
|
||||
# )
|
||||
# insert source directory
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).parents[1].absolute()))
|
||||
#sys.path.insert(0, str(pathlib.Path(__file__).parents[1].absolute()))
|
||||
|
||||
main()
|
||||
|
|
17
rare/app.py
17
rare/app.py
|
@ -149,17 +149,25 @@ class App(RareApp):
|
|||
def start_app(self):
|
||||
for igame in self.core.get_installed_list():
|
||||
if not os.path.exists(igame.install_path):
|
||||
legendary_utils.uninstall(igame.app_name, self.core)
|
||||
# lk; since install_path is lost anyway, set keep_files to True
|
||||
# lk: to avoid spamming the log with "file not found" errors
|
||||
legendary_utils.uninstall_game(self.core, igame.app_name, keep_files=True)
|
||||
logger.info(f"Uninstalled {igame.title}, because no game files exist")
|
||||
continue
|
||||
if not os.path.exists(os.path.join(igame.install_path, igame.executable.replace("\\", "/").lstrip("/"))):
|
||||
# lk: games that don't have an override and can't find their executable due to case sensitivity
|
||||
# lk: will still erroneously require verification. This might need to be removed completely
|
||||
# lk: or be decoupled from the verification requirement
|
||||
if override_exe := self.core.lgd.config.get(igame.app_name, "override_exe", fallback=""):
|
||||
igame_executable = override_exe
|
||||
else:
|
||||
igame_executable = igame.executable
|
||||
if not os.path.exists(os.path.join(igame.install_path, igame_executable.replace("\\", "/").lstrip("/"))):
|
||||
igame.needs_verification = True
|
||||
self.core.lgd.set_installed_game(igame.app_name, igame)
|
||||
logger.info(f"{igame.title} needs verification")
|
||||
|
||||
self.mainwindow = MainWindow()
|
||||
self.launch_dialog.close()
|
||||
self.tray_icon = TrayIcon(self)
|
||||
self.tray_icon: TrayIcon = TrayIcon(self)
|
||||
self.tray_icon.exit_action.triggered.connect(self.exit_app)
|
||||
self.tray_icon.start_rare.triggered.connect(self.show_mainwindow)
|
||||
self.tray_icon.activated.connect(
|
||||
|
@ -226,6 +234,7 @@ class App(RareApp):
|
|||
self.mainwindow.hide()
|
||||
threadpool = QThreadPool.globalInstance()
|
||||
threadpool.waitForDone()
|
||||
self.core.exit()
|
||||
if self.mainwindow is not None:
|
||||
self.mainwindow.close()
|
||||
if self.tray_icon is not None:
|
||||
|
|
|
@ -1,28 +1,32 @@
|
|||
import os
|
||||
import platform
|
||||
import platform as pf
|
||||
import sys
|
||||
from multiprocessing import Queue as MPQueue
|
||||
from typing import Tuple
|
||||
from typing import Tuple, List, Union, Optional
|
||||
|
||||
from PyQt5.QtCore import Qt, QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtGui import QCloseEvent, QKeyEvent
|
||||
from PyQt5.QtWidgets import QDialog, QFileDialog, QCheckBox
|
||||
from legendary.core import LegendaryCore
|
||||
from legendary.models.downloading import ConditionCheckResult
|
||||
from legendary.models.game import Game
|
||||
from legendary.utils.selective_dl import games
|
||||
from legendary.utils.selective_dl import get_sdl_appname
|
||||
|
||||
from rare.lgndr.cli import LegendaryCLI
|
||||
from rare.lgndr.api_arguments import LgndrInstallGameArgs
|
||||
from rare.lgndr.api_exception import LgndrException
|
||||
from rare.lgndr.api_monkeys import LgndrIndirectStatus
|
||||
from rare.lgndr.core import LegendaryCore
|
||||
from rare.shared import LegendaryCoreSingleton, ApiResultsSingleton, ArgumentsSingleton
|
||||
from rare.ui.components.dialogs.install_dialog import Ui_InstallDialog
|
||||
from rare.utils.extra_widgets import PathEdit
|
||||
from rare.utils.models import InstallDownloadModel, InstallQueueItemModel
|
||||
from rare.utils.utils import get_size
|
||||
from rare.models.install import InstallDownloadModel, InstallQueueItemModel
|
||||
from rare.utils.misc import get_size
|
||||
from rare.utils import config_helper
|
||||
|
||||
|
||||
class InstallDialog(QDialog, Ui_InstallDialog):
|
||||
result_ready = pyqtSignal(InstallQueueItemModel)
|
||||
|
||||
def __init__(self, dl_item: InstallQueueItemModel, update=False, silent=False, parent=None):
|
||||
def __init__(self, dl_item: InstallQueueItemModel, update=False, repair=False, silent=False, parent=None):
|
||||
super(InstallDialog, self).__init__(parent)
|
||||
self.setupUi(self)
|
||||
self.setAttribute(Qt.WA_DeleteOnClose, True)
|
||||
|
@ -31,7 +35,6 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
|||
self.core = LegendaryCoreSingleton()
|
||||
self.api_results = ApiResultsSingleton()
|
||||
self.dl_item = dl_item
|
||||
self.dl_item.status_q = MPQueue()
|
||||
self.app_name = self.dl_item.options.app_name
|
||||
self.game = (
|
||||
self.core.get_game(self.app_name)
|
||||
|
@ -42,6 +45,7 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
|||
self.game_path = self.game.metadata.get("customAttributes", {}).get("FolderName", {}).get("value", "")
|
||||
|
||||
self.update = update
|
||||
self.repair = repair
|
||||
self.silent = silent
|
||||
|
||||
self.options_changed = False
|
||||
|
@ -84,18 +88,20 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
|||
platforms.append("Mac")
|
||||
self.platform_combo_box.addItems(platforms)
|
||||
self.platform_combo_box.currentIndexChanged.connect(lambda: self.option_changed(None))
|
||||
self.platform_combo_box.currentIndexChanged.connect(lambda: self.error_box())
|
||||
self.platform_combo_box.currentIndexChanged.connect(
|
||||
lambda i: self.error_box(
|
||||
self.tr("Warning"),
|
||||
self.tr("You will not be able to run the Game if you choose {}").format(
|
||||
self.tr("You will not be able to run the game if you select <b>{}</b> as platform").format(
|
||||
self.platform_combo_box.itemText(i)
|
||||
),
|
||||
)
|
||||
if (self.platform_combo_box.currentText() == "Mac" and platform.system() != "Darwin")
|
||||
if (self.platform_combo_box.currentText() == "Mac" and pf.system() != "Darwin")
|
||||
else None
|
||||
)
|
||||
self.platform_combo_box.currentTextChanged.connect(self.setup_sdl_list)
|
||||
|
||||
if platform.system() == "Darwin" and "Mac" in platforms:
|
||||
if pf.system() == "Darwin" and "Mac" in platforms:
|
||||
self.platform_combo_box.setCurrentIndex(platforms.index("Mac"))
|
||||
|
||||
self.max_workers_spin.setValue(self.core.lgd.config.getint("Legendary", "max_workers", fallback=0))
|
||||
|
@ -109,22 +115,10 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
|||
self.ignore_space_check.stateChanged.connect(self.option_changed)
|
||||
self.download_only_check.stateChanged.connect(lambda: self.non_reload_option_changed("download_only"))
|
||||
self.shortcut_cb.stateChanged.connect(lambda: self.non_reload_option_changed("shortcut"))
|
||||
self.sdl_list_checks = list()
|
||||
try:
|
||||
for key, info in games[self.app_name].items():
|
||||
cb = QDataCheckBox(info["name"], info["tags"])
|
||||
if key == "__required":
|
||||
self.dl_item.options.sdl_list.extend(info["tags"])
|
||||
cb.setChecked(True)
|
||||
cb.setDisabled(True)
|
||||
self.sdl_list_layout.addWidget(cb)
|
||||
self.sdl_list_checks.append(cb)
|
||||
self.sdl_list_frame.resize(self.sdl_list_frame.minimumSize())
|
||||
for cb in self.sdl_list_checks:
|
||||
cb.stateChanged.connect(self.option_changed)
|
||||
except (KeyError, AttributeError):
|
||||
self.sdl_list_frame.setVisible(False)
|
||||
self.sdl_list_label.setVisible(False)
|
||||
|
||||
self.sdl_list_cbs: List[TagCheckBox] = []
|
||||
self.config_tags: Optional[List[str]] = None
|
||||
self.setup_sdl_list("Mac" if pf.system() == "Darwin" and "Mac" in platforms else "Windows")
|
||||
|
||||
self.install_button.setEnabled(False)
|
||||
|
||||
|
@ -138,7 +132,7 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
|||
self.shortcut_cb.setVisible(False)
|
||||
self.shortcut_lbl.setVisible(False)
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
if pf.system() == "Darwin":
|
||||
self.shortcut_cb.setDisabled(True)
|
||||
self.shortcut_cb.setChecked(False)
|
||||
self.shortcut_cb.setToolTip(self.tr("Creating a shortcut is not supported on MacOS"))
|
||||
|
@ -153,7 +147,7 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
|||
self.verify_button.clicked.connect(self.verify_clicked)
|
||||
self.install_button.clicked.connect(self.install_clicked)
|
||||
|
||||
self.resize(self.minimumSize())
|
||||
self.install_dialog_layout.setSizeConstraint(self.install_dialog_layout.SetFixedSize)
|
||||
|
||||
def execute(self):
|
||||
if self.silent:
|
||||
|
@ -163,21 +157,54 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
|||
self.verify_clicked()
|
||||
self.show()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setup_sdl_list(self, platform="Windows"):
|
||||
for cb in self.sdl_list_cbs:
|
||||
cb.disconnect()
|
||||
cb.deleteLater()
|
||||
self.sdl_list_cbs.clear()
|
||||
|
||||
if config_tags := self.core.lgd.config.get(self.game.app_name, 'install_tags', fallback=None):
|
||||
self.config_tags = config_tags.split(",")
|
||||
config_disable_sdl = self.core.lgd.config.getboolean(self.game.app_name, 'disable_sdl', fallback=False)
|
||||
sdl_name = get_sdl_appname(self.game.app_name)
|
||||
if not config_disable_sdl and sdl_name is not None:
|
||||
# FIXME: this should be updated whenever platform changes
|
||||
sdl_data = self.core.get_sdl_data(sdl_name, platform=platform)
|
||||
if sdl_data:
|
||||
for tag, info in sdl_data.items():
|
||||
cb = TagCheckBox(info["name"], info["tags"])
|
||||
if tag == "__required":
|
||||
cb.setChecked(True)
|
||||
cb.setDisabled(True)
|
||||
if self.config_tags is not None:
|
||||
if all(elem in self.config_tags for elem in info["tags"]):
|
||||
cb.setChecked(True)
|
||||
self.sdl_list_layout.addWidget(cb)
|
||||
self.sdl_list_cbs.append(cb)
|
||||
self.sdl_list_frame.resize(self.sdl_list_frame.minimumSize())
|
||||
for cb in self.sdl_list_cbs:
|
||||
cb.stateChanged.connect(self.option_changed)
|
||||
else:
|
||||
self.sdl_list_frame.setVisible(False)
|
||||
self.sdl_list_label.setVisible(False)
|
||||
|
||||
def get_options(self):
|
||||
self.dl_item.options.base_path = self.install_dir_edit.text() if not self.update else None
|
||||
|
||||
self.dl_item.options.max_workers = self.max_workers_spin.value()
|
||||
self.dl_item.options.max_shm = self.max_memory_spin.value()
|
||||
self.dl_item.options.dl_optimizations = self.dl_optimizations_check.isChecked()
|
||||
self.dl_item.options.shared_memory = self.max_memory_spin.value()
|
||||
self.dl_item.options.order_opt = self.dl_optimizations_check.isChecked()
|
||||
self.dl_item.options.force = self.force_download_check.isChecked()
|
||||
self.dl_item.options.ignore_space_req = self.ignore_space_check.isChecked()
|
||||
self.dl_item.options.ignore_space = self.ignore_space_check.isChecked()
|
||||
self.dl_item.options.no_install = self.download_only_check.isChecked()
|
||||
self.dl_item.options.platform = self.platform_combo_box.currentText()
|
||||
self.dl_item.options.sdl_list = [""]
|
||||
for cb in self.sdl_list_checks:
|
||||
if data := cb.isChecked():
|
||||
# noinspection PyTypeChecker
|
||||
self.dl_item.options.sdl_list.extend(data)
|
||||
if self.sdl_list_cbs:
|
||||
self.dl_item.options.install_tag = [""]
|
||||
for cb in self.sdl_list_cbs:
|
||||
if data := cb.isChecked():
|
||||
# noinspection PyTypeChecker
|
||||
self.dl_item.options.install_tag.extend(data)
|
||||
|
||||
def get_download_info(self):
|
||||
self.dl_item.download = None
|
||||
|
@ -218,6 +245,12 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
|||
self.dl_item.options.install_preqs = self.install_preqs_check.isChecked()
|
||||
|
||||
def cancel_clicked(self):
|
||||
if self.config_tags:
|
||||
config_helper.add_option(self.game.app_name, 'install_tags', ','.join(self.config_tags))
|
||||
else:
|
||||
# lk: this is purely for cleaning any install tags we might have added erroneously to the config
|
||||
config_helper.remove_option(self.game.app_name, 'install_tags')
|
||||
|
||||
self.dl_item.download = None
|
||||
self.reject_close = False
|
||||
self.close()
|
||||
|
@ -243,7 +276,7 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
|||
self.cancel_button.setEnabled(True)
|
||||
if self.silent:
|
||||
self.close()
|
||||
if platform.system() == "Windows" or ArgumentsSingleton().debug:
|
||||
if pf.system() == "Windows" or ArgumentsSingleton().debug:
|
||||
if dl_item.igame.prereq_info and not dl_item.igame.prereq_info.get("installed", False):
|
||||
self.install_preqs_check.setVisible(True)
|
||||
self.install_preqs_lbl.setVisible(True)
|
||||
|
@ -299,59 +332,34 @@ class InstallInfoWorker(QRunnable):
|
|||
self.signals = InstallInfoWorker.Signals()
|
||||
self.core = core
|
||||
self.dl_item = dl_item
|
||||
self.is_overlay_install = self.dl_item.options.overlay
|
||||
self.game = game
|
||||
|
||||
@pyqtSlot()
|
||||
def run(self):
|
||||
try:
|
||||
if not self.is_overlay_install:
|
||||
download = InstallDownloadModel(
|
||||
*self.core.prepare_download(
|
||||
app_name=self.dl_item.options.app_name,
|
||||
base_path=self.dl_item.options.base_path,
|
||||
force=self.dl_item.options.force,
|
||||
no_install=self.dl_item.options.no_install,
|
||||
status_q=self.dl_item.status_q,
|
||||
max_shm=self.dl_item.options.max_shm,
|
||||
max_workers=self.dl_item.options.max_workers,
|
||||
# game_folder=,
|
||||
# disable_patching=,
|
||||
# override_manifest=,
|
||||
# override_old_manifest=,
|
||||
# override_base_url=,
|
||||
platform=self.dl_item.options.platform,
|
||||
# file_prefix_filter=,
|
||||
# file_exclude_filter=,
|
||||
# file_install_tag=,
|
||||
dl_optimizations=self.dl_item.options.dl_optimizations,
|
||||
# dl_timeout=,
|
||||
repair=self.dl_item.options.repair,
|
||||
# repair_use_latest=,
|
||||
ignore_space_req=self.dl_item.options.ignore_space_req,
|
||||
# disable_delta=,
|
||||
# override_delta_manifest=,
|
||||
# reset_sdl=,
|
||||
sdl_prompt=lambda app_name, title: self.dl_item.options.sdl_list,
|
||||
)
|
||||
if not self.dl_item.options.overlay:
|
||||
cli = LegendaryCLI(self.core)
|
||||
status = LgndrIndirectStatus()
|
||||
result = cli.install_game(
|
||||
LgndrInstallGameArgs(**self.dl_item.options.as_install_kwargs(), indirect_status=status)
|
||||
)
|
||||
|
||||
if result:
|
||||
download = InstallDownloadModel(*result)
|
||||
else:
|
||||
raise LgndrException(status.message)
|
||||
else:
|
||||
if not os.path.exists(path := self.dl_item.options.base_path):
|
||||
os.makedirs(path)
|
||||
|
||||
dlm, analysis, igame = self.core.prepare_overlay_install(
|
||||
path=self.dl_item.options.base_path,
|
||||
status_queue=self.dl_item.status_q,
|
||||
max_workers=self.dl_item.options.max_workers,
|
||||
force=self.dl_item.options.force,
|
||||
path=self.dl_item.options.base_path
|
||||
)
|
||||
|
||||
download = InstallDownloadModel(
|
||||
dlmanager=dlm,
|
||||
dlm=dlm,
|
||||
analysis=analysis,
|
||||
game=self.game,
|
||||
igame=igame,
|
||||
game=self.game,
|
||||
repair=False,
|
||||
repair_file="",
|
||||
res=ConditionCheckResult(), # empty
|
||||
|
@ -361,19 +369,21 @@ class InstallInfoWorker(QRunnable):
|
|||
self.signals.result.emit(download)
|
||||
else:
|
||||
self.signals.failed.emit("\n".join(str(i) for i in download.res.failures))
|
||||
except LgndrException as ret:
|
||||
self.signals.failed.emit(ret.message)
|
||||
except Exception as e:
|
||||
self.signals.failed.emit(str(e))
|
||||
self.signals.finished.emit()
|
||||
|
||||
|
||||
class QDataCheckBox(QCheckBox):
|
||||
def __init__(self, text, data=None, parent=None):
|
||||
super(QDataCheckBox, self).__init__(parent)
|
||||
class TagCheckBox(QCheckBox):
|
||||
def __init__(self, text, tags: List[str], parent=None):
|
||||
super(TagCheckBox, self).__init__(parent)
|
||||
self.setText(text)
|
||||
self.data = data
|
||||
self.tags = tags
|
||||
|
||||
def isChecked(self):
|
||||
if super(QDataCheckBox, self).isChecked():
|
||||
return self.data
|
||||
def isChecked(self) -> Union[bool, List[str]]:
|
||||
if super(TagCheckBox, self).isChecked():
|
||||
return self.tags
|
||||
else:
|
||||
return False
|
||||
|
|
|
@ -10,7 +10,7 @@ from rare.models.apiresults import ApiResults
|
|||
from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton, ApiResultsSingleton
|
||||
from rare.shared.image_manager import ImageManagerSingleton
|
||||
from rare.ui.components.dialogs.launch_dialog import Ui_LaunchDialog
|
||||
from rare.utils.utils import CloudWorker
|
||||
from rare.utils.misc import CloudWorker
|
||||
|
||||
logger = getLogger("Login")
|
||||
|
||||
|
|
|
@ -117,8 +117,9 @@ class LoginDialog(QDialog):
|
|||
self.close()
|
||||
else:
|
||||
raise ValueError("Login failed.")
|
||||
except ValueError as e:
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
self.core.lgd.invalidate_userdata()
|
||||
self.ui.next_button.setEnabled(False)
|
||||
self.logged_in = False
|
||||
QMessageBox.warning(self, "Error", str(e))
|
||||
QMessageBox.warning(None, self.tr("Login error"), str(e))
|
||||
|
|
|
@ -10,7 +10,7 @@ from legendary.utils import webview_login
|
|||
|
||||
from rare.ui.components.dialogs.login.browser_login import Ui_BrowserLogin
|
||||
from rare.utils.extra_widgets import IndicatorLineEdit
|
||||
from rare.utils.utils import icon
|
||||
from rare.utils.misc import icon
|
||||
|
||||
logger = getLogger("BrowserLogin")
|
||||
|
||||
|
|
|
@ -1,36 +1,38 @@
|
|||
from typing import Tuple
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog,
|
||||
QLabel,
|
||||
QVBoxLayout,
|
||||
QCheckBox,
|
||||
QFormLayout,
|
||||
QHBoxLayout,
|
||||
QPushButton,
|
||||
)
|
||||
|
||||
from legendary.models.game import Game
|
||||
from rare.utils.utils import icon
|
||||
from rare.utils.misc import icon
|
||||
|
||||
|
||||
class UninstallDialog(QDialog):
|
||||
def __init__(self, game: Game):
|
||||
super(UninstallDialog, self).__init__()
|
||||
self.setWindowTitle("Uninstall Game")
|
||||
self.info = 0
|
||||
self.setAttribute(Qt.WA_DeleteOnClose, True)
|
||||
self.layout = QVBoxLayout()
|
||||
self.setWindowTitle("Uninstall Game")
|
||||
layout = QVBoxLayout()
|
||||
self.info_text = QLabel(
|
||||
self.tr("Do you really want to uninstall {}").format(game.app_title)
|
||||
self.tr("Do you really want to uninstall <b>{}</b> ?").format(game.app_title)
|
||||
)
|
||||
self.layout.addWidget(self.info_text)
|
||||
self.keep_files = QCheckBox(self.tr("Keep Files"))
|
||||
self.form = QFormLayout()
|
||||
self.form.setContentsMargins(0, 10, 0, 10)
|
||||
self.form.addRow(QLabel(self.tr("Do you want to keep files?")), self.keep_files)
|
||||
self.layout.addLayout(self.form)
|
||||
layout.addWidget(self.info_text)
|
||||
self.keep_files = QCheckBox(self.tr("Keep game files?"))
|
||||
self.keep_config = QCheckBox(self.tr("Keep game configuation?"))
|
||||
form_layout = QVBoxLayout()
|
||||
form_layout.setContentsMargins(6, 6, 0, 6)
|
||||
form_layout.addWidget(self.keep_files)
|
||||
form_layout.addWidget(self.keep_config)
|
||||
layout.addLayout(form_layout)
|
||||
|
||||
self.button_layout = QHBoxLayout()
|
||||
button_layout = QHBoxLayout()
|
||||
self.ok_button = QPushButton(
|
||||
icon("ei.remove-circle", color="red"), self.tr("Uninstall")
|
||||
)
|
||||
|
@ -39,20 +41,22 @@ class UninstallDialog(QDialog):
|
|||
self.cancel_button = QPushButton(self.tr("Cancel"))
|
||||
self.cancel_button.clicked.connect(self.cancel)
|
||||
|
||||
self.button_layout.addStretch(1)
|
||||
self.button_layout.addWidget(self.ok_button)
|
||||
self.button_layout.addWidget(self.cancel_button)
|
||||
self.layout.addLayout(self.button_layout)
|
||||
self.setLayout(self.layout)
|
||||
button_layout.addWidget(self.ok_button)
|
||||
button_layout.addStretch(1)
|
||||
button_layout.addWidget(self.cancel_button)
|
||||
layout.addLayout(button_layout)
|
||||
self.setLayout(layout)
|
||||
|
||||
def get_information(self):
|
||||
self.options: Tuple[bool, bool, bool] = (False, False, False)
|
||||
|
||||
def get_options(self) -> Tuple[bool, bool, bool]:
|
||||
self.exec_()
|
||||
return self.info
|
||||
return self.options
|
||||
|
||||
def ok(self):
|
||||
self.info = {"keep_files": self.keep_files.isChecked()}
|
||||
self.options = (True, self.keep_files.isChecked(), self.keep_config.isChecked())
|
||||
self.close()
|
||||
|
||||
def cancel(self):
|
||||
self.info = 0
|
||||
self.options = (False, False, False)
|
||||
self.close()
|
||||
|
|
|
@ -2,14 +2,14 @@ from PyQt5.QtCore import QSize
|
|||
from PyQt5.QtWidgets import QMenu, QTabWidget, QWidget, QWidgetAction, QShortcut
|
||||
|
||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
|
||||
from rare.components.tabs.account import MiniWidget
|
||||
from rare.components.tabs.account import AccountWidget
|
||||
from rare.components.tabs.downloads import DownloadsTab
|
||||
from rare.components.tabs.games import GamesTab
|
||||
from rare.components.tabs.settings import SettingsTab
|
||||
from rare.components.tabs.settings.debug import DebugSettings
|
||||
from rare.components.tabs.shop import Shop
|
||||
from rare.components.tabs.tab_utils import MainTabBar, TabButtonWidget
|
||||
from rare.utils.utils import icon
|
||||
from rare.utils.misc import icon
|
||||
|
||||
|
||||
class TabWidget(QTabWidget):
|
||||
|
@ -54,9 +54,9 @@ class TabWidget(QTabWidget):
|
|||
self.addTab(self.account, "")
|
||||
self.setTabEnabled(disabled_tab + 1, False)
|
||||
|
||||
self.mini_widget = MiniWidget()
|
||||
self.account_widget = AccountWidget()
|
||||
account_action = QWidgetAction(self)
|
||||
account_action.setDefaultWidget(self.mini_widget)
|
||||
account_action.setDefaultWidget(self.account_widget)
|
||||
account_button = TabButtonWidget("mdi.account-circle", "Account", fallback_icon="fa.user")
|
||||
account_button.setMenu(QMenu())
|
||||
account_button.menu().addAction(account_action)
|
||||
|
|
|
@ -3,34 +3,33 @@ import webbrowser
|
|||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QMessageBox, QLabel, QPushButton
|
||||
|
||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
||||
from rare.utils.utils import icon
|
||||
from rare.utils.misc import icon
|
||||
|
||||
|
||||
class MiniWidget(QWidget):
|
||||
class AccountWidget(QWidget):
|
||||
def __init__(self):
|
||||
super(MiniWidget, self).__init__()
|
||||
self.layout = QVBoxLayout()
|
||||
super(AccountWidget, self).__init__()
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.signals = GlobalSignalsSingleton()
|
||||
self.layout.addWidget(QLabel("Account"))
|
||||
|
||||
username = self.core.lgd.userdata.get("display_name")
|
||||
if not username:
|
||||
username = "Offline"
|
||||
|
||||
self.layout.addWidget(QLabel(self.tr("Logged in as {}").format(username)))
|
||||
|
||||
self.open_browser = QPushButton(icon("fa.external-link"), self.tr("Account settings"))
|
||||
self.open_browser.clicked.connect(
|
||||
lambda: webbrowser.open(
|
||||
"https://www.epicgames.com/account/personal?productName=epicgames"
|
||||
)
|
||||
)
|
||||
self.layout.addWidget(self.open_browser)
|
||||
|
||||
self.logout_button = QPushButton(self.tr("Logout"))
|
||||
self.logout_button.clicked.connect(self.logout)
|
||||
self.layout.addWidget(self.logout_button)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(QLabel(self.tr("Account")))
|
||||
layout.addWidget(QLabel(self.tr("Logged in as <b>{}</b>").format(username)))
|
||||
layout.addWidget(self.open_browser)
|
||||
layout.addWidget(self.logout_button)
|
||||
|
||||
def logout(self):
|
||||
reply = QMessageBox.question(
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import datetime
|
||||
from logging import getLogger
|
||||
from typing import List, Dict
|
||||
from typing import List, Dict, Union
|
||||
|
||||
from PyQt5.QtCore import QThread, pyqtSignal, QSettings
|
||||
from PyQt5.QtCore import QThread, pyqtSignal, QSettings, pyqtSlot
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget,
|
||||
QMessageBox,
|
||||
|
@ -11,17 +11,17 @@ from PyQt5.QtWidgets import (
|
|||
QPushButton,
|
||||
QGroupBox,
|
||||
)
|
||||
|
||||
from legendary.core import LegendaryCore
|
||||
from legendary.models.downloading import UIUpdate
|
||||
from legendary.models.game import Game, InstalledGame
|
||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
||||
|
||||
from rare.components.dialogs.install_dialog import InstallDialog
|
||||
from rare.components.tabs.downloads.dl_queue_widget import DlQueueWidget, DlWidget
|
||||
from rare.components.tabs.downloads.download_thread import DownloadThread
|
||||
from rare.lgndr.downloading import UIUpdate
|
||||
from rare.models.install import InstallOptionsModel, InstallQueueItemModel
|
||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
||||
from rare.ui.components.tabs.downloads.downloads_tab import Ui_DownloadsTab
|
||||
from rare.utils.models import InstallOptionsModel, InstallQueueItemModel
|
||||
from rare.utils.utils import get_size
|
||||
from rare.utils.misc import get_size, create_desktop_link
|
||||
|
||||
logger = getLogger("Download")
|
||||
|
||||
|
@ -56,8 +56,8 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
|||
self.update_layout.addWidget(self.update_text)
|
||||
self.update_text.setVisible(len(updates) == 0)
|
||||
|
||||
for name in updates:
|
||||
self.add_update(self.core.get_installed_game(name))
|
||||
for app_name in updates:
|
||||
self.add_update(app_name)
|
||||
|
||||
self.queue_widget.item_removed.connect(self.queue_item_removed)
|
||||
|
||||
|
@ -66,7 +66,7 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
|||
self.signals.game_uninstalled.connect(self.remove_update)
|
||||
|
||||
self.signals.add_download.connect(
|
||||
lambda app_name: self.add_update(self.core.get_installed_game(app_name))
|
||||
lambda app_name: self.add_update(app_name)
|
||||
)
|
||||
self.signals.game_uninstalled.connect(self.game_uninstalled)
|
||||
|
||||
|
@ -77,14 +77,17 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
|||
w.update_button.setDisabled(False)
|
||||
w.update_with_settings.setDisabled(False)
|
||||
|
||||
def add_update(self, igame: InstalledGame):
|
||||
widget = UpdateWidget(self.core, igame, self)
|
||||
def add_update(self, app_name: str):
|
||||
if old_widget := self.update_widgets.get(app_name, False):
|
||||
old_widget.deleteLater()
|
||||
self.update_widgets.pop(app_name)
|
||||
widget = UpdateWidget(self.core, app_name, self)
|
||||
self.update_layout.addWidget(widget)
|
||||
self.update_widgets[igame.app_name] = widget
|
||||
self.update_widgets[app_name] = widget
|
||||
widget.update_signal.connect(self.get_install_options)
|
||||
if QSettings().value("auto_update", False, bool):
|
||||
self.get_install_options(
|
||||
InstallOptionsModel(app_name=igame.app_name, update=True, silent=True)
|
||||
InstallOptionsModel(app_name=app_name, update=True, silent=True)
|
||||
)
|
||||
widget.update_button.setDisabled(True)
|
||||
self.update_text.setVisible(False)
|
||||
|
@ -97,14 +100,14 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
|||
self.queue_widget.update_queue(self.dl_queue)
|
||||
break
|
||||
|
||||
# game has available update
|
||||
if app_name in self.update_widgets.keys():
|
||||
self.remove_update(app_name)
|
||||
|
||||
# if game is updating
|
||||
if self.active_game and self.active_game.app_name == app_name:
|
||||
self.stop_download()
|
||||
|
||||
# game has available update
|
||||
if app_name in self.update_widgets.keys():
|
||||
self.remove_update(app_name)
|
||||
|
||||
def remove_update(self, app_name):
|
||||
if w := self.update_widgets.get(app_name):
|
||||
w.deleteLater()
|
||||
|
@ -120,6 +123,7 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
|||
|
||||
def stop_download(self):
|
||||
self.thread.kill()
|
||||
self.kill_button.setEnabled(False)
|
||||
|
||||
def install_game(self, queue_item: InstallQueueItemModel):
|
||||
if self.active_game is None:
|
||||
|
@ -134,8 +138,8 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
|||
self.queue_widget.update_queue(self.dl_queue)
|
||||
self.active_game = queue_item.download.game
|
||||
self.thread = DownloadThread(self.core, queue_item)
|
||||
self.thread.status.connect(self.status)
|
||||
self.thread.statistics.connect(self.statistics)
|
||||
self.thread.ret_status.connect(self.status)
|
||||
self.thread.ui_update.connect(self.progress_update)
|
||||
self.thread.start()
|
||||
self.kill_button.setDisabled(False)
|
||||
self.analysis = queue_item.download.analysis
|
||||
|
@ -143,8 +147,16 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
|||
|
||||
self.signals.installation_started.emit(self.active_game.app_name)
|
||||
|
||||
def status(self, text):
|
||||
if text == "finish":
|
||||
@pyqtSlot(DownloadThread.ReturnStatus)
|
||||
def status(self, result: DownloadThread.ReturnStatus):
|
||||
if result.ret_code == result.ReturnCode.FINISHED:
|
||||
if result.shortcuts:
|
||||
if not create_desktop_link(result.app_name, self.core, "desktop"):
|
||||
# maybe add it to download summary, to show in finished downloads
|
||||
pass
|
||||
else:
|
||||
logger.info("Desktop shortcut written")
|
||||
|
||||
self.dl_name.setText(self.tr("Download finished. Reload library"))
|
||||
logger.info(f"Download finished: {self.active_game.app_title}")
|
||||
|
||||
|
@ -179,10 +191,10 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
|||
else:
|
||||
self.queue_widget.update_queue(self.dl_queue)
|
||||
|
||||
elif text[:5] == "error":
|
||||
QMessageBox.warning(self, "warn", f"Download error: {text[6:]}")
|
||||
elif result.ret_code == result.ReturnCode.ERROR:
|
||||
QMessageBox.warning(self, self.tr("Error"), f"Download error: {result.message}")
|
||||
|
||||
elif text == "stop":
|
||||
elif result.ret_code == result.ReturnCode.STOPPED:
|
||||
self.reset_infos()
|
||||
if w := self.update_widgets.get(self.active_game.app_name):
|
||||
w.update_button.setDisabled(False)
|
||||
|
@ -202,7 +214,7 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
|||
self.downloaded.setText("n/a")
|
||||
self.analysis = None
|
||||
|
||||
def statistics(self, ui_update: UIUpdate):
|
||||
def progress_update(self, ui_update: UIUpdate):
|
||||
self.progress_bar.setValue(
|
||||
100 * ui_update.total_downloaded // self.analysis.dl_size
|
||||
)
|
||||
|
@ -218,12 +230,16 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
|||
100 * ui_update.total_downloaded // self.analysis.dl_size
|
||||
)
|
||||
|
||||
def get_time(self, seconds: int) -> str:
|
||||
def get_time(self, seconds: Union[int, float]) -> str:
|
||||
return str(datetime.timedelta(seconds=seconds))
|
||||
|
||||
def on_install_dialog_closed(self, download_item: InstallQueueItemModel):
|
||||
if download_item:
|
||||
self.install_game(download_item)
|
||||
# lk: In case the download in comming from game verification/repair
|
||||
if w := self.update_widgets.get(download_item.options.app_name):
|
||||
w.update_button.setDisabled(True)
|
||||
w.update_with_settings.setDisabled(True)
|
||||
self.signals.set_main_tab_index.emit(1)
|
||||
else:
|
||||
if w := self.update_widgets.get(download_item.options.app_name):
|
||||
|
@ -241,19 +257,6 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
|||
install_dialog.result_ready.connect(self.on_install_dialog_closed)
|
||||
install_dialog.execute()
|
||||
|
||||
def start_download(self, download_item: InstallQueueItemModel):
|
||||
downloads = (
|
||||
len(self.downloadTab.dl_queue)
|
||||
+ len(self.downloadTab.update_widgets.keys())
|
||||
+ 1
|
||||
)
|
||||
self.setTabText(
|
||||
1, "Downloads" + ((" (" + str(downloads) + ")") if downloads != 0 else "")
|
||||
)
|
||||
self.setCurrentIndex(1)
|
||||
self.downloadTab.install_game(download_item)
|
||||
self.games_tab.start_download(download_item.options.app_name)
|
||||
|
||||
@property
|
||||
def is_download_active(self):
|
||||
return self.active_game is not None
|
||||
|
@ -262,37 +265,37 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
|
|||
class UpdateWidget(QWidget):
|
||||
update_signal = pyqtSignal(InstallOptionsModel)
|
||||
|
||||
def __init__(self, core: LegendaryCore, igame: InstalledGame, parent):
|
||||
def __init__(self, core: LegendaryCore, app_name: str, parent):
|
||||
super(UpdateWidget, self).__init__(parent=parent)
|
||||
self.core = core
|
||||
self.game = igame
|
||||
self.game: Game = core.get_game(app_name)
|
||||
self.igame: InstalledGame = self.core.get_installed_game(app_name)
|
||||
|
||||
self.layout = QVBoxLayout()
|
||||
self.title = QLabel(self.game.title)
|
||||
self.layout.addWidget(self.title)
|
||||
layout = QVBoxLayout()
|
||||
self.title = QLabel(self.igame.title)
|
||||
layout.addWidget(self.title)
|
||||
|
||||
self.update_button = QPushButton(self.tr("Update Game"))
|
||||
self.update_button.clicked.connect(lambda: self.update_game(True))
|
||||
self.update_with_settings = QPushButton("Update with settings")
|
||||
self.update_with_settings.clicked.connect(lambda: self.update_game(False))
|
||||
self.layout.addWidget(self.update_button)
|
||||
self.layout.addWidget(self.update_with_settings)
|
||||
self.layout.addWidget(
|
||||
layout.addWidget(self.update_button)
|
||||
layout.addWidget(self.update_with_settings)
|
||||
layout.addWidget(
|
||||
QLabel(
|
||||
self.tr("Version: ")
|
||||
+ self.game.version
|
||||
+ " -> "
|
||||
+ self.core.get_asset(
|
||||
self.game.app_name, self.game.platform, False
|
||||
).build_version
|
||||
self.tr("Version: <b>")
|
||||
+ self.igame.version
|
||||
+ "</b> >> <b>"
|
||||
+ self.game.app_version(self.igame.platform)
|
||||
+ "</b>"
|
||||
)
|
||||
)
|
||||
|
||||
self.setLayout(self.layout)
|
||||
self.setLayout(layout)
|
||||
|
||||
def update_game(self, auto: bool):
|
||||
self.update_button.setDisabled(True)
|
||||
self.update_with_settings.setDisabled(True)
|
||||
self.update_signal.emit(
|
||||
InstallOptionsModel(app_name=self.game.app_name, silent=auto)
|
||||
InstallOptionsModel(app_name=self.igame.app_name, silent=auto)
|
||||
) # True if settings
|
||||
|
|
|
@ -10,8 +10,8 @@ from PyQt5.QtWidgets import (
|
|||
QWidget,
|
||||
)
|
||||
|
||||
from rare.utils.models import InstallQueueItemModel
|
||||
from rare.utils.utils import icon
|
||||
from rare.models.install import InstallQueueItemModel
|
||||
from rare.utils.misc import icon
|
||||
|
||||
logger = getLogger("QueueWidget")
|
||||
|
||||
|
|
|
@ -1,209 +1,150 @@
|
|||
import os
|
||||
import platform
|
||||
import queue
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from logging import getLogger
|
||||
from queue import Empty
|
||||
from typing import List, Optional, Dict
|
||||
|
||||
import psutil
|
||||
from PyQt5.QtCore import QThread, pyqtSignal, QProcess
|
||||
from legendary.core import LegendaryCore
|
||||
from legendary.models.downloading import UIUpdate, WriterTask
|
||||
|
||||
from rare.shared import GlobalSignalsSingleton
|
||||
from rare.utils.models import InstallQueueItemModel
|
||||
from rare.utils.utils import create_desktop_link
|
||||
from rare.lgndr.api_monkeys import DLManagerSignals
|
||||
from rare.lgndr.cli import LegendaryCLI
|
||||
from rare.lgndr.downloading import UIUpdate
|
||||
from rare.models.install import InstallQueueItemModel
|
||||
from rare.shared import GlobalSignalsSingleton, ArgumentsSingleton
|
||||
|
||||
logger = getLogger("DownloadThread")
|
||||
|
||||
|
||||
class DownloadThread(QThread):
|
||||
status = pyqtSignal(str)
|
||||
statistics = pyqtSignal(UIUpdate)
|
||||
@dataclass
|
||||
class ReturnStatus:
|
||||
class ReturnCode(IntEnum):
|
||||
ERROR = 1
|
||||
STOPPED = 2
|
||||
FINISHED = 3
|
||||
|
||||
def __init__(self, core: LegendaryCore, queue_item: InstallQueueItemModel):
|
||||
app_name: str
|
||||
ret_code: ReturnCode = ReturnCode.ERROR
|
||||
message: str = ""
|
||||
dlcs: Optional[List[Dict]] = None
|
||||
sync_saves: bool = False
|
||||
tip_url: str = ""
|
||||
shortcuts: bool = False
|
||||
|
||||
ret_status = pyqtSignal(ReturnStatus)
|
||||
ui_update = pyqtSignal(UIUpdate)
|
||||
|
||||
def __init__(self, core: LegendaryCore, item: InstallQueueItemModel):
|
||||
super(DownloadThread, self).__init__()
|
||||
self.core = core
|
||||
self.signals = GlobalSignalsSingleton()
|
||||
self.dlm = queue_item.download.dlmanager
|
||||
self.no_install = queue_item.options.no_install
|
||||
self.status_q = queue_item.status_q
|
||||
self.igame = queue_item.download.igame
|
||||
self.repair = queue_item.download.repair
|
||||
self.repair_file = queue_item.download.repair_file
|
||||
self.queue_item = queue_item
|
||||
|
||||
self._kill = False
|
||||
self.core: LegendaryCore = core
|
||||
self.item: InstallQueueItemModel = item
|
||||
self.dlm_signals: DLManagerSignals = DLManagerSignals()
|
||||
|
||||
def run(self):
|
||||
start_time = time.time()
|
||||
dl_stopped = False
|
||||
cli = LegendaryCLI(self.core)
|
||||
self.item.download.dlm.logging_queue = cli.logging_queue
|
||||
self.item.download.dlm.proc_debug = ArgumentsSingleton().debug
|
||||
ret = DownloadThread.ReturnStatus(self.item.download.game.app_name)
|
||||
start_t = time.time()
|
||||
try:
|
||||
|
||||
self.dlm.start()
|
||||
self.item.download.dlm.start()
|
||||
time.sleep(1)
|
||||
while self.dlm.is_alive():
|
||||
if self._kill:
|
||||
self.status.emit("stop")
|
||||
logger.info("Download stopping...")
|
||||
|
||||
# The code below is a temporary solution.
|
||||
# It should be removed once legendary supports stopping downloads more gracefully.
|
||||
|
||||
self.dlm.running = False
|
||||
|
||||
# send conditions to unlock threads if they aren't already
|
||||
for cond in self.dlm.conditions:
|
||||
with cond:
|
||||
cond.notify()
|
||||
|
||||
# make sure threads are dead.
|
||||
for t in self.dlm.threads:
|
||||
t.join(timeout=5.0)
|
||||
if t.is_alive():
|
||||
logger.warning(f"Thread did not terminate! {repr(t)}")
|
||||
|
||||
# clean up all the queues, otherwise this process won't terminate properly
|
||||
for name, q in zip(
|
||||
(
|
||||
"Download jobs",
|
||||
"Writer jobs",
|
||||
"Download results",
|
||||
"Writer results",
|
||||
),
|
||||
(
|
||||
self.dlm.dl_worker_queue,
|
||||
self.dlm.writer_queue,
|
||||
self.dlm.dl_result_q,
|
||||
self.dlm.writer_result_q,
|
||||
),
|
||||
):
|
||||
logger.debug(f'Cleaning up queue "{name}"')
|
||||
try:
|
||||
while True:
|
||||
_ = q.get_nowait()
|
||||
except Empty:
|
||||
q.close()
|
||||
q.join_thread()
|
||||
except AttributeError:
|
||||
logger.warning(f"Queue {name} did not close")
|
||||
|
||||
if self.dlm.writer_queue:
|
||||
# cancel installation
|
||||
self.dlm.writer_queue.put_nowait(WriterTask("", kill=True))
|
||||
|
||||
# forcibly kill DL workers that are not actually dead yet
|
||||
for child in self.dlm.children:
|
||||
if child.exitcode is None:
|
||||
child.terminate()
|
||||
|
||||
if self.dlm.shared_memory:
|
||||
# close up shared memory
|
||||
self.dlm.shared_memory.close()
|
||||
self.dlm.shared_memory.unlink()
|
||||
self.dlm.shared_memory = None
|
||||
|
||||
self.dlm.kill()
|
||||
|
||||
# force kill any threads that are somehow still alive
|
||||
for proc in psutil.process_iter():
|
||||
# check whether the process name matches
|
||||
if (
|
||||
sys.platform in ["linux", "darwin"]
|
||||
and proc.name() == "DownloadThread"
|
||||
):
|
||||
proc.kill()
|
||||
elif (
|
||||
sys.platform == "win32"
|
||||
and proc.name() == "python.exe"
|
||||
and proc.create_time() >= start_time
|
||||
):
|
||||
proc.kill()
|
||||
|
||||
logger.info("Download stopped. It can be continued later.")
|
||||
dl_stopped = True
|
||||
while self.item.download.dlm.is_alive():
|
||||
try:
|
||||
if not dl_stopped:
|
||||
self.statistics.emit(self.status_q.get(timeout=1))
|
||||
self.ui_update.emit(self.item.download.dlm.status_queue.get(timeout=1.0))
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
self.dlm.join()
|
||||
|
||||
if self.dlm_signals.update:
|
||||
try:
|
||||
self.item.download.dlm.signals_queue.put(self.dlm_signals, block=False, timeout=1.0)
|
||||
except queue.Full:
|
||||
pass
|
||||
time.sleep(self.item.download.dlm.update_interval / 10)
|
||||
self.item.download.dlm.join()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Installation failed after {time.time() - start_time:.02f} seconds: {e}"
|
||||
)
|
||||
self.status.emit(f"error {e}")
|
||||
return
|
||||
|
||||
else:
|
||||
if dl_stopped:
|
||||
return
|
||||
self.status.emit("dl_finished")
|
||||
end_t = time.time()
|
||||
logger.info(f"Download finished in {end_t - start_time}s")
|
||||
game = self.core.get_game(self.igame.app_name)
|
||||
logger.error(f"Installation failed after {end_t - start_t:.02f} seconds.")
|
||||
logger.warning(f"The following exception occurred while waiting for the downloader to finish: {e!r}.")
|
||||
ret.ret_code = ret.ReturnCode.ERROR
|
||||
ret.message = f"{e!r}"
|
||||
self.ret_status.emit(ret)
|
||||
return
|
||||
else:
|
||||
end_t = time.time()
|
||||
if self.dlm_signals.kill is True:
|
||||
logger.info(f"Download stopped after {end_t - start_t:.02f} seconds.")
|
||||
ret.ret_code = ret.ReturnCode.STOPPED
|
||||
self.ret_status.emit(ret)
|
||||
return
|
||||
logger.info(f"Download finished in {end_t - start_t:.02f} seconds.")
|
||||
|
||||
if self.queue_item.options.overlay:
|
||||
ret.ret_code = ret.ReturnCode.FINISHED
|
||||
|
||||
if self.item.options.overlay:
|
||||
self.signals.overlay_installation_finished.emit()
|
||||
self.core.finish_overlay_install(self.igame)
|
||||
self.status.emit("finish")
|
||||
self.core.finish_overlay_install(self.item.download.igame)
|
||||
self.ret_status.emit(ret)
|
||||
return
|
||||
|
||||
if not self.no_install:
|
||||
postinstall = self.core.install_game(self.igame)
|
||||
if not self.item.options.no_install:
|
||||
postinstall = self.core.install_game(self.item.download.igame)
|
||||
if postinstall:
|
||||
self._handle_postinstall(postinstall, self.igame)
|
||||
# LegendaryCLI(self.core)._handle_postinstall(
|
||||
# postinstall,
|
||||
# self.item.download.igame,
|
||||
# False,
|
||||
# self.item.options.install_preqs,
|
||||
# )
|
||||
self._handle_postinstall(postinstall, self.item.download.igame)
|
||||
|
||||
dlcs = self.core.get_dlc_for_game(self.igame.app_name)
|
||||
if dlcs:
|
||||
print("The following DLCs are available for this game:")
|
||||
dlcs = self.core.get_dlc_for_game(self.item.download.igame.app_name)
|
||||
if dlcs and not self.item.options.skip_dlcs:
|
||||
ret.dlcs = []
|
||||
for dlc in dlcs:
|
||||
print(
|
||||
f" - {dlc.app_title} (App name: {dlc.app_name}, version: {dlc.app_version})"
|
||||
ret.dlcs.append(
|
||||
{
|
||||
"app_name": dlc.app_name,
|
||||
"app_title": dlc.app_title,
|
||||
"app_version": dlc.app_version(self.item.options.platform),
|
||||
}
|
||||
)
|
||||
print(
|
||||
"Manually installing DLCs works the same; just use the DLC app name instead."
|
||||
)
|
||||
|
||||
# install_dlcs = QMessageBox.question(self, "", "Do you want to install the prequisites", QMessageBox.Yes|QMessageBox.No) == QMessageBox.Yes
|
||||
# TODO
|
||||
if game.supports_cloud_saves and not game.is_dlc:
|
||||
logger.info(
|
||||
'This game supports cloud saves, syncing is handled by the "sync-saves" command.'
|
||||
)
|
||||
logger.info(
|
||||
f'To download saves for this game run "legendary sync-saves {game.app_name}"'
|
||||
)
|
||||
old_igame = self.core.get_installed_game(game.app_name)
|
||||
if old_igame and self.repair and os.path.exists(self.repair_file):
|
||||
if old_igame.needs_verification:
|
||||
old_igame.needs_verification = False
|
||||
self.core.install_game(old_igame)
|
||||
if (
|
||||
self.item.download.game.supports_cloud_saves
|
||||
or self.item.download.game.supports_mac_cloud_saves
|
||||
) and not self.item.download.game.is_dlc:
|
||||
ret.sync_saves = True
|
||||
|
||||
logger.debug("Removing repair file.")
|
||||
os.remove(self.repair_file)
|
||||
if old_igame and old_igame.install_tags != self.igame.install_tags:
|
||||
old_igame.install_tags = self.igame.install_tags
|
||||
self.logger.info("Deleting now untagged files.")
|
||||
self.core.uninstall_tag(old_igame)
|
||||
self.core.install_game(old_igame)
|
||||
# show tip again after installation finishes so users hopefully actually see it
|
||||
if tip_url := self.core.get_game_tip(self.item.download.igame.app_name):
|
||||
ret.tip_url = tip_url
|
||||
|
||||
if not self.queue_item.options.update and self.queue_item.options.create_shortcut:
|
||||
if not create_desktop_link(self.queue_item.options.app_name, self.core, "desktop"):
|
||||
# maybe add it to download summary, to show in finished downloads
|
||||
pass
|
||||
else:
|
||||
logger.info("Desktop shortcut written")
|
||||
LegendaryCLI(self.core).install_game_cleanup(
|
||||
self.item.download.game,
|
||||
self.item.download.igame,
|
||||
self.item.download.repair,
|
||||
self.item.download.repair_file,
|
||||
)
|
||||
|
||||
self.status.emit("finish")
|
||||
if not self.item.options.update and self.item.options.create_shortcut:
|
||||
ret.shortcuts = True
|
||||
|
||||
self.ret_status.emit(ret)
|
||||
|
||||
def _handle_postinstall(self, postinstall, igame):
|
||||
logger.info(f"Postinstall info: {postinstall}")
|
||||
logger.info("This game lists the following prequisites to be installed:")
|
||||
logger.info(f'- {postinstall["name"]}: {" ".join((postinstall["path"], postinstall["args"]))}')
|
||||
if platform.system() == "Windows":
|
||||
if self.queue_item.options.install_preqs:
|
||||
if not self.item.options.install_preqs:
|
||||
logger.info("Marking prerequisites as installed...")
|
||||
self.core.prereq_installed(self.item.download.igame.app_name)
|
||||
else:
|
||||
logger.info("Launching prerequisite executable..")
|
||||
self.core.prereq_installed(igame.app_name)
|
||||
req_path, req_exec = os.path.split(postinstall["path"])
|
||||
work_dir = os.path.join(igame.install_path, req_path)
|
||||
|
@ -211,15 +152,14 @@ class DownloadThread(QThread):
|
|||
proc = QProcess()
|
||||
proc.setProcessChannelMode(QProcess.MergedChannels)
|
||||
proc.readyReadStandardOutput.connect(
|
||||
lambda: logger.debug(
|
||||
str(proc.readAllStandardOutput().data(), "utf-8", "ignore")
|
||||
))
|
||||
proc.start(fullpath, postinstall.get("args", []))
|
||||
lambda: logger.debug(str(proc.readAllStandardOutput().data(), "utf-8", "ignore"))
|
||||
)
|
||||
proc.setNativeArguments(postinstall.get("args", []))
|
||||
proc.setWorkingDirectory(work_dir)
|
||||
proc.start(fullpath)
|
||||
proc.waitForFinished() # wait, because it is inside the thread
|
||||
else:
|
||||
self.core.prereq_installed(self.igame.app_name)
|
||||
else:
|
||||
logger.info("Automatic installation not available on Linux.")
|
||||
|
||||
def kill(self):
|
||||
self._kill = True
|
||||
self.dlm_signals.kill = True
|
||||
|
|
|
@ -139,6 +139,16 @@ class GamesTab(QStackedWidget):
|
|||
self.view_stack.setCurrentWidget(self.icon_view_scroll)
|
||||
|
||||
self.head_bar.search_bar.textChanged.connect(lambda x: self.filter_games("", x))
|
||||
self.head_bar.search_bar.textChanged.connect(
|
||||
lambda x: self.icon_view_scroll.verticalScrollBar().setSliderPosition(
|
||||
self.icon_view_scroll.verticalScrollBar().minimum()
|
||||
)
|
||||
)
|
||||
self.head_bar.search_bar.textChanged.connect(
|
||||
lambda x: self.list_view_scroll.verticalScrollBar().setSliderPosition(
|
||||
self.list_view_scroll.verticalScrollBar().minimum()
|
||||
)
|
||||
)
|
||||
self.head_bar.filterChanged.connect(self.filter_games)
|
||||
self.head_bar.refresh_list.clicked.connect(self.update_list)
|
||||
self.head_bar.view.toggled.connect(self.toggle_view)
|
||||
|
@ -320,8 +330,8 @@ class GamesTab(QStackedWidget):
|
|||
visible = True
|
||||
|
||||
if (
|
||||
search_text not in widget.game.app_name.lower()
|
||||
and search_text not in widget.game.app_title.lower()
|
||||
search_text.lower() not in widget.game.app_name.lower()
|
||||
and search_text.lower() not in widget.game.app_title.lower()
|
||||
):
|
||||
opacity = 0.25
|
||||
else:
|
||||
|
@ -345,7 +355,7 @@ class GamesTab(QStackedWidget):
|
|||
# lk: it sorts by installed then by title
|
||||
installing_widget = self.icon_view.layout().remove(type(self.installing_widget).__name__)
|
||||
if sort_by:
|
||||
self.icon_view.layout().sort(lambda x: (sort_by not in x.widget().game.app_title.lower(),))
|
||||
self.icon_view.layout().sort(lambda x: (sort_by.lower() not in x.widget().game.app_title.lower(),))
|
||||
else:
|
||||
self.icon_view.layout().sort(
|
||||
lambda x: (
|
||||
|
|
|
@ -11,7 +11,7 @@ from legendary.core import LegendaryCore
|
|||
from legendary.models.game import SaveGameStatus, InstalledGame, SaveGameFile
|
||||
from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton, ApiResultsSingleton
|
||||
from rare.ui.components.dialogs.sync_save_dialog import Ui_SyncSaveDialog
|
||||
from rare.utils.utils import icon
|
||||
from rare.utils.misc import icon
|
||||
|
||||
logger = getLogger("Cloud Saves")
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
|||
from rare.shared.image_manager import ImageManagerSingleton, ImageSize
|
||||
from rare.ui.components.tabs.games.game_info.game_dlc import Ui_GameDlc
|
||||
from rare.ui.components.tabs.games.game_info.game_dlc_widget import Ui_GameDlcWidget
|
||||
from rare.utils.models import InstallOptionsModel
|
||||
from rare.models.install import InstallOptionsModel
|
||||
from rare.widgets.image_widget import ImageWidget
|
||||
|
||||
|
||||
|
|
|
@ -37,9 +37,9 @@ from rare.shared.image_manager import ImageManagerSingleton, ImageSize
|
|||
from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo
|
||||
from rare.utils.extra_widgets import PathEdit
|
||||
from rare.utils.legendary_utils import VerifyWorker
|
||||
from rare.utils.models import InstallOptionsModel
|
||||
from rare.models.install import InstallOptionsModel
|
||||
from rare.utils.steam_grades import SteamWorker
|
||||
from rare.utils.utils import get_size
|
||||
from rare.utils.misc import get_size
|
||||
from rare.widgets.image_widget import ImageWidget
|
||||
|
||||
logger = getLogger("GameInfo")
|
||||
|
@ -117,77 +117,111 @@ class GameInfo(QWidget, Ui_GameInfo):
|
|||
self.game_utils.update_list.emit(self.game.app_name)
|
||||
self.uninstalled.emit(self.game.app_name)
|
||||
|
||||
@pyqtSlot()
|
||||
def repair(self):
|
||||
repair_file = os.path.join(self.core.lgd.get_tmp_path(), f"{self.game.app_name}.repair")
|
||||
""" 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")
|
||||
if not os.path.exists(repair_file):
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"Warning",
|
||||
self.tr("Error - {}").format(self.igame.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)
|
||||
|
||||
def repair_game(self, igame: InstalledGame):
|
||||
game = self.core.get_game(igame.app_name)
|
||||
ans = False
|
||||
if igame.version != game.app_version(igame.platform):
|
||||
ans = QMessageBox.question(
|
||||
self,
|
||||
self.tr("Repair and update?"),
|
||||
self.tr(
|
||||
"There is an update for <b>{}</b> from <b>{}</b> to <b>{}</b>."
|
||||
"Do you want to update the game while repairing it?"
|
||||
).format(igame.title, igame.version, game.app_version(igame.platform)),
|
||||
) == QMessageBox.Yes
|
||||
self.signals.install_game.emit(
|
||||
InstallOptionsModel(app_name=self.game.app_name, repair=True, update=True)
|
||||
InstallOptionsModel(
|
||||
app_name=igame.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("Path does not exist")
|
||||
logger.error(f"Installation path {self.igame.install_path} for {self.igame.title} does not exist")
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"Warning",
|
||||
self.tr("Installation path of {} does not exist. Cannot verify").format(self.igame.title),
|
||||
self.tr("Error - {}").format(self.igame.title),
|
||||
self.tr("Installation path for <b>{}</b> does not exist. Cannot continue.").format(self.igame.title),
|
||||
)
|
||||
return
|
||||
self.verify_game(self.igame)
|
||||
|
||||
def verify_game(self, igame: InstalledGame):
|
||||
self.verify_widget.setCurrentIndex(1)
|
||||
verify_worker = VerifyWorker(self.game.app_name)
|
||||
verify_worker.signals.status.connect(self.verify_statistics)
|
||||
verify_worker.signals.summary.connect(self.finish_verify)
|
||||
verify_worker = VerifyWorker(igame.app_name)
|
||||
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.verify_progress.setValue(0)
|
||||
self.verify_threads[self.game.app_name] = verify_worker
|
||||
self.verify_threads[igame.app_name] = verify_worker
|
||||
self.verify_pool.start(verify_worker)
|
||||
self.move_button.setEnabled(False)
|
||||
|
||||
def verify_statistics(self, num, total, app_name):
|
||||
def verify_cleanup(self, app_name: str):
|
||||
self.verify_widget.setCurrentIndex(0)
|
||||
self.verify_threads.pop(app_name)
|
||||
self.move_button.setEnabled(True)
|
||||
self.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)
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
self.tr("Error - {}").format(igame.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.verify_progress.setValue(num * 100 // total)
|
||||
|
||||
def finish_verify(self, failed, missing, app_name):
|
||||
if failed == missing == 0:
|
||||
@pyqtSlot(str, bool, int, int)
|
||||
def verify_result(self, app_name, success, failed, missing):
|
||||
self.verify_cleanup(app_name)
|
||||
self.repair_button.setDisabled(success)
|
||||
igame = self.core.get_installed_game(app_name)
|
||||
if success:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Summary",
|
||||
"Game was verified successfully. No missing or corrupt files found",
|
||||
self.tr("Summary - {}").format(igame.title),
|
||||
self.tr("<b>{}</b> has been verified successfully. "
|
||||
"No missing or corrupt files found").format(igame.title),
|
||||
)
|
||||
igame = self.core.get_installed_game(app_name)
|
||||
if igame.needs_verification:
|
||||
igame.needs_verification = False
|
||||
self.core.lgd.set_installed_game(igame.app_name, igame)
|
||||
self.verification_finished.emit(igame)
|
||||
elif failed == missing == -1:
|
||||
QMessageBox.warning(self, "Warning", self.tr("Something went wrong"))
|
||||
|
||||
self.verification_finished.emit(igame)
|
||||
else:
|
||||
ans = QMessageBox.question(
|
||||
self,
|
||||
"Summary",
|
||||
self.tr("Summary - {}").format(igame.title),
|
||||
self.tr(
|
||||
"Verification failed, {} file(s) corrupted, {} file(s) are missing. Do you want to repair them?"
|
||||
"Verification failed, <b>{}</b> file(s) corrupted, <b>{}</b> file(s) are missing. "
|
||||
"Do you want to repair them?"
|
||||
).format(failed, missing),
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.Yes,
|
||||
)
|
||||
if ans == QMessageBox.Yes:
|
||||
self.signals.install_game.emit(
|
||||
InstallOptionsModel(app_name=app_name, repair=True, update=True)
|
||||
)
|
||||
self.verify_widget.setCurrentIndex(0)
|
||||
self.verify_threads.pop(app_name)
|
||||
self.move_button.setEnabled(True)
|
||||
self.verify_button.setEnabled(True)
|
||||
self.repair_game(igame)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def move_game(self, dest_path):
|
||||
|
@ -318,7 +352,9 @@ class GameInfo(QWidget, Ui_GameInfo):
|
|||
self.uninstall_button.setDisabled(False)
|
||||
self.verify_button.setDisabled(False)
|
||||
if not self.args.offline:
|
||||
self.repair_button.setDisabled(False)
|
||||
self.repair_button.setDisabled(
|
||||
not os.path.exists(os.path.join(self.core.lgd.get_tmp_path(), f"{self.igame.app_name}.repair"))
|
||||
)
|
||||
self.game_actions_stack.setCurrentIndex(0)
|
||||
|
||||
try:
|
||||
|
|
|
@ -10,7 +10,7 @@ from rare.components.tabs.settings import DefaultGameSettings
|
|||
from rare.components.tabs.settings.widgets.pre_launch import PreLaunchSettings
|
||||
from rare.utils import config_helper
|
||||
from rare.utils.extra_widgets import PathEdit
|
||||
from rare.utils.utils import icon, WineResolver, get_raw_save_path
|
||||
from rare.utils.misc import icon, WineResolver, get_raw_save_path
|
||||
|
||||
logger = getLogger("GameSettings")
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ from rare.shared.image_manager import ImageManagerSingleton, ImageSize
|
|||
from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo
|
||||
from rare.utils.extra_widgets import SideTabWidget
|
||||
from rare.utils.json_formatter import QJsonModel
|
||||
from rare.utils.models import InstallOptionsModel
|
||||
from rare.models.install import InstallOptionsModel
|
||||
from rare.utils.steam_grades import SteamWorker
|
||||
from rare.widgets.image_widget import ImageWidget
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ from rare.components.tabs.games import CloudSaveUtils
|
|||
from rare.game_launch_helper import message_models
|
||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
|
||||
from rare.utils import legendary_utils
|
||||
from rare.utils import utils
|
||||
from rare.utils import misc
|
||||
from rare.utils.meta import RareGameMeta
|
||||
|
||||
logger = getLogger("GameUtils")
|
||||
|
@ -24,7 +24,7 @@ class GameProcess(QObject):
|
|||
game_launched = pyqtSignal(str)
|
||||
tried_connections = 0
|
||||
|
||||
def __init__(self, app_name: str, on_startup=False, always_ask_sync: bool= False):
|
||||
def __init__(self, app_name: str, on_startup=False, always_ask_sync: bool = False):
|
||||
super(GameProcess, self).__init__()
|
||||
self.app_name = app_name
|
||||
self.on_startup = on_startup
|
||||
|
@ -152,7 +152,7 @@ class GameUtils(QObject):
|
|||
if not os.path.exists(igame.install_path):
|
||||
if QMessageBox.Yes == QMessageBox.question(
|
||||
None,
|
||||
"Uninstall",
|
||||
self.tr("Uninstall - {}").format(igame.title),
|
||||
self.tr(
|
||||
"Game files of {} do not exist. Remove it from installed games?"
|
||||
).format(igame.title),
|
||||
|
@ -164,10 +164,12 @@ class GameUtils(QObject):
|
|||
else:
|
||||
return False
|
||||
|
||||
infos = UninstallDialog(game).get_information()
|
||||
if infos == 0:
|
||||
proceed, keep_files, keep_config = UninstallDialog(game).get_options()
|
||||
if not proceed:
|
||||
return False
|
||||
legendary_utils.uninstall(game.app_name, self.core, infos)
|
||||
success, message = legendary_utils.uninstall_game(self.core, game.app_name, keep_files, keep_config)
|
||||
if not success:
|
||||
QMessageBox.warning(None, self.tr("Uninstall - {}").format(igame.title), message, QMessageBox.Close)
|
||||
self.signals.game_uninstalled.emit(app_name)
|
||||
return True
|
||||
|
||||
|
@ -206,7 +208,7 @@ class GameUtils(QObject):
|
|||
wine_pfx: str = None,
|
||||
ask_always_sync: bool = False,
|
||||
):
|
||||
executable = utils.get_rare_executable()
|
||||
executable = misc.get_rare_executable()
|
||||
executable, args = executable[0], executable[1:]
|
||||
args.extend([
|
||||
"start", app_name
|
||||
|
|
|
@ -9,7 +9,7 @@ from PyQt5.QtWidgets import QFrame, QMessageBox, QAction
|
|||
from rare.components.tabs.games.game_utils import GameUtils
|
||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
|
||||
from rare.shared.image_manager import ImageManagerSingleton, ImageSize
|
||||
from rare.utils.utils import create_desktop_link
|
||||
from rare.utils.misc import create_desktop_link
|
||||
from rare.widgets.image_widget import ImageWidget
|
||||
|
||||
logger = getLogger("Game")
|
||||
|
|
|
@ -9,7 +9,7 @@ from rare.components.tabs.games.game_widgets.base_installed_widget import (
|
|||
)
|
||||
from rare.shared import LegendaryCoreSingleton
|
||||
from rare.shared.image_manager import ImageSize
|
||||
from rare.utils.utils import icon
|
||||
from rare.utils.misc import icon
|
||||
from rare.widgets.elide_label import ElideLabel
|
||||
|
||||
logger = getLogger("GameWidgetInstalled")
|
||||
|
@ -43,7 +43,7 @@ class InstalledIconWidget(BaseInstalledWidget):
|
|||
minilayout.setSpacing(0)
|
||||
miniwidget.setLayout(minilayout)
|
||||
|
||||
self.title_label = ElideLabel(f"<h4>{self.game.app_title}</h4>", parent=miniwidget)
|
||||
self.title_label = ElideLabel(f"<b>{self.game.app_title}</b>", parent=miniwidget)
|
||||
self.title_label.setAlignment(Qt.AlignTop)
|
||||
self.title_label.setObjectName("game_widget")
|
||||
minilayout.addWidget(self.title_label, stretch=2)
|
||||
|
|
|
@ -6,8 +6,7 @@ from PyQt5.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout
|
|||
from rare.components.tabs.games.game_widgets.base_installed_widget import (
|
||||
BaseInstalledWidget,
|
||||
)
|
||||
from rare.utils.utils import get_size
|
||||
from rare.utils.utils import icon
|
||||
from rare.utils.misc import icon, get_size
|
||||
|
||||
logger = getLogger("GameWidget")
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ class UninstalledIconWidget(BaseUninstalledWidget):
|
|||
minilayout.setSpacing(0)
|
||||
miniwidget.setLayout(minilayout)
|
||||
|
||||
self.title_label = ElideLabel(f"<h4>{game.app_title}</h4>", parent=miniwidget)
|
||||
self.title_label = ElideLabel(f"<b>{game.app_title}</b>", parent=miniwidget)
|
||||
self.title_label.setAlignment(Qt.AlignTop)
|
||||
self.title_label.setObjectName("game_widget")
|
||||
minilayout.addWidget(self.title_label, stretch=2)
|
||||
|
|
|
@ -10,7 +10,7 @@ from qtawesome import IconWidget
|
|||
|
||||
from rare.shared import ApiResultsSingleton
|
||||
from rare.utils.extra_widgets import SelectViewWidget, ButtonLineEdit
|
||||
from rare.utils.utils import icon
|
||||
from rare.utils.misc import icon
|
||||
|
||||
|
||||
class GameListHeadBar(QWidget):
|
||||
|
|
|
@ -6,6 +6,7 @@ from typing import Tuple, Iterable, List
|
|||
from PyQt5.QtCore import Qt, QThreadPool, QRunnable, pyqtSlot, pyqtSignal
|
||||
from PyQt5.QtWidgets import QGroupBox, QListWidgetItem, QFileDialog, QMessageBox
|
||||
|
||||
from rare.lgndr.api_exception import LgndrException
|
||||
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_list_group import (
|
||||
|
@ -13,7 +14,7 @@ from rare.ui.components.tabs.games.import_sync.egl_sync_list_group import (
|
|||
)
|
||||
from rare.utils.extra_widgets import PathEdit
|
||||
from rare.utils.models import PathSpec
|
||||
from rare.utils.utils import WineResolver
|
||||
from rare.utils.misc import WineResolver
|
||||
|
||||
logger = getLogger("EGLSync")
|
||||
|
||||
|
@ -183,11 +184,18 @@ class EGLSyncListItem(QListWidgetItem):
|
|||
def is_checked(self) -> bool:
|
||||
return self.checkState() == Qt.Checked
|
||||
|
||||
def action(self) -> None:
|
||||
def action(self) -> str:
|
||||
error = ""
|
||||
if self.export:
|
||||
error = self.core.egl_export(self.game.app_name)
|
||||
try:
|
||||
self.core.egl_export(self.game.app_name)
|
||||
except LgndrException as ret:
|
||||
error = ret.message
|
||||
else:
|
||||
error = self.core.egl_import(self.game.app_name)
|
||||
try:
|
||||
self.core.egl_import(self.game.app_name)
|
||||
except LgndrException as ret:
|
||||
error = ret.message
|
||||
return error
|
||||
|
||||
@property
|
||||
|
@ -307,85 +315,3 @@ class EGLSyncWorker(QRunnable):
|
|||
def run(self):
|
||||
self.import_list.action()
|
||||
self.export_list.action()
|
||||
|
||||
|
||||
"""
|
||||
from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout, QLabel, QCheckBox, QPushButton, QDialog
|
||||
|
||||
|
||||
class DisableSyncDialog(QDialog):
|
||||
info = 1, False
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(DisableSyncDialog, self).__init__(parent=parent)
|
||||
self.layout = QVBoxLayout()
|
||||
|
||||
self.question = QLabel(self.tr("Do you really want to disable sync with Epic Games Store"))
|
||||
self.layout.addWidget(self.question)
|
||||
|
||||
self.remove_metadata = QCheckBox(self.tr("Remove metadata from installed games"))
|
||||
self.layout.addWidget(self.remove_metadata)
|
||||
|
||||
self.button_layout = QHBoxLayout()
|
||||
self.button_layout.addStretch(1)
|
||||
|
||||
self.ok_button = QPushButton(self.tr("Ok"))
|
||||
self.cancel_button = QPushButton(self.tr("Cancel"))
|
||||
|
||||
self.ok_button.clicked.connect(self.ok)
|
||||
self.cancel_button.clicked.connect(self.cancel)
|
||||
|
||||
self.button_layout.addWidget(self.ok_button)
|
||||
self.button_layout.addWidget(self.cancel_button)
|
||||
|
||||
self.layout.addStretch(1)
|
||||
self.layout.addLayout(self.button_layout)
|
||||
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def ok(self):
|
||||
self.info = 0, self.remove_metadata.isChecked()
|
||||
self.close()
|
||||
|
||||
def cancel(self):
|
||||
self.close()
|
||||
|
||||
def get_information(self):
|
||||
self.exec_()
|
||||
return self.info
|
||||
|
||||
|
||||
class EGLSyncItemWidget(QGroupBox):
|
||||
def __init__(self, game, export: bool, parent=None):
|
||||
super(EGLSyncItemWidget, self).__init__(parent=parent)
|
||||
self.layout = QHBoxLayout()
|
||||
self.export = export
|
||||
self.game = game
|
||||
if export:
|
||||
self.app_title_label = QLabel(game.title)
|
||||
else:
|
||||
title = self.core.get_game(game.app_name).app_title
|
||||
self.app_title_label = QLabel(title)
|
||||
self.layout.addWidget(self.app_title_label)
|
||||
self.button = QPushButton(self.tr("Export") if export else self.tr("Import"))
|
||||
|
||||
if export:
|
||||
self.button.clicked.connect(self.export_game)
|
||||
else:
|
||||
self.button.clicked.connect(self.import_game)
|
||||
|
||||
self.layout.addWidget(self.button)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def export_game(self):
|
||||
self.core.egl_export(self.game.app_name)
|
||||
# FIXME: on update_egl_widget this is going to crash because
|
||||
# FIXME: the item is not removed from the list in the python's side
|
||||
self.deleteLater()
|
||||
|
||||
def import_game(self):
|
||||
self.core.egl_import(self.game.app_name)
|
||||
# FIXME: on update_egl_widget this is going to crash because
|
||||
# FIXME: the item is not removed from the list in the python's side
|
||||
self.deleteLater()
|
||||
"""
|
||||
|
|
|
@ -10,10 +10,13 @@ from PyQt5.QtCore import Qt, QModelIndex, pyqtSignal, QRunnable, QObject, QThrea
|
|||
from PyQt5.QtGui import QStandardItemModel
|
||||
from PyQt5.QtWidgets import QFileDialog, QGroupBox, QCompleter, QTreeView, QHeaderView, qApp, QMessageBox
|
||||
|
||||
from rare.lgndr.cli import LegendaryCLI
|
||||
from rare.lgndr.api_arguments import LgndrImportGameArgs
|
||||
from rare.lgndr.api_monkeys import LgndrIndirectStatus
|
||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ApiResultsSingleton
|
||||
from rare.ui.components.tabs.games.import_sync.import_group import Ui_ImportGroup
|
||||
from rare.utils import legendary_utils
|
||||
from rare.utils.extra_widgets import IndicatorLineEdit, PathEdit
|
||||
from rare.widgets.elide_label import ElideLabel
|
||||
|
||||
logger = getLogger("Import")
|
||||
|
||||
|
@ -25,8 +28,7 @@ def find_app_name(path: str, core) -> Optional[str]:
|
|||
with open(os.path.join(path, ".egstore", i)) as file:
|
||||
app_name = json.load(file).get("AppName")
|
||||
return app_name
|
||||
elif app_name := legendary_utils.resolve_aliases(
|
||||
core, os.path.basename(os.path.normpath(path))):
|
||||
elif app_name := LegendaryCLI(core).resolve_aliases(os.path.basename(os.path.normpath(path))):
|
||||
# return None if game does not exist (Workaround for overlay)
|
||||
if not core.get_game(app_name):
|
||||
return None
|
||||
|
@ -45,7 +47,9 @@ class ImportResult(IntEnum):
|
|||
@dataclass
|
||||
class ImportedGame:
|
||||
result: ImportResult
|
||||
path: Optional[str] = None
|
||||
app_name: Optional[str] = None
|
||||
app_title: Optional[str] = None
|
||||
message: Optional[str] = None
|
||||
|
||||
|
||||
|
@ -54,14 +58,15 @@ class ImportWorker(QRunnable):
|
|||
finished = pyqtSignal(list)
|
||||
progress = pyqtSignal(int)
|
||||
|
||||
def __init__(self, path: str, import_folder: bool = False, app_name: str = None):
|
||||
def __init__(self, path: str, app_name: str = None, import_folder: bool = False, import_dlcs: bool = False):
|
||||
super(ImportWorker, self).__init__()
|
||||
self.signals = self.Signals()
|
||||
self.core = LegendaryCoreSingleton()
|
||||
|
||||
self.path = Path(path)
|
||||
self.import_folder = import_folder
|
||||
self.app_name = app_name
|
||||
self.tr = lambda message: qApp.translate("ImportThread", message)
|
||||
self.import_folder = import_folder
|
||||
self.import_dlcs = import_dlcs
|
||||
|
||||
def run(self) -> None:
|
||||
result_list: List = []
|
||||
|
@ -80,26 +85,30 @@ class ImportWorker(QRunnable):
|
|||
self.signals.finished.emit(result_list)
|
||||
|
||||
def __try_import(self, path: Path, app_name: str = None) -> ImportedGame:
|
||||
result = ImportedGame(ImportResult.ERROR, None, None)
|
||||
result = ImportedGame(ImportResult.ERROR)
|
||||
result.path = str(path)
|
||||
if app_name or (app_name := find_app_name(str(path), self.core)):
|
||||
result.app_name = app_name
|
||||
err = self.__import_game(app_name, path)
|
||||
if err:
|
||||
result.app_title = app_title = self.core.get_game(app_name).app_title
|
||||
success, message = self.__import_game(path, app_name, app_title)
|
||||
if not success:
|
||||
result.result = ImportResult.FAILED
|
||||
result.message = err
|
||||
result.message = message
|
||||
else:
|
||||
result.result = ImportResult.SUCCESS
|
||||
else:
|
||||
result.message = self.tr("Could not find AppName for {}").format(str(path))
|
||||
return result
|
||||
|
||||
def __import_game(self, app_name: str, path: Path) -> str:
|
||||
if not (err := legendary_utils.import_game(self.core, app_name=app_name, path=str(path))):
|
||||
igame = self.core.get_installed_game(app_name)
|
||||
logger.info(f"Successfully imported {igame.title}")
|
||||
return ""
|
||||
else:
|
||||
return err
|
||||
def __import_game(self, path: Path, app_name: str, app_title: str):
|
||||
cli = LegendaryCLI(self.core)
|
||||
status = LgndrIndirectStatus()
|
||||
args = LgndrImportGameArgs(
|
||||
app_path=str(path),
|
||||
app_name=app_name,
|
||||
indirect_status=status,
|
||||
get_boolean_choice=lambda prompt, default=True: self.import_dlcs
|
||||
)
|
||||
cli.import_game(args)
|
||||
return status.success, status.message
|
||||
|
||||
|
||||
class AppNameCompleter(QCompleter):
|
||||
|
@ -181,17 +190,17 @@ class ImportGroup(QGroupBox):
|
|||
self.app_name_edit.textChanged.connect(self.app_name_changed)
|
||||
self.ui.app_name_layout.addWidget(self.app_name_edit)
|
||||
|
||||
self.ui.import_folder_check.stateChanged.connect(self.import_folder_changed)
|
||||
self.ui.import_dlcs_check.setEnabled(False)
|
||||
|
||||
self.ui.import_button.setEnabled(False)
|
||||
self.ui.import_button.clicked.connect(
|
||||
lambda: self.import_pressed(self.path_edit.text())
|
||||
)
|
||||
|
||||
self.ui.import_folder_check.stateChanged.connect(
|
||||
lambda s: self.ui.import_button.setEnabled(s or (not s and self.app_name_edit.is_valid))
|
||||
)
|
||||
self.ui.import_folder_check.stateChanged.connect(
|
||||
lambda s: self.app_name_edit.setEnabled(not s)
|
||||
)
|
||||
self.info_label = ElideLabel(text="", parent=self)
|
||||
self.ui.button_info_layout.addWidget(self.info_label)
|
||||
|
||||
self.threadpool = QThreadPool.globalInstance()
|
||||
|
||||
def path_edit_cb(self, path) -> Tuple[bool, str, str]:
|
||||
|
@ -205,8 +214,8 @@ class ImportGroup(QGroupBox):
|
|||
return False, path, ""
|
||||
|
||||
def path_changed(self, path):
|
||||
self.ui.info_label.setText("")
|
||||
self.ui.import_folder_check.setChecked(False)
|
||||
self.info_label.setText("")
|
||||
self.ui.import_folder_check.setCheckState(Qt.Unchecked)
|
||||
if self.path_edit.is_valid:
|
||||
self.app_name_edit.setText(find_app_name(path, self.core))
|
||||
else:
|
||||
|
@ -220,17 +229,36 @@ class ImportGroup(QGroupBox):
|
|||
else:
|
||||
return False, text, IndicatorLineEdit.reasons.game_not_installed
|
||||
|
||||
def app_name_changed(self, text):
|
||||
self.ui.info_label.setText("")
|
||||
def app_name_changed(self, app_name: str):
|
||||
self.info_label.setText("")
|
||||
self.ui.import_dlcs_check.setCheckState(Qt.Unchecked)
|
||||
if self.app_name_edit.is_valid:
|
||||
self.ui.import_dlcs_check.setEnabled(
|
||||
bool(self.core.get_dlc_for_game(app_name))
|
||||
)
|
||||
self.ui.import_button.setEnabled(True)
|
||||
else:
|
||||
self.ui.import_dlcs_check.setEnabled(False)
|
||||
self.ui.import_button.setEnabled(False)
|
||||
|
||||
def import_folder_changed(self, state):
|
||||
self.app_name_edit.setEnabled(not state)
|
||||
self.ui.import_dlcs_check.setCheckState(Qt.Unchecked)
|
||||
self.ui.import_dlcs_check.setEnabled(
|
||||
state
|
||||
or (self.app_name_edit.is_valid and bool(self.core.get_dlc_for_game(self.app_name_edit.text())))
|
||||
)
|
||||
self.ui.import_button.setEnabled(state or (not state and self.app_name_edit.is_valid))
|
||||
|
||||
def import_pressed(self, path=None):
|
||||
if not path:
|
||||
path = self.path_edit.text()
|
||||
worker = ImportWorker(path, self.ui.import_folder_check.isChecked(), self.app_name_edit.text())
|
||||
worker = ImportWorker(
|
||||
path,
|
||||
self.app_name_edit.text(),
|
||||
self.ui.import_folder_check.isChecked(),
|
||||
self.ui.import_dlcs_check.isChecked(),
|
||||
)
|
||||
worker.signals.finished.connect(self.import_finished)
|
||||
worker.signals.progress.connect(self.import_progress)
|
||||
self.threadpool.start(worker)
|
||||
|
@ -251,16 +279,16 @@ class ImportGroup(QGroupBox):
|
|||
if len(result) == 1:
|
||||
res = result[0]
|
||||
if res.result == ImportResult.SUCCESS:
|
||||
self.ui.info_label.setText(
|
||||
self.tr("{} was imported successfully").format(self.core.get_game(res.app_name).app_title)
|
||||
self.info_label.setText(
|
||||
self.tr("Success: <b>{}</b> imported").format(res.app_title)
|
||||
)
|
||||
elif res.result == ImportResult.FAILED:
|
||||
self.ui.info_label.setText(
|
||||
self.tr("Failed: {}").format(res.message)
|
||||
self.info_label.setText(
|
||||
self.tr("Failed: <b>{}</b> - {}").format(res.app_title, res.message)
|
||||
)
|
||||
else:
|
||||
self.ui.info_label.setText(
|
||||
self.tr("Error: {}").format(res.message)
|
||||
self.info_label.setText(
|
||||
self.tr("Error: Could not find AppName for <b>{}</b>").format(res.path)
|
||||
)
|
||||
else:
|
||||
success = [r for r in result if r.result == ImportResult.SUCCESS]
|
||||
|
@ -280,15 +308,15 @@ class ImportGroup(QGroupBox):
|
|||
details: List = []
|
||||
for res in success:
|
||||
details.append(
|
||||
self.tr("{} was imported successfully").format(self.core.get_game(res.app_name).app_title)
|
||||
self.tr("Success: {} imported").format(res.app_title)
|
||||
)
|
||||
for res in failure:
|
||||
details.append(
|
||||
self.tr("Failed: {}").format(res.message)
|
||||
self.tr("Failed: {} - {}").format(res.app_title, res.message)
|
||||
)
|
||||
for res in errored:
|
||||
details.append(
|
||||
self.tr("Error: {}").format(res.message)
|
||||
self.tr("Error: Could not find AppName for {}").format(res.path)
|
||||
)
|
||||
messagebox.setDetailedText("\n".join(details))
|
||||
messagebox.show()
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import platform
|
||||
|
||||
from rare.components.tabs.settings.widgets.linux import LinuxSettings
|
||||
from rare.utils.extra_widgets import SideTabWidget
|
||||
from .about import About
|
||||
from .legendary import LegendarySettings
|
||||
from rare.components.tabs.settings.widgets.linux import LinuxSettings
|
||||
from .rare import RareSettings
|
||||
from .default_game_settings import DefaultGameSettings
|
||||
from .legendary import LegendarySettings
|
||||
from .rare import RareSettings
|
||||
|
||||
|
||||
class SettingsTab(SideTabWidget):
|
||||
|
|
|
@ -11,7 +11,7 @@ from rare.components.tabs.settings.widgets.ubisoft_activation import UbiActivati
|
|||
from rare.shared import LegendaryCoreSingleton
|
||||
from rare.ui.components.tabs.settings.legendary import Ui_LegendarySettings
|
||||
from rare.utils.extra_widgets import PathEdit, IndicatorLineEdit
|
||||
from rare.utils.utils import get_size
|
||||
from rare.utils.misc import get_size
|
||||
|
||||
logger = getLogger("LegendarySettings")
|
||||
|
||||
|
@ -174,10 +174,10 @@ class LegendarySettings(QWidget, Ui_LegendarySettings):
|
|||
if not keep_manifests:
|
||||
logger.debug("Removing manifests...")
|
||||
installed = [
|
||||
(ig.app_name, ig.version) for ig in self.core.get_installed_list()
|
||||
(ig.app_name, ig.version, ig.platform) for ig in self.core.get_installed_list()
|
||||
]
|
||||
installed.extend(
|
||||
(ig.app_name, ig.version) for ig in self.core.get_installed_dlc_list()
|
||||
(ig.app_name, ig.version, ig.platform) for ig in self.core.get_installed_dlc_list()
|
||||
)
|
||||
self.core.lgd.clean_manifests(installed)
|
||||
|
||||
|
|
|
@ -10,14 +10,15 @@ from PyQt5.QtWidgets import QWidget, QMessageBox
|
|||
from rare.shared import LegendaryCoreSingleton
|
||||
from rare.components.tabs.settings.widgets.rpc import RPCSettings
|
||||
from rare.ui.components.tabs.settings.rare import Ui_RareSettings
|
||||
from rare.utils import utils
|
||||
from rare.utils.paths import cache_dir
|
||||
from rare.utils.utils import (
|
||||
from rare.utils.misc import (
|
||||
get_translations,
|
||||
get_color_schemes,
|
||||
set_color_pallete,
|
||||
get_style_sheets,
|
||||
set_style_sheet,
|
||||
get_size,
|
||||
create_desktop_link,
|
||||
)
|
||||
|
||||
logger = getLogger("RareSettings")
|
||||
|
@ -148,7 +149,7 @@ class RareSettings(QWidget, Ui_RareSettings):
|
|||
for i in os.listdir(logdir):
|
||||
size += os.path.getsize(os.path.join(logdir, i))
|
||||
|
||||
self.log_dir_size_label.setText(utils.get_size(size))
|
||||
self.log_dir_size_label.setText(get_size(size))
|
||||
# self.log_dir_clean_button.setVisible(False)
|
||||
# self.log_dir_size_label.setVisible(False)
|
||||
|
||||
|
@ -160,7 +161,7 @@ class RareSettings(QWidget, Ui_RareSettings):
|
|||
def create_start_menu_link(self):
|
||||
try:
|
||||
if not os.path.exists(self.start_menu_link):
|
||||
utils.create_desktop_link(type_of_link="start_menu", for_rare=True)
|
||||
create_desktop_link(type_of_link="start_menu", for_rare=True)
|
||||
self.startmenu_link_btn.setText(self.tr("Remove start menu link"))
|
||||
else:
|
||||
os.remove(self.start_menu_link)
|
||||
|
@ -169,23 +170,24 @@ class RareSettings(QWidget, Ui_RareSettings):
|
|||
logger.error(str(e))
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"Error",
|
||||
f"Permission error, cannot remove {self.start_menu_link}",
|
||||
self.tr("Error"),
|
||||
self.tr("Permission error, cannot remove {}").format(self.start_menu_link),
|
||||
)
|
||||
|
||||
def create_desktop_link(self):
|
||||
try:
|
||||
if not os.path.exists(self.desktop_file):
|
||||
utils.create_desktop_link(type_of_link="desktop", for_rare=True)
|
||||
create_desktop_link(type_of_link="desktop", for_rare=True)
|
||||
self.desktop_link_btn.setText(self.tr("Remove Desktop link"))
|
||||
else:
|
||||
os.remove(self.desktop_file)
|
||||
self.desktop_link_btn.setText(self.tr("Create desktop link"))
|
||||
except PermissionError as e:
|
||||
logger.error(str(e))
|
||||
logger.warning(
|
||||
self,
|
||||
"Error",
|
||||
f"Permission error, cannot remove {self.desktop_file}",
|
||||
self.tr("Error"),
|
||||
self.tr("Permission error, cannot remove {}").format(self.start_menu_link),
|
||||
)
|
||||
|
||||
def on_color_select_changed(self, color):
|
||||
|
|
|
@ -6,7 +6,7 @@ from PyQt5.QtWidgets import QGroupBox, QTableWidgetItem, QMessageBox, QPushButto
|
|||
from rare.shared import LegendaryCoreSingleton
|
||||
from rare.ui.components.tabs.settings.widgets.env_vars import Ui_EnvVars
|
||||
from rare.utils import config_helper
|
||||
from rare.utils.utils import icon
|
||||
from rare.utils.misc import icon
|
||||
|
||||
logger = getLogger("EnvVars")
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ from PyQt5.QtWidgets import QGroupBox, QMessageBox
|
|||
from legendary.utils import eos
|
||||
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
||||
from rare.ui.components.tabs.settings.widgets.eos_widget import Ui_EosWidget
|
||||
from rare.utils.models import InstallOptionsModel
|
||||
from rare.models.install import InstallOptionsModel
|
||||
|
||||
logger = getLogger("EOS")
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ from PyQt5.QtWidgets import QWidget, QLabel, QHBoxLayout, QSizePolicy, QPushButt
|
|||
|
||||
from legendary.models.game import Game
|
||||
from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton
|
||||
from rare.utils.utils import icon
|
||||
from rare.utils.misc import icon
|
||||
|
||||
logger = getLogger("Ubisoft")
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ from PyQt5.QtWidgets import QHBoxLayout, QLabel, QPushButton, QInputDialog, QFra
|
|||
from rare import shared
|
||||
from rare.ui.components.tabs.settings.widgets.wrapper import Ui_WrapperSettings
|
||||
from rare.utils import config_helper
|
||||
from rare.utils.utils import icon
|
||||
from rare.utils.misc import icon
|
||||
|
||||
logger = getLogger("Wrapper Settings")
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ from rare.shared import LegendaryCoreSingleton
|
|||
from rare.components.tabs.shop.shop_models import ShopGame
|
||||
from rare.ui.components.tabs.store.shop_game_info import Ui_shop_info
|
||||
from rare.utils.extra_widgets import WaitingSpinner, ImageLabel
|
||||
from rare.utils.utils import icon
|
||||
from rare.utils.misc import icon
|
||||
|
||||
logger = logging.getLogger("ShopInfo")
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout
|
|||
from rare.components.tabs.shop.shop_models import ImageUrlModel
|
||||
from rare.ui.components.tabs.store.wishlist_widget import Ui_WishlistWidget
|
||||
from rare.utils.extra_widgets import ImageLabel
|
||||
from rare.utils.utils import icon
|
||||
from rare.utils.misc import icon
|
||||
|
||||
logger = logging.getLogger("GameWidgets")
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ from rare.components.tabs.shop import ShopApiCore
|
|||
from rare.components.tabs.shop.game_widgets import WishlistWidget
|
||||
from rare.ui.components.tabs.store.wishlist import Ui_Wishlist
|
||||
from rare.utils.extra_widgets import WaitingSpinner
|
||||
from rare.utils.utils import icon
|
||||
from rare.utils.misc import icon
|
||||
|
||||
|
||||
class Wishlist(QStackedWidget, Ui_Wishlist):
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from PyQt5.QtCore import QSize
|
||||
from PyQt5.QtWidgets import QTabBar, QToolButton
|
||||
|
||||
from rare.utils.utils import icon
|
||||
from rare.utils.misc import icon
|
||||
|
||||
|
||||
class MainTabBar(QTabBar):
|
||||
|
|
|
@ -69,7 +69,7 @@ class GameProcessApp(RareApp):
|
|||
self.game_process = QProcess()
|
||||
self.app_name = app_name
|
||||
self.logger = getLogger(self.app_name)
|
||||
self.core = LegendaryCoreSingleton(True)
|
||||
self.core = LegendaryCoreSingleton(init=True)
|
||||
|
||||
lang = self.settings.value("language", self.core.language_code, type=str)
|
||||
self.load_translator(lang)
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 50f71cbd9b2ae0b31615e8f7d8d8595922d3cdf3
|
5
rare/lgndr/__init__.py
Normal file
5
rare/lgndr/__init__.py
Normal 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.
|
||||
"""
|
136
rare/lgndr/api_arguments.py
Normal file
136
rare/lgndr/api_arguments.py
Normal file
|
@ -0,0 +1,136 @@
|
|||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from typing import Callable, List, Optional, Dict
|
||||
|
||||
from .api_monkeys import (
|
||||
LgndrIndirectStatus,
|
||||
GetBooleanChoiceProtocol,
|
||||
get_boolean_choice,
|
||||
verify_stdout,
|
||||
DLManagerSignals
|
||||
)
|
||||
from .downloading import UIUpdate
|
||||
|
||||
"""
|
||||
@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
|
||||
# Rare: Extra arguments
|
||||
indirect_status: LgndrIndirectStatus = LgndrIndirectStatus()
|
||||
get_boolean_choice: GetBooleanChoiceProtocol = get_boolean_choice
|
||||
|
||||
|
||||
@dataclass
|
||||
class LgndrUninstallGameArgs:
|
||||
app_name: str
|
||||
keep_files: bool = False
|
||||
yes: bool = False
|
||||
# Rare: Extra arguments
|
||||
indirect_status: LgndrIndirectStatus = LgndrIndirectStatus()
|
||||
get_boolean_choice: GetBooleanChoiceProtocol = get_boolean_choice
|
||||
|
||||
|
||||
@dataclass
|
||||
class LgndrVerifyGameArgs:
|
||||
app_name: str
|
||||
# Rare: Extra arguments
|
||||
indirect_status: LgndrIndirectStatus = LgndrIndirectStatus()
|
||||
verify_stdout: Callable[[int, int, float, float], None] = verify_stdout
|
||||
|
||||
|
||||
@dataclass
|
||||
class LgndrInstallGameArgs:
|
||||
app_name: str
|
||||
base_path: str = ""
|
||||
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: Optional[List[str]] = 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
|
||||
# FIXME: move to LgndrInstallGameRealArgs
|
||||
skip_dlcs: bool = False
|
||||
with_dlcs: bool = False
|
||||
# end of FIXME
|
||||
yes: bool = True
|
||||
# Rare: Extra arguments
|
||||
indirect_status: LgndrIndirectStatus = LgndrIndirectStatus()
|
||||
get_boolean_choice: GetBooleanChoiceProtocol = get_boolean_choice
|
||||
sdl_prompt: Callable[[str, str], List[str]] = lambda app_name, title: [""]
|
||||
verify_stdout: Callable[[int, int, float, float], None] = verify_stdout
|
||||
|
||||
# def __post_init__(self):
|
||||
# if self.sdl_prompt is None:
|
||||
# self.sdl_prompt: Callable[[str, str], list] = \
|
||||
# lambda app_name, title: self.install_tag if self.install_tag is not None else [""]
|
||||
|
||||
|
||||
@dataclass
|
||||
class LgndrInstallGameRealArgs:
|
||||
app_name: str
|
||||
platform: str = "Windows"
|
||||
repair_mode: bool = False
|
||||
repair_file: str = ""
|
||||
no_install: bool = False
|
||||
save_path: str = ""
|
||||
skip_dlcs: bool = False
|
||||
with_dlcs: bool = False
|
||||
dlm_debug: bool = False
|
||||
yes: bool = False
|
||||
# Rare: Extra arguments
|
||||
install_preqs: bool = False
|
||||
indirect_status: LgndrIndirectStatus = LgndrIndirectStatus()
|
||||
ui_update: Callable[[UIUpdate], None] = lambda ui: None
|
||||
dlm_signals: DLManagerSignals = DLManagerSignals()
|
||||
|
||||
|
||||
@dataclass
|
||||
class LgndrInstallGameRealRet:
|
||||
class ReturnCode(IntEnum):
|
||||
ERROR = 1
|
||||
STOPPED = 2
|
||||
FINISHED = 3
|
||||
|
||||
app_name: str
|
||||
ret_code: ReturnCode = ReturnCode.ERROR
|
||||
message: str = ""
|
||||
dlcs: Optional[List[Dict]] = None
|
||||
sync_saves: bool = False
|
||||
tip_url: str = ""
|
||||
shortcuts: bool = False
|
32
rare/lgndr/api_exception.py
Normal file
32
rare/lgndr/api_exception.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
import logging
|
||||
import warnings
|
||||
|
||||
|
||||
class LgndrException(RuntimeError):
|
||||
def __init__(self, message="Error in Legendary"):
|
||||
self.message = message
|
||||
super(LgndrException, self).__init__(self.message)
|
||||
|
||||
|
||||
class LgndrWarning(RuntimeWarning):
|
||||
def __init__(self, message="Warning in Legendary"):
|
||||
self.message = message
|
||||
super(LgndrWarning, self).__init__(self.message)
|
||||
|
||||
|
||||
class LgndrCLILogHandler(logging.Handler):
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
# lk: FATAL is the same as CRITICAL
|
||||
if record.levelno == logging.ERROR or record.levelno == logging.CRITICAL:
|
||||
raise LgndrException(record.getMessage())
|
||||
# if record.levelno < logging.ERROR or record.levelno == logging.WARNING:
|
||||
# warnings.warn(record.getMessage())
|
||||
|
||||
|
||||
class LgndrCoreLogHandler(logging.Handler):
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
# lk: FATAL is the same as CRITICAL
|
||||
if record.levelno == logging.CRITICAL:
|
||||
raise LgndrException(record.getMessage())
|
||||
# if record.levelno < logging.CRITICAL:
|
||||
# warnings.warn(record.getMessage())
|
106
rare/lgndr/api_monkeys.py
Normal file
106
rare/lgndr/api_monkeys.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
import logging
|
||||
from dataclasses import dataclass
|
||||
|
||||
from typing_extensions import Protocol
|
||||
|
||||
|
||||
class GetBooleanChoiceProtocol(Protocol):
|
||||
def __call__(self, prompt: str, default: bool = ...) -> bool:
|
||||
...
|
||||
|
||||
|
||||
def get_boolean_choice(prompt: str, default: bool = True) -> bool:
|
||||
return default
|
||||
|
||||
|
||||
def verify_stdout(a0: int, a1: int, a2: float, a3: float) -> None:
|
||||
print(f"Verification progress: {a0}/{a1} ({a2:.01f}%) [{a3:.1f} MiB/s]\t\r")
|
||||
|
||||
|
||||
class DLManagerSignals:
|
||||
_kill = False
|
||||
_update = False
|
||||
|
||||
@property
|
||||
def kill(self) -> bool:
|
||||
self._update = False
|
||||
return self._kill
|
||||
|
||||
@kill.setter
|
||||
def kill(self, value: bool) -> None:
|
||||
self._update = True
|
||||
self._kill = value
|
||||
|
||||
@property
|
||||
def update(self) -> bool:
|
||||
_update = self._update
|
||||
self._update = False
|
||||
return _update
|
||||
|
||||
|
||||
@dataclass
|
||||
class LgndrIndirectStatus:
|
||||
success: bool = False
|
||||
message: str = ""
|
||||
|
||||
def __len__(self):
|
||||
if self.message:
|
||||
return 2
|
||||
else:
|
||||
return 0
|
||||
|
||||
def __bool__(self):
|
||||
return self.success
|
||||
|
||||
def __getitem__(self, item):
|
||||
if item == 0:
|
||||
return self.success
|
||||
elif item == 1:
|
||||
return self.message
|
||||
else:
|
||||
raise IndexError
|
||||
|
||||
def __iter__(self):
|
||||
return iter((self.success, self.message))
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
|
||||
class LgndrIndirectLogger:
|
||||
def __init__(
|
||||
self, status: LgndrIndirectStatus, logger: logging.Logger = None, level: int = logging.ERROR
|
||||
):
|
||||
self.logger = logger
|
||||
self.level = level
|
||||
self.status = status
|
||||
|
||||
def set_logger(self, logger: logging.Logger):
|
||||
self.logger = logger
|
||||
|
||||
def set_level(self, level: int):
|
||||
self.level = level
|
||||
|
||||
def _log(self, level: int, msg: str):
|
||||
self.status.success = True if level < self.level else False
|
||||
self.status.message = msg
|
||||
if self.logger:
|
||||
self.logger.log(level, msg)
|
||||
|
||||
def debug(self, msg: str):
|
||||
self._log(logging.DEBUG, msg)
|
||||
|
||||
def info(self, msg: str):
|
||||
self._log(logging.INFO, msg)
|
||||
|
||||
def warning(self, msg: str):
|
||||
self._log(logging.WARNING, msg)
|
||||
|
||||
def error(self, msg: str):
|
||||
self._log(logging.ERROR, msg)
|
||||
|
||||
def critical(self, msg: str):
|
||||
self._log(logging.CRITICAL, msg)
|
||||
|
||||
def fatal(self, msg: str):
|
||||
self.critical(msg)
|
623
rare/lgndr/cli.py
Normal file
623
rare/lgndr/cli.py
Normal file
|
@ -0,0 +1,623 @@
|
|||
import logging
|
||||
import os
|
||||
import queue
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Optional, Union, Tuple
|
||||
|
||||
from legendary.cli import LegendaryCLI as LegendaryCLIReal
|
||||
from legendary.models.downloading import AnalysisResult, ConditionCheckResult
|
||||
from legendary.models.game import Game, InstalledGame, VerifyResult
|
||||
from legendary.utils.lfs import validate_files
|
||||
from legendary.utils.selective_dl import get_sdl_appname
|
||||
|
||||
from .api_arguments import (
|
||||
LgndrInstallGameArgs,
|
||||
LgndrImportGameArgs,
|
||||
LgndrVerifyGameArgs,
|
||||
LgndrUninstallGameArgs,
|
||||
LgndrInstallGameRealArgs,
|
||||
LgndrInstallGameRealRet,
|
||||
)
|
||||
from .api_monkeys import LgndrIndirectStatus, LgndrIndirectLogger
|
||||
from .core import LegendaryCore
|
||||
from .manager import DLManager
|
||||
|
||||
|
||||
# fmt: off
|
||||
class LegendaryCLI(LegendaryCLIReal):
|
||||
|
||||
# noinspection PyMissingConstructor
|
||||
def __init__(self, core: LegendaryCore):
|
||||
self.core = core
|
||||
self.logger = logging.getLogger('cli')
|
||||
self.logging_queue = None
|
||||
self.ql = self.setup_threaded_logging()
|
||||
|
||||
def __del__(self):
|
||||
self.ql.stop()
|
||||
|
||||
def resolve_aliases(self, name):
|
||||
return super(LegendaryCLI, self)._resolve_aliases(name)
|
||||
|
||||
def install_game(self, args: LgndrInstallGameArgs) -> Optional[Tuple[DLManager, AnalysisResult, InstalledGame, Game, bool, Optional[str], ConditionCheckResult]]:
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
|
||||
get_boolean_choice = args.get_boolean_choice
|
||||
sdl_prompt = args.sdl_prompt
|
||||
|
||||
args.app_name = self._resolve_aliases(args.app_name)
|
||||
if self.core.is_installed(args.app_name):
|
||||
igame = self.core.get_installed_game(args.app_name)
|
||||
args.platform = igame.platform
|
||||
if igame.needs_verification and not args.repair_mode:
|
||||
logger.info('Game needs to be verified before updating, switching to repair mode...')
|
||||
args.repair_mode = True
|
||||
|
||||
repair_file = None
|
||||
# Rare: The 'args.no_install' flags is set externally from the InstallDialog
|
||||
if args.repair_mode:
|
||||
args.repair_mode = True
|
||||
args.no_install = args.repair_and_update is False
|
||||
repair_file = os.path.join(self.core.lgd.get_tmp_path(), f'{args.app_name}.repair')
|
||||
|
||||
# Rare: Rare is already logged in
|
||||
|
||||
if args.file_prefix or args.file_exclude_prefix:
|
||||
args.no_install = True
|
||||
|
||||
# Rare: Rare runs updates on already installed games only
|
||||
|
||||
game = self.core.get_game(args.app_name, update_meta=True, platform=args.platform)
|
||||
|
||||
if not game:
|
||||
logger.error(f'Could not find "{args.app_name}" in list of available games, '
|
||||
f'did you type the name correctly?')
|
||||
return
|
||||
|
||||
# Rare: Rare checks this before calling 'install_game'
|
||||
|
||||
if args.platform not in game.asset_infos:
|
||||
if not args.no_install:
|
||||
if self.core.lgd.config.getboolean('Legendary', 'install_platform_fallback', fallback=True):
|
||||
logger.warning(f'App has no asset for platform "{args.platform}", falling back to "Windows".')
|
||||
args.platform = 'Windows'
|
||||
else:
|
||||
logger.error(f'No app asset found for platform "{args.platform}", run '
|
||||
f'"legendary info --platform {args.platform}" and make '
|
||||
f'sure the app is available for the specified platform.')
|
||||
return
|
||||
else:
|
||||
logger.warning(f'No asset found for platform "{args.platform}", '
|
||||
f'trying anyway since --no-install is set.')
|
||||
|
||||
if game.is_dlc:
|
||||
logger.info('Install candidate is DLC')
|
||||
app_name = game.metadata['mainGameItem']['releaseInfo'][0]['appId']
|
||||
base_game = self.core.get_game(app_name)
|
||||
# check if base_game is actually installed
|
||||
if not self.core.is_installed(app_name):
|
||||
# download mode doesn't care about whether something's installed
|
||||
if not args.no_install:
|
||||
logger.fatal(f'Base game "{app_name}" is not installed!')
|
||||
return
|
||||
else:
|
||||
base_game = None
|
||||
|
||||
if args.repair_mode:
|
||||
if not self.core.is_installed(game.app_name):
|
||||
logger.error(f'Game "{game.app_title}" ({game.app_name}) is not installed!')
|
||||
return
|
||||
|
||||
if not os.path.exists(repair_file):
|
||||
logger.info('Game has not been verified yet.')
|
||||
# Rare: we do not want to verify while preparing the download in the InstallDialog
|
||||
# Rare: we handle it differently through the GameInfo tab
|
||||
logger.error('Game has not been verified yet.')
|
||||
return
|
||||
else:
|
||||
logger.info(f'Using existing repair file: {repair_file}')
|
||||
|
||||
# check if SDL should be disabled
|
||||
sdl_enabled = not args.install_tag and not game.is_dlc
|
||||
config_tags = self.core.lgd.config.get(game.app_name, 'install_tags', fallback=None)
|
||||
config_disable_sdl = self.core.lgd.config.getboolean(game.app_name, 'disable_sdl', fallback=False)
|
||||
# remove config flag if SDL is reset
|
||||
if config_disable_sdl and args.reset_sdl and not args.disable_sdl:
|
||||
self.core.lgd.config.remove_option(game.app_name, 'disable_sdl')
|
||||
# if config flag is not yet set, set it and remove previous install tags
|
||||
elif not config_disable_sdl and args.disable_sdl:
|
||||
logger.info('Clearing install tags from config and disabling SDL for title.')
|
||||
if config_tags:
|
||||
self.core.lgd.config.remove_option(game.app_name, 'install_tags')
|
||||
config_tags = None
|
||||
self.core.lgd.config.set(game.app_name, 'disable_sdl', True)
|
||||
sdl_enabled = False
|
||||
# just disable SDL, but keep config tags that have been manually specified
|
||||
elif config_disable_sdl or args.disable_sdl:
|
||||
sdl_enabled = False
|
||||
|
||||
if sdl_enabled and ((sdl_name := get_sdl_appname(game.app_name)) is not None):
|
||||
if not self.core.is_installed(game.app_name) or config_tags is None or args.reset_sdl:
|
||||
sdl_data = self.core.get_sdl_data(sdl_name, platform=args.platform)
|
||||
if sdl_data:
|
||||
if args.skip_sdl:
|
||||
args.install_tag = ['']
|
||||
if '__required' in sdl_data:
|
||||
args.install_tag.extend(sdl_data['__required']['tags'])
|
||||
else:
|
||||
args.install_tag = sdl_prompt(sdl_data, game.app_title)
|
||||
self.core.lgd.config.set(game.app_name, 'install_tags', ','.join(args.install_tag))
|
||||
else:
|
||||
logger.error(f'Unable to get SDL data for {sdl_name}')
|
||||
else:
|
||||
args.install_tag = config_tags.split(',')
|
||||
elif args.install_tag and not game.is_dlc and not args.no_install:
|
||||
config_tags = ','.join(args.install_tag)
|
||||
logger.info(f'Saving install tags for "{game.app_name}" to config: {config_tags}')
|
||||
self.core.lgd.config.set(game.app_name, 'install_tags', config_tags)
|
||||
elif not game.is_dlc:
|
||||
if config_tags and args.reset_sdl:
|
||||
logger.info('Clearing install tags from config.')
|
||||
self.core.lgd.config.remove_option(game.app_name, 'install_tags')
|
||||
elif config_tags:
|
||||
logger.info(f'Using install tags from config: {config_tags}')
|
||||
args.install_tag = config_tags.split(',')
|
||||
|
||||
logger.info(f'Preparing download for "{game.app_title}" ({game.app_name})...')
|
||||
# todo use status queue to print progress from CLI
|
||||
# This has become a little ridiculous hasn't it?
|
||||
dlm, analysis, igame = self.core.prepare_download(game=game, base_game=base_game, base_path=args.base_path,
|
||||
force=args.force, max_shm=args.shared_memory,
|
||||
max_workers=args.max_workers, game_folder=args.game_folder,
|
||||
disable_patching=args.disable_patching,
|
||||
override_manifest=args.override_manifest,
|
||||
override_old_manifest=args.override_old_manifest,
|
||||
override_base_url=args.override_base_url,
|
||||
platform=args.platform,
|
||||
file_prefix_filter=args.file_prefix,
|
||||
file_exclude_filter=args.file_exclude_prefix,
|
||||
file_install_tag=args.install_tag,
|
||||
dl_optimizations=args.order_opt,
|
||||
dl_timeout=args.dl_timeout,
|
||||
repair=args.repair_mode,
|
||||
repair_use_latest=args.repair_and_update,
|
||||
disable_delta=args.disable_delta,
|
||||
override_delta_manifest=args.override_delta_manifest,
|
||||
preferred_cdn=args.preferred_cdn,
|
||||
disable_https=args.disable_https)
|
||||
|
||||
# game is either up-to-date or hasn't changed, so we have nothing to do
|
||||
if not analysis.dl_size:
|
||||
logger.info('Download size is 0, the game is either already up to date or has not changed. Exiting...')
|
||||
self.install_game_cleanup(game, igame, args.repair_mode, repair_file)
|
||||
return
|
||||
|
||||
res = self.core.check_installation_conditions(analysis=analysis, install=igame, game=game,
|
||||
updating=self.core.is_installed(args.app_name),
|
||||
ignore_space_req=args.ignore_space)
|
||||
|
||||
return dlm, analysis, igame, game, args.repair_mode, repair_file, res
|
||||
|
||||
# Rare: This is currently handled in DownloadThread, this is a trial
|
||||
def install_game_real(self, args: LgndrInstallGameRealArgs, dlm: DLManager, game: Game, igame: InstalledGame) -> LgndrInstallGameRealRet:
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
|
||||
ret = LgndrInstallGameRealRet(game.app_name)
|
||||
|
||||
start_t = time.time()
|
||||
|
||||
try:
|
||||
# set up logging stuff (should be moved somewhere else later)
|
||||
dlm.logging_queue = self.logging_queue
|
||||
dlm.proc_debug = args.dlm_debug
|
||||
|
||||
dlm.start()
|
||||
while dlm.is_alive():
|
||||
try:
|
||||
args.ui_update(dlm.status_queue.get(timeout=1.0))
|
||||
except queue.Empty:
|
||||
pass
|
||||
if args.dlm_signals.update:
|
||||
try:
|
||||
dlm.signals_queue.put(args.dlm_signals, block=False, timeout=1.0)
|
||||
except queue.Full:
|
||||
pass
|
||||
time.sleep(dlm.update_interval / 10)
|
||||
dlm.join()
|
||||
except Exception as e:
|
||||
end_t = time.time()
|
||||
logger.info(f'Installation failed after {end_t - start_t:.02f} seconds.')
|
||||
logger.warning(f'The following exception occurred while waiting for the downloader to finish: {e!r}. '
|
||||
f'Try restarting the process, the resume file will be used to start where it failed. '
|
||||
f'If it continues to fail please open an issue on GitHub.')
|
||||
ret.ret_code = ret.ReturnCode.ERROR
|
||||
ret.message = f"{e!r}"
|
||||
return ret
|
||||
else:
|
||||
end_t = time.time()
|
||||
if args.dlm_signals.kill is True:
|
||||
logger.info(f"Download stopped after {end_t - start_t:.02f} seconds.")
|
||||
ret.exit_code = ret.ReturnCode.STOPPED
|
||||
return ret
|
||||
logger.info(f"Download finished in {end_t - start_t:.02f} seconds.")
|
||||
if not args.no_install:
|
||||
# Allow setting savegame directory at install time so sync-saves will work immediately
|
||||
if (game.supports_cloud_saves or game.supports_mac_cloud_saves) and args.save_path:
|
||||
igame.save_path = args.save_path
|
||||
|
||||
postinstall = self.core.install_game(igame)
|
||||
if postinstall:
|
||||
self._handle_postinstall(postinstall, igame, yes=args.yes, choice=args.install_preqs)
|
||||
|
||||
dlcs = self.core.get_dlc_for_game(game.app_name)
|
||||
if dlcs and not args.skip_dlcs:
|
||||
for dlc in dlcs:
|
||||
ret.dlcs.append(
|
||||
{
|
||||
"app_name": dlc.app_name,
|
||||
"app_title": dlc.app_title,
|
||||
"app_version": dlc.app_version(args.platform)
|
||||
}
|
||||
)
|
||||
|
||||
# Rare: We do not install DLCs automatically, we offer to do so through our downloads tab
|
||||
|
||||
if (game.supports_cloud_saves or game.supports_mac_cloud_saves) and not game.is_dlc:
|
||||
# todo option to automatically download saves after the installation
|
||||
# args does not have the required attributes for sync_saves in here,
|
||||
# not sure how to solve that elegantly.
|
||||
logger.info(f'This game supports cloud saves, syncing is handled by the "sync-saves" command. '
|
||||
f'To download saves for this game run "legendary sync-saves {args.app_name}"')
|
||||
ret.sync_saves = True
|
||||
|
||||
# show tip again after installation finishes so users hopefully actually see it
|
||||
if tip_url := self.core.get_game_tip(igame.app_name):
|
||||
ret.tip_url = tip_url
|
||||
|
||||
self.install_game_cleanup(game, igame, args.repair_mode, args.repair_file)
|
||||
|
||||
logger.info(f'Finished installation process in {end_t - start_t:.02f} seconds.')
|
||||
|
||||
return ret
|
||||
|
||||
def install_game_cleanup(self, game: Game, igame: InstalledGame, repair_mode: bool = False, repair_file: str = '') -> None:
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(LgndrIndirectStatus(), self.logger)
|
||||
|
||||
old_igame = self.core.get_installed_game(game.app_name)
|
||||
if old_igame and repair_mode and os.path.exists(repair_file):
|
||||
if old_igame.needs_verification:
|
||||
old_igame.needs_verification = False
|
||||
self.core.install_game(old_igame)
|
||||
|
||||
logger.debug('Removing repair file.')
|
||||
os.remove(repair_file)
|
||||
|
||||
# check if install tags have changed, if they did; try deleting files that are no longer required.
|
||||
if old_igame and old_igame.install_tags != igame.install_tags:
|
||||
old_igame.install_tags = igame.install_tags
|
||||
logger.info('Deleting now untagged files.')
|
||||
self.core.uninstall_tag(old_igame)
|
||||
self.core.install_game(old_igame)
|
||||
|
||||
def _handle_postinstall(self, postinstall, igame, yes=False, choice=True):
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(LgndrIndirectStatus(), self.logger)
|
||||
# noinspection PyShadowingBuiltins
|
||||
def print(x): self.logger.info(x) if x else None
|
||||
# noinspection PyShadowingBuiltins
|
||||
def input(x): return 'y' if choice else 'i'
|
||||
|
||||
print('\nThis game lists the following prequisites to be installed:')
|
||||
print(f'- {postinstall["name"]}: {" ".join((postinstall["path"], postinstall["args"]))}')
|
||||
print('')
|
||||
|
||||
if os.name == 'nt':
|
||||
if yes:
|
||||
c = 'n' # we don't want to launch anything, just silent install.
|
||||
else:
|
||||
choice = input('Do you wish to install the prerequisites? ([y]es, [n]o, [i]gnore): ')
|
||||
c = choice.lower()[0]
|
||||
print('')
|
||||
|
||||
if c == 'i': # just set it to installed
|
||||
logger.info('Marking prerequisites as installed...')
|
||||
self.core.prereq_installed(igame.app_name)
|
||||
elif c == 'y': # set to installed and launch installation
|
||||
logger.info('Launching prerequisite executable..')
|
||||
self.core.prereq_installed(igame.app_name)
|
||||
req_path, req_exec = os.path.split(postinstall['path'])
|
||||
work_dir = os.path.join(igame.install_path, req_path)
|
||||
fullpath = os.path.join(work_dir, req_exec)
|
||||
try:
|
||||
p = subprocess.Popen([fullpath, postinstall['args']], cwd=work_dir, shell=True)
|
||||
p.wait()
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to run prereq executable with: {e!r}')
|
||||
else:
|
||||
logger.info('Automatic installation not available on Linux.')
|
||||
|
||||
def uninstall_game(self, args: LgndrUninstallGameArgs) -> None:
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger, logging.WARNING)
|
||||
get_boolean_choice = args.get_boolean_choice
|
||||
|
||||
args.app_name = self._resolve_aliases(args.app_name)
|
||||
igame = self.core.get_installed_game(args.app_name)
|
||||
if not igame:
|
||||
logger.error(f'Game {args.app_name} not installed, cannot uninstall!')
|
||||
return
|
||||
|
||||
if not args.yes:
|
||||
if not get_boolean_choice(f'Do you wish to uninstall "{igame.title}"?', default=False):
|
||||
return
|
||||
|
||||
try:
|
||||
if not igame.is_dlc:
|
||||
# Remove DLC first so directory is empty when game uninstall runs
|
||||
dlcs = self.core.get_dlc_for_game(igame.app_name)
|
||||
for dlc in dlcs:
|
||||
if (idlc := self.core.get_installed_game(dlc.app_name)) is not None:
|
||||
logger.info(f'Uninstalling DLC "{dlc.app_name}"...')
|
||||
self.core.uninstall_game(idlc, delete_files=not args.keep_files)
|
||||
|
||||
logger.info(f'Removing "{igame.title}" from "{igame.install_path}"...')
|
||||
self.core.uninstall_game(igame, delete_files=not args.keep_files,
|
||||
delete_root_directory=not igame.is_dlc)
|
||||
logger.info('Game has been uninstalled.')
|
||||
return
|
||||
except Exception as e:
|
||||
logger.warning(f'Removing game failed: {e!r}, please remove {igame.install_path} manually.')
|
||||
return
|
||||
|
||||
def verify_game(self, args: Union[LgndrVerifyGameArgs, LgndrInstallGameArgs], print_command=True, repair_mode=False, repair_online=False) -> Optional[Tuple[int, int]]:
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
|
||||
|
||||
args.app_name = self._resolve_aliases(args.app_name)
|
||||
if not self.core.is_installed(args.app_name):
|
||||
logger.error(f'Game "{args.app_name}" is not installed')
|
||||
return
|
||||
|
||||
logger.info(f'Loading installed manifest for "{args.app_name}"')
|
||||
igame = self.core.get_installed_game(args.app_name)
|
||||
if not os.path.exists(igame.install_path):
|
||||
logger.error(f'Install path "{igame.install_path}" does not exist, make sure all necessary mounts '
|
||||
f'are available. If you previously deleted the game folder without uninstalling, run '
|
||||
f'"legendary uninstall -y {igame.app_name}" and reinstall from scratch.')
|
||||
return
|
||||
|
||||
manifest_data, _ = self.core.get_installed_manifest(args.app_name)
|
||||
if manifest_data is None:
|
||||
if repair_mode:
|
||||
if not repair_online:
|
||||
logger.critical('No manifest could be loaded, the manifest file may be missing!')
|
||||
raise ValueError('Local manifest is missing')
|
||||
|
||||
logger.warning('No manifest could be loaded, the file may be missing. Downloading the latest manifest.')
|
||||
game = self.core.get_game(args.app_name, platform=igame.platform)
|
||||
manifest_data, _ = self.core.get_cdn_manifest(game, igame.platform)
|
||||
# Rare: Save the manifest if we downloaded it because it was missing
|
||||
self.core.lgd.save_manifest(game.app_name, manifest_data,
|
||||
version=self.core.load_manifest(manifest_data).meta.build_version,
|
||||
platform=igame.platform)
|
||||
else:
|
||||
logger.critical(f'Manifest appears to be missing! To repair, run "legendary repair '
|
||||
f'{args.app_name} --repair-and-update", this will however redownload all files '
|
||||
f'that do not match the latest manifest in their entirety.')
|
||||
return
|
||||
|
||||
manifest = self.core.load_manifest(manifest_data)
|
||||
|
||||
files = sorted(manifest.file_manifest_list.elements,
|
||||
key=lambda a: a.filename.lower())
|
||||
|
||||
# build list of hashes
|
||||
if config_tags := self.core.lgd.config.get(args.app_name, 'install_tags', fallback=None):
|
||||
install_tags = set(i.strip() for i in config_tags.split(','))
|
||||
file_list = [
|
||||
(f.filename, f.sha_hash.hex())
|
||||
for f in files
|
||||
if any(it in install_tags for it in f.install_tags) or not f.install_tags
|
||||
]
|
||||
else:
|
||||
file_list = [(f.filename, f.sha_hash.hex()) for f in files]
|
||||
|
||||
total = len(file_list)
|
||||
total_size = sum(manifest.file_manifest_list.get_file_by_path(fm[0]).file_size
|
||||
for fm in file_list)
|
||||
num = processed = last_processed = 0
|
||||
speed = 0.0
|
||||
percentage = 0.0
|
||||
failed = []
|
||||
missing = []
|
||||
|
||||
last_update = time.time()
|
||||
|
||||
logger.info(f'Verifying "{igame.title}" version "{manifest.meta.build_version}"')
|
||||
repair_file = []
|
||||
for result, path, result_hash, bytes_read in validate_files(igame.install_path, file_list):
|
||||
processed += bytes_read
|
||||
percentage = (processed / total_size) * 100.0
|
||||
num += 1
|
||||
|
||||
if (delta := ((current_time := time.time()) - last_update)) > 1 or (not last_processed and delta > 1):
|
||||
last_update = current_time
|
||||
speed = (processed - last_processed) / 1024 / 1024 / delta
|
||||
last_processed = processed
|
||||
|
||||
if args.verify_stdout:
|
||||
args.verify_stdout(num, total, percentage, speed)
|
||||
|
||||
if result == VerifyResult.HASH_MATCH:
|
||||
repair_file.append(f'{result_hash}:{path}')
|
||||
continue
|
||||
elif result == VerifyResult.HASH_MISMATCH:
|
||||
logger.error(f'File does not match hash: "{path}"')
|
||||
repair_file.append(f'{result_hash}:{path}')
|
||||
failed.append(path)
|
||||
elif result == VerifyResult.FILE_MISSING:
|
||||
logger.error(f'File is missing: "{path}"')
|
||||
missing.append(path)
|
||||
else:
|
||||
logger.error(f'Other failure (see log), treating file as missing: "{path}"')
|
||||
missing.append(path)
|
||||
|
||||
if args.verify_stdout:
|
||||
args.verify_stdout(num, total, percentage, speed)
|
||||
|
||||
# always write repair file, even if all match
|
||||
if repair_file:
|
||||
repair_filename = os.path.join(self.core.lgd.get_tmp_path(), f'{args.app_name}.repair')
|
||||
with open(repair_filename, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(repair_file))
|
||||
logger.debug(f'Written repair file to "{repair_filename}"')
|
||||
|
||||
if not missing and not failed:
|
||||
logger.info('Verification finished successfully.')
|
||||
return 0, 0
|
||||
else:
|
||||
logger.error(f'Verification failed, {len(failed)} file(s) corrupted, {len(missing)} file(s) are missing.')
|
||||
if print_command:
|
||||
logger.info(f'Run "legendary repair {args.app_name}" to repair your game installation.')
|
||||
return len(failed), len(missing)
|
||||
|
||||
def import_game(self, args: LgndrImportGameArgs) -> None:
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
|
||||
get_boolean_choice = args.get_boolean_choice
|
||||
|
||||
# make sure path is absolute
|
||||
args.app_path = os.path.abspath(args.app_path)
|
||||
args.app_name = self._resolve_aliases(args.app_name)
|
||||
|
||||
if not os.path.exists(args.app_path):
|
||||
logger.error(f'Specified path "{args.app_path}" does not exist!')
|
||||
return
|
||||
|
||||
if self.core.is_installed(args.app_name):
|
||||
logger.error('Game is already installed!')
|
||||
return
|
||||
|
||||
if not self.core.login():
|
||||
logger.error('Log in failed!')
|
||||
return
|
||||
|
||||
# do some basic checks
|
||||
game = self.core.get_game(args.app_name, update_meta=True, platform=args.platform)
|
||||
if not game:
|
||||
logger.fatal(f'Did not find game "{args.app_name}" on account.')
|
||||
return
|
||||
|
||||
if game.is_dlc:
|
||||
release_info = game.metadata.get('mainGameItem', {}).get('releaseInfo')
|
||||
if release_info:
|
||||
main_game_appname = release_info[0]['appId']
|
||||
main_game_title = game.metadata['mainGameItem']['title']
|
||||
if not self.core.is_installed(main_game_appname):
|
||||
logger.error(f'Import candidate is DLC but base game "{main_game_title}" '
|
||||
f'(App name: "{main_game_appname}") is not installed!')
|
||||
return
|
||||
else:
|
||||
logger.fatal(f'Unable to get base game information for DLC, cannot continue.')
|
||||
return
|
||||
|
||||
# get everything needed for import from core, then run additional checks.
|
||||
manifest, igame = self.core.import_game(game, args.app_path, platform=args.platform)
|
||||
exe_path = os.path.join(args.app_path, manifest.meta.launch_exe.lstrip('/'))
|
||||
# check if most files at least exist or if user might have specified the wrong directory
|
||||
total = len(manifest.file_manifest_list.elements)
|
||||
found = sum(os.path.exists(os.path.join(args.app_path, f.filename))
|
||||
for f in manifest.file_manifest_list.elements)
|
||||
ratio = found / total
|
||||
|
||||
if not found:
|
||||
logger.error(f'No files belonging to {"DLC" if game.is_dlc else "Game"} "{game.app_title}" '
|
||||
f'({game.app_name}) found in the specified location, please verify that the path is correct.')
|
||||
if not game.is_dlc:
|
||||
# check if game folder is in path, suggest alternative
|
||||
folder = game.metadata.get('customAttributes', {}).get('FolderName', {}).get('value', game.app_name)
|
||||
if folder and folder not in args.app_path:
|
||||
new_path = os.path.join(args.app_path, folder)
|
||||
logger.info(f'Did you mean "{new_path}"?')
|
||||
return
|
||||
|
||||
if not game.is_dlc and not os.path.exists(exe_path) and not args.disable_check:
|
||||
logger.error(f'Game executable could not be found at "{exe_path}", '
|
||||
f'please verify that the specified path is correct.')
|
||||
return
|
||||
|
||||
if ratio < 0.95:
|
||||
logger.warning('Some files are missing from the game installation, install may not '
|
||||
'match latest Epic Games Store version or might be corrupted.')
|
||||
else:
|
||||
logger.info(f'{"DLC" if game.is_dlc else "Game"} install appears to be complete.')
|
||||
|
||||
self.core.install_game(igame)
|
||||
if igame.needs_verification:
|
||||
logger.info(f'NOTE: The {"DLC" if game.is_dlc else "Game"} installation will have to be '
|
||||
f'verified before it can be updated with legendary.')
|
||||
logger.info(f'Run "legendary repair {args.app_name}" to do so.')
|
||||
else:
|
||||
logger.info(f'Installation had Epic Games Launcher metadata for version "{igame.version}", '
|
||||
f'verification will not be required.')
|
||||
|
||||
# check for importable DLC
|
||||
if not args.skip_dlcs:
|
||||
dlcs = self.core.get_dlc_for_game(game.app_name)
|
||||
if dlcs:
|
||||
logger.info(f'Found {len(dlcs)} items of DLC that could be imported.')
|
||||
import_dlc = True
|
||||
if not args.yes and not args.with_dlcs:
|
||||
if not get_boolean_choice(f'Do you wish to automatically attempt to import all DLCs?'):
|
||||
import_dlc = False
|
||||
|
||||
if import_dlc:
|
||||
for dlc in dlcs:
|
||||
args.app_name = dlc.app_name
|
||||
self.import_game(args)
|
||||
|
||||
logger.info(f'{"DLC" if game.is_dlc else "Game"} "{game.app_title}" has been imported.')
|
||||
return
|
||||
|
||||
def move(self, args):
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
|
||||
|
||||
app_name = self._resolve_aliases(args.app_name)
|
||||
igame = self.core.get_installed_game(app_name, skip_sync=True)
|
||||
if not igame:
|
||||
logger.error(f'No installed game found for "{app_name}"')
|
||||
return
|
||||
|
||||
old_base, game_folder = os.path.split(igame.install_path.replace('\\', '/'))
|
||||
new_path = os.path.join(args.new_path, game_folder)
|
||||
logger.info(f'Moving "{game_folder}" from "{old_base}" to "{args.new_path}"')
|
||||
|
||||
if not args.skip_move:
|
||||
try:
|
||||
if not os.path.exists(args.new_path):
|
||||
os.makedirs(args.new_path)
|
||||
|
||||
os.rename(igame.install_path, new_path)
|
||||
except Exception as e:
|
||||
if isinstance(e, OSError) and e.errno == 18:
|
||||
logger.error(f'Moving to a different drive is not supported. Move the folder manually to '
|
||||
f'"{new_path}" and run "legendary move {app_name} "{args.new_path}" --skip-move"')
|
||||
elif isinstance(e, FileExistsError):
|
||||
logger.error(f'The target path already contains a folder called "{game_folder}", '
|
||||
f'please remove or rename it first.')
|
||||
else:
|
||||
logger.error(f'Moving failed with unknown error {e!r}.')
|
||||
logger.info(f'Try moving the folder manually to "{new_path}" and running '
|
||||
f'"legendary move {app_name} "{args.new_path}" --skip-move"')
|
||||
return
|
||||
else:
|
||||
logger.info(f'Not moving, just rewriting legendary metadata...')
|
||||
|
||||
igame.install_path = new_path
|
||||
self.core.install_game(igame)
|
||||
logger.info('Finished.')
|
||||
|
||||
# fmt: on
|
97
rare/lgndr/core.py
Normal file
97
rare/lgndr/core.py
Normal file
|
@ -0,0 +1,97 @@
|
|||
from multiprocessing import Queue
|
||||
|
||||
from legendary.core import LegendaryCore as LegendaryCoreReal
|
||||
from legendary.models.downloading import AnalysisResult
|
||||
from legendary.models.game import Game, InstalledGame
|
||||
from legendary.models.manifest import ManifestMeta
|
||||
|
||||
from .api_exception import LgndrException, LgndrCoreLogHandler
|
||||
from .manager import DLManager
|
||||
|
||||
# import legendary.core
|
||||
# legendary.core.DLManager = DLManager
|
||||
|
||||
|
||||
# fmt: off
|
||||
class LegendaryCore(LegendaryCoreReal):
|
||||
|
||||
def __init__(self, override_config=None, timeout=10.0):
|
||||
super(LegendaryCore, self).__init__(override_config=override_config, timeout=timeout)
|
||||
self.handler = LgndrCoreLogHandler()
|
||||
self.log.addHandler(self.handler)
|
||||
|
||||
# skip_sync defaults to false but since Rare is persistent, skip by default
|
||||
# def get_installed_game(self, app_name, skip_sync=True) -> InstalledGame:
|
||||
# return super(LegendaryCore, self).get_installed_game(app_name, skip_sync)
|
||||
|
||||
def prepare_download(self, game: Game, base_game: Game = None, base_path: str = '',
|
||||
status_q: Queue = None, max_shm: 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_filter: list = None,
|
||||
file_exclude_filter: list = None, file_install_tag: list = None,
|
||||
dl_optimizations: bool = False, dl_timeout: int = 10,
|
||||
repair: bool = False, repair_use_latest: bool = False,
|
||||
disable_delta: bool = False, override_delta_manifest: str = '',
|
||||
egl_guid: str = '', preferred_cdn: str = None,
|
||||
disable_https: bool = False) -> (DLManager, AnalysisResult, ManifestMeta):
|
||||
dlm, analysis, igame = super(LegendaryCore, self).prepare_download(
|
||||
game=game, base_game=base_game, base_path=base_path,
|
||||
status_q=status_q, max_shm=max_shm, 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_filter=file_prefix_filter,
|
||||
file_exclude_filter=file_exclude_filter, file_install_tag=file_install_tag,
|
||||
dl_optimizations=dl_optimizations, dl_timeout=dl_timeout,
|
||||
repair=repair, repair_use_latest=repair_use_latest,
|
||||
disable_delta=disable_delta, override_delta_manifest=override_delta_manifest,
|
||||
egl_guid=egl_guid, preferred_cdn=preferred_cdn,
|
||||
disable_https=disable_https
|
||||
)
|
||||
# lk: monkeypatch run_real (the method that emits the stats) into DLManager
|
||||
dlm.run_real = DLManager.run_real.__get__(dlm, DLManager)
|
||||
# lk: set the queue for reporting statistics back the UI
|
||||
dlm.status_queue = Queue()
|
||||
# lk: set the queue to send control signals to the DLManager
|
||||
# lk: this doesn't exist in the original class, but it is monkeypatched in
|
||||
dlm.signals_queue = Queue()
|
||||
return dlm, analysis, igame
|
||||
|
||||
def uninstall_game(self, installed_game: InstalledGame, delete_files=True, delete_root_directory=False):
|
||||
try:
|
||||
super(LegendaryCore, self).uninstall_game(installed_game, delete_files, delete_root_directory)
|
||||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
pass
|
||||
|
||||
def egl_import(self, app_name):
|
||||
try:
|
||||
super(LegendaryCore, self).egl_import(app_name)
|
||||
except LgndrException as ret:
|
||||
raise ret
|
||||
finally:
|
||||
pass
|
||||
|
||||
def egl_export(self, app_name):
|
||||
try:
|
||||
super(LegendaryCore, self).egl_export(app_name)
|
||||
except LgndrException as ret:
|
||||
raise ret
|
||||
finally:
|
||||
pass
|
||||
|
||||
def prepare_overlay_install(self, path=None):
|
||||
dlm, analysis_result, igame = super(LegendaryCore, self).prepare_overlay_install(path)
|
||||
# lk: monkeypatch status_q (the queue for download stats)
|
||||
dlm.run_real = DLManager.run_real.__get__(dlm, DLManager)
|
||||
# lk: set the queue for reporting statistics back the UI
|
||||
dlm.status_queue = Queue()
|
||||
# lk: set the queue to send control signals to the DLManager
|
||||
# lk: this doesn't exist in the original class, but it is monkeypatched in
|
||||
dlm.signals_queue = Queue()
|
||||
return dlm, analysis_result, igame
|
||||
|
||||
# fmt: on
|
25
rare/lgndr/downloading.py
Normal file
25
rare/lgndr/downloading.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class UIUpdate:
|
||||
"""
|
||||
Status update object sent from the manager to the CLI/GUI to update status indicators
|
||||
Inheritance doesn't work due to optional arguments in UIUpdate proper
|
||||
"""
|
||||
progress: float
|
||||
download_speed: float
|
||||
write_speed: float
|
||||
read_speed: float
|
||||
memory_usage: float
|
||||
runtime: float
|
||||
estimated_time_left: float
|
||||
processed_chunks: int
|
||||
chunk_tasks: int
|
||||
total_downloaded: float
|
||||
total_written: float
|
||||
cache_usage: float
|
||||
active_tasks: int
|
||||
download_compressed_speed: float
|
||||
current_filename: Optional[str] = None
|
229
rare/lgndr/manager.py
Normal file
229
rare/lgndr/manager.py
Normal file
|
@ -0,0 +1,229 @@
|
|||
import logging
|
||||
import os
|
||||
import queue
|
||||
import time
|
||||
from multiprocessing import Queue as MPQueue
|
||||
from multiprocessing.shared_memory import SharedMemory
|
||||
from sys import exit
|
||||
from threading import Condition, Thread
|
||||
|
||||
from legendary.downloader.mp.manager import DLManager as DLManagerReal
|
||||
from legendary.downloader.mp.workers import DLWorker, FileWorker
|
||||
from legendary.models.downloading import ChunkTask, SharedMemorySegment, TerminateWorkerTask
|
||||
|
||||
from .downloading import UIUpdate
|
||||
from .api_monkeys import DLManagerSignals
|
||||
|
||||
|
||||
# fmt: off
|
||||
class DLManager(DLManagerReal):
|
||||
# Rare: prototype to avoid undefined variable in type checkers
|
||||
signals_queue: MPQueue
|
||||
|
||||
# @staticmethod
|
||||
def run_real(self):
|
||||
self.shared_memory = SharedMemory(create=True, size=self.max_shared_memory)
|
||||
self.log.debug(f'Created shared memory of size: {self.shared_memory.size / 1024 / 1024:.02f} MiB')
|
||||
|
||||
# create the shared memory segments and add them to their respective pools
|
||||
for i in range(int(self.shared_memory.size / self.analysis.biggest_chunk)):
|
||||
_sms = SharedMemorySegment(offset=i * self.analysis.biggest_chunk,
|
||||
end=i * self.analysis.biggest_chunk + self.analysis.biggest_chunk)
|
||||
self.sms.append(_sms)
|
||||
|
||||
self.log.debug(f'Created {len(self.sms)} shared memory segments.')
|
||||
|
||||
# Create queues
|
||||
self.dl_worker_queue = MPQueue(-1)
|
||||
self.writer_queue = MPQueue(-1)
|
||||
self.dl_result_q = MPQueue(-1)
|
||||
self.writer_result_q = MPQueue(-1)
|
||||
|
||||
self.log.info(f'Starting download workers...')
|
||||
for i in range(self.max_workers):
|
||||
w = DLWorker(f'DLWorker {i + 1}', self.dl_worker_queue, self.dl_result_q,
|
||||
self.shared_memory.name, logging_queue=self.logging_queue,
|
||||
dl_timeout=self.dl_timeout)
|
||||
self.children.append(w)
|
||||
w.start()
|
||||
|
||||
self.log.info('Starting file writing worker...')
|
||||
writer_p = FileWorker(self.writer_queue, self.writer_result_q, self.dl_dir,
|
||||
self.shared_memory.name, self.cache_dir, self.logging_queue)
|
||||
self.children.append(writer_p)
|
||||
writer_p.start()
|
||||
|
||||
num_chunk_tasks = sum(isinstance(t, ChunkTask) for t in self.tasks)
|
||||
num_dl_tasks = len(self.chunks_to_dl)
|
||||
num_tasks = len(self.tasks)
|
||||
num_shared_memory_segments = len(self.sms)
|
||||
self.log.debug(f'Chunks to download: {num_dl_tasks}, File tasks: {num_tasks}, Chunk tasks: {num_chunk_tasks}')
|
||||
|
||||
# active downloader tasks
|
||||
self.active_tasks = 0
|
||||
processed_chunks = 0
|
||||
processed_tasks = 0
|
||||
total_dl = 0
|
||||
total_write = 0
|
||||
|
||||
# synchronization conditions
|
||||
shm_cond = Condition()
|
||||
task_cond = Condition()
|
||||
self.conditions = [shm_cond, task_cond]
|
||||
|
||||
# start threads
|
||||
s_time = time.time()
|
||||
self.threads.append(Thread(target=self.download_job_manager, args=(task_cond, shm_cond)))
|
||||
self.threads.append(Thread(target=self.dl_results_handler, args=(task_cond,)))
|
||||
self.threads.append(Thread(target=self.fw_results_handler, args=(shm_cond,)))
|
||||
|
||||
for t in self.threads:
|
||||
t.start()
|
||||
|
||||
last_update = time.time()
|
||||
|
||||
# Rare: kill requested
|
||||
kill_request = False
|
||||
|
||||
while processed_tasks < num_tasks:
|
||||
delta = time.time() - last_update
|
||||
if not delta:
|
||||
time.sleep(self.update_interval)
|
||||
continue
|
||||
|
||||
# update all the things
|
||||
processed_chunks += self.num_processed_since_last
|
||||
processed_tasks += self.num_tasks_processed_since_last
|
||||
|
||||
total_dl += self.bytes_downloaded_since_last
|
||||
total_write += self.bytes_written_since_last
|
||||
|
||||
dl_speed = self.bytes_downloaded_since_last / delta
|
||||
dl_unc_speed = self.bytes_decompressed_since_last / delta
|
||||
w_speed = self.bytes_written_since_last / delta
|
||||
r_speed = self.bytes_read_since_last / delta
|
||||
# c_speed = self.num_processed_since_last / delta
|
||||
|
||||
# set temporary counters to 0
|
||||
self.bytes_read_since_last = self.bytes_written_since_last = 0
|
||||
self.bytes_downloaded_since_last = self.num_processed_since_last = 0
|
||||
self.bytes_decompressed_since_last = self.num_tasks_processed_since_last = 0
|
||||
last_update = time.time()
|
||||
|
||||
perc = (processed_chunks / num_chunk_tasks) * 100
|
||||
runtime = time.time() - s_time
|
||||
total_avail = len(self.sms)
|
||||
total_used = (num_shared_memory_segments - total_avail) * (self.analysis.biggest_chunk / 1024 / 1024)
|
||||
|
||||
if runtime and processed_chunks:
|
||||
average_speed = processed_chunks / runtime
|
||||
estimate = (num_chunk_tasks - processed_chunks) / average_speed
|
||||
hours, estimate = int(estimate // 3600), estimate % 3600
|
||||
minutes, seconds = int(estimate // 60), int(estimate % 60)
|
||||
|
||||
rt_hours, runtime = int(runtime // 3600), runtime % 3600
|
||||
rt_minutes, rt_seconds = int(runtime // 60), int(runtime % 60)
|
||||
else:
|
||||
estimate = 0
|
||||
hours = minutes = seconds = 0
|
||||
rt_hours = rt_minutes = rt_seconds = 0
|
||||
|
||||
# Rare: Disable up to INFO logging level for the segment below
|
||||
log_level = self.log.level
|
||||
self.log.setLevel(logging.ERROR)
|
||||
self.log.info(f'= Progress: {perc:.02f}% ({processed_chunks}/{num_chunk_tasks}), '
|
||||
f'Running for {rt_hours:02d}:{rt_minutes:02d}:{rt_seconds:02d}, '
|
||||
f'ETA: {hours:02d}:{minutes:02d}:{seconds:02d}')
|
||||
self.log.info(f' - Downloaded: {total_dl / 1024 / 1024:.02f} MiB, '
|
||||
f'Written: {total_write / 1024 / 1024:.02f} MiB')
|
||||
self.log.info(f' - Cache usage: {total_used:.02f} MiB, active tasks: {self.active_tasks}')
|
||||
self.log.info(f' + Download\t- {dl_speed / 1024 / 1024:.02f} MiB/s (raw) '
|
||||
f'/ {dl_unc_speed / 1024 / 1024:.02f} MiB/s (decompressed)')
|
||||
self.log.info(f' + Disk\t- {w_speed / 1024 / 1024:.02f} MiB/s (write) / '
|
||||
f'{r_speed / 1024 / 1024:.02f} MiB/s (read)')
|
||||
# Rare: Restore previous logging level
|
||||
self.log.setLevel(log_level)
|
||||
|
||||
# send status update to back to instantiator (if queue exists)
|
||||
if self.status_queue:
|
||||
try:
|
||||
self.status_queue.put(UIUpdate(
|
||||
progress=perc, download_speed=dl_unc_speed, write_speed=w_speed, read_speed=r_speed,
|
||||
runtime=round(runtime),
|
||||
estimated_time_left=round(estimate),
|
||||
processed_chunks=processed_chunks,
|
||||
chunk_tasks=num_chunk_tasks,
|
||||
total_downloaded=total_dl,
|
||||
total_written=total_write,
|
||||
cache_usage=total_used,
|
||||
active_tasks=self.active_tasks,
|
||||
download_compressed_speed=dl_speed,
|
||||
memory_usage=total_used * 1024 * 1024
|
||||
), timeout=1.0)
|
||||
except Exception as e:
|
||||
self.log.warning(f'Failed to send status update to queue: {e!r}')
|
||||
|
||||
# Rare: queue of control signals
|
||||
try:
|
||||
signals: DLManagerSignals = self.signals_queue.get(timeout=0.5)
|
||||
self.log.warning('Immediate stop requested!')
|
||||
if signals.kill is True:
|
||||
# lk: graceful but not what legendary does
|
||||
self.running = False
|
||||
# send conditions to unlock threads if they aren't already
|
||||
for cond in self.conditions:
|
||||
with cond:
|
||||
cond.notify()
|
||||
kill_request = True
|
||||
break
|
||||
# # lk: alternative way, but doesn't clean shm
|
||||
# for i in range(self.max_workers):
|
||||
# self.dl_worker_queue.put_nowait(TerminateWorkerTask())
|
||||
#
|
||||
# self.log.info('Waiting for installation to finish...')
|
||||
# self.writer_queue.put_nowait(TerminateWorkerTask())
|
||||
# raise KeyboardInterrupt
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
time.sleep(self.update_interval)
|
||||
|
||||
for i in range(self.max_workers):
|
||||
self.dl_worker_queue.put_nowait(TerminateWorkerTask())
|
||||
|
||||
self.log.info('Waiting for installation to finish...')
|
||||
self.writer_queue.put_nowait(TerminateWorkerTask())
|
||||
|
||||
writer_p.join(timeout=10.0)
|
||||
if writer_p.exitcode is None:
|
||||
self.log.warning(f'Terminating writer process, no exit code!')
|
||||
writer_p.terminate()
|
||||
|
||||
# forcibly kill DL workers that are not actually dead yet
|
||||
for child in self.children:
|
||||
if child.exitcode is None:
|
||||
child.terminate()
|
||||
|
||||
# make sure all the threads are dead.
|
||||
for t in self.threads:
|
||||
t.join(timeout=5.0)
|
||||
if t.is_alive():
|
||||
self.log.warning(f'Thread did not terminate! {repr(t)}')
|
||||
|
||||
# clean up resume file
|
||||
if self.resume_file and not kill_request:
|
||||
try:
|
||||
os.remove(self.resume_file)
|
||||
except OSError as e:
|
||||
self.log.warning(f'Failed to remove resume file: {e!r}')
|
||||
|
||||
# close up shared memory
|
||||
self.shared_memory.close()
|
||||
self.shared_memory.unlink()
|
||||
self.shared_memory = None
|
||||
|
||||
self.log.info('All done! Download manager quitting...')
|
||||
# finally, exit the process.
|
||||
exit(0)
|
||||
|
||||
# fmt: on
|
65
rare/models/install.py
Normal file
65
rare/models/install.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
import os
|
||||
import platform as pf
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Callable, Dict
|
||||
|
||||
from legendary.models.downloading import AnalysisResult, ConditionCheckResult
|
||||
from legendary.models.game import Game, InstalledGame
|
||||
|
||||
from rare.lgndr.manager import DLManager
|
||||
|
||||
|
||||
@dataclass
|
||||
class InstallOptionsModel:
|
||||
app_name: str
|
||||
base_path: str = ""
|
||||
shared_memory: int = 1024
|
||||
max_workers: int = os.cpu_count() * 2
|
||||
force: bool = False
|
||||
platform: str = "Windows"
|
||||
install_tag: Optional[List[str]] = None
|
||||
order_opt: bool = False
|
||||
repair_mode: bool = False
|
||||
repair_and_update: bool = False
|
||||
no_install: bool = False
|
||||
ignore_space: bool = False
|
||||
skip_dlcs: bool = False
|
||||
with_dlcs: bool = False
|
||||
# Rare's internal arguments
|
||||
# FIXME: Do we really need all of these?
|
||||
create_shortcut: bool = True
|
||||
overlay: bool = False
|
||||
update: bool = False
|
||||
silent: bool = False
|
||||
install_preqs: bool = pf.system() == "Windows"
|
||||
|
||||
def __post_init__(self):
|
||||
self.sdl_prompt: Callable[[str, str], list] = \
|
||||
lambda app_name, title: self.install_tag if self.install_tag is not None else [""]
|
||||
|
||||
def as_install_kwargs(self) -> Dict:
|
||||
return {
|
||||
k: getattr(self, k)
|
||||
for k in self.__dict__
|
||||
if k not in ["update", "silent", "create_shortcut", "overlay", "install_preqs"]
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class InstallDownloadModel:
|
||||
dlm: DLManager
|
||||
analysis: AnalysisResult
|
||||
igame: InstalledGame
|
||||
game: Game
|
||||
repair: bool
|
||||
repair_file: str
|
||||
res: ConditionCheckResult
|
||||
|
||||
|
||||
@dataclass
|
||||
class InstallQueueItemModel:
|
||||
download: Optional[InstallDownloadModel] = None
|
||||
options: Optional[InstallOptionsModel] = None
|
||||
|
||||
def __bool__(self):
|
||||
return (self.download is not None) and (self.options is not None)
|
|
@ -1,6 +1,6 @@
|
|||
from PyQt5.QtCore import QObject, pyqtSignal
|
||||
|
||||
from rare.utils.models import InstallOptionsModel
|
||||
from .install import InstallOptionsModel
|
||||
|
||||
|
||||
class GlobalSignals(QObject):
|
||||
|
|
|
@ -8,7 +8,7 @@ and only ONCE!
|
|||
from argparse import Namespace
|
||||
from typing import Optional
|
||||
|
||||
from legendary.core import LegendaryCore
|
||||
from rare.lgndr.core import LegendaryCore
|
||||
|
||||
from rare.models.apiresults import ApiResults
|
||||
from rare.models.signals import GlobalSignals
|
||||
|
|
|
@ -14,10 +14,11 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
|||
class Ui_ImportGroup(object):
|
||||
def setupUi(self, ImportGroup):
|
||||
ImportGroup.setObjectName("ImportGroup")
|
||||
ImportGroup.resize(501, 136)
|
||||
ImportGroup.resize(501, 162)
|
||||
ImportGroup.setWindowTitle("ImportGroup")
|
||||
ImportGroup.setWindowFilePath("")
|
||||
self.formLayout = QtWidgets.QFormLayout(ImportGroup)
|
||||
self.formLayout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
|
||||
self.formLayout.setObjectName("formLayout")
|
||||
self.path_edit_label = QtWidgets.QLabel(ImportGroup)
|
||||
self.path_edit_label.setObjectName("path_edit_label")
|
||||
|
@ -40,6 +41,15 @@ class Ui_ImportGroup(object):
|
|||
self.import_folder_check.setFont(font)
|
||||
self.import_folder_check.setObjectName("import_folder_check")
|
||||
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.import_folder_check)
|
||||
self.import_dlcs_label = QtWidgets.QLabel(ImportGroup)
|
||||
self.import_dlcs_label.setObjectName("import_dlcs_label")
|
||||
self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.import_dlcs_label)
|
||||
self.import_dlcs_check = QtWidgets.QCheckBox(ImportGroup)
|
||||
font = QtGui.QFont()
|
||||
font.setItalic(True)
|
||||
self.import_dlcs_check.setFont(font)
|
||||
self.import_dlcs_check.setObjectName("import_dlcs_check")
|
||||
self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.import_dlcs_check)
|
||||
self.button_info_layout = QtWidgets.QHBoxLayout()
|
||||
self.button_info_layout.setObjectName("button_info_layout")
|
||||
self.import_button = QtWidgets.QPushButton(ImportGroup)
|
||||
|
@ -50,11 +60,7 @@ class Ui_ImportGroup(object):
|
|||
self.import_button.setSizePolicy(sizePolicy)
|
||||
self.import_button.setObjectName("import_button")
|
||||
self.button_info_layout.addWidget(self.import_button)
|
||||
self.info_label = QtWidgets.QLabel(ImportGroup)
|
||||
self.info_label.setText("")
|
||||
self.info_label.setObjectName("info_label")
|
||||
self.button_info_layout.addWidget(self.info_label)
|
||||
self.formLayout.setLayout(3, QtWidgets.QFormLayout.FieldRole, self.button_info_layout)
|
||||
self.formLayout.setLayout(4, QtWidgets.QFormLayout.FieldRole, self.button_info_layout)
|
||||
|
||||
self.retranslateUi(ImportGroup)
|
||||
QtCore.QMetaObject.connectSlotsByName(ImportGroup)
|
||||
|
@ -66,6 +72,8 @@ class Ui_ImportGroup(object):
|
|||
self.app_name_label.setText(_translate("ImportGroup", "Override app name"))
|
||||
self.import_folder_label.setText(_translate("ImportGroup", "Import all folders"))
|
||||
self.import_folder_check.setText(_translate("ImportGroup", "Scan the installation path for game folders and import them"))
|
||||
self.import_dlcs_label.setText(_translate("ImportGroup", "Import DLCs"))
|
||||
self.import_dlcs_check.setText(_translate("ImportGroup", "If a game has DLCs, try to import them too"))
|
||||
self.import_button.setText(_translate("ImportGroup", "Import Game"))
|
||||
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>501</width>
|
||||
<height>136</height>
|
||||
<height>162</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
|
@ -20,6 +20,9 @@
|
|||
<string>Import EGL game from a directory</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<property name="labelAlignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="path_edit_label">
|
||||
<property name="text">
|
||||
|
@ -59,7 +62,26 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="import_dlcs_label">
|
||||
<property name="text">
|
||||
<string>Import DLCs</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QCheckBox" name="import_dlcs_check">
|
||||
<property name="font">
|
||||
<font>
|
||||
<italic>true</italic>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>If a game has DLCs, try to import them too</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<layout class="QHBoxLayout" name="button_info_layout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="import_button">
|
||||
|
@ -74,13 +96,6 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="info_label">
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
|
|
|
@ -36,7 +36,7 @@ from PyQt5.QtWidgets import (
|
|||
|
||||
from rare.utils.paths import tmp_dir
|
||||
from rare.utils.qt_requests import QtRequestManager
|
||||
from rare.utils.utils import icon as qta_icon
|
||||
from rare.utils.misc import icon as qta_icon
|
||||
|
||||
logger = getLogger("ExtraWidgets")
|
||||
|
||||
|
|
|
@ -2,20 +2,19 @@ import os
|
|||
import platform
|
||||
from logging import getLogger
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QCoreApplication, QObject, QRunnable, QStandardPaths
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QRunnable, QStandardPaths
|
||||
from legendary.core import LegendaryCore
|
||||
from legendary.models.game import VerifyResult
|
||||
from legendary.utils.lfs import validate_files
|
||||
|
||||
from rare.shared import LegendaryCoreSingleton
|
||||
from rare.lgndr.api_arguments import LgndrVerifyGameArgs, LgndrUninstallGameArgs
|
||||
from rare.lgndr.api_monkeys import LgndrIndirectStatus
|
||||
from rare.lgndr.cli import LegendaryCLI
|
||||
from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton
|
||||
from rare.utils import config_helper
|
||||
|
||||
logger = getLogger("Legendary Utils")
|
||||
|
||||
|
||||
def uninstall(app_name: str, core: LegendaryCore, options=None):
|
||||
if not options:
|
||||
options = {"keep_files": False}
|
||||
def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False, keep_config=False):
|
||||
igame = core.get_installed_game(app_name)
|
||||
|
||||
# remove shortcuts link
|
||||
|
@ -40,31 +39,24 @@ def uninstall(app_name: str, core: LegendaryCore, options=None):
|
|||
if os.path.exists(start_menu_shortcut):
|
||||
os.remove(start_menu_shortcut)
|
||||
|
||||
try:
|
||||
# Remove DLC first so directory is empty when game uninstall runs
|
||||
dlcs = core.get_dlc_for_game(app_name)
|
||||
for dlc in dlcs:
|
||||
if (idlc := core.get_installed_game(dlc.app_name)) is not None:
|
||||
logger.info(f'Uninstalling DLC "{dlc.app_name}"...')
|
||||
core.uninstall_game(idlc, delete_files=not options["keep_files"])
|
||||
|
||||
logger.info(f'Removing "{igame.title}" from "{igame.install_path}"...')
|
||||
core.uninstall_game(
|
||||
igame, delete_files=not options["keep_files"], delete_root_directory=True
|
||||
status = LgndrIndirectStatus()
|
||||
LegendaryCLI(core).uninstall_game(
|
||||
LgndrUninstallGameArgs(
|
||||
app_name=app_name,
|
||||
keep_files=keep_files,
|
||||
indirect_status=status,
|
||||
yes=True,
|
||||
)
|
||||
logger.info("Game has been uninstalled.")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Removing game failed: {e!r}, please remove {igame.install_path} manually."
|
||||
)
|
||||
if not options["keep_files"]:
|
||||
)
|
||||
if not keep_config:
|
||||
logger.info("Removing sections in config file")
|
||||
config_helper.remove_section(app_name)
|
||||
config_helper.remove_section(f"{app_name}.env")
|
||||
|
||||
config_helper.save_config()
|
||||
|
||||
return status.success, status.message
|
||||
|
||||
|
||||
def update_manifest(app_name: str, core: LegendaryCore):
|
||||
game = core.get_game(app_name)
|
||||
|
@ -78,146 +70,65 @@ def update_manifest(app_name: str, core: LegendaryCore):
|
|||
new_manifest = core.load_manifest(new_manifest_data)
|
||||
logger.debug(f"Base urls: {base_urls}")
|
||||
# save manifest with version name as well for testing/downgrading/etc.
|
||||
core.lgd.save_manifest(
|
||||
game.app_name, new_manifest_data, version=new_manifest.meta.build_version
|
||||
)
|
||||
|
||||
|
||||
class VerifySignals(QObject):
|
||||
status = pyqtSignal(int, int, str)
|
||||
summary = pyqtSignal(int, int, str)
|
||||
core.lgd.save_manifest(game.app_name, new_manifest_data, version=new_manifest.meta.build_version)
|
||||
|
||||
|
||||
class VerifyWorker(QRunnable):
|
||||
class Signals(QObject):
|
||||
status = pyqtSignal(str, int, int, float, float)
|
||||
result = pyqtSignal(str, bool, int, int)
|
||||
error = pyqtSignal(str, str)
|
||||
|
||||
num: int = 0
|
||||
total: int = 1 # set default to 1 to avoid DivisionByZero before it is initialized
|
||||
|
||||
def __init__(self, app_name):
|
||||
super(VerifyWorker, self).__init__()
|
||||
self.signals = VerifySignals()
|
||||
self.signals = VerifyWorker.Signals()
|
||||
self.setAutoDelete(True)
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.args = ArgumentsSingleton()
|
||||
self.app_name = app_name
|
||||
|
||||
def status_callback(self, num: int, total: int, percentage: float, speed: float):
|
||||
self.signals.status.emit(self.app_name, num, total, percentage, speed)
|
||||
|
||||
def run(self):
|
||||
if not self.core.is_installed(self.app_name):
|
||||
logger.error(f'Game "{self.app_name}" is not installed')
|
||||
return
|
||||
|
||||
logger.info(f'Loading installed manifest for "{self.app_name}"')
|
||||
igame = self.core.get_installed_game(self.app_name)
|
||||
manifest_data, _ = self.core.get_installed_manifest(self.app_name)
|
||||
manifest = self.core.load_manifest(manifest_data)
|
||||
|
||||
files = sorted(manifest.file_manifest_list.elements,
|
||||
key=lambda a: a.filename.lower())
|
||||
|
||||
# build list of hashes
|
||||
file_list = [(f.filename, f.sha_hash.hex()) for f in files]
|
||||
total = len(file_list)
|
||||
num = 0
|
||||
failed = []
|
||||
missing = []
|
||||
|
||||
logger.info(f'Verifying "{igame.title}" version "{manifest.meta.build_version}"')
|
||||
repair_file = []
|
||||
for result, path, result_hash, _ in validate_files(igame.install_path, file_list):
|
||||
num += 1
|
||||
self.signals.status.emit(num, total, self.app_name)
|
||||
|
||||
if result == VerifyResult.HASH_MATCH:
|
||||
repair_file.append(f"{result_hash}:{path}")
|
||||
continue
|
||||
elif result == VerifyResult.HASH_MISMATCH:
|
||||
logger.error(f'File does not match hash: "{path}"')
|
||||
repair_file.append(f"{result_hash}:{path}")
|
||||
failed.append(path)
|
||||
elif result == VerifyResult.FILE_MISSING:
|
||||
logger.error(f'File is missing: "{path}"')
|
||||
missing.append(path)
|
||||
else:
|
||||
logger.error(f'Other failure (see log), treating file as missing: "{path}"')
|
||||
missing.append(path)
|
||||
|
||||
# always write repair file, even if all match
|
||||
if repair_file:
|
||||
repair_filename = os.path.join(self.core.lgd.get_tmp_path(), f'{self.app_name}.repair')
|
||||
with open(repair_filename, 'w') as f:
|
||||
f.write('\n'.join(repair_file))
|
||||
logger.debug(f'Written repair file to "{repair_filename}"')
|
||||
|
||||
if not missing and not failed:
|
||||
logger.info('Verification finished successfully.')
|
||||
else:
|
||||
logger.warning(
|
||||
f'Verification failed, {len(failed)} file(s) corrupted, {len(missing)} file(s) are missing.')
|
||||
self.signals.summary.emit(len(failed), len(missing), self.app_name)
|
||||
|
||||
|
||||
# FIXME: lk: ah ef me sideways, we can't even import this thing properly
|
||||
# FIXME: lk: so copy it here
|
||||
def resolve_aliases(core: LegendaryCore, name):
|
||||
# make sure aliases exist if not yet created
|
||||
core.update_aliases(force=False)
|
||||
name = name.strip()
|
||||
# resolve alias (if any) to real app name
|
||||
return core.lgd.config.get(
|
||||
section='Legendary.aliases', option=name,
|
||||
fallback=core.lgd.aliases.get(name.lower(), name)
|
||||
)
|
||||
|
||||
|
||||
def import_game(core: LegendaryCore, app_name: str, path: str) -> str:
|
||||
_tr = QCoreApplication.translate
|
||||
logger.info(f"Import {app_name}")
|
||||
game = core.get_game(app_name, update_meta=False)
|
||||
if not game:
|
||||
return _tr("LgdUtils", "Could not get game for {}").format(app_name)
|
||||
|
||||
if core.is_installed(app_name):
|
||||
logger.error(f"{game.app_title} is already installed")
|
||||
return _tr("LgdUtils", "{} is already installed").format(game.app_title)
|
||||
|
||||
if not os.path.exists(path):
|
||||
logger.error("Path does not exist")
|
||||
return _tr("LgdUtils", "Path does not exist")
|
||||
|
||||
manifest, igame = core.import_game(game, path)
|
||||
exe_path = os.path.join(path, manifest.meta.launch_exe.lstrip("/"))
|
||||
|
||||
if not os.path.exists(exe_path):
|
||||
logger.error(f"Launch Executable of {game.app_title} does not exist")
|
||||
return _tr("LgdUtils", "Launch executable of {} does not exist").format(
|
||||
game.app_title
|
||||
cli = LegendaryCLI(self.core)
|
||||
status = LgndrIndirectStatus()
|
||||
args = LgndrVerifyGameArgs(
|
||||
app_name=self.app_name, indirect_status=status, verify_stdout=self.status_callback
|
||||
)
|
||||
|
||||
if game.is_dlc:
|
||||
release_info = game.metadata.get("mainGameItem", {}).get("releaseInfo")
|
||||
if release_info:
|
||||
main_game_appname = release_info[0]["appId"]
|
||||
main_game_title = game.metadata["mainGameItem"]["title"]
|
||||
if not core.is_installed(main_game_appname):
|
||||
return _tr("LgdUtils", "Game is a DLC, but {} is not installed").format(
|
||||
main_game_title
|
||||
# lk: first pass, verify with the current manifest
|
||||
repair_mode = False
|
||||
result = cli.verify_game(
|
||||
args, print_command=False, repair_mode=repair_mode, repair_online=not self.args.offline
|
||||
)
|
||||
if result is None:
|
||||
# lk: second pass with downloading the latest manifest
|
||||
# lk: this happens if the manifest was not found and repair_mode was not requested
|
||||
# lk: we already have checked if the directory exists before starting the worker
|
||||
try:
|
||||
# lk: this try-except block handles the exception caused by a missing manifest
|
||||
# lk: and is raised only in the case we are offline
|
||||
repair_mode = True
|
||||
result = cli.verify_game(
|
||||
args, print_command=False, repair_mode=repair_mode, repair_online=not self.args.offline
|
||||
)
|
||||
else:
|
||||
return _tr("LgdUtils", "Unable to get base game information for DLC")
|
||||
if result is None:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
self.signals.error.emit(self.app_name, status.message)
|
||||
return
|
||||
|
||||
total = len(manifest.file_manifest_list.elements)
|
||||
found = sum(
|
||||
os.path.exists(os.path.join(path, f.filename))
|
||||
for f in manifest.file_manifest_list.elements
|
||||
)
|
||||
ratio = found / total
|
||||
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)
|
||||
|
||||
if ratio < 0.9:
|
||||
logger.warning(
|
||||
"Game files are missing. It may be not the latest version or it is corrupt"
|
||||
)
|
||||
# return False
|
||||
core.install_game(igame)
|
||||
if igame.needs_verification:
|
||||
logger.info(f"{igame.title} needs verification")
|
||||
|
||||
logger.info(f"Successfully imported Game: {game.app_title}")
|
||||
return ""
|
||||
self.signals.result.emit(self.app_name, success, *result)
|
||||
|
|
|
@ -4,7 +4,7 @@ import shlex
|
|||
import subprocess
|
||||
import sys
|
||||
from logging import getLogger
|
||||
from typing import List
|
||||
from typing import List, Union
|
||||
|
||||
import qtawesome
|
||||
import requests
|
||||
|
@ -157,7 +157,7 @@ def get_latest_version():
|
|||
return "0.0.0"
|
||||
|
||||
|
||||
def get_size(b: int) -> str:
|
||||
def get_size(b: Union[int, float]) -> str:
|
||||
for i in ["", "K", "M", "G", "T", "P", "E"]:
|
||||
if b < 1024:
|
||||
return f"{b:.2f}{i}B"
|
||||
|
@ -165,7 +165,10 @@ def get_size(b: int) -> str:
|
|||
|
||||
|
||||
def get_rare_executable() -> List[str]:
|
||||
if platform.system() == "Linux" or platform.system() == "Darwin":
|
||||
# lk: detech if nuitka
|
||||
if "__compiled__" in globals():
|
||||
executable = [sys.executable]
|
||||
elif platform.system() == "Linux" or platform.system() == "Darwin":
|
||||
# TODO flatpak
|
||||
if p := os.environ.get("APPIMAGE"):
|
||||
executable = [p]
|
|
@ -1,61 +1,7 @@
|
|||
import os
|
||||
import platform as pf
|
||||
from dataclasses import field, dataclass
|
||||
from multiprocessing import Queue
|
||||
from typing import Union, List, Optional
|
||||
from typing import Union, List
|
||||
|
||||
from legendary.core import LegendaryCore
|
||||
from legendary.downloader.mp.manager import DLManager
|
||||
from legendary.models.downloading import AnalysisResult, ConditionCheckResult
|
||||
from legendary.models.game import Game, InstalledGame
|
||||
|
||||
|
||||
@dataclass
|
||||
class InstallOptionsModel:
|
||||
app_name: str
|
||||
base_path: str = ""
|
||||
max_shm: int = 1024
|
||||
max_workers: int = os.cpu_count() * 2
|
||||
repair: bool = False
|
||||
no_install: bool = False
|
||||
ignore_space_req: bool = False
|
||||
force: bool = False
|
||||
sdl_list: list = field(default_factory=lambda: [""])
|
||||
update: bool = False
|
||||
silent: bool = False
|
||||
platform: str = ""
|
||||
dl_optimizations: bool = False
|
||||
overlay: bool = False
|
||||
create_shortcut: bool = True
|
||||
install_preqs: bool = pf.system() == "Windows"
|
||||
|
||||
def set_no_install(self, enabled: bool) -> None:
|
||||
self.no_install = enabled
|
||||
|
||||
|
||||
@dataclass
|
||||
class InstallDownloadModel:
|
||||
dlmanager: DLManager
|
||||
analysis: AnalysisResult
|
||||
game: Game
|
||||
igame: InstalledGame
|
||||
repair: bool
|
||||
repair_file: str
|
||||
res: ConditionCheckResult
|
||||
|
||||
|
||||
@dataclass
|
||||
class InstallQueueItemModel:
|
||||
status_q: Optional[Queue] = None
|
||||
download: Optional[InstallDownloadModel] = None
|
||||
options: Optional[InstallOptionsModel] = None
|
||||
|
||||
def __bool__(self):
|
||||
return (
|
||||
(self.status_q is not None)
|
||||
and (self.download is not None)
|
||||
and (self.options is not None)
|
||||
)
|
||||
|
||||
|
||||
class PathSpec:
|
||||
|
@ -80,9 +26,7 @@ class PathSpec:
|
|||
|
||||
@property
|
||||
def wine_egl_programdata(self):
|
||||
return self.egl_programdata.replace("\\", "/").replace(
|
||||
"%PROGRAMDATA%", self.wine_programdata
|
||||
)
|
||||
return self.egl_programdata.replace("\\", "/").replace("%PROGRAMDATA%", self.wine_programdata)
|
||||
|
||||
def wine_egl_prefixes(self, results: int = 0) -> Union[List[str], str]:
|
||||
possible_prefixes = [
|
||||
|
|
|
@ -11,7 +11,7 @@ from legendary.core import LegendaryCore
|
|||
|
||||
import rare.resources.resources
|
||||
from rare.utils.paths import resources_path
|
||||
from rare.utils.utils import set_color_pallete, set_style_sheet
|
||||
from rare.utils.misc import set_color_pallete, set_style_sheet
|
||||
|
||||
|
||||
class RareApp(QApplication):
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
typing_extensions
|
||||
requests
|
||||
PyQt5
|
||||
QtAwesome
|
||||
psutil
|
||||
pypresence
|
||||
setuptools
|
||||
legendary-gl
|
||||
pywin32; platform_system == "Windows"
|
||||
|
|
Loading…
Reference in a new issue