1
0
Fork 0
mirror of synced 2024-06-23 08:40:45 +12:00

Merge pull request #202 from Dummerle/rework_game_launch

Add game helper to launch games in a detached process
This commit is contained in:
Dummerle 2022-06-19 00:34:34 +02:00 committed by GitHub
commit e2f55f0f21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 619 additions and 159 deletions

View file

@ -1,5 +1,5 @@
#!/usr/bin/python
import argparse
import os
import pathlib
import sys
@ -7,7 +7,6 @@ from argparse import ArgumentParser
def main():
# fix cx_freeze
import multiprocessing
@ -53,6 +52,22 @@ def main():
launch_parser = subparsers.add_parser("launch")
launch_parser.add_argument("app_name", help="Name of the app", metavar="<App Name>")
launch_minimal_parser = subparsers.add_parser("start")
launch_minimal_parser.add_argument("app_name", help="AppName of the game to launch",
metavar="<App Name>", action="store")
launch_minimal_parser.add_argument("--offline", help="Launch game offline",
action="store_true")
launch_minimal_parser.add_argument("--skip_update_check", help="Do not check for updates",
action="store_true")
launch_minimal_parser.add_argument('--wine-bin', dest='wine_bin', action='store', metavar='<wine binary>',
default=os.environ.get('LGDRY_WINE_BINARY', None),
help='Set WINE binary to use to launch the app')
launch_minimal_parser.add_argument('--wine-prefix', dest='wine_pfx', action='store', metavar='<wine pfx path>',
default=os.environ.get('LGDRY_WINE_PREFIX', None),
help='Set WINE prefix to use')
launch_minimal_parser.add_argument("--ask-alyways-sync", help="Ask for cloud saves",
action="store_true")
args = parser.parse_args()
if args.desktop_shortcut:
@ -74,6 +89,11 @@ def main():
print(f"Rare {__version__} Codename: {code_name}")
return
if args.subparser == "start":
from rare import game_launch_helper as helper
helper.start_game(args)
return
from rare.utils import singleton
try:

View file

@ -2,7 +2,7 @@ import datetime
import sys
from dataclasses import dataclass
from logging import getLogger
from typing import Union, List
from typing import Union, List, Dict
from PyQt5.QtCore import QObject, pyqtSignal, QRunnable, QThreadPool, Qt, QSettings
from PyQt5.QtWidgets import QDialog, QMessageBox, QSizePolicy, QLayout, QApplication
@ -139,7 +139,7 @@ class CloudSaveUtils(QObject):
self.thread_pool = QThreadPool.globalInstance()
def get_latest_saves(self, saves: List[SaveGameFile]) -> dict:
def get_latest_saves(self, saves: List[SaveGameFile]) -> Dict[str, SaveGameFile]:
save_games = set()
for igame in self.core.get_installed_list():
game = self.core.get_game(igame.app_name)

View file

