import os import platform from logging import getLogger from pathlib import Path from typing import Tuple from PyQt5.QtCore import QSettings, QThreadPool, Qt, QSize from PyQt5.QtWidgets import ( QWidget, QFileDialog, QMessageBox, QLabel, QPushButton, QSizePolicy, QHBoxLayout, ) from legendary.core import LegendaryCore from legendary.models.game import InstalledGame, Game from rare.components.tabs.settings.linux import LinuxSettings from rare.components.tabs.settings.widgets.wrapper import WrapperSettings from rare.ui.components.tabs.games.game_info.game_settings import Ui_GameSettings from rare.utils import config_helper from rare.utils.extra_widgets import PathEdit from rare.utils.utils import WineResolver, get_raw_save_path from rare.utils.utils import icon logger = getLogger("GameSettings") def find_proton_wrappers(): possible_proton_wrappers = [] compatibilitytools_dirs = [ os.path.expanduser("~/.steam/steam/steamapps/common"), "/usr/share/steam/compatibilitytools.d", os.path.expanduser("~/.steam/compatibilitytools.d"), os.path.expanduser("~/.steam/root/compatibilitytools.d"), ] for c in compatibilitytools_dirs: if os.path.exists(c): for i in os.listdir(c): proton = os.path.join(c, i, "proton") compatibilitytool = os.path.join(c, i, "compatibilitytool.vdf") toolmanifest = os.path.join(c, i, "toolmanifest.vdf") if os.path.exists(proton) and ( os.path.exists(compatibilitytool) or os.path.exists(toolmanifest) ): wrapper = f'"{proton}" run' possible_proton_wrappers.append(wrapper) if not possible_proton_wrappers: logger.warning("Unable to find any Proton version") return possible_proton_wrappers class GameSettings(QWidget, Ui_GameSettings): game: Game igame: InstalledGame # variable to no update when changing game change = False def __init__(self, core: LegendaryCore, parent): super(GameSettings, self).__init__(parent=parent) self.setupUi(self) self.core = core self.settings = QSettings() self.wrapper_settings = WrapperSettings() self.launch_settings_group.layout().addRow( QLabel("Wrapper"), self.wrapper_settings ) self.cloud_save_path_edit = PathEdit( "", file_type=QFileDialog.DirectoryOnly, ph_text=self.tr("Cloud save path"), edit_func=lambda text: (os.path.exists(text), text, PathEdit.reasons.dir_not_exist), save_func=self.save_save_path, ) self.cloud_layout.addRow( QLabel(self.tr("Save path")), self.cloud_save_path_edit ) self.compute_save_path_button = QPushButton( icon("fa.magic"), self.tr("Auto compute save path") ) self.compute_save_path_button.setSizePolicy( QSizePolicy.Maximum, QSizePolicy.Fixed ) self.compute_save_path_button.clicked.connect(self.compute_save_path) self.cloud_layout.addRow(None, self.compute_save_path_button) self.offline.currentIndexChanged.connect( lambda x: self.update_combobox(x, "offline") ) self.skip_update.currentIndexChanged.connect( lambda x: self.update_combobox(x, "skip_update_check") ) self.cloud_sync.stateChanged.connect( lambda: self.settings.setValue( f"{self.game.app_name}/auto_sync_cloud", self.cloud_sync.isChecked() ) ) self.launch_params.textChanged.connect( lambda x: self.save_line_edit("start_params", x) ) if platform.system() != "Windows": self.possible_proton_wrappers = find_proton_wrappers() self.proton_wrapper.addItems(self.possible_proton_wrappers) self.proton_wrapper.currentIndexChanged.connect(self.change_proton) self.proton_prefix = PathEdit( file_type=QFileDialog.DirectoryOnly, edit_func=self.proton_prefix_edit, save_func=self.proton_prefix_save, ) self.proton_prefix_layout.addWidget(self.proton_prefix) self.linux_settings = LinuxAppSettings() # FIXME: Remove the spacerItem and margins from the linux settings # FIXME: This should be handled differently at soem point in the future self.linux_settings.layout().setContentsMargins(0, 0, 0, 0) for item in [ self.linux_settings.layout().itemAt(idx) for idx in range(self.linux_settings.layout().count()) ]: if item.spacerItem(): self.linux_settings.layout().removeItem(item) del item # FIXME: End of FIXME self.linux_settings_layout.addWidget(self.linux_settings) self.linux_settings_layout.setAlignment(Qt.AlignTop) else: self.linux_settings_widget.setVisible(False) self.game_settings_contents_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")) def compute_save_path(self): if ( self.core.is_installed(self.game.app_name) and self.game.supports_cloud_saves ): try: new_path = self.core.get_save_path(self.game.app_name) except Exception as e: logger.warning(str(e)) self.cloud_save_path_edit.setText(self.tr("Loading")) self.cloud_save_path_edit.setDisabled(True) self.compute_save_path_button.setDisabled(True) resolver = WineResolver( get_raw_save_path(self.game), self.game.app_name, self.core ) app_name = self.game.app_name[:] resolver.signals.result_ready.connect( lambda x: self.wine_resolver_finished(x, app_name) ) QThreadPool.globalInstance().start(resolver) return else: self.cloud_save_path_edit.setText(new_path) def wine_resolver_finished(self, path, app_name): logger.info( f"Wine resolver finished for {app_name}. Computed save path: {path}" ) if app_name == self.game.app_name: self.cloud_save_path_edit.setDisabled(False) self.compute_save_path_button.setDisabled(False) if path and not os.path.exists(path): try: os.makedirs(path) except PermissionError: self.cloud_save_path_edit.setText("") QMessageBox.warning( None, "Error", self.tr( "Error while launching {}. No permission to create {}" ).format(self.game.app_title, path), ) return if not path: self.cloud_save_path_edit.setText("") return self.cloud_save_path_edit.setText(path) elif path: igame = self.core.get_installed_game(app_name) igame.save_path = path self.core.lgd.set_installed_game(app_name, igame) def save_save_path(self, text): if self.game.supports_cloud_saves and self.change: self.igame.save_path = text self.core.lgd.set_installed_game(self.igame.app_name, self.igame) def save_line_edit(self, option, value): if value: config_helper.add_option(self.game.app_name, option, value) else: config_helper.remove_option(self.game.app_name, option) config_helper.save_config() if option == "wine_prefix": if self.game.supports_cloud_saves: self.compute_save_path() def update_combobox(self, i, option): if self.change: # remove section if i: if i == 1: config_helper.add_option(self.game.app_name, option, "true") if i == 2: config_helper.add_option(self.game.app_name, option, "false") else: config_helper.remove_option(self.game.app_name, option) config_helper.save_config() def change_proton(self, i): if self.change: # First combo box entry: Don't use Proton if i == 0: self.wrapper_settings.delete_wrapper("proton") config_helper.remove_option(self.game.app_name, "no_wine") config_helper.remove_option(f"{self.game.app_name}.env", "STEAM_COMPAT_DATA_PATH") config_helper.remove_option(f"{self.game.app_name}.env", "STEAM_COMPAT_CLIENT_INSTALL_PATH") self.proton_prefix.setEnabled(False) # lk: TODO: This has to be fixed properly. # lk: It happens because of the widget update. Mask it for now behind disabling the save button self.linux_settings.wine_groupbox.setEnabled(True) else: self.proton_prefix.setEnabled(True) self.linux_settings.wine_groupbox.setEnabled(False) wrapper = self.possible_proton_wrappers[i - 1] self.wrapper_settings.add_wrapper(wrapper) config_helper.add_option(self.game.app_name, "no_wine", "true") config_helper.add_option( f"{self.game.app_name}.env", "STEAM_COMPAT_DATA_PATH", os.path.expanduser("~/.proton"), ) config_helper.add_option( f"{self.game.app_name}.env", "STEAM_COMPAT_CLIENT_INSTALL_PATH", str(Path.home().joinpath(".steam", "steam")) ) self.proton_prefix.setText(os.path.expanduser("~/.proton")) # Don't use Wine self.linux_settings.wine_exec.setText("") self.linux_settings.wine_prefix.setText("") config_helper.save_config() def proton_prefix_edit(self, text: str) -> Tuple[bool, str, str]: if not text: text = os.path.expanduser("~/.proton") return True, text, "" parent = os.path.dirname(text) return os.path.exists(parent), text, PathEdit.reasons.dir_not_exist def proton_prefix_save(self, text: str): config_helper.add_option( f"{self.game.app_name}.env", "STEAM_COMPAT_DATA_PATH", text ) config_helper.save_config() def update_game(self, app_name: str): self.change = False self.game = self.core.get_game(app_name) self.igame = self.core.get_installed_game(self.game.app_name) if self.igame: if self.igame.can_run_offline: offline = self.core.lgd.config.get( self.game.app_name, "offline", fallback="unset" ) if offline == "true": self.offline.setCurrentIndex(1) elif offline == "false": self.offline.setCurrentIndex(2) else: self.offline.setCurrentIndex(0) self.offline.setEnabled(True) else: self.offline.setEnabled(False) else: self.offline.setEnabled(False) skip_update = self.core.lgd.config.get( self.game.app_name, "skip_update_check", fallback="unset" ) if skip_update == "true": self.skip_update.setCurrentIndex(1) elif skip_update == "false": self.skip_update.setCurrentIndex(2) else: self.skip_update.setCurrentIndex(0) self.game_title.setText(f"

