GameCenter: integrate Runner1 resolution and improve daemon launch

This commit is contained in:
Marco Allegretti 2026-02-09 14:03:01 +01:00
parent 97d915abc6
commit 4573a3106e
6 changed files with 885 additions and 69 deletions

View file

@ -7,9 +7,13 @@
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusError>
#include <QDBusInterface>
#include <QDBusMessage>
#include <QDBusObjectPath>
#include <QDBusReply>
#include <QDBusVariant>
#include <QDBusVirtualObject>
#include <QDir>
#include <QProcess>
#include <QProcessEnvironment>
#include <QStandardPaths>
@ -19,8 +23,14 @@
#include <QRegularExpression>
#include <QSet>
#include <utility>
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 QString ensureScopeUnitName(const QString &unitName)
{
if (unitName.endsWith(QLatin1String(".scope"))) {
@ -49,6 +59,10 @@ static QVariant normalizeForDbus(QVariant v)
return QString(v.toChar());
}
if (v.userType() == QMetaType::QString || v.userType() == QMetaType::QStringList) {
return v;
}
if (v.canConvert<QDBusArgument>()) {
const QDBusArgument arg = v.value<QDBusArgument>();
const QMap<QString, QString> asStringMap = qdbus_cast<QMap<QString, QString>>(arg);
@ -77,7 +91,7 @@ static QVariant normalizeForDbus(QVariant v)
}
return m;
}
if (v.canConvert<QVariantList>()) {
if (v.userType() == QMetaType::QVariantList) {
QVariantList list = v.toList();
for (QVariant &item : list) {
item = normalizeForDbus(item);
@ -96,6 +110,57 @@ static QVariantMap normalizeVariantMapForDbus(const QVariantMap &map)
return out;
}
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 = normalizeVariantMapForDbus(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 = normalizeVariantMapForDbus(retryReply.value());
return true;
}
return false;
}
out = normalizeVariantMapForDbus(reply.value());
return true;
}
static QString mapStringValue(const QVariantMap &map, const QStringList &keys)
{
for (const QString &k : keys) {
@ -171,6 +236,445 @@ private:
} // namespace
static const QString kGameCenterService = QStringLiteral("org.kde.GameCenter1");
static const QString kGameCenterPath = QStringLiteral("/org/kde/GameCenter1");
static const QString kGameCenterInterface = QStringLiteral("org.kde.GameCenter1");
namespace
{
static QString gameCenterIntrospectionXml()
{
return QStringLiteral(
"<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-Bus Object Introspection 1.0//EN\" "
"\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">"
"<node>"
" <interface name=\"org.freedesktop.DBus.Introspectable\">"
" <method name=\"Introspect\">"
" <arg name=\"xml\" type=\"s\" direction=\"out\"/>"
" </method>"
" </interface>"
" <interface name=\"org.kde.GameCenter1\">"
" <method name=\"Ping\">"
" <arg name=\"out\" type=\"s\" direction=\"out\"/>"
" </method>"
" <method name=\"Version\">"
" <arg name=\"major\" type=\"u\" direction=\"out\"/>"
" <arg name=\"minor\" type=\"u\" direction=\"out\"/>"
" </method>"
" <method name=\"GetCapabilities\">"
" <arg name=\"caps\" type=\"a{sv}\" direction=\"out\"/>"
" </method>"
" <method name=\"ListSessions\">"
" <arg name=\"sessions\" type=\"av\" direction=\"out\"/>"
" </method>"
" <method name=\"GetSession\">"
" <arg name=\"sessionId\" type=\"s\" direction=\"in\"/>"
" <arg name=\"session\" type=\"a{sv}\" direction=\"out\"/>"
" </method>"
" <method name=\"Launch\">"
" <arg name=\"launchSpec\" type=\"a{sv}\" direction=\"in\"/>"
" <arg name=\"sessionId\" type=\"s\" direction=\"out\"/>"
" </method>"
" <method name=\"Stop\">"
" <arg name=\"sessionId\" type=\"s\" direction=\"in\"/>"
" </method>"
" <method name=\"StopByGameId\">"
" <arg name=\"gameId\" type=\"s\" direction=\"in\"/>"
" </method>"
" <method name=\"SetPolicy\">"
" <arg name=\"policy\" type=\"a{sv}\" direction=\"in\"/>"
" </method>"
" <method name=\"GetPolicy\">"
" <arg name=\"policy\" type=\"a{sv}\" direction=\"out\"/>"
" </method>"
" <signal name=\"SessionAdded\">"
" <arg name=\"session\" type=\"a{sv}\"/>"
" </signal>"
" <signal name=\"SessionChanged\">"
" <arg name=\"session\" type=\"a{sv}\"/>"
" </signal>"
" <signal name=\"SessionRemoved\">"
" <arg name=\"sessionId\" type=\"s\"/>"
" <arg name=\"finalState\" type=\"a{sv}\"/>"
" </signal>"
" <signal name=\"LaunchFailed\">"
" <arg name=\"error\" type=\"a{sv}\"/>"
" </signal>"
" </interface>"
"</node>");
}
} // namespace
class GameCenterSystemProxyPrivate;
class GameCenterProxyVirtualObject : public QDBusVirtualObject
{
Q_OBJECT
public:
explicit GameCenterProxyVirtualObject(GameCenterSystemProxyPrivate *proxy, QObject *parent = nullptr)
: QDBusVirtualObject(parent)
, m_proxy(proxy)
{
}
QString introspect(const QString &path) const override;
bool handleMessage(const QDBusMessage &message, const QDBusConnection &connection) override;
private:
GameCenterSystemProxyPrivate *m_proxy = nullptr;
};
class BackendSignalForwarder : public QObject
{
Q_OBJECT
public:
explicit BackendSignalForwarder(uint uid, GameCenterSystemProxyPrivate *proxy, QObject *parent = nullptr)
: QObject(parent)
, m_uid(uid)
, m_proxy(proxy)
{
}
public Q_SLOTS:
void onSessionAdded(const QVariantMap &session);
void onSessionChanged(const QVariantMap &session);
void onSessionRemoved(const QString &sessionId, const QVariantMap &finalState);
void onLaunchFailed(const QVariantMap &error);
private:
uint m_uid = 0;
GameCenterSystemProxyPrivate *m_proxy = nullptr;
};
struct BackendBusEntry {
BackendBusEntry(const QDBusConnection &bus, BackendSignalForwarder *forwarder)
: bus(bus)
, forwarder(forwarder)
{
}
QDBusConnection bus;
BackendSignalForwarder *forwarder = nullptr;
};
class GameCenterSystemProxyPrivate : public QObject
{
Q_OBJECT
public:
explicit GameCenterSystemProxyPrivate(QObject *parent = nullptr)
: QObject(parent)
{
}
bool init();
bool handleSystemMessage(const QDBusMessage &message, const QDBusConnection &connection);
void forwardSignalToUid(uint uid, const QString &member, const QList<QVariant> &args);
private Q_SLOTS:
void handleServiceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner);
private:
bool uidForSystemSender(const QString &senderUniqueName, uint &uid) const;
void rememberClient(const QString &senderUniqueName, uint uid);
void forgetClient(const QString &senderUniqueName);
BackendBusEntry *ensureBackend(uint uid);
QDBusConnection m_systemBus = QDBusConnection::systemBus();
std::unique_ptr<GameCenterProxyVirtualObject> m_virtualObject;
QHash<uint, BackendBusEntry> m_backends;
QHash<QString, uint> m_clientUid;
QHash<uint, QSet<QString>> m_uidClients;
};
QString GameCenterProxyVirtualObject::introspect(const QString &path) const
{
if (path == kGameCenterPath) {
return gameCenterIntrospectionXml();
}
return {};
}
bool GameCenterProxyVirtualObject::handleMessage(const QDBusMessage &message, const QDBusConnection &connection)
{
if (!m_proxy) {
return false;
}
return m_proxy->handleSystemMessage(message, connection);
}
void BackendSignalForwarder::onSessionAdded(const QVariantMap &session)
{
if (m_proxy) {
m_proxy->forwardSignalToUid(m_uid, QStringLiteral("SessionAdded"), {session});
}
}
void BackendSignalForwarder::onSessionChanged(const QVariantMap &session)
{
if (m_proxy) {
m_proxy->forwardSignalToUid(m_uid, QStringLiteral("SessionChanged"), {session});
}
}
void BackendSignalForwarder::onSessionRemoved(const QString &sessionId, const QVariantMap &finalState)
{
if (m_proxy) {
m_proxy->forwardSignalToUid(m_uid, QStringLiteral("SessionRemoved"), {sessionId, finalState});
}
}
void BackendSignalForwarder::onLaunchFailed(const QVariantMap &error)
{
if (m_proxy) {
m_proxy->forwardSignalToUid(m_uid, QStringLiteral("LaunchFailed"), {error});
}
}
bool GameCenterSystemProxyPrivate::init()
{
if (!m_systemBus.isConnected()) {
return false;
}
if (!m_systemBus.registerService(kGameCenterService)) {
return false;
}
m_virtualObject = std::make_unique<GameCenterProxyVirtualObject>(this);
if (!m_systemBus.registerVirtualObject(kGameCenterPath, m_virtualObject.get(), QDBusConnection::SingleNode)) {
return false;
}
if (m_systemBus.interface()) {
connect(m_systemBus.interface(), &QDBusConnectionInterface::serviceOwnerChanged, this, &GameCenterSystemProxyPrivate::handleServiceOwnerChanged);
}
return true;
}
bool GameCenterSystemProxyPrivate::uidForSystemSender(const QString &senderUniqueName, uint &uid) const
{
if (!m_systemBus.interface()) {
return false;
}
const QDBusReply<uint> uidReply = m_systemBus.interface()->serviceUid(senderUniqueName);
if (!uidReply.isValid()) {
return false;
}
uid = uidReply.value();
return true;
}
void GameCenterSystemProxyPrivate::rememberClient(const QString &senderUniqueName, uint uid)
{
const auto it = m_clientUid.constFind(senderUniqueName);
if (it != m_clientUid.constEnd()) {
if (it.value() == uid) {
return;
}
forgetClient(senderUniqueName);
}
m_clientUid.insert(senderUniqueName, uid);
m_uidClients[uid].insert(senderUniqueName);
}
void GameCenterSystemProxyPrivate::forgetClient(const QString &senderUniqueName)
{
const auto it = m_clientUid.find(senderUniqueName);
if (it == m_clientUid.end()) {
return;
}
const uint uid = it.value();
m_clientUid.erase(it);
auto clientsIt = m_uidClients.find(uid);
if (clientsIt != m_uidClients.end()) {
clientsIt.value().remove(senderUniqueName);
if (clientsIt.value().isEmpty()) {
m_uidClients.erase(clientsIt);
}
}
}
void GameCenterSystemProxyPrivate::handleServiceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner)
{
Q_UNUSED(oldOwner)
if (!newOwner.isEmpty()) {
return;
}
if (!name.startsWith(QLatin1Char(':'))) {
return;
}
forgetClient(name);
}
BackendBusEntry *GameCenterSystemProxyPrivate::ensureBackend(uint uid)
{
const auto it = m_backends.find(uid);
if (it != m_backends.end()) {
return &it.value();
}
const QString address = QStringLiteral("unix:path=/run/user/%1/bus").arg(uid);
const QString connectionName = QStringLiteral("gamecenter-user-%1").arg(uid);
QDBusConnection backend = QDBusConnection::connectToBus(address, connectionName);
if (!backend.isConnected()) {
return nullptr;
}
auto *forwarder = new BackendSignalForwarder(uid, this, this);
BackendBusEntry entry(backend, forwarder);
backend.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("SessionAdded"), forwarder, SLOT(onSessionAdded(QVariantMap)));
backend
.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("SessionChanged"), forwarder, SLOT(onSessionChanged(QVariantMap)));
backend.connect(kGameCenterService,
kGameCenterPath,
kGameCenterInterface,
QStringLiteral("SessionRemoved"),
forwarder,
SLOT(onSessionRemoved(QString, QVariantMap)));
backend.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("LaunchFailed"), forwarder, SLOT(onLaunchFailed(QVariantMap)));
auto inserted = m_backends.insert(uid, entry);
return &inserted.value();
}
void GameCenterSystemProxyPrivate::forwardSignalToUid(uint uid, const QString &member, const QList<QVariant> &args)
{
if (!m_systemBus.isConnected()) {
return;
}
const auto clientsIt = m_uidClients.constFind(uid);
if (clientsIt == m_uidClients.constEnd()) {
return;
}
const QSet<QString> clients = clientsIt.value();
for (const QString &client : clients) {
if (client.isEmpty()) {
continue;
}
QDBusMessage signal = QDBusMessage::createTargetedSignal(client, kGameCenterPath, kGameCenterInterface, member);
signal.setArguments(args);
m_systemBus.send(signal);
}
}
bool GameCenterSystemProxyPrivate::handleSystemMessage(const QDBusMessage &message, const QDBusConnection &connection)
{
if (message.type() != QDBusMessage::MethodCallMessage) {
return false;
}
if (message.interface() == QLatin1String("org.freedesktop.DBus.Introspectable") && message.member() == QLatin1String("Introspect")) {
if (!message.isReplyRequired()) {
return true;
}
connection.send(message.createReply(gameCenterIntrospectionXml()));
return true;
}
const QString sender = message.service();
if (sender.isEmpty()) {
if (message.isReplyRequired()) {
connection.send(message.createErrorReply(QDBusError::AccessDenied, QStringLiteral("missing sender")));
}
return true;
}
uint uid = 0;
if (!uidForSystemSender(sender, uid)) {
if (message.isReplyRequired()) {
connection.send(message.createErrorReply(QDBusError::AccessDenied, QStringLiteral("failed to determine caller uid")));
}
return true;
}
rememberClient(sender, uid);
BackendBusEntry *backend = ensureBackend(uid);
if (!backend) {
if (message.isReplyRequired()) {
connection.send(message.createErrorReply(QDBusError::ServiceUnknown, QStringLiteral("user bus backend unavailable")));
}
return true;
}
QDBusMessage forward = QDBusMessage::createMethodCall(kGameCenterService, message.path(), message.interface(), message.member());
forward.setArguments(message.arguments());
forward.setAutoStartService(true);
const QDBusMessage backendReply = backend->bus.call(forward, QDBus::Block, -1);
if (!message.isReplyRequired()) {
return true;
}
if (backendReply.type() == QDBusMessage::ErrorMessage) {
connection.send(message.createErrorReply(backendReply.errorName(), backendReply.errorMessage()));
return true;
}
if (backendReply.type() != QDBusMessage::ReplyMessage) {
connection.send(message.createErrorReply(QDBusError::Failed, QStringLiteral("backend did not reply")));
return true;
}
QDBusMessage out = message.createReply(backendReply.arguments());
if (message.interface() == kGameCenterInterface && message.member() == QLatin1String("GetCapabilities")) {
if (!out.arguments().isEmpty()) {
QVariant capsV = unwrapDbusVariant(out.arguments().first());
QVariantMap caps;
if (capsV.canConvert<QDBusArgument>()) {
const QDBusArgument arg = capsV.value<QDBusArgument>();
caps = qdbus_cast<QVariantMap>(arg);
} else if (capsV.canConvert<QVariantMap>()) {
caps = capsV.toMap();
}
if (!caps.isEmpty()) {
caps.insert(QStringLiteral("supportsSystemBus"), true);
out.setArguments({caps});
}
}
}
connection.send(out);
return true;
}
GameCenterSystemProxy::GameCenterSystemProxy(QObject *parent)
: QObject(parent)
, d(std::make_unique<GameCenterSystemProxyPrivate>())
{
}
GameCenterSystemProxy::~GameCenterSystemProxy() = default;
bool GameCenterSystemProxy::init()
{
return d->init();
}
void GameCenterDaemon::watchSystemdUnit(const QString &sessionId, const QString &unitName, const QDBusObjectPath &unitPath)
{
if (unitName.isEmpty()) {
@ -545,6 +1049,69 @@ QString GameCenterDaemon::launchDirect(const QVariantMap &launchSpec)
envOverrides = extractVariantMap(launchSpec.value(QStringLiteral("envOverrides")));
}
const QString runnerId = mapStringValue(launchSpec, {QStringLiteral("runnerId")});
const QString runner = mapStringValue(launchSpec, {QStringLiteral("runner")});
const QString runnerPath = mapStringValue(launchSpec, {QStringLiteral("runnerPath")});
const QString prefixPath = mapStringValue(launchSpec, {QStringLiteral("prefixPath")});
const QString requestedProgram = mapStringValue(launchSpec, {QStringLiteral("requestedProgram")});
QStringList requestedArgs;
if (launchSpec.contains(QStringLiteral("requestedArgs"))) {
requestedArgs = variantToStringList(launchSpec.value(QStringLiteral("requestedArgs")));
}
QVariantMap requestedEnvOverrides;
if (launchSpec.contains(QStringLiteral("requestedEnvOverrides"))) {
requestedEnvOverrides = extractVariantMap(launchSpec.value(QStringLiteral("requestedEnvOverrides")));
}
const bool wantsRunnerResolution = !runnerId.isEmpty() || runner == QLatin1String("wine") || runner == QLatin1String("proton");
if (wantsRunnerResolution) {
const QString baseProgram = requestedProgram.isEmpty() ? program : requestedProgram;
const QStringList baseArgs = requestedArgs.isEmpty() ? args : requestedArgs;
const QVariantMap baseEnv = requestedEnvOverrides.isEmpty() ? envOverrides : requestedEnvOverrides;
const QVariantMap resolveSpec = {
{QStringLiteral("runnerId"), runnerId},
{QStringLiteral("runner"), runner},
{QStringLiteral("runnerPath"), runnerPath},
{QStringLiteral("gameId"), gameId},
{QStringLiteral("prefixPath"), prefixPath},
{QStringLiteral("program"), baseProgram},
{QStringLiteral("args"), baseArgs},
{QStringLiteral("envOverrides"), baseEnv},
};
QVariantMap resolved;
if (tryResolveWithRunnerManager(resolveSpec, resolved)) {
if (!resolved.value(QStringLiteral("ok")).toBool()) {
QVariantMap ctx = launchSpec;
ctx.insert(QStringLiteral("runnerResolution"), resolved);
failLaunch(resolved.value(QStringLiteral("error")).toString(), ctx);
return {};
}
program = mapStringValue(resolved, {QStringLiteral("finalProgram")});
if (resolved.contains(QStringLiteral("finalArgs"))) {
args = variantToStringList(resolved.value(QStringLiteral("finalArgs")));
}
if (resolved.contains(QStringLiteral("effectiveEnv"))) {
envOverrides = extractVariantMap(resolved.value(QStringLiteral("effectiveEnv")));
}
const QString resolvedPrefixPath = mapStringValue(resolved, {QStringLiteral("resolvedPrefixPath")});
if (!resolvedPrefixPath.isEmpty()) {
QDir().mkpath(resolvedPrefixPath);
}
} else if (!runnerId.isEmpty()) {
QVariantMap ctx = launchSpec;
ctx.insert(QStringLiteral("runnerResolution"),
QVariantMap{{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("runner service unavailable")}});
failLaunch(QStringLiteral("runner service is not available"), ctx);
return {};
}
}
const QString sessionId = QUuid::createUuid().toString(QUuid::WithoutBraces);
const QString unitName = ensureScopeUnitName(QStringLiteral("alakarte-game-%1").arg(sessionId));

View file

@ -3,6 +3,8 @@
#pragma once
#include <memory>
#include <QDBusObjectPath>
#include <QDateTime>
#include <QHash>
@ -94,3 +96,19 @@ private:
QHash<QString, QString> m_unitNameToSessionId;
QHash<QString, QObject *> m_unitPathWatchers;
};
class GameCenterSystemProxyPrivate;
class GameCenterSystemProxy : public QObject
{
Q_OBJECT
public:
explicit GameCenterSystemProxy(QObject *parent = nullptr);
~GameCenterSystemProxy() override;
bool init();
private:
std::unique_ptr<GameCenterSystemProxyPrivate> d;
};