@ -1,40 +1,119 @@
import datetime
import json
import os
import platform
import shutil
from dataclasses import dataclass
from logging import getLogger
from PyQt5.QtCore import QObject, QSettings, QProcess, QProcessEnvironment, pyqtSignal, QUrl
from PyQt5.QtCore import QObject, QSettings, QProcess, QProcessEnvironment, pyqtSignal, QUrl, QTimer
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QLocalSocket
from PyQt5.QtWidgets import QMessageBox, QPushButton
from legendary.models.game import LaunchParameters, InstalledGame
from rare.components.dialogs.uninstall_dialog import UninstallDialog
from rare.components.extra.console import Console
from rare.components.tabs.games import CloudSaveUtils
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
from rare.utils import legendary_utils
from rare.utils import utils
from rare.utils.meta import RareGameMeta
from rare.game_launch_helper import message_models
logger = getLogger("GameUtils")
class GameProcess(QProcess):
game_finished = pyqtSignal(int, str)
class GameProcess(QObject):
game_finished = pyqtSignal(int, str) # exit_code, appname
game_launched = pyqtSignal(str)
tried_connections = 0
# noinspection PyUnresolvedReferences
def __init__(self, app_name):
def __init__(self, app_name: str, on_startup=False, always_ask_sync: bool= False):
super(GameProcess, self).__init__()
self.app_name = app_name
self.finished.connect(self._game_finished)
self.on_startup = on_startup
self.game = LegendaryCoreSingleton().get_game(app_name)
self.socket = QLocalSocket()
self.socket.connected.connect(self._socket_connected)
self.socket.errorOccurred.connect(self._error_occurred)
self.socket.readyRead.connect(self._message_available)
self.always_ask_sync = always_ask_sync
def _game_finished(self, exit_code):
def close_socket():
try:
self.socket.close()
except RuntimeError:
pass
self.socket.disconnected.connect(close_socket)
self.timer = QTimer()
if not on_startup:
# wait a short time for process started
self.timer.timeout.connect(self.connect_to_server)
self.timer.start(200)
else:
# nothing happens, if no server available
self.connect_to_server()
def connect_to_server(self):
self.socket.connectToServer(f"rare_{self.app_name}")
self.tried_connections += 1
if self.tried_connections > 50: # 10 seconds
QMessageBox.warning(None, "Error", self.tr("Connection to game process failed (Timeout)"))
self.timer.stop()
self.game_finished.emit(1, self.app_name)
def _message_available(self):
message = self.socket.readAll().data()
if not message.startswith(b"{"):
logger.error(f"Received unsupported message: {message.decode('utf-8')}")
return
try:
self.game_finished.emit(exit_code, self.app_name)
except RuntimeError: # Do not raise an exception, if rare finished, but game not
pass
data = json.loads(message)
except json.JSONDecodeError as e:
logger.error(e)
logger.error("Could not load json data")
return
action = data.get("action", -1)
if action == -1:
logger.error("Got unexpected action")
elif action == message_models.Actions.finished:
logger.info(f"{self.app_name} finished")
model = message_models.FinishedModel.from_json(data)
self.socket.close()
self._game_finished(model.exit_code)
elif action == message_models.Actions.error:
model = message_models.ErrorModel.from_json(data)
logger.error(f"Error in game {self.game.app_title}: {model.error_string}")
QMessageBox.warning(None, "Error", self.tr(
"Error in game {}: \n{}").format(self.game.app_title, model.error_string))
elif action == message_models.Actions.state_update:
model = message_models.StateChangedModel.from_json(data)
if model.new_state == message_models.StateChangedModel.States.started:
logger.info("Launched Game")
self.game_launched.emit(self.app_name)
def _socket_connected(self):
self.timer.stop()
self.timer.deleteLater()
logger.info(f"Connection established for {self.app_name}")
if self.on_startup:
logger.info(f"Found {self.app_name} running at startup")
# FIXME run this after startup, widgets do not exist at this time
QTimer.singleShot(1000, lambda: self.game_launched.emit(self.app_name))
def _error_occurred(self, _):
if self.on_startup:
self.socket.close()
self.deleteLater()
self._game_finished(-1234) # 1234 is exit code for startup
logger.error(f"{self.app_name}: {self.socket.errorString()}")
def _game_finished(self, exit_code: int):
self.game_finished.emit(exit_code, self.app_name)
@dataclass
@ -63,6 +142,12 @@ class GameUtils(QObject):
self.cloud_save_utils.sync_finished.connect(self.sync_finished)
self.game_meta = RareGameMeta()
for igame in self.core.get_installed_list():
game_process = GameProcess(igame.app_name, True)
game_process.game_finished.connect(self.game_finished)
game_process.game_launched.connect(self.game_launched.emit)
self.running_games[igame.app_name] = game_process
def uninstall_game(self, app_name) -> bool:
# returns if uninstalled
game = self.core.get_game(app_name)
@ -95,6 +180,7 @@ class GameUtils(QObject):
game = self.core.get_game(app_name)
dont_sync_after_finish = False
# TODO move this to helper
if game.supports_cloud_saves and not offline:
try:
sync = self.cloud_save_utils.sync_before_launch_game(app_name)
@ -123,76 +209,33 @@ class GameUtils(QObject):
wine_pfx: str = None,
ask_always_sync: bool = False,
):
if self.args.offline:
offline = True
game = self.core.get_game(app_name)
igame = self.core.get_installed_game(app_name)
executable = utils.get_rare_executable()
executable, args = executable[0], executable[1:]
args.extend([
"start", app_name
])
if offline:
args.append("--offline")
if skip_update_check:
args.append("--skip-update-check")
if wine_bin:
args.extend(["--wine-bin", wine_bin])
if wine_pfx:
args.extend(["--wine-prefix", wine_pfx])
if ask_always_sync:
args.extend("--ask-always-sync")
meta_data = self.game_meta.get_game(app_name)
meta_data.last_played = datetime.datetime.now()
self.game_meta.set_game(app_name, meta_data)
if not game:
logger.error(f"{app_name} not found")
self.finished.emit(app_name, self.tr("Game not found in available games"))
return
if QSettings().value("confirm_start", False, bool):
if (
not QMessageBox.question(
None,
"Launch",
self.tr("Do you want to launch {}").format(game.app_title),
QMessageBox.Yes | QMessageBox.No,
) == QMessageBox.Yes
):
logger.info("Cancel Startup")
self.finished.emit(app_name, "")
return
logger.info(f"Launching {game.app_title}")
if game.third_party_store == "Origin":
offline = False
else:
if not igame:
logger.error(f"{app_name} is not installed")
if game.is_dlc:
logger.error("Game is dlc")
self.finished.emit(
app_name, self.tr("Game is a DLC. Please launch base game instead")
)
return
if not os.path.exists(igame.install_path):
logger.error("Game doesn't exist")
self.finished.emit(
app_name,
self.tr(
"Game files of {} do not exist. Please install game"
).format(game.app_title),
)
return
def _launch_real():
process = self._get_process(app_name, env)
self.console.log("\n"*2)
if game.third_party_store != "Origin":
self._launch_game(igame, process, offline, skip_update_check, ask_always_sync)
else:
self._launch_origin(app_name, process)
env = self.core.get_app_environment(app_name, wine_pfx=wine_pfx)
pre_cmd, wait = self.core.get_pre_launch_command(app_name)
if pre_cmd:
pre_cmd = pre_cmd.split()
pre_proc = self._launch_pre_command(env)
self.console.log("\n"*2)
pre_proc.start(pre_cmd[0], pre_cmd[1:])
if wait:
pre_proc.finished.connect(_launch_real)
return
_launch_real()
# kill me, if I don't change it before commit
QProcess.startDetached(executable, args)
logger.info(f"Start new Process: ({executable} {' '.join(args)})")
game_process = GameProcess(app_name, ask_always_sync)
game_process.game_finished.connect(self.game_finished)
game_process.game_launched.connect(self.game_launched.emit)
self.running_games[app_name] = game_process
def game_finished(self, exit_code, app_name):
if exit_code == -1234:
self.running_games.pop(app_name)
logger.info(f"Game exited with exit code: {exit_code}")
self.console.log(f"Game exited with code: {exit_code}")
self.signals.set_discord_rpc.emit("")
@ -314,76 +357,6 @@ class GameUtils(QObject):
command.append(origin_uri)
process.start(command[0], command[1:])
def _launch_game(self, igame: InstalledGame, process: QProcess, offline: bool,
skip_update_check: bool, ask_always_sync: bool):
if not offline: # skip for update
if not skip_update_check and not self.core.is_noupdate_game(igame.app_name):
# check updates
try:
latest = self.core.get_asset(
igame.app_name, igame.platform, update=False
)
except ValueError:
self.finished.emit(igame.app_name, self.tr("Metadata doesn't exist"))
return
else:
if latest.build_version != igame.version:
self.finished.emit(igame.app_name, self.tr("Please update game"))
return
params: LaunchParameters = self.core.get_launch_parameters(
app_name=igame.app_name, offline=offline
)
full_params = list()
if os.environ.get("container") == "flatpak":
full_params.extend(["flatpak-spawn", "--host"])
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)
process.setWorkingDirectory(params.working_directory)
if platform.system() != "Windows":
# wine prefixes
for env in ["STEAM_COMPAT_DATA_PATH", "WINEPREFIX"]:
if val := process.processEnvironment().value(env, ""):
if not os.path.exists(val):
try:
os.makedirs(val)
except PermissionError as e:
logger.error(str(e))
QMessageBox.warning(
None,
"Error",
self.tr(
"Error while launching {}. No permission to create {} for {}"
).format(igame.title, val, env),
)
process.deleteLater()
return
# check wine executable
if shutil.which(full_params[0]) is None:
QMessageBox.warning(None, "Warning", self.tr("'{}' does not exist").format(full_params[0]))
return
running_game = RunningGameModel(
process=process, app_name=igame.app_name, always_ask_sync=ask_always_sync
)
process.start(full_params[0], full_params[1:])
self.game_launched.emit(igame.app_name)
self.signals.set_discord_rpc.emit(igame.app_name)
logger.info(f"{igame.title} launched")
self.running_games[igame.app_name] = running_game
def sync_finished(self, app_name):
if app_name in self.launch_queue.keys():
self.cloud_save_finished.emit(app_name)

