Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split game settings to generic and compatibility tabs #550

Merged
merged 11 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 25 additions & 28 deletions rare/commands/launcher/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import json
import os
import platform
import shlex
import subprocess
Expand Down Expand Up @@ -35,7 +34,7 @@
from rare.widgets.rare_app import RareApp, RareAppException
from .cloud_sync_dialog import CloudSyncDialog, CloudSyncDialogResult
from .console_dialog import ConsoleDialog
from .lgd_helper import get_launch_args, InitArgs, get_configured_process, LaunchArgs, GameArgsError
from .lgd_helper import get_launch_args, InitArgs, LaunchArgs, dict_to_qprocenv, get_configured_qprocess

DETACHED_APP_NAMES = {
"0a2d9f6403244d12969e11da6713137b", # Fall Guys
Expand Down Expand Up @@ -87,19 +86,19 @@ def prepare_launch(self, args: InitArgs) -> Optional[LaunchArgs]:
return None

if launch_args.pre_launch_command:
proc = get_configured_process()
proc.setProcessEnvironment(launch_args.environment)
proc = get_configured_qprocess(shlex.split(launch_args.pre_launch_command), launch_args.environment)
proc.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
proc.readyReadStandardOutput.connect(
lambda: self.logger.info(str(proc.readAllStandardOutput().data(), "utf-8", "ignore"))
)
self.signals.pre_launch_command_started.emit()
prelaunch = shlex.split(launch_args.pre_launch_command)
command = prelaunch.pop(0) if len(prelaunch) else ""
arguments = prelaunch if len(prelaunch) else []
self.logger.info("Running pre-launch command %s, %s", command, shlex.join(arguments))
self.logger.info("Running pre-launch command %s, %s", proc.program(), proc.arguments())
if launch_args.pre_launch_wait:
proc.start(command, arguments)
proc.start()
self.logger.info("Waiting for pre-launch command to finish")
proc.waitForFinished(-1)
else:
proc.startDetached(command, arguments)
proc.startDetached()
return launch_args


Expand Down Expand Up @@ -306,54 +305,51 @@ def launch_game(self, args: LaunchArgs):

if self.args.dry_run:
self.logger.info("Dry run %s (%s)", self.rgame.app_title, self.rgame.app_name)
self.logger.info("%s %s", args.executable, " ".join(args.arguments))
self.logger.info("Command: %s, %s", args.executable, " ".join(args.arguments))
if self.console:
self.console.log(f"Dry run {self.rgame.app_title} ({self.rgame.app_name})")
self.console.log(f"{shlex.join((args.executable, *args.arguments))}")
self.console.accept_close = True
self.stop()
return

if args.is_origin_game:
if platform.system() == "Windows" and args.is_origin_game:
# executable is a protocol link (link2ea://launchgame/...)
QDesktopServices.openUrl(QUrl(args.executable))
self.stop() # stop because it is not a subprocess
return

self.logger.debug("Launch command %s, %s", args.executable, " ".join(args.arguments))
self.logger.info("Starting %s (%s)", self.rgame.app_title, self.rgame.app_name)
self.logger.info("Command: %s, %s", args.executable, " ".join(args.arguments))
self.logger.debug("Working directory %s", args.working_directory)

if self.rgame.app_name in DETACHED_APP_NAMES and platform.system() == "Windows":
if self.console:
self.console.log(f"Launching {args.executable} as a detached process")
subprocess.Popen(
(args.executable, *args.arguments),
stdin=None, stdout=None, stderr=None,
cwd=args.working_directory,
env={i: args.environment.value(i) for i in args.environment.keys()},
env=args.environment,
shell=True,
creationflags=subprocess.DETACHED_PROCESS,
)
self.stop() # stop because we do not attach to the output
return

# TODO: move to environment configuration, do not resuse variables from this block
# Sanity check environment (mostly for Linux)
command_line = shlex.join((args.executable, *args.arguments))
if os.environ.get("XDG_CURRENT_DESKTOP", None) == "gamescope" or "gamescope" in command_line:
# disable mangohud in gamescope
args.environment.insert("MANGOHUD", "0")
# TODO: end

self.game_process.setProgram(args.executable)
self.game_process.setArguments(args.arguments)
if args.working_directory:
self.game_process.setWorkingDirectory(args.working_directory)
self.game_process.setProcessEnvironment(args.environment)
self.game_process.setProcessEnvironment(dict_to_qprocenv(args.environment))
# send start message after process started
self.game_process.started.connect(lambda: self.send_message(StateChangedModel(
action=Actions.state_update, app_name=self.rgame.app_name,
new_state=StateChangedModel.States.started
self.game_process.started.connect(
lambda: self.send_message(StateChangedModel(
action=Actions.state_update,
app_name=self.rgame.app_name,
new_state=StateChangedModel.States.started
)))
# self.logger.debug("Executing prelaunch command %s, %s", args.executable, args.arguments)
self.game_process.start(args.executable, args.arguments)
self.game_process.start()

def error_occurred(self, error_str: str):
self.logger.warning(error_str)
Expand Down Expand Up @@ -466,6 +462,7 @@ def sighandler(s, frame):
app.logger.info("%s received. Stopping", strsignal(s))
app.stop()
app.exit(1)
return 1

signal(SIGINT, sighandler)
signal(SIGTERM, sighandler)
Expand Down
6 changes: 3 additions & 3 deletions rare/commands/launcher/cloud_sync_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def __init__(self, igame: InstalledGame, dt_local: datetime, dt_remote: datetime
layout.addWidget(sync_widget)

self.accept_button.setText(self.tr("Skip"))
self.accept_button.setIcon(qta_icon("fa.chevron-right"))
self.accept_button.setIcon(qta_icon("fa.chevron-right", "fa5s.chevron-right"))

self.setCentralLayout(layout)

Expand All @@ -65,8 +65,8 @@ def __init__(self, igame: InstalledGame, dt_local: datetime, dt_remote: datetime
self.sync_ui.date_info_remote.setText(
dt_remote.astimezone(local_tz).strftime("%A, %d %B %Y %X") if dt_remote else "None")

self.sync_ui.icon_local.setPixmap(qta_icon("mdi.harddisk", "fa.desktop").pixmap(128, 128))
self.sync_ui.icon_remote.setPixmap(qta_icon("mdi.cloud-outline", "ei.cloud").pixmap(128, 128))
self.sync_ui.icon_local.setPixmap(qta_icon("mdi.harddisk", "fa5s.desktop").pixmap(128, 128))
self.sync_ui.icon_remote.setPixmap(qta_icon("mdi.cloud-outline", "fa5s.cloud").pixmap(128, 128))

self.sync_ui.upload_button.clicked.connect(self.__on_upload)
self.sync_ui.download_button.clicked.connect(self.__on_download)
Expand Down
18 changes: 9 additions & 9 deletions rare/commands/launcher/console_dialog.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import os
from typing import Union
from typing import Union, Dict

from PySide6.QtCore import QProcessEnvironment, Signal, QSize, Qt
from PySide6.QtGui import QTextCursor, QFont, QCursor, QCloseEvent
Expand All @@ -16,12 +15,12 @@

from rare.ui.commands.launcher.console_env import Ui_ConsoleEnv
from rare.widgets.dialogs import dialog_title, game_title
from .lgd_helper import dict_to_qprocenv


class ConsoleDialog(QDialog):
term = Signal()
kill = Signal()
env: QProcessEnvironment

def __init__(self, app_title: str, parent=None):
super(ConsoleDialog, self).__init__(parent=parent)
Expand Down Expand Up @@ -65,8 +64,9 @@ def __init__(self, app_title: str, parent=None):

self.setLayout(layout)

self.env_variables = ConsoleEnv(app_title, self)
self.env_variables.hide()
self.env_variables: QProcessEnvironment = None
self.env_console = ConsoleEnv(app_title, self)
self.env_console.hide()

self.accept_close = False

Expand Down Expand Up @@ -107,12 +107,12 @@ def save(self):
f.close()
self.save_button.setText(self.tr("Saved"))

def set_env(self, env: QProcessEnvironment):
self.env = env
def set_env(self, env: Dict):
self.env_variables = dict_to_qprocenv(env)

def show_env(self):
self.env_variables.setTable(self.env)
self.env_variables.show()
self.env_console.setTable(self.env_variables)
self.env_console.show()

def log(self, text: str):
self.console_edit.log(f"Rare: {text}")
Expand Down
120 changes: 68 additions & 52 deletions rare/commands/launcher/lgd_helper.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import os
import platform
import shlex
import shutil
from argparse import Namespace
from dataclasses import dataclass, field
from logging import getLogger
from typing import List
from typing import List, Dict, Tuple

from PySide6.QtCore import QProcess, QProcessEnvironment
from legendary.models.game import LaunchParameters
Expand Down Expand Up @@ -47,7 +48,7 @@ class LaunchArgs:
executable: str = ""
arguments: List[str] = field(default_factory=list)
working_directory: str = ""
environment: QProcessEnvironment = None
environment: Dict[str, str] = field(default_factory=dict)
pre_launch_command: str = ""
pre_launch_wait: bool = False
is_origin_game: bool = False # only for windows to launch as url
Expand All @@ -62,30 +63,19 @@ def get_origin_params(rgame: RareGameSlim, init_args: InitArgs, launch_args: Lau

origin_uri = core.get_origin_uri(app_name, init_args.offline)
if platform.system() == "Windows":
launch_args.executable = origin_uri
launch_args.arguments = []
# only set it here true, because on linux it is a launch command like every other game
launch_args.is_origin_game = True
return launch_args

command = core.get_app_launch_command(app_name)
if not os.path.exists(command[0]) and shutil.which(command[0]) is None:
return launch_args
command.append(origin_uri)

env = core.get_app_environment(app_name)
launch_args.environment = QProcessEnvironment.systemEnvironment()

if os.environ.get("container") == "flatpak":
flatpak_command = ["flatpak-spawn", "--host"]
flatpak_command.extend(f"--env={name}={value}" for name, value in env.items())
command = flatpak_command + command
command = [origin_uri]
else:
for name, value in env.items():
launch_args.environment.insert(name, value)
command = core.get_app_launch_command(app_name)
if not os.path.exists(command[0]) and shutil.which(command[0]) is None:
return launch_args
command.append(origin_uri)

exe, args, env = prepare_process(command, core.get_app_environment(app_name))

launch_args.executable = command[0]
launch_args.arguments = command[1:]
launch_args.is_origin_game = True
launch_args.executable = exe
launch_args.arguments = args
launch_args.environment = env

return launch_args

Expand Down Expand Up @@ -118,26 +108,17 @@ def get_game_params(rgame: RareGameSlim, args: InitArgs, launch_args: LaunchArgs
)

full_params = []
launch_args.environment = QProcessEnvironment.systemEnvironment()

if os.environ.get("container") == "flatpak":
full_params.extend(["flatpak-spawn", "--host"])
full_params.extend(
f"--env={name}={value}"
for name, value in params.environment.items()
)
else:
for name, value in params.environment.items():
launch_args.environment.insert(name, value)

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)

launch_args.executable = full_params[0]
launch_args.arguments = full_params[1:]
exe, args, env = prepare_process(full_params, params.environment)

launch_args.executable = exe
launch_args.arguments = args
launch_args.environment = env
launch_args.working_directory = params.working_directory

return launch_args
Expand Down Expand Up @@ -171,18 +152,53 @@ def get_launch_args(rgame: RareGameSlim, init_args: InitArgs = None) -> LaunchAr
return resp


def get_configured_process(env: dict = None):
def prepare_process(command: List[str], environment: Dict) -> Tuple[str, List[str], Dict]:
logger.debug("Preparing process: %s", command)

# Sanity check environment (mostly for Linux)
command_line = shlex.join(command)
if os.environ.get("XDG_CURRENT_DESKTOP", None) == "gamescope" or "gamescope" in command_line:
# disable mangohud in gamescope
environment["MANGOHUD"] = "0"
# ensure shader compat dirs exist
if platform.system() in {"Linux", "FreeBSD"}:
for key in {
"WINEPREFIX",
"__GL_SHADER_DISK_CACHE_PATH", "MESA_SHADER_CACHE_DIR",
"DXVK_STATE_CACHE_PATH", "VKD3D_SHADER_CACHE_PATH",
}:
if key in environment and not os.path.isdir(environment[key]):
os.makedirs(environment[key], exist_ok=True)
if "STEAM_COMPAT_DATA_PATH" in environment:
compat_pfx = os.path.join(environment["STEAM_COMPAT_DATA_PATH"], "pfx")
if not os.path.isdir(compat_pfx):
os.makedirs(environment[key], exist_ok=True)

_env = os.environ.copy()
_command = command.copy()

if os.environ.get("container") == "flatpak":
flatpak_command = ["flatpak-spawn", "--host"]
flatpak_command.extend(f"--env={name}={value}" for name, value in environment.items())
_command = flatpak_command + command
else:
_env.update(environment)

return _command[0], _command[1:] if len(_command) > 1 else [], _env


def dict_to_qprocenv(env: Dict) -> QProcessEnvironment:
_env = QProcessEnvironment()
for name, value in env.items():
_env.insert(name, value)
return _env


def get_configured_qprocess(command: List[str], environment: Dict) -> QProcess:
cmd, args, env = prepare_process(command, environment)
proc = QProcess()
proc.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
proc.readyReadStandardOutput.connect(
lambda: logger.info(
str(proc.readAllStandardOutput().data(), "utf-8", "ignore")
)
)
environment = QProcessEnvironment.systemEnvironment()
if env:
for e in env:
environment.insert(e, env[e])
proc.setProcessEnvironment(environment)

return proc
proc.setProcessChannelMode(QProcess.ProcessChannelMode.SeparateChannels)
proc.setProcessEnvironment(dict_to_qprocenv(env))
proc.setProgram(cmd)
proc.setArguments(args)
return proc
6 changes: 3 additions & 3 deletions rare/components/dialogs/install_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,14 @@ def __init__(self, rgame: RareGame, options: InstallOptionsModel, parent=None):
bicon = qta_icon("ri.install-line")
if options.repair_mode:
header = self.tr("Repair")
bicon = qta_icon("fa.wrench")
bicon = qta_icon("fa.wrench", "mdi.progress-wrench")
if options.repair_and_update:
header = self.tr("Repair and update")
elif options.update:
header = self.tr("Update")
elif options.reset_sdl:
header = self.tr("Modify")
bicon = qta_icon("fa.gear")
bicon = qta_icon("fa.gear", "mdi.content-save-edit-outline")
self.setWindowTitle(game_title(header, rgame.app_title))
self.setSubtitle(game_title(header, rgame.app_title))

Expand Down Expand Up @@ -198,7 +198,7 @@ def __init__(self, rgame: RareGame, options: InstallOptionsModel, parent=None):
self.accept_button.setObjectName("InstallButton")

self.action_button.setText(self.tr("Verify"))
self.action_button.setIcon(qta_icon("fa.check"))
self.action_button.setIcon(qta_icon("fa.check", "fa5s.check"))

self.setCentralWidget(install_widget)

Expand Down
6 changes: 3 additions & 3 deletions rare/components/dialogs/login/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ def __init__(self, core: LegendaryCore, parent=None):

self.login_stack.setCurrentWidget(self.landing_page)

self.ui.exit_button.setIcon(qta_icon("fa.remove"))
self.ui.back_button.setIcon(qta_icon("fa.chevron-left"))
self.ui.next_button.setIcon(qta_icon("fa.chevron-right"))
self.ui.exit_button.setIcon(qta_icon("fa.remove", "fa5s.times"))
self.ui.back_button.setIcon(qta_icon("fa.chevron-left", "fa5s.chevron-left"))
self.ui.next_button.setIcon(qta_icon("fa.chevron-right", "fa5s.chevron-right"))

# lk: Set next as the default button only to stop closing the dialog when pressing enter
self.ui.exit_button.setAutoDefault(False)
Expand Down
Loading