From 021a687fea219621b9ccbfc2ed3d0a1e7b860743 Mon Sep 17 00:00:00 2001 From: Mathieu Comandon Date: Mon, 30 Jan 2023 19:25:16 -0800 Subject: [PATCH] Build a cache with game paths --- lutris/game_actions.py | 2 +- lutris/gui/addgameswindow.py | 2 +- .../{add_game.py => add_game_dialog.py} | 0 lutris/gui/dialogs/game_import.py | 32 ++++----- lutris/gui/dialogs/uninstall_game.py | 4 +- lutris/gui/lutriswindow.py | 7 +- lutris/scanners/lutris.py | 70 +++++++++++++++++++ lutris/startup.py | 2 + tests/test_dialogs.py | 2 +- 9 files changed, 96 insertions(+), 25 deletions(-) rename lutris/gui/config/{add_game.py => add_game_dialog.py} (100%) diff --git a/lutris/game_actions.py b/lutris/game_actions.py index 32bffd5e1..94615368a 100644 --- a/lutris/game_actions.py +++ b/lutris/game_actions.py @@ -12,7 +12,7 @@ from lutris.config import duplicate_game_config from lutris.database.games import add_game, get_game_by_field, get_unusued_game_name from lutris.game import Game from lutris.gui import dialogs -from lutris.gui.config.add_game import AddGameDialog +from lutris.gui.config.add_game_dialog import AddGameDialog from lutris.gui.config.edit_game import EditGameConfigDialog from lutris.gui.dialogs import QuestionDialog from lutris.gui.dialogs.log import LogWindow diff --git a/lutris/gui/addgameswindow.py b/lutris/gui/addgameswindow.py index 1657d4510..800d7af37 100644 --- a/lutris/gui/addgameswindow.py +++ b/lutris/gui/addgameswindow.py @@ -5,7 +5,7 @@ from gi.repository import Gio, GLib, Gtk from lutris import api from lutris.exceptions import watch_errors -from lutris.gui.config.add_game import AddGameDialog +from lutris.gui.config.add_game_dialog import AddGameDialog from lutris.gui.dialogs import ErrorDialog from lutris.gui.dialogs.game_import import ImportGameDialog from lutris.gui.widgets.common import FileChooserEntry diff --git a/lutris/gui/config/add_game.py b/lutris/gui/config/add_game_dialog.py similarity index 100% rename from lutris/gui/config/add_game.py rename to lutris/gui/config/add_game_dialog.py diff --git a/lutris/gui/dialogs/game_import.py b/lutris/gui/dialogs/game_import.py index da8ba0f88..4f4f20968 100644 --- a/lutris/gui/dialogs/game_import.py +++ b/lutris/gui/dialogs/game_import.py @@ -2,18 +2,19 @@ from collections import OrderedDict from copy import deepcopy from gettext import gettext as _ -from gi.repository import Gtk, GLib +from gi.repository import GLib, Gtk from lutris.config import write_game_config -from lutris.database.games import add_game, get_games +from lutris.database.games import add_game from lutris.game import Game from lutris.gui.dialogs import ModalDialog from lutris.scanners.default_installers import DEFAULT_INSTALLERS +from lutris.scanners.lutris import add_to_path_cache, get_path_cache from lutris.scanners.tosec import clean_rom_name, guess_platform, search_tosec_by_md5 from lutris.services.lutris import download_lutris_media from lutris.util.jobs import AsyncCall from lutris.util.log import logger -from lutris.util.strings import slugify, gtk_safe +from lutris.util.strings import gtk_safe, slugify from lutris.util.system import get_md5_hash, get_md5_in_zip @@ -87,17 +88,7 @@ class ImportGameDialog(ModalDialog): logger.debug('Game not launched') def search_checksums(self): - all_games = get_games() - - def find_game(filepath): - for db_game in all_games: - g = Game(db_game["id"]) - if not g.config: - continue - for _key, value in g.config.game_config.items(): - if value == filepath: - logger.debug("Found %s", g) - return g + game_path_cache = get_path_cache() def show_progress(filepath, message): # It's not safe to directly update labels from a worker thread, so @@ -108,11 +99,13 @@ class ImportGameDialog(ModalDialog): for filename in self.files: try: show_progress(filename, _("Looking for installed game...")) - game = find_game(filename) - if game: - # Found a game to launch instead of installing, but we can't safely - # do this on this thread. - result = [{"name": game.name, "game": game, "roms": []}] + if filename in game_path_cache.values(): + for game_id in game_path_cache: + if game_path_cache[game_id] == filename: + # Found a game to launch instead of installing, but we can't safely + # do this on this thread. + game = Game(game_id) + result = [{"name": game.name, "game": game, "roms": []}] else: show_progress(filename, _("Calculating checksum...")) if filename.lower().endswith(".zip"): @@ -206,4 +199,5 @@ class ImportGameDialog(ModalDialog): configpath=configpath ) download_lutris_media(slug) + add_to_path_cache(Game(game_id)) return game_id diff --git a/lutris/gui/dialogs/uninstall_game.py b/lutris/gui/dialogs/uninstall_game.py index 02f1919b6..796109b63 100644 --- a/lutris/gui/dialogs/uninstall_game.py +++ b/lutris/gui/dialogs/uninstall_game.py @@ -5,10 +5,11 @@ from gi.repository import Gtk, Pango from lutris.database.games import get_games from lutris.game import Game from lutris.gui.dialogs import ModalDialog, QuestionDialog +from lutris.scanners.lutris import remove_from_path_cache from lutris.util.jobs import AsyncCall from lutris.util.log import logger from lutris.util.strings import gtk_safe, human_size -from lutris.util.system import get_disk_size, is_removeable, reverse_expanduser, path_exists +from lutris.util.system import get_disk_size, is_removeable, path_exists, reverse_expanduser class UninstallGameDialog(ModalDialog): @@ -99,6 +100,7 @@ class UninstallGameDialog(ModalDialog): self.folder_label.set_markup(_("Uninstalling game and deleting files...")) else: self.folder_label.set_markup(_("Uninstalling game...")) + remove_from_path_cache(self.game) self.game.remove(self.delete_files) self.destroy() diff --git a/lutris/gui/lutriswindow.py b/lutris/gui/lutriswindow.py index ce7316419..14c850c60 100644 --- a/lutris/gui/lutriswindow.py +++ b/lutris/gui/lutriswindow.py @@ -27,6 +27,7 @@ from lutris.gui.widgets.game_bar import GameBar from lutris.gui.widgets.gi_composites import GtkTemplate from lutris.gui.widgets.sidebar import LutrisSidebar from lutris.gui.widgets.utils import load_icon_theme, open_uri +from lutris.scanners.lutris import remove_from_path_cache # pylint: disable=no-member from lutris.services.base import BaseService from lutris.services.lutris import LutrisService @@ -132,7 +133,7 @@ class LutrisWindow(Gtk.ApplicationWindow, GObject.add_emission_hook(BaseService, "service-games-loaded", self.on_service_games_updated) GObject.add_emission_hook(Game, "game-updated", self.on_game_updated) GObject.add_emission_hook(Game, "game-stopped", self.on_game_stopped) - GObject.add_emission_hook(Game, "game-removed", self.on_game_collection_changed) + GObject.add_emission_hook(Game, "game-removed", self.on_game_removed) GObject.add_emission_hook(Game, "game-unhandled_error", self.on_game_unhandled_error) def _init_actions(self): @@ -874,9 +875,11 @@ class LutrisWindow(Gtk.ApplicationWindow, self.game_store.remove_game(game.id) return True - def on_game_collection_changed(self, _sender): + def on_game_removed(self, game): """Simple method used to refresh the view""" + remove_from_path_cache(game) self.emit("view-updated") + return True @watch_errors() diff --git a/lutris/scanners/lutris.py b/lutris/scanners/lutris.py index 64b9ee93b..2a09fa813 100644 --- a/lutris/scanners/lutris.py +++ b/lutris/scanners/lutris.py @@ -1,13 +1,19 @@ +import json import os +import time +from lutris import settings from lutris.api import get_api_games, get_game_installers from lutris.database.games import get_games +from lutris.game import Game from lutris.installer.errors import MissingGameDependency from lutris.installer.interpreter import ScriptInterpreter from lutris.services.lutris import download_lutris_media from lutris.util.log import logger from lutris.util.strings import slugify +GAME_PATH_CACHE_PATH = os.path.join(settings.CACHE_DIR, "game-paths.json") + def get_game_slugs_and_folders(dirname): """Scan a directory for games previously installed with lutris""" @@ -95,3 +101,67 @@ def scan_directory(dirname): installed_map = {slug: folder for slug, folder in slugs_map.items() if slug in slugs_installed} missing_map = {slug: folder for slug, folder in slugs_map.items() if slug not in slugs_installed} return installed_map, missing_map + + +def get_path_from_config(game): + """Return the path of the main entry point for a game""" + if not game.config: + logger.warning("Game %s has no configuration", game) + return "" + for key in ["exe", "main_file", "iso", "rom"]: + if key in game.config.game_config: + path = game.config.game_config[key] + if not path.startswith("/"): + path = os.path.join(game.directory, path) + return path + logger.warning("No path found in %s", game.config) + return "" + + +def get_game_paths(): + game_paths = {} + all_games = get_games(filters={'installed': 1}) + for db_game in all_games: + game = Game(db_game["id"]) + path = get_path_from_config(game) + if not path: + logger.warning("Game %s has no path", game) + continue + game_paths[db_game["id"]] = path + return game_paths + + +def build_path_cache(): + """Generate a new cache path""" + start_time = time.time() + with open(GAME_PATH_CACHE_PATH, "w", encoding="utf-8") as cache_file: + game_paths = get_game_paths() + json.dump(game_paths, cache_file, indent=2) + end_time = time.time() + logger.debug("Game path cache built in %0.2f seconds", end_time - start_time) + + +def add_to_path_cache(game): + path = get_path_from_config(game) + if not path: + logger.warning("No path for %s", game) + return + current_cache = get_path_cache() + current_cache[game.id] = path + with open(GAME_PATH_CACHE_PATH, "w", encoding="utf-8") as cache_file: + json.dump(current_cache, cache_file, indent=2) + + +def remove_from_path_cache(game): + current_cache = get_path_cache() + if game.id not in current_cache: + return + del current_cache[game.id] + with open(GAME_PATH_CACHE_PATH, "w", encoding="utf-8") as cache_file: + json.dump(current_cache, cache_file, indent=2) + + +def get_path_cache(): + """Return the contents of the path cache file""" + with open(GAME_PATH_CACHE_PATH, encoding="utf-8") as cache_file: + return json.load(cache_file) diff --git a/lutris/startup.py b/lutris/startup.py index 94c1d92cc..438a906dd 100644 --- a/lutris/startup.py +++ b/lutris/startup.py @@ -17,6 +17,7 @@ from lutris.game import Game from lutris.gui.dialogs import DontShowAgainDialog from lutris.runners.json import load_json_runners from lutris.runtime import RuntimeUpdater +from lutris.scanners.lutris import build_path_cache from lutris.services import DEFAULT_SERVICES from lutris.services.lutris import sync_media from lutris.util.graphics import drivers, vkquery @@ -202,6 +203,7 @@ def init_lutris(): if not settings.read_setting(service, section="services"): settings.write_setting(service, True, section="services") cleanup_games() + build_path_cache() class StartupRuntimeUpdater(RuntimeUpdater): diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index 1dfd9b5f6..369a3b90f 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -2,7 +2,7 @@ from unittest import TestCase from lutris import runners from lutris.gui.application import Application -from lutris.gui.config.add_game import AddGameDialog +from lutris.gui.config.add_game_dialog import AddGameDialog # from lutris import settings from lutris.gui.config.common import GameDialogCommon from lutris.gui.views.store import sort_func