View file

@ -0,0 +1,211 @@
import json
import logging
import sys
import time
import traceback
from argparse import Namespace
from logging import getLogger
from typing import Union
from PyQt5.QtCore import QObject, QProcess, pyqtSignal, QUrl, QRunnable, QThreadPool
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QLocalServer, QLocalSocket
from PyQt5.QtWidgets import QApplication
from .lgd_helper import get_launch_args, InitArgs, get_configured_process, LaunchArgs, GameArgsError
from .message_models import ErrorModel, Actions, FinishedModel, BaseModel, StateChangedModel
from ..shared import LegendaryCoreSingleton
class PreLaunchThread(QRunnable):
class Signals(QObject):
ready_to_launch = pyqtSignal(LaunchArgs)
started_pre_launch_command = pyqtSignal()
pre_launch_command_finished = pyqtSignal(int) # exit_code
error_occurred = pyqtSignal(str)
def __init__(self, args: InitArgs):
super(PreLaunchThread, self).__init__()
self.core = LegendaryCoreSingleton()
self.app_name = args.app_name
self.signals = self.Signals()
def run(self) -> None:
args = self.prepare_launch(self.app_name)
if not args:
return
self.signals.ready_to_launch.emit(args)
def prepare_launch(self, app_name) -> Union[LaunchArgs, None]:
try:
args = get_launch_args(self.core, InitArgs(app_name))
except GameArgsError as e:
self.signals.error_occurred.emit(str(e))
return None
if not args:
return None
if args.pre_launch_command:
proc = get_configured_process()
proc.setProcessEnvironment(args.env)
self.signals.started_pre_launch_command.emit()
proc.start(args.pre_launch_command[0], args.pre_launch_command[1:])
if args.pre_launch_wait:
proc.waitForFinished(-1)
return args
class GameProcessHelper(QObject):
game_process: QProcess
server: QLocalServer
socket: QLocalSocket = None
exit_app = pyqtSignal()
success: bool = True
def __init__(self, app_name: str):
super(GameProcessHelper, self).__init__()
self.game_process = QProcess()
self.app_name = app_name
self.logger = getLogger(self.app_name)
self.core = LegendaryCoreSingleton(True)
self.server = QLocalServer()
ret = self.server.listen(f"rare_{self.app_name}")
if not ret:
self.logger.error(self.server.errorString())
self.logger.info("Server is running")
self.server.close()
self.success = False
return
self.server.newConnection.connect(self.new_server_connection)
self.game_process.finished.connect(self.game_finished)
self.game_process.errorOccurred.connect(
lambda err: self.error_occurred(self.game_process.errorString()))
self.start_time = time.time()
def new_server_connection(self):
if self.socket is not None:
try:
self.socket.disconnectFromServer()
except RuntimeError:
pass
self.logger.info("New connection")
self.socket = self.server.nextPendingConnection()
self.socket.disconnected.connect(self.socket.deleteLater)
self.socket.disconnected.connect(lambda: self.logger.info("Server disconnected"))
self.socket.flush()
def send_message(self, message: BaseModel):
if self.socket:
self.socket.write(json.dumps(message.__dict__).encode("utf-8"))
self.socket.flush()
else:
self.logger.error("Can't send message")
def game_finished(self, exit_code):
self.logger.info("game finished")
self.send_message(
FinishedModel(
action=Actions.finished,
app_name=self.app_name,
exit_code=exit_code,
playtime=int(time.time() - self.start_time)
)
)
self.stop()
def launch_game(self, args: LaunchArgs):
# should never happen
if not args:
self.stop()
return
self.start_time = time.time()
if args.is_origin_game:
QDesktopServices.openUrl(QUrl(args.executable))
self.stop() # stop because it is no subprocess
return
if args.cwd:
self.game_process.setWorkingDirectory(args.cwd)
self.game_process.start(args.executable, args.args)
self.send_message(
StateChangedModel(
action=Actions.state_update, app_name=self.app_name,
new_state=StateChangedModel.States.started
)
)
def error_occurred(self, error_str: str):
self.logger.warning(error_str)
self.send_message(ErrorModel(
error_string=error_str, app_name=self.app_name,
action=Actions.error)
)
self.stop()
def start(self, args: InitArgs):
if not args.offline:
try:
if not self.core.login():
raise ValueError("You are not logged in")
except ValueError:
# automatically launch offline if available
self.logger.error("Not logged in. Try to launch game offline")
args.offline = True
worker = PreLaunchThread(args)
worker.signals.ready_to_launch.connect(self.launch_game)
worker.signals.error_occurred.connect(self.error_occurred)
# worker.signals.started_pre_launch_command(None)
QThreadPool.globalInstance().start(worker)
def stop(self):
self.logger.info("Stopping server")
self.server.close()
self.server.deleteLater()
self.exit_app.emit()
def start_game(args: Namespace):
args = InitArgs.from_argparse(args)
logging.basicConfig(
format="[%(name)s] %(levelname)s: %(message)s",
level=logging.INFO,
)
app = QApplication(sys.argv)
helper = GameProcessHelper(args.app_name)
def excepthook(exc_type, exc_value, exc_tb):
tb = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
helper.logger.fatal(tb)
try:
helper.send_message(ErrorModel(
app_name=args.app_name,
action=Actions.error,
error_string=tb
))
except RuntimeError:
pass
helper.stop()
sys.excepthook = excepthook
if not helper.success:
return
helper.start(args)
helper.exit_app.connect(lambda: app.exit(0))
# this button is for debug. Closing with keyboard interrupt does not kill the server
# quit_button = QPushButton("Quit")
# quit_button.show()
# quit_button.clicked.connect(lambda: app.exit(0))
app.exec_()

