1
0
Fork 0
mirror of synced 2024-06-02 10:44:40 +12:00

Implement LgndrIndirectLogger to return the last logged message from LegendaryAPI

The indirect return is stored in a `LgndrIndirectStatus` object that provides checking and unpacking features

Lgndr: `LgndrInstallGameArgs.install_tag` default to None
Lgndr: add default `move` method.
Lgndr: monkeypatch `dlm.status_queue` after preparing a download to reduce `InstallQueueItemModel`
InstallOptionsModel: implement `as_install_kwargs` to pass only relevant arguments to `LgndrInstallGameArgs` in InstallDialog
InstallOptionsModel: rename `sdl_list` to `install_tag` as they were the same thing
LegendaryUtils: Update to use `LgndrIndirectStatus`
UninstallDialog: Add option to keep configuration decoupled from keeping game data
GameUtils: Add messagebox to show error messages from legendary after uninstalling a game
InstallDialog: Update to use `LgndrIndirectStatus`
InstallDialog: Update selectable download handling to match legendary's
DownloadThread: Remove multiple instance variables, instead reference them directly from `InstallQueueItemModel` instance
ImportGroup: Replace `info_label` with an `ElideLabel` for displaying long messages
ImportGroup: Don't translate message strings in the `ImportWorker`
GameInfo: Call `repair()` if needed after verification instead of handling it locally
GamesTab: Fix string matching for capitalized strings and scroll to top on when searching
This commit is contained in:
loathingKernel 2022-07-24 01:06:35 +03:00
parent 28531eec38
commit ee5adce18b
15 changed files with 365 additions and 321 deletions

View file

@ -1,20 +1,19 @@
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.models.downloading import ConditionCheckResult
from legendary.models.game import Game
from legendary.utils.selective_dl import get_sdl_appname
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 legendary.models.downloading import ConditionCheckResult
from legendary.models.game import Game
from legendary.utils.selective_dl import games
from rare.shared import LegendaryCLISingleton, LegendaryCoreSingleton, ApiResultsSingleton, ArgumentsSingleton
from rare.ui.components.dialogs.install_dialog import Ui_InstallDialog
from rare.utils.extra_widgets import PathEdit
@ -34,7 +33,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)
@ -87,18 +85,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))
@ -112,22 +112,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)
@ -141,7 +129,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"))
@ -156,7 +144,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:
@ -166,6 +154,37 @@ 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 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
@ -176,11 +195,12 @@ class InstallDialog(QDialog, Ui_InstallDialog):
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
@ -221,6 +241,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:
self.core.lgd.config.set(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
self.core.lgd.config.remove_option(self.game.app_name, 'install_tags')
self.dl_item.download = None
self.reject_close = False
self.close()
@ -246,7 +272,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)
@ -302,52 +328,27 @@ 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:
if not self.dl_item.options.overlay:
cli = LegendaryCLISingleton()
download = InstallDownloadModel(
# *self.core.prepare_download(
*cli.install_game(LgndrInstallGameArgs(
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,
shared_memory=self.dl_item.options.shared_memory,
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=,
order_opt=self.dl_item.options.order_opt,
# dl_timeout=,
repair_mode=self.dl_item.options.repair_mode,
repair_and_update=self.dl_item.options.repair_and_update,
ignore_space=self.dl_item.options.ignore_space,
# disable_delta=,
# override_delta_manifest=,
# reset_sdl=,
sdl_prompt=lambda app_name, title: self.dl_item.options.sdl_list,)
)
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_q=self.dl_item.status_q,
path=self.dl_item.options.base_path
)
download = InstallDownloadModel(
@ -371,14 +372,14 @@ class InstallInfoWorker(QRunnable):
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

View file

@ -1,4 +1,3 @@
from enum import Enum, IntEnum
from typing import Tuple
from PyQt5.QtCore import Qt
@ -7,7 +6,6 @@ from PyQt5.QtWidgets import (
QLabel,
QVBoxLayout,
QCheckBox,
QFormLayout,
QHBoxLayout,
QPushButton,
)
@ -23,13 +21,15 @@ class UninstallDialog(QDialog):
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)
)
layout.addWidget(self.info_text)
self.keep_files = QCheckBox(self.tr("Keep Files"))
form_layout = QFormLayout()
form_layout.setContentsMargins(0, 10, 0, 10)
form_layout.addRow(QLabel(self.tr("Do you want to keep files?")), self.keep_files)
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)
button_layout = QHBoxLayout()
@ -41,22 +41,22 @@ class UninstallDialog(QDialog):
self.cancel_button = QPushButton(self.tr("Cancel"))
self.cancel_button.clicked.connect(self.cancel)
button_layout.addStretch(1)
button_layout.addWidget(self.ok_button)
button_layout.addStretch(1)
button_layout.addWidget(self.cancel_button)
layout.addLayout(button_layout)
self.setLayout(layout)
self.options: Tuple[bool, bool] = (False, False)
self.options: Tuple[bool, bool, bool] = (False, False, False)
def get_options(self) -> Tuple[bool, bool]:
def get_options(self) -> Tuple[bool, bool, bool]:
self.exec_()
return self.options
def ok(self):
self.options = (True, self.keep_files.isChecked())
self.options = (True, self.keep_files.isChecked(), self.keep_config.isChecked())
self.close()
def cancel(self):
self.options = (False, False)
self.options = (False, False, False)
self.close()