View file

@ -1,6 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include <QCommandLineOption>
#include <QCommandLineParser>
#include <QCoreApplication>
#include "gamecenterdaemon.h"
@ -9,6 +11,21 @@ int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QCommandLineParser parser;
parser.setApplicationDescription(QStringLiteral("A-La-Karte Game Center"));
parser.addHelpOption();
parser.addOption(QCommandLineOption(QStringLiteral("system"), QStringLiteral("Run as system bus proxy")));
parser.process(app);
if (parser.isSet(QStringLiteral("system"))) {
GameCenterSystemProxy proxy;
if (!proxy.init()) {
return 1;
}
return app.exec();
}
GameCenterDaemon daemon;
if (!daemon.init()) {
return 1;

View file

@ -7,6 +7,8 @@
#include <QCoreApplication>
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusError>
#include <QDBusInterface>
#include <QDBusReply>
#include <QDBusVariant>
@ -19,16 +21,88 @@
#include <QTimer>
#include <QUrl>
static QStringList steamCandidateRoots()
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)
{
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"),
};
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)));
}
static QVariant unwrapDbusVariant(QVariant v)
@ -48,6 +122,59 @@ static QVariantMap unwrapVariantMap(const QVariantMap &map)
return out;
}
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();
static QString findSteamClientInstallPathFromProton(const QString &protonExe)
{
if (protonExe.isEmpty()) {
@ -130,40 +257,23 @@ static QString discoverDefaultProtonExecutable()
return cached;
}
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"),
};
}
GameLauncher::GameLauncher(QObject *parent)
: QObject(parent)
, m_daemonAvailable(false)
, m_usingSystemBus(false)
{
QDBusConnection bus = QDBusConnection::sessionBus();
if (bus.isConnected()) {
bus.connect(QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("/org/kde/GameCenter1"),
QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("SessionAdded"),
this,
SLOT(onDaemonSessionAdded(QVariantMap)));
bus.connect(QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("/org/kde/GameCenter1"),
QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("SessionChanged"),
this,
SLOT(onDaemonSessionChanged(QVariantMap)));
bus.connect(QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("/org/kde/GameCenter1"),
QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("SessionRemoved"),
this,
SLOT(onDaemonSessionRemoved(QString, QVariantMap)));
bus.connect(QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("/org/kde/GameCenter1"),
QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("LaunchFailed"),
this,
SLOT(onDaemonLaunchFailed(QVariantMap)));
}
if (auto *app = qobject_cast<App *>(parent)) {
connect(app->gameModel(), &GameModel::countChanged, this, [this]() {
QTimer::singleShot(0, this, [this]() {
@ -222,12 +332,14 @@ QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
}
const QString runner = game->launchRunner().trimmed();
const QString runnerId = game->launchRunnerId().trimmed();
const QString runnerPath = game->launchRunnerPath().trimmed();
const QString prefixPath = game->launchPrefixPath().trimmed();
const QVariantMap launchEnv = game->launchEnv();
const bool hasLaunchOverrides = !runner.isEmpty() || !runnerPath.isEmpty() || !prefixPath.isEmpty() || !launchEnv.isEmpty();
const bool hasLaunchOverrides = !runner.isEmpty() || !runnerId.isEmpty() || !runnerPath.isEmpty() || !prefixPath.isEmpty() || !launchEnv.isEmpty();
info.insert(QStringLiteral("runner"), runner);
info.insert(QStringLiteral("runnerId"), runnerId);
info.insert(QStringLiteral("runnerPath"), runnerPath);
info.insert(QStringLiteral("prefixPath"), prefixPath);
info.insert(QStringLiteral("workingDirectory"), game->workingDirectory());
@ -287,6 +399,47 @@ QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
return info;
}
if (!runnerId.isEmpty() || runner == QLatin1String("wine") || runner == QLatin1String("proton")) {
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},
};
QVariantMap resolved;
if (tryResolveWithRunnerManager(runnerSpec, resolved)) {
if (!resolved.value(QStringLiteral("ok")).toBool()) {
info.insert(QStringLiteral("error"), resolved.value(QStringLiteral("error")).toString());
return info;
}
info.insert(QStringLiteral("finalProgram"), resolved.value(QStringLiteral("finalProgram")).toString());
info.insert(QStringLiteral("finalArgs"), resolved.value(QStringLiteral("finalArgs")).toStringList());
info.insert(QStringLiteral("effectiveEnv"), resolved.value(QStringLiteral("effectiveEnv")).toMap());
const QString resolvedPrefixPath = resolved.value(QStringLiteral("resolvedPrefixPath")).toString();
if (!resolvedPrefixPath.isEmpty()) {
info.insert(QStringLiteral("resolvedPrefixPath"), resolvedPrefixPath);
}
const QString resolvedSteamInstallPath = resolved.value(QStringLiteral("resolvedSteamInstallPath")).toString();
if (!resolvedSteamInstallPath.isEmpty()) {
info.insert(QStringLiteral("resolvedSteamInstallPath"), resolvedSteamInstallPath);
}
info.insert(QStringLiteral("ok"), true);
return info;
} else if (!runnerId.isEmpty()) {
info.insert(QStringLiteral("error"), tr("Runner service is not available"));
return info;
}
}
if (runner == QLatin1String("custom")) {
if (runnerPath.isEmpty()) {
info.insert(QStringLiteral("error"), tr("Custom runner is enabled but no runner path is configured"));
@ -354,7 +507,6 @@ QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
}
void GameLauncher::launchGame(Game *game)
{
if (!game) {
return;
@ -376,7 +528,12 @@ void GameLauncher::launchGame(Game *game)
const QString launchType = info.value(QStringLiteral("launchType")).toString();
const QString provider = info.value(QStringLiteral("provider")).toString();
if (info.value(QStringLiteral("runner")).toString() == QLatin1String("proton")) {
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")) {
const QString resolvedPrefixPath = info.value(QStringLiteral("resolvedPrefixPath")).toString();
if (!resolvedPrefixPath.isEmpty()) {
QDir().mkpath(resolvedPrefixPath);
@ -390,10 +547,10 @@ void GameLauncher::launchGame(Game *game)
// Always try daemon first — for all launch types
{
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("/org/kde/GameCenter1"),
QStringLiteral("org.kde.GameCenter1"),
QDBusConnection::sessionBus());
QDBusInterface iface(kGameCenterService,
kGameCenterPath,
kGameCenterInterface,
m_usingSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus());
QVariantMap launchSpec = {
{QStringLiteral("command"), launchCommand},
{QStringLiteral("gameId"), game->id()},
@ -401,6 +558,26 @@ void GameLauncher::launchGame(Game *game)
{QStringLiteral("provider"), provider},
{QStringLiteral("origin"), QStringLiteral("ui")},
};
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);
}
if (!finalProgram.isEmpty()) {
launchSpec.insert(QStringLiteral("program"), finalProgram);
launchSpec.insert(QStringLiteral("args"), finalArgs);
@ -427,6 +604,8 @@ void GameLauncher::launchGame(Game *game)
return;
}
const QString launchError = reply.isValid() ? QString() : reply.error().message();
// Daemon call failed — fallback for URL-type commands only
if (launchType == QLatin1String("url")) {
const QString url = info.value(QStringLiteral("url")).toString();
@ -439,7 +618,11 @@ void GameLauncher::launchGame(Game *game)
}
// No fallback for non-URL commands — emit error
Q_EMIT gameError(game, tr("Game Center daemon is not available"));
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"));
}
}
}
@ -449,10 +632,10 @@ void GameLauncher::stopGame(Game *game)
return;
}
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("/org/kde/GameCenter1"),
QStringLiteral("org.kde.GameCenter1"),
QDBusConnection::sessionBus());
QDBusInterface iface(kGameCenterService,
kGameCenterPath,
kGameCenterInterface,
m_usingSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus());
iface.call(QStringLiteral("StopByGameId"), game->id());
}
@ -583,10 +766,10 @@ void GameLauncher::onDaemonLaunchFailed(const QVariantMap &error)
void GameLauncher::syncDaemonSessions()
{
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("/org/kde/GameCenter1"),
QStringLiteral("org.kde.GameCenter1"),
QDBusConnection::sessionBus());
QDBusInterface iface(kGameCenterService,
kGameCenterPath,
kGameCenterInterface,
m_usingSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus());
const QDBusReply<QVariantList> reply = iface.call(QStringLiteral("ListSessions"));
if (!reply.isValid()) {
@ -623,13 +806,28 @@ void GameLauncher::applyRunningStateToLibrary()
void GameLauncher::checkDaemonAvailability()
{
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("/org/kde/GameCenter1"),
QStringLiteral("org.kde.GameCenter1"),
QDBusConnection::sessionBus());
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;
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
const bool available = reply.isValid() && reply.value() == QLatin1String("ok");
if (available != m_daemonAvailable) {
m_daemonAvailable = available;
Q_EMIT daemonAvailableChanged();

View file

@ -48,6 +48,7 @@ private:
QHash<QString, QString> m_daemonGameToSession;
QHash<QString, QString> m_daemonSessionToGame;
bool m_daemonAvailable = false;
bool m_usingSystemBus = false;
void checkDaemonAvailability();
void syncDaemonSessions();

View file

@ -17,6 +17,26 @@
K_PLUGIN_CLASS_WITH_JSON(AlakarteRunner, "plasma-runner-alakarte.json")
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 bool launchViaDaemon(QDBusConnection bus, const QVariantMap &launchSpec)
{
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("Launch"), launchSpec);
return reply.isValid() && !reply.value().isEmpty();
}
AlakarteRunner::AlakarteRunner(QObject *parent, const KPluginMetaData &metaData)
: KRunner::AbstractRunner(parent, metaData)
{
@ -154,10 +174,6 @@ void AlakarteRunner::run(const KRunner::RunnerContext &context, const KRunner::Q
// Always try daemon first for all commands (including Steam/Lutris)
if (!match.id().isEmpty()) {
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("/org/kde/GameCenter1"),
QStringLiteral("org.kde.GameCenter1"),
QDBusConnection::sessionBus());
QVariantMap launchSpec = {
{QStringLiteral("command"), command},
{QStringLiteral("gameId"), match.id()},
@ -165,8 +181,7 @@ void AlakarteRunner::run(const KRunner::RunnerContext &context, const KRunner::Q
{QStringLiteral("origin"), QStringLiteral("krunner")},
};
const QDBusReply<QString> reply = iface.call(QStringLiteral("Launch"), launchSpec);
if (reply.isValid() && !reply.value().isEmpty()) {
if (launchViaDaemon(QDBusConnection::systemBus(), launchSpec) || launchViaDaemon(QDBusConnection::sessionBus(), launchSpec)) {
return;
}
}