From 888d62a96dfff08bee92f1cb8075dda08ae94e48 Mon Sep 17 00:00:00 2001 From: derrod Date: Tue, 28 Sep 2021 04:04:15 +0200 Subject: [PATCH] [cli/core/models] Refactor launch parameters and add --json option Primarily intended to make it easier for third-party applications (mainly Heroic) to handle launch options on their own by simply taking the necessary information from legendary and ignoring user-defined stuff. Also useful for debugging. --- legendary/cli.py | 50 +++++++++++++++------- legendary/core.py | 91 ++++++++++++++++++++-------------------- legendary/models/game.py | 19 ++++++++- 3 files changed, 98 insertions(+), 62 deletions(-) diff --git a/legendary/cli.py b/legendary/cli.py index b39a2d6..df8deee 100644 --- a/legendary/cli.py +++ b/legendary/cli.py @@ -518,12 +518,12 @@ class LegendaryCLI: logger.error('Game is out of date, please update or launch with update check skipping!') exit(1) - params, cwd, env = self.core.get_launch_parameters(app_name=app_name, offline=args.offline, - extra_args=extra, user=args.user_name_override, - wine_bin=args.wine_bin, wine_pfx=args.wine_pfx, - language=args.language, wrapper=args.wrapper, - disable_wine=args.no_wine, - executable_override=args.executable_override) + params = self.core.get_launch_parameters(app_name=app_name, offline=args.offline, + extra_args=extra, user=args.user_name_override, + wine_bin=args.wine_bin, wine_pfx=args.wine_pfx, + language=args.language, wrapper=args.wrapper, + disable_wine=args.no_wine, + executable_override=args.executable_override) if args.set_defaults: self.core.lgd.config[app_name] = dict() @@ -546,19 +546,37 @@ class LegendaryCLI: if args.wrapper: self.core.lgd.config[app_name]['wrapper'] = args.wrapper + if args.json: + print(json.dumps(vars(params))) + return + + full_params = list() + full_params.extend(params.launch_command) + full_params.append(os.path.join(params.game_directory, params.game_executable)) + full_params.extend(params.game_parameters) + full_params.extend(params.egl_parameters) + full_params.extend(params.user_parameters) + + env_overrides = [] + if params.environment: + for env_var, env_value in params.environment.items(): + if env_var in os.environ: + continue + env_overrides.append((env_var, env_value)) + if args.dry_run: logger.info(f'Not Launching {app_name} (dry run)') - logger.info(f'Launch parameters: {shlex.join(params)}') - logger.info(f'Working directory: {cwd}') - if env: - logger.info('Environment overrides:', env) + logger.info(f'Launch parameters: {shlex.join(full_params)}') + logger.info(f'Working directory: {params.working_directory}') + if env_overrides: + logger.info('Environment overrides: {}'.format(', '.join(f'{k}={v}' for k, v in env_overrides))) else: logger.info(f'Launching {app_name}...') - logger.debug(f'Launch parameters: {shlex.join(params)}') - logger.debug(f'Working directory: {cwd}') - if env: - logger.debug('Environment overrides:', env) - subprocess.Popen(params, cwd=cwd, env=env) + logger.debug(f'Launch parameters: {shlex.join(full_params)}') + logger.debug(f'Working directory: {params.working_directory}') + if env_overrides: + logger.debug('Environment overrides: {}'.format(', '.join(f'{k}={v}' for k, v in env_overrides))) + subprocess.Popen(full_params, cwd=params.working_directory, env=params.environment) def launch_origin(self, args): # login is not required to launch the game, but linking does require it. @@ -1325,6 +1343,8 @@ def main(): help='Override executable to launch (relative path)') launch_parser.add_argument('--origin', dest='origin', action='store_true', help='Launch Origin to activate or run the game.') + launch_parser.add_argument('--json', dest='json', action='store_true', + help='Print launch information as JSON and exit') if os.name != 'nt': launch_parser.add_argument('--wine', dest='wine_bin', action='store', metavar='', diff --git a/legendary/core.py b/legendary/core.py index 520118f..3a68b94 100644 --- a/legendary/core.py +++ b/legendary/core.py @@ -438,10 +438,48 @@ class LegendaryCore: wine_bin: str = None, wine_pfx: str = None, language: str = None, wrapper: str = None, disable_wine: bool = False, - executable_override: str = None) -> (list, str, dict): + executable_override: str = None) -> LaunchParameters: install = self.lgd.get_installed_game(app_name) game = self.lgd.get_game_meta(app_name) + if executable_override or (executable_override := self.lgd.config.get(app_name, 'override_exe', fallback=None)): + game_exe = executable_override.replace('\\', '/') + exe_path = os.path.join(install.install_path, game_exe) + if not os.path.exists(exe_path): + raise ValueError(f'Executable path is invalid: {exe_path}') + else: + game_exe = install.executable.replace('\\', '/').lstrip('/') + exe_path = os.path.join(install.install_path, game_exe) + + working_dir = os.path.split(exe_path)[0] + + params = LaunchParameters(game_executable=game_exe, game_directory=install.install_path, + working_directory=working_dir, + environment=self.get_app_environment(app_name, wine_pfx=wine_pfx)) + + if wrapper or (wrapper := self.lgd.config.get(app_name, 'wrapper', + fallback=self.lgd.config.get('default', 'wrapper', + fallback=None))): + params.launch_command.extend(shlex.split(wrapper)) + + if os.name != 'nt' and not disable_wine: + if not wine_bin: + # check if there's a default override + wine_bin = self.lgd.config.get('default', 'wine_executable', fallback='wine') + # check if there's a game specific override + wine_bin = self.lgd.config.get(app_name, 'wine_executable', fallback=wine_bin) + + if not self.lgd.config.getboolean(app_name, 'no_wine', + fallback=self.lgd.config.get('default', 'no_wine', fallback=False)): + params.launch_command.append(wine_bin) + + if install.launch_parameters: + try: + params.game_parameters.extend(shlex.split(install.launch_parameters, posix=False)) + except ValueError as e: + self.log.warning(f'Parsing predefined launch parameters failed with: {e!r}, ' + f'input: {install.launch_parameters}') + game_token = '' if not offline: self.log.info('Getting authentication token...') @@ -454,45 +492,7 @@ class LegendaryCore: if user: user_name = user - if executable_override or (executable_override := self.lgd.config.get(app_name, 'override_exe', fallback=None)): - game_exe = os.path.join(install.install_path, - executable_override.replace('\\', '/')) - if not os.path.exists(game_exe): - raise ValueError(f'Executable path is invalid: {game_exe}') - else: - game_exe = os.path.join(install.install_path, - install.executable.replace('\\', '/').lstrip('/')) - - working_dir = os.path.split(game_exe)[0] - - params = [] - - if wrapper or (wrapper := self.lgd.config.get(app_name, 'wrapper', - fallback=self.lgd.config.get('default', 'wrapper', - fallback=None))): - params.extend(shlex.split(wrapper)) - - if os.name != 'nt' and not disable_wine: - if not wine_bin: - # check if there's a default override - wine_bin = self.lgd.config.get('default', 'wine_executable', fallback='wine') - # check if there's a game specific override - wine_bin = self.lgd.config.get(app_name, 'wine_executable', fallback=wine_bin) - - if not self.lgd.config.getboolean(app_name, 'no_wine', - fallback=self.lgd.config.get('default', 'no_wine', fallback=False)): - params.append(wine_bin) - - params.append(game_exe) - - if install.launch_parameters: - try: - params.extend(shlex.split(install.launch_parameters, posix=False)) - except ValueError as e: - self.log.warning(f'Parsing predefined launch parameters failed with: {e!r}, ' - f'input: {install.launch_parameters}') - - params.extend([ + params.egl_parameters.extend([ '-AUTH_LOGIN=unused', f'-AUTH_PASSWORD={game_token}', '-AUTH_TYPE=exchangecode', @@ -507,13 +507,13 @@ class LegendaryCore: f'{game.asset_info.namespace}{game.asset_info.catalog_item_id}.ovt') with open(ovt_path, 'wb') as f: f.write(ovt) - params.append(f'-epicovt={ovt_path}') + params.egl_parameters.append(f'-epicovt={ovt_path}') language_code = self.lgd.config.get(app_name, 'language', fallback=language) if not language_code: # fall back to system or config language language_code = self.language_code - params.extend([ + params.egl_parameters.extend([ '-EpicPortal', f'-epicusername={user_name}', f'-epicuserid={account_id}', @@ -521,13 +521,12 @@ class LegendaryCore: ]) if extra_args: - params.extend(extra_args) + params.user_parameters.extend(extra_args) if config_args := self.lgd.config.get(app_name, 'start_params', fallback=None): - params.extend(shlex.split(config_args.strip())) + params.user_parameters.extend(shlex.split(config_args.strip())) - env = self.get_app_environment(app_name, wine_pfx=wine_pfx) - return params, working_dir, env + return params def get_origin_uri(self, app_name: str, offline: bool = False) -> str: if offline: diff --git a/legendary/models/game.py b/legendary/models/game.py index 16e2d79..12d69bf 100644 --- a/legendary/models/game.py +++ b/legendary/models/game.py @@ -1,8 +1,8 @@ # coding: utf-8 +from dataclasses import dataclass, field from enum import Enum - class GameAsset: def __init__(self): self.app_name = '' @@ -144,3 +144,20 @@ class VerifyResult(Enum): HASH_MISMATCH = 1 FILE_MISSING = 2 OTHER_ERROR = 3 + + +@dataclass +class LaunchParameters: + # game-supplied parameters + game_parameters: list = field(default_factory=list) + game_executable: str = '' + game_directory: str = '' + # EGL parameters (auth, ovt, etc.) + egl_parameters: list = field(default_factory=list) + # command line before executable (WINE, gamemode, etc.) + launch_command: list = field(default_factory=list) + # working directory for launched process + working_directory: str = '' + # user and environment supplied options + user_parameters: list = field(default_factory=list) + environment: dict = field(default_factory=dict)