View file

@ -23,17 +23,11 @@ class DownloadThread(QThread):
status = pyqtSignal(str)
statistics = pyqtSignal(UIUpdate)
def __init__(self, core: LegendaryCore, queue_item: InstallQueueItemModel):
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.core: LegendaryCore = core
self.item: InstallQueueItemModel = item
self._kill = False
@ -42,9 +36,9 @@ class DownloadThread(QThread):
dl_stopped = False
try:
self.dlm.start()
self.item.download.dlmanager.start()
time.sleep(1)
while self.dlm.is_alive():
while self.item.download.dlmanager.is_alive():
if self._kill:
self.status.emit("stop")
logger.info("Download stopping...")
@ -52,15 +46,15 @@ class DownloadThread(QThread):
# The code below is a temporary solution.
# It should be removed once legendary supports stopping downloads more gracefully.
self.dlm.running = False
self.item.download.dlmanager.running = False
# send conditions to unlock threads if they aren't already
for cond in self.dlm.conditions:
for cond in self.item.download.dlmanager.conditions:
with cond:
cond.notify()
# make sure threads are dead.
for t in self.dlm.threads:
for t in self.item.download.dlmanager.threads:
t.join(timeout=5.0)
if t.is_alive():
logger.warning(f"Thread did not terminate! {repr(t)}")
@ -74,10 +68,10 @@ class DownloadThread(QThread):
"Writer results",
),
(
self.dlm.dl_worker_queue,
self.dlm.writer_queue,
self.dlm.dl_result_q,
self.dlm.writer_result_q,
self.item.download.dlmanager.dl_worker_queue,
self.item.download.dlmanager.writer_queue,
self.item.download.dlmanager.dl_result_q,
self.item.download.dlmanager.writer_result_q,
),
):
logger.debug(f'Cleaning up queue "{name}"')
@ -90,22 +84,22 @@ class DownloadThread(QThread):
except AttributeError:
logger.warning(f"Queue {name} did not close")
if self.dlm.writer_queue:
if self.item.download.dlmanager.writer_queue:
# cancel installation
self.dlm.writer_queue.put_nowait(WriterTask("", kill=True))
self.item.download.dlmanager.writer_queue.put_nowait(WriterTask("", kill=True))
# forcibly kill DL workers that are not actually dead yet
for child in self.dlm.children:
for child in self.item.download.dlmanager.children:
if child.exitcode is None:
child.terminate()
if self.dlm.shared_memory:
if self.item.download.dlmanager.shared_memory:
# close up shared memory
self.dlm.shared_memory.close()
self.dlm.shared_memory.unlink()
self.dlm.shared_memory = None
self.item.download.dlmanager.shared_memory.close()
self.item.download.dlmanager.shared_memory.unlink()
self.item.download.dlmanager.shared_memory = None
self.dlm.kill()
self.item.download.dlmanager.kill()
# force kill any threads that are somehow still alive
for proc in psutil.process_iter():
@ -126,11 +120,11 @@ class DownloadThread(QThread):
dl_stopped = True
try:
if not dl_stopped:
self.statistics.emit(self.status_q.get(timeout=1))
self.statistics.emit(self.item.download.dlmanager.status_queue.get(timeout=1))
except queue.Empty:
pass
self.dlm.join()
self.item.download.dlmanager.join()
except Exception as e:
logger.error(
@ -145,20 +139,20 @@ class DownloadThread(QThread):
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)
game = self.core.get_game(self.item.download.igame.app_name)
if self.queue_item.options.overlay:
if self.item.options.overlay:
self.signals.overlay_installation_finished.emit()
self.core.finish_overlay_install(self.igame)
self.core.finish_overlay_install(self.item.download.igame)
self.status.emit("finish")
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)
self._handle_postinstall(postinstall, self.item.download.igame)
dlcs = self.core.get_dlc_for_game(self.igame.app_name)
dlcs = self.core.get_dlc_for_game(self.item.download.igame.app_name)
if dlcs:
print("The following DLCs are available for this game:")
for dlc in dlcs:
@ -179,21 +173,21 @@ class DownloadThread(QThread):
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 and self.item.download.repair and os.path.exists(self.item.download.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(self.repair_file)
if old_igame and old_igame.install_tags != self.igame.install_tags:
old_igame.install_tags = self.igame.install_tags
os.remove(self.item.download.repair_file)
if old_igame and old_igame.install_tags != self.item.download.igame.install_tags:
old_igame.install_tags = self.item.download.igame.install_tags
logger.info("Deleting now untagged files.")
self.core.uninstall_tag(old_igame)
self.core.install_game(old_igame)
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"):
if not self.item.options.update and self.item.options.create_shortcut:
if not create_desktop_link(self.item.options.app_name, self.core, "desktop"):
# maybe add it to download summary, to show in finished downloads
pass
else:
@ -204,7 +198,7 @@ class DownloadThread(QThread):
def _handle_postinstall(self, postinstall, igame):
logger.info(f"Postinstall info: {postinstall}")
if platform.system() == "Windows":
if self.queue_item.options.install_preqs:
if self.item.options.install_preqs:
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)
@ -218,7 +212,7 @@ class DownloadThread(QThread):
proc.start(fullpath, postinstall.get("args", []))
proc.waitForFinished() # wait, because it is inside the thread
else:
self.core.prereq_installed(self.igame.app_name)
self.core.prereq_installed(self.item.download.igame.app_name)
else:
logger.info("Automatic installation not available on Linux.")

