mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-02-09 21:13:08 +00:00
Core: per-game compatibility and env overrides
This commit is contained in:
parent
49997ed0cb
commit
99406a1989
4 changed files with 637 additions and 10 deletions
149
src/game.cpp
149
src/game.cpp
|
|
@ -4,6 +4,8 @@
|
||||||
#include "game.h"
|
#include "game.h"
|
||||||
|
|
||||||
#include <KLocalizedString>
|
#include <KLocalizedString>
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
#include <QJsonValue>
|
||||||
#include <QLocale>
|
#include <QLocale>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
|
||||||
|
|
@ -131,6 +133,58 @@ void Game::setWorkingDirectory(const QString &dir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QVariantMap Game::launchEnv() const
|
||||||
|
{
|
||||||
|
return m_launchEnv;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::setLaunchEnv(const QVariantMap &env)
|
||||||
|
{
|
||||||
|
if (m_launchEnv != env) {
|
||||||
|
m_launchEnv = env;
|
||||||
|
Q_EMIT launchEnvChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Game::launchRunner() const
|
||||||
|
{
|
||||||
|
return m_launchRunner;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::setLaunchRunner(const QString &runner)
|
||||||
|
{
|
||||||
|
if (m_launchRunner != runner) {
|
||||||
|
m_launchRunner = runner;
|
||||||
|
Q_EMIT launchRunnerChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Game::launchRunnerPath() const
|
||||||
|
{
|
||||||
|
return m_launchRunnerPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::setLaunchRunnerPath(const QString &path)
|
||||||
|
{
|
||||||
|
if (m_launchRunnerPath != path) {
|
||||||
|
m_launchRunnerPath = path;
|
||||||
|
Q_EMIT launchRunnerPathChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Game::launchPrefixPath() const
|
||||||
|
{
|
||||||
|
return m_launchPrefixPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::setLaunchPrefixPath(const QString &path)
|
||||||
|
{
|
||||||
|
if (m_launchPrefixPath != path) {
|
||||||
|
m_launchPrefixPath = path;
|
||||||
|
Q_EMIT launchPrefixPathChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QString Game::platform() const
|
QString Game::platform() const
|
||||||
{
|
{
|
||||||
return m_platform;
|
return m_platform;
|
||||||
|
|
@ -268,6 +322,32 @@ QJsonObject Game::toJson() const
|
||||||
obj[QStringLiteral("favorite")] = m_favorite;
|
obj[QStringLiteral("favorite")] = m_favorite;
|
||||||
obj[QStringLiteral("hidden")] = m_hidden;
|
obj[QStringLiteral("hidden")] = m_hidden;
|
||||||
obj[QStringLiteral("installed")] = m_installed;
|
obj[QStringLiteral("installed")] = m_installed;
|
||||||
|
|
||||||
|
const bool hasLaunchConfig = !m_launchEnv.isEmpty() || !m_launchRunner.isEmpty() || !m_launchRunnerPath.isEmpty() || !m_launchPrefixPath.isEmpty();
|
||||||
|
if (hasLaunchConfig) {
|
||||||
|
QJsonObject launchObj;
|
||||||
|
|
||||||
|
if (!m_launchEnv.isEmpty()) {
|
||||||
|
QJsonObject envObj;
|
||||||
|
for (auto it = m_launchEnv.constBegin(); it != m_launchEnv.constEnd(); ++it) {
|
||||||
|
envObj.insert(it.key(), QJsonValue::fromVariant(it.value()));
|
||||||
|
}
|
||||||
|
launchObj.insert(QStringLiteral("env"), envObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_launchRunner.isEmpty()) {
|
||||||
|
launchObj.insert(QStringLiteral("runner"), m_launchRunner);
|
||||||
|
}
|
||||||
|
if (!m_launchRunnerPath.isEmpty()) {
|
||||||
|
launchObj.insert(QStringLiteral("runnerPath"), m_launchRunnerPath);
|
||||||
|
}
|
||||||
|
if (!m_launchPrefixPath.isEmpty()) {
|
||||||
|
launchObj.insert(QStringLiteral("prefixPath"), m_launchPrefixPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.insert(QStringLiteral("launch"), launchObj);
|
||||||
|
}
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,26 +356,85 @@ Game *Game::fromJson(const QJsonObject &json, QObject *parent)
|
||||||
QString id = json[QStringLiteral("id")].toString();
|
QString id = json[QStringLiteral("id")].toString();
|
||||||
QString name = json[QStringLiteral("name")].toString();
|
QString name = json[QStringLiteral("name")].toString();
|
||||||
|
|
||||||
if (id.isEmpty() || name.isEmpty()) {
|
if (name.isEmpty()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QString platformId = json[QStringLiteral("platformId")].toString();
|
||||||
|
const QString platform = json[QStringLiteral("platform")].toString();
|
||||||
|
const QString launchCommand = json[QStringLiteral("launchCommand")].toString();
|
||||||
|
const QString workingDirectory = json[QStringLiteral("workingDirectory")].toString();
|
||||||
|
if (id.isEmpty()) {
|
||||||
|
const QByteArray fingerprint =
|
||||||
|
(platformId + QLatin1Char('|') + platform + QLatin1Char('|') + name + QLatin1Char('|') + launchCommand + QLatin1Char('|') + workingDirectory)
|
||||||
|
.toUtf8();
|
||||||
|
const QByteArray hash = QCryptographicHash::hash(fingerprint, QCryptographicHash::Sha1).toHex();
|
||||||
|
id = QStringLiteral("legacy-") + QString::fromLatin1(hash);
|
||||||
|
}
|
||||||
|
|
||||||
Game *game = new Game(id, name, parent);
|
Game *game = new Game(id, name, parent);
|
||||||
game->setDescription(json[QStringLiteral("description")].toString());
|
game->setDescription(json[QStringLiteral("description")].toString());
|
||||||
game->setDeveloper(json[QStringLiteral("developer")].toString());
|
game->setDeveloper(json[QStringLiteral("developer")].toString());
|
||||||
game->setPublisher(json[QStringLiteral("publisher")].toString());
|
game->setPublisher(json[QStringLiteral("publisher")].toString());
|
||||||
game->setCoverUrl(QUrl(json[QStringLiteral("coverUrl")].toString()));
|
game->setCoverUrl(QUrl(json[QStringLiteral("coverUrl")].toString()));
|
||||||
game->setIconUrl(QUrl(json[QStringLiteral("iconUrl")].toString()));
|
game->setIconUrl(QUrl(json[QStringLiteral("iconUrl")].toString()));
|
||||||
game->setLaunchCommand(json[QStringLiteral("launchCommand")].toString());
|
game->setLaunchCommand(launchCommand);
|
||||||
game->setWorkingDirectory(json[QStringLiteral("workingDirectory")].toString());
|
game->setWorkingDirectory(workingDirectory);
|
||||||
game->setPlatform(json[QStringLiteral("platform")].toString());
|
game->setPlatform(platform);
|
||||||
game->setPlatformId(json[QStringLiteral("platformId")].toString());
|
game->setPlatformId(platformId);
|
||||||
game->setDateAdded(QDateTime::fromString(json[QStringLiteral("dateAdded")].toString(), Qt::ISODate));
|
game->setDateAdded(QDateTime::fromString(json[QStringLiteral("dateAdded")].toString(), Qt::ISODate));
|
||||||
game->setLastPlayed(QDateTime::fromString(json[QStringLiteral("lastPlayed")].toString(), Qt::ISODate));
|
game->setLastPlayed(QDateTime::fromString(json[QStringLiteral("lastPlayed")].toString(), Qt::ISODate));
|
||||||
game->setPlayTime(json[QStringLiteral("playTime")].toInteger());
|
game->setPlayTime(json[QStringLiteral("playTime")].toInteger());
|
||||||
game->setFavorite(json[QStringLiteral("favorite")].toBool());
|
game->setFavorite(json[QStringLiteral("favorite")].toBool());
|
||||||
game->setHidden(json[QStringLiteral("hidden")].toBool());
|
game->setHidden(json[QStringLiteral("hidden")].toBool());
|
||||||
game->setInstalled(json[QStringLiteral("installed")].toBool(true));
|
game->setInstalled(json[QStringLiteral("installed")].toBool(true));
|
||||||
|
|
||||||
|
QVariantMap env;
|
||||||
|
QString runner;
|
||||||
|
QString runnerPath;
|
||||||
|
QString prefixPath;
|
||||||
|
const QJsonValue launchValue = json.value(QStringLiteral("launch"));
|
||||||
|
if (launchValue.isObject()) {
|
||||||
|
const QJsonObject launchObj = launchValue.toObject();
|
||||||
|
const QJsonValue envValue = launchObj.value(QStringLiteral("env"));
|
||||||
|
if (envValue.isObject()) {
|
||||||
|
env = envValue.toObject().toVariantMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
runner = launchObj.value(QStringLiteral("runner")).toString();
|
||||||
|
runnerPath = launchObj.value(QStringLiteral("runnerPath")).toString();
|
||||||
|
prefixPath = launchObj.value(QStringLiteral("prefixPath")).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonValue legacyEnvValue = json.value(QStringLiteral("launchEnv"));
|
||||||
|
if (env.isEmpty() && legacyEnvValue.isObject()) {
|
||||||
|
env = legacyEnvValue.toObject().toVariantMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runner.isEmpty()) {
|
||||||
|
runner = json.value(QStringLiteral("launchRunner")).toString();
|
||||||
|
}
|
||||||
|
if (runnerPath.isEmpty()) {
|
||||||
|
runnerPath = json.value(QStringLiteral("launchRunnerPath")).toString();
|
||||||
|
}
|
||||||
|
if (prefixPath.isEmpty()) {
|
||||||
|
prefixPath = json.value(QStringLiteral("launchPrefixPath")).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!env.isEmpty()) {
|
||||||
|
game->setLaunchEnv(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!runner.isEmpty()) {
|
||||||
|
game->setLaunchRunner(runner);
|
||||||
|
}
|
||||||
|
if (!runnerPath.isEmpty()) {
|
||||||
|
game->setLaunchRunnerPath(runnerPath);
|
||||||
|
}
|
||||||
|
if (!prefixPath.isEmpty()) {
|
||||||
|
game->setLaunchPrefixPath(prefixPath);
|
||||||
|
}
|
||||||
|
|
||||||
return game;
|
return game;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
25
src/game.h
25
src/game.h
|
|
@ -9,6 +9,7 @@
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
class Game : public QObject
|
class Game : public QObject
|
||||||
{
|
{
|
||||||
|
|
@ -25,6 +26,10 @@ class Game : public QObject
|
||||||
Q_PROPERTY(QUrl iconUrl READ iconUrl WRITE setIconUrl NOTIFY iconUrlChanged)
|
Q_PROPERTY(QUrl iconUrl READ iconUrl WRITE setIconUrl NOTIFY iconUrlChanged)
|
||||||
Q_PROPERTY(QString launchCommand READ launchCommand WRITE setLaunchCommand NOTIFY launchCommandChanged)
|
Q_PROPERTY(QString launchCommand READ launchCommand WRITE setLaunchCommand NOTIFY launchCommandChanged)
|
||||||
Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged)
|
Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged)
|
||||||
|
Q_PROPERTY(QVariantMap launchEnv READ launchEnv WRITE setLaunchEnv NOTIFY launchEnvChanged)
|
||||||
|
Q_PROPERTY(QString launchRunner READ launchRunner WRITE setLaunchRunner NOTIFY launchRunnerChanged)
|
||||||
|
Q_PROPERTY(QString launchRunnerPath READ launchRunnerPath WRITE setLaunchRunnerPath NOTIFY launchRunnerPathChanged)
|
||||||
|
Q_PROPERTY(QString launchPrefixPath READ launchPrefixPath WRITE setLaunchPrefixPath NOTIFY launchPrefixPathChanged)
|
||||||
Q_PROPERTY(QString platform READ platform WRITE setPlatform NOTIFY platformChanged)
|
Q_PROPERTY(QString platform READ platform WRITE setPlatform NOTIFY platformChanged)
|
||||||
Q_PROPERTY(QString platformId READ platformId WRITE setPlatformId NOTIFY platformIdChanged)
|
Q_PROPERTY(QString platformId READ platformId WRITE setPlatformId NOTIFY platformIdChanged)
|
||||||
Q_PROPERTY(QDateTime dateAdded READ dateAdded WRITE setDateAdded NOTIFY dateAddedChanged)
|
Q_PROPERTY(QDateTime dateAdded READ dateAdded WRITE setDateAdded NOTIFY dateAddedChanged)
|
||||||
|
|
@ -65,6 +70,18 @@ public:
|
||||||
QString workingDirectory() const;
|
QString workingDirectory() const;
|
||||||
void setWorkingDirectory(const QString &dir);
|
void setWorkingDirectory(const QString &dir);
|
||||||
|
|
||||||
|
QVariantMap launchEnv() const;
|
||||||
|
void setLaunchEnv(const QVariantMap &env);
|
||||||
|
|
||||||
|
QString launchRunner() const;
|
||||||
|
void setLaunchRunner(const QString &runner);
|
||||||
|
|
||||||
|
QString launchRunnerPath() const;
|
||||||
|
void setLaunchRunnerPath(const QString &path);
|
||||||
|
|
||||||
|
QString launchPrefixPath() const;
|
||||||
|
void setLaunchPrefixPath(const QString &path);
|
||||||
|
|
||||||
QString platform() const;
|
QString platform() const;
|
||||||
void setPlatform(const QString &platform);
|
void setPlatform(const QString &platform);
|
||||||
|
|
||||||
|
|
@ -108,6 +125,10 @@ Q_SIGNALS:
|
||||||
void iconUrlChanged();
|
void iconUrlChanged();
|
||||||
void launchCommandChanged();
|
void launchCommandChanged();
|
||||||
void workingDirectoryChanged();
|
void workingDirectoryChanged();
|
||||||
|
void launchEnvChanged();
|
||||||
|
void launchRunnerChanged();
|
||||||
|
void launchRunnerPathChanged();
|
||||||
|
void launchPrefixPathChanged();
|
||||||
void platformChanged();
|
void platformChanged();
|
||||||
void platformIdChanged();
|
void platformIdChanged();
|
||||||
void dateAddedChanged();
|
void dateAddedChanged();
|
||||||
|
|
@ -128,6 +149,10 @@ private:
|
||||||
QUrl m_iconUrl;
|
QUrl m_iconUrl;
|
||||||
QString m_launchCommand;
|
QString m_launchCommand;
|
||||||
QString m_workingDirectory;
|
QString m_workingDirectory;
|
||||||
|
QVariantMap m_launchEnv;
|
||||||
|
QString m_launchRunner;
|
||||||
|
QString m_launchRunnerPath;
|
||||||
|
QString m_launchPrefixPath;
|
||||||
QString m_platform;
|
QString m_platform;
|
||||||
QString m_platformId;
|
QString m_platformId;
|
||||||
QDateTime m_dateAdded;
|
QDateTime m_dateAdded;
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,107 @@
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QProcessEnvironment>
|
||||||
|
#include <QStandardPaths>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
|
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"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString findSteamClientInstallPathFromProton(const QString &protonExe)
|
||||||
|
{
|
||||||
|
if (protonExe.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QDir dir(QFileInfo(protonExe).absoluteDir());
|
||||||
|
for (int i = 0; i < 10; ++i) {
|
||||||
|
if (dir.exists(QStringLiteral("steamapps"))) {
|
||||||
|
return dir.absolutePath();
|
||||||
|
}
|
||||||
|
if (!dir.cdUp()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const QString &root : steamCandidateRoots()) {
|
||||||
|
if (QDir(root).exists(QStringLiteral("steamapps"))) {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString discoverDefaultProtonExecutable()
|
||||||
|
{
|
||||||
|
static bool done = false;
|
||||||
|
static QString cached;
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
done = true;
|
||||||
|
|
||||||
|
QStringList candidates;
|
||||||
|
for (const QString &root : steamCandidateRoots()) {
|
||||||
|
const QString compatTools = root + QStringLiteral("/compatibilitytools.d");
|
||||||
|
QDir compatDir(compatTools);
|
||||||
|
if (compatDir.exists()) {
|
||||||
|
const QStringList toolDirs = compatDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||||
|
for (const QString &tool : toolDirs) {
|
||||||
|
const QString protonPath = compatTools + QLatin1Char('/') + tool + QStringLiteral("/proton");
|
||||||
|
if (QFileInfo::exists(protonPath)) {
|
||||||
|
candidates.append(protonPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString common = root + QStringLiteral("/steamapps/common");
|
||||||
|
QDir commonDir(common);
|
||||||
|
if (commonDir.exists()) {
|
||||||
|
const QStringList dirs = commonDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||||
|
for (const QString &d : dirs) {
|
||||||
|
if (!d.contains(QStringLiteral("Proton"), Qt::CaseInsensitive)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QString protonPath = common + QLatin1Char('/') + d + QStringLiteral("/proton");
|
||||||
|
if (QFileInfo::exists(protonPath)) {
|
||||||
|
candidates.append(protonPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.removeDuplicates();
|
||||||
|
candidates.sort();
|
||||||
|
|
||||||
|
for (const QString &c : candidates) {
|
||||||
|
if (c.contains(QStringLiteral("Proton - Experimental"))) {
|
||||||
|
cached = c;
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!candidates.isEmpty()) {
|
||||||
|
cached = candidates.constLast();
|
||||||
|
}
|
||||||
|
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
GameLauncher::GameLauncher(QObject *parent)
|
GameLauncher::GameLauncher(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
{
|
{
|
||||||
|
|
@ -46,6 +144,12 @@ void GameLauncher::launchGame(Game *game)
|
||||||
return;
|
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_runningGames.contains(game->id())) {
|
||||||
Q_EMIT gameError(game, tr("Game is already running"));
|
Q_EMIT gameError(game, tr("Game is already running"));
|
||||||
|
|
@ -53,7 +157,7 @@ void GameLauncher::launchGame(Game *game)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Steam URLs
|
// Handle Steam URLs
|
||||||
if (command.startsWith(QLatin1String("steam://"))) {
|
if (!hasLaunchOverrides && command.startsWith(QLatin1String("steam://"))) {
|
||||||
QDesktopServices::openUrl(QUrl(command));
|
QDesktopServices::openUrl(QUrl(command));
|
||||||
game->setLastPlayed(QDateTime::currentDateTime());
|
game->setLastPlayed(QDateTime::currentDateTime());
|
||||||
Q_EMIT gameStarted(game);
|
Q_EMIT gameStarted(game);
|
||||||
|
|
@ -61,7 +165,7 @@ void GameLauncher::launchGame(Game *game)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Lutris URLs
|
// Handle Lutris URLs
|
||||||
if (command.startsWith(QLatin1String("lutris "))) {
|
if (!hasLaunchOverrides && command.startsWith(QLatin1String("lutris "))) {
|
||||||
QString lutrisCommand = command.mid(7); // Remove "lutris "
|
QString lutrisCommand = command.mid(7); // Remove "lutris "
|
||||||
QDesktopServices::openUrl(QUrl(lutrisCommand));
|
QDesktopServices::openUrl(QUrl(lutrisCommand));
|
||||||
game->setLastPlayed(QDateTime::currentDateTime());
|
game->setLastPlayed(QDateTime::currentDateTime());
|
||||||
|
|
@ -72,6 +176,21 @@ void GameLauncher::launchGame(Game *game)
|
||||||
// Start process for other commands
|
// Start process for other commands
|
||||||
QProcess *process = new QProcess(this);
|
QProcess *process = new QProcess(this);
|
||||||
|
|
||||||
|
// Apply per-game environment variables
|
||||||
|
QProcessEnvironment env;
|
||||||
|
bool envChanged = false;
|
||||||
|
if (!launchEnv.isEmpty()) {
|
||||||
|
env = QProcessEnvironment::systemEnvironment();
|
||||||
|
envChanged = true;
|
||||||
|
for (auto it = launchEnv.constBegin(); it != launchEnv.constEnd(); ++it) {
|
||||||
|
const QString key = it.key();
|
||||||
|
if (key.isEmpty() || key.contains(QLatin1Char('='))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
env.insert(key, it.value().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set working directory if available
|
// Set working directory if available
|
||||||
if (!game->workingDirectory().isEmpty()) {
|
if (!game->workingDirectory().isEmpty()) {
|
||||||
process->setWorkingDirectory(game->workingDirectory());
|
process->setWorkingDirectory(game->workingDirectory());
|
||||||
|
|
@ -84,7 +203,14 @@ void GameLauncher::launchGame(Game *game)
|
||||||
m_processToGame.insert(process, game);
|
m_processToGame.insert(process, game);
|
||||||
|
|
||||||
// Parse command and arguments
|
// Parse command and arguments
|
||||||
QStringList parts = QProcess::splitCommand(command);
|
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()) {
|
if (parts.isEmpty()) {
|
||||||
cleanupProcess(process);
|
cleanupProcess(process);
|
||||||
Q_EMIT gameError(game, tr("Invalid launch command"));
|
Q_EMIT gameError(game, tr("Invalid launch command"));
|
||||||
|
|
@ -92,7 +218,89 @@ void GameLauncher::launchGame(Game *game)
|
||||||
}
|
}
|
||||||
|
|
||||||
QString program = parts.takeFirst();
|
QString program = parts.takeFirst();
|
||||||
process->start(program, parts);
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString winePrefix = prefixPath;
|
||||||
|
if (winePrefix.isEmpty()) {
|
||||||
|
winePrefix = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/prefixes/") + game->id();
|
||||||
|
}
|
||||||
|
QDir().mkpath(winePrefix);
|
||||||
|
|
||||||
|
if (!envChanged) {
|
||||||
|
env = QProcessEnvironment::systemEnvironment();
|
||||||
|
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);
|
||||||
|
|
||||||
|
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)) {
|
if (!process->waitForStarted(5000)) {
|
||||||
QString error = process->errorString();
|
QString error = process->errorString();
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ Kirigami.Dialog {
|
||||||
property var game: null
|
property var game: null
|
||||||
property bool isEditing: game !== null
|
property bool isEditing: game !== null
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
id: envModel
|
||||||
|
}
|
||||||
|
|
||||||
signal gameCreated(var game)
|
signal gameCreated(var game)
|
||||||
signal gameUpdated(var game)
|
signal gameUpdated(var game)
|
||||||
|
|
||||||
|
|
@ -31,13 +35,24 @@ Kirigami.Dialog {
|
||||||
Kirigami.Action {
|
Kirigami.Action {
|
||||||
text: isEditing ? i18n("Apply") : i18n("Add")
|
text: isEditing ? i18n("Apply") : i18n("Add")
|
||||||
icon.name: isEditing ? "dialog-ok-apply" : "list-add"
|
icon.name: isEditing ? "dialog-ok-apply" : "list-add"
|
||||||
enabled: nameField.text.trim() !== "" && executableField.text.trim() !== ""
|
enabled: nameField.text.trim() !== "" && executableField.text.trim() !== "" && (runnerCombo.currentIndex !== 3 || runnerPathField.text.trim() !== "")
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
|
let runnerValue = dialog.runnerFromIndex(runnerCombo.currentIndex)
|
||||||
|
let runnerPathValue = runnerPathField.text.trim()
|
||||||
|
let prefixPathValue = prefixPathField.text.trim()
|
||||||
|
if (runnerValue === "") {
|
||||||
|
runnerPathValue = ""
|
||||||
|
prefixPathValue = ""
|
||||||
|
}
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
game.name = nameField.text.trim()
|
game.name = nameField.text.trim()
|
||||||
game.developer = developerField.text.trim()
|
game.developer = developerField.text.trim()
|
||||||
game.launchCommand = executableField.text.trim()
|
game.launchCommand = executableField.text.trim()
|
||||||
game.workingDirectory = workingDirField.text.trim()
|
game.workingDirectory = workingDirField.text.trim()
|
||||||
|
game.launchEnv = dialog.envModelToMap()
|
||||||
|
game.launchRunner = runnerValue
|
||||||
|
game.launchRunnerPath = runnerPathValue
|
||||||
|
game.launchPrefixPath = prefixPathValue
|
||||||
if (selectedCoverPath !== "") {
|
if (selectedCoverPath !== "") {
|
||||||
App.setCoverFromFile(game, selectedCoverPath)
|
App.setCoverFromFile(game, selectedCoverPath)
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +63,10 @@ Kirigami.Dialog {
|
||||||
if (newGame) {
|
if (newGame) {
|
||||||
newGame.developer = developerField.text.trim()
|
newGame.developer = developerField.text.trim()
|
||||||
newGame.workingDirectory = workingDirField.text.trim()
|
newGame.workingDirectory = workingDirField.text.trim()
|
||||||
|
newGame.launchEnv = dialog.envModelToMap()
|
||||||
|
newGame.launchRunner = runnerValue
|
||||||
|
newGame.launchRunnerPath = runnerPathValue
|
||||||
|
newGame.launchPrefixPath = prefixPathValue
|
||||||
if (selectedCoverPath !== "") {
|
if (selectedCoverPath !== "") {
|
||||||
App.setCoverFromFile(newGame, selectedCoverPath)
|
App.setCoverFromFile(newGame, selectedCoverPath)
|
||||||
}
|
}
|
||||||
|
|
@ -109,18 +128,75 @@ Kirigami.Dialog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function envModelToMap() {
|
||||||
|
let result = ({})
|
||||||
|
for (let i = 0; i < envModel.count; i++) {
|
||||||
|
let row = envModel.get(i)
|
||||||
|
let k = (row.key || "").trim()
|
||||||
|
if (k === "") continue
|
||||||
|
if (k.indexOf("=") !== -1) continue
|
||||||
|
result[k] = String(row.value || "")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEnvVar(key, value) {
|
||||||
|
let k = (key || "").trim()
|
||||||
|
if (k === "") return
|
||||||
|
for (let i = 0; i < envModel.count; i++) {
|
||||||
|
let row = envModel.get(i)
|
||||||
|
if ((row.key || "").trim() === k) {
|
||||||
|
envModel.setProperty(i, "value", String(value || ""))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
envModel.append({ key: k, value: String(value || "") })
|
||||||
|
}
|
||||||
|
|
||||||
|
function runnerFromIndex(idx) {
|
||||||
|
if (idx === 1) return "wine"
|
||||||
|
if (idx === 2) return "proton"
|
||||||
|
if (idx === 3) return "custom"
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function runnerToIndex(runner) {
|
||||||
|
let r = (runner || "").trim().toLowerCase()
|
||||||
|
if (r === "wine") return 1
|
||||||
|
if (r === "proton") return 2
|
||||||
|
if (r === "custom") return 3
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
function loadFields() {
|
function loadFields() {
|
||||||
selectedCoverPath = ""
|
selectedCoverPath = ""
|
||||||
|
envModel.clear()
|
||||||
if (isEditing && game) {
|
if (isEditing && game) {
|
||||||
nameField.text = game.name || ""
|
nameField.text = game.name || ""
|
||||||
developerField.text = game.developer || ""
|
developerField.text = game.developer || ""
|
||||||
executableField.text = game.launchCommand || ""
|
executableField.text = game.launchCommand || ""
|
||||||
workingDirField.text = game.workingDirectory || ""
|
workingDirField.text = game.workingDirectory || ""
|
||||||
|
|
||||||
|
runnerCombo.currentIndex = dialog.runnerToIndex(game.launchRunner)
|
||||||
|
runnerPathField.text = game.launchRunnerPath || ""
|
||||||
|
prefixPathField.text = game.launchPrefixPath || ""
|
||||||
|
|
||||||
|
let env = game.launchEnv || ({})
|
||||||
|
let keys = Object.keys(env)
|
||||||
|
keys.sort()
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
let k = keys[i]
|
||||||
|
envModel.append({ key: k, value: String(env[k]) })
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
nameField.text = ""
|
nameField.text = ""
|
||||||
developerField.text = ""
|
developerField.text = ""
|
||||||
executableField.text = ""
|
executableField.text = ""
|
||||||
workingDirField.text = ""
|
workingDirField.text = ""
|
||||||
|
|
||||||
|
runnerCombo.currentIndex = 0
|
||||||
|
runnerPathField.text = ""
|
||||||
|
prefixPathField.text = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,6 +291,168 @@ Kirigami.Dialog {
|
||||||
text: ""
|
text: ""
|
||||||
placeholderText: i18n("Optional")
|
placeholderText: i18n("Optional")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormCard {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
FormCard.FormHeader {
|
||||||
|
title: i18n("Compatibility")
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.margins: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
spacing: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
text: i18n("Runner")
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ComboBox {
|
||||||
|
id: runnerCombo
|
||||||
|
Layout.fillWidth: true
|
||||||
|
model: [i18n("Native"), i18n("Wine"), i18n("Proton"), i18n("Custom")]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormTextFieldDelegate {
|
||||||
|
id: runnerPathField
|
||||||
|
label: i18n("Runner Path")
|
||||||
|
text: ""
|
||||||
|
placeholderText: runnerCombo.currentIndex === 2 ? i18n("Auto-detect Proton") : (runnerCombo.currentIndex === 1 ? i18n("Use system Wine") : i18n("Required for Custom"))
|
||||||
|
enabled: runnerCombo.currentIndex !== 0
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
text: i18n("Browse Runner...")
|
||||||
|
icon.name: "document-open"
|
||||||
|
enabled: runnerCombo.currentIndex !== 0
|
||||||
|
onClicked: runnerFileDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormTextFieldDelegate {
|
||||||
|
id: prefixPathField
|
||||||
|
label: i18n("Prefix Path")
|
||||||
|
text: ""
|
||||||
|
placeholderText: i18n("Default (AppDataLocation/prefixes/<gameId>)")
|
||||||
|
enabled: runnerCombo.currentIndex === 1 || runnerCombo.currentIndex === 2
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
text: i18n("Browse Prefix...")
|
||||||
|
icon.name: "document-open-folder"
|
||||||
|
enabled: runnerCombo.currentIndex === 1 || runnerCombo.currentIndex === 2
|
||||||
|
onClicked: prefixFolderDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormCard {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
FormCard.FormHeader {
|
||||||
|
title: i18n("Environment Variables")
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormTextDelegate {
|
||||||
|
text: i18n("These variables are applied only when launching this game from A-La-Karte.")
|
||||||
|
textItem.wrapMode: Text.WordWrap
|
||||||
|
textItem.font: Kirigami.Theme.smallFont
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
text: i18n("Add Variable")
|
||||||
|
icon.name: "list-add"
|
||||||
|
onClicked: envModel.append({ key: "", value: "" })
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
text: i18n("Preset: MangoHud")
|
||||||
|
description: i18n("Sets MANGOHUD=1")
|
||||||
|
icon.name: "games-hint"
|
||||||
|
onClicked: dialog.setEnvVar("MANGOHUD", "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
text: i18n("Preset: GameMode")
|
||||||
|
description: i18n("Sets GAMEMODERUN=1")
|
||||||
|
icon.name: "speedometer"
|
||||||
|
onClicked: dialog.setEnvVar("GAMEMODERUN", "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
FormCard.FormDelegateSeparator {}
|
||||||
|
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
text: i18n("Clear Variables")
|
||||||
|
icon.name: "edit-clear"
|
||||||
|
enabled: envModel.count > 0
|
||||||
|
onClicked: envModel.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.margins: Kirigami.Units.largeSpacing
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: envModel
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
QQC2.TextField {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredWidth: 1
|
||||||
|
placeholderText: i18n("KEY")
|
||||||
|
text: key
|
||||||
|
onTextChanged: envModel.setProperty(index, "key", text)
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.TextField {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredWidth: 2
|
||||||
|
placeholderText: i18n("VALUE")
|
||||||
|
text: value
|
||||||
|
onTextChanged: envModel.setProperty(index, "value", text)
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolButton {
|
||||||
|
icon.name: "list-remove"
|
||||||
|
onClicked: envModel.remove(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FormCard.FormCard {
|
FormCard.FormCard {
|
||||||
|
|
@ -339,6 +577,23 @@ Kirigami.Dialog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileDialog {
|
||||||
|
id: runnerFileDialog
|
||||||
|
title: i18n("Select Runner Executable")
|
||||||
|
fileMode: FileDialog.OpenFile
|
||||||
|
onAccepted: {
|
||||||
|
runnerPathField.text = selectedFile.toString().replace("file://", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FolderDialog {
|
||||||
|
id: prefixFolderDialog
|
||||||
|
title: i18n("Select Prefix Folder")
|
||||||
|
onAccepted: {
|
||||||
|
prefixPathField.text = selectedFolder.toString().replace("file://", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function saveCoverImage(game) {
|
function saveCoverImage(game) {
|
||||||
if (selectedCoverPath === "") return
|
if (selectedCoverPath === "") return
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue