diff --git a/src/gamecenter/gamecenterdaemon.cpp b/src/gamecenter/gamecenterdaemon.cpp index 7cda242..c450fbd 100644 --- a/src/gamecenter/gamecenterdaemon.cpp +++ b/src/gamecenter/gamecenterdaemon.cpp @@ -7,9 +7,13 @@ #include #include #include +#include +#include #include #include #include +#include +#include #include #include #include @@ -19,8 +23,14 @@ #include +#include + #include +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()) { const QDBusArgument arg = v.value(); const QMap asStringMap = qdbus_cast>(arg); @@ -77,7 +91,7 @@ static QVariant normalizeForDbus(QVariant v) } return m; } - if (v.canConvert()) { + 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 retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec); + if (retryReply.isValid()) { + out = normalizeVariantMapForDbus(retryReply.value()); + return true; + } + } + } + return false; + } + + iface.setTimeout(2000); + const QDBusReply 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 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( + "" + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""); +} + +} // 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 &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 m_virtualObject; + + QHash m_backends; + QHash m_clientUid; + QHash> 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(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 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 &args) +{ + if (!m_systemBus.isConnected()) { + return; + } + + const auto clientsIt = m_uidClients.constFind(uid); + if (clientsIt == m_uidClients.constEnd()) { + return; + } + + const QSet 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()) { + const QDBusArgument arg = capsV.value(); + caps = qdbus_cast(arg); + } else if (capsV.canConvert()) { + 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()) +{ +} + +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)); diff --git a/src/gamecenter/gamecenterdaemon.h b/src/gamecenter/gamecenterdaemon.h index f6094b5..4ddf944 100644 --- a/src/gamecenter/gamecenterdaemon.h +++ b/src/gamecenter/gamecenterdaemon.h @@ -3,6 +3,8 @@ #pragma once +#include + #include #include #include @@ -94,3 +96,19 @@ private: QHash m_unitNameToSessionId; QHash m_unitPathWatchers; }; + +class GameCenterSystemProxyPrivate; + +class GameCenterSystemProxy : public QObject +{ + Q_OBJECT + +public: + explicit GameCenterSystemProxy(QObject *parent = nullptr); + ~GameCenterSystemProxy() override; + + bool init(); + +private: + std::unique_ptr d; +}; diff --git a/src/gamecenter/main.cpp b/src/gamecenter/main.cpp index ccb3dbb..d307dc2 100644 --- a/src/gamecenter/main.cpp +++ b/src/gamecenter/main.cpp @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2026 A-La-Karte Contributors +#include +#include #include #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; diff --git a/src/gamelauncher.cpp b/src/gamelauncher.cpp index 36205e6..bd1bec3 100644 --- a/src/gamelauncher.cpp +++ b/src/gamelauncher.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -19,16 +21,88 @@ #include #include -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 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 retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec); + if (retryReply.isValid()) { + out = unwrapVariantMap(retryReply.value()); + return true; + } + } + } + return false; + } + + iface.setTimeout(2000); + const QDBusReply 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 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(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 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 reply = iface.call(QStringLiteral("Ping")); - const bool available = reply.isValid() && reply.value() == QLatin1String("ok"); if (available != m_daemonAvailable) { m_daemonAvailable = available; Q_EMIT daemonAvailableChanged(); diff --git a/src/gamelauncher.h b/src/gamelauncher.h index a4eddee..80415b5 100644 --- a/src/gamelauncher.h +++ b/src/gamelauncher.h @@ -48,6 +48,7 @@ private: QHash m_daemonGameToSession; QHash m_daemonSessionToGame; bool m_daemonAvailable = false; + bool m_usingSystemBus = false; void checkDaemonAvailability(); void syncDaemonSessions(); diff --git a/src/krunner/alakarterunner.cpp b/src/krunner/alakarterunner.cpp index 8a7fa69..763f8b2 100644 --- a/src/krunner/alakarterunner.cpp +++ b/src/krunner/alakarterunner.cpp @@ -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 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 reply = iface.call(QStringLiteral("Launch"), launchSpec); - if (reply.isValid() && !reply.value().isEmpty()) { + if (launchViaDaemon(QDBusConnection::systemBus(), launchSpec) || launchViaDaemon(QDBusConnection::sessionBus(), launchSpec)) { return; } }