View file

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

View file

@ -129,7 +129,7 @@ class GameInfo(QWidget, Ui_GameInfo):
)
return
self.signals.install_game.emit(
InstallOptionsModel(app_name=self.game.app_name, repair_mode=True, update=True)
InstallOptionsModel(app_name=self.game.app_name, repair_mode=True, repair_and_update=True, update=True)
)
def verify(self):
@ -163,24 +163,24 @@ class GameInfo(QWidget, Ui_GameInfo):
@pyqtSlot(str, bool, int, int)
def verify_result(self, app_name, success, failed, missing):
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("Game 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"))
QMessageBox.warning(self, self.tr("Warning - {}").format(igame.title), self.tr("Something went wrong"))
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?"
).format(failed, missing),
@ -188,9 +188,7 @@ class GameInfo(QWidget, Ui_GameInfo):
QMessageBox.Yes,
)
if ans == QMessageBox.Yes:
self.signals.install_game.emit(
InstallOptionsModel(app_name=app_name, repair_mode=True, update=True)
)
self.repair()
self.verify_widget.setCurrentIndex(0)
self.verify_threads.pop(app_name)
self.move_button.setEnabled(True)

View file

@ -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
proceed, keep_files = UninstallDialog(game).get_options()
proceed, keep_files, keep_config = UninstallDialog(game).get_options()
if not proceed:
return False
legendary_utils.uninstall_game(self.core, game.app_name, keep_files)
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

View file

