Launcher: use Game Center daemon

Route game launching and running-state tracking through the\nGame Center DBus daemon. This removes the in-process QProcess-based\ntracking in favor of daemon sessions and adds availability checks.
This commit is contained in:
Marco Allegretti 2026-02-06 14:02:24 +01:00
parent 017073c162
commit fa9c86415f
2 changed files with 336 additions and 244 deletions

View file

@ -5,11 +5,16 @@
#include "app.h" #include "app.h"
#include <QCoreApplication> #include <QCoreApplication>
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusReply>
#include <QDBusVariant>
#include <QDateTime> #include <QDateTime>
#include <QDesktopServices> #include <QDesktopServices>
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
#include <QProcessEnvironment> #include <QProcess>
#include <QStandardPaths> #include <QStandardPaths>
#include <QTimer> #include <QTimer>
#include <QUrl> #include <QUrl>
@ -26,6 +31,23 @@ static QStringList steamCandidateRoots()
}; };
} }
static QVariant unwrapDbusVariant(QVariant v)
{
if (v.canConvert<QDBusVariant>()) {
v = v.value<QDBusVariant>().variant();
}
return v;
}
static QVariantMap unwrapVariantMap(const QVariantMap &map)
{
QVariantMap out;
for (auto it = map.constBegin(); it != map.constEnd(); ++it) {
out.insert(it.key(), unwrapDbusVariant(it.value()));
}
return out;
}
static QString findSteamClientInstallPathFromProton(const QString &protonExe) static QString findSteamClientInstallPathFromProton(const QString &protonExe)
{ {
if (protonExe.isEmpty()) { if (protonExe.isEmpty()) {
@ -111,25 +133,60 @@ static QString discoverDefaultProtonExecutable()
GameLauncher::GameLauncher(QObject *parent) GameLauncher::GameLauncher(QObject *parent)
: QObject(parent) : QObject(parent)
{ {
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]() {
applyRunningStateToLibrary();
});
});
}
QTimer::singleShot(0, this, [this]() {
checkDaemonAvailability();
syncDaemonSessions();
});
} }
GameLauncher::~GameLauncher() GameLauncher::~GameLauncher()
{ {
// Cleanup all running processes m_daemonGameToSession.clear();
for (QProcess *process : m_runningGames.values()) { m_daemonSessionToGame.clear();
process->disconnect();
process->terminate();
process->waitForFinished(3000);
process->deleteLater();
}
m_runningGames.clear();
m_processToGame.clear();
} }
bool GameLauncher::hasRunningGames() const bool GameLauncher::hasRunningGames() const
{ {
return !m_runningGames.isEmpty(); return !m_daemonGameToSession.isEmpty();
} }
QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
@ -144,7 +201,18 @@ QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
info.insert(QStringLiteral("gameId"), game->id()); info.insert(QStringLiteral("gameId"), game->id());
info.insert(QStringLiteral("gameName"), game->name()); info.insert(QStringLiteral("gameName"), game->name());
info.insert(QStringLiteral("isRunning"), m_runningGames.contains(game->id())); info.insert(QStringLiteral("displayName"), game->name());
info.insert(QStringLiteral("isRunning"), m_daemonGameToSession.contains(game->id()));
// Detect provider from platform
const QString platform = game->platform().toLower();
if (platform == QLatin1String("steam")) {
info.insert(QStringLiteral("provider"), QStringLiteral("steam"));
} else if (platform == QLatin1String("lutris")) {
info.insert(QStringLiteral("provider"), QStringLiteral("lutris"));
} else {
info.insert(QStringLiteral("provider"), QStringLiteral("manual"));
}
const QString command = game->launchCommand(); const QString command = game->launchCommand();
info.insert(QStringLiteral("launchCommand"), command); info.insert(QStringLiteral("launchCommand"), command);
@ -179,7 +247,7 @@ QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
info.insert(QStringLiteral("ok"), true); info.insert(QStringLiteral("ok"), true);
info.insert(QStringLiteral("launchType"), QStringLiteral("url")); info.insert(QStringLiteral("launchType"), QStringLiteral("url"));
info.insert(QStringLiteral("url"), command); info.insert(QStringLiteral("url"), command);
info.insert(QStringLiteral("usesDesktopServices"), true); info.insert(QStringLiteral("provider"), QStringLiteral("steam"));
return info; return info;
} }
@ -187,7 +255,7 @@ QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
info.insert(QStringLiteral("ok"), true); info.insert(QStringLiteral("ok"), true);
info.insert(QStringLiteral("launchType"), QStringLiteral("url")); info.insert(QStringLiteral("launchType"), QStringLiteral("url"));
info.insert(QStringLiteral("url"), command.mid(7)); info.insert(QStringLiteral("url"), command.mid(7));
info.insert(QStringLiteral("usesDesktopServices"), true); info.insert(QStringLiteral("provider"), QStringLiteral("lutris"));
return info; return info;
} }
@ -286,281 +354,297 @@ QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
} }
void GameLauncher::launchGame(Game *game) void GameLauncher::launchGame(Game *game)
{ {
if (!game) { if (!game) {
return; return;
} }
QString command = game->launchCommand();
if (command.isEmpty()) {
Q_EMIT gameError(game, tr("No launch command configured"));
return;
}
const QString runner = game->launchRunner().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();
// Check if already running // Check if already running
if (m_runningGames.contains(game->id())) { if (m_daemonGameToSession.contains(game->id())) {
Q_EMIT gameError(game, tr("Game is already running")); Q_EMIT gameError(game, tr("Game is already running"));
return; return;
} }
// Handle Steam URLs const QVariantMap info = resolveLaunchInfo(game);
if (!hasLaunchOverrides && command.startsWith(QLatin1String("steam://"))) { if (!info.value(QStringLiteral("ok")).toBool()) {
QDesktopServices::openUrl(QUrl(command)); Q_EMIT gameError(game, info.value(QStringLiteral("error")).toString());
game->setLastPlayed(QDateTime::currentDateTime());
Q_EMIT gameStarted(game);
return; return;
} }
// Handle Lutris URLs const QString launchCommand = info.value(QStringLiteral("launchCommand")).toString();
if (!hasLaunchOverrides && command.startsWith(QLatin1String("lutris "))) { const QString launchType = info.value(QStringLiteral("launchType")).toString();
QString lutrisCommand = command.mid(7); // Remove "lutris " const QString provider = info.value(QStringLiteral("provider")).toString();
QDesktopServices::openUrl(QUrl(lutrisCommand));
game->setLastPlayed(QDateTime::currentDateTime()); if (info.value(QStringLiteral("runner")).toString() == QLatin1String("proton")) {
Q_EMIT gameStarted(game); const QString resolvedPrefixPath = info.value(QStringLiteral("resolvedPrefixPath")).toString();
return; if (!resolvedPrefixPath.isEmpty()) {
QDir().mkpath(resolvedPrefixPath);
}
} }
// Start process for other commands const QString finalProgram = info.value(QStringLiteral("finalProgram")).toString();
QProcess *process = new QProcess(this); const QStringList finalArgs = info.value(QStringLiteral("finalArgs")).toStringList();
const QString workingDirectory = info.value(QStringLiteral("workingDirectory")).toString();
const QVariantMap effectiveEnv = info.value(QStringLiteral("effectiveEnv")).toMap();
// Apply per-game environment variables // Always try daemon first — for all launch types
QProcessEnvironment env; {
bool envChanged = false; QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"),
if (!launchEnv.isEmpty()) { QStringLiteral("/org/kde/GameCenter1"),
env = QProcessEnvironment::systemEnvironment(); QStringLiteral("org.kde.GameCenter1"),
envChanged = true; QDBusConnection::sessionBus());
for (auto it = launchEnv.constBegin(); it != launchEnv.constEnd(); ++it) { QVariantMap launchSpec = {
const QString key = it.key(); {QStringLiteral("command"), launchCommand},
if (key.isEmpty() || key.contains(QLatin1Char('='))) { {QStringLiteral("gameId"), game->id()},
continue; {QStringLiteral("displayName"), game->name()},
{QStringLiteral("provider"), provider},
{QStringLiteral("origin"), QStringLiteral("ui")},
};
if (!finalProgram.isEmpty()) {
launchSpec.insert(QStringLiteral("program"), finalProgram);
launchSpec.insert(QStringLiteral("args"), finalArgs);
}
if (!effectiveEnv.isEmpty()) {
launchSpec.insert(QStringLiteral("envOverrides"), effectiveEnv);
}
if (!workingDirectory.isEmpty()) {
launchSpec.insert(QStringLiteral("workingDirectory"), workingDirectory);
}
const QDBusReply<QString> reply = iface.call(QStringLiteral("Launch"), launchSpec);
if (reply.isValid() && !reply.value().isEmpty()) {
m_daemonGameToSession.insert(game->id(), reply.value());
m_daemonSessionToGame.insert(reply.value(), game->id());
game->setRunning(true);
game->setLastPlayed(QDateTime::currentDateTime());
Q_EMIT gameStarted(game);
Q_EMIT runningGamesChanged();
if (App::instance()->config()->exitAfterLaunch()) {
QTimer::singleShot(500, qApp, &QCoreApplication::quit);
} }
env.insert(key, it.value().toString());
}
}
// Set working directory if available
if (!game->workingDirectory().isEmpty()) {
process->setWorkingDirectory(game->workingDirectory());
}
connect(process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &GameLauncher::onProcessFinished);
connect(process, &QProcess::errorOccurred, this, &GameLauncher::onProcessError);
m_runningGames.insert(game->id(), process);
m_processToGame.insert(process, game);
// Parse command and arguments
QStringList parts;
if (command.startsWith(QLatin1String("steam://"))) {
parts = {QStringLiteral("xdg-open"), command};
} else if (command.startsWith(QLatin1String("lutris "))) {
parts = {QStringLiteral("xdg-open"), command.mid(7)};
} else {
parts = QProcess::splitCommand(command);
}
if (parts.isEmpty()) {
cleanupProcess(process);
Q_EMIT gameError(game, tr("Invalid launch command"));
return;
}
QString program = parts.takeFirst();
QString finalProgram = program;
QStringList finalArgs = parts;
if ((runner == QLatin1String("wine") || runner == QLatin1String("proton")) && (program == QLatin1String("xdg-open"))) {
cleanupProcess(process);
Q_EMIT gameError(game, tr("This runner cannot be used with URL-based launch commands"));
return;
}
if (runner == QLatin1String("custom")) {
if (runnerPath.isEmpty()) {
cleanupProcess(process);
Q_EMIT gameError(game, tr("Custom runner is enabled but no runner path is configured"));
return;
}
finalProgram = runnerPath;
finalArgs = {program};
finalArgs.append(parts);
} else if (runner == QLatin1String("wine")) {
QString wineExe = runnerPath;
if (wineExe.isEmpty()) {
wineExe = QStandardPaths::findExecutable(QStringLiteral("wine"));
}
if (wineExe.isEmpty()) {
cleanupProcess(process);
Q_EMIT gameError(game, tr("Wine runner is enabled but Wine was not found"));
return; return;
} }
QString winePrefix = prefixPath; // Daemon call failed — fallback for URL-type commands only
if (winePrefix.isEmpty()) { if (launchType == QLatin1String("url")) {
winePrefix = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/prefixes/") + game->id(); const QString url = info.value(QStringLiteral("url")).toString();
} if (!url.isEmpty()) {
QDir().mkpath(winePrefix); QDesktopServices::openUrl(QUrl(url));
game->setLastPlayed(QDateTime::currentDateTime());
if (!envChanged) { Q_EMIT gameStarted(game);
env = QProcessEnvironment::systemEnvironment(); return;
envChanged = true; }
}
env.insert(QStringLiteral("WINEPREFIX"), winePrefix);
finalProgram = wineExe;
finalArgs = {program};
finalArgs.append(parts);
} else if (runner == QLatin1String("proton")) {
QString protonExe = runnerPath;
if (protonExe.isEmpty()) {
protonExe = discoverDefaultProtonExecutable();
}
if (protonExe.isEmpty()) {
cleanupProcess(process);
Q_EMIT gameError(game, tr("Proton runner is enabled but no Proton installation was found"));
return;
} }
QString steamInstallPath = findSteamClientInstallPathFromProton(protonExe); // No fallback for non-URL commands — emit error
Q_EMIT gameError(game, tr("Game Center daemon is not available"));
QString compatDataPath = prefixPath;
if (compatDataPath.isEmpty()) {
compatDataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/prefixes/") + game->id();
}
QDir().mkpath(compatDataPath);
if (!envChanged) {
env = QProcessEnvironment::systemEnvironment();
envChanged = true;
}
env.insert(QStringLiteral("STEAM_COMPAT_DATA_PATH"), compatDataPath);
if (!steamInstallPath.isEmpty()) {
env.insert(QStringLiteral("STEAM_COMPAT_CLIENT_INSTALL_PATH"), steamInstallPath);
}
finalProgram = protonExe;
finalArgs = {QStringLiteral("run"), program};
finalArgs.append(parts);
}
if (envChanged) {
process->setProcessEnvironment(env);
}
process->start(finalProgram, finalArgs);
if (!process->waitForStarted(5000)) {
QString error = process->errorString();
cleanupProcess(process);
Q_EMIT gameError(game, tr("Failed to start game: %1").arg(error));
return;
}
game->setRunning(true);
game->setLastPlayed(QDateTime::currentDateTime());
Q_EMIT gameStarted(game);
Q_EMIT runningGamesChanged();
// Exit after launch if configured
if (App::instance()->config()->exitAfterLaunch()) {
QTimer::singleShot(500, qApp, &QCoreApplication::quit);
} }
} }
void GameLauncher::stopGame(Game *game) void GameLauncher::stopGame(Game *game)
{ {
if (!game || !m_runningGames.contains(game->id())) { if (!game) {
return; return;
} }
QProcess *process = m_runningGames.value(game->id()); QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"),
process->terminate(); QStringLiteral("/org/kde/GameCenter1"),
QStringLiteral("org.kde.GameCenter1"),
if (!process->waitForFinished(5000)) { QDBusConnection::sessionBus());
process->kill(); iface.call(QStringLiteral("StopByGameId"), game->id());
}
} }
bool GameLauncher::isGameRunning(Game *game) const bool GameLauncher::isGameRunning(Game *game) const
{ {
return game && m_runningGames.contains(game->id()); return game && m_daemonGameToSession.contains(game->id());
} }
void GameLauncher::onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) void GameLauncher::onDaemonSessionAdded(const QVariantMap &session)
{ {
Q_UNUSED(exitStatus) const QVariantMap s = unwrapVariantMap(session);
const QString sessionId = s.value(QStringLiteral("sessionId")).toString();
QProcess *process = qobject_cast<QProcess *>(sender()); const QString gameId = s.value(QStringLiteral("gameId")).toString();
if (!process) { if (sessionId.isEmpty() || gameId.isEmpty()) {
return; return;
} }
Game *game = m_processToGame.value(process); if (m_daemonSessionToGame.contains(sessionId)) {
return;
}
m_daemonGameToSession.insert(gameId, sessionId);
m_daemonSessionToGame.insert(sessionId, gameId);
Game *game = App::instance()->gameModel()->gameById(gameId);
if (game) {
game->setRunning(true);
}
Q_EMIT runningGamesChanged();
applyRunningStateToLibrary();
}
void GameLauncher::onDaemonSessionChanged(const QVariantMap &session)
{
const QVariantMap s = unwrapVariantMap(session);
const QString sessionId = s.value(QStringLiteral("sessionId")).toString();
const QString gameId = s.value(QStringLiteral("gameId")).toString();
if (sessionId.isEmpty() || gameId.isEmpty()) {
return;
}
m_daemonGameToSession.insert(gameId, sessionId);
m_daemonSessionToGame.insert(sessionId, gameId);
const QString state = s.value(QStringLiteral("state")).toString();
if (state == QLatin1String("Exited")) {
return;
}
Game *game = App::instance()->gameModel()->gameById(gameId);
if (game) {
game->setRunning(true);
}
Q_EMIT runningGamesChanged();
applyRunningStateToLibrary();
}
void GameLauncher::onDaemonSessionRemoved(const QString &sessionId, const QVariantMap &finalState)
{
if (sessionId.isEmpty()) {
return;
}
QString gameId = m_daemonSessionToGame.take(sessionId);
if (!gameId.isEmpty()) {
m_daemonGameToSession.remove(gameId);
} else {
const QVariantMap fs = unwrapVariantMap(finalState);
gameId = fs.value(QStringLiteral("gameId")).toString();
if (!gameId.isEmpty()) {
m_daemonGameToSession.remove(gameId);
}
}
Game *game = !gameId.isEmpty() ? App::instance()->gameModel()->gameById(gameId) : nullptr;
if (game) { if (game) {
game->setRunning(false); game->setRunning(false);
Q_EMIT gameStopped(game, exitCode); const QVariantMap fs = unwrapVariantMap(finalState);
Q_EMIT gameStopped(game, fs.value(QStringLiteral("exitCode")).toInt());
} }
cleanupProcess(process);
Q_EMIT runningGamesChanged(); Q_EMIT runningGamesChanged();
applyRunningStateToLibrary();
} }
void GameLauncher::onProcessError(QProcess::ProcessError error) void GameLauncher::onDaemonLaunchFailed(const QVariantMap &error)
{ {
QProcess *process = qobject_cast<QProcess *>(sender()); const QVariantMap e = unwrapVariantMap(error);
if (!process) { QVariantMap context;
{
QVariant ctx = unwrapDbusVariant(e.value(QStringLiteral("context")));
if (ctx.canConvert<QVariantMap>()) {
context = ctx.toMap();
} else if (ctx.canConvert<QDBusArgument>()) {
context = qdbus_cast<QVariantMap>(ctx.value<QDBusArgument>());
}
}
QString gameId = unwrapDbusVariant(context.value(QStringLiteral("gameId"))).toString();
if (gameId.isEmpty()) {
gameId = e.value(QStringLiteral("gameId")).toString();
}
if (gameId.isEmpty()) {
return; return;
} }
Game *game = m_processToGame.value(process); const QString message = e.value(QStringLiteral("error")).toString();
if (message.isEmpty()) {
QString errorMessage;
switch (error) {
case QProcess::FailedToStart:
errorMessage = tr("Failed to start the game process");
break;
case QProcess::Crashed:
errorMessage = tr("Game process crashed");
break;
case QProcess::Timedout:
errorMessage = tr("Game process timed out");
break;
case QProcess::WriteError:
errorMessage = tr("Failed to write to game process");
break;
case QProcess::ReadError:
errorMessage = tr("Failed to read from game process");
break;
default:
errorMessage = tr("Unknown error occurred");
break;
}
if (game) {
game->setRunning(false);
Q_EMIT gameError(game, errorMessage);
}
cleanupProcess(process);
Q_EMIT runningGamesChanged();
}
void GameLauncher::cleanupProcess(QProcess *process)
{
if (!process) {
return; return;
} }
Game *game = m_processToGame.take(process); QTimer::singleShot(100, this, [this, gameId, message]() {
if (game) { if (m_daemonGameToSession.contains(gameId)) {
m_runningGames.remove(game->id()); return;
}
Game *game = App::instance()->gameModel()->gameById(gameId);
if (!game) {
return;
}
Q_EMIT gameError(game, message);
});
}
void GameLauncher::syncDaemonSessions()
{
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("/org/kde/GameCenter1"),
QStringLiteral("org.kde.GameCenter1"),
QDBusConnection::sessionBus());
const QDBusReply<QVariantList> reply = iface.call(QStringLiteral("ListSessions"));
if (!reply.isValid()) {
return;
} }
process->deleteLater(); const QVariantList list = reply.value();
for (const QVariant &v : list) {
QVariantMap map;
if (v.canConvert<QVariantMap>()) {
map = v.toMap();
} else if (v.canConvert<QDBusArgument>()) {
map = qdbus_cast<QVariantMap>(v.value<QDBusArgument>());
}
if (!map.isEmpty()) {
onDaemonSessionAdded(map);
}
}
applyRunningStateToLibrary();
}
void GameLauncher::applyRunningStateToLibrary()
{
const QList<Game *> games = App::instance()->gameModel()->allGames();
for (Game *game : games) {
if (!game) {
continue;
}
const bool shouldBeRunning = m_daemonGameToSession.contains(game->id());
game->setRunning(shouldBeRunning);
}
}
void GameLauncher::checkDaemonAvailability()
{
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"),
QStringLiteral("/org/kde/GameCenter1"),
QStringLiteral("org.kde.GameCenter1"),
QDBusConnection::sessionBus());
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();
}
}
bool GameLauncher::daemonAvailable() const
{
return m_daemonAvailable;
}
void GameLauncher::retryDaemonConnection()
{
checkDaemonAvailability();
if (m_daemonAvailable) {
syncDaemonSessions();
}
} }

