1
0
Fork 0
mirror of synced 2024-05-18 11:32:50 +12:00

Various WIP

* Use `vars()` instead of directly accessing `__dict__`
* Remove `auto_update` from RareGame's metadata
* Correct check for updating the Steam App ID (We want to keep any changes from the user)
* Collect both Wine and Proton prefixes when removing overlay registry keys.
* Add few convenience functions in config_helper and paths.
This commit is contained in:
loathingKernel 2023-09-20 01:39:15 +03:00
parent 7a5bb0b732
commit af6d7c5055
51 changed files with 1626 additions and 1065 deletions

View file

@ -16,6 +16,7 @@ from PyQt5.QtWidgets import (
QHBoxLayout,
)
from rare.models.options import options
from rare.components.tabs import MainTabWidget
from rare.components.tray_icon import TrayIcon
from rare.shared import RareCore
@ -93,8 +94,8 @@ class MainWindow(QMainWindow):
# self.status_timer.start()
width, height = 1280, 720
if self.settings.value("save_size", False, bool):
width, height = self.settings.value("window_size", (width, height), tuple)
if self.settings.value(*options.save_size):
width, height = self.settings.value(*options.window_size)
self.resize(width, height)
@ -151,9 +152,9 @@ class MainWindow(QMainWindow):
self._window_launched = True
def hide(self) -> None:
if self.settings.value("save_size", False, bool):
if self.settings.value(*options.save_size):
size = self.size().width(), self.size().height()
self.settings.setValue("window_size", size)
self.settings.setValue(options.window_size.key, size)
super(MainWindow, self).hide()
def toggle(self):
@ -214,7 +215,7 @@ class MainWindow(QMainWindow):
# lk: `accept_close` is set to `True` by the `close()` method, overrides exiting to tray in `closeEvent()`
# lk: ensures exiting instead of hiding when `close()` is called programmatically
if not self.__accept_close:
if self.settings.value("sys_tray", True, bool):
if self.settings.value(*options.sys_tray):
self.hide()
e.ignore()
return

View file

@ -15,6 +15,7 @@ from rare.components.dialogs.uninstall_dialog import UninstallDialog
from rare.lgndr.models.downloading import UIUpdate
from rare.models.game import RareGame
from rare.models.install import InstallOptionsModel, InstallQueueItemModel, UninstallOptionsModel
from rare.models.options import options
from rare.shared import RareCore
from rare.shared.workers.install_info import InstallInfoWorker
from rare.shared.workers.uninstall import UninstallWorker
@ -105,9 +106,13 @@ class DownloadsTab(QWidget):
def __add_update(self, update: Union[str, RareGame]):
if isinstance(update, str):
update = self.rcore.get_game(update)
if QSettings().value(
f"{update.app_name}/auto_update", False, bool
) or QSettings().value("auto_update", False, bool):
auto_update = QSettings(self).value(
f"{update.app_name}/{options.auto_update.key}",
QSettings(self).value(*options.auto_update),
options.auto_update.dtype
)
if auto_update:
self.__get_install_options(
InstallOptionsModel(app_name=update.app_name, update=True, silent=True)
)

View file

@ -15,7 +15,7 @@ from rare.shared import RareCore
from rare.widgets.library_layout import LibraryLayout
from rare.widgets.sliding_stack import SlidingStackedWidget
from .game_info import GameInfoTabs
from .game_widgets import LibraryWidgetController
from .game_widgets import LibraryWidgetController, LibraryFilter, LibraryOrder
from .game_widgets.icon_game_widget import IconGameWidget
from .game_widgets.list_game_widget import ListGameWidget
from .head_bar import GameListHeadBar
@ -94,11 +94,11 @@ class GamesTab(QStackedWidget):
self.head_bar.search_bar.textChanged.connect(self.scroll_to_top)
self.head_bar.filterChanged.connect(self.filter_games)
self.head_bar.filterChanged.connect(self.scroll_to_top)
self.head_bar.refresh_list.clicked.connect(self.library_controller.update_list)
self.head_bar.orderChanged.connect(self.order_games)
self.head_bar.orderChanged.connect(self.scroll_to_top)
self.head_bar.refresh_list.clicked.connect(self.library_controller.update_game_views)
self.head_bar.view.toggled.connect(self.toggle_view)
self.active_filter: str = self.head_bar.filter.currentData(Qt.UserRole)
# signals
self.signals.game.installed.connect(self.update_count_games_label)
self.signals.game.uninstalled.connect(self.update_count_games_label)
@ -157,7 +157,7 @@ class GamesTab(QStackedWidget):
continue
self.icon_view.layout().addWidget(icon_widget)
self.list_view.layout().addWidget(list_widget)
self.filter_games(self.active_filter)
self.filter_games(self.head_bar.current_filter())
self.update_count_games_label()
def add_library_widget(self, rgame: RareGame):
@ -170,18 +170,26 @@ class GamesTab(QStackedWidget):
list_widget.show_info.connect(self.show_game_info)
return icon_widget, list_widget
@pyqtSlot(str)
@pyqtSlot(str, str)
def filter_games(self, filter_name="all", search_text: str = ""):
@pyqtSlot(int)
@pyqtSlot(int, str)
def filter_games(self, library_filter: LibraryFilter = LibraryFilter.ALL, search_text: str = ""):
if not search_text and (t := self.head_bar.search_bar.text()):
search_text = t
if filter_name:
self.active_filter = filter_name
if not filter_name and (t := self.active_filter):
filter_name = t
# if library_filter:
# self.active_filter = filter_type
# if not library_filter and (t := self.active_filter):
# library_filter = t
self.library_controller.filter_list(filter_name, search_text.lower())
self.library_controller.filter_game_views(library_filter, search_text.lower())
@pyqtSlot(int)
@pyqtSlot(int, str)
def order_games(self, library_order: LibraryOrder = LibraryFilter.ALL, search_text: str = ""):
if not search_text and (t := self.head_bar.search_bar.text()):
search_text = t
self.library_controller.order_game_views(library_order, search_text.lower())
def toggle_view(self):
self.settings.setValue("icon_view", not self.head_bar.view.isChecked())

View file

