[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.
This commit is contained in:
derrod 2021-09-28 04:04:15 +02:00
parent ee2432c443
commit 888d62a96d
3 changed files with 98 additions and 62 deletions

View file

@ -518,12 +518,12 @@ class LegendaryCLI:
logger.error('Game is out of date, please update or launch with update check skipping!') logger.error('Game is out of date, please update or launch with update check skipping!')
exit(1) exit(1)
params, cwd, env = self.core.get_launch_parameters(app_name=app_name, offline=args.offline, params = self.core.get_launch_parameters(app_name=app_name, offline=args.offline,
extra_args=extra, user=args.user_name_override, extra_args=extra, user=args.user_name_override,
wine_bin=args.wine_bin, wine_pfx=args.wine_pfx, wine_bin=args.wine_bin, wine_pfx=args.wine_pfx,
language=args.language, wrapper=args.wrapper, language=args.language, wrapper=args.wrapper,
disable_wine=args.no_wine, disable_wine=args.no_wine,
executable_override=args.executable_override) executable_override=args.executable_override)
if args.set_defaults: if args.set_defaults:
self.core.lgd.config[app_name] = dict() self.core.lgd.config[app_name] = dict()
@ -546,19 +546,37 @@ class LegendaryCLI:
if args.wrapper: if args.wrapper:
self.core.lgd.config[app_name]['wrapper'] = 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: if args.dry_run:
logger.info(f'Not Launching {app_name} (dry run)') logger.info(f'Not Launching {app_name} (dry run)')
logger.info(f'Launch parameters: {shlex.join(params)}') logger.info(f'Launch parameters: {shlex.join(full_params)}')
logger.info(f'Working directory: {cwd}') logger.info(f'Working directory: {params.working_directory}')
if env: if env_overrides:
logger.info('Environment overrides:', env) logger.info('Environment overrides: {}'.format(', '.join(f'{k}={v}' for k, v in env_overrides)))
else: else:
logger.info(f'Launching {app_name}...') logger.info(f'Launching {app_name}...')
logger.debug(f'Launch parameters: {shlex.join(params)}') logger.debug(f'Launch parameters: {shlex.join(full_params)}')
logger.debug(f'Working directory: {cwd}') logger.debug(f'Working directory: {params.working_directory}')
if env: if env_overrides:
logger.debug('Environment overrides:', env) logger.debug('Environment overrides: {}'.format(', '.join(f'{k}={v}' for k, v in env_overrides)))
subprocess.Popen(params, cwd=cwd, env=env) subprocess.Popen(full_params, cwd=params.working_directory, env=params.environment)
def launch_origin(self, args): def launch_origin(self, args):
# login is not required to launch the game, but linking does require it. # 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)') help='Override executable to launch (relative path)')
launch_parser.add_argument('--origin', dest='origin', action='store_true', launch_parser.add_argument('--origin', dest='origin', action='store_true',
help='Launch Origin to activate or run the game.') 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': if os.name != 'nt':
launch_parser.add_argument('--wine', dest='wine_bin', action='store', metavar='<wine binary>', launch_parser.add_argument('--wine', dest='wine_bin', action='store', metavar='<wine binary>',

View file

@ -438,10 +438,48 @@ class LegendaryCore:
wine_bin: str = None, wine_pfx: str = None, wine_bin: str = None, wine_pfx: str = None,
language: str = None, wrapper: str = None, language: str = None, wrapper: str = None,
disable_wine: bool = False, disable_wine: bool = False,
executable_override: str = None) -> (list, str, dict): executable_override: str = None) -> LaunchParameters:
install = self.lgd.get_installed_game(app_name) install = self.lgd.get_installed_game(app_name)
game = self.lgd.get_game_meta(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 = '' game_token = ''
if not offline: if not offline:
self.log.info('Getting authentication token...') self.log.info('Getting authentication token...')
@ -454,45 +492,7 @@ class LegendaryCore:
if user: if user:
user_name = user user_name = user
if executable_override or (executable_override := self.lgd.config.get(app_name, 'override_exe', fallback=None)): params.egl_parameters.extend([
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([
'-AUTH_LOGIN=unused', '-AUTH_LOGIN=unused',
f'-AUTH_PASSWORD={game_token}', f'-AUTH_PASSWORD={game_token}',
'-AUTH_TYPE=exchangecode', '-AUTH_TYPE=exchangecode',
@ -507,13 +507,13 @@ class LegendaryCore:
f'{game.asset_info.namespace}{game.asset_info.catalog_item_id}.ovt') f'{game.asset_info.namespace}{game.asset_info.catalog_item_id}.ovt')
with open(ovt_path, 'wb') as f: with open(ovt_path, 'wb') as f:
f.write(ovt) 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) language_code = self.lgd.config.get(app_name, 'language', fallback=language)
if not language_code: # fall back to system or config language if not language_code: # fall back to system or config language
language_code = self.language_code language_code = self.language_code
params.extend([ params.egl_parameters.extend([
'-EpicPortal', '-EpicPortal',
f'-epicusername={user_name}', f'-epicusername={user_name}',
f'-epicuserid={account_id}', f'-epicuserid={account_id}',
@ -521,13 +521,12 @@ class LegendaryCore:
]) ])
if extra_args: 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): 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
return params, working_dir, env
def get_origin_uri(self, app_name: str, offline: bool = False) -> str: def get_origin_uri(self, app_name: str, offline: bool = False) -> str:
if offline: if offline:

View file

@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
from dataclasses import dataclass, field
from enum import Enum from enum import Enum
class GameAsset: class GameAsset:
def __init__(self): def __init__(self):
self.app_name = '' self.app_name = ''
@ -144,3 +144,20 @@ class VerifyResult(Enum):
HASH_MISMATCH = 1 HASH_MISMATCH = 1
FILE_MISSING = 2 FILE_MISSING = 2
OTHER_ERROR = 3 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)