{self.game.app_title}

") self.wrapper_settings.load_settings(app_name) if platform.system() != "Windows": self.linux_settings.update_game(app_name) if self.igame and self.igame.platform == "Mac": self.linux_settings_widget.setVisible(False) else: self.linux_settings_widget.setVisible(True) proton = self.wrapper_settings.extra_wrappers.get("proton", "").replace('"', "") if proton and "proton" in proton: self.proton_prefix.setEnabled(True) self.proton_wrapper.setCurrentText( f'"{proton.replace(" run", "")}" run' ) proton_prefix = self.core.lgd.config.get( f"{app_name}.env", "STEAM_COMPAT_DATA_PATH", fallback=self.tr("Please select path for proton prefix"), ) self.proton_prefix.setText(proton_prefix) self.linux_settings.wine_groupbox.setEnabled(False) else: self.proton_wrapper.setCurrentIndex(0) self.proton_prefix.setEnabled(False) self.linux_settings.wine_groupbox.setEnabled(True) if not self.game.supports_cloud_saves: self.cloud_group.setEnabled(False) self.cloud_save_path_edit.setText("") else: self.cloud_group.setEnabled(True) sync_cloud = self.settings.value( f"{self.game.app_name}/auto_sync_cloud", True, bool ) self.cloud_sync.setChecked(sync_cloud) if self.igame.save_path: self.cloud_save_path_edit.setText(self.igame.save_path) else: self.cloud_save_path_edit.setText("") self.launch_params.setText( self.core.lgd.config.get(self.game.app_name, "start_params", fallback="") ) self.override_exe_edit.setText( self.core.lgd.config.get(self.game.app_name, "override_exe", fallback="") ) self.change = True 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)