"""Steam for Linux runner""" import os from gettext import gettext as _ from lutris.exceptions import MissingGameExecutableError, UnavailableRunnerError from lutris.monitored_command import MonitoredCommand from lutris.runners import NonInstallableRunnerError from lutris.runners.runner import Runner from lutris.util import linux, system from lutris.util.log import logger from lutris.util.steam.appmanifest import get_appmanifest_from_appid, get_path_from_appmanifest from lutris.util.steam.config import get_default_acf, get_steam_dir, get_steamapps_dirs from lutris.util.steam.vdfutils import to_vdf from lutris.util.strings import split_arguments def get_steam_pid(): """Return pid of Steam process.""" return system.get_pid("steam$") def is_running(): """Checks if Steam is running.""" return bool(get_steam_pid()) class steam(Runner): description = _("Runs Steam for Linux games") human_name = _("Steam") platforms = [_("Linux")] runner_executable = "steam" flatpak_id = "com.valvesoftware.Steam" game_options = [ { "option": "appid", "label": _("Application ID"), "type": "string", "help": _( "The application ID can be retrieved from the game's " "page at steampowered.com. Example: 235320 is the " "app ID for Original War in: \n" "http://store.steampowered.com/app/235320/" ), }, { "option": "args", "type": "string", "label": _("Arguments"), "help": _( "Command line arguments used when launching the game.\nIgnored when Steam Big Picture mode is enabled." ), }, { "option": "run_without_steam", "label": _("DRM free mode (Do not launch Steam)"), "type": "bool", "default": False, "advanced": True, "help": _("Run the game directly without Steam, requires the game binary path to be set"), }, { "option": "steamless_binary", "type": "file", "label": _("Game binary path"), "advanced": True, "help": _("Path to the game executable (Required by DRM free mode)"), }, ] runner_options = [ { "option": "start_in_big_picture", "label": _("Start Steam in Big Picture mode"), "type": "bool", "default": False, "help": _( "Launches Steam in Big Picture mode.\n" "Only works if Steam is not running or " "already running in Big Picture mode.\n" "Useful when playing with a Steam Controller." ), }, { "option": "args", "type": "string", "label": _("Arguments"), "advanced": True, "help": _("Extra command line arguments used when launching Steam"), }, ] system_options_override = [ {"option": "disable_runtime", "default": True}, {"option": "gamemode", "default": False}, ] @property def runnable_alone(self): return not linux.LINUX_SYSTEM.is_flatpak() @property def appid(self): return self.game_config.get("appid") or "" @property def game_path(self): if not self.appid: return None return self.get_game_path_from_appid(self.appid) @property def steam_data_dir(self): """Main installation directory for Steam""" return get_steam_dir() def get_appmanifest(self): """Return an AppManifest instance for the game""" appmanifests = [] for apps_path in get_steamapps_dirs(): appmanifest = get_appmanifest_from_appid(apps_path, self.appid) if appmanifest: appmanifests.append(appmanifest) if len(appmanifests) > 1: logger.warning("More than one AppManifest for %s returning only 1st", self.appid) if appmanifests: return appmanifests[0] def get_executable(self) -> str: if linux.LINUX_SYSTEM.is_flatpak(): # Fallback to xgd-open for Steam URIs in Flatpak return system.find_required_executable("xdg-open") runner_executable = self.runner_config.get("runner_executable") if runner_executable and os.path.isfile(runner_executable): return runner_executable return system.find_required_executable(self.runner_executable) @property def working_dir(self): """Return the working directory to use when running the game.""" if self.game_config.get("run_without_steam"): steamless_binary = self.game_config.get("steamless_binary") if steamless_binary and os.path.isfile(steamless_binary): return os.path.dirname(steamless_binary) return super().working_dir @property def launch_args(self): """Provide launch arguments for Steam""" command = self.get_command() if self.runner_config.get("start_in_big_picture"): command.append("-bigpicture") return command + split_arguments(self.runner_config.get("args") or "") def get_game_path_from_appid(self, appid): """Return the game directory.""" for apps_path in get_steamapps_dirs(): game_path = get_path_from_appmanifest(apps_path, appid) if game_path: return game_path logger.info("Data path for SteamApp %s not found.", appid) return "" def get_default_steamapps_path(self): steamapps_paths = get_steamapps_dirs() if steamapps_paths: return steamapps_paths[0] return "" def install(self, install_ui_delegate, version=None, callback=None): raise NonInstallableRunnerError( message=_("Steam for Linux installation is not handled by Lutris."), message_markup=_( "Steam for Linux installation is not handled by Lutris.\n" "Please go to " "http://steampowered.com" " or install Steam with the package provided by your distribution." ), ) def install_game(self, appid, generate_acf=False): logger.debug("Installing steam game %s", appid) if generate_acf: acf_data = get_default_acf(appid, appid) acf_content = to_vdf(acf_data) steamapps_path = self.get_default_steamapps_path() if not steamapps_path: raise UnavailableRunnerError(_("Could not find Steam path, is Steam installed?")) acf_path = os.path.join(steamapps_path, "appmanifest_%s.acf" % appid) with open(acf_path, "w", encoding="utf-8") as acf_file: acf_file.write(acf_content) system.spawn(self.get_command() + [f"steam://install/{appid}"]) def get_run_data(self): return {"command": self.launch_args, "env": self.get_env()} def play(self): game_args = self.game_config.get("args") or "" binary_path = self.game_config.get("steamless_binary") if self.game_config.get("run_without_steam") and binary_path: # Start without steam if not system.path_exists(binary_path): raise MissingGameExecutableError(filename=binary_path) command = [binary_path] else: # Start through steam if linux.LINUX_SYSTEM.is_flatpak(): if game_args: steam_uri = "steam://run/%s//%s/" % (self.appid, game_args) else: steam_uri = "steam://rungameid/%s" % self.appid return { "command": self.launch_args + [steam_uri], "env": self.get_env(), } command = self.launch_args if self.runner_config.get("start_in_big_picture") or not game_args: command.append("steam://rungameid/%s" % self.appid) else: command.append("-applaunch") command.append(self.appid) if game_args: for arg in split_arguments(game_args): command.append(arg) return { "command": command, "env": self.get_env(), } def remove_game_data(self, app_id=None, **kwargs): if not self.is_installed(): return False app_id = app_id or self.appid command = MonitoredCommand( self.get_command() + [f"steam://uninstall/{app_id}"], runner=self, env=self.get_env(), ) command.start()