def shutdown(self): """Orders Steam to shutdown""" logger.info("Shutting down Steam") shutdown_command = MonitoredCommand( (self.launch_args + ["-shutdown"]), runner=self, env=self.get_env(os_env=False) ) shutdown_command.start()
def remove_game_data(self, appid=None, **kwargs): if not self.is_installed(): return False command = MonitoredCommand( [self.get_executable(), "steam://uninstall/%s" % (appid or self.appid)], runner=self, env=self.get_env(), ) command.start()
def remove_game_data(self, appid=None, **kwargs): if not self.is_installed(): logger.warning("Trying to remove a winesteam game but it's not installed.") return False self.force_shutdown() uninstall_command = MonitoredCommand( (self.launch_args + ["steam://uninstall/%s" % (appid or self.appid)]), runner=self, env=self.get_env(os_env=False) ) uninstall_command.start()
def on_game_quit(self): """Restore some settings and cleanup after game quit.""" if self.prelaunch_executor and self.prelaunch_executor.is_running: logger.info("Stopping prelaunch script") self.prelaunch_executor.stop() self.heartbeat = None if self.state != self.STATE_STOPPED: logger.warning("Game still running (state: %s)", self.state) self.stop() # Check for post game script postexit_command = self.runner.system_config.get("postexit_command") if postexit_command: command_array = shlex.split(postexit_command) if system.path_exists(command_array[0]): logger.info("Running post-exit command: %s", postexit_command) postexit_thread = MonitoredCommand( command_array, include_processes=[os.path.basename(postexit_command)], env=self.game_runtime_config["env"], cwd=self.directory, ) postexit_thread.start() if self.discord_presence.available: self.discord_presence.clear_discord_rich_presence() quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("%s stopped at %s", self.name, quit_time) self.lastplayed = int(time.time()) self.save(save_config=False) os.chdir(os.path.expanduser("~")) if self.resolution_changed or self.runner.system_config.get("reset_desktop"): DISPLAY_MANAGER.set_resolution(self.original_outputs) if self.compositor_disabled: self.set_desktop_compositing(True) if self.screen_saver_inhibitor_cookie is not None: SCREEN_SAVER_INHIBITOR.uninhibit(self.screen_saver_inhibitor_cookie) self.screen_saver_inhibitor_cookie = None if self.runner.system_config.get("use_us_layout"): subprocess.Popen(["setxkbmap"], env=os.environ).communicate() if self.runner.system_config.get("restore_gamma"): restore_gamma() self.process_return_codes()
def remove_game_data(self, appid=None, **kwargs): if not self.is_installed(): return False command = MonitoredCommand( [ self.get_executable(), "steam://uninstall/%s" % (appid or self.appid) ], runner=self, env=self.get_env(), ) command.start()
def remove_game_data(self, appid=None, **kwargs): """Uninstall a game from Steam""" if not self.is_installed(): logger.warning("Trying to remove a winesteam game but it's not installed.") return False self.force_shutdown() uninstall_command = MonitoredCommand( (self.launch_args + ["steam://uninstall/%s" % (appid or self.appid)]), runner=self, env=self.get_env(os_env=False), ) uninstall_command.start()
def on_game_quit(self): """Restore some settings and cleanup after game quit.""" if not self.timer.finished: self.timer.end() self.playtime = self.timer.duration + self.playtime if self.prelaunch_executor and self.prelaunch_executor.is_running: logger.info("Stopping prelaunch script") self.prelaunch_executor.stop() self.heartbeat = None if self.state != self.STATE_STOPPED: logger.warning("Game still running (state: %s)", self.state) self.stop() # Check for post game script postexit_command = self.runner.system_config.get("postexit_command") if system.path_exists(postexit_command): logger.info("Running post-exit command: %s", postexit_command) postexit_thread = MonitoredCommand( [postexit_command], include_processes=[os.path.basename(postexit_command)], cwd=self.directory, ) postexit_thread.start() quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("%s stopped at %s", self.name, quit_time) self.lastplayed = int(time.time()) self.save(metadata_only=True) os.chdir(os.path.expanduser("~")) if self.resolution_changed or self.runner.system_config.get("reset_desktop"): display.change_resolution(self.original_outputs) if self.compositor_disabled: self.set_desktop_compositing(True) if self.runner.system_config.get("use_us_layout"): subprocess.Popen(["setxkbmap"], env=os.environ).communicate() if self.runner.system_config.get("restore_gamma"): display.restore_gamma() self.process_return_codes() self.emit('game-stop') if self.exit_main_loop: exit()
def on_game_quit(self): """Restore some settings and cleanup after game quit.""" if self.prelaunch_executor and self.prelaunch_executor.is_running: logger.info("Stopping prelaunch script") self.prelaunch_executor.stop() self.heartbeat = None if self.state != self.STATE_STOPPED: logger.warning("Game still running (state: %s)", self.state) self.stop() # Check for post game script postexit_command = self.runner.system_config.get("postexit_command") if system.path_exists(postexit_command): logger.info("Running post-exit command: %s", postexit_command) postexit_thread = MonitoredCommand( [postexit_command], include_processes=[os.path.basename(postexit_command)], cwd=self.directory, ) postexit_thread.start() if self.discord_presence.available: self.discord_presence.clear_discord_rich_presence() quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("%s stopped at %s", self.name, quit_time) self.lastplayed = int(time.time()) self.save(metadata_only=True) os.chdir(os.path.expanduser("~")) if self.resolution_changed or self.runner.system_config.get("reset_desktop"): display.change_resolution(self.original_outputs) if self.compositor_disabled: self.set_desktop_compositing(True) if self.runner.system_config.get("use_us_layout"): subprocess.Popen(["setxkbmap"], env=os.environ).communicate() if self.runner.system_config.get("restore_gamma"): display.restore_gamma() self.process_return_codes() if self.exit_main_loop: exit()
def remove_game_data(self, appid=None, **kwargs): if not self.is_installed(): installed = self.install_dialog() if not installed: return False appid = appid if appid else self.appid if appid is None: raise RuntimeError("No appid given for uninstallation " "(game config=%s)" % self.game_config) logger.debug("Launching Steam uninstall of game %s", appid) command = [self.get_executable(), "steam://uninstall/%s" % appid] thread = MonitoredCommand(command, runner=self, env=self.get_env(), watch=False) thread.start()
def run(self, *args): """Run the runner alone.""" if not self.runnable_alone: return if not self.is_installed() and not self.install_dialog(): logger.info("Runner install cancelled") return command_data = self.get_run_data() command = command_data.get("command") env = (command_data.get("env") or {}).copy() if hasattr(self, "prelaunch"): self.prelaunch() command_runner = MonitoredCommand(command, runner=self, env=env) command_runner.start()
def run(self, *args): """Run the runner alone.""" if not self.runnable_alone: return if not self.is_installed(): if not self.install_dialog(): logger.info("Runner install cancelled") return command_data = self.get_run_data() command = command_data.get("command") env = (command_data.get("env") or {}).copy() if hasattr(self, "prelaunch"): self.prelaunch() command_runner = MonitoredCommand(command, runner=self, env=env) command_runner.start()
def start_xephyr(self, display=":2"): """Start a monitored Xephyr instance""" if not system.find_executable("Xephyr"): raise GameConfigError("Unable to find Xephyr, install it or disable the Xephyr option") xephyr_depth = "8" if self.runner.system_config.get("xephyr") == "8bpp" else "16" xephyr_resolution = self.runner.system_config.get("xephyr_resolution") or "640x480" xephyr_command = [ "Xephyr", display, "-ac", "-screen", xephyr_resolution + "x" + xephyr_depth, "-glamor", "-reset", "-terminate", ] if self.runner.system_config.get("xephyr_fullscreen"): xephyr_command.append("-fullscreen") xephyr_thread = MonitoredCommand(xephyr_command) xephyr_thread.start() time.sleep(3) return display
def execute(self, data): """Run an executable file.""" args = [] terminal = None working_dir = None env = {} if isinstance(data, dict): self._check_required_params([("file", "command")], data, "execute") if "command" in data and "file" in data: raise ScriptingError( "Parameters file and command can't be used " "at the same time for the execute command", data, ) file_ref = data.get("file", "") command = data.get("command", "") args_string = data.get("args", "") for arg in shlex.split(args_string): args.append(self._substitute(arg)) terminal = data.get("terminal") working_dir = data.get("working_dir") if not data.get("disable_runtime"): # Possibly need to handle prefer_system_libs here env.update(runtime.get_env()) # Loading environment variables set in the script env.update(self.script_env) # Environment variables can also be passed to the execute command local_env = data.get("env") or {} env.update({key: self._substitute(value) for key, value in local_env.items()}) include_processes = shlex.split(data.get("include_processes", "")) exclude_processes = shlex.split(data.get("exclude_processes", "")) elif isinstance(data, str): command = data include_processes = [] exclude_processes = [] else: raise ScriptingError("No parameters supplied to execute command.", data) if command: file_ref = "bash" args = ["-c", self._get_file(command.strip())] include_processes.append("bash") else: # Determine whether 'file' value is a file id or a path file_ref = self._get_file(file_ref) if system.path_exists(file_ref) and not system.is_executable(file_ref): logger.warning("Making %s executable", file_ref) system.make_executable(file_ref) exec_path = system.find_executable(file_ref) if not exec_path: raise ScriptingError("Unable to find executable %s" % file_ref) if terminal: terminal = system.get_default_terminal() if not working_dir or not os.path.exists(working_dir): working_dir = self.target_path command = MonitoredCommand( [exec_path] + args, env=env, term=terminal, cwd=working_dir, include_processes=include_processes, exclude_processes=exclude_processes, ) command.start() GLib.idle_add(self.parent.attach_logger, command) self.heartbeat = GLib.timeout_add(1000, self._monitor_task, command) return "STOP"
class Game(GObject.Object): """This class takes cares of loading the configuration for a game and running it. """ STATE_IDLE = "idle" STATE_STOPPED = "stopped" STATE_RUNNING = "running" __gsignals__ = { "game-error": (GObject.SIGNAL_RUN_FIRST, None, (str,)), "game-start": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-started": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-stop": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-stopped": (GObject.SIGNAL_RUN_FIRST, None, (int,)), "game-removed": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-updated": (GObject.SIGNAL_RUN_FIRST, None, ()), } def __init__(self, game_id=None): super().__init__() self.id = game_id # pylint: disable=invalid-name self.runner = None self.config = None # Load attributes from database game_data = pga.get_game_by_field(game_id, "id") self.slug = game_data.get("slug") or "" self.runner_name = game_data.get("runner") or "" self.directory = game_data.get("directory") or "" self.name = game_data.get("name") or "" self.game_config_id = game_data.get("configpath") or "" self.is_installed = bool(game_data.get("installed") and self.game_config_id) self.platform = game_data.get("platform") or "" self.year = game_data.get("year") or "" self.lastplayed = game_data.get("lastplayed") or 0 self.steamid = game_data.get("steamid") or "" self.has_custom_banner = bool(game_data.get("has_custom_banner")) self.has_custom_icon = bool(game_data.get("has_custom_icon")) self.discord_presence = DiscordPresence() try: self.playtime = float(game_data.get("playtime") or 0.0) except ValueError: logger.error("Invalid playtime value %s", game_data.get("playtime")) self.playtime = 0.0 if self.game_config_id: self.load_config() self.game_thread = None self.prelaunch_executor = None self.heartbeat = None self.killswitch = None self.state = self.STATE_IDLE self.exit_main_loop = False self.xboxdrv_thread = None self.game_runtime_config = {} self.resolution_changed = False self.compositor_disabled = False self.stop_compositor = self.start_compositor = "" self.original_outputs = None self._log_buffer = None self.timer = Timer() @property def log_buffer(self): if self._log_buffer is None: self._log_buffer = Gtk.TextBuffer() self._log_buffer.create_tag("warning", foreground="red") if self.game_thread: self.game_thread.set_log_buffer(self._log_buffer) self._log_buffer.set_text(self.game_thread.stdout) return self._log_buffer def __repr__(self): return self.__unicode__() def __unicode__(self): value = self.name if self.runner_name: value += " (%s)" % self.runner_name return value @property def formatted_playtime(self): """Return a human readable formatted play time""" return strings.get_formatted_playtime(self.playtime) @property def is_search_result(self): return self.id < 0 @staticmethod def show_error_message(message): """Display an error message based on the runner's output.""" if message["error"] == "CUSTOM": message_text = message["text"].replace("&", "&") dialogs.ErrorDialog(message_text) elif message["error"] == "RUNNER_NOT_INSTALLED": dialogs.ErrorDialog("Error the runner is not installed") elif message["error"] == "NO_BIOS": dialogs.ErrorDialog("A bios file is required to run this game") elif message["error"] == "FILE_NOT_FOUND": filename = message["file"] if filename: message_text = "The file {} could not be found".format( filename.replace("&", "&") ) else: message_text = "No file provided" dialogs.ErrorDialog(message_text) elif message["error"] == "NOT_EXECUTABLE": message_text = message["file"].replace("&", "&") dialogs.ErrorDialog("The file %s is not executable" % message_text) def get_browse_dir(self): """Return the path to open with the Browse Files action.""" return self.runner.browse_dir def _get_runner(self): """Return the runner instance for this game's configuration""" try: runner_class = import_runner(self.runner_name) return runner_class(self.config) except InvalidRunner: logger.error( "Unable to import runner %s for %s", self.runner_name, self.slug ) def load_config(self): """Load the game's configuration.""" if not self.is_installed: return self.config = LutrisConfig( runner_slug=self.runner_name, game_config_id=self.game_config_id ) self.runner = self._get_runner() if self.discord_presence.available: self.discord_presence.client_id = self.config.system_config.get("discord_client_id") or DEFAULT_DISCORD_CLIENT_ID self.discord_presence.game_name = self.config.system_config.get("discord_custom_game_name") or self.name self.discord_presence.show_runner = self.config.system_config.get("discord_show_runner", True) self.discord_presence.runner_name = self.config.system_config.get("discord_custom_runner_name") or self.runner_name self.discord_presence.rpc_enabled = self.config.system_config.get("discord_rpc_enabled", True) def set_desktop_compositing(self, enable): """Enables or disables compositing""" if enable: system.execute(self.start_compositor, shell=True) else: self.start_compositor, self.stop_compositor = ( display.get_compositor_commands() ) if not (self.compositor_disabled or not self.stop_compositor): system.execute(self.stop_compositor, shell=True) self.compositor_disabled = True def remove(self, from_library=False, from_disk=False): if from_disk and self.runner: logger.debug("Removing game %s from disk", self.id) self.runner.remove_game_data(game_path=self.directory) # Do not keep multiple copies of the same game existing_games = pga.get_games_where(slug=self.slug) if len(existing_games) > 1: from_library = True if from_library: logger.debug("Removing game %s from library", self.id) pga.delete_game(self.id) else: pga.set_uninstalled(self.id) if self.config: self.config.remove() xdgshortcuts.remove_launcher(self.slug, self.id, desktop=True, menu=True) self.is_installed = False self.emit("game-removed") return from_library def set_platform_from_runner(self): """Set the game's platform from the runner""" if not self.runner: logger.warning("Game has no runner, can't set platform") return self.platform = self.runner.get_platform() if not self.platform: logger.warning("Can't get platform for runner %s", self.runner.human_name) def save(self, metadata_only=False): """ Save the game's config and metadata, if `metadata_only` is set to True, do not save the config. This is useful when exiting the game since the config might have changed and we don't want to override the changes. """ logger.debug("Saving %s", self) if not metadata_only: self.config.save() self.set_platform_from_runner() self.id = pga.add_or_update( name=self.name, runner=self.runner_name, slug=self.slug, platform=self.platform, year=self.year, lastplayed=self.lastplayed, directory=self.directory, installed=self.is_installed, configpath=self.config.game_config_id, steamid=self.steamid, id=self.id, playtime=self.playtime, ) self.emit("game-updated") def prelaunch(self): """Verify that the current game can be launched.""" if not self.runner.is_installed(): installed = self.runner.install_dialog() if not installed: return False if self.runner.use_runtime(): runtime_updater = runtime.RuntimeUpdater() if runtime_updater.is_updating(): logger.warning("Runtime updates: %s", runtime_updater.current_updates) dialogs.ErrorDialog( "Runtime currently updating", "Game might not work as expected" ) if "wine" in self.runner_name and not wine.get_system_wine_version(): # TODO find a reference to the root window or better yet a way not # to have Gtk dependent code in this class. root_window = None dialogs.WineNotInstalledWarning(parent=root_window) return True def play(self): """Launch the game.""" if not self.runner: dialogs.ErrorDialog("Invalid game configuration: Missing runner") self.state = self.STATE_STOPPED self.emit('game-stop') return if not self.prelaunch(): self.state = self.STATE_STOPPED self.emit('game-stop') return self.emit("game-start") if hasattr(self.runner, "prelaunch"): logger.debug("Prelaunching %s", self.runner) try: jobs.AsyncCall(self.runner.prelaunch, self.configure_game) except Exception as ex: logger.error(ex) raise else: self.configure_game(True) @watch_lutris_errors def configure_game(self, prelaunched, error=None): """Get the game ready to start, applying all the options This methods sets the game_runtime_config attribute. """ if error: logger.error(error) dialogs.ErrorDialog(str(error)) if not prelaunched: logger.error("Game prelaunch unsuccessful") dialogs.ErrorDialog("An error prevented the game from running") self.state = self.STATE_STOPPED self.emit('game-stop') return system_config = self.runner.system_config self.original_outputs = sorted( display.get_outputs(), key=lambda e: e.name == system_config.get("display") ) gameplay_info = self.runner.play() if "error" in gameplay_info: self.show_error_message(gameplay_info) self.state = self.STATE_STOPPED self.emit('game-stop') return logger.debug("Launching %s: %s", self.name, gameplay_info) logger.debug("Game info: %s", json.dumps(gameplay_info, indent=2)) env = {} sdl_gamecontrollerconfig = system_config.get("sdl_gamecontrollerconfig") if sdl_gamecontrollerconfig: path = os.path.expanduser(sdl_gamecontrollerconfig) if system.path_exists(path): with open(path, "r") as controllerdb_file: sdl_gamecontrollerconfig = controllerdb_file.read() env["SDL_GAMECONTROLLERCONFIG"] = sdl_gamecontrollerconfig sdl_video_fullscreen = system_config.get("sdl_video_fullscreen") or "" env["SDL_VIDEO_FULLSCREEN_DISPLAY"] = sdl_video_fullscreen restrict_to_display = system_config.get("display") if restrict_to_display != "off": if restrict_to_display == "primary": restrict_to_display = None for output in self.original_outputs: if output.primary: restrict_to_display = output.name break if not restrict_to_display: logger.warning("No primary display set") else: found = False for output in self.original_outputs: if output.name == restrict_to_display: found = True break if not found: logger.warning("Selected display %s not found", restrict_to_display) restrict_to_display = None if restrict_to_display: display.turn_off_except(restrict_to_display) time.sleep(3) self.resolution_changed = True resolution = system_config.get("resolution") if resolution != "off": display.change_resolution(resolution) time.sleep(3) self.resolution_changed = True if system_config.get("reset_pulse"): audio.reset_pulse() self.killswitch = system_config.get("killswitch") if self.killswitch and not system.path_exists(self.killswitch): # Prevent setting a killswitch to a file that doesn't exists self.killswitch = None # Command launch_arguments = gameplay_info["command"] optimus = system_config.get("optimus") if optimus == "primusrun" and system.find_executable("primusrun"): launch_arguments.insert(0, "primusrun") elif optimus == "optirun" and system.find_executable("optirun"): launch_arguments.insert(0, "virtualgl") launch_arguments.insert(0, "-b") launch_arguments.insert(0, "optirun") elif optimus == "pvkrun" and system.find_executable("pvkrun"): launch_arguments.insert(0, "pvkrun") xephyr = system_config.get("xephyr") or "off" if xephyr != "off": if not system.find_executable("Xephyr"): raise GameConfigError( "Unable to find Xephyr, install it or disable the Xephyr option" ) xephyr_depth = "8" if xephyr == "8bpp" else "16" xephyr_resolution = system_config.get("xephyr_resolution") or "640x480" xephyr_command = [ "Xephyr", ":2", "-ac", "-screen", xephyr_resolution + "x" + xephyr_depth, "-glamor", "-reset", "-terminate" ] if system_config.get("xephyr_fullscreen"): xephyr_command.append("-fullscreen") xephyr_thread = MonitoredCommand(xephyr_command) xephyr_thread.start() time.sleep(3) env["DISPLAY"] = ":2" if system_config.get("use_us_layout"): setxkbmap_command = ["setxkbmap", "-model", "pc101", "us", "-print"] xkbcomp_command = ["xkbcomp", "-", os.environ.get("DISPLAY", ":0")] xkbcomp = subprocess.Popen(xkbcomp_command, stdin=subprocess.PIPE) subprocess.Popen( setxkbmap_command, env=os.environ, stdout=xkbcomp.stdin ).communicate() xkbcomp.communicate() pulse_latency = system_config.get("pulse_latency") if pulse_latency: env["PULSE_LATENCY_MSEC"] = "60" vk_icd = system_config.get("vk_icd") if vk_icd and vk_icd != "off" and system.path_exists(vk_icd): env["VK_ICD_FILENAMES"] = vk_icd fps_limit = system_config.get("fps_limit") or "" if fps_limit: strangle_cmd = system.find_executable("strangle") launch_arguments = [strangle_cmd, fps_limit] + launch_arguments prefix_command = system_config.get("prefix_command") or "" if prefix_command: launch_arguments = ( shlex.split(os.path.expandvars(prefix_command)) + launch_arguments ) single_cpu = system_config.get("single_cpu") or False if single_cpu: logger.info("The game will run on a single CPU core") launch_arguments.insert(0, "0") launch_arguments.insert(0, "-c") launch_arguments.insert(0, "taskset") terminal = system_config.get("terminal") if terminal: terminal = system_config.get("terminal_app", system.get_default_terminal()) if terminal and not system.find_executable(terminal): dialogs.ErrorDialog( "The selected terminal application " "could not be launched:\n" "%s" % terminal ) self.state = self.STATE_STOPPED self.emit('game-stop') return # Env vars game_env = gameplay_info.get("env") or self.runner.get_env() env.update(game_env) # LD_PRELOAD ld_preload = gameplay_info.get("ld_preload") if ld_preload: env["LD_PRELOAD"] = ld_preload # Feral gamemode gamemode = system_config.get("gamemode") if gamemode: env["LD_PRELOAD"] = ":".join( [ path for path in [ env.get("LD_PRELOAD"), "/usr/$LIB/libgamemodeauto.so", ] if path ] ) # LD_LIBRARY_PATH game_ld_libary_path = gameplay_info.get("ld_library_path") if game_ld_libary_path: ld_library_path = env.get("LD_LIBRARY_PATH") if not ld_library_path: ld_library_path = "$LD_LIBRARY_PATH" env["LD_LIBRARY_PATH"] = ":".join([game_ld_libary_path, ld_library_path]) include_processes = shlex.split(system_config.get("include_processes", "")) exclude_processes = shlex.split(system_config.get("exclude_processes", "")) self.game_runtime_config = { "args": launch_arguments, "env": env, "terminal": terminal, "include_processes": include_processes, "exclude_processes": exclude_processes } if system_config.get("disable_compositor"): self.set_desktop_compositing(False) # xboxdrv setup xboxdrv_config = system_config.get("xboxdrv") if xboxdrv_config: self.xboxdrv_start(xboxdrv_config) prelaunch_command = system_config.get("prelaunch_command") if system.path_exists(prelaunch_command): self.prelaunch_executor = MonitoredCommand( [prelaunch_command], include_processes=[os.path.basename(prelaunch_command)], cwd=self.directory, ) self.prelaunch_executor.start() logger.info("Running %s in the background", prelaunch_command) if system_config.get("prelaunch_wait"): self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.prelaunch_beat) else: self.start_game() def start_game(self): self.game_thread = MonitoredCommand( self.game_runtime_config["args"], runner=self.runner, env=self.game_runtime_config["env"], term=self.game_runtime_config["terminal"], log_buffer=self._log_buffer, include_processes=self.game_runtime_config["include_processes"], exclude_processes=self.game_runtime_config["exclude_processes"], ) if hasattr(self.runner, "stop"): self.game_thread.stop_func = self.runner.stop self.game_thread.start() self.timer.start() self.emit("game-started") self.state = self.STATE_RUNNING self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.beat) def stop_game(self): self.state = self.STATE_STOPPED self.emit('game-stop') if not self.timer.finished: self.timer.end() self.playtime += self.timer.duration / 3600 def xboxdrv_start(self, config): command = [ "pkexec", "xboxdrv", "--daemon", "--detach-kernel-driver", "--dbus", "session", "--silent", ] + shlex.split(config) logger.debug("[xboxdrv] %s", " ".join(command)) self.xboxdrv_thread = MonitoredCommand(command, include_processes=["xboxdrv"]) self.xboxdrv_thread.stop_func = self.xboxdrv_stop self.xboxdrv_thread.start() @staticmethod def reload_xpad(): """Reloads the xpads module. The path is hardcoded because this script is allowed to be executed as root with a PolicyKit rule put in place by the packages. Note to packagers: If you don't intend to create a PolicyKit rule for this script then don't package it as calling it will fail. """ if system.path_exists("/usr/share/lutris/bin/resetxpad"): os.system("pkexec /usr/share/lutris/bin/resetxpad") def xboxdrv_stop(self): os.system("pkexec xboxdrvctl --shutdown") self.reload_xpad() def prelaunch_beat(self): """Watch the prelaunch command""" if self.prelaunch_executor.is_running: return True self.start_game() return False def beat(self): """Watch the game's process(es).""" if self.game_thread.error: dialogs.ErrorDialog( "<b>Error lauching the game:</b>\n" + self.game_thread.error ) self.on_game_quit() return False # The killswitch file should be set to a device (ie. /dev/input/js0) # When that device is unplugged, the game is forced to quit. killswitch_engage = self.killswitch and not system.path_exists(self.killswitch) if not self.game_thread.is_running or killswitch_engage: logger.debug("Game thread stopped") self.on_game_quit() return False if self.discord_presence.available: self.discord_presence.update_discord_rich_presence() return True def stop(self): """Stops the game""" if self.state == self.STATE_STOPPED: logger.debug("Game already stopped") return logger.info("Stopping %s", self) if self.runner.system_config.get("xboxdrv"): logger.debug("Stopping xboxdrv") self.xboxdrv_thread.stop() if self.game_thread: jobs.AsyncCall(self.game_thread.stop, None) self.stop_game() def on_game_quit(self): """Restore some settings and cleanup after game quit.""" if self.prelaunch_executor and self.prelaunch_executor.is_running: logger.info("Stopping prelaunch script") self.prelaunch_executor.stop() self.heartbeat = None if self.state != self.STATE_STOPPED: logger.warning("Game still running (state: %s)", self.state) self.stop() # Check for post game script postexit_command = self.runner.system_config.get("postexit_command") if system.path_exists(postexit_command): logger.info("Running post-exit command: %s", postexit_command) postexit_thread = MonitoredCommand( [postexit_command], include_processes=[os.path.basename(postexit_command)], cwd=self.directory, ) postexit_thread.start() if self.discord_presence.available: self.discord_presence.clear_discord_rich_presence() quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("%s stopped at %s", self.name, quit_time) self.lastplayed = int(time.time()) self.save(metadata_only=True) os.chdir(os.path.expanduser("~")) if self.resolution_changed or self.runner.system_config.get("reset_desktop"): display.change_resolution(self.original_outputs) if self.compositor_disabled: self.set_desktop_compositing(True) if self.runner.system_config.get("use_us_layout"): subprocess.Popen(["setxkbmap"], env=os.environ).communicate() if self.runner.system_config.get("restore_gamma"): display.restore_gamma() self.process_return_codes() if self.exit_main_loop: exit() def process_return_codes(self): """Do things depending on how the game quitted.""" if self.game_thread.return_code == 127: # Error missing shared lib error = "error while loading shared lib" error_line = strings.lookup_string_in_text(error, self.game_thread.stdout) if error_line: dialogs.ErrorDialog( "<b>Error: Missing shared library.</b>" "\n\n%s" % error_line ) if self.game_thread.return_code == 1: # Error Wine version conflict error = "maybe the wrong wineserver" if strings.lookup_string_in_text(error, self.game_thread.stdout): dialogs.ErrorDialog( "<b>Error: A different Wine version is " "already using the same Wine prefix.</b>" ) def notify_steam_game_changed(self, appmanifest): """Receive updates from Steam games and set the thread's ready state accordingly""" if not self.game_thread: return if "Fully Installed" in appmanifest.states and not self.game_thread.ready_state: logger.info("Steam game %s is fully installed", appmanifest.steamid) self.game_thread.ready_state = True elif "Update Required" in appmanifest.states and self.game_thread.ready_state: logger.info( "Steam game %s updating, setting game thread as not ready", appmanifest.steamid, ) self.game_thread.ready_state = False
def configure_game(self, prelaunched, error=None): """Get the game ready to start, applying all the options This methods sets the game_runtime_config attribute. """ if error: logger.error(error) dialogs.ErrorDialog(str(error)) if not prelaunched: logger.error("Game prelaunch unsuccessful") dialogs.ErrorDialog("An error prevented the game from running") self.state = self.STATE_STOPPED self.emit("game-stop") return system_config = self.runner.system_config self.original_outputs = DISPLAY_MANAGER.get_config() gameplay_info = self.runner.play() if "error" in gameplay_info: self.show_error_message(gameplay_info) self.state = self.STATE_STOPPED self.emit("game-stop") return logger.debug("Launching %s: %s", self.name, gameplay_info) logger.debug("Game info: %s", json.dumps(gameplay_info, indent=2)) env = {} sdl_gamecontrollerconfig = system_config.get( "sdl_gamecontrollerconfig") if sdl_gamecontrollerconfig: path = os.path.expanduser(sdl_gamecontrollerconfig) if system.path_exists(path): with open(path, "r") as controllerdb_file: sdl_gamecontrollerconfig = controllerdb_file.read() env["SDL_GAMECONTROLLERCONFIG"] = sdl_gamecontrollerconfig sdl_video_fullscreen = system_config.get("sdl_video_fullscreen") or "" env["SDL_VIDEO_FULLSCREEN_DISPLAY"] = sdl_video_fullscreen restrict_to_display = system_config.get("display") if restrict_to_display != "off": if restrict_to_display == "primary": restrict_to_display = None for output in self.original_outputs: if output.primary: restrict_to_display = output.name break if not restrict_to_display: logger.warning("No primary display set") else: found = False for output in self.original_outputs: if output.name == restrict_to_display: found = True break if not found: logger.warning("Selected display %s not found", restrict_to_display) restrict_to_display = None if restrict_to_display: turn_off_except(restrict_to_display) time.sleep(3) self.resolution_changed = True resolution = system_config.get("resolution") if resolution != "off": DISPLAY_MANAGER.set_resolution(resolution) time.sleep(3) self.resolution_changed = True if system_config.get("reset_pulse"): audio.reset_pulse() self.killswitch = system_config.get("killswitch") if self.killswitch and not system.path_exists(self.killswitch): # Prevent setting a killswitch to a file that doesn't exists self.killswitch = None # Command launch_arguments = gameplay_info["command"] optimus = system_config.get("optimus") if optimus == "primusrun" and system.find_executable("primusrun"): launch_arguments.insert(0, "primusrun") elif optimus == "optirun" and system.find_executable("optirun"): launch_arguments.insert(0, "virtualgl") launch_arguments.insert(0, "-b") launch_arguments.insert(0, "optirun") elif optimus == "pvkrun" and system.find_executable("pvkrun"): launch_arguments.insert(0, "pvkrun") xephyr = system_config.get("xephyr") or "off" if xephyr != "off": if not system.find_executable("Xephyr"): raise GameConfigError( "Unable to find Xephyr, install it or disable the Xephyr option" ) xephyr_depth = "8" if xephyr == "8bpp" else "16" xephyr_resolution = system_config.get( "xephyr_resolution") or "640x480" xephyr_command = [ "Xephyr", ":2", "-ac", "-screen", xephyr_resolution + "x" + xephyr_depth, "-glamor", "-reset", "-terminate", ] if system_config.get("xephyr_fullscreen"): xephyr_command.append("-fullscreen") xephyr_thread = MonitoredCommand(xephyr_command) xephyr_thread.start() time.sleep(3) env["DISPLAY"] = ":2" if system_config.get("use_us_layout"): setxkbmap_command = [ "setxkbmap", "-model", "pc101", "us", "-print" ] xkbcomp_command = ["xkbcomp", "-", os.environ.get("DISPLAY", ":0")] xkbcomp = subprocess.Popen(xkbcomp_command, stdin=subprocess.PIPE) subprocess.Popen(setxkbmap_command, env=os.environ, stdout=xkbcomp.stdin).communicate() xkbcomp.communicate() if system_config.get("aco"): env["RADV_PERFTEST"] = "aco" pulse_latency = system_config.get("pulse_latency") if pulse_latency: env["PULSE_LATENCY_MSEC"] = "60" vk_icd = system_config.get("vk_icd") if vk_icd and vk_icd != "off" and system.path_exists(vk_icd): env["VK_ICD_FILENAMES"] = vk_icd fps_limit = system_config.get("fps_limit") or "" if fps_limit: strangle_cmd = system.find_executable("strangle") if strangle_cmd: launch_arguments = [strangle_cmd, fps_limit] + launch_arguments else: logger.warning( "libstrangle is not available on this system, FPS limiter disabled" ) prefix_command = system_config.get("prefix_command") or "" if prefix_command: launch_arguments = ( shlex.split(os.path.expandvars(prefix_command)) + launch_arguments) single_cpu = system_config.get("single_cpu") or False if single_cpu: logger.info("The game will run on a single CPU core") launch_arguments.insert(0, "0") launch_arguments.insert(0, "-c") launch_arguments.insert(0, "taskset") terminal = system_config.get("terminal") if terminal: terminal = system_config.get("terminal_app", system.get_default_terminal()) if terminal and not system.find_executable(terminal): dialogs.ErrorDialog("The selected terminal application " "could not be launched:\n" "%s" % terminal) self.state = self.STATE_STOPPED self.emit("game-stop") return # Env vars game_env = gameplay_info.get("env") or self.runner.get_env() env.update(game_env) env["game_name"] = self.name # Prime vars prime = system_config.get("prime") if prime: env["__NV_PRIME_RENDER_OFFLOAD"] = "1" env["__GLX_VENDOR_LIBRARY_NAME"] = "nvidia" env["__VK_LAYER_NV_optimus"] = "NVIDIA_only" # LD_PRELOAD ld_preload = gameplay_info.get("ld_preload") if ld_preload: env["LD_PRELOAD"] = ld_preload # Feral gamemode gamemode = system_config.get("gamemode") if gamemode: env["LD_PRELOAD"] = ":".join([ path for path in [ env.get("LD_PRELOAD"), "libgamemodeauto.so", ] if path ]) # LD_LIBRARY_PATH game_ld_libary_path = gameplay_info.get("ld_library_path") if game_ld_libary_path: ld_library_path = env.get("LD_LIBRARY_PATH") if not ld_library_path: ld_library_path = "$LD_LIBRARY_PATH" env["LD_LIBRARY_PATH"] = ":".join( [game_ld_libary_path, ld_library_path]) include_processes = shlex.split( system_config.get("include_processes", "")) exclude_processes = shlex.split( system_config.get("exclude_processes", "")) self.game_runtime_config = { "args": launch_arguments, "env": env, "terminal": terminal, "include_processes": include_processes, "exclude_processes": exclude_processes, } if system_config.get("disable_compositor"): self.set_desktop_compositing(False) prelaunch_command = system_config.get("prelaunch_command") if system.path_exists(prelaunch_command): self.prelaunch_executor = MonitoredCommand( [prelaunch_command], include_processes=[os.path.basename(prelaunch_command)], env=self.game_runtime_config["env"], cwd=self.directory, ) self.prelaunch_executor.start() logger.info("Running %s in the background", prelaunch_command) if system_config.get("prelaunch_wait"): self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.prelaunch_beat) else: self.start_game()
class Game(GObject.Object): """This class takes cares of loading the configuration for a game and running it. """ STATE_IDLE = "idle" STATE_STOPPED = "stopped" STATE_RUNNING = "running" __gsignals__ = { "game-error": (GObject.SIGNAL_RUN_FIRST, None, (str, )), "game-start": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-started": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-stop": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-stopped": (GObject.SIGNAL_RUN_FIRST, None, (int, )), "game-removed": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-updated": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-installed": (GObject.SIGNAL_RUN_FIRST, None, ()), } def __init__(self, game_id=None): super().__init__() self.id = game_id # pylint: disable=invalid-name self.runner = None self.config = None # Load attributes from database game_data = pga.get_game_by_field(game_id, "id") self.slug = game_data.get("slug") or "" self.runner_name = game_data.get("runner") or "" self.directory = game_data.get("directory") or "" self.name = game_data.get("name") or "" self.game_config_id = game_data.get("configpath") or "" self.is_installed = bool( game_data.get("installed") and self.game_config_id) self.platform = game_data.get("platform") or "" self.year = game_data.get("year") or "" self.lastplayed = game_data.get("lastplayed") or 0 self.steamid = game_data.get("steamid") or "" self.has_custom_banner = bool(game_data.get("has_custom_banner")) self.has_custom_icon = bool(game_data.get("has_custom_icon")) self.discord_presence = DiscordPresence() try: self.playtime = float(game_data.get("playtime") or 0.0) except ValueError: logger.error("Invalid playtime value %s", game_data.get("playtime")) self.playtime = 0.0 if self.game_config_id: self.load_config() self.game_thread = None self.prelaunch_executor = None self.heartbeat = None self.killswitch = None self.state = self.STATE_IDLE self.game_runtime_config = {} self.resolution_changed = False self.compositor_disabled = False self.stop_compositor = self.start_compositor = "" self.original_outputs = None self._log_buffer = None self.timer = Timer() def __repr__(self): return self.__str__() def __str__(self): value = self.name if self.runner_name: value += " (%s)" % self.runner_name return value @property def log_buffer(self): """Access the log buffer object, creating it if necessary""" if self._log_buffer is None: self._log_buffer = Gtk.TextBuffer() self._log_buffer.create_tag("warning", foreground="red") if self.game_thread: self.game_thread.set_log_buffer(self._log_buffer) self._log_buffer.set_text(self.game_thread.stdout) return self._log_buffer @property def formatted_playtime(self): """Return a human readable formatted play time""" return strings.get_formatted_playtime(self.playtime) @property def is_search_result(self): """Return whether or not the game is a remote game from search results. This is bad, find another way to do this. """ return self.id < 0 @staticmethod def show_error_message(message): """Display an error message based on the runner's output.""" if message["error"] == "CUSTOM": message_text = message["text"].replace("&", "&") dialogs.ErrorDialog(message_text) elif message["error"] == "RUNNER_NOT_INSTALLED": dialogs.ErrorDialog("Error the runner is not installed") elif message["error"] == "NO_BIOS": dialogs.ErrorDialog("A bios file is required to run this game") elif message["error"] == "FILE_NOT_FOUND": filename = message["file"] if filename: message_text = "The file {} could not be found".format( filename.replace("&", "&")) else: message_text = "No file provided" dialogs.ErrorDialog(message_text) elif message["error"] == "NOT_EXECUTABLE": message_text = message["file"].replace("&", "&") dialogs.ErrorDialog("The file %s is not executable" % message_text) def get_browse_dir(self): """Return the path to open with the Browse Files action.""" return self.runner.browse_dir def _get_runner(self): """Return the runner instance for this game's configuration""" try: runner_class = import_runner(self.runner_name) return runner_class(self.config) except InvalidRunner: logger.error("Unable to import runner %s for %s", self.runner_name, self.slug) def load_config(self): """Load the game's configuration.""" if not self.is_installed: return self.config = LutrisConfig(runner_slug=self.runner_name, game_config_id=self.game_config_id) self.runner = self._get_runner() if self.discord_presence.available: self.discord_presence.client_id = ( self.config.system_config.get("discord_client_id") or DEFAULT_DISCORD_CLIENT_ID) self.discord_presence.game_name = ( self.config.system_config.get("discord_custom_game_name") or self.name) self.discord_presence.show_runner = self.config.system_config.get( "discord_show_runner", True) self.discord_presence.runner_name = ( self.config.system_config.get("discord_custom_runner_name") or self.runner_name) self.discord_presence.rpc_enabled = self.config.system_config.get( "discord_rpc_enabled", True) def set_desktop_compositing(self, enable): """Enables or disables compositing""" if enable: system.execute(self.start_compositor, shell=True) else: ( self.start_compositor, self.stop_compositor, ) = get_compositor_commands() if not (self.compositor_disabled or not self.stop_compositor): system.execute(self.stop_compositor, shell=True) self.compositor_disabled = True def remove(self, from_library=False, from_disk=False): """Uninstall a game Params: from_library (bool): Completely remove the game from library, do not set it as uninstalled from_disk (bool): Delete the game files Return: bool: Updated value for from_library """ if from_disk and self.runner: logger.debug("Removing game %s from disk", self.id) self.runner.remove_game_data(game_path=self.directory) # Do not keep multiple copies of the same game existing_games = pga.get_games_where(slug=self.slug) if len(existing_games) > 1: from_library = True if from_library: logger.debug("Removing game %s from library", self.id) pga.delete_game(self.id) else: pga.set_uninstalled(self.id) if self.config: self.config.remove() xdgshortcuts.remove_launcher(self.slug, self.id, desktop=True, menu=True) self.is_installed = False self.emit("game-removed") return from_library def set_platform_from_runner(self): """Set the game's platform from the runner""" if not self.runner: logger.warning("Game has no runner, can't set platform") return self.platform = self.runner.get_platform() if not self.platform: logger.warning("Can't get platform for runner %s", self.runner.human_name) def save(self, metadata_only=False): """ Save the game's config and metadata, if `metadata_only` is set to True, do not save the config. This is useful when exiting the game since the config might have changed and we don't want to override the changes. """ logger.debug("Saving %s", self) if not metadata_only: self.config.save() self.set_platform_from_runner() self.id = pga.add_or_update( name=self.name, runner=self.runner_name, slug=self.slug, platform=self.platform, year=self.year, lastplayed=self.lastplayed, directory=self.directory, installed=self.is_installed, configpath=self.config.game_config_id, steamid=self.steamid, id=self.id, playtime=self.playtime, ) self.emit("game-updated") def is_launchable(self): """Verify that the current game can be launched.""" if not self.runner.is_installed(): installed = self.runner.install_dialog() if not installed: return False if self.runner.use_runtime(): runtime_updater = runtime.RuntimeUpdater() if runtime_updater.is_updating(): logger.warning("Runtime updates: %s", runtime_updater.current_updates) dialogs.ErrorDialog("Runtime currently updating", "Game might not work as expected") if ("wine" in self.runner_name and not wine.get_system_wine_version() and not LINUX_SYSTEM.is_flatpak): # TODO find a reference to the root window or better yet a way not # to have Gtk dependent code in this class. root_window = None dialogs.WineNotInstalledWarning(parent=root_window) return True def play(self): """Launch the game.""" if not self.runner: dialogs.ErrorDialog("Invalid game configuration: Missing runner") self.state = self.STATE_STOPPED self.emit("game-stop") return if not self.is_launchable(): self.state = self.STATE_STOPPED self.emit("game-stop") return self.emit("game-start") jobs.AsyncCall(self.runner.prelaunch, self.configure_game) @watch_lutris_errors def configure_game(self, prelaunched, error=None): """Get the game ready to start, applying all the options This methods sets the game_runtime_config attribute. """ if error: logger.error(error) dialogs.ErrorDialog(str(error)) if not prelaunched: logger.error("Game prelaunch unsuccessful") dialogs.ErrorDialog("An error prevented the game from running") self.state = self.STATE_STOPPED self.emit("game-stop") return system_config = self.runner.system_config self.original_outputs = DISPLAY_MANAGER.get_config() gameplay_info = self.runner.play() if "error" in gameplay_info: self.show_error_message(gameplay_info) self.state = self.STATE_STOPPED self.emit("game-stop") return logger.debug("Launching %s: %s", self.name, gameplay_info) logger.debug("Game info: %s", json.dumps(gameplay_info, indent=2)) env = {} sdl_gamecontrollerconfig = system_config.get( "sdl_gamecontrollerconfig") if sdl_gamecontrollerconfig: path = os.path.expanduser(sdl_gamecontrollerconfig) if system.path_exists(path): with open(path, "r") as controllerdb_file: sdl_gamecontrollerconfig = controllerdb_file.read() env["SDL_GAMECONTROLLERCONFIG"] = sdl_gamecontrollerconfig sdl_video_fullscreen = system_config.get("sdl_video_fullscreen") or "" env["SDL_VIDEO_FULLSCREEN_DISPLAY"] = sdl_video_fullscreen restrict_to_display = system_config.get("display") if restrict_to_display != "off": if restrict_to_display == "primary": restrict_to_display = None for output in self.original_outputs: if output.primary: restrict_to_display = output.name break if not restrict_to_display: logger.warning("No primary display set") else: found = False for output in self.original_outputs: if output.name == restrict_to_display: found = True break if not found: logger.warning("Selected display %s not found", restrict_to_display) restrict_to_display = None if restrict_to_display: turn_off_except(restrict_to_display) time.sleep(3) self.resolution_changed = True resolution = system_config.get("resolution") if resolution != "off": DISPLAY_MANAGER.set_resolution(resolution) time.sleep(3) self.resolution_changed = True if system_config.get("reset_pulse"): audio.reset_pulse() self.killswitch = system_config.get("killswitch") if self.killswitch and not system.path_exists(self.killswitch): # Prevent setting a killswitch to a file that doesn't exists self.killswitch = None # Command launch_arguments = gameplay_info["command"] optimus = system_config.get("optimus") if optimus == "primusrun" and system.find_executable("primusrun"): launch_arguments.insert(0, "primusrun") elif optimus == "optirun" and system.find_executable("optirun"): launch_arguments.insert(0, "virtualgl") launch_arguments.insert(0, "-b") launch_arguments.insert(0, "optirun") elif optimus == "pvkrun" and system.find_executable("pvkrun"): launch_arguments.insert(0, "pvkrun") xephyr = system_config.get("xephyr") or "off" if xephyr != "off": if not system.find_executable("Xephyr"): raise GameConfigError( "Unable to find Xephyr, install it or disable the Xephyr option" ) xephyr_depth = "8" if xephyr == "8bpp" else "16" xephyr_resolution = system_config.get( "xephyr_resolution") or "640x480" xephyr_command = [ "Xephyr", ":2", "-ac", "-screen", xephyr_resolution + "x" + xephyr_depth, "-glamor", "-reset", "-terminate", ] if system_config.get("xephyr_fullscreen"): xephyr_command.append("-fullscreen") xephyr_thread = MonitoredCommand(xephyr_command) xephyr_thread.start() time.sleep(3) env["DISPLAY"] = ":2" if system_config.get("use_us_layout"): setxkbmap_command = [ "setxkbmap", "-model", "pc101", "us", "-print" ] xkbcomp_command = ["xkbcomp", "-", os.environ.get("DISPLAY", ":0")] xkbcomp = subprocess.Popen(xkbcomp_command, stdin=subprocess.PIPE) subprocess.Popen(setxkbmap_command, env=os.environ, stdout=xkbcomp.stdin).communicate() xkbcomp.communicate() if system_config.get("aco"): env["RADV_PERFTEST"] = "aco" pulse_latency = system_config.get("pulse_latency") if pulse_latency: env["PULSE_LATENCY_MSEC"] = "60" vk_icd = system_config.get("vk_icd") if vk_icd and vk_icd != "off" and system.path_exists(vk_icd): env["VK_ICD_FILENAMES"] = vk_icd fps_limit = system_config.get("fps_limit") or "" if fps_limit: strangle_cmd = system.find_executable("strangle") if strangle_cmd: launch_arguments = [strangle_cmd, fps_limit] + launch_arguments else: logger.warning( "libstrangle is not available on this system, FPS limiter disabled" ) prefix_command = system_config.get("prefix_command") or "" if prefix_command: launch_arguments = ( shlex.split(os.path.expandvars(prefix_command)) + launch_arguments) single_cpu = system_config.get("single_cpu") or False if single_cpu: logger.info("The game will run on a single CPU core") launch_arguments.insert(0, "0") launch_arguments.insert(0, "-c") launch_arguments.insert(0, "taskset") terminal = system_config.get("terminal") if terminal: terminal = system_config.get("terminal_app", system.get_default_terminal()) if terminal and not system.find_executable(terminal): dialogs.ErrorDialog("The selected terminal application " "could not be launched:\n" "%s" % terminal) self.state = self.STATE_STOPPED self.emit("game-stop") return # Env vars game_env = gameplay_info.get("env") or self.runner.get_env() env.update(game_env) env["game_name"] = self.name # Prime vars prime = system_config.get("prime") if prime: env["__NV_PRIME_RENDER_OFFLOAD"] = "1" env["__GLX_VENDOR_LIBRARY_NAME"] = "nvidia" env["__VK_LAYER_NV_optimus"] = "NVIDIA_only" # LD_PRELOAD ld_preload = gameplay_info.get("ld_preload") if ld_preload: env["LD_PRELOAD"] = ld_preload # Feral gamemode gamemode = system_config.get("gamemode") if gamemode: env["LD_PRELOAD"] = ":".join([ path for path in [ env.get("LD_PRELOAD"), "libgamemodeauto.so", ] if path ]) # LD_LIBRARY_PATH game_ld_libary_path = gameplay_info.get("ld_library_path") if game_ld_libary_path: ld_library_path = env.get("LD_LIBRARY_PATH") if not ld_library_path: ld_library_path = "$LD_LIBRARY_PATH" env["LD_LIBRARY_PATH"] = ":".join( [game_ld_libary_path, ld_library_path]) include_processes = shlex.split( system_config.get("include_processes", "")) exclude_processes = shlex.split( system_config.get("exclude_processes", "")) self.game_runtime_config = { "args": launch_arguments, "env": env, "terminal": terminal, "include_processes": include_processes, "exclude_processes": exclude_processes, } if system_config.get("disable_compositor"): self.set_desktop_compositing(False) prelaunch_command = system_config.get("prelaunch_command") if system.path_exists(prelaunch_command): self.prelaunch_executor = MonitoredCommand( [prelaunch_command], include_processes=[os.path.basename(prelaunch_command)], env=self.game_runtime_config["env"], cwd=self.directory, ) self.prelaunch_executor.start() logger.info("Running %s in the background", prelaunch_command) if system_config.get("prelaunch_wait"): self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.prelaunch_beat) else: self.start_game() def start_game(self): """Run a background command to lauch the game""" self.game_thread = MonitoredCommand( self.game_runtime_config["args"], title=self.name, runner=self.runner, env=self.game_runtime_config["env"], term=self.game_runtime_config["terminal"], log_buffer=self._log_buffer, include_processes=self.game_runtime_config["include_processes"], exclude_processes=self.game_runtime_config["exclude_processes"], ) if hasattr(self.runner, "stop"): self.game_thread.stop_func = self.runner.stop self.game_thread.start() self.timer.start() self.emit("game-started") self.state = self.STATE_RUNNING self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.beat) def stop_game(self): """Cleanup after a game as stopped""" self.state = self.STATE_STOPPED self.emit("game-stop") if not self.timer.finished: self.timer.end() self.playtime += self.timer.duration / 3600 def prelaunch_beat(self): """Watch the prelaunch command""" if self.prelaunch_executor and self.prelaunch_executor.is_running: return True self.start_game() return False def beat(self): """Watch the game's process(es).""" if self.game_thread.error: dialogs.ErrorDialog("<b>Error lauching the game:</b>\n" + self.game_thread.error) self.on_game_quit() return False # The killswitch file should be set to a device (ie. /dev/input/js0) # When that device is unplugged, the game is forced to quit. killswitch_engage = self.killswitch and not system.path_exists( self.killswitch) if not self.game_thread.is_running or killswitch_engage: logger.debug("Game thread stopped") self.on_game_quit() return False if self.discord_presence.available: self.discord_presence.update_discord_rich_presence() return True def stop(self): """Stops the game""" if self.state == self.STATE_STOPPED: logger.debug("Game already stopped") return logger.info("Stopping %s", self) if self.game_thread: jobs.AsyncCall(self.game_thread.stop, None) self.stop_game() def on_game_quit(self): """Restore some settings and cleanup after game quit.""" if self.prelaunch_executor and self.prelaunch_executor.is_running: logger.info("Stopping prelaunch script") self.prelaunch_executor.stop() self.heartbeat = None if self.state != self.STATE_STOPPED: logger.warning("Game still running (state: %s)", self.state) self.stop() # Check for post game script postexit_command = self.runner.system_config.get("postexit_command") if system.path_exists(postexit_command): logger.info("Running post-exit command: %s", postexit_command) postexit_thread = MonitoredCommand( [postexit_command], include_processes=[os.path.basename(postexit_command)], env=self.game_runtime_config["env"], cwd=self.directory, ) postexit_thread.start() if self.discord_presence.available: self.discord_presence.clear_discord_rich_presence() quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("%s stopped at %s", self.name, quit_time) self.lastplayed = int(time.time()) self.save(metadata_only=True) os.chdir(os.path.expanduser("~")) if self.resolution_changed or self.runner.system_config.get( "reset_desktop"): DISPLAY_MANAGER.set_resolution(self.original_outputs) if self.compositor_disabled: self.set_desktop_compositing(True) if self.runner.system_config.get("use_us_layout"): subprocess.Popen(["setxkbmap"], env=os.environ).communicate() if self.runner.system_config.get("restore_gamma"): restore_gamma() self.process_return_codes() def process_return_codes(self): """Do things depending on how the game quitted.""" if self.game_thread.return_code == 127: # Error missing shared lib error = "error while loading shared lib" error_line = strings.lookup_string_in_text(error, self.game_thread.stdout) if error_line: dialogs.ErrorDialog("<b>Error: Missing shared library.</b>" "\n\n%s" % error_line) if self.game_thread.return_code == 1: # Error Wine version conflict error = "maybe the wrong wineserver" if strings.lookup_string_in_text(error, self.game_thread.stdout): dialogs.ErrorDialog("<b>Error: A different Wine version is " "already using the same Wine prefix.</b>") def notify_steam_game_changed(self, appmanifest): """Receive updates from Steam games and set the thread's ready state accordingly""" if not self.game_thread: return if "Fully Installed" in appmanifest.states and not self.game_thread.ready_state: logger.info("Steam game %s is fully installed", appmanifest.steamid) self.game_thread.ready_state = True elif "Update Required" in appmanifest.states and self.game_thread.ready_state: logger.info( "Steam game %s updating, setting game thread as not ready", appmanifest.steamid, ) self.game_thread.ready_state = False
class Game(GObject.Object): """This class takes cares of loading the configuration for a game and running it. """ STATE_STOPPED = "stopped" STATE_LAUNCHING = "launching" STATE_RUNNING = "running" __gsignals__ = { "game-error": (GObject.SIGNAL_RUN_FIRST, None, (str, )), "game-launch": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-start": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-started": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-stop": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-stopped": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-removed": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-updated": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-install": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-installed": (GObject.SIGNAL_RUN_FIRST, None, ()), } def __init__(self, game_id=None): super().__init__() self.id = game_id # pylint: disable=invalid-name self.runner = None self.config = None # Load attributes from database game_data = games_db.get_game_by_field(game_id, "id") self.slug = game_data.get("slug") or "" self.runner_name = game_data.get("runner") or "" self.directory = game_data.get("directory") or "" self.name = game_data.get("name") or "" self.game_config_id = game_data.get("configpath") or "" self.is_installed = bool(game_data.get("installed") and self.game_config_id) self.is_hidden = bool(game_data.get("hidden")) self.platform = game_data.get("platform") or "" self.year = game_data.get("year") or "" self.lastplayed = game_data.get("lastplayed") or 0 self.has_custom_banner = bool(game_data.get("has_custom_banner")) self.has_custom_icon = bool(game_data.get("has_custom_icon")) self.service = game_data.get("service") self.appid = game_data.get("service_id") self.discord_presence = DiscordPresence() self.playtime = game_data.get("playtime") or 0.0 if self.game_config_id: self.load_config() self.game_thread = None self.prelaunch_executor = None self.heartbeat = None self.killswitch = None self.state = self.STATE_STOPPED self.game_runtime_config = {} self.resolution_changed = False self.compositor_disabled = False self.original_outputs = None self._log_buffer = None self.timer = Timer() self.screen_saver_inhibitor_cookie = None def __repr__(self): return self.__str__() def __str__(self): value = self.name if self.runner_name: value += " (%s)" % self.runner_name return value @property def is_favorite(self): """Return whether the game is in the user's favorites""" categories = categories_db.get_categories_in_game(self.id) for category in categories: if category == "favorite": return True return False def add_to_favorites(self): """Add the game to the 'favorite' category""" favorite = categories_db.get_category("favorite") if not favorite: favorite = categories_db.add_category("favorite") categories_db.add_game_to_category(self.id, favorite["id"]) self.emit("game-updated") def remove_from_favorites(self): """Remove game from favorites""" favorite = categories_db.get_category("favorite") categories_db.remove_category_from_game(self.id, favorite["id"]) self.emit("game-updated") def hide(self): """Do not show this game in the UI""" self.is_hidden = True self.save() def unhide(self): """Remove the game from hidden games""" self.is_hidden = False self.save() @property def log_buffer(self): """Access the log buffer object, creating it if necessary""" _log_buffer = LOG_BUFFERS.get(self.id) if _log_buffer: return _log_buffer _log_buffer = Gtk.TextBuffer() _log_buffer.create_tag("warning", foreground="red") if self.game_thread: self.game_thread.set_log_buffer(self._log_buffer) _log_buffer.set_text(self.game_thread.stdout) LOG_BUFFERS[self.id] = _log_buffer return _log_buffer @property def formatted_playtime(self): """Return a human readable formatted play time""" return strings.get_formatted_playtime(self.playtime) @staticmethod def show_error_message(message): """Display an error message based on the runner's output.""" if message["error"] == "CUSTOM": message_text = message["text"].replace("&", "&") dialogs.ErrorDialog(message_text) elif message["error"] == "RUNNER_NOT_INSTALLED": dialogs.ErrorDialog(_("Error the runner is not installed")) elif message["error"] == "NO_BIOS": dialogs.ErrorDialog(_("A bios file is required to run this game")) elif message["error"] == "FILE_NOT_FOUND": filename = message["file"] if filename: message_text = _("The file {} could not be found").format(filename.replace("&", "&")) else: message_text = _("No file provided") dialogs.ErrorDialog(message_text) elif message["error"] == "NOT_EXECUTABLE": message_text = message["file"].replace("&", "&") dialogs.ErrorDialog(_("The file %s is not executable") % message_text) elif message["error"] == "PATH_NOT_SET": message_text = _("The path '%s' is not set. please set it in the options.") % message["path"] dialogs.ErrorDialog(message_text) else: dialogs.ErrorDialog(_("Unhandled error: %s") % message["error"]) def get_browse_dir(self): """Return the path to open with the Browse Files action.""" return self.runner.game_path def _get_runner(self): """Return the runner instance for this game's configuration""" try: runner_class = import_runner(self.runner_name) return runner_class(self.config) except InvalidRunner: logger.error("Unable to import runner %s for %s", self.runner_name, self.slug) def load_config(self): """Load the game's configuration.""" if not self.is_installed: return self.config = LutrisConfig(runner_slug=self.runner_name, game_config_id=self.game_config_id) self.runner = self._get_runner() if self.discord_presence.available: self.discord_presence.client_id = ( self.config.system_config.get("discord_client_id") or DEFAULT_DISCORD_CLIENT_ID ) self.discord_presence.game_name = (self.config.system_config.get("discord_custom_game_name") or self.name) self.discord_presence.show_runner = self.config.system_config.get("discord_show_runner", True) self.discord_presence.runner_name = ( self.config.system_config.get("discord_custom_runner_name") or self.runner_name ) self.discord_presence.rpc_enabled = self.config.system_config.get("discord_rpc_enabled", True) def set_desktop_compositing(self, enable): """Enables or disables compositing""" if enable: if self.compositor_disabled: enable_compositing() self.compositor_disabled = False else: if not self.compositor_disabled: disable_compositing() self.compositor_disabled = True def remove(self, delete_files=False): """Uninstall a game Params: delete_files (bool): Delete the game files """ if delete_files and self.runner: self.runner.remove_game_data(game_path=self.directory) games_db.set_uninstalled(self.id) if self.config: self.config.remove() xdgshortcuts.remove_launcher(self.slug, self.id, desktop=True, menu=True) self.is_installed = False self.emit("game-removed") def set_platform_from_runner(self): """Set the game's platform from the runner""" if not self.runner: logger.warning("Game has no runner, can't set platform") return self.platform = self.runner.get_platform() if not self.platform: logger.warning("Can't get platform for runner %s", self.runner.human_name) def save(self, save_config=False): """ Save the game's config and metadata, if `save_config` is set to False, do not save the config. This is useful when exiting the game since the config might have changed and we don't want to override the changes. """ if self.config: logger.debug("Saving %s with config ID %s", self, self.config.game_config_id) configpath = self.config.game_config_id if save_config: self.config.save() else: logger.warning("Saving %s without a configuration", self) configpath = "" self.set_platform_from_runner() self.id = games_db.add_or_update( name=self.name, runner=self.runner_name, slug=self.slug, platform=self.platform, directory=self.directory, installed=self.is_installed, year=self.year, lastplayed=self.lastplayed, configpath=configpath, id=self.id, playtime=self.playtime, hidden=self.is_hidden, service=self.service, service_id=self.appid, ) self.emit("game-updated") def is_launchable(self): """Verify that the current game can be launched.""" if not self.runner.is_installed(): installed = self.runner.install_dialog() if not installed: return False if self.runner.use_runtime(): runtime_updater = runtime.RuntimeUpdater() if runtime_updater.is_updating(): logger.warning("Runtime updates: %s", runtime_updater.current_updates) dialogs.ErrorDialog(_("Runtime currently updating"), _("Game might not work as expected")) if ("wine" in self.runner_name and not wine.get_system_wine_version() and not LINUX_SYSTEM.is_flatpak): # TODO find a reference to the root window or better yet a way not # to have Gtk dependent code in this class. root_window = None dialogs.WineNotInstalledWarning(parent=root_window) return True def restrict_to_display(self, display): outputs = DISPLAY_MANAGER.get_config() if display == "primary": display = None for output in outputs: if output.primary: display = output.name break if not display: logger.warning("No primary display set") else: found = False for output in outputs: if output.name == display: found = True break if not found: logger.warning("Selected display %s not found", display) display = None if display: turn_off_except(display) time.sleep(3) return True return False def start_xephyr(self, display=":2"): """Start a monitored Xephyr instance""" if not system.find_executable("Xephyr"): raise GameConfigError("Unable to find Xephyr, install it or disable the Xephyr option") xephyr_command = get_xephyr_command(display, self.runner.system_config) xephyr_thread = MonitoredCommand(xephyr_command) xephyr_thread.start() time.sleep(3) return display @staticmethod def set_keyboard_layout(layout): setxkbmap_command = ["setxkbmap", "-model", "pc101", layout, "-print"] xkbcomp_command = ["xkbcomp", "-", os.environ.get("DISPLAY", ":0")] xkbcomp = subprocess.Popen(xkbcomp_command, stdin=subprocess.PIPE) subprocess.Popen(setxkbmap_command, env=os.environ, stdout=xkbcomp.stdin).communicate() xkbcomp.communicate() def start_prelaunch_command(self): """Start the prelaunch command specified in the system options""" prelaunch_command = self.runner.system_config.get("prelaunch_command") command_array = shlex.split(prelaunch_command) if not system.path_exists(command_array[0]): logger.warning("Command %s not found", command_array[0]) return self.prelaunch_executor = MonitoredCommand( command_array, include_processes=[os.path.basename(command_array[0])], env=self.game_runtime_config["env"], cwd=self.directory, ) self.prelaunch_executor.start() logger.info("Running %s in the background", prelaunch_command) def get_terminal(self): """Return the terminal used to run the game into or None if the game is not run from a terminal. Remember that only games using text mode should use the terminal. """ if self.runner.system_config.get("terminal"): terminal = self.runner.system_config.get("terminal_app", system.get_default_terminal()) if terminal and not system.find_executable(terminal): raise GameConfigError(_("The selected terminal application could not be launched:\n%s") % terminal) return terminal def get_killswitch(self): """Return the path to a file that is monitored during game execution. If the file stops existing, the game is stopped. """ killswitch = self.runner.system_config.get("killswitch") # Prevent setting a killswitch to a file that doesn't exists if killswitch and system.path_exists(self.killswitch): return killswitch def get_gameplay_info(self): """Return the information provided by a runner's play method. Checks for possible errors. """ gameplay_info = self.runner.play() if "error" in gameplay_info: self.show_error_message(gameplay_info) self.state = self.STATE_STOPPED self.emit("game-stop") return return gameplay_info @watch_lutris_errors def configure_game(self, prelaunched, error=None): # noqa: C901 """Get the game ready to start, applying all the options This methods sets the game_runtime_config attribute. """ if error: logger.error(error) dialogs.ErrorDialog(str(error)) if not prelaunched: logger.error("Game prelaunch unsuccessful") dialogs.ErrorDialog(_("An error prevented the game from running")) self.state = self.STATE_STOPPED self.emit("game-stop") return gameplay_info = self.get_gameplay_info() if not gameplay_info: return command, env = get_launch_parameters(self.runner, gameplay_info) env["game_name"] = self.name # What is this used for?? self.game_runtime_config = { "args": command, "env": env, "terminal": self.get_terminal(), "include_processes": shlex.split(self.runner.system_config.get("include_processes", "")), "exclude_processes": shlex.split(self.runner.system_config.get("exclude_processes", "")), } # Audio control if self.runner.system_config.get("reset_pulse"): audio.reset_pulse() # Input control if self.runner.system_config.get("use_us_layout"): self.set_keyboard_layout("us") # Display control self.original_outputs = DISPLAY_MANAGER.get_config() if self.runner.system_config.get("disable_compositor"): self.set_desktop_compositing(False) if self.runner.system_config.get("disable_screen_saver"): self.screen_saver_inhibitor_cookie = SCREEN_SAVER_INHIBITOR.inhibit(self.name) if self.runner.system_config.get("display") != "off": self.resolution_changed = self.restrict_to_display(self.runner.system_config.get("display")) resolution = self.runner.system_config.get("resolution") if resolution != "off": DISPLAY_MANAGER.set_resolution(resolution) time.sleep(3) self.resolution_changed = True xephyr = self.runner.system_config.get("xephyr") or "off" if xephyr != "off": env["DISPLAY"] = self.start_xephyr() # Execution control self.killswitch = self.get_killswitch() if self.runner.system_config.get("prelaunch_command"): self.start_prelaunch_command() if self.runner.system_config.get("prelaunch_wait"): # Monitor the prelaunch command and wait until it has finished self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.prelaunch_beat) else: self.start_game() def launch(self): """Request launching a game. The game may not be installed yet.""" if not self.is_installed: self.emit("game-install") return wait_for_dxvk_init() self.load_config() # Reload the config before launching it. if not self.runner: dialogs.ErrorDialog(_("Invalid game configuration: Missing runner")) return if not self.is_launchable(): logger.error("Game is not launchable") return self.state = self.STATE_LAUNCHING self.emit("game-start") jobs.AsyncCall(self.runner.prelaunch, self.configure_game) def start_game(self): """Run a background command to lauch the game""" self.game_thread = MonitoredCommand( self.game_runtime_config["args"], title=self.name, runner=self.runner, env=self.game_runtime_config["env"], term=self.game_runtime_config["terminal"], log_buffer=self.log_buffer, include_processes=self.game_runtime_config["include_processes"], exclude_processes=self.game_runtime_config["exclude_processes"], ) if hasattr(self.runner, "stop"): self.game_thread.stop_func = self.runner.stop self.game_thread.start() self.timer.start() self.state = self.STATE_RUNNING self.emit("game-started") self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.beat) def stop_game(self): """Cleanup after a game as stopped""" self.state = self.STATE_STOPPED self.emit("game-stop") if not self.timer.finished: self.timer.end() self.playtime += self.timer.duration / 3600 def prelaunch_beat(self): """Watch the prelaunch command""" if self.prelaunch_executor and self.prelaunch_executor.is_running: return True self.start_game() return False def beat(self): """Watch the game's process(es).""" if self.game_thread.error: dialogs.ErrorDialog(_("<b>Error lauching the game:</b>\n") + self.game_thread.error) self.on_game_quit() return False # The killswitch file should be set to a device (ie. /dev/input/js0) # When that device is unplugged, the game is forced to quit. killswitch_engage = self.killswitch and not system.path_exists(self.killswitch) if not self.game_thread.is_running or killswitch_engage: logger.debug("Game thread stopped") self.on_game_quit() return False if self.discord_presence.available: self.discord_presence.update_discord_rich_presence() return True def stop(self): """Stops the game""" if self.state == self.STATE_STOPPED: logger.debug("Game already stopped") return logger.info("Stopping %s", self) if self.game_thread: jobs.AsyncCall(self.game_thread.stop, None) self.stop_game() def on_game_quit(self): """Restore some settings and cleanup after game quit.""" if self.prelaunch_executor and self.prelaunch_executor.is_running: logger.info("Stopping prelaunch script") self.prelaunch_executor.stop() self.heartbeat = None if self.state != self.STATE_STOPPED: logger.warning("Game still running (state: %s)", self.state) self.stop() # Check for post game script postexit_command = self.runner.system_config.get("postexit_command") if postexit_command: command_array = shlex.split(postexit_command) if system.path_exists(command_array[0]): logger.info("Running post-exit command: %s", postexit_command) postexit_thread = MonitoredCommand( command_array, include_processes=[os.path.basename(postexit_command)], env=self.game_runtime_config["env"], cwd=self.directory, ) postexit_thread.start() if self.discord_presence.available: self.discord_presence.clear_discord_rich_presence() quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("%s stopped at %s", self.name, quit_time) self.lastplayed = int(time.time()) self.save(save_config=False) os.chdir(os.path.expanduser("~")) if self.resolution_changed or self.runner.system_config.get("reset_desktop"): DISPLAY_MANAGER.set_resolution(self.original_outputs) if self.compositor_disabled: self.set_desktop_compositing(True) if self.screen_saver_inhibitor_cookie is not None: SCREEN_SAVER_INHIBITOR.uninhibit(self.screen_saver_inhibitor_cookie) self.screen_saver_inhibitor_cookie = None if self.runner.system_config.get("use_us_layout"): subprocess.Popen(["setxkbmap"], env=os.environ).communicate() if self.runner.system_config.get("restore_gamma"): restore_gamma() self.process_return_codes() def process_return_codes(self): """Do things depending on how the game quitted.""" if self.game_thread.return_code == 127: # Error missing shared lib error = "error while loading shared lib" error_line = strings.lookup_string_in_text(error, self.game_thread.stdout) if error_line: dialogs.ErrorDialog(_("<b>Error: Missing shared library.</b>\n\n%s") % error_line) if self.game_thread.return_code == 1: # Error Wine version conflict error = "maybe the wrong wineserver" if strings.lookup_string_in_text(error, self.game_thread.stdout): dialogs.ErrorDialog(_("<b>Error: A different Wine version is already using the same Wine prefix.</b>")) def write_script(self, script_path): """Output the launch argument in a bash script""" gameplay_info = self.get_gameplay_info() if not gameplay_info: return export_bash_script(self.runner, gameplay_info, script_path) def move(self, new_location): logger.info("Moving %s to %s", self, new_location) new_config = "" old_location = self.directory if old_location: game_directory = os.path.basename(old_location) target_directory = os.path.join(new_location, game_directory) else: target_directory = new_location self.directory = target_directory self.save() if not old_location: logger.info("Previous location wasn't set. Cannot continue moving") return target_directory with open(self.config.game_config_path) as config_file: for line in config_file.readlines(): if target_directory in line: new_config += line else: new_config += line.replace(old_location, target_directory) with open(self.config.game_config_path, "w") as config_file: config_file.write(new_config) if not system.path_exists(old_location): logger.warning("Location %s doesn't exist, files already moved?", old_location) return if new_location.startswith(old_location): logger.warning("Can't move %s to one of its children %s", old_location, new_location) return target_directory try: shutil.move(old_location, new_location) except OSError as ex: logger.error( "Failed to move %s to %s, you may have to move files manually (Exception: %s)", old_location, new_location, ex ) return target_directory
def configure_game(self, prelaunched, error=None): """Get the game ready to start, applying all the options This methods sets the game_runtime_config attribute. """ if error: logger.error(error) dialogs.ErrorDialog(str(error)) if not prelaunched: logger.error("Game prelaunch unsuccessful") dialogs.ErrorDialog("An error prevented the game from running") self.state = self.STATE_STOPPED self.emit('game-stop') return system_config = self.runner.system_config self.original_outputs = sorted( display.get_outputs(), key=lambda e: e.name == system_config.get("display") ) gameplay_info = self.runner.play() if "error" in gameplay_info: self.show_error_message(gameplay_info) self.state = self.STATE_STOPPED self.emit('game-stop') return logger.debug("Launching %s: %s", self.name, gameplay_info) logger.debug("Game info: %s", json.dumps(gameplay_info, indent=2)) env = {} sdl_gamecontrollerconfig = system_config.get("sdl_gamecontrollerconfig") if sdl_gamecontrollerconfig: path = os.path.expanduser(sdl_gamecontrollerconfig) if system.path_exists(path): with open(path, "r") as controllerdb_file: sdl_gamecontrollerconfig = controllerdb_file.read() env["SDL_GAMECONTROLLERCONFIG"] = sdl_gamecontrollerconfig sdl_video_fullscreen = system_config.get("sdl_video_fullscreen") or "" env["SDL_VIDEO_FULLSCREEN_DISPLAY"] = sdl_video_fullscreen restrict_to_display = system_config.get("display") if restrict_to_display != "off": if restrict_to_display == "primary": restrict_to_display = None for output in self.original_outputs: if output.primary: restrict_to_display = output.name break if not restrict_to_display: logger.warning("No primary display set") else: found = False for output in self.original_outputs: if output.name == restrict_to_display: found = True break if not found: logger.warning("Selected display %s not found", restrict_to_display) restrict_to_display = None if restrict_to_display: display.turn_off_except(restrict_to_display) time.sleep(3) self.resolution_changed = True resolution = system_config.get("resolution") if resolution != "off": display.change_resolution(resolution) time.sleep(3) self.resolution_changed = True if system_config.get("reset_pulse"): audio.reset_pulse() self.killswitch = system_config.get("killswitch") if self.killswitch and not system.path_exists(self.killswitch): # Prevent setting a killswitch to a file that doesn't exists self.killswitch = None # Command launch_arguments = gameplay_info["command"] optimus = system_config.get("optimus") if optimus == "primusrun" and system.find_executable("primusrun"): launch_arguments.insert(0, "primusrun") elif optimus == "optirun" and system.find_executable("optirun"): launch_arguments.insert(0, "virtualgl") launch_arguments.insert(0, "-b") launch_arguments.insert(0, "optirun") elif optimus == "pvkrun" and system.find_executable("pvkrun"): launch_arguments.insert(0, "pvkrun") xephyr = system_config.get("xephyr") or "off" if xephyr != "off": if not system.find_executable("Xephyr"): raise GameConfigError( "Unable to find Xephyr, install it or disable the Xephyr option" ) xephyr_depth = "8" if xephyr == "8bpp" else "16" xephyr_resolution = system_config.get("xephyr_resolution") or "640x480" xephyr_command = [ "Xephyr", ":2", "-ac", "-screen", xephyr_resolution + "x" + xephyr_depth, "-glamor", "-reset", "-terminate" ] if system_config.get("xephyr_fullscreen"): xephyr_command.append("-fullscreen") xephyr_thread = MonitoredCommand(xephyr_command) xephyr_thread.start() time.sleep(3) env["DISPLAY"] = ":2" if system_config.get("use_us_layout"): setxkbmap_command = ["setxkbmap", "-model", "pc101", "us", "-print"] xkbcomp_command = ["xkbcomp", "-", os.environ.get("DISPLAY", ":0")] xkbcomp = subprocess.Popen(xkbcomp_command, stdin=subprocess.PIPE) subprocess.Popen( setxkbmap_command, env=os.environ, stdout=xkbcomp.stdin ).communicate() xkbcomp.communicate() pulse_latency = system_config.get("pulse_latency") if pulse_latency: env["PULSE_LATENCY_MSEC"] = "60" vk_icd = system_config.get("vk_icd") if vk_icd and vk_icd != "off" and system.path_exists(vk_icd): env["VK_ICD_FILENAMES"] = vk_icd fps_limit = system_config.get("fps_limit") or "" if fps_limit: strangle_cmd = system.find_executable("strangle") launch_arguments = [strangle_cmd, fps_limit] + launch_arguments prefix_command = system_config.get("prefix_command") or "" if prefix_command: launch_arguments = ( shlex.split(os.path.expandvars(prefix_command)) + launch_arguments ) single_cpu = system_config.get("single_cpu") or False if single_cpu: logger.info("The game will run on a single CPU core") launch_arguments.insert(0, "0") launch_arguments.insert(0, "-c") launch_arguments.insert(0, "taskset") terminal = system_config.get("terminal") if terminal: terminal = system_config.get("terminal_app", system.get_default_terminal()) if terminal and not system.find_executable(terminal): dialogs.ErrorDialog( "The selected terminal application " "could not be launched:\n" "%s" % terminal ) self.state = self.STATE_STOPPED self.emit('game-stop') return # Env vars game_env = gameplay_info.get("env") or self.runner.get_env() env.update(game_env) # LD_PRELOAD ld_preload = gameplay_info.get("ld_preload") if ld_preload: env["LD_PRELOAD"] = ld_preload # Feral gamemode gamemode = system_config.get("gamemode") if gamemode: env["LD_PRELOAD"] = ":".join( [ path for path in [ env.get("LD_PRELOAD"), "/usr/$LIB/libgamemodeauto.so", ] if path ] ) # LD_LIBRARY_PATH game_ld_libary_path = gameplay_info.get("ld_library_path") if game_ld_libary_path: ld_library_path = env.get("LD_LIBRARY_PATH") if not ld_library_path: ld_library_path = "$LD_LIBRARY_PATH" env["LD_LIBRARY_PATH"] = ":".join([game_ld_libary_path, ld_library_path]) include_processes = shlex.split(system_config.get("include_processes", "")) exclude_processes = shlex.split(system_config.get("exclude_processes", "")) self.game_runtime_config = { "args": launch_arguments, "env": env, "terminal": terminal, "include_processes": include_processes, "exclude_processes": exclude_processes } if system_config.get("disable_compositor"): self.set_desktop_compositing(False) # xboxdrv setup xboxdrv_config = system_config.get("xboxdrv") if xboxdrv_config: self.xboxdrv_start(xboxdrv_config) prelaunch_command = system_config.get("prelaunch_command") if system.path_exists(prelaunch_command): self.prelaunch_executor = MonitoredCommand( [prelaunch_command], include_processes=[os.path.basename(prelaunch_command)], cwd=self.directory, ) self.prelaunch_executor.start() logger.info("Running %s in the background", prelaunch_command) if system_config.get("prelaunch_wait"): self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.prelaunch_beat) else: self.start_game()
def wineexec( executable, args="", wine_path=None, prefix=None, arch=None, # pylint: disable=too-many-locals working_dir=None, winetricks_wine="", blocking=False, config=None, include_processes=[], exclude_processes=[], disable_runtime=False, env={}, overrides=None, ): """ Execute a Wine command. Args: executable (str): wine program to run, pass None to run wine itself args (str): program arguments wine_path (str): path to the wine version to use prefix (str): path to the wine prefix to use arch (str): wine architecture of the prefix working_dir (str): path to the working dir for the process winetricks_wine (str): path to the wine version used by winetricks blocking (bool): if true, do not run the process in a thread config (LutrisConfig): LutrisConfig object for the process context watch (list): list of process names to monitor (even when in a ignore list) Returns: Process results if the process is running in blocking mode or MonitoredCommand instance otherwise. """ executable = str(executable) if executable else "" if isinstance(include_processes, str): include_processes = shlex.split(include_processes) if isinstance(exclude_processes, str): exclude_processes = shlex.split(exclude_processes) if not wine_path: wine = import_runner("wine") wine_path = wine().get_executable() if not wine_path: raise RuntimeError("Wine is not installed") if not working_dir: if os.path.isfile(executable): working_dir = os.path.dirname(executable) executable, _args, working_dir = get_real_executable(executable, working_dir) if _args: args = '{} "{}"'.format(_args[0], _args[1]) # Create prefix if necessary if arch not in ("win32", "win64"): arch = detect_arch(prefix, wine_path) if not detect_prefix_arch(prefix): wine_bin = winetricks_wine if winetricks_wine else wine_path create_prefix(prefix, wine_path=wine_bin, arch=arch) wineenv = {"WINEARCH": arch} if winetricks_wine: wineenv["WINE"] = winetricks_wine else: wineenv["WINE"] = wine_path if prefix: wineenv["WINEPREFIX"] = prefix wine_config = config or LutrisConfig(runner_slug="wine") disable_runtime = disable_runtime or wine_config.system_config["disable_runtime"] if use_lutris_runtime(wine_path=wineenv["WINE"], force_disable=disable_runtime): if WINE_DIR in wine_path: wine_root_path = os.path.dirname(os.path.dirname(wine_path)) elif WINE_DIR in winetricks_wine: wine_root_path = os.path.dirname(os.path.dirname(winetricks_wine)) else: wine_root_path = None wineenv["LD_LIBRARY_PATH"] = ":".join( runtime.get_paths( prefer_system_libs=wine_config.system_config["prefer_system_libs"], wine_path=wine_root_path, ) ) if overrides: wineenv["WINEDLLOVERRIDES"] = get_overrides_env(overrides) wineenv.update(env) command_parameters = [wine_path] if executable: command_parameters.append(executable) command_parameters += shlex.split(args) if blocking: return system.execute(command_parameters, env=wineenv, cwd=working_dir) wine = import_runner("wine") command = MonitoredCommand( command_parameters, runner=wine(), env=wineenv, cwd=working_dir, include_processes=include_processes, exclude_processes=exclude_processes, ) command.start() return command
def execute(self, data): """Run an executable file.""" args = [] terminal = None working_dir = None env = {} if isinstance(data, dict): self._check_required_params([("file", "command")], data, "execute") if "command" in data and "file" in data: raise ScriptingError( "Parameters file and command can't be used " "at the same time for the execute command", data, ) file_ref = data.get("file", "") command = data.get("command", "") args_string = data.get("args", "") for arg in shlex.split(args_string): args.append(self._substitute(arg)) terminal = data.get("terminal") working_dir = data.get("working_dir") if not data.get("disable_runtime", False): # Possibly need to handle prefer_system_libs here env.update(runtime.get_env()) # Loading environment variables set in the script env.update(self.script_env) # Environment variables can also be passed to the execute command local_env = data.get("env") or {} env.update({ key: self._substitute(value) for key, value in local_env.items() }) include_processes = shlex.split(data.get("include_processes", "")) exclude_processes = shlex.split(data.get("exclude_processes", "")) elif isinstance(data, str): command = data include_processes = [] exclude_processes = [] else: raise ScriptingError("No parameters supplied to execute command.", data) if command: file_ref = "bash" args = ["-c", self._get_file(command.strip())] include_processes.append("bash") else: # Determine whether 'file' value is a file id or a path file_ref = self._get_file(file_ref) exec_path = system.find_executable(file_ref) if not exec_path: raise ScriptingError("Unable to find executable %s" % file_ref) if not os.access(exec_path, os.X_OK): logger.warning("Making %s executable", exec_path) self.chmodx(exec_path) if terminal: terminal = system.get_default_terminal() if not working_dir or not os.path.exists(working_dir): working_dir = self.target_path command = MonitoredCommand( [exec_path] + args, env=env, term=terminal, cwd=working_dir, include_processes=include_processes, exclude_processes=exclude_processes, ) command.start() GLib.idle_add(self.parent.attach_logger, command) self.heartbeat = GLib.timeout_add(1000, self._monitor_task, command) return "STOP"
def wineexec( executable, args="", wine_path=None, prefix=None, arch=None, # pylint: disable=too-many-locals working_dir=None, winetricks_wine="", blocking=False, config=None, include_processes=[], exclude_processes=[], disable_runtime=False, env={}, overrides=None, ): """ Execute a Wine command. Args: executable (str): wine program to run, pass None to run wine itself args (str): program arguments wine_path (str): path to the wine version to use prefix (str): path to the wine prefix to use arch (str): wine architecture of the prefix working_dir (str): path to the working dir for the process winetricks_wine (str): path to the wine version used by winetricks blocking (bool): if true, do not run the process in a thread config (LutrisConfig): LutrisConfig object for the process context watch (list): list of process names to monitor (even when in a ignore list) Returns: Process results if the process is running in blocking mode or MonitoredCommand instance otherwise. """ executable = str(executable) if executable else "" if isinstance(include_processes, str): include_processes = shlex.split(include_processes) if isinstance(exclude_processes, str): exclude_processes = shlex.split(exclude_processes) if not wine_path: wine = import_runner("wine") wine_path = wine().get_executable() if not wine_path: raise RuntimeError("Wine is not installed") if not working_dir: if os.path.isfile(executable): working_dir = os.path.dirname(executable) executable, _args, working_dir = get_real_executable( executable, working_dir) if _args: args = '{} "{}"'.format(_args[0], _args[1]) # Create prefix if necessary if arch not in ("win32", "win64"): arch = detect_arch(prefix, wine_path) if not detect_prefix_arch(prefix): wine_bin = winetricks_wine if winetricks_wine else wine_path create_prefix(prefix, wine_path=wine_bin, arch=arch) wineenv = {"WINEARCH": arch} if winetricks_wine: wineenv["WINE"] = winetricks_wine else: wineenv["WINE"] = wine_path if prefix: wineenv["WINEPREFIX"] = prefix wine_config = config or LutrisConfig(runner_slug="wine") disable_runtime = disable_runtime or wine_config.system_config[ "disable_runtime"] if use_lutris_runtime(wine_path=wineenv["WINE"], force_disable=disable_runtime): if WINE_DIR in wine_path: wine_root_path = os.path.dirname(os.path.dirname(wine_path)) elif WINE_DIR in winetricks_wine: wine_root_path = os.path.dirname(os.path.dirname(winetricks_wine)) else: wine_root_path = None wineenv["LD_LIBRARY_PATH"] = ":".join( runtime.get_paths( prefer_system_libs=wine_config. system_config["prefer_system_libs"], wine_path=wine_root_path, )) if overrides: wineenv["WINEDLLOVERRIDES"] = get_overrides_env(overrides) wineenv.update(env) command_parameters = [wine_path] if executable: command_parameters.append(executable) command_parameters += shlex.split(args) if blocking: return system.execute(command_parameters, env=wineenv, cwd=working_dir) wine = import_runner("wine") command = MonitoredCommand( command_parameters, runner=wine(), env=wineenv, cwd=working_dir, include_processes=include_processes, exclude_processes=exclude_processes, ) command.start() return command
class Game(GObject.Object): """This class takes cares of loading the configuration for a game and running it. """ STATE_IDLE = "idle" STATE_STOPPED = "stopped" STATE_RUNNING = "running" __gsignals__ = { "game-error": (GObject.SIGNAL_RUN_FIRST, None, (str, )), "game-start": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-started": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-stop": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-stopped": (GObject.SIGNAL_RUN_FIRST, None, (int, )), "game-removed": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-updated": (GObject.SIGNAL_RUN_FIRST, None, ()), "game-installed": (GObject.SIGNAL_RUN_FIRST, None, ()), } def __init__(self, game_id=None): super().__init__() self.id = game_id # pylint: disable=invalid-name self.runner = None self.config = None # Load attributes from database game_data = pga.get_game_by_field(game_id, "id") self.slug = game_data.get("slug") or "" self.runner_name = game_data.get("runner") or "" self.directory = game_data.get("directory") or "" self.name = game_data.get("name") or "" self.game_config_id = game_data.get("configpath") or "" self.is_installed = bool( game_data.get("installed") and self.game_config_id) self.platform = game_data.get("platform") or "" self.year = game_data.get("year") or "" self.lastplayed = game_data.get("lastplayed") or 0 self.steamid = game_data.get("steamid") or "" self.has_custom_banner = bool(game_data.get("has_custom_banner")) self.has_custom_icon = bool(game_data.get("has_custom_icon")) self.discord_presence = DiscordPresence() try: self.playtime = float(game_data.get("playtime") or 0.0) except ValueError: logger.error("Invalid playtime value %s", game_data.get("playtime")) self.playtime = 0.0 if self.game_config_id: self.load_config() self.game_thread = None self.prelaunch_executor = None self.heartbeat = None self.killswitch = None self.state = self.STATE_IDLE self.game_runtime_config = {} self.resolution_changed = False self.compositor_disabled = False self.original_outputs = None self._log_buffer = None self.timer = Timer() self.screen_saver_inhibitor_cookie = None def __repr__(self): return self.__str__() def __str__(self): value = self.name if self.runner_name: value += " (%s)" % self.runner_name return value @property def log_buffer(self): """Access the log buffer object, creating it if necessary""" if self._log_buffer is None: self._log_buffer = Gtk.TextBuffer() self._log_buffer.create_tag("warning", foreground="red") if self.game_thread: self.game_thread.set_log_buffer(self._log_buffer) self._log_buffer.set_text(self.game_thread.stdout) return self._log_buffer @property def formatted_playtime(self): """Return a human readable formatted play time""" return strings.get_formatted_playtime(self.playtime) @property def is_search_result(self): """Return whether or not the game is a remote game from search results. This is bad, find another way to do this. """ return self.id < 0 @staticmethod def show_error_message(message): """Display an error message based on the runner's output.""" if message["error"] == "CUSTOM": message_text = message["text"].replace("&", "&") dialogs.ErrorDialog(message_text) elif message["error"] == "RUNNER_NOT_INSTALLED": dialogs.ErrorDialog(_("Error the runner is not installed")) elif message["error"] == "NO_BIOS": dialogs.ErrorDialog(_("A bios file is required to run this game")) elif message["error"] == "FILE_NOT_FOUND": filename = message["file"] if filename: message_text = _("The file {} could not be found").format( filename.replace("&", "&")) else: message_text = _("No file provided") dialogs.ErrorDialog(message_text) elif message["error"] == "NOT_EXECUTABLE": message_text = message["file"].replace("&", "&") dialogs.ErrorDialog( _("The file %s is not executable") % message_text) elif message["error"] == "PATH_NOT_SET": message_text = _( "The path '%s' is not set. please set it in the options." ) % message["path"] dialogs.ErrorDialog(message_text) else: dialogs.ErrorDialog(_("Unhandled error: %s") % message["error"]) def get_browse_dir(self): """Return the path to open with the Browse Files action.""" return self.runner.game_path def _get_runner(self): """Return the runner instance for this game's configuration""" try: runner_class = import_runner(self.runner_name) return runner_class(self.config) except InvalidRunner: logger.error("Unable to import runner %s for %s", self.runner_name, self.slug) def load_config(self): """Load the game's configuration.""" if not self.is_installed: return self.config = LutrisConfig(runner_slug=self.runner_name, game_config_id=self.game_config_id) self.runner = self._get_runner() if self.discord_presence.available: self.discord_presence.client_id = ( self.config.system_config.get("discord_client_id") or DEFAULT_DISCORD_CLIENT_ID) self.discord_presence.game_name = ( self.config.system_config.get("discord_custom_game_name") or self.name) self.discord_presence.show_runner = self.config.system_config.get( "discord_show_runner", True) self.discord_presence.runner_name = ( self.config.system_config.get("discord_custom_runner_name") or self.runner_name) self.discord_presence.rpc_enabled = self.config.system_config.get( "discord_rpc_enabled", True) def set_desktop_compositing(self, enable): """Enables or disables compositing""" if enable: if self.compositor_disabled: enable_compositing() self.compositor_disabled = False else: if not self.compositor_disabled: disable_compositing() self.compositor_disabled = True def remove(self, from_library=False, from_disk=False): """Uninstall a game Params: from_library (bool): Completely remove the game from library, do not set it as uninstalled from_disk (bool): Delete the game files Return: bool: Updated value for from_library """ if from_disk and self.runner: logger.debug("Removing game %s from disk", self.id) self.runner.remove_game_data(game_path=self.directory) # Do not keep multiple copies of the same game existing_games = pga.get_games_where(slug=self.slug) if len(existing_games) > 1: from_library = True if from_library: logger.debug("Removing game %s from library", self.id) pga.delete_game(self.id) else: pga.set_uninstalled(self.id) if self.config: self.config.remove() xdgshortcuts.remove_launcher(self.slug, self.id, desktop=True, menu=True) self.is_installed = False self.emit("game-removed") return from_library def set_platform_from_runner(self): """Set the game's platform from the runner""" if not self.runner: logger.warning("Game has no runner, can't set platform") return self.platform = self.runner.get_platform() if not self.platform: logger.warning("Can't get platform for runner %s", self.runner.human_name) def save(self, metadata_only=False): """ Save the game's config and metadata, if `metadata_only` is set to True, do not save the config. This is useful when exiting the game since the config might have changed and we don't want to override the changes. """ logger.debug("Saving %s", self) if not metadata_only: self.config.save() self.set_platform_from_runner() self.id = pga.add_or_update( name=self.name, runner=self.runner_name, slug=self.slug, platform=self.platform, year=self.year, lastplayed=self.lastplayed, directory=self.directory, installed=self.is_installed, configpath=self.config.game_config_id, steamid=self.steamid, id=self.id, playtime=self.playtime, ) self.emit("game-updated") def is_launchable(self): """Verify that the current game can be launched.""" if not self.runner.is_installed(): installed = self.runner.install_dialog() if not installed: return False if self.runner.use_runtime(): runtime_updater = runtime.RuntimeUpdater() if runtime_updater.is_updating(): logger.warning("Runtime updates: %s", runtime_updater.current_updates) dialogs.ErrorDialog(_("Runtime currently updating"), _("Game might not work as expected")) if ("wine" in self.runner_name and not wine.get_system_wine_version() and not LINUX_SYSTEM.is_flatpak): # TODO find a reference to the root window or better yet a way not # to have Gtk dependent code in this class. root_window = None dialogs.WineNotInstalledWarning(parent=root_window) return True def play(self): """Launch the game.""" if not self.runner: dialogs.ErrorDialog( _("Invalid game configuration: Missing runner")) self.state = self.STATE_STOPPED self.emit("game-stop") return if not self.is_launchable(): self.state = self.STATE_STOPPED self.emit("game-stop") return self.emit("game-start") jobs.AsyncCall(self.runner.prelaunch, self.configure_game) def restrict_to_display(self, display): outputs = DISPLAY_MANAGER.get_config() if display == "primary": display = None for output in outputs: if output.primary: display = output.name break if not display: logger.warning("No primary display set") else: found = False for output in outputs: if output.name == display: found = True break if not found: logger.warning("Selected display %s not found", display) display = None if display: turn_off_except(display) time.sleep(3) return True return False def get_launch_parameters(self, gameplay_info): system_config = self.runner.system_config launch_arguments = gameplay_info["command"] optimus = system_config.get("optimus") if optimus == "primusrun" and system.find_executable("primusrun"): launch_arguments.insert(0, "primusrun") elif optimus == "optirun" and system.find_executable("optirun"): launch_arguments.insert(0, "virtualgl") launch_arguments.insert(0, "-b") launch_arguments.insert(0, "optirun") elif optimus == "pvkrun" and system.find_executable("pvkrun"): launch_arguments.insert(0, "pvkrun") # Mangohud activation mangohud = system_config.get("mangohud") or "" if mangohud and system.find_executable("mangohud"): # This is probably not the way to go. This only work with a few # Wine games. It will probably crash it, or do nothing at all. # I have never got mangohud to work on anything other than a Wine # game. dialogs.NoticeDialog( "MangoHud support is experimental. Expect the " "game to crash or the framerate counter not to " "appear at all.") launch_arguments = ["mangohud"] + launch_arguments fps_limit = system_config.get("fps_limit") or "" if fps_limit: strangle_cmd = system.find_executable("strangle") if strangle_cmd: launch_arguments = [strangle_cmd, fps_limit] + launch_arguments else: logger.warning( "libstrangle is not available on this system, FPS limiter disabled" ) prefix_command = system_config.get("prefix_command") or "" if prefix_command: launch_arguments = ( shlex.split(os.path.expandvars(prefix_command)) + launch_arguments) single_cpu = system_config.get("single_cpu") or False if single_cpu: logger.info("The game will run on a single CPU core") launch_arguments.insert(0, "0") launch_arguments.insert(0, "-c") launch_arguments.insert(0, "taskset") env = {} env.update(self.runner.get_env()) env.update(gameplay_info.get("env") or {}) env["game_name"] = self.name # Set environment variables dependent on gameplay info # LD_PRELOAD ld_preload = gameplay_info.get("ld_preload") if ld_preload: env["LD_PRELOAD"] = ld_preload # LD_LIBRARY_PATH game_ld_libary_path = gameplay_info.get("ld_library_path") if game_ld_libary_path: ld_library_path = env.get("LD_LIBRARY_PATH") if not ld_library_path: ld_library_path = "$LD_LIBRARY_PATH" env["LD_LIBRARY_PATH"] = ":".join( [game_ld_libary_path, ld_library_path]) # Feral gamemode gamemode = system_config.get( "gamemode") and LINUX_SYSTEM.gamemode_available() if gamemode: if system.find_executable("gamemoderun"): launch_arguments.insert(0, "gamemoderun") else: env["LD_PRELOAD"] = ":".join([ path for path in [ env.get("LD_PRELOAD"), "libgamemodeauto.so", ] if path ]) return launch_arguments, env def start_xephyr(self, display=":2"): """Start a monitored Xephyr instance""" if not system.find_executable("Xephyr"): raise GameConfigError( "Unable to find Xephyr, install it or disable the Xephyr option" ) xephyr_depth = "8" if self.runner.system_config.get( "xephyr") == "8bpp" else "16" xephyr_resolution = self.runner.system_config.get( "xephyr_resolution") or "640x480" xephyr_command = [ "Xephyr", display, "-ac", "-screen", xephyr_resolution + "x" + xephyr_depth, "-glamor", "-reset", "-terminate", ] if self.runner.system_config.get("xephyr_fullscreen"): xephyr_command.append("-fullscreen") xephyr_thread = MonitoredCommand(xephyr_command) xephyr_thread.start() time.sleep(3) return display @staticmethod def set_keyboard_layout(layout): setxkbmap_command = ["setxkbmap", "-model", "pc101", layout, "-print"] xkbcomp_command = ["xkbcomp", "-", os.environ.get("DISPLAY", ":0")] xkbcomp = subprocess.Popen(xkbcomp_command, stdin=subprocess.PIPE) subprocess.Popen(setxkbmap_command, env=os.environ, stdout=xkbcomp.stdin).communicate() xkbcomp.communicate() def start_prelaunch_command(self): """Start the prelaunch command specified in the system options""" prelaunch_command = self.runner.system_config.get("prelaunch_command") command_array = shlex.split(prelaunch_command) if not system.path_exists(command_array[0]): logger.warning("Command %s not found", command_array[0]) return self.prelaunch_executor = MonitoredCommand( command_array, include_processes=[os.path.basename(command_array[0])], env=self.game_runtime_config["env"], cwd=self.directory, ) self.prelaunch_executor.start() logger.info("Running %s in the background", prelaunch_command) def get_terminal(self): """Return the terminal used to run the game into or None if the game is not run from a terminal. Remember that only games using text mode should use the terminal. """ if self.runner.system_config.get("terminal"): terminal = self.runner.system_config.get( "terminal_app", system.get_default_terminal()) if terminal and not system.find_executable(terminal): raise GameConfigError( _("The selected terminal application could not be launched:\n%s" ) % terminal) return terminal def get_killswitch(self): """Return the path to a file that is monitored during game execution. If the file stops existing, the game is stopped. """ killswitch = self.runner.system_config.get("killswitch") # Prevent setting a killswitch to a file that doesn't exists if killswitch and system.path_exists(self.killswitch): return killswitch def get_gameplay_info(self): """Return the information provided by a runner's play method. Checks for possible errors. """ gameplay_info = self.runner.play() if "error" in gameplay_info: self.show_error_message(gameplay_info) self.state = self.STATE_STOPPED self.emit("game-stop") return return gameplay_info @watch_lutris_errors def configure_game(self, prelaunched, error=None): # noqa: C901 """Get the game ready to start, applying all the options This methods sets the game_runtime_config attribute. """ if error: logger.error(error) dialogs.ErrorDialog(str(error)) if not prelaunched: logger.error("Game prelaunch unsuccessful") dialogs.ErrorDialog(_("An error prevented the game from running")) self.state = self.STATE_STOPPED self.emit("game-stop") return gameplay_info = self.get_gameplay_info() if not gameplay_info: return command, env = self.get_launch_parameters(gameplay_info) self.game_runtime_config = { "args": command, "env": env, "terminal": self.get_terminal(), "include_processes": shlex.split(self.runner.system_config.get("include_processes", "")), "exclude_processes": shlex.split(self.runner.system_config.get("exclude_processes", "")), } # Audio control if self.runner.system_config.get("reset_pulse"): audio.reset_pulse() # Input control if self.runner.system_config.get("use_us_layout"): self.set_keyboard_layout("us") # Display control self.original_outputs = DISPLAY_MANAGER.get_config() if self.runner.system_config.get("disable_compositor"): self.set_desktop_compositing(False) if self.runner.system_config.get("disable_screen_saver"): self.screen_saver_inhibitor_cookie = SCREEN_SAVER_INHIBITOR.inhibit( self.name) if self.runner.system_config.get("display") != "off": self.resolution_changed = self.restrict_to_display( self.runner.system_config.get("display")) resolution = self.runner.system_config.get("resolution") if resolution != "off": DISPLAY_MANAGER.set_resolution(resolution) time.sleep(3) self.resolution_changed = True xephyr = self.runner.system_config.get("xephyr") or "off" if xephyr != "off": env["DISPLAY"] = self.start_xephyr() # Execution control self.killswitch = self.get_killswitch() if self.runner.system_config.get("prelaunch_command"): self.start_prelaunch_command() if self.runner.system_config.get("prelaunch_wait"): # Monitor the prelaunch command and wait until it has finished self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.prelaunch_beat) else: self.start_game() def start_game(self): """Run a background command to lauch the game""" self.game_thread = MonitoredCommand( self.game_runtime_config["args"], title=self.name, runner=self.runner, env=self.game_runtime_config["env"], term=self.game_runtime_config["terminal"], log_buffer=self._log_buffer, include_processes=self.game_runtime_config["include_processes"], exclude_processes=self.game_runtime_config["exclude_processes"], ) if hasattr(self.runner, "stop"): self.game_thread.stop_func = self.runner.stop self.game_thread.start() self.timer.start() self.emit("game-started") self.state = self.STATE_RUNNING self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.beat) def stop_game(self): """Cleanup after a game as stopped""" self.state = self.STATE_STOPPED self.emit("game-stop") if not self.timer.finished: self.timer.end() self.playtime += self.timer.duration / 3600 def prelaunch_beat(self): """Watch the prelaunch command""" if self.prelaunch_executor and self.prelaunch_executor.is_running: return True self.start_game() return False def beat(self): """Watch the game's process(es).""" if self.game_thread.error: dialogs.ErrorDialog( _("<b>Error lauching the game:</b>\n") + self.game_thread.error) self.on_game_quit() return False # The killswitch file should be set to a device (ie. /dev/input/js0) # When that device is unplugged, the game is forced to quit. killswitch_engage = self.killswitch and not system.path_exists( self.killswitch) if not self.game_thread.is_running or killswitch_engage: logger.debug("Game thread stopped") self.on_game_quit() return False if self.discord_presence.available: self.discord_presence.update_discord_rich_presence() return True def stop(self): """Stops the game""" if self.state == self.STATE_STOPPED: logger.debug("Game already stopped") return logger.info("Stopping %s", self) if self.game_thread: jobs.AsyncCall(self.game_thread.stop, None) self.stop_game() def on_game_quit(self): """Restore some settings and cleanup after game quit.""" if self.prelaunch_executor and self.prelaunch_executor.is_running: logger.info("Stopping prelaunch script") self.prelaunch_executor.stop() self.heartbeat = None if self.state != self.STATE_STOPPED: logger.warning("Game still running (state: %s)", self.state) self.stop() # Check for post game script postexit_command = self.runner.system_config.get("postexit_command") if postexit_command: command_array = shlex.split(postexit_command) if system.path_exists(command_array[0]): logger.info("Running post-exit command: %s", postexit_command) postexit_thread = MonitoredCommand( command_array, include_processes=[os.path.basename(postexit_command)], env=self.game_runtime_config["env"], cwd=self.directory, ) postexit_thread.start() if self.discord_presence.available: self.discord_presence.clear_discord_rich_presence() quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("%s stopped at %s", self.name, quit_time) self.lastplayed = int(time.time()) self.save(metadata_only=True) os.chdir(os.path.expanduser("~")) if self.resolution_changed or self.runner.system_config.get( "reset_desktop"): DISPLAY_MANAGER.set_resolution(self.original_outputs) if self.compositor_disabled: self.set_desktop_compositing(True) if self.screen_saver_inhibitor_cookie is not None: SCREEN_SAVER_INHIBITOR.uninhibit( self.screen_saver_inhibitor_cookie) self.screen_saver_inhibitor_cookie = None if self.runner.system_config.get("use_us_layout"): subprocess.Popen(["setxkbmap"], env=os.environ).communicate() if self.runner.system_config.get("restore_gamma"): restore_gamma() self.process_return_codes() def process_return_codes(self): """Do things depending on how the game quitted.""" if self.game_thread.return_code == 127: # Error missing shared lib error = "error while loading shared lib" error_line = strings.lookup_string_in_text(error, self.game_thread.stdout) if error_line: dialogs.ErrorDialog( _("<b>Error: Missing shared library.</b>\n\n%s") % error_line) if self.game_thread.return_code == 1: # Error Wine version conflict error = "maybe the wrong wineserver" if strings.lookup_string_in_text(error, self.game_thread.stdout): dialogs.ErrorDialog( _("<b>Error: A different Wine version is already using the same Wine prefix.</b>" )) def notify_steam_game_changed(self, appmanifest): """Receive updates from Steam games and set the thread's ready state accordingly""" if not self.game_thread: return if "Fully Installed" in appmanifest.states and not self.game_thread.ready_state: logger.info("Steam game %s is fully installed", appmanifest.steamid) self.game_thread.ready_state = True elif "Update Required" in appmanifest.states and self.game_thread.ready_state: logger.info( "Steam game %s updating, setting game thread as not ready", appmanifest.steamid, ) self.game_thread.ready_state = False def write_script(self, script_path): """Output the launch argument in a bash script""" gameplay_info = self.get_gameplay_info() if not gameplay_info: return command, env = self.get_launch_parameters(gameplay_info) # Override TERM otherwise the script might not run env["TERM"] = "xterm" script_content = "#!/bin/bash\n\n\n" script_content += "# Environment variables\n\n" for env_var in env: script_content += "export %s=\"%s\"\n" % (env_var, env[env_var]) script_content += "\n\n# Command\n\n" script_content += shlex.join(command) with open(script_path, "w") as script_file: script_file.write(script_content) os.chmod(script_path, os.stat(script_path).st_mode | stat.S_IEXEC)