@ -3,7 +3,7 @@ import platform
from logging import getLogger
from typing import Tuple
from PyQt5.QtCore import QThreadPool, QSettings
from PyQt5.QtCore import QThreadPool, QSettings, pyqtSlot
from PyQt5.QtWidgets import (
QWidget,
QFileDialog,
@ -19,10 +19,11 @@ from legendary.models.game import SaveGameStatus
from rare.models.game import RareGame
from rare.shared import RareCore
from rare.shared.workers.wine_resolver import WineResolver
from rare.shared.workers.wine_resolver import WineSavePathResolver
from rare.ui.components.tabs.games.game_info.cloud_settings_widget import Ui_CloudSettingsWidget
from rare.ui.components.tabs.games.game_info.cloud_sync_widget import Ui_CloudSyncWidget
from rare.utils.misc import icon
from rare.utils.metrics import timelogger
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
from rare.widgets.loading_widget import LoadingWidget
from rare.widgets.side_tab import SideTabContents
@ -114,13 +115,14 @@ class CloudSaves(QWidget, SideTabContents):
def compute_save_path(self):
if self.rgame.is_installed and self.rgame.game.supports_cloud_saves:
try:
new_path = self.core.get_save_path(self.rgame.app_name)
with timelogger(logger, "Detecting save path"):
new_path = self.core.get_save_path(self.rgame.app_name)
if platform.system() != "Windows" and not os.path.exists(new_path):
raise ValueError(f'Path "{new_path}" does not exist.')
except Exception as e:
logger.warning(str(e))
resolver = WineResolver(self.core, self.rgame.raw_save_path, self.rgame.app_name)
if not resolver.wine_env.get("WINEPREFIX"):
resolver = WineSavePathResolver(self.core, self.rgame)
if not resolver.environment.get("WINEPREFIX"):
del resolver
self.cloud_save_path_edit.setText("")
QMessageBox.warning(self, "Warning", "No wine prefix selected. Please set it in settings")
@ -129,14 +131,14 @@ class CloudSaves(QWidget, SideTabContents):
self.cloud_save_path_edit.setDisabled(True)
self.compute_save_path_button.setDisabled(True)
app_name = self.rgame.app_name
resolver.signals.result_ready.connect(lambda x: self.wine_resolver_finished(x, app_name))
resolver.signals.result_ready.connect(self.__on_wine_resolver_result)
QThreadPool.globalInstance().start(resolver)
return
else:
self.cloud_save_path_edit.setText(new_path)
def wine_resolver_finished(self, path, app_name):
@pyqtSlot(str, str)
def __on_wine_resolver_result(self, path, app_name):
logger.info(f"Wine resolver finished for {app_name}. Computed save path: {path}")
if app_name == self.rgame.app_name:
self.cloud_save_path_edit.setDisabled(False)
@ -158,8 +160,6 @@ class CloudSaves(QWidget, SideTabContents):
self.cloud_save_path_edit.setText("")
return
self.cloud_save_path_edit.setText(path)
elif path:
self.rcore.get_game(app_name).save_path = path
def __update_widget(self):
supports_saves = self.rgame.igame is not None and (

View file

@ -4,7 +4,7 @@ from logging import getLogger
from typing import Tuple
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QLabel, QFileDialog
from PyQt5.QtWidgets import QLabel, QFileDialog, QFormLayout
from legendary.models.game import Game, InstalledGame
from rare.components.tabs.settings import DefaultGameSettings
@ -20,10 +20,6 @@ logger = getLogger("GameSettings")
class GameSettings(DefaultGameSettings, SideTabContents):
def __init__(self, parent=None):
super(GameSettings, self).__init__(False, parent=parent)
self.pre_launch_settings = PreLaunchSettings()
self.ui.launch_settings_group.layout().addRow(
QLabel(self.tr("Pre-launch command")), self.pre_launch_settings
)
self.ui.skip_update.currentIndexChanged.connect(
lambda x: self.update_combobox("skip_update_check", x)
@ -43,9 +39,17 @@ class GameSettings(DefaultGameSettings, SideTabContents):
save_func=self.override_exe_save_callback,
parent=self
)
self.ui.launch_settings_layout.insertRow(
self.ui.launch_settings_layout.getWidgetPosition(self.ui.launch_params)[0] + 1,
QLabel(self.tr("Override executable"), self), self.override_exe_edit
self.ui.launch_settings_layout.setWidget(
self.ui.launch_settings_layout.getWidgetPosition(self.ui.override_exe_label)[0],
QFormLayout.FieldRole,
self.override_exe_edit
)
self.pre_launch_settings = PreLaunchSettings(parent=self)
self.ui.launch_settings_layout.setWidget(
self.ui.launch_settings_layout.getWidgetPosition(self.ui.pre_launch_label)[0],
QFormLayout.FieldRole,
self.pre_launch_settings
)
self.ui.game_settings_layout.setAlignment(Qt.AlignTop)
@ -126,9 +130,9 @@ class GameSettings(DefaultGameSettings, SideTabContents):
self.set_title.emit(self.game.app_title)
if platform.system() != "Windows":
if self.igame and self.igame.platform == "Mac":
self.ui.linux_settings_widget.setVisible(False)
self.linux_settings.setVisible(False)
else:
self.ui.linux_settings_widget.setVisible(True)
self.linux_settings.setVisible(True)
self.ui.launch_params.setText(self.core.lgd.config.get(self.game.app_name, "start_params", fallback=""))
self.override_exe_edit.setText(

View file

@ -1,4 +1,4 @@
from typing import Tuple, List, Union, Optional
from typing import Tuple, List, Union
from PyQt5.QtCore import QObject, pyqtSlot
from PyQt5.QtWidgets import QWidget
@ -6,6 +6,7 @@ from PyQt5.QtWidgets import QWidget
from rare.lgndr.core import LegendaryCore
from rare.models.game import RareGame
from rare.models.signals import GlobalSignals
from rare.models.library import LibraryFilter, LibraryOrder
from rare.shared import RareCore
from .icon_game_widget import IconGameWidget
from .list_game_widget import ListGameWidget
@ -20,8 +21,8 @@ class LibraryWidgetController(QObject):
self.core: LegendaryCore = self.rcore.core()
self.signals: GlobalSignals = self.rcore.signals()
self.signals.game.installed.connect(self.sort_list)
self.signals.game.uninstalled.connect(self.sort_list)
self.signals.game.installed.connect(self.order_game_views)
self.signals.game.uninstalled.connect(self.order_game_views)
def add_game(self, rgame: RareGame):
return self.add_widgets(rgame)
@ -32,24 +33,26 @@ class LibraryWidgetController(QObject):
return icon_widget, list_widget
@staticmethod
def __visibility(widget: Union[IconGameWidget,ListGameWidget], filter_name, search_text) -> Tuple[bool, float]:
if filter_name == "hidden":
def __visibility(
widget: Union[IconGameWidget, ListGameWidget], library_filter, search_text
) -> Tuple[bool, float]:
if library_filter == LibraryFilter.HIDDEN:
visible = "hidden" in widget.rgame.metadata.tags
elif "hidden" in widget.rgame.metadata.tags:
visible = False
elif filter_name == "installed":
elif library_filter == LibraryFilter.INSTALLED:
visible = widget.rgame.is_installed and not widget.rgame.is_unreal
elif filter_name == "offline":
elif library_filter == LibraryFilter.OFFLINE:
visible = widget.rgame.can_run_offline and not widget.rgame.is_unreal
elif filter_name == "32bit":
elif library_filter == LibraryFilter.WIN32:
visible = widget.rgame.is_win32 and not widget.rgame.is_unreal
elif filter_name == "mac":
elif library_filter == LibraryFilter.MAC:
visible = widget.rgame.is_mac and not widget.rgame.is_unreal
elif filter_name == "installable":
elif library_filter == LibraryFilter.INSTALLABLE:
visible = not widget.rgame.is_non_asset and not widget.rgame.is_unreal
elif filter_name == "include_ue":
elif library_filter == LibraryFilter.INCLUDE_UE:
visible = True
elif filter_name == "all":
elif library_filter == LibraryFilter.ALL:
visible = not widget.rgame.is_unreal
else:
visible = True
@ -64,7 +67,7 @@ class LibraryWidgetController(QObject):
return visible, opacity
def filter_list(self, filter_name="all", search_text: str = ""):
def filter_game_views(self, filter_name="all", search_text: str = ""):
icon_widgets = self._icon_container.findChildren(IconGameWidget)
list_widgets = self._list_container.findChildren(ListGameWidget)
for iw in icon_widgets:
@ -75,42 +78,52 @@ class LibraryWidgetController(QObject):
visibility, opacity = self.__visibility(lw, filter_name, search_text)
lw.setOpacity(opacity)
lw.setVisible(visibility)
self.sort_list(search_text)
self.order_game_views(search_text=search_text)
@pyqtSlot()
def sort_list(self, sort_by: str = ""):
# lk: this is the existing sorting implemenation
# lk: it sorts by installed then by title
if sort_by:
self._icon_container.layout().sort(lambda x: (sort_by not in x.widget().rgame.app_title.lower(),))
else:
self._icon_container.layout().sort(
key=lambda x: (
# Sort by grant date
# x.widget().rgame.is_installed,
# not x.widget().rgame.is_non_asset,
# x.widget().rgame.grant_date(),
# ), reverse=True
not x.widget().rgame.is_installed,
x.widget().rgame.is_non_asset,
x.widget().rgame.app_title,
)
)
def order_game_views(self, order_by: LibraryOrder = LibraryOrder.TITLE, search_text: str = ""):
list_widgets = self._list_container.findChildren(ListGameWidget)
if sort_by:
list_widgets.sort(key=lambda x: (sort_by not in x.rgame.app_title.lower(),))
else:
list_widgets.sort(
# Sort by grant date
# key=lambda x: (x.rgame.is_installed, not x.rgame.is_non_asset, x.rgame.grant_date()), reverse=True
key=lambda x: (not x.rgame.is_installed, x.rgame.is_non_asset, x.rgame.app_title)
if search_text:
self._icon_container.layout().sort(
lambda x: (search_text not in x.widget().rgame.app_title.lower(),)
)
list_widgets.sort(key=lambda x: (search_text not in x.rgame.app_title.lower(),))
else:
if (newest := order_by == LibraryOrder.NEWEST) or order_by == LibraryOrder.OLDEST:
# Sort by grant date
self._icon_container.layout().sort(
key=lambda x: (x.widget().rgame.is_installed, not x.widget().rgame.is_non_asset, x.widget().rgame.grant_date()),
reverse=newest,
)
list_widgets.sort(
key=lambda x: (x.rgame.is_installed, not x.rgame.is_non_asset, x.rgame.grant_date()),
reverse=newest,
)
elif order_by == LibraryOrder.RECENT:
# Sort by recently played
self._icon_container.layout().sort(
key=lambda x: (not x.widget().rgame.is_installed, x.widget().rgame.is_non_asset, x.widget().rgame.metadata.last_played),
reverse=True,
)
list_widgets.sort(
key=lambda x: (not x.rgame.is_installed, x.rgame.is_non_asset, x.rgame.metadata.last_played),
reverse=True,
)
else:
# Sort by title
self._icon_container.layout().sort(
key=lambda x: (not x.widget().rgame.is_installed, x.widget().rgame.is_non_asset, x.widget().rgame.app_title)
)
list_widgets.sort(
key=lambda x: (not x.rgame.is_installed, x.rgame.is_non_asset, x.rgame.app_title)
)
for idx, wl in enumerate(list_widgets):
self._list_container.layout().insertWidget(idx, wl)
@pyqtSlot()
@pyqtSlot(list)
def update_list(self, app_names: List[str] = None):
def update_game_views(self, app_names: List[str] = None):
if not app_names:
# lk: base it on icon widgets, the two lists should be identical
icon_widgets = self._icon_container.findChildren(IconGameWidget)
@ -129,7 +142,7 @@ class LibraryWidgetController(QObject):
game = self.rcore.get_game(app_name)
lw = ListGameWidget(game)
self._list_container.layout().addWidget(lw)
self.sort_list()
self.order_game_views()
def __find_widget(self, app_name: str) -> Tuple[Union[IconGameWidget, None], Union[ListGameWidget, None]]:
iw = self._icon_container.findChild(IconGameWidget, app_name)

View file

@ -1,22 +1,25 @@
import platform as pf
from PyQt5.QtCore import QSettings, pyqtSignal, pyqtSlot, Qt
from PyQt5.QtCore import QSettings, pyqtSignal, pyqtSlot, QSize, Qt
from PyQt5.QtWidgets import (
QLabel,
QPushButton,
QWidget,
QHBoxLayout,
QComboBox, QToolButton, QMenu, QAction,
QComboBox,
QToolButton,
QMenu,
QAction,
)
from qtawesome import IconWidget
from rare.shared import RareCore
from rare.models.options import options
from rare.utils.extra_widgets import SelectViewWidget, ButtonLineEdit
from rare.utils.misc import icon
from .game_widgets import LibraryFilter, LibraryOrder
class GameListHeadBar(QWidget):
filterChanged: pyqtSignal = pyqtSignal(str)
filterChanged: pyqtSignal = pyqtSignal(int)
orderChanged: pyqtSignal = pyqtSignal(int)
goto_import: pyqtSignal = pyqtSignal()
goto_egl_sync: pyqtSignal = pyqtSignal()
goto_eos_ubisoft: pyqtSignal = pyqtSignal()
@ -27,43 +30,63 @@ class GameListHeadBar(QWidget):
self.settings = QSettings(self)
self.filter = QComboBox(self)
self.filter.addItem(self.tr("All games"), "all")
self.filter.addItem(self.tr("Installed"), "installed")
self.filter.addItem(self.tr("Offline"), "offline")
# self.filter.addItem(self.tr("Hidden"), "hidden")
self.filter.addItem(self.tr("All games"), LibraryFilter.ALL)
self.filter.addItem(self.tr("Installed"), LibraryFilter.INSTALLED)
self.filter.addItem(self.tr("Offline"), LibraryFilter.OFFLINE)
# self.filter.addItem(self.tr("Hidden"), LibraryFilter.HIDDEN)
if self.rcore.bit32_games:
self.filter.addItem(self.tr("32bit games"), "32bit")
self.filter.addItem(self.tr("32bit games"), LibraryFilter.WIN32)
if self.rcore.mac_games:
self.filter.addItem(self.tr("macOS games"), "mac")
self.filter.addItem(self.tr("macOS games"), LibraryFilter.MAC)
if self.rcore.origin_games:
self.filter.addItem(self.tr("Exclude Origin"), "installable")
self.filter.addItem(self.tr("Include Unreal"), "include_ue")
filter_default = "mac" if pf.system() == "Darwin" else "all"
filter_index = i if (i := self.filter.findData(filter_default, Qt.UserRole)) >= 0 else 0
self.filter.addItem(self.tr("Exclude Origin"), LibraryFilter.INSTALLABLE)
self.filter.addItem(self.tr("Include Unreal"), LibraryFilter.INCLUDE_UE)
try:
self.filter.setCurrentIndex(self.settings.value("library_filter", filter_index, int))
except TypeError:
self.settings.setValue("library_filter", filter_index)
self.filter.setCurrentIndex(filter_index)
self.filter.currentIndexChanged.connect(self.filter_changed)
self.filter.setCurrentIndex(self.filter.findData(
LibraryFilter(self.settings.value(*options.library_filter))
))
except (TypeError, ValueError):
self.settings.setValue(options.library_filter.key, options.library_filter.default)
self.filter.setCurrentIndex(self.filter.findData(options.library_filter.default))
self.filter.currentIndexChanged.connect(self.__filter_changed)
integrations_menu = QMenu(self)
import_action = QAction(icon("mdi.import", "fa.arrow-down"), self.tr("Import Game"), integrations_menu)
self.order = QComboBox(parent=self)
sortings = {
LibraryOrder.TITLE: self.tr("Title"),
LibraryOrder.RECENT: self.tr("Recently played"),
LibraryOrder.NEWEST: self.tr("Newest"),
LibraryOrder.OLDEST: self.tr("Oldest"),
}
for data, text in sortings.items():
self.order.addItem(text, data)
try:
self.order.setCurrentIndex(self.order.findData(
LibraryOrder(self.settings.value(*options.library_order))
))
except (TypeError, ValueError):
self.settings.setValue(options.library_order.key, options.library_order.default)
self.order.setCurrentIndex(self.order.findData(options.library_order.default))
self.order.currentIndexChanged.connect(self.__order_changed)
integrations_menu = QMenu(parent=self)
import_action = QAction(
icon("mdi.import", "fa.arrow-down"), self.tr("Import Game"), integrations_menu
)
import_action.triggered.connect(self.goto_import)
egl_sync_action = QAction(icon("mdi.sync", "fa.refresh"), self.tr("Sync with EGL"), integrations_menu)
egl_sync_action.triggered.connect(self.goto_egl_sync)
eos_ubisoft_action = QAction(icon("mdi.rocket", "fa.rocket"), self.tr("Epic Overlay and Ubisoft"),
integrations_menu)
eos_ubisoft_action = QAction(
icon("mdi.rocket", "fa.rocket"), self.tr("Epic Overlay and Ubisoft"), integrations_menu
)
eos_ubisoft_action.triggered.connect(self.goto_eos_ubisoft)
integrations_menu.addAction(import_action)
integrations_menu.addAction(egl_sync_action)
integrations_menu.addAction(eos_ubisoft_action)
integrations = QToolButton(self)
integrations = QToolButton(parent=self)
integrations.setText(self.tr("Integrations"))
integrations.setMenu(integrations_menu)
integrations.setPopupMode(QToolButton.InstantPopup)
@ -76,8 +99,8 @@ class GameListHeadBar(QWidget):
checked = QSettings().value("icon_view", True, bool)
installed_tooltip = self.tr("Installed games")
self.installed_icon = IconWidget(parent=self)
self.installed_icon.setIcon(icon("ph.floppy-disk-back-fill"))
self.installed_icon = QLabel(parent=self)
self.installed_icon.setPixmap(icon("ph.floppy-disk-back-fill").pixmap(QSize(16, 16)))
self.installed_icon.setToolTip(installed_tooltip)
self.installed_label = QLabel(parent=self)
font = self.installed_label.font()
@ -85,24 +108,25 @@ class GameListHeadBar(QWidget):
self.installed_label.setFont(font)
self.installed_label.setToolTip(installed_tooltip)
available_tooltip = self.tr("Available games")
self.available_icon = IconWidget(parent=self)
self.available_icon.setIcon(icon("ph.floppy-disk-back-light"))
self.available_icon = QLabel(parent=self)
self.available_icon.setPixmap(icon("ph.floppy-disk-back-light").pixmap(QSize(16, 16)))
self.available_icon.setToolTip(available_tooltip)
self.available_label = QLabel(parent=self)
self.available_label.setToolTip(available_tooltip)
self.view = SelectViewWidget(checked)
self.refresh_list = QPushButton()
self.refresh_list = QPushButton(parent=self)
self.refresh_list.setIcon(icon("fa.refresh")) # Reload icon
self.refresh_list.clicked.connect(self.refresh_clicked)
self.refresh_list.clicked.connect(self.__refresh_clicked)
layout = QHBoxLayout()
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 5, 0, 5)
layout.addWidget(self.filter)
layout.addWidget(self.order)
layout.addStretch(0)
layout.addWidget(integrations)
layout.addStretch(5)
layout.addStretch(2)
layout.addWidget(self.search_bar)
layout.addStretch(2)
layout.addWidget(self.installed_icon)
@ -113,17 +137,29 @@ class GameListHeadBar(QWidget):
layout.addWidget(self.view)
layout.addStretch(2)
layout.addWidget(self.refresh_list)
self.setLayout(layout)
def set_games_count(self, inst: int, avail: int) -> None:
self.installed_label.setText(str(inst))
self.available_label.setText(str(avail))
@pyqtSlot()
def refresh_clicked(self):
def __refresh_clicked(self):
self.rcore.fetch()
def current_filter(self) -> int:
return int(self.filter.currentData(Qt.UserRole))
@pyqtSlot(int)
def filter_changed(self, index: int):
self.filterChanged.emit(self.filter.itemData(index, Qt.UserRole))
self.settings.setValue("library_filter", index)
def __filter_changed(self, index: int):
data = int(self.filter.itemData(index, Qt.UserRole))
self.filterChanged.emit(data)
self.settings.setValue(options.library_filter.key, data)
def current_order(self) -> int:
return int(self.order.currentData(Qt.UserRole))
@pyqtSlot(int)
def __order_changed(self, index: int):
data = int(self.order.itemData(index, Qt.UserRole))
self.orderChanged.emit(data)
self.settings.setValue(options.library_order.key, data)

View file

@ -13,9 +13,10 @@ from legendary.models.game import InstalledGame
from rare.lgndr.glue.exception import LgndrException
from rare.models.pathspec import PathSpec
from rare.shared import RareCore
from rare.shared.workers.wine_resolver import WineResolver
from rare.shared.workers.wine_resolver import WinePathResolver
from rare.ui.components.tabs.games.integrations.egl_sync_group import Ui_EGLSyncGroup
from rare.ui.components.tabs.games.integrations.egl_sync_list_group import Ui_EGLSyncListGroup
from rare.utils import runners
from rare.widgets.elide_label import ElideLabel
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
@ -87,10 +88,10 @@ class EGLSyncGroup(QGroupBox):
def __run_wine_resolver(self):
self.egl_path_info.setText(self.tr("Updating..."))
wine_resolver = WineResolver(
self.core,
PathSpec.egl_programdata,
"default"
wine_resolver = WinePathResolver(
self.core.get_app_launch_command("default"),
runners.get_environment(self.core.get_app_environment("default")),
PathSpec.egl_programdata()
)
wine_resolver.signals.result_ready.connect(self.__on_wine_resolver_result)
QThreadPool.globalInstance().start(wine_resolver)
@ -122,14 +123,8 @@ class EGLSyncGroup(QGroupBox):
os.path.join(path, "dosdevices/c:")
):
# path is a wine prefix
path = os.path.join(
path,
"dosdevices/c:",
"ProgramData/Epic/EpicGamesLauncher/Data/Manifests",
)
elif not path.rstrip("/").endswith(
"ProgramData/Epic/EpicGamesLauncher/Data/Manifests"
):
path = PathSpec.prefix_egl_programdata(path)
elif not path.rstrip("/").endswith(PathSpec.wine_egl_programdata()):
# lower() might or might not be needed in the check
return False, path, IndicatorReasonsCommon.WRONG_FORMAT
if os.path.exists(path):

View file

@ -1,9 +1,9 @@
from rare.components.tabs.settings.widgets.linux import LinuxSettings
from rare.components.tabs.settings.widgets.wine import LinuxSettings
from rare.shared import ArgumentsSingleton
from rare.widgets.side_tab import SideTabWidget
from .about import About
from .debug import DebugSettings
from .game_settings import DefaultGameSettings
from .game import DefaultGameSettings
from .legendary import LegendarySettings
from .rare import RareSettings
@ -13,9 +13,14 @@ class SettingsTab(SideTabWidget):
super(SettingsTab, self).__init__(parent=parent)
self.args = ArgumentsSingleton()
self.rare_index = self.addTab(RareSettings(self), "Rare")
self.legendary_index = self.addTab(LegendarySettings(self), "Legendary")
self.settings_index = self.addTab(DefaultGameSettings(True, self), self.tr("Default Settings"))
rare_settings = RareSettings(self)
self.rare_index = self.addTab(rare_settings, "Rare")
legendary_settings = LegendarySettings(self)
self.legendary_index = self.addTab(legendary_settings, "Legendary")
game_settings = DefaultGameSettings(True, self)
self.settings_index = self.addTab(game_settings, self.tr("Defaults"))
self.about = About(self)
self.about_index = self.addTab(self.about, "About", "About")

View file

@ -0,0 +1,95 @@
import platform as pf
from logging import getLogger
from PyQt5.QtCore import QSettings, Qt
from PyQt5.QtGui import QShowEvent
from PyQt5.QtWidgets import (
QWidget,
QLabel, QFormLayout
)
from components.tabs.settings.widgets.dxvk import DxvkSettings
from components.tabs.settings.widgets.mangohud import MangoHudSettings
from rare.components.tabs.settings.widgets.env_vars import EnvVars
from rare.components.tabs.settings.widgets.wine import LinuxSettings
from rare.components.tabs.settings.widgets.proton import ProtonSettings
from rare.components.tabs.settings.widgets.wrapper import WrapperSettings
from rare.shared import LegendaryCoreSingleton
from rare.ui.components.tabs.settings.game import Ui_GameSettings
logger = getLogger("GameSettings")
class DefaultGameSettings(QWidget):
# variable to no update when changing game
change = False
app_name: str
def __init__(self, is_default, parent=None):
super(DefaultGameSettings, self).__init__(parent=parent)
self.ui = Ui_GameSettings()
self.ui.setupUi(self)
self.core = LegendaryCoreSingleton()
self.settings = QSettings(self)
self.wrapper_settings = WrapperSettings(self)
self.ui.launch_layout.setWidget(
self.ui.launch_layout.getWidgetPosition(self.ui.wrapper_label)[0],
QFormLayout.FieldRole,
self.wrapper_settings
)
self.env_vars = EnvVars(self)
# dxvk
self.dxvk = DxvkSettings(self)
self.dxvk.environ_changed.connect(self.env_vars.reset_model)
self.dxvk.load_settings(self.app_name)
self.mangohud = MangoHudSettings(self)
self.mangohud.environ_changed.connect(self.environ_changed)
self.mangohud.load_settings(self.name)
if pf.system() != "Windows":
self.linux_settings = LinuxAppSettings(self)
self.ui.game_settings_layout.addWidget(self.linux_settings)
self.linux_settings.mangohud.set_wrapper_activated.connect(
lambda active: self.wrapper_settings.add_wrapper("mangohud")
if active else self.wrapper_settings.delete_wrapper("mangohud"))
self.linux_settings.environ_changed.connect(self.env_vars.reset_model)
if pf.system() != "Darwin":
self.proton_settings = ProtonSettings(self.linux_settings, self.wrapper_settings)
self.linux_settings.ui.linux_settings_layout.insertWidget(0, self.proton_settings)
self.proton_settings.environ_changed.connect(self.env_vars.reset_model)
self.ui.game_settings_layout.setAlignment(Qt.AlignTop)
self.ui.main_layout.addWidget(self.dxvk)
self.ui.main_layout.addWidget(self.mangohud)
self.ui.main_layout.addWidget(self.env_vars)
if is_default:
self.ui.launch_layout.removeRow(self.ui.skip_update_label)
self.ui.launch_layout.removeRow(self.ui.offline_label)
self.ui.launch_layout.removeRow(self.ui.launch_params_label)
self.ui.launch_layout.removeRow(self.ui.override_exe_label)
self.ui.launch_layout.removeRow(self.ui.pre_launch_label)
self.load_settings("default")
def load_settings(self, app_name):
self.app_name = app_name
self.wrapper_settings.load_settings(app_name)
if pf.system() != "Windows":
self.linux_settings.update_game(app_name)
if pf.system() != "Darwin":
proton = self.wrapper_settings.wrappers.get("proton", "")
if proton:
proton = proton.text
self.proton_settings.load_settings(app_name, proton)
else:
proton = ""
self.linux_settings.ui.wine_groupbox.setDisabled(bool(proton))
self.env_vars.update_game(app_name)

View file

@ -1,106 +0,0 @@
import platform
from logging import getLogger
from PyQt5.QtCore import QSettings, Qt
from PyQt5.QtWidgets import (
QWidget,
QLabel
)
from rare.components.tabs.settings.widgets.env_vars import EnvVars
from rare.components.tabs.settings.widgets.wrapper import WrapperSettings
from rare.shared import LegendaryCoreSingleton
from rare.ui.components.tabs.settings.game_settings import Ui_GameSettings
if platform.system() != "Windows":
from rare.components.tabs.settings.widgets.linux import LinuxSettings
if platform.system() != "Darwin":
from rare.components.tabs.settings.widgets.proton import ProtonSettings
logger = getLogger("GameSettings")
class DefaultGameSettings(QWidget):
# variable to no update when changing game
change = False
app_name: str
def __init__(self, is_default, parent=None):
super(DefaultGameSettings, self).__init__(parent=parent)
self.ui = Ui_GameSettings()
self.ui.setupUi(self)
self.core = LegendaryCoreSingleton()
self.settings = QSettings()
self.wrapper_settings = WrapperSettings()
self.ui.launch_settings_group.layout().addRow(
QLabel("Wrapper"), self.wrapper_settings
)
self.env_vars = EnvVars(self)
self.ui.game_settings_layout.addWidget(self.env_vars)
if platform.system() != "Windows":
self.linux_settings = LinuxAppSettings()
if platform.system() != "Darwin":
self.proton_settings = ProtonSettings(self.linux_settings, self.wrapper_settings)
self.ui.proton_layout.addWidget(self.proton_settings)
self.proton_settings.environ_changed.connect(self.env_vars.reset_model)
# FIXME: Remove the spacerItem and margins from the linux settings
# FIXME: This should be handled differently at soem point in the future
# NOTE: specerItem has been removed
self.linux_settings.layout().setContentsMargins(0, 0, 0, 0)
# FIXME: End of FIXME
self.ui.linux_settings_layout.addWidget(self.linux_settings)
self.ui.linux_settings_layout.setAlignment(Qt.AlignTop)
self.ui.game_settings_layout.setAlignment(Qt.AlignTop)
self.linux_settings.mangohud.set_wrapper_activated.connect(
lambda active: self.wrapper_settings.add_wrapper("mangohud")
if active else self.wrapper_settings.delete_wrapper("mangohud"))
self.linux_settings.environ_changed.connect(self.env_vars.reset_model)
else:
self.ui.linux_settings_widget.setVisible(False)
if is_default:
self.ui.launch_settings_layout.removeRow(self.ui.skip_update)
self.ui.launch_settings_layout.removeRow(self.ui.offline)
self.ui.launch_settings_layout.removeRow(self.ui.launch_params)
self.load_settings("default")
def load_settings(self, app_name):
self.app_name = app_name
self.wrapper_settings.load_settings(app_name)
if platform.system() != "Windows":
self.linux_settings.update_game(app_name)
proton = self.wrapper_settings.wrappers.get("proton", "")
if proton:
proton = proton.text
if platform.system() != "Darwin":
self.proton_settings.load_settings(app_name, proton)
else:
proton = ""
if proton:
self.linux_settings.ui.wine_groupbox.setEnabled(False)
else:
self.linux_settings.ui.wine_groupbox.setEnabled(True)
self.env_vars.update_game(app_name)
if platform.system() != "Windows":
class LinuxAppSettings(LinuxSettings):
def __init__(self):
super(LinuxAppSettings, self).__init__()
def update_game(self, app_name):
self.name = app_name
self.wine_prefix.setText(self.load_prefix())
self.wine_exec.setText(self.load_setting(self.name, "wine_executable"))
self.dxvk.load_settings(self.name)
self.mangohud.load_settings(self.name)

View file

@ -6,6 +6,7 @@ from typing import Tuple, List
from PyQt5.QtCore import QObject, pyqtSignal, QThreadPool, QSettings
from PyQt5.QtWidgets import QSizePolicy, QWidget, QFileDialog, QMessageBox
from rare.models.options import options
from rare.shared import LegendaryCoreSingleton
from rare.shared.workers.worker import Worker
from rare.ui.components.tabs.settings.legendary import Ui_LegendarySettings
@ -100,33 +101,30 @@ class LegendarySettings(QWidget, Ui_LegendarySettings):
)
self.locale_layout.addWidget(self.locale_edit)
self.fetch_win32_check.setChecked(self.settings.value("win32_meta", False, bool))
self.fetch_win32_check.setChecked(self.settings.value(*options.win32_meta))
self.fetch_win32_check.stateChanged.connect(
lambda: self.settings.setValue("win32_meta", self.fetch_win32_check.isChecked())
lambda: self.settings.setValue(options.win32_meta.key, self.fetch_win32_check.isChecked())
)
self.fetch_macos_check.setChecked(self.settings.value("macos_meta", pf.system() == "Darwin", bool))
self.fetch_macos_check.setChecked(self.settings.value(*options.macos_meta))
self.fetch_macos_check.stateChanged.connect(
lambda: self.settings.setValue("macos_meta", self.fetch_macos_check.isChecked())
lambda: self.settings.setValue(options.macos_meta.key, self.fetch_macos_check.isChecked())
)
self.fetch_macos_check.setDisabled(pf.system() == "Darwin")
self.fetch_unreal_check.setChecked(self.settings.value("unreal_meta", False, bool))
self.fetch_unreal_check.setChecked(self.settings.value(*options.unreal_meta))
self.fetch_unreal_check.stateChanged.connect(
lambda: self.settings.setValue("unreal_meta", self.fetch_unreal_check.isChecked())
lambda: self.settings.setValue(options.unreal_meta.key, self.fetch_unreal_check.isChecked())
)
self.exclude_non_asset_check.setChecked(
self.settings.value("exclude_non_asset", False, bool)
)
self.exclude_non_asset_check.setChecked(self.settings.value(*options.exclude_non_asset))
self.exclude_non_asset_check.stateChanged.connect(
lambda: self.settings.setValue("exclude_non_asset", self.exclude_non_asset_check.isChecked())
)
self.exclude_entitlements_check.setChecked(
self.settings.value("exclude_entitlements", False, bool)
lambda: self.settings.setValue(options.exclude_non_asset.key, self.exclude_non_asset_check.isChecked())
)
self.exclude_entitlements_check.setChecked(self.settings.value(*options.exclude_entitlements))
self.exclude_entitlements_check.stateChanged.connect(
lambda: self.settings.setValue("exclude_entitlements", self.exclude_entitlements_check.isChecked())
lambda: self.settings.setValue(options.exclude_entitlements.key, self.exclude_entitlements_check.isChecked())
)
self.refresh_metadata_button.clicked.connect(self.refresh_metadata)

View file

@ -8,6 +8,7 @@ from PyQt5.QtCore import QSettings, Qt
from PyQt5.QtWidgets import QWidget, QMessageBox
from rare.components.tabs.settings.widgets.rpc import RPCSettings
from rare.models.options import options
from rare.shared import LegendaryCoreSingleton
from rare.ui.components.tabs.settings.rare import Ui_RareSettings
from rare.utils.misc import (
@ -39,18 +40,8 @@ class RareSettings(QWidget, Ui_RareSettings):
super(RareSettings, self).__init__(parent=parent)
self.setupUi(self)
self.core = LegendaryCoreSingleton()
# (widget_name, option_name, default)
self.checkboxes = [
(self.sys_tray, "sys_tray", True),
(self.auto_update, "auto_update", False),
(self.confirm_start, "confirm_start", False),
(self.auto_sync_cloud, "auto_sync_cloud", False),
(self.notification, "notification", True),
(self.save_size, "save_size", False),
(self.log_games, "show_console", False),
]
self.settings = QSettings(self)
self.settings = QSettings()
language = self.settings.value("language", self.core.language_code, type=str)
# Select lang
@ -85,29 +76,37 @@ class RareSettings(QWidget, Ui_RareSettings):
self.rpc = RPCSettings(self)
self.right_layout.insertWidget(1, self.rpc, alignment=Qt.AlignTop)
self.init_checkboxes(self.checkboxes)
self.sys_tray.setChecked(self.settings.value(*options.sys_tray))
self.sys_tray.stateChanged.connect(
lambda: self.settings.setValue("sys_tray", self.sys_tray.isChecked())
lambda: self.settings.setValue(options.sys_tray.key, self.sys_tray.isChecked())
)
self.auto_update.setChecked(self.settings.value(*options.auto_update))
self.auto_update.stateChanged.connect(
lambda: self.settings.setValue("auto_update", self.auto_update.isChecked())
lambda: self.settings.setValue(options.auto_update.key, self.auto_update.isChecked())
)
self.confirm_start.setChecked(self.settings.value(*options.confirm_start))
self.confirm_start.stateChanged.connect(
lambda: self.settings.setValue(
"confirm_start", self.confirm_start.isChecked()
)
lambda: self.settings.setValue(options.confirm_start.key, self.confirm_start.isChecked())
)
self.auto_sync_cloud.setChecked(self.settings.value(*options.auto_sync_cloud))
self.auto_sync_cloud.stateChanged.connect(
lambda: self.settings.setValue(
"auto_sync_cloud", self.auto_sync_cloud.isChecked()
)
lambda: self.settings.setValue(options.auto_sync_cloud.key, self.auto_sync_cloud.isChecked())
)
self.notification.setChecked(self.settings.value(*options.notification))
self.notification.stateChanged.connect(
lambda: self.settings.setValue("notification", self.notification.isChecked())
lambda: self.settings.setValue(options.notification.key, self.notification.isChecked())
)
self.save_size.setChecked(self.settings.value(*options.save_size))
self.save_size.stateChanged.connect(self.save_window_size)
self.log_games.setChecked(self.settings.value(*options.log_games))
self.log_games.stateChanged.connect(
lambda: self.settings.setValue("show_console", self.log_games.isChecked())
lambda: self.settings.setValue(options.log_games.key, self.log_games.isChecked())
)
if desktop_links_supported():
@ -221,13 +220,8 @@ class RareSettings(QWidget, Ui_RareSettings):
subprocess.Popen([opener, log_dir()])
def save_window_size(self):
self.settings.setValue("save_size", self.save_size.isChecked())
self.settings.remove("window_size")
self.settings.setValue(options.save_size.key, self.save_size.isChecked())
self.settings.remove(options.window_size.key)
def update_lang(self, i: int):
self.settings.setValue("language", languages[i][0])
def init_checkboxes(self, checkboxes):
for cb in checkboxes:
widget, option, default = cb
widget.setChecked(self.settings.value(option, default, bool))

View file

@ -1,10 +1,10 @@
from PyQt5.QtCore import QCoreApplication
from .overlay_settings import OverlaySettings, CustomOption
from .overlays import OverlaySettings, CustomOption
class DxvkSettings(OverlaySettings):
def __init__(self):
def __init__(self, parent=None):
super(DxvkSettings, self).__init__(
[
("fps", QCoreApplication.translate("DxvkSettings", "FPS")),
@ -19,7 +19,8 @@ class DxvkSettings(OverlaySettings):
[
(CustomOption.number_input("scale", 1, True), QCoreApplication.translate("DxvkSettings", "Scale"))
],
"DXVK_HUD", "0"
"DXVK_HUD", "0",
parent=parent
)
self.setTitle(self.tr("DXVK Settings"))

View file

@ -11,12 +11,14 @@ from rare.lgndr.core import LegendaryCore
from rare.utils.misc import icon
if platform.system() != "Windows":
from rare.utils.runners.wine import get_wine_environment
if platform.system() != "Darwin":
from rare.utils import proton
from rare.utils.runners.proton import get_steam_environment
class EnvVarsTableModel(QAbstractTableModel):
def __init__(self, core: LegendaryCore, parent = None):
def __init__(self, core: LegendaryCore, parent=None):
super(EnvVarsTableModel, self).__init__(parent=parent)
self.core = core
@ -27,14 +29,13 @@ class EnvVarsTableModel(QAbstractTableModel):
self.__data_map: ChainMap = ChainMap()
self.__readonly = [
"STEAM_COMPAT_DATA_PATH",
"WINEPREFIX",
"DXVK_HUD",
"MANGOHUD_CONFIG",
]
if platform.system() != "Windows":
self.__readonly.extend(get_wine_environment().keys())
if platform.system() != "Darwin":
self.__readonly.extend(proton.get_steam_environment(None).keys())
self.__readonly.extend(get_steam_environment().keys())
self.__default: str = "default"
self.__appname: str = None
@ -256,8 +257,6 @@ class EnvVarsTableModel(QAbstractTableModel):
if __name__ == "__main__":
from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout, QTableView, QHeaderView
from rare.resources import static_css
from rare.resources.stylesheets import RareStyle
from rare.utils.misc import set_style_sheet
from legendary.core import LegendaryCore

View file

@ -5,18 +5,17 @@ from PyQt5.QtCore import QCoreApplication, pyqtSignal
from PyQt5.QtWidgets import QMessageBox
from rare.shared import LegendaryCoreSingleton
from .overlay_settings import OverlaySettings, CustomOption, ActivationStates
from rare.utils import config_helper
from .overlays import OverlaySettings, CustomOption, ActivationStates
position_values = ["default", "top-left", "top-right", "middle-left", "middle-right", "bottom-left",
"bottom-right", "top-center"]
class MangoHudSettings(OverlaySettings):
set_wrapper_activated = pyqtSignal(bool)
def __init__(self):
def __init__(self, parent=None):
super(MangoHudSettings, self).__init__(
[
("fps", QCoreApplication.translate("MangoSettings", "FPS")),
@ -45,7 +44,8 @@ class MangoHudSettings(OverlaySettings):
QCoreApplication.translate("MangoSettings", "Position")
)
],
"MANGOHUD_CONFIG", "no_display", set_activation_state=self.set_activation_state
"MANGOHUD_CONFIG", "no_display", set_activation_state=self.set_activation_state,
parent=parent
)
self.core = LegendaryCoreSingleton()
self.setTitle(self.tr("MangoHud Settings"))

View file

@ -82,8 +82,8 @@ class OverlaySettings(QGroupBox, Ui_OverlaySettings):
def __init__(self, checkboxes_map: List[Tuple[str, str]], value_map: List[Tuple[CustomOption, str]],
config_env_var_name: str, no_display_value: str,
set_activation_state: Callable[[Enum], None] = lambda x: None):
super(OverlaySettings, self).__init__()
set_activation_state: Callable[[Enum], None] = lambda x: None, parent=None):
super(OverlaySettings, self).__init__(parent=parent)
self.setupUi(self)
self.core = LegendaryCoreSingleton()
self.config_env_var_name = config_env_var_name

View file

@ -2,18 +2,18 @@ import os
import shutil
from typing import Tuple
from PyQt5.QtWidgets import QHBoxLayout, QCheckBox, QFileDialog
from PyQt5.QtWidgets import QHBoxLayout, QCheckBox, QFileDialog, QWidget
from rare.shared import LegendaryCoreSingleton
from rare.utils import config_helper
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
class PreLaunchSettings(QHBoxLayout):
class PreLaunchSettings(QWidget):
app_name: str
def __init__(self):
super(PreLaunchSettings, self).__init__()
def __init__(self, parent=None):
super(PreLaunchSettings, self).__init__(parent=parent)
self.core = LegendaryCoreSingleton()
self.edit = PathEdit(
path="",
@ -22,12 +22,15 @@ class PreLaunchSettings(QHBoxLayout):
edit_func=self.edit_command,
save_func=self.save_pre_launch_command,
)
self.layout().addWidget(self.edit)
self.wait_check = QCheckBox(self.tr("Wait for finish"))
self.layout().addWidget(self.wait_check)
self.wait_check.stateChanged.connect(self.save_wait_finish)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.edit)
layout.addWidget(self.wait_check)
def edit_command(self, text: str) -> Tuple[bool, str, int]:
if not text.strip():
return True, text, IndicatorReasonsCommon.VALID

View file

@ -1,85 +1,106 @@
import os
from logging import getLogger
from pathlib import Path
from typing import Tuple
from typing import Tuple, Union, Optional
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtGui import QShowEvent
from PyQt5.QtWidgets import QGroupBox, QFileDialog
from rare.components.tabs.settings import LinuxSettings
from rare.shared import LegendaryCoreSingleton
from rare.models.wrapper import Wrapper, WrapperType
from rare.shared import RareCore
from rare.shared.wrappers import Wrappers
from rare.ui.components.tabs.settings.proton import Ui_ProtonSettings
from rare.utils import config_helper, proton
from rare.utils import config_helper as config
from rare.utils.runners import proton
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
from .wrapper import WrapperSettings
logger = getLogger("Proton")
logger = getLogger("ProtonSettings")
class ProtonSettings(QGroupBox):
# str: option key
environ_changed = pyqtSignal(str)
app_name: str
changeable = True
environ_changed: pyqtSignal = pyqtSignal(str)
# bool: state
tool_enabled: pyqtSignal = pyqtSignal(bool)
def __init__(self, linux_settings: LinuxSettings, wrapper_settings: WrapperSettings):
super(ProtonSettings, self).__init__()
def __init__(self, parent=None):
super(ProtonSettings, self).__init__(parent=parent)
self.ui = Ui_ProtonSettings()
self.ui.setupUi(self)
self._linux_settings = linux_settings
self._wrapper_settings = wrapper_settings
self.core = LegendaryCoreSingleton()
self.possible_proton_combos = proton.find_proton_combos()
self.ui.proton_combo.addItems(self.possible_proton_combos)
self.ui.proton_combo.currentIndexChanged.connect(self.change_proton)
self.ui.proton_combo.currentIndexChanged.connect(self.__on_proton_changed)
self.proton_prefix = PathEdit(
file_mode=QFileDialog.DirectoryOnly,
edit_func=self.proton_prefix_edit,
save_func=self.proton_prefix_save,
placeholder=self.tr("Please select path for proton prefix")
placeholder=self.tr("Please select path for proton prefix"),
)
self.ui.prefix_layout.addWidget(self.proton_prefix)
def change_proton(self, i):
if not self.changeable:
return
# First combo box entry: Don't use Proton
if i == 0:
self._wrapper_settings.delete_wrapper("proton")
config_helper.remove_option(self.app_name, "no_wine")
config_helper.remove_option(f"{self.app_name}.env", "STEAM_COMPAT_DATA_PATH")
self.environ_changed.emit("STEAM_COMPAT_DATA_PATH")
config_helper.remove_option(f"{self.app_name}.env", "STEAM_COMPAT_CLIENT_INSTALL_PATH")
self.environ_changed.emit("STEAM_COMPAT_CLIENT_INSTALL_PATH")
self.app_name: str = "default"
self.core = RareCore.instance().core()
self.wrappers: Wrappers = RareCore.instance().wrappers()
self.tool_wrapper: Optional[Wrapper] = None
self.proton_prefix.setEnabled(False)
self.proton_prefix.setText("")
self._linux_settings.ui.wine_groupbox.setEnabled(True)
else:
self.proton_prefix.setEnabled(True)
self._linux_settings.ui.wine_groupbox.setEnabled(False)
wrapper = self.possible_proton_combos[i - 1]
self._wrapper_settings.add_wrapper(wrapper)
config_helper.add_option(self.app_name, "no_wine", "true")
config_helper.add_option(
f"{self.app_name}.env",
"STEAM_COMPAT_CLIENT_INSTALL_PATH",
str(Path.home().joinpath(".steam", "steam"))
def showEvent(self, a0: QShowEvent) -> None:
if a0.spontaneous():
return super().showEvent(a0)
self.ui.proton_combo.blockSignals(True)
self.ui.proton_combo.clear()
self.ui.proton_combo.addItem(self.tr("Don't use a compatibility tool"), None)
tools = proton.find_tools()
for tool in tools:
self.ui.proton_combo.addItem(tool.name, tool)
try:
wrapper = next(
filter(lambda w: w.is_compat_tool, self.wrappers.get_game_wrapper_list(self.app_name))
)
self.environ_changed.emit("STEAM_COMPAT_CLIENT_INSTALL_PATH")
self.tool_wrapper = wrapper
tool = next(filter(lambda t: t.checksum == wrapper.checksum, tools))
index = self.ui.proton_combo.findData(tool)
except StopIteration:
index = 0
self.ui.proton_combo.setCurrentIndex(index)
self.ui.proton_combo.blockSignals(False)
enabled = bool(self.ui.proton_combo.currentIndex())
self.proton_prefix.blockSignals(True)
self.proton_prefix.setText(config.get_envvar(self.app_name, "STEAM_COMPAT_DATA_PATH", fallback=""))
self.proton_prefix.setEnabled(enabled)
self.proton_prefix.blockSignals(False)
super().showEvent(a0)
self.proton_prefix.setText(os.path.expanduser("~/.proton"))
def __on_proton_changed(self, index):
steam_tool: Union[proton.ProtonTool, proton.CompatibilityTool] = self.ui.proton_combo.itemData(index)
# Don't use Wine
self._linux_settings.wine_exec.setText("")
self._linux_settings.wine_prefix.setText("")
steam_environ = proton.get_steam_environment(steam_tool)
for key, value in steam_environ.items():
if not value:
config.remove_envvar(self.app_name, key)
else:
config.add_envvar(self.app_name, key, value)
self.environ_changed.emit(key)
config_helper.save_config()
wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
if self.tool_wrapper and self.tool_wrapper in wrappers:
wrappers.remove(self.tool_wrapper)
if steam_tool is None:
self.tool_wrapper = None
else:
wrapper = Wrapper(
command=steam_tool.command(), name=steam_tool.name, wtype=WrapperType.COMPAT_TOOL
)
wrappers.append(wrapper)
self.tool_wrapper = wrapper
self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
def proton_prefix_edit(self, text: str) -> Tuple[bool, str, int]:
self.proton_prefix.setEnabled(steam_tool is not None)
self.proton_prefix.setText(os.path.expanduser("~/.proton") if steam_tool is not None else "")
self.tool_enabled.emit(steam_tool is not None)
config.save_config()
@staticmethod
def proton_prefix_edit(text: str) -> Tuple[bool, str, int]:
if not text:
return False, text, IndicatorReasonsCommon.EMPTY
parent_dir = os.path.dirname(text)
@ -88,28 +109,9 @@ class ProtonSettings(QGroupBox):
def proton_prefix_save(self, text: str):
if not text:
return
config_helper.add_option(
f"{self.app_name}.env", "STEAM_COMPAT_DATA_PATH", text
)
config.add_envvar(self.app_name, "STEAM_COMPAT_DATA_PATH", text)
self.environ_changed.emit("STEAM_COMPAT_DATA_PATH")
config_helper.save_config()
config.save_config()
def load_settings(self, app_name: str, proton: str):
self.changeable = False
def load_settings(self, app_name: str):
self.app_name = app_name
proton = proton.replace('"', "")
self.proton_prefix.setEnabled(bool(proton))
if proton:
self.ui.proton_combo.setCurrentText(
f'"{proton.replace(" run", "")}" run'
)
else:
self.ui.proton_combo.setCurrentIndex(0)
proton_prefix = self.core.lgd.config.get(
f"{app_name}.env",
"STEAM_COMPAT_DATA_PATH",
fallback="",
)
self.proton_prefix.setText(proton_prefix)
self.changeable = True

View file

@ -2,6 +2,7 @@ from PyQt5.QtCore import QSettings
from PyQt5.QtWidgets import QGroupBox
from rare.shared import GlobalSignalsSingleton
from rare.models.options import options
from rare.ui.components.tabs.settings.widgets.rpc import Ui_RPCSettings
@ -13,22 +14,22 @@ class RPCSettings(QGroupBox, Ui_RPCSettings):
self.settings = QSettings()
self.enable.setCurrentIndex(self.settings.value("rpc_enable", 0, int))
self.enable.currentIndexChanged.connect(self.changed)
self.enable.setCurrentIndex(self.settings.value(*options.rpc_enable))
self.enable.currentIndexChanged.connect(self.__enable_changed)
self.show_game.setChecked((self.settings.value("rpc_name", True, bool)))
self.show_game.setChecked((self.settings.value(*options.rpc_name)))
self.show_game.stateChanged.connect(
lambda: self.settings.setValue("rpc_game", self.show_game.isChecked())
lambda: self.settings.setValue(options.rpc_name.key, self.show_game.isChecked())
)
self.show_os.setChecked((self.settings.value("rpc_os", True, bool)))
self.show_os.setChecked((self.settings.value(*options.rpc_os)))
self.show_os.stateChanged.connect(
lambda: self.settings.setValue("rpc_os", self.show_os.isChecked())
lambda: self.settings.setValue(options.rpc_os.key, self.show_os.isChecked())
)
self.show_time.setChecked((self.settings.value("rpc_time", True, bool)))
self.show_time.setChecked((self.settings.value(*options.rpc_time)))
self.show_time.stateChanged.connect(
lambda: self.settings.setValue("rpc_time", self.show_time.isChecked())
lambda: self.settings.setValue(options.rpc_time.key, self.show_time.isChecked())
)
try:
@ -37,6 +38,6 @@ class RPCSettings(QGroupBox, Ui_RPCSettings):
self.setDisabled(True)
self.setToolTip(self.tr("Pypresence is not installed"))
def changed(self, i):
self.settings.setValue("rpc_enable", i)
def __enable_changed(self, i):
self.settings.setValue(options.rpc_enable.key, i)
self.signals.discord_rpc.apply_settings.emit()

View file

@ -0,0 +1,15 @@
from components.tabs.settings import LinuxSettings
class LinuxAppSettings(LinuxSettings):
def __init__(self, parent=None):
super(LinuxAppSettings, self).__init__(parent=parent)
def update_game(self, app_name):
self.name = app_name
self.wine_prefix.setText(self.load_prefix())
self.wine_exec.setText(self.load_setting(self.name, "wine_executable"))
self.dxvk.load_settings(self.name)
self.mangohud.load_settings(self.name)

View file

@ -3,31 +3,29 @@ import shutil
from logging import getLogger
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QFileDialog, QWidget
from PyQt5.QtWidgets import QFileDialog, QWidget, QFormLayout, QGroupBox
from rare.components.tabs.settings.widgets.dxvk import DxvkSettings
from rare.components.tabs.settings.widgets.mangohud import MangoHudSettings
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
from rare.ui.components.tabs.settings.linux import Ui_LinuxSettings
from rare.ui.components.tabs.settings.widgets.wine import Ui_WineSettings
from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon
from rare.utils import config_helper
logger = getLogger("LinuxSettings")
class LinuxSettings(QWidget):
class WineSettings(QGroupBox):
# str: option key
environ_changed = pyqtSignal(str)
def __init__(self, name=None, parent=None):
super(LinuxSettings, self).__init__(parent=parent)
self.ui = Ui_LinuxSettings()
super(WineSettings, self).__init__(parent=parent)
self.ui = Ui_WineSettings()
self.ui.setupUi(self)
self.core = LegendaryCoreSingleton()
self.signals = GlobalSignalsSingleton()
self.name = name if name is not None else "default"
self.app_name: str = "default"
# Wine prefix
self.wine_prefix = PathEdit(
@ -36,49 +34,46 @@ class LinuxSettings(QWidget):
edit_func=lambda path: (os.path.isdir(path) or not path, path, IndicatorReasonsCommon.DIR_NOT_EXISTS),
save_func=self.save_prefix,
)
self.ui.prefix_layout.addWidget(self.wine_prefix)
self.ui.main_layout.setWidget(
self.ui.main_layout.getWidgetPosition(self.ui.prefix_label)[0],
QFormLayout.FieldRole,
self.wine_prefix
)
# Wine executable
self.wine_exec = PathEdit(
self.load_setting(self.name, "wine_executable"),
self.load_setting(self.app_name, "wine_executable"),
file_mode=QFileDialog.ExistingFile,
name_filters=["wine", "wine64"],
edit_func=lambda text: (os.path.exists(text) or not text, text, IndicatorReasonsCommon.DIR_NOT_EXISTS),
save_func=lambda text: self.save_setting(
text, section=self.name, setting="wine_executable"
text, section=self.app_name, setting="wine_executable"
),
)
self.ui.exec_layout.addWidget(self.wine_exec)
# dxvk
self.dxvk = DxvkSettings()
self.dxvk.environ_changed.connect(self.environ_changed)
self.ui.linux_layout.addWidget(self.dxvk)
self.dxvk.load_settings(self.name)
self.mangohud = MangoHudSettings()
self.mangohud.environ_changed.connect(self.environ_changed)
self.ui.linux_layout.addWidget(self.mangohud)
self.mangohud.load_settings(self.name)
self.ui.main_layout.setWidget(
self.ui.main_layout.getWidgetPosition(self.ui.exec_label)[0],
QFormLayout.FieldRole,
self.wine_exec
)
def load_prefix(self) -> str:
return self.load_setting(
f"{self.name}.env",
f"{self.app_name}.env",
"WINEPREFIX",
fallback=self.load_setting(self.name, "wine_prefix"),
fallback=self.load_setting(self.app_name, "wine_prefix"),
)
def save_prefix(self, text: str):
self.save_setting(text, f"{self.name}.env", "WINEPREFIX")
self.save_setting(text, f"{self.app_name}.env", "WINEPREFIX")
self.environ_changed.emit("WINEPREFIX")
self.save_setting(text, self.name, "wine_prefix")
self.save_setting(text, self.app_name, "wine_prefix")
self.signals.application.prefix_updated.emit()
def load_setting(self, section: str, setting: str, fallback: str = ""):
return self.core.lgd.config.get(section, setting, fallback=fallback)
def save_setting(self, text: str, section: str, setting: str):
@staticmethod
def save_setting(text: str, section: str, setting: str):
if text:
config_helper.add_option(section, setting, text)
logger.debug(f"Set {setting} in {f'[{section}]'} to {text}")

View file

@ -1,9 +1,9 @@
import re
import shlex
import shutil
from logging import getLogger
from typing import Dict, Optional
from typing import Optional
from PyQt5.QtCore import pyqtSignal, QSettings, QSize, Qt, QMimeData, pyqtSlot, QCoreApplication
from PyQt5.QtCore import pyqtSignal, QSize, Qt, QMimeData, pyqtSlot, QCoreApplication
from PyQt5.QtGui import QDrag, QDropEvent, QDragEnterEvent, QDragMoveEvent, QFont, QMouseEvent
from PyQt5.QtWidgets import (
QHBoxLayout,
@ -16,46 +16,41 @@ from PyQt5.QtWidgets import (
QScrollArea,
QAction,
QToolButton,
QMenu,
QMenu, QDialog,
)
from rare.models.wrapper import Wrapper
from rare.shared import RareCore
from rare.ui.components.tabs.settings.widgets.wrapper import Ui_WrapperSettings
from rare.utils import config_helper
from rare.utils.misc import icon
from rare.utils.runners import proton
logger = getLogger("WrapperSettings")
extra_wrapper_regex = {
"proton": "\".*proton\" run", # proton
"mangohud": "mangohud" # mangohud
}
# extra_wrapper_regex = {
# "proton": "\".*proton\" run", # proton
# }
class Wrapper:
class WrapperDialog(QDialog):
pass
class WrapperWidget(QFrame):
update_wrapper = pyqtSignal(str, str)
delete_wrapper = pyqtSignal(str)
# object: current, object: new
update_wrapper = pyqtSignal(object, object)
# object: current
delete_wrapper = pyqtSignal(object)
def __init__(self, text: str, show_text=None, parent=None):
def __init__(self, wrapper: Wrapper, parent=None):
super(WrapperWidget, self).__init__(parent=parent)
if not show_text:
show_text = text.split()[0]
self.setFrameShape(QFrame.StyledPanel)
self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
self.setToolTip(wrapper.command)
self.text = text
self.setToolTip(text)
unmanaged = show_text in extra_wrapper_regex.keys()
text_lbl = QLabel(show_text, parent=self)
text_lbl = QLabel(wrapper.name, parent=self)
text_lbl.setFont(QFont("monospace"))
text_lbl.setDisabled(unmanaged)
text_lbl.setEnabled(wrapper.is_editable)
image_lbl = QLabel(parent=self)
image_lbl.setPixmap(icon("mdi.drag-vertical").pixmap(QSize(20, 20)))
@ -72,8 +67,8 @@ class WrapperWidget(QFrame):
manage_button.setIcon(icon("mdi.menu"))
manage_button.setMenu(manage_menu)
manage_button.setPopupMode(QToolButton.InstantPopup)
manage_button.setDisabled(unmanaged)
if unmanaged:
manage_button.setEnabled(wrapper.is_editable)
if not wrapper.is_editable:
manage_button.setToolTip(self.tr("Manage through settings"))
else:
manage_button.setToolTip(self.tr("Manage"))
@ -85,28 +80,39 @@ class WrapperWidget(QFrame):
layout.addWidget(manage_button)
self.setLayout(layout)
self.wrapper = wrapper
# lk: set object names for the stylesheet
self.setObjectName(type(self).__name__)
manage_button.setObjectName(f"{self.objectName()}Button")
@pyqtSlot()
def __delete(self):
self.delete_wrapper.emit(self.text)
def data(self) -> Wrapper:
return self.wrapper
@pyqtSlot()
def __delete(self) -> None:
self.delete_wrapper.emit(self.wrapper)
self.deleteLater()
@pyqtSlot()
def __edit(self) -> None:
dialog = QInputDialog(self)
dialog.setWindowTitle(f"{self.tr('Edit wrapper')} - {QCoreApplication.instance().applicationName()}")
dialog.setLabelText(self.tr("Edit wrapper command"))
dialog.setTextValue(self.text)
dialog.setTextValue(self.wrapper.command)
accepted = dialog.exec()
wrapper = dialog.textValue()
command = dialog.textValue()
dialog.deleteLater()
if accepted and wrapper:
self.update_wrapper.emit(self.text, wrapper)
if accepted and command:
new_wrapper = Wrapper(command=shlex.split(command))
self.update_wrapper.emit(self.wrapper, new_wrapper)
self.deleteLater()
def mouseMoveEvent(self, a0: QMouseEvent) -> None:
if a0.buttons() == Qt.LeftButton:
a0.accept()
if self.wrapper.is_compat_tool:
return
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
@ -114,30 +120,22 @@ class WrapperWidget(QFrame):
class WrapperSettings(QWidget):
def __init__(self):
super(WrapperSettings, self).__init__()
def __init__(self, parent=None):
super(WrapperSettings, self).__init__(parent=parent)
self.ui = Ui_WrapperSettings()
self.ui.setupUi(self)
self.wrappers: Dict[str, WrapperWidget] = {}
self.app_name: str = "default"
self.wrapper_scroll = QScrollArea(self.ui.widget_stack)
self.wrapper_scroll.setWidgetResizable(True)
self.wrapper_scroll.setSizeAdjustPolicy(QScrollArea.AdjustToContents)
self.wrapper_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.wrapper_scroll.setProperty("no_kinetic_scroll", True)
self.scroll_content = WrapperContainer(
save_cb=self.save, parent=self.wrapper_scroll
)
self.wrapper_scroll.setWidget(self.scroll_content)
self.wrapper_container = WrapperContainer(parent=self.wrapper_scroll)
self.wrapper_container.orderChanged.connect(self.__on_order_changed)
self.wrapper_scroll.setWidget(self.wrapper_container)
self.ui.widget_stack.insertWidget(0, self.wrapper_scroll)
self.core = RareCore.instance().core()
self.ui.add_button.clicked.connect(self.add_button_pressed)
self.settings = QSettings()
self.ui.add_button.clicked.connect(self.__on_add_button_pressed)
self.wrapper_scroll.horizontalScrollBar().rangeChanged.connect(self.adjust_scrollarea)
# lk: set object names for the stylesheet
@ -149,18 +147,24 @@ class WrapperSettings(QWidget):
self.wrapper_scroll.verticalScrollBar().setObjectName(
f"{self.wrapper_scroll.objectName()}Bar")
self.ui.wrapper_settings_layout.setAlignment(Qt.AlignTop)
self.app_name: str = "default"
self.core = RareCore.instance().core()
self.wrappers = RareCore.instance().wrappers()
@pyqtSlot(int, int)
def adjust_scrollarea(self, min: int, max: int):
wrapper_widget = self.scroll_content.findChild(WrapperWidget)
def adjust_scrollarea(self, minh: int, maxh: int):
wrapper_widget = self.wrapper_container.findChild(WrapperWidget)
if not wrapper_widget:
return
return
# lk: when the scrollbar is not visible, min and max are 0
if max > min:
if maxh > minh:
self.wrapper_scroll.setMaximumHeight(
wrapper_widget.sizeHint().height()
+ self.wrapper_scroll.rect().height() // 2
- self.wrapper_scroll.contentsRect().height() // 2
+ self.scroll_content.layout().spacing()
+ self.wrapper_container.layout().spacing()
+ self.wrapper_scroll.horizontalScrollBar().sizeHint().height()
)
else:
@ -170,187 +174,183 @@ class WrapperSettings(QWidget):
- self.wrapper_scroll.contentsRect().height()
)
def get_wrapper_string(self):
return " ".join(self.get_wrapper_list())
@pyqtSlot(QWidget, int)
def __on_order_changed(self, widget: WrapperWidget, new_index: int):
wrapper = widget.data()
wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
wrappers.remove(wrapper)
wrappers.insert(new_index, wrapper)
self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
def get_wrapper_list(self):
wrappers = list(self.wrappers.values())
wrappers.sort(key=lambda x: self.scroll_content.layout().indexOf(x))
return [w.text for w in wrappers]
def add_button_pressed(self):
@pyqtSlot()
def __on_add_button_pressed(self):
dialog = QInputDialog(self)
dialog.setWindowTitle(f"{self.tr('Add wrapper')} - {QCoreApplication.instance().applicationName()}")
dialog.setLabelText(self.tr("Enter wrapper command"))
accepted = dialog.exec()
wrapper = dialog.textValue()
command = dialog.textValue()
dialog.deleteLater()
if accepted:
self.add_wrapper(wrapper)
def add_wrapper(self, text: str, position: int = -1, from_load: bool = False):
if text == "mangohud" and self.wrappers.get("mangohud"):
return
show_text = ""
for key, extra_wrapper in extra_wrapper_regex.items():
if re.match(extra_wrapper, text):
show_text = key
if not show_text:
show_text = text.split()[0]
# validate
if not text.strip(): # is empty
return
if not from_load:
if self.wrappers.get(text):
QMessageBox.warning(
self, self.tr("Warning"), self.tr("Wrapper <b>{0}</b> is already in the list").format(text)
)
return
if show_text != "proton" and not shutil.which(text.split()[0]):
if (
QMessageBox.question(
self,
self.tr("Warning"),
self.tr("Wrapper <b>{0}</b> is not in $PATH. Add it anyway?").format(show_text),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
)
== QMessageBox.No
):
return
if text == "proton":
QMessageBox.warning(
self,
self.tr("Warning"),
self.tr("Do not insert <b>proton</b> manually. Add it through Proton settings"),
)
return
wrapper = Wrapper(shlex.split(command))
self.add_user_wrapper(wrapper)
def __add_wrapper(self, wrapper: Wrapper, position: int = -1):
self.ui.widget_stack.setCurrentIndex(0)
if widget := self.wrappers.get(show_text, None):
widget.deleteLater()
widget = WrapperWidget(text, show_text, self.scroll_content)
widget = WrapperWidget(wrapper, self.wrapper_container)
if position < 0:
self.scroll_content.layout().addWidget(widget)
self.wrapper_container.addWidget(widget)
else:
self.scroll_content.layout().insertWidget(position, widget)
self.wrapper_container.insertWidget(position, widget)
self.adjust_scrollarea(
self.wrapper_scroll.horizontalScrollBar().minimum(),
self.wrapper_scroll.horizontalScrollBar().maximum(),
)
widget.update_wrapper.connect(self.update_wrapper)
widget.delete_wrapper.connect(self.delete_wrapper)
widget.update_wrapper.connect(self.__update_wrapper)
widget.delete_wrapper.connect(self.__delete_wrapper)
self.wrappers[show_text] = widget
def add_wrapper(self, wrapper: Wrapper, position: int = -1):
wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
if position < 0 or wrapper.is_compat_tool:
wrappers.append(wrapper)
else:
wrappers.insert(position, wrapper)
self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
self.__add_wrapper(wrapper, position)
if not from_load:
self.save()
def add_user_wrapper(self, wrapper: Wrapper, position: int = -1):
if not wrapper:
return
@pyqtSlot(str)
def delete_wrapper(self, text: str):
text = text.split()[0]
widget = self.wrappers.get(text, None)
if widget:
self.wrappers.pop(text)
widget.deleteLater()
compat_cmds = [tool.command() for tool in proton.find_tools()]
if wrapper.command in compat_cmds:
QMessageBox.warning(
self,
self.tr("Warning"),
self.tr("Do not insert compatibility tools manually. Add them through Proton settings"),
)
return
if not self.wrappers:
# if text == "mangohud" and self.wrappers.get("mangohud"):
# return
# show_text = ""
# for key, extra_wrapper in extra_wrapper_regex.items():
# if re.match(extra_wrapper, text):
# show_text = key
# if not show_text:
# show_text = text.split()[0]
if wrapper.checksum in self.wrappers.get_game_md5sum_list(self.app_name):
QMessageBox.warning(
self, self.tr("Warning"), self.tr("Wrapper <b>{0}</b> is already in the list").format(wrapper.command)
)
return
if not shutil.which(wrapper.executable):
ans = QMessageBox.question(
self,
self.tr("Warning"),
self.tr("Wrapper <b>{0}</b> is not in $PATH. Add it anyway?").format(wrapper.executable),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if ans == QMessageBox.No:
return
self.add_wrapper(wrapper, position)
@pyqtSlot(object)
def __delete_wrapper(self, wrapper: Wrapper):
wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
wrappers.remove(wrapper)
self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
if not wrappers:
self.wrapper_scroll.setMaximumHeight(self.ui.label_page.sizeHint().height())
self.ui.widget_stack.setCurrentIndex(1)
self.save()
@pyqtSlot(object, object)
def __update_wrapper(self, old: Wrapper, new: Wrapper):
wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
index = wrappers.index(old)
wrappers.remove(old)
wrappers.insert(index, new)
self.wrappers.set_game_wrapper_list(self.app_name, wrappers)
self.__add_wrapper(new, index)
@pyqtSlot(str, str)
def update_wrapper(self, old: str, new: str):
key = old.split()[0]
idx = self.scroll_content.layout().indexOf(self.wrappers[key])
self.delete_wrapper(key)
self.add_wrapper(new, position=idx)
def save(self):
# save wrappers twice, to support wrappers with spaces
if len(self.wrappers) == 0:
config_helper.remove_option(self.app_name, "wrapper")
self.settings.remove(f"{self.app_name}/wrapper")
else:
config_helper.add_option(self.app_name, "wrapper", self.get_wrapper_string())
self.settings.setValue(f"{self.app_name}/wrapper", self.get_wrapper_list())
def load_settings(self, app_name: str):
self.app_name = app_name
for i in self.wrappers.values():
i.deleteLater()
self.wrappers.clear()
wrappers = self.settings.value(f"{self.app_name}/wrapper", [], str)
if not wrappers and (cfg := self.core.lgd.config.get(self.app_name, "wrapper", fallback="")):
logger.info("Loading wrappers from legendary config")
# no qt wrapper, but legendary wrapper, to have backward compatibility
pattern = re.compile(r'''((?:[^ "']|"[^"]*"|'[^']*')+)''')
wrappers = pattern.split(cfg)[1::2]
for wrapper in wrappers:
self.add_wrapper(wrapper, from_load=True)
if not self.wrappers:
@pyqtSlot()
def update_state(self):
for w in self.wrapper_container.findChildren(WrapperWidget, options=Qt.FindDirectChildrenOnly):
w.deleteLater()
wrappers = self.wrappers.get_game_wrapper_list(self.app_name)
if not wrappers:
self.wrapper_scroll.setMaximumHeight(self.ui.label_page.sizeHint().height())
self.ui.widget_stack.setCurrentIndex(1)
else:
self.ui.widget_stack.setCurrentIndex(0)
for wrapper in wrappers:
self.__add_wrapper(wrapper)
self.save()
def load_settings(self, app_name: str):
self.app_name = app_name
self.update_state()
class WrapperContainer(QWidget):
# QWidget: moving widget, int: new index
orderChanged: pyqtSignal = pyqtSignal(QWidget, int)
def __init__(self, save_cb, parent=None):
def __init__(self, parent=None):
super(WrapperContainer, self).__init__(parent=parent)
self.setAcceptDrops(True)
self.save = save_cb
layout = QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.setLayout(layout)
self.__layout = QHBoxLayout(self)
self.__layout.setContentsMargins(0, 0, 0, 0)
self.__layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.drag_widget: Optional[QWidget] = None
self.__drag_widget: Optional[QWidget] = None
# lk: set object names for the stylesheet
self.setObjectName(type(self).__name__)
# def count(self) -> int:
# return self.__layout.count()
#
# def itemData(self, index: int) -> Any:
# widget: WrapperWidget = self.__layout.itemAt(index).widget()
# return widget.data()
def addWidget(self, widget: WrapperWidget):
self.__layout.addWidget(widget)
def insertWidget(self, index: int, widget: WrapperWidget):
self.__layout.insertWidget(index, widget)
def dragEnterEvent(self, e: QDragEnterEvent):
widget = e.source()
self.drag_widget = widget
self.__drag_widget = widget
e.accept()
def _get_drop_index(self, x):
drag_idx = self.layout().indexOf(self.drag_widget)
def __get_drop_index(self, x) -> int:
drag_idx = self.__layout.indexOf(self.__drag_widget)
if drag_idx > 0:
prev_widget = self.layout().itemAt(drag_idx - 1).widget()
if x < self.drag_widget.x() - prev_widget.width() // 2:
prev_widget = self.__layout.itemAt(drag_idx - 1).widget()
if x < self.__drag_widget.x() - prev_widget.width() // 2:
return drag_idx - 1
if drag_idx < self.layout().count() - 1:
next_widget = self.layout().itemAt(drag_idx + 1).widget()
if x > self.drag_widget.x() + self.drag_widget.width() + next_widget.width() // 2:
if drag_idx < self.__layout.count() - 1:
next_widget = self.__layout.itemAt(drag_idx + 1).widget()
if x > self.__drag_widget.x() + self.__drag_widget.width() + next_widget.width() // 2:
return drag_idx + 1
return drag_idx
def dragMoveEvent(self, e: QDragMoveEvent) -> None:
i = self._get_drop_index(e.pos().x())
self.layout().insertWidget(i, self.drag_widget)
new_x = self.__get_drop_index(e.pos().x())
self.__layout.insertWidget(new_x, self.__drag_widget)
def dropEvent(self, e: QDropEvent):
pos = e.pos()
widget = e.source()
index = self._get_drop_index(pos.x())
self.layout().insertWidget(index, widget)
self.drag_widget = None
new_x = self.__get_drop_index(pos.x())
self.__layout.insertWidget(new_x, widget)
self.__drag_widget = None
self.orderChanged.emit(widget, new_x)
e.accept()
self.save()

View file

@ -5,6 +5,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QSettings
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QAction, QApplication
from rare.models.options import options
from rare.shared import RareCore
logger = getLogger("TrayIcon")
@ -61,7 +62,7 @@ class TrayIcon(QSystemTrayIcon):
@pyqtSlot(str, str)
def notify(self, title: str, body: str):
if self.settings.value("notification", True, bool):
if self.settings.value(*options.notification):
self.showMessage(f"{QApplication.applicationName()} - {title}", body, QSystemTrayIcon.Information, 4000)
@pyqtSlot()

View file

@ -17,6 +17,7 @@ from legendary.models.game import SaveGameStatus
from rare.lgndr.core import LegendaryCore
from rare.models.base_game import RareGameSlim
from rare.models.launcher import ErrorModel, Actions, FinishedModel, BaseModel, StateChangedModel
from rare.models.options import options
from rare.widgets.rare_app import RareApp, RareAppException
from .cloud_sync_dialog import CloudSyncDialog, CloudSyncDialogResult
from .console_dialog import ConsoleDialog
@ -145,7 +146,7 @@ class RareLauncher(RareApp):
lang = self.settings.value("language", self.core.language_code, type=str)
self.load_translator(lang)
if QSettings().value("show_console", False, bool):
if QSettings(self).value(*options.log_games):
self.console = ConsoleDialog()
self.console.show()

View file

@ -18,6 +18,8 @@ def main() -> int:
sys.stderr = open(os.devnull, 'w')
os.environ["QT_QPA_PLATFORMTHEME"] = ""
os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1"
os.environ["QT_SCALE_FACTOR_ROUNDING_POLICY"] = "Floor"
if "LEGENDARY_CONFIG_PATH" in os.environ:
os.environ["LEGENDARY_CONFIG_PATH"] = os.path.expanduser(os.environ["LEGENDARY_CONFIG_PATH"])

View file

@ -10,6 +10,7 @@ from legendary.lfs import eos
from legendary.models.game import SaveGameFile, SaveGameStatus, Game, InstalledGame
from legendary.utils.selective_dl import get_sdl_appname
from rare.models.options import options
from rare.lgndr.core import LegendaryCore
from rare.models.install import UninstallOptionsModel, InstallOptionsModel
@ -222,11 +223,13 @@ class RareGameSlim(RareGameBase):
@property
def auto_sync_saves(self):
return self.supports_cloud_saves and QSettings().value(
f"{self.app_name}/auto_sync_cloud",
QSettings().value("auto_sync_cloud", False, bool),
bool
auto_sync_cloud = QSettings(self).value(
f"{self.app_name}/{options.auto_sync_cloud.key}",
options.auto_sync_cloud.default,
options.auto_sync_cloud.dtype
)
auto_sync_cloud = auto_sync_cloud or QSettings(self).value(*options.auto_sync_cloud)
return self.supports_cloud_saves and auto_sync_cloud
@property
def save_path(self) -> Optional[str]:

View file

@ -2,7 +2,7 @@ import json
import os
import platform
from dataclasses import dataclass, field
from datetime import datetime
from datetime import datetime, UTC
from logging import getLogger
from threading import Lock
from typing import List, Optional, Dict, Set
@ -19,7 +19,7 @@ from rare.shared.game_process import GameProcess
from rare.shared.image_manager import ImageManager
from rare.utils.paths import data_dir, get_rare_executable
from rare.utils.steam_grades import get_rating
from rare.utils.config_helper import add_envvar, remove_envvar
from rare.utils.config_helper import add_envvar
logger = getLogger("RareGame")
@ -27,11 +27,10 @@ logger = getLogger("RareGame")
class RareGame(RareGameSlim):
@dataclass
class Metadata:
auto_update: bool = False
queued: bool = False
queue_pos: Optional[int] = None
last_played: datetime = datetime.min
grant_date: Optional[datetime] = None
grant_date: datetime = datetime.min
steam_appid: Optional[int] = None
steam_grade: Optional[str] = None
steam_date: datetime = datetime.min
@ -40,24 +39,23 @@ class RareGame(RareGameSlim):
@classmethod
def from_dict(cls, data: Dict):
return cls(
auto_update=data.get("auto_update", False),
queued=data.get("queued", False),
queue_pos=data.get("queue_pos", None),
last_played=datetime.fromisoformat(data["last_played"]) if data.get("last_played", None) else datetime.min,
grant_date=datetime.fromisoformat(data["grant_date"]) if data.get("grant_date", None) else None,
last_played=datetime.fromisoformat(x) if (x := data.get("last_played", None)) else datetime.min,
grant_date=datetime.fromisoformat(x) if (x := data.get("grant_date", None)) else datetime.min,
steam_appid=data.get("steam_appid", None),
steam_grade=data.get("steam_grade", None),
steam_date=datetime.fromisoformat(data["steam_date"]) if data.get("steam_date", None) else datetime.min,
steam_date=datetime.fromisoformat(x) if (x := data.get("steam_date", None)) else datetime.min,
tags=data.get("tags", []),
)
def as_dict(self):
@property
def __dict__(self):
return dict(
auto_update=self.auto_update,
queued=self.queued,
queue_pos=self.queue_pos,
last_played=self.last_played.isoformat() if self.last_played else datetime.min,
grant_date=self.grant_date.isoformat() if self.grant_date else None,
grant_date=self.grant_date.isoformat() if self.grant_date else datetime.min,
steam_appid=self.steam_appid,
steam_grade=self.steam_grade,
steam_date=self.steam_date.isoformat() if self.steam_date else datetime.min,
@ -81,8 +79,7 @@ class RareGame(RareGameSlim):
self.pixmap: QPixmap = QPixmap()
self.metadata: RareGame.Metadata = RareGame.Metadata()
self.__load_metadata()
if self.metadata.grant_date is None:
self.grant_date()
self.grant_date()
self.owned_dlcs: Set[RareGame] = set()
@ -143,13 +140,14 @@ class RareGame(RareGameSlim):
def __load_metadata_json() -> Dict:
if RareGame.__metadata_json is None:
metadata = {}
file = os.path.join(data_dir(), "game_meta.json")
try:
with open(os.path.join(data_dir(), "game_meta.json"), "r") as metadata_fh:
metadata = json.load(metadata_fh)
with open(file, "r") as f:
metadata = json.load(f)
except FileNotFoundError:
logger.info("Game metadata json file does not exist.")
logger.info("%s does not exist", file)
except json.JSONDecodeError:
logger.warning("Game metadata json file is corrupt.")
logger.warning("%s is corrupt", file)
finally:
RareGame.__metadata_json = metadata
return RareGame.__metadata_json
@ -166,9 +164,9 @@ class RareGame(RareGameSlim):
with RareGame.__metadata_lock:
metadata: Dict = self.__load_metadata_json()
# pylint: disable=unsupported-assignment-operation
metadata[self.app_name] = self.metadata.as_dict()
with open(os.path.join(data_dir(), "game_meta.json"), "w") as metadata_json:
json.dump(metadata, metadata_json, indent=2)
metadata[self.app_name] = vars(self.metadata)
with open(os.path.join(data_dir(), "game_meta.json"), "w+") as file:
json.dump(metadata, file, indent=2)
def update_game(self):
self.game = self.core.get_game(
@ -432,29 +430,27 @@ class RareGame(RareGameSlim):
def steam_grade(self) -> str:
if platform.system() == "Windows" or self.is_unreal:
return "na"
elapsed_time = abs(datetime.utcnow() - self.metadata.steam_date)
if (
self.metadata.steam_grade is not None
and self.metadata.steam_appid is not None
and elapsed_time.days < 3
):
return self.metadata.steam_grade
if self.metadata.steam_grade != "pending":
elapsed_time = abs(datetime.utcnow() - self.metadata.steam_date)
def _set_steam_grade():
appid, rating = get_rating(self.core, self.app_name)
self.set_steam_grade(appid, rating)
worker = QRunnable.create(_set_steam_grade)
QThreadPool.globalInstance().start(worker)
return "pending"
if elapsed_time.days > 3 and (self.metadata.steam_grade is None or self.metadata.steam_appid is None):
def _set_steam_grade():
appid, rating = get_rating(self.core, self.app_name)
self.set_steam_grade(appid, rating)
worker = QRunnable.create(_set_steam_grade)
QThreadPool.globalInstance().start(worker)
self.metadata.steam_grade = "pending"
return self.metadata.steam_grade
@property
def steam_appid(self) -> Optional[int]:
return self.metadata.steam_appid
def set_steam_grade(self, appid: int, grade: str) -> None:
if appid or self.steam_appid is None:
if appid and self.steam_appid is None:
add_envvar(self.app_name, "SteamAppId", str(appid))
add_envvar(self.app_name, "SteamGameId", str(appid))
add_envvar(self.app_name, "STEAM_COMPAT_APP_ID", str(appid))
self.metadata.steam_appid = appid
self.metadata.steam_grade = grade
self.metadata.steam_date = datetime.utcnow()
@ -463,17 +459,17 @@ class RareGame(RareGameSlim):
def grant_date(self, force=False) -> datetime:
if (entitlements := self.core.lgd.entitlements) is None:
return self.metadata.grant_date
if self.metadata.grant_date is None or force:
return self.metadata.grant_date.replace(tzinfo=UTC)
if self.metadata.grant_date == datetime.min.replace(tzinfo=UTC) or force:
logger.debug("Grant date for %s not found in metadata, resolving", self.app_name)
matching = filter(lambda ent: ent["namespace"] == self.game.namespace, entitlements)
entitlement = next(matching, None)
grant_date = datetime.fromisoformat(
entitlement["grantDate"].replace("Z", "+00:00")
) if entitlement else None
) if entitlement else datetime.min.replace(tzinfo=UTC)
self.metadata.grant_date = grant_date
self.__save_metadata()
return self.metadata.grant_date
return self.metadata.grant_date.replace(tzinfo=UTC)
def set_origin_attributes(self, path: str, size: int = 0) -> None:
self.__origin_install_path = path

19
rare/models/library.py Normal file
View file

@ -0,0 +1,19 @@
from enum import IntEnum
class LibraryFilter(IntEnum):
ALL = 1
INSTALLED = 2
OFFLINE = 3
HIDDEN = 4
WIN32 = 5
MAC = 6
INSTALLABLE = 7
INCLUDE_UE = 8
class LibraryOrder(IntEnum):
TITLE = 1
RECENT = 2
NEWEST = 3
OLDEST = 4

53
rare/models/options.py Normal file
View file

@ -0,0 +1,53 @@
import platform as pf
from argparse import Namespace
from typing import Any, Type
from .library import LibraryFilter, LibraryOrder
class Value(Namespace):
key: str
default: Any
dtype: Type
def __len__(self):
return len(self.__dict__)
def __iter__(self):
for v in self.__dict__.values():
yield v
# They key names are set to the existing option name
class Defaults(Namespace):
win32_meta = Value(key="win32_meta", default=False, dtype=bool)
macos_meta = Value(key="macos_meta", default=pf.system() == "Darwin", dtype=bool)
unreal_meta = Value(key="unreal_meta", default=False, dtype=bool)
exclude_non_asset = Value(key="exclude_non_asset", default=False, dtype=bool)
exclude_entitlements = Value(key="exclude_entitlements", default=False, dtype=bool)
sys_tray = Value(key="sys_tray", default=True, dtype=bool)
auto_update = Value(key="auto_update", default=False, dtype=bool)
auto_sync_cloud = Value(key="auto_sync_cloud", default=False, dtype=bool)
confirm_start = Value(key="confirm_start", default=False, dtype=bool)
save_size = Value(key="save_size", default=False, dtype=bool)
window_size = Value(key="window_size", default=(1280, 720), dtype=tuple)
notification = Value(key="notification", default=True, dtype=bool)
log_games = Value(key="show_console", default=False, dtype=bool)
library_filter = Value(
key="library_filter",
default=int(LibraryFilter.MAC if pf.system() == "Darwin" else LibraryFilter.ALL), dtype=int
)
library_order = Value(
key="library_order", default=int(LibraryOrder.TITLE), dtype=int
)
rpc_enable = Value(key="rpc_enable", default=0, dtype=int)
rpc_name = Value(key="rpc_game", default=True, dtype=bool)
rpc_time = Value(key="rpc_time", default=True, dtype=bool)
rpc_os = Value(key="rpc_os", default=True, dtype=bool)
options = Defaults()
__all__ = ['options']

View file

@ -2,46 +2,72 @@ import os
from typing import Union, List
from legendary.core import LegendaryCore
from legendary.models.game import InstalledGame
from rare.utils.config_helper import get_prefixes
class PathSpec:
__egl_path_vars = {
"{appdata}": os.path.expandvars("%LOCALAPPDATA%"),
"{userdir}": os.path.expandvars("%USERPROFILE%/Documents"),
# '{userprofile}': os.path.expandvars('%userprofile%'), # possibly wrong
"{usersavedgames}": os.path.expandvars("%USERPROFILE%/Saved Games"),
}
egl_appdata: str = r"%LOCALAPPDATA%\EpicGamesLauncher\Saved\Config\Windows"
egl_programdata: str = r"%PROGRAMDATA%\Epic\EpicGamesLauncher\Data\Manifests"
wine_programdata: str = r"dosdevices/c:/ProgramData"
def __init__(self, core: LegendaryCore = None, app_name: str = "default"):
if core is not None:
self.__egl_path_vars.update({"{epicid}": core.lgd.userdata["account_id"]})
self.app_name = app_name
@staticmethod
def egl_appdata() -> str:
return r"%LOCALAPPDATA%\EpicGamesLauncher\Saved\Config\Windows"
def cook(self, path: str) -> str:
cooked_path = [self.__egl_path_vars.get(p.lower(), p) for p in path.split("/")]
return os.path.join(*cooked_path)
@staticmethod
def egl_programdata() -> str:
return r"%PROGRAMDATA%\Epic\EpicGamesLauncher\Data\Manifests"
@property
def wine_egl_programdata(self):
return self.egl_programdata.replace("\\", "/").replace("%PROGRAMDATA%", self.wine_programdata)
@staticmethod
def wine_programdata() -> str:
return r"ProgramData"
def wine_egl_prefixes(self, results: int = 0) -> Union[List[str], str]:
possible_prefixes = [
os.path.expanduser("~/.wine"),
os.path.expanduser("~/Games/epic-games-store"),
]
@staticmethod
def wine_egl_programdata() -> str:
return PathSpec.egl_programdata(
).replace(
"\\", "/"
).replace(
"%PROGRAMDATA%", PathSpec.wine_programdata()
)
@staticmethod
def prefix_egl_programdata(prefix: str) -> str:
return os.path.join(prefix, "dosdevices/c:", PathSpec.wine_egl_programdata())
@staticmethod
def wine_egl_prefixes(results: int = 0) -> Union[List[str], str]:
possible_prefixes = get_prefixes()
prefixes = []
for prefix in possible_prefixes:
if os.path.exists(os.path.join(prefix, self.wine_egl_programdata)):
if os.path.exists(os.path.join(prefix, PathSpec.wine_egl_programdata())):
prefixes.append(prefix)
if not prefixes:
return str()
return ""
if not results:
return prefixes
elif results == 1:
return prefixes[0]
else:
return prefixes[:results]
def __init__(self, core: LegendaryCore = None, igame: InstalledGame = None):
self.__egl_path_vars = {
"{appdata}": os.path.expandvars("%LOCALAPPDATA%"),
"{userdir}": os.path.expandvars("%USERPROFILE%/Documents"),
"{userprofile}": os.path.expandvars("%userprofile%"), # possibly wrong
"{usersavedgames}": os.path.expandvars("%USERPROFILE%/Saved Games"),
}
if core is not None:
self.__egl_path_vars.update({
"{epicid}": core.lgd.userdata["account_id"]
})
if igame is not None:
self.__egl_path_vars.update({
"{installdir}": igame.install_path,
})
def resolve_egl_path_vars(self, path: str) -> str:
cooked_path = [self.__egl_path_vars.get(p.lower(), p) for p in path.split("/")]
return os.path.join(*cooked_path)

74
rare/models/wrapper.py Normal file
View file

@ -0,0 +1,74 @@
import os
import shlex
from hashlib import md5
from enum import IntEnum
from typing import Dict, List, Union
class WrapperType(IntEnum):
NONE = 0
COMPAT_TOOL = 1
LEGENDARY_IMPORT = 8
USER_DEFINED = 9
class Wrapper:
def __init__(self, command: Union[str, List[str]], name: str = None, wtype: WrapperType = None):
self.__command: List[str] = shlex.split(command) if isinstance(command, str) else command
self.__name: str = name if name is not None else os.path.basename(self.__command[0])
self.__wtype: WrapperType = wtype if wtype is not None else WrapperType.USER_DEFINED
@property
def is_compat_tool(self) -> bool:
return self.__wtype == WrapperType.COMPAT_TOOL
@property
def is_editable(self) -> bool:
return self.__wtype == WrapperType.USER_DEFINED or self.__wtype == WrapperType.LEGENDARY_IMPORT
@property
def checksum(self) -> str:
return md5(self.command.encode("utf-8")).hexdigest()
@property
def executable(self) -> str:
return shlex.quote(self.__command[0])
@property
def command(self) -> str:
return " ".join(shlex.quote(part) for part in self.__command)
@property
def name(self) -> str:
return self.__name
@property
def type(self) -> WrapperType:
return self.__wtype
def __eq__(self, other) -> bool:
return self.command == other.command
def __hash__(self):
return hash(self.__command)
def __bool__(self) -> bool:
if not self.is_editable:
return True
return bool(self.command.strip())
@classmethod
def from_dict(cls, data: Dict):
return cls(
command=data.get("command"),
name=data.get("name"),
wtype=WrapperType(data.get("wtype", WrapperType.USER_DEFINED))
)
@property
def __dict__(self):
return dict(
command=self.__command,
name=self.__name,
wtype=int(self.__wtype)
)

View file

@ -29,7 +29,7 @@ from .workers import (
)
from .workers.uninstall import uninstall_game
from .workers.worker import QueueWorkerInfo, QueueWorkerState
from rare.utils import config_helper
from .wrappers import Wrappers
logger = getLogger("RareCore")
@ -53,6 +53,8 @@ class RareCore(QObject):
self.__signals: Optional[GlobalSignals] = None
self.__core: Optional[LegendaryCore] = None
self.__image_manager: Optional[ImageManager] = None
self.__settings: Optional[QSettings] = None
self.__wrappers: Optional[Wrappers] = None
self.__start_time = time.perf_counter()
@ -61,8 +63,8 @@ class RareCore(QObject):
self.core(init=True)
config_helper.init_config_handler(self.__core)
self.image_manager(init=True)
self.settings = QSettings(self)
self.__settings = QSettings(self)
self.__wrappers = Wrappers()
self.queue_workers: List[QueueWorker] = []
self.queue_threadpool = QThreadPool()
@ -205,6 +207,12 @@ class RareCore(QObject):
self.__image_manager = ImageManager(self.signals(), self.core())
return self.__image_manager
def wrappers(self) -> Wrappers:
return self.__wrappers
def settings(self) -> QSettings:
return self.__settings
def deleteLater(self) -> None:
self.__image_manager.deleteLater()
del self.__image_manager
@ -328,10 +336,13 @@ class RareCore(QObject):
self.__core.lgd.entitlements = result
self.__fetched_entitlements = True
logger.info(f"Acquired data for {FetchWorker.Result(result_type).name}")
logger.info("Acquired data from %s worker", FetchWorker.Result(result_type).name)
if all([self.__fetched_games_dlcs, self.__fetched_entitlements]):
logger.debug(f"Fetch time {time.perf_counter() - self.__start_time} seconds")
logger.debug("Fetch time %s seconds", time.perf_counter() - self.__start_time)
self.__wrappers.import_wrappers(
self.__core, self.__settings, [rgame.app_name for rgame in self.games]
)
self.progress.emit(100, self.tr("Launching Rare"))
self.completed.emit()
QTimer.singleShot(100, self.__post_init)
@ -366,7 +377,7 @@ class RareCore(QObject):
continue
self.__library[app_name].load_saves(saves)
except (HTTPError, ConnectionError) as e:
logger.error(f"Exception while fetching saves from EGS.")
logger.error("Exception while fetching saves from EGS.")
logger.error(e)
return
logger.info(f"Saves: {len(saves_dict)}")

View file

@ -8,6 +8,7 @@ from requests.exceptions import HTTPError, ConnectionError
from rare.lgndr.core import LegendaryCore
from rare.utils.metrics import timelogger
from rare.models.options import options
from .worker import Worker
logger = getLogger("FetchWorker")
@ -32,12 +33,12 @@ class FetchWorker(Worker):
class EntitlementsWorker(FetchWorker):
def __init__(self, core: LegendaryCore, args: Namespace):
super(EntitlementsWorker, self).__init__(core, args)
def run_real(self):
want_entitlements = not self.settings.value(*options.exclude_entitlements)
entitlements = ()
want_entitlements = not self.settings.value("exclude_entitlements", False, bool)
if want_entitlements:
# Get entitlements, Ubisoft integration also uses them
self.signals.progress.emit(0, self.signals.tr("Updating entitlements"))
@ -51,20 +52,20 @@ class EntitlementsWorker(FetchWorker):
class GamesDlcsWorker(FetchWorker):
def __init__(self, core: LegendaryCore, args: Namespace):
super(GamesDlcsWorker, self).__init__(core, args)
self.exclude_non_asset = QSettings().value("exclude_non_asset", False, bool)
def run_real(self):
# Fetch regular EGL games with assets
want_unreal = self.settings.value("unreal_meta", False, bool) or self.args.debug
want_win32 = self.settings.value("win32_meta", False, bool)
want_macos = self.settings.value("macos_meta", False, bool)
# want_unreal = self.settings.value(*options.unreal_meta) or self.args.debug
# want_win32 = self.settings.value(*options.win32_meta) or self.args.debug
# want_macos = self.settings.value(*options.macos_meta) or self.args.debug
want_unreal = self.settings.value(*options.unreal_meta)
want_win32 = self.settings.value(*options.win32_meta)
want_macos = self.settings.value(*options.macos_meta)
want_non_asset = not self.settings.value(*options.exclude_non_asset)
need_macos = platform.system() == "Darwin"
need_windows = not any([want_win32, want_macos, need_macos, self.args.debug]) and not self.args.offline
need_windows = not any([want_win32, want_macos, need_macos]) and not self.args.offline
if want_win32 or self.args.debug:
if want_win32:
logger.info(
"Requesting Win32 metadata due to %s, %s Unreal engine",
"settings" if want_win32 else "debug",
@ -76,7 +77,7 @@ class GamesDlcsWorker(FetchWorker):
update_assets=not self.args.offline, platform="Win32", skip_ue=not want_unreal
)
if need_macos or want_macos or self.args.debug:
if need_macos or want_macos:
logger.info(
"Requesting macOS metadata due to %s, %s Unreal engine",
"platform" if need_macos else "settings" if want_macos else "debug",
@ -101,7 +102,6 @@ class GamesDlcsWorker(FetchWorker):
logger.info(f"Games: %s. Games with DLCs: %s", len(games), len(dlc_dict))
# Fetch non-asset games
want_non_asset = not self.settings.value("exclude_non_asset", False, bool)
if want_non_asset:
self.signals.progress.emit(30, self.signals.tr("Updating non-asset game metadata"))
try:

View file

@ -11,7 +11,7 @@ from rare.lgndr.glue.arguments import LgndrUninstallGameArgs
from rare.lgndr.glue.monkeys import LgndrIndirectStatus
from rare.models.game import RareGame
from rare.models.install import UninstallOptionsModel
from rare.utils import config_helper
from rare.utils import config_helper as config
from rare.utils.paths import desktop_links_supported, desktop_link_types, desktop_link_path
from .worker import Worker
@ -31,7 +31,7 @@ def uninstall_game(
logger.info('Removing registry entries...')
if platform.system() != "Window":
prefixes = config_helper.get_wine_prefixes()
prefixes = config.get_prefixes()
if platform.system() == "Darwin":
# TODO: add crossover support
pass
@ -65,10 +65,10 @@ def uninstall_game(
)
if not keep_config:
logger.info("Removing sections in config file")
config_helper.remove_section(rgame.app_name)
config_helper.remove_section(f"{rgame.app_name}.env")
config.remove_section(rgame.app_name)
config.remove_section(f"{rgame.app_name}.env")
config_helper.save_config()
config.save_config()
return status.success, status.message

View file

@ -3,50 +3,71 @@ import platform
import time
from configparser import ConfigParser
from logging import getLogger
from typing import Union, Iterable
from typing import Union, Iterable, Mapping, List
from PyQt5.QtCore import pyqtSignal, QObject, QRunnable
import rare.utils.wine as wine
from rare.lgndr.core import LegendaryCore
from rare.models.game import RareGame
from rare.models.pathspec import PathSpec
from rare.utils import runners, config_helper as config
from rare.utils.misc import path_size, format_size
from .worker import Worker
if platform.system() == "Windows":
# noinspection PyUnresolvedReferences
import winreg # pylint: disable=E0401
import winreg # pylint: disable=E0401
from legendary.lfs import windows_helpers
logger = getLogger("WineResolver")
class WineResolver(Worker):
class WinePathResolver(Worker):
class Signals(QObject):
result_ready = pyqtSignal(str)
result_ready = pyqtSignal(str, str)
def __init__(self, core: LegendaryCore, path: str, app_name: str):
super(WineResolver, self).__init__()
self.signals = WineResolver.Signals()
self.wine_env = wine.environ(core, app_name)
self.wine_exec = wine.wine(core, app_name)
self.path = PathSpec(core, app_name).cook(path)
def __init__(self, command: List[str], environ: Mapping, path: str):
super(WinePathResolver, self). __init__()
self.signals = WinePathResolver.Signals()
self.command = command
self.environ = environ
self.path = path
@staticmethod
def _resolve_unix_path(cmd, env, path: str) -> str:
logger.info("Resolving path '%s'", path)
wine_path = runners.resolve_path(cmd, env, path)
logger.debug("Resolved Wine path '%s'", path)
unix_path = runners.convert_to_unix_path(cmd, env, wine_path)
logger.debug("Resolved Unix path '%s'", unix_path)
return unix_path
def run_real(self):
if "WINEPREFIX" not in self.wine_env or not os.path.exists(self.wine_env["WINEPREFIX"]):
# pylint: disable=E1136
self.signals.result_ready[str].emit("")
return
if not os.path.exists(self.wine_exec):
# pylint: disable=E1136
self.signals.result_ready[str].emit("")
return
path = wine.resolve_path(self.wine_exec, self.wine_env, self.path)
path = self._resolve_unix_path(self.command, self.environ, self.path)
self.signals.result_ready.emit(path, "default")
return
class WineSavePathResolver(WinePathResolver):
def __init__(self, core: LegendaryCore, rgame: RareGame):
cmd = core.get_app_launch_command(rgame.app_name)
env = core.get_app_environment(rgame.app_name)
env = runners.get_environment(env, silent=True)
path = PathSpec(core, rgame.igame).resolve_egl_path_vars(rgame.raw_save_path)
if not (cmd and env and path):
raise RuntimeError(f"Cannot setup {type(self).__name__}, missing infomation")
super(WineSavePathResolver, self).__init__(cmd, env, path)
self.rgame = rgame
def run_real(self):
logger.info("Resolving save path for %s (%s)", self.rgame.app_title, self.rgame.app_name)
path = self._resolve_unix_path(self.command, self.environ, self.path)
# Clean wine output
real_path = wine.convert_to_unix_path(self.wine_exec, self.wine_env, path)
# pylint: disable=E1136
self.signals.result_ready[str].emit(real_path)
if os.path.exists(path):
self.rgame.save_path = path
self.signals.result_ready.emit(path, self.rgame.app_name)
return
@ -55,9 +76,7 @@ class OriginWineWorker(QRunnable):
super(OriginWineWorker, self).__init__()
self.__cache: dict[str, ConfigParser] = {}
self.core = core
if isinstance(games, RareGame):
games = [games]
self.games = games
self.games = [games] if isinstance(games, RareGame) else games
def run(self) -> None:
t = time.time()
@ -79,15 +98,19 @@ class OriginWineWorker(QRunnable):
if platform.system() == "Windows":
install_dir = windows_helpers.query_registry_value(winreg.HKEY_LOCAL_MACHINE, reg_path, reg_key)
else:
wine_env = wine.environ(self.core, rgame.app_name)
wine_exec = wine.wine(self.core, rgame.app_name)
command = self.core.get_app_launch_command(rgame.app_name)
environ = self.core.get_app_environment(rgame.app_name)
environ = runners.get_environment(environ, silent=True)
prefix = config.get_prefix(rgame.app_name)
if not prefix:
return
use_wine = False
if not use_wine:
# lk: this is the original way of getting the path by parsing "system.reg"
wine_prefix = wine.prefix(self.core, rgame.app_name)
reg = self.__cache.get(wine_prefix, None) or wine.read_registry("system.reg", wine_prefix)
self.__cache[wine_prefix] = reg
reg = self.__cache.get(prefix, None) or runners.read_registry("system.reg", prefix)
self.__cache[prefix] = reg
reg_path = reg_path.replace("SOFTWARE", "Software").replace("WOW6432Node", "Wow6432Node")
# lk: split and rejoin the registry path to avoid slash expansion
@ -96,11 +119,11 @@ class OriginWineWorker(QRunnable):
install_dir = reg.get(reg_path, f'"{reg_key}"', fallback=None)
else:
# lk: this is the alternative way of getting the path by using wine itself
install_dir = wine.query_reg_key(wine_exec, wine_env, f"HKLM\\{reg_path}", reg_key)
install_dir = runners.query_reg_key(command, environ, f"HKLM\\{reg_path}", reg_key)
if install_dir:
logger.debug("Found Wine install directory %s", install_dir)
install_dir = wine.convert_to_unix_path(wine_exec, wine_env, install_dir)
install_dir = runners.convert_to_unix_path(command, environ, install_dir)
if install_dir:
logger.debug("Found Unix install directory %s", install_dir)
else:

169
rare/shared/wrappers.py Normal file
View file

@ -0,0 +1,169 @@
import json
import os
from logging import getLogger
import shlex
from typing import List, Dict, Iterable
from rare.utils import config_helper as config
from PyQt5.QtCore import QSettings
from rare.lgndr.core import LegendaryCore
from rare.models.wrapper import Wrapper, WrapperType
from rare.utils.paths import config_dir
logger = getLogger("Wrappers")
class Wrappers:
def __init__(self):
self.__file = os.path.join(config_dir(), "wrappers.json")
self.__wrappers_dict = {}
try:
with open(self.__file) as f:
self.__wrappers_dict = json.load(f)
except FileNotFoundError:
logger.info("%s does not exist", self.__file)
except json.JSONDecodeError:
logger.warning("%s is corrupt", self.__file)
self.__wrappers: Dict[str, Wrapper] = {}
for wrap_id, wrapper in self.__wrappers_dict.get("wrappers", {}).items():
self.__wrappers.update({wrap_id: Wrapper.from_dict(wrapper)})
self.__applists: Dict[str, List[str]] = {}
for app_name, wrapper_list in self.__wrappers_dict.get("applists", {}).items():
self.__applists.update({app_name: wrapper_list})
def import_wrappers(self, core: LegendaryCore, settings: QSettings, app_names: List):
for app_name in app_names:
wrappers = self.get_game_wrapper_list(app_name)
if not wrappers and (commands := settings.value(f"{app_name}/wrapper", [], list)):
logger.info("Importing wrappers from Rare's config")
settings.remove(f"{app_name}/wrapper")
for command in commands:
wrapper = Wrapper(command=shlex.split(command))
wrappers.append(wrapper)
self.set_game_wrapper_list(app_name, wrappers)
logger.debug("Imported previous wrappers in %s Rare: %s", app_name, wrapper.name)
# NOTE: compatibility with Legendary
if not wrappers and (command := core.lgd.config.get(app_name, "wrapper", fallback="")):
logger.info("Importing wrappers from legendary's config")
# no qt wrapper, but legendary wrapper, to have backward compatibility
# pattern = re.compile(r'''((?:[^ "']|"[^"]*"|'[^']*')+)''')
# wrappers = pattern.split(command)[1::2]
wrapper = Wrapper(
command=shlex.split(command),
name="Imported from Legendary",
wtype=WrapperType.LEGENDARY_IMPORT
)
wrappers = [wrapper]
self.set_game_wrapper_list(app_name, wrappers)
logger.debug("Imported existing wrappers in %s legendary: %s", app_name, wrapper.name)
@property
def user_wrappers(self) -> Iterable[Wrapper]:
return filter(lambda w: w.is_editable, self.__wrappers.values())
# for wrap in self.__wrappers.values():
# if wrap.is_user_defined:
# yield wrap
def get_game_wrapper_string(self, app_name: str) -> str:
commands = [wrapper.command for wrapper in self.get_game_wrapper_list(app_name)]
return " ".join(commands)
def get_game_wrapper_list(self, app_name: str) -> List[Wrapper]:
_wrappers = []
for wrap_id in self.__applists.get(app_name, []):
if wrap := self.__wrappers.get(wrap_id, None):
_wrappers.append(wrap)
return _wrappers
def get_game_md5sum_list(self, app_name: str) -> List[str]:
return self.__applists.get(app_name, [])
def set_game_wrapper_list(self, app_name: str, wrappers: List[Wrapper]) -> None:
_wrappers = sorted(wrappers, key=lambda w: w.is_compat_tool)
for w in _wrappers:
if (md5sum := w.checksum) in self.__wrappers.keys():
if w != self.__wrappers[md5sum]:
logger.error(
"Non-unique md5sum for different wrappers %s, %s",
w.name,
self.__wrappers[md5sum].name,
)
if w.is_compat_tool:
self.__wrappers.update({md5sum: w})
else:
self.__wrappers.update({md5sum: w})
self.__applists[app_name] = [w.checksum for w in _wrappers]
self.__save_config(app_name)
self.__save_wrappers()
def __save_config(self, app_name: str):
command_string = self.get_game_wrapper_string(app_name)
if command_string:
config.add_option(app_name, "wrapper", command_string)
else:
config.remove_option(app_name, "wrapper")
config.save_config()
def __save_wrappers(self):
existing = {wrap_id for wrap_id in self.__wrappers.keys()}
in_use = {wrap_id for wrappers in self.__applists.values() for wrap_id in wrappers}
for redudant in existing.difference(in_use):
del self.__wrappers[redudant]
self.__wrappers_dict["wrappers"] = self.__wrappers
self.__wrappers_dict["applists"] = self.__applists
with open(os.path.join(self.__file), "w+") as f:
json.dump(self.__wrappers_dict, f, default=lambda o: vars(o), indent=2)
if __name__ == "__main__":
from pprint import pprint
from argparse import Namespace
from rare.utils.runners import proton
global config_dir
config_dir = os.getcwd
global config
config = Namespace()
config.add_option = lambda x, y, z: print(x, y, z)
config.remove_option = lambda x, y: print(x, y)
config.save_config = lambda: print()
wr = Wrappers()
w1 = Wrapper(command=["/usr/bin/w1"], wtype=WrapperType.NONE)
w2 = Wrapper(command=["/usr/bin/w2"], wtype=WrapperType.COMPAT_TOOL)
w3 = Wrapper(command=["/usr/bin/w3"], wtype=WrapperType.USER_DEFINED)
w4 = Wrapper(command=["/usr/bin/w4"], wtype=WrapperType.USER_DEFINED)
wr.set_game_wrapper_list("testgame", [w1, w2, w3, w4])
w5 = Wrapper(command=["/usr/bin/w5"], wtype=WrapperType.COMPAT_TOOL)
wr.set_game_wrapper_list("testgame2", [w2, w1, w5])
w6 = Wrapper(command=["/usr/bin/w 6", "-w", "-t"], wtype=WrapperType.USER_DEFINED)
wr.set_game_wrapper_list("testgame", [w1, w2, w3, w6])
w7 = Wrapper(command=["/usr/bin/w2"], wtype=WrapperType.COMPAT_TOOL)
wrs = wr.get_game_wrapper_list("testgame")
wrs.remove(w7)
wr.set_game_wrapper_list("testgame", wrs)
game_wrappers = wr.get_game_wrapper_list("testgame")
pprint(game_wrappers)
game_wrappers = wr.get_game_wrapper_list("testgame2")
pprint(game_wrappers)
for i, tool in enumerate(proton.find_tools()):
wt = Wrapper(command=tool.command(), name=tool.name, wtype=WrapperType.COMPAT_TOOL)
wr.set_game_wrapper_list(f"compat_game_{i}", [wt])
print(wt.command)
for wrp in wr.user_wrappers:
pprint(wrp)

View file

@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/game_settings.ui'
#
# Created by: PyQt5 UI code generator 5.15.10
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_GameSettings(object):
def setupUi(self, GameSettings):
GameSettings.setObjectName("GameSettings")
GameSettings.resize(393, 202)
self.main_layout = QtWidgets.QVBoxLayout(GameSettings)
self.main_layout.setObjectName("main_layout")
self.launch_settings_group = QtWidgets.QGroupBox(GameSettings)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.launch_settings_group.sizePolicy().hasHeightForWidth())
self.launch_settings_group.setSizePolicy(sizePolicy)
self.launch_settings_group.setObjectName("launch_settings_group")
self.launch_layout = QtWidgets.QFormLayout(self.launch_settings_group)
self.launch_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.launch_layout.setObjectName("launch_layout")
self.skip_update_label = QtWidgets.QLabel(self.launch_settings_group)
self.skip_update_label.setObjectName("skip_update_label")
self.launch_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.skip_update_label)
self.skip_update_combo = QtWidgets.QComboBox(self.launch_settings_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.skip_update_combo.sizePolicy().hasHeightForWidth())
self.skip_update_combo.setSizePolicy(sizePolicy)
self.skip_update_combo.setObjectName("skip_update_combo")
self.skip_update_combo.addItem("")
self.skip_update_combo.addItem("")
self.skip_update_combo.addItem("")
self.launch_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.skip_update_combo)
self.offline_label = QtWidgets.QLabel(self.launch_settings_group)
self.offline_label.setObjectName("offline_label")
self.launch_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.offline_label)
self.offline_combo = QtWidgets.QComboBox(self.launch_settings_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.offline_combo.sizePolicy().hasHeightForWidth())
self.offline_combo.setSizePolicy(sizePolicy)
self.offline_combo.setObjectName("offline_combo")
self.offline_combo.addItem("")
self.offline_combo.addItem("")
self.offline_combo.addItem("")
self.launch_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.offline_combo)
self.launch_params_label = QtWidgets.QLabel(self.launch_settings_group)
self.launch_params_label.setObjectName("launch_params_label")
self.launch_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.launch_params_label)
self.launch_params_edit = QtWidgets.QLineEdit(self.launch_settings_group)
self.launch_params_edit.setObjectName("launch_params_edit")
self.launch_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.launch_params_edit)
self.pre_launch_label = QtWidgets.QLabel(self.launch_settings_group)
self.pre_launch_label.setObjectName("pre_launch_label")
self.launch_layout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.pre_launch_label)
self.override_exe_label = QtWidgets.QLabel(self.launch_settings_group)
self.override_exe_label.setObjectName("override_exe_label")
self.launch_layout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.override_exe_label)
self.wrapper_label = QtWidgets.QLabel(self.launch_settings_group)
self.wrapper_label.setObjectName("wrapper_label")
self.launch_layout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.wrapper_label)
self.main_layout.addWidget(self.launch_settings_group)
self.retranslateUi(GameSettings)
def retranslateUi(self, GameSettings):
_translate = QtCore.QCoreApplication.translate
GameSettings.setWindowTitle(_translate("GameSettings", "GameSettings"))
self.launch_settings_group.setTitle(_translate("GameSettings", "Launch Settings"))
self.skip_update_label.setText(_translate("GameSettings", "Skip update check"))
self.skip_update_combo.setItemText(0, _translate("GameSettings", "Default"))
self.skip_update_combo.setItemText(1, _translate("GameSettings", "Yes"))
self.skip_update_combo.setItemText(2, _translate("GameSettings", "No"))
self.offline_label.setText(_translate("GameSettings", "Offline mode"))
self.offline_combo.setItemText(0, _translate("GameSettings", "Default"))
self.offline_combo.setItemText(1, _translate("GameSettings", "Yes"))
self.offline_combo.setItemText(2, _translate("GameSettings", "No"))
self.launch_params_label.setText(_translate("GameSettings", "Launch parameters"))
self.launch_params_edit.setPlaceholderText(_translate("GameSettings", "parameters"))
self.pre_launch_label.setText(_translate("GameSettings", "Pre-launch command"))
self.override_exe_label.setText(_translate("GameSettings", "Override executable"))
self.wrapper_label.setText(_translate("GameSettings", "Wrappers"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
GameSettings = QtWidgets.QWidget()
ui = Ui_GameSettings()
ui.setupUi(GameSettings)
GameSettings.show()
sys.exit(app.exec_())

View file

@ -6,14 +6,14 @@
<rect>
<x>0</x>
<y>0</y>
<width>483</width>
<height>154</height>
<width>393</width>
<height>202</height>
</rect>
</property>
<property name="windowTitle">
<string>GameSettings</string>
</property>
<layout class="QVBoxLayout" name="game_settings_layout">
<layout class="QVBoxLayout" name="main_layout">
<item>
<widget class="QGroupBox" name="launch_settings_group">
<property name="sizePolicy">
@ -25,7 +25,7 @@
<property name="title">
<string>Launch Settings</string>
</property>
<layout class="QFormLayout" name="launch_settings_layout">
<layout class="QFormLayout" name="launch_layout">
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
@ -37,7 +37,7 @@
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="skip_update">
<widget class="QComboBox" name="skip_update_combo">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
@ -69,7 +69,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="offline">
<widget class="QComboBox" name="offline_combo">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
@ -101,12 +101,33 @@
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="launch_params">
<widget class="QLineEdit" name="launch_params_edit">
<property name="placeholderText">
<string>parameters</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="pre_launch_label">
<property name="text">
<string>Pre-launch command</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="override_exe_label">
<property name="text">
<string>Override executable</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="wrapper_label">
<property name="text">
<string>Wrappers</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View file

@ -1,100 +0,0 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/game_settings.ui'
#
# Created by: PyQt5 UI code generator 5.15.9
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_GameSettings(object):
def setupUi(self, GameSettings):
GameSettings.setObjectName("GameSettings")
GameSettings.resize(505, 261)
self.game_settings_layout = QtWidgets.QVBoxLayout(GameSettings)
self.game_settings_layout.setObjectName("game_settings_layout")
self.launch_settings_group = QtWidgets.QGroupBox(GameSettings)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.launch_settings_group.sizePolicy().hasHeightForWidth())
self.launch_settings_group.setSizePolicy(sizePolicy)
self.launch_settings_group.setObjectName("launch_settings_group")
self.launch_settings_layout = QtWidgets.QFormLayout(self.launch_settings_group)
self.launch_settings_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.launch_settings_layout.setObjectName("launch_settings_layout")
self.skip_update_label = QtWidgets.QLabel(self.launch_settings_group)
self.skip_update_label.setObjectName("skip_update_label")
self.launch_settings_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.skip_update_label)
self.skip_update = QtWidgets.QComboBox(self.launch_settings_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.skip_update.sizePolicy().hasHeightForWidth())
self.skip_update.setSizePolicy(sizePolicy)
self.skip_update.setObjectName("skip_update")
self.skip_update.addItem("")
self.skip_update.addItem("")
self.skip_update.addItem("")
self.launch_settings_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.skip_update)
self.offline_label = QtWidgets.QLabel(self.launch_settings_group)
self.offline_label.setObjectName("offline_label")
self.launch_settings_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.offline_label)
self.offline = QtWidgets.QComboBox(self.launch_settings_group)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.offline.sizePolicy().hasHeightForWidth())
self.offline.setSizePolicy(sizePolicy)
self.offline.setObjectName("offline")
self.offline.addItem("")
self.offline.addItem("")
self.offline.addItem("")
self.launch_settings_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.offline)
self.launch_params_label = QtWidgets.QLabel(self.launch_settings_group)
self.launch_params_label.setObjectName("launch_params_label")
self.launch_settings_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.launch_params_label)
self.launch_params = QtWidgets.QLineEdit(self.launch_settings_group)
self.launch_params.setObjectName("launch_params")
self.launch_settings_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.launch_params)
self.game_settings_layout.addWidget(self.launch_settings_group)
self.proton_layout = QtWidgets.QVBoxLayout()
self.proton_layout.setObjectName("proton_layout")
self.game_settings_layout.addLayout(self.proton_layout)
self.linux_settings_widget = QtWidgets.QWidget(GameSettings)
self.linux_settings_widget.setObjectName("linux_settings_widget")
self.linux_settings_layout = QtWidgets.QVBoxLayout(self.linux_settings_widget)
self.linux_settings_layout.setContentsMargins(0, 0, 0, 0)
self.linux_settings_layout.setObjectName("linux_settings_layout")
self.game_settings_layout.addWidget(self.linux_settings_widget)
self.retranslateUi(GameSettings)
def retranslateUi(self, GameSettings):
_translate = QtCore.QCoreApplication.translate
GameSettings.setWindowTitle(_translate("GameSettings", "GameSettings"))
self.launch_settings_group.setTitle(_translate("GameSettings", "Launch Settings"))
self.skip_update_label.setText(_translate("GameSettings", "Skip update check"))
self.skip_update.setItemText(0, _translate("GameSettings", "Default"))
self.skip_update.setItemText(1, _translate("GameSettings", "Yes"))
self.skip_update.setItemText(2, _translate("GameSettings", "No"))
self.offline_label.setText(_translate("GameSettings", "Offline mode"))
self.offline.setItemText(0, _translate("GameSettings", "Default"))
self.offline.setItemText(1, _translate("GameSettings", "Yes"))
self.offline.setItemText(2, _translate("GameSettings", "No"))
self.launch_params_label.setText(_translate("GameSettings", "Launch parameters"))
self.launch_params.setPlaceholderText(_translate("GameSettings", "parameters"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
GameSettings = QtWidgets.QWidget()
ui = Ui_GameSettings()
ui.setupUi(GameSettings)
GameSettings.show()
sys.exit(app.exec_())

View file

@ -1,63 +0,0 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/linux.ui'
#
# Created by: PyQt5 UI code generator 5.15.9
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_LinuxSettings(object):
def setupUi(self, LinuxSettings):
LinuxSettings.setObjectName("LinuxSettings")
LinuxSettings.resize(394, 84)
self.linux_layout = QtWidgets.QVBoxLayout(LinuxSettings)
self.linux_layout.setObjectName("linux_layout")
self.wine_groupbox = QtWidgets.QGroupBox(LinuxSettings)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.wine_groupbox.sizePolicy().hasHeightForWidth())
self.wine_groupbox.setSizePolicy(sizePolicy)
self.wine_groupbox.setObjectName("wine_groupbox")
self.wine_layout = QtWidgets.QFormLayout(self.wine_groupbox)
self.wine_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.wine_layout.setObjectName("wine_layout")
self.prefix_label = QtWidgets.QLabel(self.wine_groupbox)
self.prefix_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.prefix_label.setObjectName("prefix_label")
self.wine_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.prefix_label)
self.prefix_layout = QtWidgets.QVBoxLayout()
self.prefix_layout.setObjectName("prefix_layout")
self.wine_layout.setLayout(0, QtWidgets.QFormLayout.FieldRole, self.prefix_layout)
self.exec_label = QtWidgets.QLabel(self.wine_groupbox)
self.exec_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.exec_label.setObjectName("exec_label")
self.wine_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.exec_label)
self.exec_layout = QtWidgets.QVBoxLayout()
self.exec_layout.setObjectName("exec_layout")
self.wine_layout.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.exec_layout)
self.linux_layout.addWidget(self.wine_groupbox)
self.retranslateUi(LinuxSettings)
def retranslateUi(self, LinuxSettings):
_translate = QtCore.QCoreApplication.translate
LinuxSettings.setWindowTitle(_translate("LinuxSettings", "LinuxSettings"))
self.wine_groupbox.setTitle(_translate("LinuxSettings", "Wine Settings"))
self.prefix_label.setText(_translate("LinuxSettings", "Prefix"))
self.exec_label.setText(_translate("LinuxSettings", "Executable"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
LinuxSettings = QtWidgets.QWidget()
ui = Ui_LinuxSettings()
ui.setupUi(LinuxSettings)
LinuxSettings.show()
sys.exit(app.exec_())

View file

@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LinuxSettings</class>
<widget class="QWidget" name="LinuxSettings">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>394</width>
<height>84</height>
</rect>
</property>
<property name="windowTitle">
<string>LinuxSettings</string>
</property>
<layout class="QVBoxLayout" name="linux_layout">
<item>
<widget class="QGroupBox" name="wine_groupbox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Wine Settings</string>
</property>
<layout class="QFormLayout" name="wine_layout">
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<item row="0" column="0">
<widget class="QLabel" name="prefix_label">
<property name="text">
<string>Prefix</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QVBoxLayout" name="prefix_layout"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="exec_label">
<property name="text">
<string>Executable</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QVBoxLayout" name="exec_layout"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/proton.ui'
#
# Created by: PyQt5 UI code generator 5.15.6
# Created by: PyQt5 UI code generator 5.15.10
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@ -14,14 +14,14 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_ProtonSettings(object):
def setupUi(self, ProtonSettings):
ProtonSettings.setObjectName("ProtonSettings")
ProtonSettings.resize(190, 86)
ProtonSettings.resize(180, 84)
ProtonSettings.setWindowTitle("ProtonSettings")
self.proton_settings_layout = QtWidgets.QFormLayout(ProtonSettings)
self.proton_settings_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.proton_settings_layout.setObjectName("proton_settings_layout")
self.main_layout = QtWidgets.QFormLayout(ProtonSettings)
self.main_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.main_layout.setObjectName("main_layout")
self.proton_wrapper_label = QtWidgets.QLabel(ProtonSettings)
self.proton_wrapper_label.setObjectName("proton_wrapper_label")
self.proton_settings_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.proton_wrapper_label)
self.main_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.proton_wrapper_label)
self.proton_combo = QtWidgets.QComboBox(ProtonSettings)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
@ -30,13 +30,10 @@ class Ui_ProtonSettings(object):
self.proton_combo.setSizePolicy(sizePolicy)
self.proton_combo.setObjectName("proton_combo")
self.proton_combo.addItem("")
self.proton_settings_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.proton_combo)
self.main_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.proton_combo)
self.proton_prefix_label = QtWidgets.QLabel(ProtonSettings)
self.proton_prefix_label.setObjectName("proton_prefix_label")
self.proton_settings_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.proton_prefix_label)
self.prefix_layout = QtWidgets.QHBoxLayout()
self.prefix_layout.setObjectName("prefix_layout")
self.proton_settings_layout.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.prefix_layout)
self.main_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.proton_prefix_label)
self.retranslateUi(ProtonSettings)