@ -1,6 +1,5 @@
import json
import os
from argparse import Namespace
from dataclasses import dataclass
from enum import IntEnum
from logging import getLogger
@ -13,9 +12,11 @@ from PyQt5.QtWidgets import QFileDialog, QGroupBox, QCompleter, QTreeView, QHead
from rare.lgndr.api_arguments import LgndrImportGameArgs
from rare.lgndr.api_exception import LgndrException
from rare.lgndr.api_monkeys import LgndrIndirectStatus
from rare.shared import LegendaryCLISingleton, LegendaryCoreSingleton, GlobalSignalsSingleton, ApiResultsSingleton
from rare.ui.components.tabs.games.import_sync.import_group import Ui_ImportGroup
from rare.utils.extra_widgets import IndicatorLineEdit, PathEdit
from rare.widgets.elide_label import ElideLabel
logger = getLogger("Import")
@ -46,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
@ -59,7 +62,6 @@ class ImportWorker(QRunnable):
super(ImportWorker, self).__init__()
self.signals = self.Signals()
self.core = LegendaryCoreSingleton()
self.tr = lambda message: qApp.translate("ImportThread", message)
self.path = Path(path)
self.app_name = app_name
@ -83,29 +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
app_title = self.core.get_game(app_name).app_title
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 = f"{app_title} - {message}"
result.message = message
else:
result.result = ImportResult.SUCCESS
result.message = self.tr("{} - Imported successfully").format(app_title)
else:
result.message = self.tr("Could not find AppName for {}").format(str(path))
return result
def __import_game(self, path: Path, app_name: str, app_title: str):
cli = LegendaryCLISingleton()
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
)
return cli.import_game(args)
cli.import_game(args)
return status.success, status.message
class AppNameCompleter(QCompleter):
@ -194,6 +197,10 @@ class ImportGroup(QGroupBox):
self.ui.import_button.clicked.connect(
lambda: self.import_pressed(self.path_edit.text())
)
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]:
@ -207,7 +214,7 @@ class ImportGroup(QGroupBox):
return False, path, ""
def path_changed(self, path):
self.ui.info_label.setText("")
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))
@ -223,7 +230,7 @@ class ImportGroup(QGroupBox):
return False, text, IndicatorLineEdit.reasons.game_not_installed
def app_name_changed(self, app_name: str):
self.ui.info_label.setText("")
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(
@ -272,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("Success: {}").format(res.message)
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]
@ -301,15 +308,15 @@ class ImportGroup(QGroupBox):
details: List = []
for res in success:
details.append(
self.tr("Success: {}").format(res.message)
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()

View file

@ -1,8 +1,8 @@
from dataclasses import dataclass
from multiprocessing import Queue
from typing import Callable, List
from typing import Callable, List, Optional
from .api_monkeys import get_boolean_choice
from .api_monkeys import get_boolean_choice, LgndrIndirectStatus
"""
@dataclass(kw_only=True)
@ -25,6 +25,7 @@ class LgndrImportGameArgs:
with_dlcs: bool = False
yes: bool = False
# Rare: Extra arguments
indirect_status: LgndrIndirectStatus = LgndrIndirectStatus()
get_boolean_choice: Callable[[str, bool], bool] = lambda prompt, default=True: default
@ -34,6 +35,7 @@ class LgndrUninstallGameArgs:
keep_files: bool = False
yes: bool = False
# Rare: Extra arguments
indirect_status: LgndrIndirectStatus = LgndrIndirectStatus()
get_boolean_choice: Callable[[str, bool], bool] = lambda prompt, default=True: default
@ -41,6 +43,7 @@ class LgndrUninstallGameArgs:
class LgndrVerifyGameArgs:
app_name: str
# Rare: Extra arguments
indirect_status: LgndrIndirectStatus = LgndrIndirectStatus()
verify_stdout: Callable[[int, int, float, float], None] = lambda a0, a1, a2, a3: print(
f"Verification progress: {a0}/{a1} ({a2:.01f}%) [{a3:.1f} MiB/s]\t\r"
)
@ -50,7 +53,6 @@ class LgndrVerifyGameArgs:
class LgndrInstallGameArgs:
app_name: str
base_path: str = ""
status_q: Queue = None
shared_memory: int = 0
max_workers: int = 0
force: bool = False
@ -62,7 +64,7 @@ class LgndrInstallGameArgs:
platform: str = "Windows"
file_prefix: List = None
file_exclude_prefix: List = None
install_tag: List = None
install_tag: Optional[List[str]] = None
order_opt: bool = False
dl_timeout: int = 10
repair_mode: bool = False
@ -79,6 +81,7 @@ class LgndrInstallGameArgs:
disable_https: bool = False
yes: bool = True
# Rare: Extra arguments
indirect_status: LgndrIndirectStatus = LgndrIndirectStatus()
get_boolean_choice: Callable[[str, bool], bool] = lambda prompt, default=True: default
sdl_prompt: Callable[[str, str], List[str]] = lambda sdl_data, title: [""]
verify_stdout: Callable[[int, int, float, float], None] = lambda a0, a1, a2, a3: print(

View file

@ -1,4 +1,5 @@
import logging
from dataclasses import dataclass
from PyQt5.QtWidgets import QMessageBox, QLabel
@ -24,17 +25,52 @@ class UILogHandler(logging.Handler):
self.widget.setText(record.getMessage())
class LgndrReturnLogger:
def __init__(self, logger: logging.Logger, level: int = logging.ERROR):
@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.value = False
self.message = ""
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.value = True if level < self.level else False
self.message = msg
self.logger.log(level, msg)
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)
@ -53,26 +89,3 @@ class LgndrReturnLogger:
def fatal(self, msg: str):
self.critical(msg)
def __len__(self):
if self.message:
return 2
else:
return 0
def __bool__(self):
return self.value
def __getitem__(self, item):
if item == 0:
return self.value
elif item == 1:
return self.message
else:
raise IndexError
def __iter__(self):
return iter((self.value, self.message))
def __str__(self):
return self.message

View file

@ -1,8 +1,7 @@
import functools
import os
import logging
import os
import time
from typing import Optional, Union
from typing import Optional, Union, Tuple
from legendary.cli import LegendaryCLI as LegendaryCLIReal
from legendary.models.downloading import AnalysisResult, ConditionCheckResult
@ -10,51 +9,27 @@ 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
from .api_monkeys import LgndrIndirectStatus, LgndrIndirectLogger
from .core import LegendaryCore
from .manager import DLManager
from .api_arguments import LgndrInstallGameArgs, LgndrImportGameArgs, LgndrVerifyGameArgs, LgndrUninstallGameArgs
from .api_monkeys import return_exit, get_boolean_choice, LgndrReturnLogger
class LegendaryCLI(LegendaryCLIReal):
"""
@staticmethod
def apply_wrap(func):
@functools.wraps(func)
def inner(self, args, *oargs, **kwargs):
old_exit = legendary.cli.exit
legendary.cli.exit = exit
# old_choice = legendary.cli.get_boolean_choice
# if hasattr(args, 'get_boolean_choice') and args.get_boolean_choice is not None:
# legendary.cli.get_boolean_choice = args.get_boolean_choice
try:
return func(self, args, *oargs, **kwargs)
except LgndrException as ret:
raise ret
finally:
# legendary.cli.get_boolean_choice = old_choice
legendary.cli.exit = old_exit
return inner
"""
def __init__(self, override_config=None, api_timeout=None):
self.core = LegendaryCore(override_config, timeout=api_timeout)
self.logger = logging.getLogger('cli')
self.logging_queue = None
# self.handler = LgndrCLILogHandler()
# self.logger.addHandler(self.handler)
def resolve_aliases(self, name):
return super(LegendaryCLI, self)._resolve_aliases(name)
def install_game(self, args: LgndrInstallGameArgs) -> (DLManager, AnalysisResult, InstalledGame, Game, bool, Optional[str], ConditionCheckResult):
# Override logger for the local context to use message as part of the return value
logger = LgndrReturnLogger(self.logger)
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):
@ -71,15 +46,21 @@ class LegendaryCLI(LegendaryCLIReal):
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 logger
return
# Rare: Rare checks this before calling 'install_game'
if args.platform not in game.asset_infos:
if not args.no_install:
@ -90,7 +71,7 @@ class LegendaryCLI(LegendaryCLIReal):
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 logger
return
else:
logger.warning(f'No asset found for platform "{args.platform}", '
f'trying anyway since --no-install is set.')
@ -104,25 +85,30 @@ class LegendaryCLI(LegendaryCLIReal):
# 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: Dodge the path below for now
logger.error('Game has not been verified yet.')
return
if not args.yes:
if not args.get_boolean_choice(f'Verify "{game.app_name}" now ("no" will abort repair)?'):
return logger
if not get_boolean_choice(f'Verify "{game.app_name}" now ("no" will abort repair)?'):
return
try:
self.verify_game(args, print_command=False, repair_mode=True, repair_online=args.repair_and_update)
except ValueError:
logger.error('To repair a game with a missing manifest you must run the command with '
'"--repair-and-update". However this will redownload any file that does '
'not match the current hash in its entirety.')
return logger
return
else:
logger.info(f'Using existing repair file: {repair_file}')
@ -154,7 +140,7 @@ class LegendaryCLI(LegendaryCLIReal):
if '__required' in sdl_data:
args.install_tag.extend(sdl_data['__required']['tags'])
else:
args.install_tag = args.sdl_prompt(sdl_data, game.app_title)
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}')
@ -176,7 +162,6 @@ class LegendaryCLI(LegendaryCLIReal):
# 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,
status_q=args.status_q,
force=args.force, max_shm=args.shared_memory,
max_workers=args.max_workers, game_folder=args.game_folder,
disable_patching=args.disable_patching,
@ -200,8 +185,7 @@ class LegendaryCLI(LegendaryCLIReal):
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.clean_post_install(game, igame, args.repair_mode, repair_file)
logger.error('Nothing to do.')
return
res = self.core.check_installation_conditions(analysis=analysis, install=igame, game=game,
updating=self.core.is_installed(args.app_name),
@ -209,9 +193,9 @@ class LegendaryCLI(LegendaryCLIReal):
return dlm, analysis, igame, game, args.repair_mode, repair_file, res
def clean_post_install(self, game: Game, igame: InstalledGame, repair: bool = False, repair_file: str = ''):
# Override logger for the local context to use message as part of the return value
logger = LgndrReturnLogger(self.logger)
def clean_post_install(self, game: Game, igame: InstalledGame, repair: 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 and os.path.exists(repair_file):
@ -229,24 +213,23 @@ class LegendaryCLI(LegendaryCLIReal):
self.core.uninstall_tag(old_igame)
self.core.install_game(old_igame)
return logger
def handle_postinstall(self, postinstall, igame, yes=False):
super(LegendaryCLI, self)._handle_postinstall(postinstall, igame, yes)
def uninstall_game(self, args: LgndrUninstallGameArgs):
# Override logger for the local context to use message as part of the return value
logger = LgndrReturnLogger(self.logger, level=logging.WARNING)
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 logger
return
if not args.yes:
if not args.get_boolean_choice(f'Do you wish to uninstall "{igame.title}"?', default=False):
return logger
if not get_boolean_choice(f'Do you wish to uninstall "{igame.title}"?', default=False):
return
try:
if not igame.is_dlc:
@ -261,19 +244,19 @@ class LegendaryCLI(LegendaryCLIReal):
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 logger
return
except Exception as e:
logger.warning(f'Removing game failed: {e!r}, please remove {igame.install_path} manually.')
return logger
return
def verify_game(self, args: Union[LgndrVerifyGameArgs, LgndrInstallGameArgs], print_command=True, repair_mode=False, repair_online=False):
# Override logger for the local context to use message as part of the return value
logger = LgndrReturnLogger(self.logger)
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
return
logger.info(f'Loading installed manifest for "{args.app_name}"')
igame = self.core.get_installed_game(args.app_name)
@ -281,7 +264,7 @@ class LegendaryCLI(LegendaryCLIReal):
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 logger
return
manifest_data, _ = self.core.get_installed_manifest(args.app_name)
if manifest_data is None:
@ -301,7 +284,7 @@ class LegendaryCLI(LegendaryCLIReal):
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 logger
return
manifest = self.core.load_manifest(manifest_data)
@ -371,16 +354,17 @@ class LegendaryCLI(LegendaryCLIReal):
if not missing and not failed:
logger.info('Verification finished successfully.')
return logger, 0, 0
return 0, 0
else:
logger.warning(f'Verification failed, {len(failed)} file(s) corrupted, {len(missing)} file(s) are missing.')
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 logger, len(failed), len(missing)
return len(failed), len(missing)
def import_game(self, args: LgndrImportGameArgs):
# Override logger for the local context to use message as part of the return value
logger = LgndrReturnLogger(self.logger)
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)
@ -388,21 +372,21 @@ class LegendaryCLI(LegendaryCLIReal):
if not os.path.exists(args.app_path):
logger.error(f'Specified path "{args.app_path}" does not exist!')
return logger
return
if self.core.is_installed(args.app_name):
logger.error('Game is already installed!')
return logger
return
if not self.core.login():
logger.error('Log in failed!')
return logger
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 logger
return
if game.is_dlc:
release_info = game.metadata.get('mainGameItem', {}).get('releaseInfo')
@ -412,10 +396,10 @@ class LegendaryCLI(LegendaryCLIReal):
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 logger
return
else:
logger.fatal(f'Unable to get base game information for DLC, cannot continue.')
return logger
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)
@ -435,12 +419,12 @@ class LegendaryCLI(LegendaryCLIReal):
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 logger
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 logger
return
if ratio < 0.95:
logger.warning('Some files are missing from the game installation, install may not '
@ -464,7 +448,7 @@ class LegendaryCLI(LegendaryCLIReal):
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 args.get_boolean_choice(f'Do you wish to automatically attempt to import all DLCs?'):
if not get_boolean_choice(f'Do you wish to automatically attempt to import all DLCs?'):
import_dlc = False
if import_dlc:
@ -473,4 +457,43 @@ class LegendaryCLI(LegendaryCLIReal):
self.import_game(args)
logger.info(f'{"DLC" if game.is_dlc else "Game"} "{game.app_title}" has been imported.')
return logger
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.')

View file

@ -44,6 +44,7 @@ class LegendaryCore(LegendaryCoreReal):
)
# lk: monkeypatch run_real (the method that emits the stats) into DLManager
dlm.run_real = DLManager.run_real.__get__(dlm, DLManager)
dlm.status_queue = Queue()
return dlm, analysis, igame
def uninstall_game(self, installed_game: InstalledGame, delete_files=True, delete_root_directory=False):
@ -70,10 +71,10 @@ class LegendaryCore(LegendaryCoreReal):
finally:
pass
def prepare_overlay_install(self, path=None, status_q: Queue = None):
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)
dlm.status_queue = status_q
dlm.status_queue = Queue()
return dlm, analysis_result, igame

View file

@ -60,11 +60,6 @@ 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.setWordWrap(True)
self.info_label.setObjectName("info_label")
self.button_info_layout.addWidget(self.info_label)
self.formLayout.setLayout(4, QtWidgets.QFormLayout.FieldRole, self.button_info_layout)
self.retranslateUi(ImportGroup)

View file

@ -96,16 +96,6 @@
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="info_label">
<property name="text">
<string notr="true"/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>

View file

@ -2,9 +2,10 @@ 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 rare.lgndr.api_monkeys import LgndrIndirectStatus
from rare.lgndr.api_arguments import LgndrVerifyGameArgs, LgndrUninstallGameArgs
from rare.lgndr.api_exception import LgndrException
from rare.shared import LegendaryCLISingleton, LegendaryCoreSingleton
@ -13,7 +14,7 @@ from rare.utils import config_helper
logger = getLogger("Legendary Utils")
def uninstall_game(core: LegendaryCore, app_name: str, 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
@ -38,21 +39,23 @@ def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False):
if os.path.exists(start_menu_shortcut):
os.remove(start_menu_shortcut)
result = LegendaryCLISingleton().uninstall_game(
status = LgndrIndirectStatus()
LegendaryCLISingleton().uninstall_game(
LgndrUninstallGameArgs(
app_name=app_name,
keep_files=keep_files,
indirect_status=status,
yes=True,
)
)
if not 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 result
return status.success, status.message
def update_manifest(app_name: str, core: LegendaryCore):
@ -93,16 +96,18 @@ class VerifyWorker(QRunnable):
self.signals.status.emit(self.app_name, num, total, percentage, speed)
def run(self):
status = LgndrIndirectStatus()
args = LgndrVerifyGameArgs(app_name=self.app_name,
indirect_status=status,
verify_stdout=self.status_callback)
# TODO: offer this as an alternative when manifest doesn't exist
# TODO: requires the client to be online. To do it this way, we need to
# TODO: somehow detect the error and offer a dialog in which case `verify_games` is
# TODO: re-run with `repair_mode` and `repair_online`
result, failed, missing = self.cli.verify_game(
result = self.cli.verify_game(
args, print_command=False, repair_mode=True, repair_online=True)
# success, failed, missing = self.cli.verify_game(args, print_command=False)
if result:
self.signals.result.emit(self.app_name, not failed and not missing, failed, missing)
self.signals.result.emit(self.app_name, not any(result), *result)
else:
self.signals.error.emit(self.app_name, result.message)
self.signals.error.emit(self.app_name, status.message)

View file

@ -2,7 +2,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, Optional, Callable, Dict
from legendary.core import LegendaryCore
from legendary.downloader.mp.manager import DLManager
@ -16,22 +16,31 @@ class InstallOptionsModel:
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
force: bool = False
sdl_list: list = field(default_factory=lambda: [""])
# 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
platform: str = ""
order_opt: 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
def __post_init__(self):
self.sdl_prompt: Callable[[str, str], list] = lambda app_name, title: self.install_tag
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
@ -47,16 +56,11 @@ class InstallDownloadModel:
@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)
)
return (self.download is not None) and (self.options is not None)
class PathSpec:
@ -81,9 +85,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 = [