From 7724205354f90e7fe29462c99d68345b2263162b Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Thu, 12 Feb 2026 14:44:20 +0100 Subject: [PATCH] runner: add game profile API --- src/runner/runnermanagerdaemon.cpp | 376 ++++++++++++++++++++++++++++- src/runner/runnermanagerdaemon.h | 14 ++ src/runnermanagerclient.cpp | 233 ++++++++++++++++++ src/runnermanagerclient.h | 16 ++ 4 files changed, 633 insertions(+), 6 deletions(-) diff --git a/src/runner/runnermanagerdaemon.cpp b/src/runner/runnermanagerdaemon.cpp index 8582288..aeda07a 100644 --- a/src/runner/runnermanagerdaemon.cpp +++ b/src/runner/runnermanagerdaemon.cpp @@ -4,10 +4,12 @@ #include "runnermanagerdaemon.h" #include +#include #include #include #include #include +#include #include #include #include @@ -32,6 +34,52 @@ static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1"); static const QString kRunnerPath = QStringLiteral("/org/kde/ALaKarte/Runner1"); +static QVariant unwrapDbusVariant(QVariant v) +{ + if (v.metaType() == QMetaType::fromType()) { + v = v.value().variant(); + } + return v; +} + +static QVariantMap unwrapVariantMap(QVariant v) +{ + v = unwrapDbusVariant(v); + if (v.metaType() == QMetaType::fromType()) { + const QDBusArgument arg = v.value(); + return qdbus_cast(arg); + } + if (v.canConvert()) { + return v.toMap(); + } + return {}; +} + +static QStringList unwrapStringList(QVariant v) +{ + v = unwrapDbusVariant(v); + if (v.metaType() == QMetaType::fromType()) { + const QDBusArgument arg = v.value(); + return qdbus_cast(arg); + } + if (v.canConvert()) { + return v.toStringList(); + } + if (v.canConvert()) { + const QVariantList list = v.toList(); + QStringList out; + out.reserve(list.size()); + for (const QVariant &item : list) { + const QString s = item.toString(); + if (!s.isEmpty()) { + out.push_back(s); + } + } + return out; + } + return {}; +} + static QString stableIdForPath(const QString &prefix, const QString &path) { const QByteArray digest = QCryptographicHash::hash(path.toUtf8(), QCryptographicHash::Sha1).toHex(); @@ -46,6 +94,7 @@ RunnerManagerDaemon::RunnerManagerDaemon(QObject *parent) bool RunnerManagerDaemon::init() { loadRegistry(); + loadGameProfiles(); m_discovered = discoverRunners(); QDBusConnection bus = QDBusConnection::sessionBus(); @@ -181,6 +230,76 @@ bool RunnerManagerDaemon::saveRegistry() const return true; } +QString RunnerManagerDaemon::gameProfilesPath() const +{ + const QString base = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + return base + QStringLiteral("/runner-game-profiles.json"); +} + +void RunnerManagerDaemon::loadGameProfiles() +{ + m_gameProfiles.clear(); + + QFile f(gameProfilesPath()); + if (!f.open(QIODevice::ReadOnly)) { + return; + } + + const QJsonDocument doc = QJsonDocument::fromJson(f.readAll()); + if (!doc.isObject()) { + return; + } + + const QJsonArray arr = doc.object().value(QStringLiteral("profiles")).toArray(); + for (const QJsonValue &v : arr) { + if (!v.isObject()) { + continue; + } + const QVariantMap map = v.toObject().toVariantMap(); + const QString gameId = map.value(QStringLiteral("gameId")).toString(); + if (gameId.isEmpty()) { + continue; + } + m_gameProfiles.insert(gameId, map); + } +} + +bool RunnerManagerDaemon::saveGameProfiles() const +{ + const QString path = gameProfilesPath(); + QDir().mkpath(QFileInfo(path).absolutePath()); + + QJsonArray arr; + for (auto it = m_gameProfiles.constBegin(); it != m_gameProfiles.constEnd(); ++it) { + QVariantMap profile = it.value(); + if (!profile.contains(QStringLiteral("gameId"))) { + profile.insert(QStringLiteral("gameId"), it.key()); + } + arr.push_back(QJsonObject::fromVariantMap(profile)); + } + + QJsonObject root; + root.insert(QStringLiteral("profiles"), arr); + + QFile f(path); + if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + return false; + } + + f.write(QJsonDocument(root).toJson(QJsonDocument::Indented)); + return true; +} + +QVariantMap RunnerManagerDaemon::gameProfileForGameId(const QString &gameId) const +{ + const QString id = gameId.trimmed(); + if (id.isEmpty()) { + return {}; + } + const auto it = m_gameProfiles.constFind(id); + return it == m_gameProfiles.constEnd() ? QVariantMap{} : it.value(); +} + QStringList RunnerManagerDaemon::steamCandidateRoots() { const QString home = QDir::homePath(); @@ -998,14 +1117,192 @@ QVariantMap RunnerManagerDaemon::DeletePrefix(const QVariantMap &spec) return out; } +QVariantMap RunnerManagerDaemon::GetGameProfile(const QString &gameId) const +{ + QVariantMap out; + out.insert(QStringLiteral("ok"), false); + + const QString id = gameId.trimmed(); + if (id.isEmpty()) { + out.insert(QStringLiteral("error"), QStringLiteral("missing gameId")); + return out; + } + + const QVariantMap profile = gameProfileForGameId(id); + out.insert(QStringLiteral("ok"), true); + out.insert(QStringLiteral("profile"), profile); + return out; +} + +QVariantList RunnerManagerDaemon::ListGameProfiles() const +{ + QVariantList out; + out.reserve(m_gameProfiles.size()); + for (auto it = m_gameProfiles.constBegin(); it != m_gameProfiles.constEnd(); ++it) { + QVariantMap profile = it.value(); + if (!profile.contains(QStringLiteral("gameId"))) { + profile.insert(QStringLiteral("gameId"), it.key()); + } + out.push_back(profile); + } + return out; +} + +QVariantMap RunnerManagerDaemon::SetGameProfile(const QVariantMap &spec) +{ + QVariantMap out; + out.insert(QStringLiteral("ok"), false); + + const QString gameId = spec.value(QStringLiteral("gameId")).toString().trimmed(); + if (gameId.isEmpty()) { + out.insert(QStringLiteral("error"), QStringLiteral("missing gameId")); + return out; + } + + QVariantMap profile; + profile.insert(QStringLiteral("gameId"), gameId); + + auto takeStringIfPresent = [&](const QString &key) { + if (!spec.contains(key)) { + return; + } + const QString v = spec.value(key).toString().trimmed(); + if (!v.isEmpty()) { + profile.insert(key, v); + } + }; + + takeStringIfPresent(QStringLiteral("runnerId")); + takeStringIfPresent(QStringLiteral("runner")); + takeStringIfPresent(QStringLiteral("runnerPath")); + takeStringIfPresent(QStringLiteral("prefixPath")); + + if (spec.contains(QStringLiteral("dllOverrides"))) { + const QVariantMap raw = unwrapVariantMap(spec.value(QStringLiteral("dllOverrides"))); + QVariantMap dllOverrides; + for (auto it = raw.constBegin(); it != raw.constEnd(); ++it) { + const QString key = it.key().trimmed(); + if (key.isEmpty() || key.contains(QLatin1Char('=')) || key.contains(QLatin1Char(';'))) { + continue; + } + const QString value = it.value().toString().trimmed(); + if (value.isEmpty()) { + continue; + } + dllOverrides.insert(key, value); + } + if (!dllOverrides.isEmpty()) { + profile.insert(QStringLiteral("dllOverrides"), dllOverrides); + } + } + + if (spec.contains(QStringLiteral("envOverrides"))) { + const QVariantMap rawEnv = unwrapVariantMap(spec.value(QStringLiteral("envOverrides"))); + QVariantMap env; + for (auto it = rawEnv.constBegin(); it != rawEnv.constEnd(); ++it) { + const QString key = it.key(); + if (key.isEmpty() || key.contains(QLatin1Char('='))) { + continue; + } + env.insert(key, it.value().toString()); + } + if (!env.isEmpty()) { + profile.insert(QStringLiteral("envOverrides"), env); + } + } + + if (spec.contains(QStringLiteral("extraArgs"))) { + const QStringList raw = unwrapStringList(spec.value(QStringLiteral("extraArgs"))); + QStringList extraArgs; + extraArgs.reserve(raw.size()); + for (const QString &a : raw) { + const QString trimmed = a.trimmed(); + if (!trimmed.isEmpty()) { + extraArgs.push_back(trimmed); + } + } + if (!extraArgs.isEmpty()) { + profile.insert(QStringLiteral("extraArgs"), extraArgs); + } + } + + if (profile.size() <= 1) { + out.insert(QStringLiteral("error"), QStringLiteral("empty profile")); + return out; + } + + m_gameProfiles.insert(gameId, profile); + if (!saveGameProfiles()) { + out.insert(QStringLiteral("error"), QStringLiteral("failed to save game profiles")); + return out; + } + + Q_EMIT GameProfilesChanged(); + out.insert(QStringLiteral("ok"), true); + out.insert(QStringLiteral("profile"), profile); + return out; +} + +QVariantMap RunnerManagerDaemon::ClearGameProfile(const QString &gameId) +{ + QVariantMap out; + out.insert(QStringLiteral("ok"), false); + + const QString id = gameId.trimmed(); + if (id.isEmpty()) { + out.insert(QStringLiteral("error"), QStringLiteral("missing gameId")); + return out; + } + + if (!m_gameProfiles.contains(id)) { + out.insert(QStringLiteral("ok"), true); + out.insert(QStringLiteral("gameId"), id); + return out; + } + + m_gameProfiles.remove(id); + if (!saveGameProfiles()) { + out.insert(QStringLiteral("error"), QStringLiteral("failed to save game profiles")); + return out; + } + + Q_EMIT GameProfilesChanged(); + out.insert(QStringLiteral("ok"), true); + out.insert(QStringLiteral("gameId"), id); + return out; +} + QVariantMap RunnerManagerDaemon::ResolveLaunch(const QVariantMap &spec) const { QVariantMap out; out.insert(QStringLiteral("ok"), false); - const QString runnerId = spec.value(QStringLiteral("runnerId")).toString(); - QString runner = spec.value(QStringLiteral("runner")).toString(); - QString runnerPath = spec.value(QStringLiteral("runnerPath")).toString(); + const QString gameId = spec.value(QStringLiteral("gameId")).toString(); + + const bool useGameProfile = spec.contains(QStringLiteral("useGameProfile")) ? spec.value(QStringLiteral("useGameProfile")).toBool() : false; + + QVariantMap profile; + if (useGameProfile && !gameId.isEmpty()) { + profile = gameProfileForGameId(gameId); + } + + const QString specRunnerId = spec.value(QStringLiteral("runnerId")).toString(); + const QString specRunner = spec.value(QStringLiteral("runner")).toString(); + const QString specRunnerPath = spec.value(QStringLiteral("runnerPath")).toString(); + + QString runnerId = specRunnerId; + QString runner = specRunner; + QString runnerPath = specRunnerPath; + + if (runnerId.isEmpty() && profile.contains(QStringLiteral("runnerId"))) { + runnerId = profile.value(QStringLiteral("runnerId")).toString(); + } + if (runner.isEmpty() && profile.contains(QStringLiteral("runner"))) { + runner = profile.value(QStringLiteral("runner")).toString(); + } + if (runnerPath.isEmpty() && profile.contains(QStringLiteral("runnerPath"))) { + runnerPath = profile.value(QStringLiteral("runnerPath")).toString(); + } if (!runnerId.isEmpty()) { const RunnerInfo info = runnerById(runnerId); @@ -1017,17 +1314,84 @@ QVariantMap RunnerManagerDaemon::ResolveLaunch(const QVariantMap &spec) const runnerPath = info.path; } - const QString gameId = spec.value(QStringLiteral("gameId")).toString(); const QString program = spec.value(QStringLiteral("program")).toString(); - const QStringList args = spec.value(QStringLiteral("args")).toStringList(); + QStringList args = unwrapStringList(spec.value(QStringLiteral("args"))); QString prefixPath = spec.value(QStringLiteral("prefixPath")).toString(); + if (prefixPath.isEmpty() && profile.contains(QStringLiteral("prefixPath"))) { + prefixPath = profile.value(QStringLiteral("prefixPath")).toString(); + } if (program.isEmpty()) { out.insert(QStringLiteral("error"), QStringLiteral("missing program")); return out; } - QVariantMap effectiveEnv = spec.value(QStringLiteral("envOverrides")).toMap(); + { + const QStringList rawProfileExtraArgs = unwrapStringList(profile.value(QStringLiteral("extraArgs"))); + for (const QString &a : rawProfileExtraArgs) { + const QString trimmed = a.trimmed(); + if (!trimmed.isEmpty()) { + args.append(trimmed); + } + } + + const QStringList rawSpecExtraArgs = unwrapStringList(spec.value(QStringLiteral("extraArgs"))); + for (const QString &a : rawSpecExtraArgs) { + const QString trimmed = a.trimmed(); + if (!trimmed.isEmpty()) { + args.append(trimmed); + } + } + } + + QVariantMap effectiveEnv; + if (profile.contains(QStringLiteral("envOverrides"))) { + effectiveEnv = profile.value(QStringLiteral("envOverrides")).toMap(); + } + const QVariantMap specEnv = unwrapVariantMap(spec.value(QStringLiteral("envOverrides"))); + for (auto it = specEnv.constBegin(); it != specEnv.constEnd(); ++it) { + effectiveEnv.insert(it.key(), it.value()); + } + + QVariantMap dllOverrides; + if (profile.contains(QStringLiteral("dllOverrides"))) { + dllOverrides = profile.value(QStringLiteral("dllOverrides")).toMap(); + } + const QVariantMap specDllOverrides = unwrapVariantMap(spec.value(QStringLiteral("dllOverrides"))); + for (auto it = specDllOverrides.constBegin(); it != specDllOverrides.constEnd(); ++it) { + const QString key = it.key().trimmed(); + if (key.isEmpty() || key.contains(QLatin1Char('=')) || key.contains(QLatin1Char(';'))) { + continue; + } + const QString value = it.value().toString().trimmed(); + if (value.isEmpty()) { + continue; + } + dllOverrides.insert(key, value); + } + + if (!dllOverrides.isEmpty() && !effectiveEnv.contains(QStringLiteral("WINEDLLOVERRIDES")) + && ((runner == QLatin1String("wine")) || (runner == QLatin1String("proton")))) { + QStringList keys; + keys.reserve(dllOverrides.size()); + for (auto it = dllOverrides.constBegin(); it != dllOverrides.constEnd(); ++it) { + keys.push_back(it.key()); + } + std::sort(keys.begin(), keys.end()); + + QStringList parts; + parts.reserve(keys.size()); + for (const QString &k : keys) { + const QString v = dllOverrides.value(k).toString().trimmed(); + if (v.isEmpty()) { + continue; + } + parts.push_back(k + QLatin1Char('=') + v); + } + if (!parts.isEmpty()) { + effectiveEnv.insert(QStringLiteral("WINEDLLOVERRIDES"), parts.join(QStringLiteral(";"))); + } + } if ((runner == QLatin1String("wine")) || (runner == QLatin1String("proton"))) { if (prefixPath.isEmpty()) { diff --git a/src/runner/runnermanagerdaemon.h b/src/runner/runnermanagerdaemon.h index 1012505..9575eee 100644 --- a/src/runner/runnermanagerdaemon.h +++ b/src/runner/runnermanagerdaemon.h @@ -42,11 +42,18 @@ public Q_SLOTS: QVariantMap DeletePrefix(const QVariantMap &spec); QVariantMap ResolveLaunch(const QVariantMap &spec) const; + QVariantMap GetGameProfile(const QString &gameId) const; + QVariantMap SetGameProfile(const QVariantMap &spec); + QVariantMap ClearGameProfile(const QString &gameId); + QVariantList ListGameProfiles() const; + Q_SIGNALS: void InstallStarted(const QString &installId, const QVariantMap &spec); void InstallProgress(const QString &installId, qint64 receivedBytes, qint64 totalBytes); void InstallFinished(const QString &installId, const QVariantMap &result); + void GameProfilesChanged(); + private: struct RunnerInfo { QString id; @@ -65,6 +72,11 @@ private: void loadRegistry(); bool saveRegistry() const; + QString gameProfilesPath() const; + void loadGameProfiles(); + bool saveGameProfiles() const; + QVariantMap gameProfileForGameId(const QString &gameId) const; + QList discoverRunners() const; QString defaultProtonExecutable() const; @@ -81,4 +93,6 @@ private: QHash> m_installReplies; QHash> m_installProcesses; + + QHash m_gameProfiles; }; diff --git a/src/runnermanagerclient.cpp b/src/runnermanagerclient.cpp index 03fa17b..1fca445 100644 --- a/src/runnermanagerclient.cpp +++ b/src/runnermanagerclient.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +27,52 @@ 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"); +namespace +{ +static QVariant unwrapDbusVariant(QVariant v) +{ + if (v.canConvert()) { + v = v.value().variant(); + } + return v; +} + +static QVariantMap unwrapVariantMap(QVariant v) +{ + v = unwrapDbusVariant(v); + if (v.metaType() == QMetaType::fromType()) { + const QDBusArgument arg = v.value(); + return qdbus_cast(arg); + } + if (v.canConvert()) { + return v.toMap(); + } + return {}; +} + +static QVariantMap normalizeMap(QVariantMap m) +{ + for (auto it = m.begin(); it != m.end(); ++it) { + it.value() = unwrapDbusVariant(it.value()); + if (it.key() == QLatin1String("envOverrides")) { + it.value() = unwrapVariantMap(it.value()); + } + } + return m; +} + +static QVariantMap normalizeResultWithProfile(QVariantMap result) +{ + result = normalizeMap(result); + if (result.contains(QStringLiteral("profile"))) { + QVariantMap profile = unwrapVariantMap(result.value(QStringLiteral("profile"))); + profile = normalizeMap(profile); + result.insert(QStringLiteral("profile"), profile); + } + return result; +} +} + static QString stableIdForUrl(const QString &type, const QString &url) { const QByteArray digest = QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Sha1).toHex(); @@ -80,6 +127,8 @@ RunnerManagerClient::RunnerManagerClient(QObject *parent) SLOT(onInstallProgress(QString, qlonglong, qlonglong))); bus.connect(kRunnerService, kRunnerPath, kRunnerInterface, QStringLiteral("InstallFinished"), this, SLOT(onInstallFinished(QString, QVariantMap))); + + bus.connect(kRunnerService, kRunnerPath, kRunnerInterface, QStringLiteral("GameProfilesChanged"), this, SLOT(onGameProfilesChanged())); } if (QCoreApplication::instance()) { @@ -89,6 +138,7 @@ RunnerManagerClient::RunnerManagerClient(QObject *parent) } refreshRunners(); + refreshGameProfiles(); } RunnerManagerClient::~RunnerManagerClient() @@ -213,6 +263,11 @@ QVariantList RunnerManagerClient::runners() const return m_runners; } +QVariantList RunnerManagerClient::gameProfiles() const +{ + return m_gameProfiles; +} + static QString normalizeHex(QString s) { s = s.trimmed().toLower(); @@ -417,6 +472,11 @@ void RunnerManagerClient::onInstallFinished(const QString &installId, const QVar refreshRunners(); } +void RunnerManagerClient::onGameProfilesChanged() +{ + refreshGameProfiles(); +} + void RunnerManagerClient::refreshRunners() { ensureRunnerDaemon(); @@ -472,6 +532,179 @@ void RunnerManagerClient::refreshRunners() }); } +void RunnerManagerClient::refreshGameProfiles() +{ + ensureRunnerDaemon(); + + QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus()); + QDBusPendingCall call = iface.asyncCall(QStringLiteral("ListGameProfiles")); + auto *watcher = new QDBusPendingCallWatcher(call, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) { + QDBusPendingReply reply = *w; + w->deleteLater(); + if (reply.isError()) { + if (!m_refreshProfilesRetryPending && (reply.error().type() == QDBusError::ServiceUnknown || reply.error().type() == QDBusError::NoReply)) { + m_refreshProfilesRetryPending = true; + ensureRunnerDaemon(); + QTimer::singleShot(500, this, [this]() { + m_refreshProfilesRetryPending = false; + refreshGameProfiles(); + }); + } + return; + } + + const QVariantList raw = reply.value(); + + QVariantList list; + list.reserve(raw.size()); + for (const QVariant &v : raw) { + QVariantMap map; + if (v.metaType() == QMetaType::fromType()) { + map = v.toMap(); + } else if (v.metaType() == QMetaType::fromType()) { + const QDBusArgument arg = v.value(); + map = qdbus_cast(arg); + } else if (v.canConvert()) { + map = v.toMap(); + } + list.push_back(normalizeMap(map)); + } + + if (m_gameProfiles == list) { + return; + } + m_gameProfiles = list; + Q_EMIT gameProfilesChanged(); + }); +} + +void RunnerManagerClient::requestGameProfile(const QString &gameId) +{ + const QString id = gameId.trimmed(); + if (id.isEmpty()) { + return; + } + + ensureRunnerDaemon(); + + auto attempts = QSharedPointer::create(0); + auto callPtr = QSharedPointer>::create(); + *callPtr = [this, id, attempts, callPtr]() { + QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus()); + QDBusPendingCall call = iface.asyncCall(QStringLiteral("GetGameProfile"), id); + auto *watcher = new QDBusPendingCallWatcher(call, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) { + QDBusPendingReply reply = *w; + w->deleteLater(); + if (reply.isError()) { + const QDBusError::ErrorType t = reply.error().type(); + if ((*attempts) < 1 && (t == QDBusError::ServiceUnknown || t == QDBusError::NoReply)) { + (*attempts)++; + ensureRunnerDaemon(); + QTimer::singleShot(500, this, [callPtr]() { + (*callPtr)(); + }); + return; + } + QVariantMap err; + err.insert(QStringLiteral("ok"), false); + err.insert(QStringLiteral("error"), reply.error().message()); + Q_EMIT gameProfileFetched(id, err); + return; + } + + QVariantMap result = normalizeResultWithProfile(reply.value()); + Q_EMIT gameProfileFetched(id, result); + }); + }; + (*callPtr)(); +} + +void RunnerManagerClient::setGameProfile(const QVariantMap &spec) +{ + const QString id = spec.value(QStringLiteral("gameId")).toString().trimmed(); + if (id.isEmpty()) { + return; + } + + ensureRunnerDaemon(); + + auto attempts = QSharedPointer::create(0); + auto callPtr = QSharedPointer>::create(); + *callPtr = [this, spec, id, attempts, callPtr]() { + QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus()); + QDBusPendingCall call = iface.asyncCall(QStringLiteral("SetGameProfile"), spec); + auto *watcher = new QDBusPendingCallWatcher(call, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) { + QDBusPendingReply reply = *w; + w->deleteLater(); + if (reply.isError()) { + const QDBusError::ErrorType t = reply.error().type(); + if ((*attempts) < 1 && (t == QDBusError::ServiceUnknown || t == QDBusError::NoReply)) { + (*attempts)++; + ensureRunnerDaemon(); + QTimer::singleShot(500, this, [callPtr]() { + (*callPtr)(); + }); + return; + } + QVariantMap err; + err.insert(QStringLiteral("ok"), false); + err.insert(QStringLiteral("error"), reply.error().message()); + Q_EMIT gameProfileSaved(id, err); + return; + } + + QVariantMap result = normalizeResultWithProfile(reply.value()); + Q_EMIT gameProfileSaved(id, result); + }); + }; + (*callPtr)(); +} + +void RunnerManagerClient::clearGameProfile(const QString &gameId) +{ + const QString id = gameId.trimmed(); + if (id.isEmpty()) { + return; + } + + ensureRunnerDaemon(); + + auto attempts = QSharedPointer::create(0); + auto callPtr = QSharedPointer>::create(); + *callPtr = [this, id, attempts, callPtr]() { + QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus()); + QDBusPendingCall call = iface.asyncCall(QStringLiteral("ClearGameProfile"), id); + auto *watcher = new QDBusPendingCallWatcher(call, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) { + QDBusPendingReply reply = *w; + w->deleteLater(); + if (reply.isError()) { + const QDBusError::ErrorType t = reply.error().type(); + if ((*attempts) < 1 && (t == QDBusError::ServiceUnknown || t == QDBusError::NoReply)) { + (*attempts)++; + ensureRunnerDaemon(); + QTimer::singleShot(500, this, [callPtr]() { + (*callPtr)(); + }); + return; + } + QVariantMap err; + err.insert(QStringLiteral("ok"), false); + err.insert(QStringLiteral("error"), reply.error().message()); + Q_EMIT gameProfileCleared(id, err); + return; + } + + QVariantMap result = normalizeMap(reply.value()); + Q_EMIT gameProfileCleared(id, result); + }); + }; + (*callPtr)(); +} + void RunnerManagerClient::uninstallRunner(const QString &runnerId) { const QString id = runnerId.trimmed(); diff --git a/src/runnermanagerclient.h b/src/runnermanagerclient.h index f78814b..17c5bc8 100644 --- a/src/runnermanagerclient.h +++ b/src/runnermanagerclient.h @@ -17,6 +17,7 @@ class RunnerManagerClient : public QObject Q_PROPERTY(QString status READ status NOTIFY statusChanged) Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged) Q_PROPERTY(QVariantList runners READ runners NOTIFY runnersChanged) + Q_PROPERTY(QVariantList gameProfiles READ gameProfiles NOTIFY gameProfilesChanged) public: explicit RunnerManagerClient(QObject *parent = nullptr); @@ -29,6 +30,7 @@ public: QString status() const; QString lastError() const; QVariantList runners() const; + QVariantList gameProfiles() const; Q_INVOKABLE void installRunnerFromUrl(const QString &url, const QString &sha256 = QString(), @@ -41,6 +43,11 @@ public: Q_INVOKABLE void refreshRunners(); Q_INVOKABLE void uninstallRunner(const QString &runnerId); + Q_INVOKABLE void refreshGameProfiles(); + Q_INVOKABLE void requestGameProfile(const QString &gameId); + Q_INVOKABLE void setGameProfile(const QVariantMap &spec); + Q_INVOKABLE void clearGameProfile(const QString &gameId); + Q_INVOKABLE void ensurePrefix(const QString &gameId, const QString &runner, const QString &prefixPath = QString()); Q_INVOKABLE void deletePrefix(const QString &gameId, const QString &prefixPath = QString()); @@ -52,14 +59,21 @@ Q_SIGNALS: void statusChanged(); void lastErrorChanged(); void runnersChanged(); + void gameProfilesChanged(); void prefixEnsured(const QString &gameId, const QString &prefixPath); void prefixDeleted(const QString &gameId, const QString &prefixPath); + void gameProfileFetched(const QString &gameId, const QVariantMap &result); + void gameProfileSaved(const QString &gameId, const QVariantMap &result); + void gameProfileCleared(const QString &gameId, const QVariantMap &result); + private Q_SLOTS: void onInstallStarted(const QString &installId, const QVariantMap &spec); void onInstallProgress(const QString &installId, qlonglong received, qlonglong total); void onInstallFinished(const QString &installId, const QVariantMap &result); + void onGameProfilesChanged(); + private: void ensureRunnerDaemon(); void shutdownSpawnedRunnerDaemon(); @@ -77,8 +91,10 @@ private: QString m_lastError; QVariantList m_runners; + QVariantList m_gameProfiles; bool m_refreshRetryPending = false; + bool m_refreshProfilesRetryPending = false; class QProcess *m_runnerdProcess = nullptr; bool m_runnerdSpawnAttempted = false;