View file

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>190</width>
<height>86</height>
<width>180</width>
<height>84</height>
</rect>
</property>
<property name="windowTitle">
@ -16,7 +16,7 @@
<property name="title">
<string>Proton Settings</string>
</property>
<layout class="QFormLayout" name="proton_settings_layout">
<layout class="QFormLayout" name="main_layout">
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
@ -49,9 +49,6 @@
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="prefix_layout"/>
</item>
</layout>
</widget>
<resources/>

View file

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/wine.ui'
#
# Created by: PyQt5 UI code generator 5.15.10
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_WineSettings(object):
def setupUi(self, WineSettings):
WineSettings.setObjectName("WineSettings")
WineSettings.resize(104, 72)
WineSettings.setWindowTitle("WIneSettings")
self.main_layout = QtWidgets.QFormLayout(WineSettings)
self.main_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.main_layout.setObjectName("main_layout")
self.prefix_label = QtWidgets.QLabel(WineSettings)
self.prefix_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.prefix_label.setObjectName("prefix_label")
self.main_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.prefix_label)
self.exec_label = QtWidgets.QLabel(WineSettings)
self.exec_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
self.exec_label.setObjectName("exec_label")
self.main_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.exec_label)
self.retranslateUi(WineSettings)
def retranslateUi(self, WineSettings):
_translate = QtCore.QCoreApplication.translate
WineSettings.setTitle(_translate("WineSettings", "Wine Settings"))
self.prefix_label.setText(_translate("WineSettings", "Prefix"))
self.exec_label.setText(_translate("WineSettings", "Executable"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
WineSettings = QtWidgets.QGroupBox()
ui = Ui_WineSettings()
ui.setupUi(WineSettings)
WineSettings.show()
sys.exit(app.exec_())

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>WineSettings</class>
<widget class="QGroupBox" name="WineSettings">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>104</width>
<height>72</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">WIneSettings</string>
</property>
<property name="title">
<string>Wine Settings</string>
</property>
<layout class="QFormLayout" name="main_layout">
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<item row="0" column="0">
<widget class="QLabel" name="prefix_label">
<property name="text">
<string>Prefix</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="exec_label">
<property name="text">
<string>Executable</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -7,6 +7,7 @@ from PyQt5.QtCore import QObject, QSettings
from pypresence import Presence
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
from rare.models.options import options
client_id = "830732538225360908"
logger = getLogger("RPC")
@ -21,7 +22,7 @@ class DiscordRPC(QObject):
self.signals = GlobalSignalsSingleton()
self.settings = QSettings()
if self.settings.value("rpc_enable", 0, int) == 1: # show always
if self.settings.value(*options.rpc_enable) == 1: # show always
self.state = 2
self.set_discord_rpc()
@ -32,7 +33,7 @@ class DiscordRPC(QObject):
self.set_discord_rpc(app_name)
def changed_settings(self, game_running: list = None):
value = self.settings.value("rpc_enable", 0, int)
value = self.settings.value(*options.rpc_enable)
if value == 2:
self.remove_rpc()
return
@ -44,7 +45,7 @@ class DiscordRPC(QObject):
self.set_discord_rpc(game_running[0])
def remove_rpc(self):
if self.settings.value("rpc_enable", 0, int) != 1:
if self.settings.value(*options.rpc_enable) != 1:
if not self.RPC:
return
try:
@ -85,8 +86,8 @@ class DiscordRPC(QObject):
self.update_rpc(app_name)
def update_rpc(self, app_name=None):
if self.settings.value("rpc_enable", 0, int) == 2 or (
not app_name and self.settings.value("rpc_enable", 0, int) == 0
if self.settings.value(*options.rpc_enable) == 2 or (
not app_name and self.settings.value(*options.rpc_enable) == 0
):
self.remove_rpc()
return
@ -96,17 +97,17 @@ class DiscordRPC(QObject):
large_image="logo", details="https://github.com/RareDevs/Rare"
)
return
if self.settings.value("rpc_name", True, bool):
if self.settings.value(*options.rpc_name):
try:
title = self.core.get_installed_game(app_name).title
except AttributeError:
logger.error(f"Could not get title of game: {app_name}")
title = app_name
start = None
if self.settings.value("rpc_time", True, bool):
if self.settings.value(*options.rpc_time):
start = str(time.time()).split(".")[0]
os = None
if self.settings.value("rpc_os", True, bool):
if self.settings.value(*options.rpc_os):
os = f"via Rare on {platform.system()}"
self.RPC.update(

View file

@ -1,31 +1,32 @@
import os
import shutil
import subprocess
from configparser import ConfigParser
from logging import getLogger
from typing import Mapping, Dict, List, Tuple
from typing import Mapping, Dict, List, Tuple, Optional
from rare.lgndr.core import LegendaryCore
from rare.utils import config_helper as config
from . import proton
from . import wine
logger = getLogger("Wine")
logger = getLogger("Runners")
# this is a copied function from legendary.utils.wine_helpers, but registry file can be specified
def read_registry(registry: str, wine_pfx: str) -> ConfigParser:
def read_registry(registry: str, prefix: str) -> ConfigParser:
accepted = ["system.reg", "user.reg"]
if registry not in accepted:
raise RuntimeError(f'Unknown target "{registry}" not in {accepted}')
reg = ConfigParser(comment_prefixes=(';', '#', '/', 'WINE'), allow_no_value=True,
strict=False)
reg.optionxform = str
reg.read(os.path.join(wine_pfx, 'system.reg'))
reg.read(os.path.join(prefix, 'system.reg'))
return reg
def execute(command: List, wine_env: Mapping) -> Tuple[str, str]:
def execute(command: List[str], environment: Mapping) -> Tuple[str, str]:
if os.environ.get("container") == "flatpak":
flatpak_command = ["flatpak-spawn", "--host"]
for name, value in wine_env.items():
for name, value in environment.items():
flatpak_command.append(f"--env={name}={value}")
command = flatpak_command + command
try:
@ -35,7 +36,7 @@ def execute(command: List, wine_env: Mapping) -> Tuple[str, str]:
stderr=subprocess.PIPE,
# Use the current environment if we are in flatpak or our own if we are on host
# In flatpak our environment is passed through `flatpak-spawn` arguments
env=os.environ.copy() if os.environ.get("container") == "flatpak" else wine_env,
env=os.environ.copy() if os.environ.get("container") == "flatpak" else environment,
shell=False,
text=True,
)
@ -45,13 +46,13 @@ def execute(command: List, wine_env: Mapping) -> Tuple[str, str]:
return res
def resolve_path(wine_exec: str, wine_env: Mapping, path: str) -> str:
def resolve_path(command: List[str], environment: Mapping, path: str) -> str:
path = path.strip().replace("/", "\\")
# lk: if path does not exist form
cmd = [wine_exec, "cmd", "/c", "echo", path]
cmd = command + ["cmd", "/c", "echo", path]
# lk: if path exists and needs a case-sensitive interpretation form
# cmd = [wine_cmd, 'cmd', '/c', f'cd {path} & cd']
out, err = execute(cmd, wine_env)
out, err = execute(cmd, environment)
out, err = out.strip(), err.strip()
if not out:
logger.error("Failed to resolve wine path due to \"%s\"", err)
@ -63,9 +64,9 @@ def query_reg_path(wine_exec: str, wine_env: Mapping, reg_path: str):
raise NotImplementedError
def query_reg_key(wine_exec: str, wine_env: Mapping, reg_path: str, reg_key) -> str:
cmd = [wine_exec, "reg", "query", reg_path, "/v", reg_key]
out, err = execute(cmd, wine_env)
def query_reg_key(command: List[str], environment: Mapping, reg_path: str, reg_key) -> str:
cmd = command + ["reg", "query", reg_path, "/v", reg_key]
out, err = execute(cmd, environment)
out, err = out.strip(), err.strip()
if not out:
logger.error("Failed to query registry key due to \"%s\"", err)
@ -83,40 +84,23 @@ def convert_to_windows_path(wine_exec: str, wine_env: Mapping, path: str) -> str
raise NotImplementedError
def convert_to_unix_path(wine_exec: str, wine_env: Mapping, path: str) -> str:
def convert_to_unix_path(command: List[str], environment: Mapping, path: str) -> str:
path = path.strip().strip('"')
cmd = [wine_exec, "winepath.exe", "-u", path]
out, err = execute(cmd, wine_env)
cmd = command + ["winepath.exe", "-u", path]
out, err = execute(cmd, environment)
out, err = out.strip(), err.strip()
if not out:
logger.error("Failed to convert to unix path due to \"%s\"", err)
return os.path.realpath(out) if (out := out.strip()) else out
def wine(core: LegendaryCore, app_name: str = "default") -> str:
_wine = core.lgd.config.get(
app_name, "wine_executable", fallback=core.lgd.config.get(
"default", "wine_executable", fallback=shutil.which("wine")
)
)
return _wine
def environ(core: LegendaryCore, app_name: str = "default") -> Dict:
# Get a clean environment if we are in flatpak, this environment will be pass
def get_environment(app_environment: Dict, silent: bool = True) -> Dict:
# Get a clean environment if we are in flatpak, this environment will be passed
# to `flatpak-spawn`, otherwise use the system's.
_environ = {} if os.environ.get("container") == "flatpak" else os.environ.copy()
_environ.update(core.get_app_environment(app_name))
_environ["WINEDEBUG"] = "-all"
_environ["WINEDLLOVERRIDES"] = "winemenubuilder=d;mscoree=d;mshtml=d;"
_environ["DISPLAY"] = ""
_environ.update(app_environment)
if silent:
_environ["WINEDEBUG"] = "-all"
_environ["WINEDLLOVERRIDES"] = "winemenubuilder=d;mscoree=d;mshtml=d;"
_environ["DISPLAY"] = ""
return _environ
def prefix(core: LegendaryCore, app_name: str = "default") -> str:
_prefix = core.lgd.config.get(
app_name, "wine_prefix", fallback=core.lgd.config.get(
"default", "wine_prefix", fallback=os.path.expanduser("~/.wine")
)
)
return _prefix if os.path.isdir(_prefix) else ""

View file

@ -1,6 +1,8 @@
import platform as pf
import os
import shlex
from dataclasses import dataclass
from hashlib import md5
from logging import getLogger
from typing import Optional, Union, List, Dict
@ -28,11 +30,22 @@ def find_libraries(steam_path: str) -> List[str]:
return libraries
# Notes:
# Anything older than 'Proton 5.13' doesn't have the 'require_tool_appid' attribute.
# Anything older than 'Proton 7.0' doesn't have the 'compatmanager_layer_name' attribute.
# In addition to that, the 'Steam Linux Runtime 1.0 (scout)' runtime lists the
# 'Steam Linux Runtime 2.0 (soldier)' runtime as a dependency and is probably what was
# being used for any version before 5.13.
#
# As a result the following implementation will list versions from 7.0 onwards which honestly
# is a good trade-off for the amount of complexity supporting everything would ensue.
@dataclass
class SteamBase:
steam_path: str
tool_path: str
toolmanifest: dict
toolmanifest: Dict
def __eq__(self, other):
return self.tool_path == other.tool_path
@ -40,23 +53,36 @@ class SteamBase:
def __hash__(self):
return hash(self.tool_path)
def commandline(self):
cmd = "".join([f"'{self.tool_path}'", self.toolmanifest["manifest"]["commandline"]])
cmd = os.path.normpath(cmd)
@property
def required_tool(self) -> Optional[str]:
return self.toolmanifest["manifest"].get("require_tool_appid", None)
def command(self, setup: bool = False) -> List[str]:
tool_path = os.path.normpath(self.tool_path)
cmd = "".join([shlex.quote(tool_path), self.toolmanifest["manifest"]["commandline"]])
# NOTE: "waitforexitandrun" seems to be the verb used in by steam to execute stuff
cmd = cmd.replace("%verb%", "waitforexitandrun")
return cmd
# `run` is used when setting up the environment, so use that if we are setting up the prefix.
verb = "run" if setup else "waitforexitandrun"
cmd = cmd.replace("%verb%", verb)
return shlex.split(cmd)
@property
def checksum(self) -> str:
command = " ".join(shlex.quote(part) for part in self.command(setup=False))
return md5(command.encode("utf-8")).hexdigest()
@dataclass
class SteamRuntime(SteamBase):
steam_library: str
appmanifest: dict
appmanifest: Dict
def name(self):
@property
def name(self) -> str:
return self.appmanifest["AppState"]["name"]
def appid(self):
@property
def appid(self) -> str:
return self.appmanifest["AppState"]["appid"]
@ -64,33 +90,36 @@ class SteamRuntime(SteamBase):
class ProtonTool(SteamRuntime):
runtime: SteamRuntime = None
def __bool__(self):
if appid := self.toolmanifest.get("require_tool_appid", False):
return self.runtime is not None and self.runtime.appid() == appid
def __bool__(self) -> bool:
if appid := self.required_tool:
return self.runtime is not None and self.runtime.appid == appid
return True
def commandline(self):
runtime_cmd = self.runtime.commandline()
cmd = super().commandline()
return " ".join([runtime_cmd, cmd])
def command(self, setup: bool = False) -> List[str]:
cmd = self.runtime.command(setup)
cmd.extend(super().command(setup))
return cmd
@dataclass
class CompatibilityTool(SteamBase):
compatibilitytool: dict
compatibilitytool: Dict
runtime: SteamRuntime = None
def __bool__(self):
if appid := self.toolmanifest.get("require_tool_appid", False):
return self.runtime is not None and self.runtime.appid() == appid
def __bool__(self) -> bool:
if appid := self.required_tool:
return self.runtime is not None and self.runtime.appid == appid
return True
def name(self):
@property
def name(self) -> str:
name, data = list(self.compatibilitytool["compatibilitytools"]["compat_tools"].items())[0]
return data["display_name"]
def commandline(self):
runtime_cmd = self.runtime.commandline() if self.runtime is not None else ""
cmd = super().commandline()
return " ".join([runtime_cmd, cmd])
def command(self, setup: bool = False) -> List[str]:
cmd = self.runtime.command(setup) if self.runtime is not None else []
cmd.extend(super().command(setup))
return cmd
def find_appmanifests(library: str) -> List[dict]:
@ -187,16 +216,18 @@ def find_runtimes(steam_path: str, library: str) -> Dict[str, SteamRuntime]:
def find_runtime(
tool: Union[ProtonTool, CompatibilityTool], runtimes: Dict[str, SteamRuntime]
) -> Optional[SteamRuntime]:
required_tool = tool.toolmanifest["manifest"].get("require_tool_appid")
required_tool = tool.required_tool
if required_tool is None:
return None
return runtimes[required_tool]
return runtimes.get(required_tool, None)
def get_steam_environment(tool: Optional[Union[ProtonTool, CompatibilityTool]], app_name: str = None) -> Dict:
environ = {}
def get_steam_environment(
tool: Optional[Union[ProtonTool, CompatibilityTool]] = None, compat_path: Optional[str] = None
) -> Dict:
# If the tool is unset, return all affected env variable names
# IMPORTANT: keep this in sync with the code below
environ = {"STEAM_COMPAT_DATA_PATH": compat_path if compat_path else ""}
if tool is None:
environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = ""
environ["STEAM_COMPAT_LIBRARY_PATHS"] = ""
@ -221,7 +252,8 @@ def find_tools() -> List[Union[ProtonTool, CompatibilityTool]]:
steam_path = find_steam()
logger.debug("Using Steam install in %s", steam_path)
steam_libraries = find_libraries(steam_path)
logger.debug("Searching for tools in libraries %s", steam_libraries)
logger.debug("Searching for tools in libraries:")
logger.debug("%s", steam_libraries)
runtimes = {}
for library in steam_libraries:
@ -236,6 +268,8 @@ def find_tools() -> List[Union[ProtonTool, CompatibilityTool]]:
runtime = find_runtime(tool, runtimes)
tool.runtime = runtime
tools = list(filter(lambda t: bool(t), tools))
return tools
@ -247,7 +281,9 @@ if __name__ == "__main__":
for tool in _tools:
print(get_steam_environment(tool))
print(tool.name(), tool.commandline())
print(tool.name)
print(tool.command())
print(" ".join(tool.command()))
def find_proton_combos():

View file

@ -0,0 +1,88 @@
import os
from dataclasses import dataclass
from logging import getLogger
from typing import Dict, Tuple, List, Optional
logger = getLogger("Wine")
lutris_runtime_paths = [
os.path.expanduser("~/.local/share/lutris")
]
__lutris_runtime: str = None
__lutris_wine: str = None
def find_lutris() -> Tuple[str, str]:
global __lutris_runtime, __lutris_wine
for path in lutris_runtime_paths:
runtime_path = os.path.join(path, "runtime")
wine_path = os.path.join(path, "runners", "wine")
if os.path.isdir(path) and os.path.isdir(runtime_path) and os.path.isdir(wine_path):
__lutris_runtime, __lutris_wine = runtime_path, wine_path
return runtime_path, wine_path
@dataclass
class WineRuntime:
name: str
path: str
environ: Dict
@dataclass
class WineRunner:
name: str
path: str
environ: Dict
runtime: Optional[WineRuntime] = None
def find_lutris_wines(runtime_path: str = None, wine_path: str = None) -> List[WineRunner]:
runners = []
if not runtime_path and not wine_path:
return runners
def __get_lib_path(executable: str, basename: str = "") -> str:
path = os.path.dirname(os.path.dirname(executable))
lib32 = os.path.realpath(os.path.join(path, "lib32", basename))
lib64 = os.path.realpath(os.path.join(path, "lib64", basename))
lib = os.path.realpath(os.path.join(path, "lib", basename))
if lib32 == lib or not os.path.exists(lib32):
ldpath = ":".join([lib64, lib])
elif lib64 == lib or not os.path.exists(lib64):
ldpath = ":".join([lib, lib32])
else:
ldpath = lib if os.path.exists(lib) else lib64
return ldpath
def get_wine_environment(executable: str = None, prefix: str = None) -> Dict:
# If the tool is unset, return all affected env variable names
# IMPORTANT: keep this in sync with the code below
environ = {"WINEPREFIX": prefix if prefix is not None else ""}
if executable is None:
environ["WINEDLLPATH"] = ""
environ["LD_LIBRARY_PATH"] = ""
else:
winedllpath = __get_lib_path(executable, "wine")
environ["WINEDLLPATH"] = winedllpath
librarypath = __get_lib_path(executable, "")
environ["LD_LIBRARY_PATH"] = librarypath
return environ
if __name__ == "__main__":
from pprint import pprint
pprint(get_wine_environment(
"/opt/wine-ge-custom/bin/wine", None))
pprint(get_wine_environment(
"/usr/bin/wine", None))
pprint(get_wine_environment(
"/usr/share/steam/compatitiblitytools.d/dist/bin/wine", None))
pprint(get_wine_environment(
os.path.expanduser("~/.local/share/Steam/compatibilitytools.d/GE-Proton8-14/files/bin/wine"), None))
pprint(get_wine_environment(
os.path.expanduser("~/.local/share/lutris/runners/wine/lutris-GE-Proton8-14-x86_64/bin/wine"), None))

View file

@ -1,6 +1,7 @@
import difflib
import os
from datetime import datetime
from enum import StrEnum, Enum
from typing import Tuple
import orjson
@ -18,6 +19,31 @@ __grades_json = None
__active_download = False
class ProtondbRatings(int, Enum):
# internal
PENDING = ("pending", -2)
FAIL = ("fail", -1)
# protondb
NA = ("na", 0)
BORKED = ("borked", 1)
BRONZE = ("bronze", 2)
SILVER = ("silver", 3)
GOLD = ("gold", 4)
PLATINUM = ("platinum", 5)
def __new__(cls, name: str, value: int):
obj = int.__new__(cls, value)
obj._value_ = value
obj._name_ = name
return obj
def __str__(self):
return self._name_
def __int__(self):
return self._value_
def get_rating(core: LegendaryCore, app_name: str) -> Tuple[int, str]:
game = core.get_game(app_name)
try:
@ -31,7 +57,7 @@ def get_rating(core: LegendaryCore, app_name: str) -> Tuple[int, str]:
return steam_id, grade
# you should iniciate the module with the game's steam code
# you should initiate the module with the game's steam code
def get_grade(steam_code):
if steam_code == 0:
return "fail"