2026-01-18 12:13:07 +00:00
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
// SPDX-FileCopyrightText: 2024 A-La-Karte Contributors
|
|
|
|
|
|
|
|
|
|
#include "gamelauncher.h"
|
|
|
|
|
#include "app.h"
|
|
|
|
|
|
|
|
|
|
#include <QCoreApplication>
|
2026-02-06 13:02:24 +00:00
|
|
|
#include <QDBusArgument>
|
|
|
|
|
#include <QDBusConnection>
|
2026-02-09 13:03:01 +00:00
|
|
|
#include <QDBusConnectionInterface>
|
|
|
|
|
#include <QDBusError>
|
2026-02-06 13:02:24 +00:00
|
|
|
#include <QDBusInterface>
|
|
|
|
|
#include <QDBusReply>
|
|
|
|
|
#include <QDBusVariant>
|
2026-01-18 12:13:07 +00:00
|
|
|
#include <QDateTime>
|
2026-01-25 13:19:57 +00:00
|
|
|
#include <QDir>
|
|
|
|
|
#include <QFileInfo>
|
2026-02-06 13:02:24 +00:00
|
|
|
#include <QProcess>
|
2026-01-25 13:19:57 +00:00
|
|
|
#include <QStandardPaths>
|
2026-01-18 12:13:07 +00:00
|
|
|
#include <QTimer>
|
|
|
|
|
|
2026-02-09 13:03:01 +00:00
|
|
|
static const QString kGameCenterService = QStringLiteral("org.kde.GameCenter1");
|
|
|
|
|
static const QString kGameCenterPath = QStringLiteral("/org/kde/GameCenter1");
|
|
|
|
|
static const QString kGameCenterInterface = QStringLiteral("org.kde.GameCenter1");
|
|
|
|
|
|
|
|
|
|
static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1");
|
|
|
|
|
static const QString kRunnerPath = QStringLiteral("/org/kde/ALaKarte/Runner1");
|
|
|
|
|
static const QString kRunnerInterface = QStringLiteral("org.kde.ALaKarte.Runner1");
|
|
|
|
|
|
|
|
|
|
static bool pingDaemon(QDBusConnection bus)
|
2026-01-25 13:19:57 +00:00
|
|
|
{
|
2026-02-09 13:03:01 +00:00
|
|
|
if (!bus.isConnected()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QDBusInterface iface(kGameCenterService, kGameCenterPath, kGameCenterInterface, bus);
|
|
|
|
|
if (!iface.isValid()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
iface.setTimeout(2000);
|
|
|
|
|
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
|
|
|
|
|
return reply.isValid() && reply.value() == QLatin1String("ok");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void disconnectDaemonSignals(QDBusConnection bus, GameLauncher *launcher)
|
|
|
|
|
{
|
|
|
|
|
if (!bus.isConnected()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bus.disconnect(kGameCenterService,
|
|
|
|
|
kGameCenterPath,
|
|
|
|
|
kGameCenterInterface,
|
|
|
|
|
QStringLiteral("SessionAdded"),
|
|
|
|
|
launcher,
|
|
|
|
|
SLOT(onDaemonSessionAdded(QVariantMap)));
|
|
|
|
|
|
|
|
|
|
bus.disconnect(kGameCenterService,
|
|
|
|
|
kGameCenterPath,
|
|
|
|
|
kGameCenterInterface,
|
|
|
|
|
QStringLiteral("SessionChanged"),
|
|
|
|
|
launcher,
|
|
|
|
|
SLOT(onDaemonSessionChanged(QVariantMap)));
|
|
|
|
|
|
|
|
|
|
bus.disconnect(kGameCenterService,
|
|
|
|
|
kGameCenterPath,
|
|
|
|
|
kGameCenterInterface,
|
|
|
|
|
QStringLiteral("SessionRemoved"),
|
|
|
|
|
launcher,
|
|
|
|
|
SLOT(onDaemonSessionRemoved(QString, QVariantMap)));
|
|
|
|
|
|
|
|
|
|
bus.disconnect(kGameCenterService,
|
|
|
|
|
kGameCenterPath,
|
|
|
|
|
kGameCenterInterface,
|
|
|
|
|
QStringLiteral("LaunchFailed"),
|
|
|
|
|
launcher,
|
|
|
|
|
SLOT(onDaemonLaunchFailed(QVariantMap)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void connectDaemonSignals(QDBusConnection bus, GameLauncher *launcher)
|
|
|
|
|
{
|
|
|
|
|
if (!bus.isConnected()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bus.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("SessionAdded"), launcher, SLOT(onDaemonSessionAdded(QVariantMap)));
|
|
|
|
|
|
|
|
|
|
bus.connect(kGameCenterService,
|
|
|
|
|
kGameCenterPath,
|
|
|
|
|
kGameCenterInterface,
|
|
|
|
|
QStringLiteral("SessionChanged"),
|
|
|
|
|
launcher,
|
|
|
|
|
SLOT(onDaemonSessionChanged(QVariantMap)));
|
|
|
|
|
|
|
|
|
|
bus.connect(kGameCenterService,
|
|
|
|
|
kGameCenterPath,
|
|
|
|
|
kGameCenterInterface,
|
|
|
|
|
QStringLiteral("SessionRemoved"),
|
|
|
|
|
launcher,
|
|
|
|
|
SLOT(onDaemonSessionRemoved(QString, QVariantMap)));
|
|
|
|
|
|
|
|
|
|
bus.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("LaunchFailed"), launcher, SLOT(onDaemonLaunchFailed(QVariantMap)));
|
2026-01-25 13:19:57 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
static QVariant unwrapDbusVariant(QVariant v)
|
|
|
|
|
{
|
|
|
|
|
if (v.canConvert<QDBusVariant>()) {
|
|
|
|
|
v = v.value<QDBusVariant>().variant();
|
|
|
|
|
}
|
|
|
|
|
return v;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static QVariantMap unwrapVariantMap(const QVariantMap &map)
|
|
|
|
|
{
|
|
|
|
|
QVariantMap out;
|
|
|
|
|
for (auto it = map.constBegin(); it != map.constEnd(); ++it) {
|
|
|
|
|
out.insert(it.key(), unwrapDbusVariant(it.value()));
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 13:03:01 +00:00
|
|
|
static bool tryResolveWithRunnerManager(const QVariantMap &spec, QVariantMap &out)
|
|
|
|
|
{
|
|
|
|
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
|
|
|
|
if (!bus.isConnected() || !bus.interface()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!bus.interface()->isServiceRegistered(kRunnerService)) {
|
|
|
|
|
bus.interface()->startService(kRunnerService);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
|
|
|
|
|
if (!iface.isValid()) {
|
|
|
|
|
if (bus.interface()->startService(kRunnerService).isValid()) {
|
|
|
|
|
QDBusInterface retryIface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
|
|
|
|
|
if (retryIface.isValid()) {
|
|
|
|
|
retryIface.setTimeout(2000);
|
|
|
|
|
const QDBusReply<QVariantMap> retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec);
|
|
|
|
|
if (retryReply.isValid()) {
|
|
|
|
|
out = unwrapVariantMap(retryReply.value());
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
iface.setTimeout(2000);
|
|
|
|
|
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ResolveLaunch"), spec);
|
|
|
|
|
if (!reply.isValid()) {
|
|
|
|
|
if (reply.error().type() == QDBusError::ServiceUnknown) {
|
|
|
|
|
bus.interface()->startService(kRunnerService);
|
|
|
|
|
QDBusInterface retryIface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
|
|
|
|
|
if (!retryIface.isValid()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
retryIface.setTimeout(2000);
|
|
|
|
|
const QDBusReply<QVariantMap> retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec);
|
|
|
|
|
if (!retryReply.isValid()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
out = unwrapVariantMap(retryReply.value());
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
out = unwrapVariantMap(reply.value());
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static QStringList steamCandidateRoots();
|
|
|
|
|
|
2026-01-25 13:19:57 +00:00
|
|
|
static QString findSteamClientInstallPathFromProton(const QString &protonExe)
|
|
|
|
|
{
|
|
|
|
|
if (protonExe.isEmpty()) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QDir dir(QFileInfo(protonExe).absoluteDir());
|
|
|
|
|
for (int i = 0; i < 10; ++i) {
|
|
|
|
|
if (dir.exists(QStringLiteral("steamapps"))) {
|
|
|
|
|
return dir.absolutePath();
|
|
|
|
|
}
|
|
|
|
|
if (!dir.cdUp()) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const QString &root : steamCandidateRoots()) {
|
|
|
|
|
if (QDir(root).exists(QStringLiteral("steamapps"))) {
|
|
|
|
|
return root;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static QString discoverDefaultProtonExecutable()
|
|
|
|
|
{
|
|
|
|
|
static bool done = false;
|
|
|
|
|
static QString cached;
|
|
|
|
|
|
|
|
|
|
if (done) {
|
|
|
|
|
return cached;
|
|
|
|
|
}
|
|
|
|
|
done = true;
|
|
|
|
|
|
|
|
|
|
QStringList candidates;
|
|
|
|
|
for (const QString &root : steamCandidateRoots()) {
|
|
|
|
|
const QString compatTools = root + QStringLiteral("/compatibilitytools.d");
|
|
|
|
|
QDir compatDir(compatTools);
|
|
|
|
|
if (compatDir.exists()) {
|
|
|
|
|
const QStringList toolDirs = compatDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
|
|
|
|
for (const QString &tool : toolDirs) {
|
|
|
|
|
const QString protonPath = compatTools + QLatin1Char('/') + tool + QStringLiteral("/proton");
|
|
|
|
|
if (QFileInfo::exists(protonPath)) {
|
|
|
|
|
candidates.append(protonPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QString common = root + QStringLiteral("/steamapps/common");
|
|
|
|
|
QDir commonDir(common);
|
|
|
|
|
if (commonDir.exists()) {
|
|
|
|
|
const QStringList dirs = commonDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
|
|
|
|
for (const QString &d : dirs) {
|
|
|
|
|
if (!d.contains(QStringLiteral("Proton"), Qt::CaseInsensitive)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const QString protonPath = common + QLatin1Char('/') + d + QStringLiteral("/proton");
|
|
|
|
|
if (QFileInfo::exists(protonPath)) {
|
|
|
|
|
candidates.append(protonPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
candidates.removeDuplicates();
|
|
|
|
|
candidates.sort();
|
|
|
|
|
|
|
|
|
|
for (const QString &c : candidates) {
|
|
|
|
|
if (c.contains(QStringLiteral("Proton - Experimental"))) {
|
|
|
|
|
cached = c;
|
|
|
|
|
return cached;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!candidates.isEmpty()) {
|
|
|
|
|
cached = candidates.constLast();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return cached;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 13:03:01 +00:00
|
|
|
static QStringList steamCandidateRoots()
|
|
|
|
|
{
|
|
|
|
|
const QString home = QDir::homePath();
|
|
|
|
|
return {
|
|
|
|
|
home + QStringLiteral("/.steam/root"),
|
|
|
|
|
home + QStringLiteral("/.steam/steam"),
|
|
|
|
|
home + QStringLiteral("/.local/share/Steam"),
|
|
|
|
|
home + QStringLiteral("/.var/app/com.valvesoftware.Steam/data/Steam"),
|
|
|
|
|
home + QStringLiteral("/.var/app/com.valvesoftware.Steam/.local/share/Steam"),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:13:07 +00:00
|
|
|
GameLauncher::GameLauncher(QObject *parent)
|
|
|
|
|
: QObject(parent)
|
2026-02-09 13:03:01 +00:00
|
|
|
, m_daemonAvailable(false)
|
|
|
|
|
, m_usingSystemBus(false)
|
2026-01-18 12:13:07 +00:00
|
|
|
{
|
2026-02-06 13:02:24 +00:00
|
|
|
if (auto *app = qobject_cast<App *>(parent)) {
|
|
|
|
|
connect(app->gameModel(), &GameModel::countChanged, this, [this]() {
|
|
|
|
|
QTimer::singleShot(0, this, [this]() {
|
|
|
|
|
applyRunningStateToLibrary();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QTimer::singleShot(0, this, [this]() {
|
|
|
|
|
checkDaemonAvailability();
|
|
|
|
|
syncDaemonSessions();
|
|
|
|
|
});
|
2026-01-18 12:13:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GameLauncher::~GameLauncher()
|
|
|
|
|
{
|
2026-02-06 13:02:24 +00:00
|
|
|
m_daemonGameToSession.clear();
|
|
|
|
|
m_daemonSessionToGame.clear();
|
2026-01-18 12:13:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool GameLauncher::hasRunningGames() const
|
|
|
|
|
{
|
2026-02-06 13:02:24 +00:00
|
|
|
return !m_daemonGameToSession.isEmpty();
|
2026-01-18 12:13:07 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-30 10:02:28 +00:00
|
|
|
QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
|
|
|
|
|
{
|
|
|
|
|
QVariantMap info;
|
|
|
|
|
info.insert(QStringLiteral("ok"), false);
|
|
|
|
|
|
|
|
|
|
if (!game) {
|
|
|
|
|
info.insert(QStringLiteral("error"), tr("No game selected"));
|
|
|
|
|
return info;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
info.insert(QStringLiteral("gameId"), game->id());
|
|
|
|
|
info.insert(QStringLiteral("gameName"), game->name());
|
2026-02-06 13:02:24 +00:00
|
|
|
info.insert(QStringLiteral("displayName"), game->name());
|
|
|
|
|
info.insert(QStringLiteral("isRunning"), m_daemonGameToSession.contains(game->id()));
|
|
|
|
|
|
|
|
|
|
// Detect provider from platform
|
|
|
|
|
const QString platform = game->platform().toLower();
|
|
|
|
|
if (platform == QLatin1String("steam")) {
|
|
|
|
|
info.insert(QStringLiteral("provider"), QStringLiteral("steam"));
|
|
|
|
|
} else if (platform == QLatin1String("lutris")) {
|
|
|
|
|
info.insert(QStringLiteral("provider"), QStringLiteral("lutris"));
|
|
|
|
|
} else {
|
|
|
|
|
info.insert(QStringLiteral("provider"), QStringLiteral("manual"));
|
|
|
|
|
}
|
2026-01-30 10:02:28 +00:00
|
|
|
|
|
|
|
|
const QString command = game->launchCommand();
|
|
|
|
|
info.insert(QStringLiteral("launchCommand"), command);
|
|
|
|
|
if (command.isEmpty()) {
|
|
|
|
|
info.insert(QStringLiteral("error"), tr("No launch command configured"));
|
|
|
|
|
return info;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QString runner = game->launchRunner().trimmed();
|
2026-02-09 13:03:01 +00:00
|
|
|
const QString runnerId = game->launchRunnerId().trimmed();
|
2026-01-30 10:02:28 +00:00
|
|
|
const QString runnerPath = game->launchRunnerPath().trimmed();
|
|
|
|
|
const QString prefixPath = game->launchPrefixPath().trimmed();
|
|
|
|
|
const QVariantMap launchEnv = game->launchEnv();
|
2026-02-09 13:03:01 +00:00
|
|
|
const bool hasLaunchOverrides = !runner.isEmpty() || !runnerId.isEmpty() || !runnerPath.isEmpty() || !prefixPath.isEmpty() || !launchEnv.isEmpty();
|
2026-01-30 10:02:28 +00:00
|
|
|
|
|
|
|
|
info.insert(QStringLiteral("runner"), runner);
|
2026-02-09 13:03:01 +00:00
|
|
|
info.insert(QStringLiteral("runnerId"), runnerId);
|
2026-01-30 10:02:28 +00:00
|
|
|
info.insert(QStringLiteral("runnerPath"), runnerPath);
|
|
|
|
|
info.insert(QStringLiteral("prefixPath"), prefixPath);
|
|
|
|
|
info.insert(QStringLiteral("workingDirectory"), game->workingDirectory());
|
|
|
|
|
info.insert(QStringLiteral("hasLaunchOverrides"), hasLaunchOverrides);
|
|
|
|
|
|
|
|
|
|
QVariantMap envOverrides;
|
|
|
|
|
for (auto it = launchEnv.constBegin(); it != launchEnv.constEnd(); ++it) {
|
|
|
|
|
const QString key = it.key();
|
|
|
|
|
if (key.isEmpty() || key.contains(QLatin1Char('='))) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
envOverrides.insert(key, it.value().toString());
|
|
|
|
|
}
|
|
|
|
|
info.insert(QStringLiteral("envOverrides"), envOverrides);
|
|
|
|
|
|
|
|
|
|
if (!hasLaunchOverrides && command.startsWith(QLatin1String("steam://"))) {
|
|
|
|
|
info.insert(QStringLiteral("ok"), true);
|
|
|
|
|
info.insert(QStringLiteral("launchType"), QStringLiteral("url"));
|
|
|
|
|
info.insert(QStringLiteral("url"), command);
|
2026-02-06 13:02:24 +00:00
|
|
|
info.insert(QStringLiteral("provider"), QStringLiteral("steam"));
|
2026-01-30 10:02:28 +00:00
|
|
|
return info;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!hasLaunchOverrides && command.startsWith(QLatin1String("lutris "))) {
|
|
|
|
|
info.insert(QStringLiteral("ok"), true);
|
|
|
|
|
info.insert(QStringLiteral("launchType"), QStringLiteral("url"));
|
|
|
|
|
info.insert(QStringLiteral("url"), command.mid(7));
|
2026-02-06 13:02:24 +00:00
|
|
|
info.insert(QStringLiteral("provider"), QStringLiteral("lutris"));
|
2026-01-30 10:02:28 +00:00
|
|
|
return info;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QStringList parts;
|
|
|
|
|
if (command.startsWith(QLatin1String("steam://"))) {
|
|
|
|
|
parts = {QStringLiteral("xdg-open"), command};
|
|
|
|
|
} else if (command.startsWith(QLatin1String("lutris "))) {
|
|
|
|
|
parts = {QStringLiteral("xdg-open"), command.mid(7)};
|
|
|
|
|
} else {
|
|
|
|
|
parts = QProcess::splitCommand(command);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (parts.isEmpty()) {
|
|
|
|
|
info.insert(QStringLiteral("error"), tr("Invalid launch command"));
|
|
|
|
|
return info;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QString program = parts.takeFirst();
|
|
|
|
|
info.insert(QStringLiteral("program"), program);
|
|
|
|
|
info.insert(QStringLiteral("args"), parts);
|
|
|
|
|
|
|
|
|
|
QString finalProgram = program;
|
|
|
|
|
QStringList finalArgs = parts;
|
|
|
|
|
|
|
|
|
|
QVariantMap effectiveEnv = envOverrides;
|
|
|
|
|
|
|
|
|
|
if ((runner == QLatin1String("wine") || runner == QLatin1String("proton")) && (program == QLatin1String("xdg-open"))) {
|
|
|
|
|
info.insert(QStringLiteral("error"), tr("This runner cannot be used with URL-based launch commands"));
|
|
|
|
|
return info;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 13:42:22 +00:00
|
|
|
{
|
2026-02-09 13:03:01 +00:00
|
|
|
QVariantMap runnerSpec = {
|
|
|
|
|
{QStringLiteral("runnerId"), runnerId},
|
|
|
|
|
{QStringLiteral("runner"), runner},
|
|
|
|
|
{QStringLiteral("runnerPath"), runnerPath},
|
|
|
|
|
{QStringLiteral("gameId"), game->id()},
|
|
|
|
|
{QStringLiteral("prefixPath"), prefixPath},
|
|
|
|
|
{QStringLiteral("program"), program},
|
|
|
|
|
{QStringLiteral("args"), parts},
|
|
|
|
|
{QStringLiteral("envOverrides"), envOverrides},
|
2026-02-12 13:42:22 +00:00
|
|
|
{QStringLiteral("useGameProfile"), true},
|
2026-02-09 13:03:01 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
QVariantMap resolved;
|
|
|
|
|
if (tryResolveWithRunnerManager(runnerSpec, resolved)) {
|
|
|
|
|
if (!resolved.value(QStringLiteral("ok")).toBool()) {
|
|
|
|
|
info.insert(QStringLiteral("error"), resolved.value(QStringLiteral("error")).toString());
|
|
|
|
|
return info;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 13:42:22 +00:00
|
|
|
if (resolved.contains(QStringLiteral("effectiveEnv"))) {
|
|
|
|
|
effectiveEnv = resolved.value(QStringLiteral("effectiveEnv")).toMap();
|
2026-02-09 13:03:01 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-12 13:42:22 +00:00
|
|
|
const QString resolvedFinalProgram = resolved.value(QStringLiteral("finalProgram")).toString();
|
|
|
|
|
const QStringList resolvedFinalArgs = resolved.value(QStringLiteral("finalArgs")).toStringList();
|
|
|
|
|
const QString resolvedPrefixPath = resolved.value(QStringLiteral("resolvedPrefixPath")).toString();
|
2026-02-09 13:03:01 +00:00
|
|
|
const QString resolvedSteamInstallPath = resolved.value(QStringLiteral("resolvedSteamInstallPath")).toString();
|
|
|
|
|
|
2026-02-12 13:42:22 +00:00
|
|
|
const bool runnerManagerWrapped =
|
|
|
|
|
(resolvedFinalProgram != program) || (resolvedFinalArgs != parts) || !resolvedPrefixPath.isEmpty() || !resolvedSteamInstallPath.isEmpty();
|
|
|
|
|
|
|
|
|
|
if (runnerManagerWrapped) {
|
|
|
|
|
info.insert(QStringLiteral("finalProgram"), resolvedFinalProgram);
|
|
|
|
|
info.insert(QStringLiteral("finalArgs"), resolvedFinalArgs);
|
|
|
|
|
info.insert(QStringLiteral("effectiveEnv"), effectiveEnv);
|
|
|
|
|
|
|
|
|
|
if (!resolvedPrefixPath.isEmpty()) {
|
|
|
|
|
info.insert(QStringLiteral("resolvedPrefixPath"), resolvedPrefixPath);
|
|
|
|
|
}
|
|
|
|
|
if (!resolvedSteamInstallPath.isEmpty()) {
|
|
|
|
|
info.insert(QStringLiteral("resolvedSteamInstallPath"), resolvedSteamInstallPath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
info.insert(QStringLiteral("ok"), true);
|
|
|
|
|
return info;
|
|
|
|
|
}
|
2026-02-09 13:03:01 +00:00
|
|
|
} else if (!runnerId.isEmpty()) {
|
|
|
|
|
info.insert(QStringLiteral("error"), tr("Runner service is not available"));
|
|
|
|
|
return info;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 10:02:28 +00:00
|
|
|
if (runner == QLatin1String("custom")) {
|
|
|
|
|
if (runnerPath.isEmpty()) {
|
|
|
|
|
info.insert(QStringLiteral("error"), tr("Custom runner is enabled but no runner path is configured"));
|
|
|
|
|
return info;
|
|
|
|
|
}
|
|
|
|
|
finalProgram = runnerPath;
|
|
|
|
|
finalArgs = {program};
|
|
|
|
|
finalArgs.append(parts);
|
|
|
|
|
} else if (runner == QLatin1String("wine")) {
|
|
|
|
|
QString wineExe = runnerPath;
|
|
|
|
|
if (wineExe.isEmpty()) {
|
|
|
|
|
wineExe = QStandardPaths::findExecutable(QStringLiteral("wine"));
|
|
|
|
|
}
|
|
|
|
|
if (wineExe.isEmpty()) {
|
|
|
|
|
info.insert(QStringLiteral("error"), tr("Wine runner is enabled but Wine was not found"));
|
|
|
|
|
return info;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString winePrefix = prefixPath;
|
|
|
|
|
if (winePrefix.isEmpty()) {
|
|
|
|
|
winePrefix = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/prefixes/") + game->id();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
effectiveEnv.insert(QStringLiteral("WINEPREFIX"), winePrefix);
|
|
|
|
|
info.insert(QStringLiteral("resolvedPrefixPath"), winePrefix);
|
|
|
|
|
|
|
|
|
|
finalProgram = wineExe;
|
|
|
|
|
finalArgs = {program};
|
|
|
|
|
finalArgs.append(parts);
|
|
|
|
|
} else if (runner == QLatin1String("proton")) {
|
|
|
|
|
QString protonExe = runnerPath;
|
|
|
|
|
if (protonExe.isEmpty()) {
|
|
|
|
|
protonExe = discoverDefaultProtonExecutable();
|
|
|
|
|
}
|
|
|
|
|
if (protonExe.isEmpty()) {
|
|
|
|
|
info.insert(QStringLiteral("error"), tr("Proton runner is enabled but no Proton installation was found"));
|
|
|
|
|
return info;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QString steamInstallPath = findSteamClientInstallPathFromProton(protonExe);
|
|
|
|
|
info.insert(QStringLiteral("resolvedSteamInstallPath"), steamInstallPath);
|
|
|
|
|
|
|
|
|
|
QString compatDataPath = prefixPath;
|
|
|
|
|
if (compatDataPath.isEmpty()) {
|
|
|
|
|
compatDataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/prefixes/") + game->id();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
effectiveEnv.insert(QStringLiteral("STEAM_COMPAT_DATA_PATH"), compatDataPath);
|
|
|
|
|
info.insert(QStringLiteral("resolvedPrefixPath"), compatDataPath);
|
|
|
|
|
|
|
|
|
|
if (!steamInstallPath.isEmpty()) {
|
|
|
|
|
effectiveEnv.insert(QStringLiteral("STEAM_COMPAT_CLIENT_INSTALL_PATH"), steamInstallPath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
finalProgram = protonExe;
|
|
|
|
|
finalArgs = {QStringLiteral("run"), program};
|
|
|
|
|
finalArgs.append(parts);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
info.insert(QStringLiteral("finalProgram"), finalProgram);
|
|
|
|
|
info.insert(QStringLiteral("finalArgs"), finalArgs);
|
|
|
|
|
info.insert(QStringLiteral("effectiveEnv"), effectiveEnv);
|
|
|
|
|
info.insert(QStringLiteral("ok"), true);
|
|
|
|
|
return info;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 12:13:07 +00:00
|
|
|
void GameLauncher::launchGame(Game *game)
|
|
|
|
|
{
|
|
|
|
|
if (!game) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if already running
|
2026-02-06 13:02:24 +00:00
|
|
|
if (m_daemonGameToSession.contains(game->id())) {
|
2026-01-18 12:13:07 +00:00
|
|
|
Q_EMIT gameError(game, tr("Game is already running"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
const QVariantMap info = resolveLaunchInfo(game);
|
|
|
|
|
if (!info.value(QStringLiteral("ok")).toBool()) {
|
|
|
|
|
Q_EMIT gameError(game, info.value(QStringLiteral("error")).toString());
|
2026-01-18 12:13:07 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
const QString launchCommand = info.value(QStringLiteral("launchCommand")).toString();
|
|
|
|
|
const QString launchType = info.value(QStringLiteral("launchType")).toString();
|
|
|
|
|
const QString provider = info.value(QStringLiteral("provider")).toString();
|
|
|
|
|
|
2026-02-09 13:03:01 +00:00
|
|
|
const QString runner = info.value(QStringLiteral("runner")).toString();
|
|
|
|
|
const QString runnerId = info.value(QStringLiteral("runnerId")).toString();
|
|
|
|
|
const QString runnerPath = info.value(QStringLiteral("runnerPath")).toString();
|
|
|
|
|
const QString prefixPath = info.value(QStringLiteral("prefixPath")).toString();
|
|
|
|
|
const QVariantMap envOverrides = info.value(QStringLiteral("envOverrides")).toMap();
|
|
|
|
|
if (runner == QLatin1String("proton") || runner == QLatin1String("wine")) {
|
2026-02-06 13:02:24 +00:00
|
|
|
const QString resolvedPrefixPath = info.value(QStringLiteral("resolvedPrefixPath")).toString();
|
|
|
|
|
if (!resolvedPrefixPath.isEmpty()) {
|
|
|
|
|
QDir().mkpath(resolvedPrefixPath);
|
|
|
|
|
}
|
2026-01-18 12:13:07 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
const QString finalProgram = info.value(QStringLiteral("finalProgram")).toString();
|
|
|
|
|
const QStringList finalArgs = info.value(QStringLiteral("finalArgs")).toStringList();
|
|
|
|
|
const QString workingDirectory = info.value(QStringLiteral("workingDirectory")).toString();
|
|
|
|
|
const QVariantMap effectiveEnv = info.value(QStringLiteral("effectiveEnv")).toMap();
|
|
|
|
|
|
|
|
|
|
// Always try daemon first — for all launch types
|
|
|
|
|
{
|
2026-02-09 13:03:01 +00:00
|
|
|
QDBusInterface iface(kGameCenterService,
|
|
|
|
|
kGameCenterPath,
|
|
|
|
|
kGameCenterInterface,
|
|
|
|
|
m_usingSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus());
|
2026-02-06 13:02:24 +00:00
|
|
|
QVariantMap launchSpec = {
|
|
|
|
|
{QStringLiteral("command"), launchCommand},
|
|
|
|
|
{QStringLiteral("gameId"), game->id()},
|
|
|
|
|
{QStringLiteral("displayName"), game->name()},
|
|
|
|
|
{QStringLiteral("provider"), provider},
|
|
|
|
|
{QStringLiteral("origin"), QStringLiteral("ui")},
|
|
|
|
|
};
|
2026-02-09 13:03:01 +00:00
|
|
|
|
|
|
|
|
if (!runnerId.isEmpty()) {
|
|
|
|
|
launchSpec.insert(QStringLiteral("runnerId"), runnerId);
|
|
|
|
|
}
|
|
|
|
|
if (!runner.isEmpty()) {
|
|
|
|
|
launchSpec.insert(QStringLiteral("runner"), runner);
|
|
|
|
|
}
|
|
|
|
|
if (!runnerPath.isEmpty()) {
|
|
|
|
|
launchSpec.insert(QStringLiteral("runnerPath"), runnerPath);
|
|
|
|
|
}
|
|
|
|
|
if (!prefixPath.isEmpty()) {
|
|
|
|
|
launchSpec.insert(QStringLiteral("prefixPath"), prefixPath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
launchSpec.insert(QStringLiteral("requestedProgram"), info.value(QStringLiteral("program")).toString());
|
|
|
|
|
launchSpec.insert(QStringLiteral("requestedArgs"), info.value(QStringLiteral("args")).toStringList());
|
|
|
|
|
if (!envOverrides.isEmpty()) {
|
|
|
|
|
launchSpec.insert(QStringLiteral("requestedEnvOverrides"), envOverrides);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
if (!finalProgram.isEmpty()) {
|
|
|
|
|
launchSpec.insert(QStringLiteral("program"), finalProgram);
|
|
|
|
|
launchSpec.insert(QStringLiteral("args"), finalArgs);
|
|
|
|
|
}
|
|
|
|
|
if (!effectiveEnv.isEmpty()) {
|
|
|
|
|
launchSpec.insert(QStringLiteral("envOverrides"), effectiveEnv);
|
|
|
|
|
}
|
|
|
|
|
if (!workingDirectory.isEmpty()) {
|
|
|
|
|
launchSpec.insert(QStringLiteral("workingDirectory"), workingDirectory);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QDBusReply<QString> reply = iface.call(QStringLiteral("Launch"), launchSpec);
|
|
|
|
|
if (reply.isValid() && !reply.value().isEmpty()) {
|
|
|
|
|
m_daemonGameToSession.insert(game->id(), reply.value());
|
|
|
|
|
m_daemonSessionToGame.insert(reply.value(), game->id());
|
|
|
|
|
game->setRunning(true);
|
|
|
|
|
game->setLastPlayed(QDateTime::currentDateTime());
|
|
|
|
|
Q_EMIT gameStarted(game);
|
|
|
|
|
Q_EMIT runningGamesChanged();
|
|
|
|
|
|
|
|
|
|
if (App::instance()->config()->exitAfterLaunch()) {
|
|
|
|
|
QTimer::singleShot(500, qApp, &QCoreApplication::quit);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-18 12:13:07 +00:00
|
|
|
|
2026-02-09 13:03:01 +00:00
|
|
|
const QString launchError = reply.isValid() ? QString() : reply.error().message();
|
|
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
// No fallback for non-URL commands — emit error
|
2026-02-09 13:03:01 +00:00
|
|
|
if (!launchError.isEmpty()) {
|
|
|
|
|
Q_EMIT gameError(game, tr("Game Center launch failed: %1").arg(launchError));
|
|
|
|
|
} else {
|
|
|
|
|
Q_EMIT gameError(game, tr("Game Center daemon is not available"));
|
|
|
|
|
}
|
2026-01-25 13:19:57 +00:00
|
|
|
}
|
2026-02-06 13:02:24 +00:00
|
|
|
}
|
2026-01-25 13:19:57 +00:00
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
void GameLauncher::stopGame(Game *game)
|
|
|
|
|
{
|
|
|
|
|
if (!game) {
|
|
|
|
|
return;
|
2026-01-18 12:13:07 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 13:03:01 +00:00
|
|
|
QDBusInterface iface(kGameCenterService,
|
|
|
|
|
kGameCenterPath,
|
|
|
|
|
kGameCenterInterface,
|
|
|
|
|
m_usingSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus());
|
2026-02-06 13:02:24 +00:00
|
|
|
iface.call(QStringLiteral("StopByGameId"), game->id());
|
|
|
|
|
}
|
2026-01-18 12:13:07 +00:00
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
bool GameLauncher::isGameRunning(Game *game) const
|
|
|
|
|
{
|
|
|
|
|
return game && m_daemonGameToSession.contains(game->id());
|
|
|
|
|
}
|
2026-01-18 12:13:07 +00:00
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
void GameLauncher::onDaemonSessionAdded(const QVariantMap &session)
|
|
|
|
|
{
|
|
|
|
|
const QVariantMap s = unwrapVariantMap(session);
|
|
|
|
|
const QString sessionId = s.value(QStringLiteral("sessionId")).toString();
|
|
|
|
|
const QString gameId = s.value(QStringLiteral("gameId")).toString();
|
|
|
|
|
if (sessionId.isEmpty() || gameId.isEmpty()) {
|
|
|
|
|
return;
|
2026-01-25 13:19:57 +00:00
|
|
|
}
|
2026-02-06 13:02:24 +00:00
|
|
|
|
|
|
|
|
if (m_daemonSessionToGame.contains(sessionId)) {
|
2026-01-18 12:13:07 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
m_daemonGameToSession.insert(gameId, sessionId);
|
|
|
|
|
m_daemonSessionToGame.insert(sessionId, gameId);
|
2026-01-25 13:19:57 +00:00
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
Game *game = App::instance()->gameModel()->gameById(gameId);
|
|
|
|
|
if (game) {
|
|
|
|
|
game->setRunning(true);
|
|
|
|
|
}
|
|
|
|
|
Q_EMIT runningGamesChanged();
|
2026-01-25 13:19:57 +00:00
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
applyRunningStateToLibrary();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GameLauncher::onDaemonSessionChanged(const QVariantMap &session)
|
|
|
|
|
{
|
|
|
|
|
const QVariantMap s = unwrapVariantMap(session);
|
|
|
|
|
const QString sessionId = s.value(QStringLiteral("sessionId")).toString();
|
|
|
|
|
const QString gameId = s.value(QStringLiteral("gameId")).toString();
|
|
|
|
|
if (sessionId.isEmpty() || gameId.isEmpty()) {
|
2026-01-25 13:19:57 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
m_daemonGameToSession.insert(gameId, sessionId);
|
|
|
|
|
m_daemonSessionToGame.insert(sessionId, gameId);
|
2026-01-25 13:19:57 +00:00
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
const QString state = s.value(QStringLiteral("state")).toString();
|
|
|
|
|
if (state == QLatin1String("Exited")) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-25 13:19:57 +00:00
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
Game *game = App::instance()->gameModel()->gameById(gameId);
|
|
|
|
|
if (game) {
|
|
|
|
|
game->setRunning(true);
|
|
|
|
|
}
|
2026-01-25 13:19:57 +00:00
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
Q_EMIT runningGamesChanged();
|
2026-01-25 13:19:57 +00:00
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
applyRunningStateToLibrary();
|
|
|
|
|
}
|
2026-01-25 13:19:57 +00:00
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
void GameLauncher::onDaemonSessionRemoved(const QString &sessionId, const QVariantMap &finalState)
|
|
|
|
|
{
|
|
|
|
|
if (sessionId.isEmpty()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-25 13:19:57 +00:00
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
QString gameId = m_daemonSessionToGame.take(sessionId);
|
|
|
|
|
if (!gameId.isEmpty()) {
|
|
|
|
|
m_daemonGameToSession.remove(gameId);
|
|
|
|
|
} else {
|
|
|
|
|
const QVariantMap fs = unwrapVariantMap(finalState);
|
|
|
|
|
gameId = fs.value(QStringLiteral("gameId")).toString();
|
|
|
|
|
if (!gameId.isEmpty()) {
|
|
|
|
|
m_daemonGameToSession.remove(gameId);
|
2026-01-25 13:19:57 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
Game *game = !gameId.isEmpty() ? App::instance()->gameModel()->gameById(gameId) : nullptr;
|
|
|
|
|
if (game) {
|
|
|
|
|
game->setRunning(false);
|
|
|
|
|
const QVariantMap fs = unwrapVariantMap(finalState);
|
|
|
|
|
Q_EMIT gameStopped(game, fs.value(QStringLiteral("exitCode")).toInt());
|
2026-01-25 13:19:57 +00:00
|
|
|
}
|
2026-02-06 13:02:24 +00:00
|
|
|
Q_EMIT runningGamesChanged();
|
2026-01-25 13:19:57 +00:00
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
applyRunningStateToLibrary();
|
|
|
|
|
}
|
2026-01-18 12:13:07 +00:00
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
void GameLauncher::onDaemonLaunchFailed(const QVariantMap &error)
|
|
|
|
|
{
|
|
|
|
|
const QVariantMap e = unwrapVariantMap(error);
|
|
|
|
|
QVariantMap context;
|
|
|
|
|
{
|
|
|
|
|
QVariant ctx = unwrapDbusVariant(e.value(QStringLiteral("context")));
|
|
|
|
|
if (ctx.canConvert<QVariantMap>()) {
|
|
|
|
|
context = ctx.toMap();
|
|
|
|
|
} else if (ctx.canConvert<QDBusArgument>()) {
|
|
|
|
|
context = qdbus_cast<QVariantMap>(ctx.value<QDBusArgument>());
|
|
|
|
|
}
|
2026-01-18 12:13:07 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
QString gameId = unwrapDbusVariant(context.value(QStringLiteral("gameId"))).toString();
|
|
|
|
|
if (gameId.isEmpty()) {
|
|
|
|
|
gameId = e.value(QStringLiteral("gameId")).toString();
|
|
|
|
|
}
|
|
|
|
|
if (gameId.isEmpty()) {
|
|
|
|
|
return;
|
2026-01-18 12:13:07 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
const QString message = e.value(QStringLiteral("error")).toString();
|
|
|
|
|
if (message.isEmpty()) {
|
2026-01-18 12:13:07 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
QTimer::singleShot(100, this, [this, gameId, message]() {
|
|
|
|
|
if (m_daemonGameToSession.contains(gameId)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-18 12:13:07 +00:00
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
Game *game = App::instance()->gameModel()->gameById(gameId);
|
|
|
|
|
if (!game) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-18 12:13:07 +00:00
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
Q_EMIT gameError(game, message);
|
|
|
|
|
});
|
2026-01-18 12:13:07 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
void GameLauncher::syncDaemonSessions()
|
2026-01-18 12:13:07 +00:00
|
|
|
{
|
2026-02-09 13:03:01 +00:00
|
|
|
QDBusInterface iface(kGameCenterService,
|
|
|
|
|
kGameCenterPath,
|
|
|
|
|
kGameCenterInterface,
|
|
|
|
|
m_usingSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus());
|
2026-01-18 12:13:07 +00:00
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
const QDBusReply<QVariantList> reply = iface.call(QStringLiteral("ListSessions"));
|
|
|
|
|
if (!reply.isValid()) {
|
2026-01-18 12:13:07 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
const QVariantList list = reply.value();
|
|
|
|
|
for (const QVariant &v : list) {
|
|
|
|
|
QVariantMap map;
|
|
|
|
|
if (v.canConvert<QVariantMap>()) {
|
|
|
|
|
map = v.toMap();
|
|
|
|
|
} else if (v.canConvert<QDBusArgument>()) {
|
|
|
|
|
map = qdbus_cast<QVariantMap>(v.value<QDBusArgument>());
|
|
|
|
|
}
|
|
|
|
|
if (!map.isEmpty()) {
|
|
|
|
|
onDaemonSessionAdded(map);
|
|
|
|
|
}
|
2026-01-18 12:13:07 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
applyRunningStateToLibrary();
|
2026-01-18 12:13:07 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
void GameLauncher::applyRunningStateToLibrary()
|
2026-01-18 12:13:07 +00:00
|
|
|
{
|
2026-02-06 13:02:24 +00:00
|
|
|
const QList<Game *> games = App::instance()->gameModel()->allGames();
|
|
|
|
|
for (Game *game : games) {
|
|
|
|
|
if (!game) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const bool shouldBeRunning = m_daemonGameToSession.contains(game->id());
|
|
|
|
|
game->setRunning(shouldBeRunning);
|
2026-01-18 12:13:07 +00:00
|
|
|
}
|
2026-02-06 13:02:24 +00:00
|
|
|
}
|
2026-01-18 12:13:07 +00:00
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
void GameLauncher::checkDaemonAvailability()
|
|
|
|
|
{
|
2026-02-09 13:03:01 +00:00
|
|
|
const bool systemAvailable = pingDaemon(QDBusConnection::systemBus());
|
|
|
|
|
const bool sessionAvailable = systemAvailable ? false : pingDaemon(QDBusConnection::sessionBus());
|
|
|
|
|
const bool available = systemAvailable || sessionAvailable;
|
|
|
|
|
const bool useSystemBus = systemAvailable;
|
|
|
|
|
|
|
|
|
|
disconnectDaemonSignals(QDBusConnection::systemBus(), this);
|
|
|
|
|
disconnectDaemonSignals(QDBusConnection::sessionBus(), this);
|
|
|
|
|
|
|
|
|
|
if (available) {
|
|
|
|
|
connectDaemonSignals(useSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus(), this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bool busChanged = m_usingSystemBus != useSystemBus;
|
|
|
|
|
if (busChanged) {
|
|
|
|
|
m_daemonGameToSession.clear();
|
|
|
|
|
m_daemonSessionToGame.clear();
|
|
|
|
|
Q_EMIT runningGamesChanged();
|
|
|
|
|
applyRunningStateToLibrary();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_usingSystemBus = useSystemBus;
|
2026-01-18 12:13:07 +00:00
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
if (available != m_daemonAvailable) {
|
|
|
|
|
m_daemonAvailable = available;
|
|
|
|
|
Q_EMIT daemonAvailableChanged();
|
2026-01-18 12:13:07 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
bool GameLauncher::daemonAvailable() const
|
2026-01-18 12:13:07 +00:00
|
|
|
{
|
2026-02-06 13:02:24 +00:00
|
|
|
return m_daemonAvailable;
|
|
|
|
|
}
|
2026-01-18 12:13:07 +00:00
|
|
|
|
2026-02-06 13:02:24 +00:00
|
|
|
void GameLauncher::retryDaemonConnection()
|
|
|
|
|
{
|
|
|
|
|
checkDaemonAvailability();
|
|
|
|
|
if (m_daemonAvailable) {
|
|
|
|
|
syncDaemonSessions();
|
2026-01-18 12:13:07 +00:00
|
|
|
}
|
|
|
|
|
}
|