View file

@ -0,0 +1,167 @@
import os
import platform
import shutil
from dataclasses import dataclass
from logging import getLogger
from typing import List, Tuple
from PyQt5.QtCore import QProcess, QProcessEnvironment
from legendary.core import LegendaryCore
from legendary.models.game import InstalledGame, LaunchParameters
logger = getLogger("Helper")
class GameArgsError(Exception):
pass
@dataclass
class InitArgs:
app_name: str
offline: bool = False
skip_version_check: bool = False
wine_prefix: str = ""
wine_bin: str = ""
@classmethod
def from_argparse(cls, args):
return cls(
app_name=args.app_name,
offline=args.offline,
skip_version_check=args.skip_update_check,
wine_bin=args.wine_bin,
wine_prefix=args.wine_pfx
)
@dataclass
class LaunchArgs:
executable: str = ""
args: List[str] = None
cwd: str = None
env: QProcessEnvironment = None
pre_launch_command: str = ""
pre_launch_wait: bool = False
is_origin_game: bool = False # only for windows to launch as url
def __bool__(self):
return bool(self.executable)
def get_origin_params(core: LegendaryCore, app_name, offline: bool,
launch_args: LaunchArgs) -> LaunchArgs:
origin_uri = core.get_origin_uri(app_name, offline)
if platform.system() == "Windows":
launch_args.executable = origin_uri
launch_args.args = []
# 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.env = QProcessEnvironment.systemEnvironment()
for name, value in env:
launch_args.env.insert(name, value)
launch_args.executable = command[0]
launch_args.args = command[1:]
return launch_args
def get_game_params(core: LegendaryCore, igame: InstalledGame, args: InitArgs,
launch_args: LaunchArgs) -> LaunchArgs:
if not args.offline: # skip for update
if not args.skip_version_check and not core.is_noupdate_game(igame.app_name):
# check updates
try:
latest = core.get_asset(
igame.app_name, igame.platform, update=False
)
except ValueError:
raise GameArgsError("Metadata doesn't exist")
else:
if latest.build_version != igame.version:
raise GameArgsError("Game is not up to date. Please update first")
params: LaunchParameters = core.get_launch_parameters(
app_name=igame.app_name, offline=args.offline
)
full_params = list()
if os.environ.get("container") == "flatpak":
full_params.extend(["flatpak-spawn", "--host"])
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.args = full_params[1:]
launch_args.env = QProcessEnvironment.systemEnvironment()
for name, value in params.environment.items():
launch_args.env.insert(name, value)
launch_args.cwd = params.working_directory
return launch_args
def get_launch_args(core: LegendaryCore, args: InitArgs = None) -> LaunchArgs:
game = core.get_game(args.app_name)
igame = core.get_installed_game(args.app_name)
resp = LaunchArgs()
if not game:
raise GameArgsError(f"Could not find metadata for ")
if game.third_party_store == "Origin":
args.offline = False
else:
if not igame:
raise GameArgsError("Game is not installed or has unsupported format")
if game.is_dlc:
raise GameArgsError("Game is a DLC")
if not os.path.exists(igame.install_path):
raise GameArgsError("Game path does not exist")
if game.third_party_store == "Origin":
resp = get_origin_params(core, args.app_name, args.offline, resp)
else:
resp = get_game_params(core, igame, args, resp)
pre_cmd, wait = core.get_pre_launch_command(args.app_name)
resp.pre_launch_command, resp.pre_launch_wait = pre_cmd, wait
return resp
def get_configured_process(env: dict = None):
proc = QProcess()
proc.readyReadStandardOutput.connect(
lambda: logger.info(
str(proc.readAllStandardOutput().data(), "utf-8", "ignore")
)
)
proc.readyReadStandardError.connect(
lambda: logger.info(
str(proc.readAllStandardError().data(), "utf-8", "ignore")
)
)
environment = QProcessEnvironment.systemEnvironment()
if env:
for e in env:
environment.insert(e, env[e])
proc.setProcessEnvironment(environment)
return proc

