diff --git a/rare/components/tabs/settings/widgets/env_vars_model.py b/rare/components/tabs/settings/widgets/env_vars_model.py index 11e16449..795533e7 100644 --- a/rare/components/tabs/settings/widgets/env_vars_model.py +++ b/rare/components/tabs/settings/widgets/env_vars_model.py @@ -8,6 +8,7 @@ from PyQt5.QtGui import QFont from rare.lgndr.core import LegendaryCore from rare.utils.misc import icon +from rare.utils import proton class EnvVarsTableModel(QAbstractTableModel): @@ -23,11 +24,11 @@ class EnvVarsTableModel(QAbstractTableModel): self.__readonly = [ "STEAM_COMPAT_DATA_PATH", - "STEAM_COMPAT_CLIENT_INSTALL_PATH", "WINEPREFIX", "DXVK_HUD", "MANGOHUD_CONFIG", ] + self.__readonly.extend(proton.get_steam_environment(None).keys()) self.__default: str = "default" self.__appname: str = None diff --git a/rare/utils/proton.py b/rare/utils/proton.py index 371f2133..aa4ec7c3 100644 --- a/rare/utils/proton.py +++ b/rare/utils/proton.py @@ -1,8 +1,251 @@ import os +from dataclasses import dataclass from logging import getLogger +from typing import Optional, Union, List, Dict + +import vdf logger = getLogger("Proton") +steam_compat_client_install_paths = [os.path.expanduser("~/.local/share/Steam")] + + +def find_steam() -> str: + # return the first valid path + for path in steam_compat_client_install_paths: + if os.path.isdir(path) and os.path.isfile(os.path.join(path, "steam.sh")): + return path + + +def find_libraries(steam_path: str) -> List[str]: + vdf_path = os.path.join(steam_path, "steamapps", "libraryfolders.vdf") + with open(vdf_path, "r") as f: + libraryfolders = vdf.load(f)["libraryfolders"] + libraries = [os.path.join(folder["path"], "steamapps") for key, folder in libraryfolders.items()] + return libraries + + +@dataclass +class SteamBase: + steam_path: str + tool_path: str + toolmanifest: dict + + def __eq__(self, other): + return self.tool_path == other.tool_path + + 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) + # NOTE: "waitforexitandrun" seems to be the verb used in by steam to execute stuff + cmd = cmd.replace("%verb%", "waitforexitandrun") + return cmd + + +@dataclass +class SteamRuntime(SteamBase): + steam_library: str + appmanifest: dict + + def name(self): + return self.appmanifest["AppState"]["name"] + + def appid(self): + return self.appmanifest["AppState"]["appid"] + + +@dataclass +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 commandline(self): + runtime_cmd = self.runtime.commandline() + cmd = super().commandline() + return " ".join([runtime_cmd, cmd]) + + +@dataclass +class CompatibilityTool(SteamBase): + 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 name(self): + 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 find_appmanifests(library: str) -> List[dict]: + appmanifests = [] + for entry in os.scandir(library): + if entry.is_file() and entry.name.endswith(".acf"): + with open(os.path.join(library, entry.name), "r") as f: + appmanifest = vdf.load(f) + appmanifests.append(appmanifest) + return appmanifests + + +def find_protons(steam_path: str, library: str) -> List[ProtonTool]: + protons = [] + appmanifests = find_appmanifests(library) + common = os.path.join(library, "common") + for appmanifest in appmanifests: + folder = appmanifest["AppState"]["installdir"] + tool_path = os.path.join(common, folder) + if os.path.isfile(vdf_file := os.path.join(tool_path, "toolmanifest.vdf")): + with open(vdf_file, "r") as f: + toolmanifest = vdf.load(f) + if toolmanifest["manifest"]["compatmanager_layer_name"] == "proton": + protons.append( + ProtonTool( + steam_path=steam_path, + steam_library=library, + appmanifest=appmanifest, + tool_path=tool_path, + toolmanifest=toolmanifest, + ) + ) + return protons + + +def find_compatibility_tools(steam_path: str) -> List[CompatibilityTool]: + compatibilitytools_paths = { + "/usr/share/steam/compatibilitytools.d", + os.path.expanduser(os.path.join(steam_path, "compatibilitytools.d")), + os.path.expanduser("~/.steam/compatibilitytools.d"), + os.path.expanduser("~/.steam/root/compatibilitytools.d"), + } + compatibilitytools_paths = { + os.path.realpath(path) for path in compatibilitytools_paths if os.path.isdir(path) + } + tools = [] + for path in compatibilitytools_paths: + for entry in os.scandir(path): + if entry.is_dir(): + tool_path = os.path.join(path, entry.name) + tool_vdf = os.path.join(tool_path, "compatibilitytool.vdf") + manifest_vdf = os.path.join(tool_path, "toolmanifest.vdf") + if os.path.isfile(tool_vdf) and os.path.isfile(manifest_vdf): + with open(tool_vdf, "r") as f: + compatibilitytool = vdf.load(f) + with open(manifest_vdf, "r") as f: + manifest = vdf.load(f) + tools.append( + CompatibilityTool( + steam_path=steam_path, + tool_path=tool_path, + toolmanifest=manifest, + compatibilitytool=compatibilitytool, + ) + ) + return tools + + +def find_runtimes(steam_path: str, library: str) -> Dict[str, SteamRuntime]: + runtimes = {} + appmanifests = find_appmanifests(library) + common = os.path.join(library, "common") + for appmanifest in appmanifests: + folder = appmanifest["AppState"]["installdir"] + tool_path = os.path.join(common, folder) + if os.path.isfile(vdf_file := os.path.join(tool_path, "toolmanifest.vdf")): + with open(vdf_file, "r") as f: + toolmanifest = vdf.load(f) + if toolmanifest["manifest"]["compatmanager_layer_name"] == "container-runtime": + runtimes.update( + { + appmanifest["AppState"]["appid"]: SteamRuntime( + steam_path=steam_path, + steam_library=library, + appmanifest=appmanifest, + tool_path=tool_path, + toolmanifest=toolmanifest, + ) + } + ) + return runtimes + + +def find_runtime( + tool: Union[ProtonTool, CompatibilityTool], runtimes: Dict[str, SteamRuntime] +) -> Optional[SteamRuntime]: + required_tool = tool.toolmanifest["manifest"].get("require_tool_appid") + if required_tool is None: + return None + return runtimes[required_tool] + + +def get_steam_environment(tool: Optional[Union[ProtonTool, CompatibilityTool]], app_name: str = None) -> Dict: + environ = {} + # If the tool is unset, return all affected env variable names + # IMPORTANT: keep this in sync with the code below + if tool is None: + environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = "" + environ["STEAM_COMPAT_LIBRARY_PATHS"] = "" + environ["STEAM_COMPAT_MOUNTS"] = "" + environ["STEAM_COMPAT_TOOL_PATHS"] = "" + return environ + + environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = tool.steam_path + if isinstance(tool, ProtonTool): + environ["STEAM_COMPAT_LIBRARY_PATHS"] = tool.steam_library + if tool.runtime is not None: + compat_mounts = [tool.tool_path, tool.runtime.tool_path] + environ["STEAM_COMPAT_MOUNTS"] = ":".join(compat_mounts) + tool_paths = [tool.tool_path] + if tool.runtime is not None: + tool_paths.append(tool.runtime.tool_path) + environ["STEAM_COMPAT_TOOL_PATHS"] = ":".join(tool_paths) + return environ + + +def find_tools() -> List[Union[ProtonTool, CompatibilityTool]]: + steam_path = find_steam() + logger.info("Using Steam install in %s", steam_path) + steam_libraries = find_libraries(steam_path) + logger.info("Searching for tools in libraries %s", steam_libraries) + + runtimes = {} + for library in steam_libraries: + runtimes.update(find_runtimes(steam_path, library)) + + tools = [] + for library in steam_libraries: + tools.extend(find_protons(steam_path, library)) + tools.extend(find_compatibility_tools(steam_path)) + + for tool in tools: + runtime = find_runtime(tool, runtimes) + tool.runtime = runtime + + return tools + + +if __name__ == "__main__": + from pprint import pprint + + _tools = find_tools() + pprint(_tools) + + for tool in _tools: + print(get_steam_environment(tool)) + print(tool.name(), tool.commandline()) + def find_proton_combos(): possible_proton_combos = [] diff --git a/requirements.txt b/requirements.txt index cf445de5..7301263b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ QtAwesome setuptools legendary-gl>=0.20.34 orjson +vdf; platform_system != "Windows" pywin32; platform_system == "Windows"