mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-02-09 21:13:08 +00:00
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:
parent
017073c162
commit
fa9c86415f
2 changed files with 336 additions and 244 deletions
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue