diff --git a/lutris/api.py b/lutris/api.py index f813e17e6..c0b57b295 100644 --- a/lutris/api.py +++ b/lutris/api.py @@ -5,9 +5,7 @@ import urllib2 import socket from lutris import settings -from lutris import pga from lutris.util import http -from lutris.util import resources from lutris.util.log import logger @@ -73,114 +71,3 @@ def get_games(slugs): game_set = ';'.join(slugs) url = settings.SITE_URL + "api/v1/game/set/%s/" % game_set return http.download_json(url, params="?format=json")['objects'] - - -def sync(caller=None): - """Synchronize from remote to local library. - - :param caller: The LutrisWindow object - :return: The synchronized games (slugs) - :rtype: set of strings - """ - logger.debug("Syncing game library") - # Get local library - local_library = pga.get_games() - local_slugs = set([game['slug'] for game in local_library]) - logger.debug("%d games in local library", len(local_slugs)) - # Get remote library - remote_library = get_library() - remote_slugs = set([game['slug'] for game in remote_library]) - logger.debug("%d games in remote library (inc. unpublished)", - len(remote_slugs)) - - not_in_local = remote_slugs.difference(local_slugs) - - added = sync_missing_games(not_in_local, remote_library, caller) - updated = sync_game_details(remote_library, caller) - return added.update(updated) - - -def sync_missing_games(not_in_local, remote_library, caller=None): - """Get missing games in local library from remote library. - - :param caller: The LutrisWindow object - :return: The slugs of the added games - :rtype: set - """ - if not not_in_local: - return set() - - for game in remote_library: - slug = game['slug'] - # Sync - if slug in not_in_local: - logger.debug("Adding to local library: %s", slug) - pga.add_game( - game['name'], slug=slug, year=game['year'], - updated=game['updated'], steamid=game['steamid'] - ) - if caller: - caller.add_game_to_view(slug) - else: - not_in_local.discard(slug) - logger.debug("%d games added", len(not_in_local)) - return not_in_local - - -def sync_game_details(remote_library, caller): - """Update local game details, - - :param caller: The LutrisWindow object - :return: The slugs of the updated games. - :rtype: set - """ - if not remote_library: - return set() - - updated = set() - - # Get remote games (TODO: use this when switched API to DRF) - # remote_games = get_games(sorted(local_slugs)) - # if not remote_games: - # return set() - - for game in remote_library: - slug = game['slug'] - sync = False - sync_icons = True - local_game = pga.get_game_by_slug(slug) - if not local_game: - continue - - # Sync updated - if game['updated'] > local_game['updated']: - sync = True - # Sync new fields - else: - for key, value in local_game.iteritems(): - if value or not key in game: - continue - if game[key]: - sync = True - sync_icons = False - if not sync: - continue - - logger.debug("Syncing details for %s" % slug) - pga.add_or_update( - local_game['name'], local_game['runner'], slug, - year=game['year'], updated=game['updated'], - steamid=game['steamid'] - ) - caller.view.update_row(game) - - # Sync icons (TODO: Only update if icon actually updated) - if sync_icons: - resources.download_icon(slug, 'banner', overwrite=True, - callback=caller.on_image_downloaded) - resources.download_icon(slug, 'icon', overwrite=True, - callback=caller.on_image_downloaded) - updated.add(slug) - - logger.debug("%d games updated", len(updated)) - return updated diff --git a/lutris/gui/lutriswindow.py b/lutris/gui/lutriswindow.py index 80934ef7d..449358c3d 100644 --- a/lutris/gui/lutriswindow.py +++ b/lutris/gui/lutriswindow.py @@ -155,7 +155,7 @@ class LutrisWindow(object): self.toggle_connection(False) sync = Sync() async_call( - sync.sync_steam, + sync.sync_steam_local, lambda r, e: async_call(self.sync_icons, None), caller=self ) @@ -166,19 +166,6 @@ class LutrisWindow(object): if self.view.__class__.__name__ == "GameGridView" \ else 'list' - def switch_splash_screen(self): - if self.view.n_games == 0: - self.splash_box.show() - self.games_scrollwindow.hide() - else: - self.splash_box.hide() - self.games_scrollwindow.show() - - def sync_icons(self): - game_list = pga.get_games() - resources.fetch_icons([game_info['slug'] for game_info in game_list], - callback=self.on_image_downloaded) - def connect_signals(self): """Connect signals from the view with the main window. This must be called each time the view is rebuilt. @@ -209,6 +196,59 @@ class LutrisWindow(object): def get_size(self, widget, _): self.window_size = widget.get_size() + def switch_splash_screen(self): + if self.view.n_games == 0: + self.splash_box.show() + self.games_scrollwindow.hide() + else: + self.splash_box.hide() + self.games_scrollwindow.show() + + def switch_view(self, view_type): + """Switch between grid view and list view.""" + logger.debug("Switching view") + self.icon_type = self.get_icon_type(view_type) + self.view.destroy() + self.view = load_view( + view_type, + get_game_list(filter_installed=self.filter_installed), + filter_text=self.search_entry.get_text(), + icon_type=self.icon_type + ) + self.view.contextual_menu = self.menu + self.connect_signals() + self.games_scrollwindow.add(self.view) + self.view.show_all() + self.view.check_resize() + # Note: set_active(True *or* False) apparently makes ALL the menuitems + # in the group send the activate signal... + if self.icon_type == 'banner_small': + self.banner_small_menuitem.set_active(True) + if self.icon_type == 'icon': + self.icon_menuitem.set_active(True) + if self.icon_type == 'banner': + self.banner_menuitem.set_active(True) + + def sync_library(self): + def set_library_synced(result, error): + self.set_status("Library synced") + self.switch_splash_screen() + self.set_status("Syncing library") + sync = Sync() + async_call( + sync.sync_all, + lambda r, e: async_call(self.sync_icons, set_library_synced), + caller=self + ) + + def sync_icons(self): + game_list = pga.get_games() + resources.fetch_icons([game_info['slug'] for game_info in game_list], + callback=self.on_image_downloaded) + + def set_status(self, text): + self.status_label.set_text(text) + def refresh_status(self): """Refresh status bar.""" if self.running_game: @@ -236,12 +276,28 @@ class LutrisWindow(object): """Open the about dialog.""" dialogs.AboutDialog() + def reset(self, *args): + """Reset the desktop to it's initial state.""" + if self.running_game: + self.running_game.quit_game() + self.status_label.set_text("Stopped %s" % self.running_game.name) + self.running_game = None + self.stop_button.set_sensitive(False) + # Callbacks def on_connect(self, *args): """Callback when a user connects to his account.""" login_dialog = dialogs.ClientLoginDialog() login_dialog.connect('connected', self.on_connect_success) + def on_connect_success(self, dialog, credentials): + if isinstance(credentials, str): + username = credentials + else: + username = credentials["username"] + self.toggle_connection(True, username) + self.sync_library() + def on_disconnect(self, *args): api.disconnect() self.toggle_connection(False) @@ -262,14 +318,6 @@ class LutrisWindow(object): logger.info(connection_status) connection_label.set_text(connection_status) - def on_connect_success(self, dialog, credentials): - if isinstance(credentials, str): - username = credentials - else: - username = credentials["username"] - self.toggle_connection(True, username) - self.sync_library() - def on_destroy(self, *args): """Signal for window close.""" view_type = 'grid' if 'GridView' in str(type(self.view)) else 'list' @@ -280,9 +328,6 @@ class LutrisWindow(object): Gtk.main_quit(*args) logger.debug("Quitting lutris") - def on_game_installed(self, view, slug): - view.set_installed(Game(slug)) - def on_runners_activate(self, _widget, _data=None): """Callback when manage runners is activated.""" RunnersDialog() @@ -302,10 +347,6 @@ class LutrisWindow(object): def on_pga_menuitem_activate(self, _widget, _data=None): dialogs.PgaSourceDialog() - def on_image_downloaded(self, game_slug): - is_installed = Game(game_slug).is_installed - self.view.update_image(game_slug, is_installed) - def on_search_entry_changed(self, widget): self.view.emit('filter-updated', widget.get_text()) @@ -325,29 +366,6 @@ class LutrisWindow(object): else: InstallerDialog(game_slug, self) - def set_status(self, text): - self.status_label.set_text(text) - - def sync_library(self): - def set_library_synced(result, error): - self.set_status("Library synced") - self.switch_splash_screen() - self.set_status("Syncing library") - sync = Sync() - async_call( - sync.sync_all, - lambda r, e: async_call(self.sync_icons, set_library_synced), - caller=self - ) - - def reset(self, *args): - """Reset the desktop to it's initial state.""" - if self.running_game: - self.running_game.quit_game() - self.status_label.set_text("Stopped %s" % self.running_game.name) - self.running_game = None - self.stop_button.set_sensitive(False) - def game_selection_changed(self, _widget): # Emulate double click to workaround GTK bug #484640 # https://bugzilla.gnome.org/show_bug.cgi?id=484640 @@ -363,6 +381,27 @@ class LutrisWindow(object): self.play_button.set_sensitive(sensitive) self.delete_button.set_sensitive(sensitive) + def on_game_installed(self, view, slug): + view.set_installed(Game(slug)) + + def on_image_downloaded(self, game_slug): + is_installed = Game(game_slug).is_installed + self.view.update_image(game_slug, is_installed) + + def add_manually(self, *args): + game = Game(self.view.selected_game) + add_game_dialog = AddGameDialog(self, game) + add_game_dialog.run() + if add_game_dialog.installed: + self.view.set_installed(game) + + def add_game(self, _widget, _data=None): + """Add a new game.""" + add_game_dialog = AddGameDialog(self) + add_game_dialog.run() + if add_game_dialog.runner_name and add_game_dialog.slug: + self.add_game_to_view(add_game_dialog.slug) + def add_game_to_view(self, slug): if not slug: raise ValueError("Missing game slug") @@ -373,20 +412,6 @@ class LutrisWindow(object): self.switch_splash_screen() GLib.idle_add(do_add_game) - def add_game(self, _widget, _data=None): - """Add a new game.""" - add_game_dialog = AddGameDialog(self) - add_game_dialog.run() - if add_game_dialog.runner_name and add_game_dialog.slug: - self.add_game_to_view(add_game_dialog.slug) - - def add_manually(self, *args): - game = Game(self.view.selected_game) - add_game_dialog = AddGameDialog(self, game) - add_game_dialog.run() - if add_game_dialog.installed: - self.view.set_installed(game) - def on_remove_game(self, _widget, _data=None): selected_game = self.view.selected_game UninstallGameDialog(slug=selected_game, @@ -439,31 +464,6 @@ class LutrisWindow(object): self.grid_view_menuitem.set_active(view_type == 'grid') self.list_view_menuitem.set_active(view_type == 'list') - def switch_view(self, view_type): - """Switch between grid view and list view.""" - logger.debug("Switching view") - self.icon_type = self.get_icon_type(view_type) - self.view.destroy() - self.view = load_view( - view_type, - get_game_list(filter_installed=self.filter_installed), - filter_text=self.search_entry.get_text(), - icon_type=self.icon_type - ) - self.view.contextual_menu = self.menu - self.connect_signals() - self.games_scrollwindow.add(self.view) - self.view.show_all() - self.view.check_resize() - # Note: set_active(True *or* False) apparently makes ALL the menuitems - # in the group send the activate signal... - if self.icon_type == 'banner_small': - self.banner_small_menuitem.set_active(True) - if self.icon_type == 'icon': - self.icon_menuitem.set_active(True) - if self.icon_type == 'banner': - self.banner_menuitem.set_active(True) - def on_icon_type_activate(self, menuitem): icon_type = menuitem.get_name() if icon_type == self.view.icon_type or not menuitem.get_active(): diff --git a/lutris/gui/widgets.py b/lutris/gui/widgets.py index 5f8456e6e..9d91a0d3a 100644 --- a/lutris/gui/widgets.py +++ b/lutris/gui/widgets.py @@ -207,10 +207,10 @@ class GameView(object): """Update a game row to show as installed""" row = self.get_row_by_slug(game.slug) if not row: - logger.error("Can't find row for %s", game.slug) - return - row[COL_RUNNER] = game.runner_name - self.update_image(game.slug, is_installed=True) + self.add_game(game) + else: + row[COL_RUNNER] = game.runner_name + self.update_image(game.slug, is_installed=True) def set_uninstalled(self, game_slug): """Update a game row to show as uninstalled""" diff --git a/lutris/installer.py b/lutris/installer.py index f02e2a1e1..d81ffdd20 100644 --- a/lutris/installer.py +++ b/lutris/installer.py @@ -159,10 +159,13 @@ class ScriptInterpreter(object): "Downloading file %d of %d", len(self.game_files) + 1, len(self.script["files"]) ) + file_index = len(self.game_files) try: - self._download_file(self.script["files"][len(self.game_files)]) + current_file = self.script["files"][file_index] except KeyError: - raise ScriptingError("Badly formatted script", self.script) + raise ScriptingError("Error getting file %d in %s", + file_index, self.script['files']) + self._download_file(current_file) else: self.current_command = 0 self._prepare_commands() @@ -553,7 +556,8 @@ class ScriptInterpreter(object): def _append_steam_data_to_files(self, runner_class): steam_runner = runner_class() - data_path = steam_runner.get_game_data_path(self.steam_data['appid']) + data_path = steam_runner.get_game_path_from_appid( + self.steam_data['appid']) if not data_path or not os.path.exists(data_path): raise ScriptingError("Unable to get Steam data for game") logger.debug("got data path: %s" % data_path) @@ -591,7 +595,7 @@ class ScriptInterpreter(object): def install_steam_game(self, runner_class): steam_runner = runner_class() appid = self.steam_data['appid'] - if not steam_runner.get_game_data_path(appid): + if not steam_runner.get_game_path_from_appid(appid): logger.debug("Installing steam game %s" % appid) # Here the user must wait for the game to finish installing, a # better way to handle this would be to poll StateFlags on the diff --git a/lutris/runners/dosbox.py b/lutris/runners/dosbox.py index 60477dd72..e10f4a8d7 100644 --- a/lutris/runners/dosbox.py +++ b/lutris/runners/dosbox.py @@ -84,6 +84,10 @@ class dosbox(Runner): "x64": "dosbox-0.74-x86_64.tar.gz", } + @property + def main_file(self): + return self.settings.get('game', {}).get('main_file') or '' + @property def browse_dir(self): """Return the path to open with the Browse Files action.""" @@ -99,7 +103,7 @@ class dosbox(Runner): return os.path.join(settings.RUNNER_DIR, "dosbox/bin/dosbox") def play(self): - main_file = self.settings["game"]["main_file"] + main_file = self.main_file if not os.path.exists(main_file): return {'error': "FILE_NOT_FOUND", 'file': main_file} diff --git a/lutris/runners/steam.py b/lutris/runners/steam.py index 3e9829e20..6ced8cce5 100644 --- a/lutris/runners/steam.py +++ b/lutris/runners/steam.py @@ -58,7 +58,7 @@ class steam(Runner): @property def game_path(self): - appid = self.settings['game'].get('appid') + appid = self.config['game'].get('appid') for apps_path in self.get_steamapps_dirs(): game_path = get_path_from_appmanifest(apps_path, appid) if game_path: @@ -84,6 +84,14 @@ class steam(Runner): if os.path.exists(path): return path + def get_game_path_from_appid(self, appid): + """Return the game directory""" + for apps_path in self.get_steamapps_dirs(): + game_path = get_path_from_appmanifest(apps_path, appid) + if game_path: + return game_path + logger.warning("Data path for SteamApp %s not found.", appid) + def get_steamapps_dirs(self): """Return a list of the Steam library main + custom folders.""" dirs = [] diff --git a/lutris/runners/winesteam.py b/lutris/runners/winesteam.py index 532a0ebfb..5003ca1dc 100644 --- a/lutris/runners/winesteam.py +++ b/lutris/runners/winesteam.py @@ -69,12 +69,18 @@ class winesteam(wine.wine): { 'option': 'appid', 'type': 'string', - 'label': 'appid' + 'label': 'Application ID', + 'help': ("The application ID can be retrieved from the game's " + "page at steampowered.com. Example: 235320 is the " + "app ID for Original War in: \n" + "http://store.steampowered.com/app/235320/") }, { 'option': 'args', 'type': 'string', - 'label': 'arguments' + 'label': 'Arguments', + 'help': ("Windows command line arguments used when launching " + "Steam") }, { 'option': 'prefix', @@ -173,6 +179,14 @@ class winesteam(wine.wine): apps = config['apps'] return apps.keys() + def get_game_path_from_appid(self, appid): + """Return the game directory""" + for apps_path in self.get_steamapps_dirs(): + game_path = get_path_from_appmanifest(apps_path, appid) + if game_path: + return game_path + logger.warning("Data path for SteamApp %s not found.", appid) + def get_steamapps_dirs(self): """Return a list of the Steam library main + custom folders.""" dirs = [] diff --git a/lutris/sync.py b/lutris/sync.py index bf9211911..307b7c248 100644 --- a/lutris/sync.py +++ b/lutris/sync.py @@ -2,11 +2,12 @@ """Synchronization of the game library with the server and other platforms.""" import os -from lutris.util.log import logger from lutris import api, pga from lutris.game import Game from lutris.runners.steam import steam from lutris.runners.winesteam import winesteam +from lutris.util import resources +from lutris.util.log import logger class Sync(object): @@ -14,10 +15,120 @@ class Sync(object): self.library = pga.get_games() def sync_all(self, caller): - api.sync(caller) - self.sync_steam(caller) + self.sync_from_remote(caller) + self.sync_steam_local(caller) - def sync_steam(self, caller): + def sync_from_remote(self, caller=None): + """Synchronize from remote to local library. + + :param caller: The LutrisWindow object + :return: The synchronized games (slugs) + :rtype: set of strings + """ + logger.debug("Syncing game library") + # Get local library + local_slugs = set([game['slug'] for game in self.library]) + logger.debug("%d games in local library", len(local_slugs)) + # Get remote library + remote_library = api.get_library() + remote_slugs = set([game['slug'] for game in remote_library]) + logger.debug("%d games in remote library (inc. unpublished)", + len(remote_slugs)) + + not_in_local = remote_slugs.difference(local_slugs) + + added = self.sync_missing_games(not_in_local, remote_library, caller) + updated = self.sync_game_details(remote_library, caller) + return added.update(updated) + + @staticmethod + def sync_missing_games(not_in_local, remote_library, caller=None): + """Get missing games in local library from remote library. + + :param caller: The LutrisWindow object + :return: The slugs of the added games + :rtype: set + """ + if not not_in_local: + return set() + + for game in remote_library: + slug = game['slug'] + # Sync + if slug in not_in_local: + logger.debug("Adding to local library: %s", slug) + pga.add_game( + game['name'], slug=slug, year=game['year'], + updated=game['updated'], steamid=game['steamid'] + ) + if caller: + caller.add_game_to_view(slug) + else: + not_in_local.discard(slug) + logger.debug("%d games added", len(not_in_local)) + return not_in_local + + @staticmethod + def sync_game_details(remote_library, caller): + """Update local game details, + + :param caller: The LutrisWindow object + :return: The slugs of the updated games. + :rtype: set + """ + if not remote_library: + return set() + updated = set() + + # Get remote games (TODO: use this when switched API to DRF) + # remote_games = get_games(sorted(local_slugs)) + # if not remote_games: + # return set() + + for game in remote_library: + slug = game['slug'] + sync = False + sync_icons = True + local_game = pga.get_game_by_slug(slug) + if not local_game: + continue + + # Sync updated + if game['updated'] > local_game['updated']: + sync = True + # Sync new DB fields + else: + for key, value in local_game.iteritems(): + if value or not key in game: + continue + if game[key]: + sync = True + sync_icons = False + if not sync: + continue + + logger.debug("Syncing details for %s" % slug) + pga.add_or_update( + local_game['name'], local_game['runner'], slug, + year=game['year'], updated=game['updated'], + steamid=game['steamid'] + ) + caller.view.update_row(game) + + # Sync icons (TODO: Only update if icon actually updated) + if sync_icons: + resources.download_icon(slug, 'banner', overwrite=True, + callback=caller.on_image_downloaded) + resources.download_icon(slug, 'icon', overwrite=True, + callback=caller.on_image_downloaded) + updated.add(slug) + + logger.debug("%d games updated", len(updated)) + return updated + + def sync_steam_local(self, caller): + """Sync Steam games in library with Steam and Wine Steam""" + logger.debug("Syncing local steam games") steam_ = steam() winesteam_ = winesteam() @@ -33,19 +144,21 @@ class Sync(object): # Set installed (steam linux only) if installed_in_steam and not game_info['installed']: + logger.debug("Setting %s as installed" % game_info['name']) pga.add_or_update(game_info['name'], 'steam', game_info['slug'], installed=1) game.config.game_config.update({'game': {'appid': str(steamid)}}) game.config.save() - caller.view.set_installed(game) + caller.view.set_installed(Game(game_info['slug'])) continue # Set uninstalled if not (installed_in_steam or installed_in_winesteam) \ and game_info['installed'] \ and game_info['runner'] in ['steam', 'winesteam']: + logger.debug("Setting %s as uninstalled" % game_info['name']) pga.add_or_update(game_info['name'], '', game_info['slug'], installed=0) @@ -53,6 +166,7 @@ class Sync(object): @staticmethod def _get_installed_steamapps(runner): + """Return a list of appIDs of the installed Steam games.""" if not runner.is_installed(): return [] installed = []