View file

@ -3,9 +3,8 @@
#pragma once #pragma once
#include <QMap> #include <QHash>
#include <QObject> #include <QObject>
#include <QProcess>
#include <QQmlEngine> #include <QQmlEngine>
#include <QVariantMap> #include <QVariantMap>
@ -17,31 +16,40 @@ class GameLauncher : public QObject
QML_ELEMENT QML_ELEMENT
Q_PROPERTY(bool hasRunningGames READ hasRunningGames NOTIFY runningGamesChanged) Q_PROPERTY(bool hasRunningGames READ hasRunningGames NOTIFY runningGamesChanged)
Q_PROPERTY(bool daemonAvailable READ daemonAvailable NOTIFY daemonAvailableChanged)
public: public:
explicit GameLauncher(QObject *parent = nullptr); explicit GameLauncher(QObject *parent = nullptr);
~GameLauncher(); ~GameLauncher();
bool hasRunningGames() const; bool hasRunningGames() const;
bool daemonAvailable() const;
Q_INVOKABLE void launchGame(Game *game); Q_INVOKABLE void launchGame(Game *game);
Q_INVOKABLE QVariantMap resolveLaunchInfo(Game *game) const; Q_INVOKABLE QVariantMap resolveLaunchInfo(Game *game) const;
Q_INVOKABLE void stopGame(Game *game); Q_INVOKABLE void stopGame(Game *game);
Q_INVOKABLE bool isGameRunning(Game *game) const; Q_INVOKABLE bool isGameRunning(Game *game) const;
Q_INVOKABLE void retryDaemonConnection();
Q_SIGNALS: Q_SIGNALS:
void gameStarted(Game *game); void gameStarted(Game *game);
void gameStopped(Game *game, int exitCode); void gameStopped(Game *game, int exitCode);
void gameError(Game *game, const QString &error); void gameError(Game *game, const QString &error);
void runningGamesChanged(); void runningGamesChanged();
void daemonAvailableChanged();
private Q_SLOTS: private Q_SLOTS:
void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); void onDaemonSessionAdded(const QVariantMap &session);
void onProcessError(QProcess::ProcessError error); void onDaemonSessionChanged(const QVariantMap &session);
void onDaemonSessionRemoved(const QString &sessionId, const QVariantMap &finalState);
void onDaemonLaunchFailed(const QVariantMap &error);
private: private:
QMap<QString, QProcess *> m_runningGames; QHash<QString, QString> m_daemonGameToSession;
QMap<QProcess *, Game *> m_processToGame; QHash<QString, QString> m_daemonSessionToGame;
bool m_daemonAvailable = false;
void cleanupProcess(QProcess *process); void checkDaemonAvailability();
void syncDaemonSessions();
void applyRunningStateToLibrary();
}; };