class Game(object): """This class takes cares about loading the configuration for a game and running it. """ def __init__(self, slug): self.slug = slug self.runner = None self.game_thread = None self.heartbeat = None self.config = None game_data = pga.get_game_by_slug(slug) self.runner_name = game_data.get('runner') or '' self.directory = game_data.get('directory') or '' self.name = game_data.get('name') or '' self.is_installed = bool(game_data.get('installed')) or False self.year = game_data.get('year') or '' self.load_config() self.resolution_changed = False self.original_outputs = None def __repr__(self): return self.__unicode__() def __unicode__(self): value = self.name if self.runner_name: value += " (%s)" % self.runner_name return value def get_browse_dir(self): """Return the path to open with the Browse Files action.""" return self.runner.browse_dir def load_config(self): """Load the game's configuration.""" self.config = LutrisConfig(game=self.slug) if self.is_installed: runner_class = import_runner(self.runner_name) if runner_class: self.runner = runner_class(self.config) else: logger.error("Unable to import runner %s for %s", self.runner_name, self.slug) def remove(self, from_library=False, from_disk=False): if from_disk: self.runner.remove_game_data(game_path=self.directory) if from_library: pga.delete_game(self.slug) self.config.remove() else: pga.set_uninstalled(self.slug) 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 hasattr(self.runner, 'prelaunch'): return self.runner.prelaunch() return True def use_runtime(self, system_config): disable_runtime = system_config.get('disable_runtime') env_runtime = os.getenv('LUTRIS_RUNTIME') if env_runtime and env_runtime.lower() in ('0', 'off'): disable_runtime = True return not disable_runtime def play(self): """Launch the game.""" if not self.runner: dialogs.ErrorDialog("Invalid game configuration: Missing runner") return False if not self.prelaunch(): return False system_config = self.runner.system_config self.original_outputs = display.get_outputs() gameplay_info = self.runner.play() logger.debug("Launching %s: %s" % (self.name, gameplay_info)) if 'error' in gameplay_info: show_error_message(gameplay_info) return False launch_arguments = gameplay_info['command'] restrict_to_display = system_config.get('display') 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: display.change_resolution(resolution) time.sleep(3) self.resolution_changed = True if system_config.get('reset_pulse'): audio.reset_pulse() primusrun = system_config.get('primusrun') if primusrun and system.find_executable('primusrun'): launch_arguments.insert(0, 'primusrun') prefix_command = system_config.get("prefix_command", '').strip() if prefix_command: launch_arguments.insert(0, prefix_command) ld_preload = gameplay_info.get('ld_preload') if ld_preload: launch_arguments.insert(0, 'LD_PRELOAD="{}"'.format(ld_preload)) ld_library_path = [] if self.use_runtime(system_config): runtime64_path = os.path.join(settings.RUNTIME_DIR, "lib64") if os.path.exists(runtime64_path): ld_library_path.append(runtime64_path) runtime32_path = os.path.join(settings.RUNTIME_DIR, "lib32") if os.path.exists(runtime32_path): ld_library_path.append(runtime32_path) game_ld_libary_path = gameplay_info.get('ld_library_path') if game_ld_libary_path: ld_library_path.append(game_ld_libary_path) if ld_library_path: ld_full = ':'.join(ld_library_path) ld_arg = 'LD_LIBRARY_PATH="{}:$LD_LIBRARY_PATH"'.format(ld_full) launch_arguments.insert(0, ld_arg) env = gameplay_info.get('env') or [] for var in env: launch_arguments.insert(0, var) killswitch = system_config.get('killswitch') self.game_thread = LutrisThread(" ".join(launch_arguments), path=self.runner.working_dir, killswitch=killswitch) if hasattr(self.runner, 'stop'): self.game_thread.set_stop_command(self.runner.stop) self.game_thread.start() if 'joy2key' in gameplay_info: self.joy2key(gameplay_info['joy2key']) xboxdrv_config = system_config.get('xboxdrv') if xboxdrv_config: self.xboxdrv_start(xboxdrv_config) if self.runner.is_watchable: # Create heartbeat every self.heartbeat = GLib.timeout_add(5000, self.poke_process) def joy2key(self, config): """Run a joy2key thread.""" if not system.find_executable('joy2key'): logger.error("joy2key is not installed") return win = "grep %s" % config['window'] if 'notwindow' in config: win += ' | grep -v %s' % config['notwindow'] wid = "xwininfo -root -tree | %s | awk '{print $1}'" % win buttons = config['buttons'] axis = "Left Right Up Down" rcfile = os.path.expanduser("~/.joy2keyrc") rc_option = '-rcfile %s' % rcfile if os.path.exists(rcfile) else '' command = "sleep 5 " command += "&& joy2key $(%s) -X %s -buttons %s -axis %s" % ( wid, rc_option, buttons, axis ) joy2key_thread = LutrisThread(command) self.game_thread.attach_thread(joy2key_thread) joy2key_thread.start() def xboxdrv_start(self, config): command = ("pkexec xboxdrv --daemon --detach-kernel-driver " "--dbus session --silent %s" % config) logger.debug("xboxdrv command: %s", command) self.xboxdrv_thread = LutrisThread(command) self.xboxdrv_thread.set_stop_command(self.xboxdrv_stop) self.xboxdrv_thread.start() def xboxdrv_stop(self): os.system("pkexec xboxdrvctl --shutdown") def poke_process(self): """Watch game's process.""" if not self.game_thread.pid: self.quit_game() return False return True def quit_game(self): """Quit the game and cleanup.""" self.heartbeat = None quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("game has quit at %s" % quit_time) if self.resolution_changed\ or self.runner.system_config.get('reset_desktop'): display.change_resolution(self.original_outputs) if self.runner.system_config.get('restore_gamma'): display.restore_gamma() if self.runner.system_config.get('xboxdrv'): self.xboxdrv_thread.stop() if self.game_thread: self.game_thread.stop()
class Game(object): """This class takes cares of loading the configuration for a game and running it. """ STATE_IDLE = 'idle' STATE_STOPPED = 'stopped' STATE_RUNNING = 'running' def __init__(self, id=None): self.id = id self.runner = None self.game_thread = None self.heartbeat = None self.config = None self.killswitch = None self.state = self.STATE_IDLE self.game_log = '' self.exit_main_loop = False game_data = pga.get_game_by_field(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.is_installed = bool(game_data.get('installed')) or False self.platform = game_data.get('platform') or '' self.year = game_data.get('year') or '' self.game_config_id = game_data.get('configpath') or '' self.steamid = game_data.get('steamid') or '' self.has_custom_banner = bool(game_data.get('has_custom_banner')) or False self.has_custom_icon = bool(game_data.get('has_custom_icon')) or False self.load_config() self.resolution_changed = False self.original_outputs = None def __repr__(self): return self.__unicode__() def __unicode__(self): value = self.name if self.runner_name: value += " (%s)" % self.runner_name return value def show_error_message(self, message): """Display an error message based on the runner's output.""" if "CUSTOM" == message['error']: message_text = message['text'].replace('&', '&') dialogs.ErrorDialog(message_text) elif "RUNNER_NOT_INSTALLED" == message['error']: dialogs.ErrorDialog('Error the runner is not installed') elif "NO_BIOS" == message['error']: dialogs.ErrorDialog("A bios file is required to run this game") elif "FILE_NOT_FOUND" == message['error']: 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 "NOT_EXECUTABLE" == message['error']: 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 load_config(self): """Load the game's configuration.""" self.config = LutrisConfig(runner_slug=self.runner_name, game_config_id=self.game_config_id) if not self.is_installed: return if not self.runner_name: logger.error('Incomplete data for %s', self.slug) return try: runner_class = import_runner(self.runner_name) except InvalidRunner: logger.error("Unable to import runner %s for %s", self.runner_name, self.slug) else: self.runner = runner_class(self.config) 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_game_by_field(self.slug, 'slug', all=True) 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) self.config.remove() shortcuts.remove_launcher(self.slug, self.id, desktop=True, menu=True) return from_library def save(self): self.config.save() self.id = pga.add_or_update( name=self.name, runner=self.runner_name, slug=self.slug, platform=self.platform, year=self.year, directory=self.directory, installed=self.is_installed, configpath=self.config.game_config_id, steamid=self.steamid, id=self.id ) 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: {}".format( runtime_updater.current_updates) ) dialogs.ErrorDialog("Runtime currently updating", "Game might not work as expected") return True def play(self): """Launch the game.""" if not self.runner: dialogs.ErrorDialog("Invalid game configuration: Missing runner") self.state = self.STATE_STOPPED return if not self.prelaunch(): self.state = self.STATE_STOPPED return if hasattr(self.runner, 'prelaunch'): jobs.AsyncCall(self.runner.prelaunch, self.do_play) else: self.do_play(True) def do_play(self, prelaunched, _error=None): if not prelaunched: self.state = self.STATE_STOPPED return system_config = self.runner.system_config self.original_outputs = display.get_outputs() gameplay_info = self.runner.play() env = {} logger.debug("Launching %s: %s" % (self.name, gameplay_info)) if 'error' in gameplay_info: self.show_error_message(gameplay_info) self.state = self.STATE_STOPPED return 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': 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 os.path.exists(self.killswitch): # Prevent setting a killswitch to a file that doesn't exists self.killswitch = None # Command launch_arguments = gameplay_info['command'] primusrun = system_config.get('primusrun') if primusrun and system.find_executable('primusrun'): launch_arguments.insert(0, 'primusrun') xephyr = system_config.get('xephyr') or 'off' if xephyr != 'off': if xephyr == '8bpp': xephyr_depth = '8' else: xephyr_depth = '16' xephyr_resolution = system_config.get('xephyr_resolution') or '640x480' xephyr_command = ['Xephyr', ':2', '-ac', '-screen', xephyr_resolution + 'x' + xephyr_depth, '-glamor', '-reset', '-terminate', '-fullscreen'] xephyr_thread = LutrisThread(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' prefix_command = system_config.get("prefix_command") or '' if prefix_command.strip(): launch_arguments.insert(0, prefix_command) 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 return # Env vars game_env = gameplay_info.get('env') or {} env.update(game_env) system_env = system_config.get('env') or {} env.update(system_env) ld_preload = gameplay_info.get('ld_preload') or '' env["LD_PRELOAD"] = ld_preload # Runtime management ld_library_path = "" if self.runner.use_runtime(): runtime_env = runtime.get_env() if 'STEAM_RUNTIME' in runtime_env and 'STEAM_RUNTIME' not in env: env['STEAM_RUNTIME'] = runtime_env['STEAM_RUNTIME'] if 'LD_LIBRARY_PATH' in runtime_env: ld_library_path = runtime_env['LD_LIBRARY_PATH'] game_ld_libary_path = gameplay_info.get('ld_library_path') if game_ld_libary_path: if not ld_library_path: ld_library_path = '$LD_LIBRARY_PATH' ld_library_path = ":".join([game_ld_libary_path, ld_library_path]) env["LD_LIBRARY_PATH"] = ld_library_path # /Env vars self.game_thread = LutrisThread(launch_arguments, runner=self.runner, env=env, rootpid=gameplay_info.get('rootpid'), term=terminal) if hasattr(self.runner, 'stop'): self.game_thread.set_stop_command(self.runner.stop) self.game_thread.start() self.state = self.STATE_RUNNING if 'joy2key' in gameplay_info: self.joy2key(gameplay_info['joy2key']) xboxdrv_config = system_config.get('xboxdrv') if xboxdrv_config: self.xboxdrv_start(xboxdrv_config) self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.beat) def joy2key(self, config): """Run a joy2key thread.""" if not system.find_executable('joy2key'): logger.error("joy2key is not installed") return win = "grep %s" % config['window'] if 'notwindow' in config: win += ' | grep -v %s' % config['notwindow'] wid = "xwininfo -root -tree | %s | awk '{print $1}'" % win buttons = config['buttons'] axis = "Left Right Up Down" rcfile = os.path.expanduser("~/.joy2keyrc") rc_option = '-rcfile %s' % rcfile if os.path.exists(rcfile) else '' command = "sleep 5 " command += "&& joy2key $(%s) -X %s -buttons %s -axis %s" % ( wid, rc_option, buttons, axis ) joy2key_thread = LutrisThread(command) self.game_thread.attach_thread(joy2key_thread) joy2key_thread.start() def xboxdrv_start(self, config): command = [ "pkexec", "xboxdrv", "--daemon", "--detach-kernel-driver", "--dbus", "session", "--silent" ] + config.split() logger.debug("[xboxdrv] %s", ' '.join(command)) self.xboxdrv_thread = LutrisThread(command) self.xboxdrv_thread.set_stop_command(self.xboxdrv_stop) self.xboxdrv_thread.start() def xboxdrv_stop(self): os.system("pkexec xboxdrvctl --shutdown") if os.path.exists("/usr/share/lutris/bin/resetxpad"): os.system("pkexec /usr/share/lutris/bin/resetxpad") 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 self.game_log = self.game_thread.stdout killswitch_engage = self.killswitch and \ not os.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 return True def stop(self): self.state = self.STATE_STOPPED if self.runner.system_config.get('xboxdrv'): self.xboxdrv_thread.stop() if self.game_thread: jobs.AsyncCall(self.game_thread.stop, None, killall=True) def on_game_quit(self): """Restore some settings and cleanup after game quit.""" self.heartbeat = None quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("%s stopped at %s", self.name, quit_time) self.state = self.STATE_STOPPED 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.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() if self.runner.system_config.get('xboxdrv') \ and self.xboxdrv_thread.is_running: self.xboxdrv_thread.stop() if self.game_thread: self.game_thread.stop() 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>")
class Game(object): """" This class takes cares about loading the configuration for a game and running it. """ def __init__(self, slug): self.slug = slug self.game_thread = None self.heartbeat = None self.game_config = None game_data = pga.get_game_by_slug(slug) self.runner_name = game_data['runner'] self.directory = game_data['directory'] self.name = game_data['name'] self.is_installed = bool(game_data['installed']) self.load_config() def get_runner(self): """ Return the runner's name """ return self.game_config['runner'] def load_config(self): """ Load the game's configuration. """ self.game_config = LutrisConfig(game=self.slug) if self.is_installed: if not self.game_config.is_valid(): logger.error("Invalid game config for %s" % self.slug) else: runner_class = import_runner(self.runner_name) self.runner = runner_class(self.game_config) def remove(self, from_library=False, from_disk=False): if from_disk: if os.path.exists(self.directory): shutil.rmtree(self.directory) if from_library: pga.delete_game(self.slug) else: pga.set_uninstalled(self.slug) self.game_config.remove() def prelaunch(self): """ Verify that the current game can be launched. """ if not self.runner.is_installed(): question = ("The required runner is not installed.\n" "Do you wish to install it now ?") install_runner_dialog = dialogs.QuestionDialog( {'question': question, 'title': "Required runner unavailable"}) if Gtk.ResponseType.YES == install_runner_dialog.result: self.runner.install() return False if hasattr(self.runner, 'prelaunch'): success = self.runner.prelaunch() return success return True def play(self): """ Launch the game. """ if not self.prelaunch(): return False gameplay_info = self.runner.play() logger.debug("Launching %s: %s" % (self.name, gameplay_info)) if isinstance(gameplay_info, dict): if 'error' in gameplay_info: show_error_message(gameplay_info) return False launch_arguments = gameplay_info['command'] else: logger.error("Old method used for returning gameplay infos") launch_arguments = gameplay_info resolution = self.game_config.get_system('resolution') if resolution: desktop_control.change_resolution(resolution) if self.game_config.get_system('reset_pulse'): desktop_control.reset_pulse() if self.game_config.get_system('hide_panels'): self.desktop.hide_panels() oss_wrapper = self.game_config.get_system("oss_wrapper") if oss_wrapper: launch_arguments.insert(0, audio.get_oss_wrapper(oss_wrapper)) ld_preload = gameplay_info.get('ld_preload') if ld_preload: launch_arguments.insert(0, 'LD_PRELOAD="{}"'.format(ld_preload)) ld_library_path = gameplay_info.get('ld_library_path') if ld_library_path: launch_arguments.insert( 0, 'LD_LIBRARY_PATH="{}"'.format(ld_library_path) ) killswitch = self.game_config.get_system('killswitch') self.heartbeat = GLib.timeout_add(5000, self.poke_process) self.game_thread = LutrisThread(" ".join(launch_arguments), path=self.runner.get_game_path(), killswitch=killswitch) if hasattr(self.runner, 'stop'): self.game_thread.set_stop_command(self.runner.stop) self.game_thread.start() if 'joy2key' in gameplay_info: self.joy2key(gameplay_info['joy2key']) xboxdrv_config = self.game_config.get_system('xboxdrv') if xboxdrv_config: self.xboxdrv(xboxdrv_config) def joy2key(self, config): """ Run a joy2key thread. """ win = "grep %s" % config['window'] if 'notwindow' in config: win = win + ' | grep -v %s' % config['notwindow'] wid = "xwininfo -root -tree | %s | awk '{print $1}'" % win buttons = config['buttons'] axis = "Left Right Up Down" rcfile = "~/.joy2keyrc" command = "sleep 5 " command += "&& joy2key $(%s) -X -rcfile %s -buttons %s -axis %s" % ( wid, rcfile, buttons, axis ) joy2key_thread = LutrisThread(command) self.game_thread.attach_thread(joy2key_thread) joy2key_thread.start() def xboxdrv(self, config): command = ("pkexec xboxdrv --daemon --detach-kernel-driver " "--dbus session --silent %s" % config) logger.debug("xboxdrv command: %s", command) thread = LutrisThread(command) thread.start() def poke_process(self): """ Watch game's process. """ if not self.game_thread.pid: self.quit_game() return False return True def quit_game(self): """ Quit the game and cleanup. """ self.heartbeat = None quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("game has quit at %s" % quit_time) if self.game_config.get_system('resolution'): desktop_control.reset_desktop() if self.game_config.get_system('xboxdrv'): logger.debug("Shutting down xboxdrv") os.system("pkexec xboxdrvctl --shutdown") if self.game_thread: self.game_thread.stop()
class Game(object): """This class takes cares about loading the configuration for a game and running it. """ STATE_IDLE = 'idle' STATE_STOPPED = 'stopped' STATE_RUNNING = 'running' def __init__(self, slug): self.slug = slug self.runner = None self.game_thread = None self.heartbeat = None self.config = None self.killswitch = None self.state = self.STATE_IDLE self.game_log = '' game_data = pga.get_game_by_slug(slug) self.runner_name = game_data.get('runner') or '' self.directory = game_data.get('directory') or '' self.name = game_data.get('name') or '' self.is_installed = bool(game_data.get('installed')) or False self.year = game_data.get('year') or '' self.load_config() self.resolution_changed = False self.original_outputs = None def __repr__(self): return self.__unicode__() def __unicode__(self): value = self.name if self.runner_name: value += " (%s)" % self.runner_name return value def get_browse_dir(self): """Return the path to open with the Browse Files action.""" return self.runner.browse_dir def load_config(self): """Load the game's configuration.""" self.config = LutrisConfig(runner_slug=self.runner_name, game_slug=self.slug) if not self.is_installed: return if not self.runner_name: logger.error('Incomplete data for %s', self.slug) return try: runner_class = import_runner(self.runner_name) except InvalidRunner: logger.error("Unable to import runner %s for %s", self.runner_name, self.slug) self.runner = runner_class(self.config) def remove(self, from_library=False, from_disk=False): if from_disk: self.runner.remove_game_data(game_path=self.directory) if from_library: pga.delete_game(self.slug) self.config.remove() else: pga.set_uninstalled(self.slug) def save(self): self.config.save() pga.add_or_update( name=self.name, runner=self.runner_name, slug=self.slug, directory=self.directory, installed=self.is_installed ) 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 hasattr(self.runner, 'prelaunch'): return self.runner.prelaunch() return True def use_runtime(self, system_config): disable_runtime = system_config.get('disable_runtime') env_runtime = os.getenv('LUTRIS_RUNTIME') if env_runtime and env_runtime.lower() in ('0', 'off'): disable_runtime = True return not disable_runtime def play(self): """Launch the game.""" if not self.runner: dialogs.ErrorDialog("Invalid game configuration: Missing runner") return False if not self.prelaunch(): return False system_config = self.runner.system_config self.original_outputs = display.get_outputs() gameplay_info = self.runner.play() logger.debug("Launching %s: %s" % (self.name, gameplay_info)) if 'error' in gameplay_info: show_error_message(gameplay_info) return False restrict_to_display = system_config.get('display') if restrict_to_display != 'off': 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 os.path.exists(self.killswitch): # Prevent setting a killswitch to a file that doesn't exists self.killswitch = None # Command launch_arguments = gameplay_info['command'] primusrun = system_config.get('primusrun') if primusrun and system.find_executable('primusrun'): launch_arguments.insert(0, 'primusrun') prefix_command = system_config.get("prefix_command") or '' if prefix_command.strip(): launch_arguments.insert(0, prefix_command) 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) return False # Env vars env = {} game_env = gameplay_info.get('env') or {} env.update(game_env) ld_preload = gameplay_info.get('ld_preload') if ld_preload: env["LD_PRELOAD"] = ld_preload ld_library_path = [] if self.use_runtime(system_config): env['STEAM_RUNTIME'] = os.path.join(settings.RUNTIME_DIR, 'steam') ld_library_path += runtime.get_runtime_paths() game_ld_libary_path = gameplay_info.get('ld_library_path') if game_ld_libary_path: ld_library_path.append(game_ld_libary_path) if ld_library_path: ld_full = ':'.join(ld_library_path) env["LD_LIBRARY_PATH"] = "{}:$LD_LIBRARY_PATH".format(ld_full) # /Env vars self.game_thread = LutrisThread(launch_arguments, runner=self.runner, env=env, rootpid=gameplay_info.get('rootpid'), term=terminal) self.state = self.STATE_RUNNING if hasattr(self.runner, 'stop'): self.game_thread.set_stop_command(self.runner.stop) self.game_thread.start() if 'joy2key' in gameplay_info: self.joy2key(gameplay_info['joy2key']) xboxdrv_config = system_config.get('xboxdrv') if xboxdrv_config: self.xboxdrv_start(xboxdrv_config) if self.runner.is_watchable: # Create heartbeat every self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.beat) def joy2key(self, config): """Run a joy2key thread.""" if not system.find_executable('joy2key'): logger.error("joy2key is not installed") return win = "grep %s" % config['window'] if 'notwindow' in config: win += ' | grep -v %s' % config['notwindow'] wid = "xwininfo -root -tree | %s | awk '{print $1}'" % win buttons = config['buttons'] axis = "Left Right Up Down" rcfile = os.path.expanduser("~/.joy2keyrc") rc_option = '-rcfile %s' % rcfile if os.path.exists(rcfile) else '' command = "sleep 5 " command += "&& joy2key $(%s) -X %s -buttons %s -axis %s" % ( wid, rc_option, buttons, axis ) joy2key_thread = LutrisThread(command) self.game_thread.attach_thread(joy2key_thread) joy2key_thread.start() def xboxdrv_start(self, config): command = [ "pkexec", "xboxdrv", "--daemon", "--detach-kernel-driver", "--dbus", "session", "--silent" ] + config.split() logger.debug("xboxdrv command: %s", command) self.xboxdrv_thread = LutrisThread(command) self.xboxdrv_thread.set_stop_command(self.xboxdrv_stop) self.xboxdrv_thread.start() def xboxdrv_stop(self): os.system("pkexec xboxdrvctl --shutdown") def beat(self): """Watch game's process.""" self.game_log = self.game_thread.stdout killswitch_engage = self.killswitch and \ not os.path.exists(self.killswitch) if not self.game_thread.is_running or killswitch_engage: self.on_game_quit() return False return True def stop(self): self.game_thread.stop(killall=True) def on_game_quit(self): """Restore some settings and cleanup after game quit.""" self.heartbeat = None quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("game has quit at %s" % quit_time) self.state = self.STATE_STOPPED if self.resolution_changed\ or self.runner.system_config.get('reset_desktop'): display.change_resolution(self.original_outputs) if self.runner.system_config.get('restore_gamma'): display.restore_gamma() if self.runner.system_config.get('xboxdrv'): self.xboxdrv_thread.stop() if self.game_thread: self.game_thread.stop()
class Game(object): """This class takes cares of loading the configuration for a game and running it. """ STATE_IDLE = 'idle' STATE_STOPPED = 'stopped' STATE_RUNNING = 'running' def __init__(self, id=None): self.id = id self.runner = None self.game_thread = None self.heartbeat = None self.config = None self.killswitch = None self.state = self.STATE_IDLE self.game_log = '' self.exit_main_loop = False game_data = pga.get_game_by_field(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.is_installed = bool(game_data.get('installed')) or False self.year = game_data.get('year') or '' self.game_config_id = game_data.get('configpath') or '' self.steamid = game_data.get('steamid') or '' self.has_custom_banner = bool(game_data.get('has_custom_banner')) or False self.has_custom_icon = bool(game_data.get('has_custom_icon')) or False self.load_config() self.resolution_changed = False self.original_outputs = None def __repr__(self): return self.__unicode__() def __unicode__(self): value = self.name if self.runner_name: value += " (%s)" % self.runner_name return value def get_browse_dir(self): """Return the path to open with the Browse Files action.""" return self.runner.browse_dir def load_config(self): """Load the game's configuration.""" self.config = LutrisConfig(runner_slug=self.runner_name, game_config_id=self.game_config_id) if not self.is_installed: return if not self.runner_name: logger.error('Incomplete data for %s', self.slug) return try: runner_class = import_runner(self.runner_name) except InvalidRunner: logger.error("Unable to import runner %s for %s", self.runner_name, self.slug) else: self.runner = runner_class(self.config) 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_game_by_field(self.slug, 'slug', all=True) 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) self.config.remove() shortcuts.remove_launcher(self.slug, self.id, desktop=True, menu=True) return from_library def save(self): self.config.save() self.id = pga.add_or_update( name=self.name, runner=self.runner_name, slug=self.slug, directory=self.directory, installed=self.is_installed, configpath=self.config.game_config_id, steamid=self.steamid, id=self.id ) 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: {}".format( runtime_updater.current_updates) ) dialogs.ErrorDialog("Runtime currently updating", "Game might not work as expected") return True def play(self): """Launch the game.""" if not self.runner: dialogs.ErrorDialog("Invalid game configuration: Missing runner") self.state = self.STATE_STOPPED return if not self.prelaunch(): self.state = self.STATE_STOPPED return if hasattr(self.runner, 'prelaunch'): jobs.AsyncCall(self.runner.prelaunch, self.do_play) else: self.do_play(True) def do_play(self, prelaunched, _error=None): if not prelaunched: self.state = self.STATE_STOPPED return system_config = self.runner.system_config self.original_outputs = display.get_outputs() gameplay_info = self.runner.play() env = {} logger.debug("Launching %s: %s" % (self.name, gameplay_info)) if 'error' in gameplay_info: show_error_message(gameplay_info) self.state = self.STATE_STOPPED return 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': 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 os.path.exists(self.killswitch): # Prevent setting a killswitch to a file that doesn't exists self.killswitch = None # Command launch_arguments = gameplay_info['command'] primusrun = system_config.get('primusrun') if primusrun and system.find_executable('primusrun'): launch_arguments.insert(0, 'primusrun') xephyr = system_config.get('xephyr') or 'off' if xephyr != 'off': if xephyr == '8bpp': xephyr_depth = '8' else: xephyr_depth = '16' xephyr_resolution = system_config.get('xephyr_resolution') or '640x480' xephyr_command = ['Xephyr', ':2', '-ac', '-screen', xephyr_resolution + 'x' + xephyr_depth, '-glamor', '-reset', '-terminate', '-fullscreen'] xephyr_thread = LutrisThread(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' prefix_command = system_config.get("prefix_command") or '' if prefix_command.strip(): launch_arguments.insert(0, prefix_command) 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 return # Env vars game_env = gameplay_info.get('env') or {} env.update(game_env) system_env = system_config.get('env') or {} env.update(system_env) ld_preload = gameplay_info.get('ld_preload') or '' env["LD_PRELOAD"] = ld_preload # Runtime management ld_library_path = "" if self.runner.use_runtime(): runtime_env = runtime.get_env() if 'STEAM_RUNTIME' in runtime_env and 'STEAM_RUNTIME' not in env: env['STEAM_RUNTIME'] = runtime_env['STEAM_RUNTIME'] if 'LD_LIBRARY_PATH' in runtime_env: ld_library_path = runtime_env['LD_LIBRARY_PATH'] game_ld_libary_path = gameplay_info.get('ld_library_path') if game_ld_libary_path: if not ld_library_path: ld_library_path = '$LD_LIBRARY_PATH' ld_library_path = ":".join(game_ld_libary_path, ld_library_path) env["LD_LIBRARY_PATH"] = ld_library_path # /Env vars self.game_thread = LutrisThread(launch_arguments, runner=self.runner, env=env, rootpid=gameplay_info.get('rootpid'), term=terminal) if hasattr(self.runner, 'stop'): self.game_thread.set_stop_command(self.runner.stop) self.game_thread.start() self.state = self.STATE_RUNNING if 'joy2key' in gameplay_info: self.joy2key(gameplay_info['joy2key']) xboxdrv_config = system_config.get('xboxdrv') if xboxdrv_config: self.xboxdrv_start(xboxdrv_config) self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.beat) def joy2key(self, config): """Run a joy2key thread.""" if not system.find_executable('joy2key'): logger.error("joy2key is not installed") return win = "grep %s" % config['window'] if 'notwindow' in config: win += ' | grep -v %s' % config['notwindow'] wid = "xwininfo -root -tree | %s | awk '{print $1}'" % win buttons = config['buttons'] axis = "Left Right Up Down" rcfile = os.path.expanduser("~/.joy2keyrc") rc_option = '-rcfile %s' % rcfile if os.path.exists(rcfile) else '' command = "sleep 5 " command += "&& joy2key $(%s) -X %s -buttons %s -axis %s" % ( wid, rc_option, buttons, axis ) joy2key_thread = LutrisThread(command) self.game_thread.attach_thread(joy2key_thread) joy2key_thread.start() def xboxdrv_start(self, config): command = [ "pkexec", "xboxdrv", "--daemon", "--detach-kernel-driver", "--dbus", "session", "--silent" ] + config.split() logger.debug("[xboxdrv] %s", ' '.join(command)) self.xboxdrv_thread = LutrisThread(command) self.xboxdrv_thread.set_stop_command(self.xboxdrv_stop) self.xboxdrv_thread.start() def xboxdrv_stop(self): os.system("pkexec xboxdrvctl --shutdown") if os.path.exists("/usr/share/lutris/bin/resetxpad"): os.system("pkexec /usr/share/lutris/bin/resetxpad") 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 self.game_log = self.game_thread.stdout killswitch_engage = self.killswitch and \ not os.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 return True def stop(self): self.state = self.STATE_STOPPED if self.runner.system_config.get('xboxdrv'): self.xboxdrv_thread.stop() if self.game_thread: jobs.AsyncCall(self.game_thread.stop, None, killall=True) def on_game_quit(self): """Restore some settings and cleanup after game quit.""" self.heartbeat = None quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("%s stopped at %s", self.name, quit_time) self.state = self.STATE_STOPPED 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.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() if self.runner.system_config.get('xboxdrv') \ and self.xboxdrv_thread.is_running: self.xboxdrv_thread.stop() if self.game_thread: self.game_thread.stop() 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>")
class Game(object): """This class takes cares about loading the configuration for a game and running it. """ def __init__(self, slug): self.slug = slug self.runner = None self.game_thread = None self.heartbeat = None self.config = None game_data = pga.get_game_by_slug(slug) self.runner_name = game_data.get('runner') or '' self.directory = game_data.get('directory') or '' self.name = game_data.get('name') or '' self.is_installed = bool(game_data.get('installed')) or False self.year = game_data.get('year') or '' self.load_config() self.resolution_changed = False self.original_outputs = None def __repr__(self): return self.__unicode__() def __unicode__(self): value = self.name if self.runner_name: value += " (%s)" % self.runner_name return value def get_browse_dir(self): """Return the path to open with the Browse Files action.""" return self.runner.browse_dir def load_config(self): """Load the game's configuration.""" self.config = LutrisConfig(game=self.slug) if self.is_installed: runner_class = import_runner(self.runner_name) if runner_class: self.runner = runner_class(self.config) else: logger.error("Unable to import runner %s for %s", self.runner_name, self.slug) def remove(self, from_library=False, from_disk=False): if from_disk: self.runner.remove_game_data(game_path=self.directory) if from_library: pga.delete_game(self.slug) self.config.remove() else: pga.set_uninstalled(self.slug) 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 hasattr(self.runner, 'prelaunch'): return self.runner.prelaunch() return True def play(self): """Launch the game.""" if not self.runner: dialogs.ErrorDialog("Invalid game configuration: Missing runner") return False if not self.prelaunch(): return False system_config = self.runner.system_config self.original_outputs = display.get_outputs() gameplay_info = self.runner.play() logger.debug("Launching %s: %s" % (self.name, gameplay_info)) if 'error' in gameplay_info: show_error_message(gameplay_info) return False launch_arguments = gameplay_info['command'] restrict_to_display = system_config.get('display') if restrict_to_display: display.turn_off_except(restrict_to_display) self.resolution_changed = True resolution = system_config.get('resolution') if resolution: display.change_resolution(resolution) self.resolution_changed = True if system_config.get('reset_pulse'): audio.reset_pulse() prefix_command = system_config.get("prefix_command", '').strip() if prefix_command and system.find_executable(prefix_command): launch_arguments.insert(0, prefix_command) ld_preload = gameplay_info.get('ld_preload') if ld_preload: launch_arguments.insert(0, 'LD_PRELOAD="{}"'.format(ld_preload)) ld_library_path = [] runtime64_path = os.path.join(settings.RUNTIME_DIR, "lib64") if os.path.exists(runtime64_path): ld_library_path.append(runtime64_path) runtime32_path = os.path.join(settings.RUNTIME_DIR, "lib32") if os.path.exists(runtime32_path): ld_library_path.append(runtime32_path) game_ld_libary_path = gameplay_info.get('ld_library_path') if game_ld_libary_path: ld_library_path.append(game_ld_libary_path) if ld_library_path: ld_full = ':'.join(ld_library_path) ld_arg = 'LD_LIBRARY_PATH="{}:$LD_LIBRARY_PATH"'.format(ld_full) launch_arguments.insert(0, ld_arg) env = gameplay_info.get('env') or [] for var in env: launch_arguments.insert(0, var) killswitch = system_config.get('killswitch') self.game_thread = LutrisThread(" ".join(launch_arguments), path=self.runner.working_dir, killswitch=killswitch) if hasattr(self.runner, 'stop'): self.game_thread.set_stop_command(self.runner.stop) self.game_thread.start() if 'joy2key' in gameplay_info: self.joy2key(gameplay_info['joy2key']) xboxdrv_config = system_config.get('xboxdrv') if xboxdrv_config: self.xboxdrv_start(xboxdrv_config) if self.runner.is_watchable: # Create heartbeat every self.heartbeat = GLib.timeout_add(5000, self.poke_process) def joy2key(self, config): """Run a joy2key thread.""" if not system.find_executable('joy2key'): logger.error("joy2key is not installed") return win = "grep %s" % config['window'] if 'notwindow' in config: win += ' | grep -v %s' % config['notwindow'] wid = "xwininfo -root -tree | %s | awk '{print $1}'" % win buttons = config['buttons'] axis = "Left Right Up Down" rcfile = os.path.expanduser("~/.joy2keyrc") rc_option = '-rcfile %s' % rcfile if os.path.exists(rcfile) else '' command = "sleep 5 " command += "&& joy2key $(%s) -X %s -buttons %s -axis %s" % ( wid, rc_option, buttons, axis ) joy2key_thread = LutrisThread(command) self.game_thread.attach_thread(joy2key_thread) joy2key_thread.start() def xboxdrv_start(self, config): command = ("pkexec xboxdrv --daemon --detach-kernel-driver " "--dbus session --silent %s" % config) logger.debug("xboxdrv command: %s", command) self.xboxdrv_thread = LutrisThread(command) self.xboxdrv_thread.set_stop_command(self.xboxdrv_stop) self.xboxdrv_thread.start() def xboxdrv_stop(self): os.system("pkexec xboxdrvctl --shutdown") def poke_process(self): """Watch game's process.""" if not self.game_thread.pid: self.quit_game() return False return True def quit_game(self): """Quit the game and cleanup.""" self.heartbeat = None quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()) logger.debug("game has quit at %s" % quit_time) if self.resolution_changed\ or self.runner.system_config.get('reset_desktop'): display.change_resolution(self.original_outputs) if self.runner.system_config.get('restore_gamma'): display.restore_gamma() if self.runner.system_config.get('xboxdrv'): self.xboxdrv_thread.stop() if self.game_thread: self.game_thread.stop()