View file

@ -0,0 +1,67 @@
from dataclasses import dataclass
class Actions:
finished = 0
error = 1
message = 2
state_update = 3
@dataclass
class BaseModel:
action: int
app_name: str
@classmethod
def from_json(cls, data: dict):
return cls(
action=data["action"],
app_name=data["app_name"]
)
@dataclass
class FinishedModel(BaseModel):
exit_code: int
playtime: int # seconds
@classmethod
def from_json(cls, data):
return cls(
**BaseModel.from_json(data).__dict__,
exit_code=data["exit_code"],
playtime=data["playtime"],
)
@dataclass
class StateChangedModel(BaseModel):
class States:
started = 0
# for future
syncing_cloud = 1
cloud_sync_finished = 2
new_state: int
@classmethod
def from_json(cls, data):
return cls(
action=data["action"],
app_name=data["app_name"],
new_state=data["new_state"]
)
@dataclass
class ErrorModel(BaseModel):
error_string: str
@classmethod
def from_json(cls, data):
return cls(
**BaseModel.from_json(data).__dict__,
error_string=data["error_string"]
)

View file

@ -251,7 +251,28 @@ def get_size(b: int) -> str:
b /= 1024
def create_desktop_link(app_name=None, core: LegendaryCore = None, type_of_link="desktop", for_rare: bool = False) -> bool:
def get_rare_executable() -> List[str]:
if platform.system() == "Linux":
# TODO flatpak
if p := os.environ.get("APPIMAGE"):
executable = [p]
else:
executable = [sys.executable, os.path.abspath(sys.argv[0])]
elif platform.system() == "Windows":
executable = [sys.executable]
if not sys.executable.endswith("Rare.exe"):
# be sure to start consoleless then
executable[0] = executable[0].replace("python.exe", "pythonw.exe")
executable.extend(["-m", "rare"])
else: # macos not tested
executable = [sys.executable]
return executable
def create_desktop_link(app_name=None, core: LegendaryCore = None, type_of_link="desktop",
for_rare: bool = False) -> bool:
if not for_rare:
igame = core.get_installed_game(app_name)
@ -276,10 +297,7 @@ def create_desktop_link(app_name=None, core: LegendaryCore = None, type_of_link=
return False
if not os.path.exists(path):
return False
if p := os.environ.get("APPIMAGE"):
executable = p
else:
executable = f"{sys.executable} {os.path.abspath(sys.argv[0])}"
executable = get_rare_executable()
if for_rare:
with open(os.path.join(path, "Rare.desktop"), "w") as desktop_file:
@ -341,9 +359,13 @@ def create_desktop_link(app_name=None, core: LegendaryCore = None, type_of_link=
shell = Dispatch("WScript.Shell")
shortcut = shell.CreateShortCut(pathLink)
executable = sys.executable
executable = get_rare_executable()
arguments = []
if len(executable) > 1:
arguments.extend(executable[1:])
executable = executable[0]
if not sys.executable.endswith("Rare.exe"):
# be sure to start consoleless then
executable = sys.executable.replace("python.exe", "pythonw.exe")