From 232197c7a9e18a3bef86616f96ebbf9701a007ac Mon Sep 17 00:00:00 2001 From: cyberrumor Date: Sat, 9 Mar 2024 19:06:55 -0800 Subject: [PATCH] Add experimental custom path support Ammo will now search in the --conf dir for files ending in .json. The filename represents the name of a game, and the contents define the properties. Games defined like this will appear in the game selection menu and should be possible to manage. This is primarily written to get GoG up and running, but I'm currently running into an issue where the GoG version of the game won't load plugins and overwrites Plugins.txt with a default every time I try to launch the game. In any case, see https://github.com/cyberrumor/ammo/issues/42 for an example of a game configured like this. --- ammo/game_controller.py | 76 +++++++++++++++++++++++++++-------------- ammo/mod_controller.py | 4 +-- 2 files changed, 52 insertions(+), 28 deletions(-) diff --git a/ammo/game_controller.py b/ammo/game_controller.py index 17868a0..88aca98 100755 --- a/ammo/game_controller.py +++ b/ammo/game_controller.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import json import re from typing import Union from dataclasses import ( @@ -7,6 +8,7 @@ ) from enum import Enum from pathlib import Path + from .mod_controller import ( Game, ModController, @@ -20,7 +22,19 @@ @dataclass(frozen=True, kw_only=True) class GameSelection: name: field(default_factory=str) - library: field(default_factory=Path) + directory: field(default_factory=Path) + data: field(default_factory=Path) + dlc_file: field(default_factory=Path) + plugin_file: field(default_factory=Path) + + def __post_init__(self): + """ + Validate that all paths are absolute. + """ + assert self.directory.is_absolute() + assert self.data.is_absolute() + assert self.dlc_file.is_absolute() + assert self.plugin_file.is_absolute() class GameController(Controller): @@ -45,15 +59,15 @@ def __init__(self, args): "Starfield": "1716740", } self.downloads = self.args.downloads.resolve(strict=True) + self.games: list[GameSelection] = [] + # Find games from instances of Steam + self.libraries: list[Path] = [] self.steam = Path.home() / ".local/share/Steam/steamapps" self.flatpak = ( Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps" ) - self.libraries: list[Path] = [] - self.games: list[GameSelection] = [] - for source in [self.steam, self.flatpak]: if (source / "libraryfolders.vdf").exists() is False: continue @@ -75,13 +89,37 @@ def __init__(self, args): if game.name not in self.ids: continue + pfx = library / f"compatdata/{self.ids[game.name]}/pfx" + app_data = pfx / "drive_c/users/steamuser/AppData/Local" + game_selection = GameSelection( name=game.name, - library=library, + directory=library / f"common/{game.name}", + data=library / f"common/{game.name}/Data", + dlc_file=app_data + / f"{game.name.replace('t 4', 't4')}/DLCList.txt", + plugin_file=app_data + / f"{game.name.replace('t 4', 't4')}/Plugins.txt", ) self.games.append(game_selection) + # Find manually configured games + if args.conf.exists(): + for i in args.conf.iterdir(): + if i.is_file() and i.suffix == ".json": + with open(i, "r") as file: + j = json.loads(file.read()) + game_selection = GameSelection( + name=i.stem, + directory=Path(j["directory"]), + data=Path(j["data"]), + dlc_file=Path(j["dlc_file"]), + plugin_file=Path(j["plugin_file"]), + ) + if game_selection not in self.games: + self.games.append(game_selection) + if len(self.games) == 0: raise FileNotFoundError( f"Supported games {list(self.ids)} not found in {self.libraries}" @@ -104,7 +142,7 @@ def __str__(self) -> str: result += "-------|-----\n" for i, game in enumerate(self.games): index = f"[{i}]" - result += f"{index:<7} {game.name} ({game.library})\n" + result += f"{index:<7} {game.name} ({game.directory})\n" return result def _autocomplete(self, text: str, state: int) -> Union[str, None]: @@ -117,9 +155,7 @@ def _populate_index_commands(self) -> None: """ for i, game in enumerate(self.games): setattr(self, str(i), lambda self, i=i: self._manage_game(i)) - full_location = str((game.library / "common" / game.name)).replace( - str(Path.home()), "~", 1 - ) + full_location = str(game.directory).replace(str(Path.home()), "~", 1) self.__dict__[str(i)].__doc__ = full_location def _manage_game(self, index: int) -> None: @@ -129,18 +165,6 @@ def _manage_game(self, index: int) -> None: """ game_selection = self.games[index] - app_id = self.ids[game_selection.name] - pfx = game_selection.library / f"compatdata/{app_id}/pfx" - directory = game_selection.library / f"common/{game_selection.name}" - app_data = ( - game_selection.library / f"{pfx}/drive_c/users/steamuser/AppData/Local" - ) - dlc_file = app_data / f"{game_selection.name.replace('t 4', 't4')}/DLCList.txt" - plugin_file = ( - app_data / f"{game_selection.name.replace('t 4', 't4')}/Plugins.txt" - ) - data = directory / "Data" - ammo_conf_dir = self.args.conf.resolve() / game_selection.name ammo_mods_dir = (self.args.mods or ammo_conf_dir / "mods").resolve() ammo_conf = ammo_conf_dir / "ammo.conf" @@ -160,13 +184,13 @@ def enabled_formula(line) -> bool: return line.strip().startswith("*") game = Game( - game_selection.name, - directory, - data, ammo_conf, - dlc_file, - plugin_file, ammo_mods_dir, + game_selection.name, + game_selection.directory, + game_selection.data, + game_selection.dlc_file, + game_selection.plugin_file, enabled_formula, ) diff --git a/ammo/mod_controller.py b/ammo/mod_controller.py index 03cb259..34421d9 100755 --- a/ammo/mod_controller.py +++ b/ammo/mod_controller.py @@ -34,13 +34,13 @@ @dataclass(frozen=True) class Game: + ammo_conf: Path + ammo_mods_dir: Path name: str directory: Path data: Path - ammo_conf: Path dlc_file: Path plugin_file: Path - ammo_mods_dir: Path enabled_formula: Callable[[str], bool] = field( default=lambda line: line.strip().startswith("*") )