mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-03-26 17:03:08 +00:00
runner: add game profile API
This commit is contained in:
parent
27a385e3c3
commit
7724205354
4 changed files with 633 additions and 6 deletions
|
|
@ -4,10 +4,12 @@
|
||||||
#include "runnermanagerdaemon.h"
|
#include "runnermanagerdaemon.h"
|
||||||
|
|
||||||
#include <QCryptographicHash>
|
#include <QCryptographicHash>
|
||||||
|
#include <QDBusArgument>
|
||||||
#include <QDBusConnection>
|
#include <QDBusConnection>
|
||||||
#include <QDBusError>
|
#include <QDBusError>
|
||||||
#include <QDBusMessage>
|
#include <QDBusMessage>
|
||||||
#include <QDBusReply>
|
#include <QDBusReply>
|
||||||
|
#include <QDBusVariant>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
#include <QEventLoop>
|
#include <QEventLoop>
|
||||||
|
|
@ -32,6 +34,52 @@
|
||||||
static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1");
|
static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1");
|
||||||
static const QString kRunnerPath = QStringLiteral("/org/kde/ALaKarte/Runner1");
|
static const QString kRunnerPath = QStringLiteral("/org/kde/ALaKarte/Runner1");
|
||||||
|
|
||||||
|
static QVariant unwrapDbusVariant(QVariant v)
|
||||||
|
{
|
||||||
|
if (v.metaType() == QMetaType::fromType<QDBusVariant>()) {
|
||||||
|
v = v.value<QDBusVariant>().variant();
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QVariantMap unwrapVariantMap(QVariant v)
|
||||||
|
{
|
||||||
|
v = unwrapDbusVariant(v);
|
||||||
|
if (v.metaType() == QMetaType::fromType<QDBusArgument>()) {
|
||||||
|
const QDBusArgument arg = v.value<QDBusArgument>();
|
||||||
|
return qdbus_cast<QVariantMap>(arg);
|
||||||
|
}
|
||||||
|
if (v.canConvert<QVariantMap>()) {
|
||||||
|
return v.toMap();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static QStringList unwrapStringList(QVariant v)
|
||||||
|
{
|
||||||
|
v = unwrapDbusVariant(v);
|
||||||
|
if (v.metaType() == QMetaType::fromType<QDBusArgument>()) {
|
||||||
|
const QDBusArgument arg = v.value<QDBusArgument>();
|
||||||
|
return qdbus_cast<QStringList>(arg);
|
||||||
|
}
|
||||||
|
if (v.canConvert<QStringList>()) {
|
||||||
|
return v.toStringList();
|
||||||
|
}
|
||||||
|
if (v.canConvert<QVariantList>()) {
|
||||||
|
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)
|
static QString stableIdForPath(const QString &prefix, const QString &path)
|
||||||
{
|
{
|
||||||
const QByteArray digest = QCryptographicHash::hash(path.toUtf8(), QCryptographicHash::Sha1).toHex();
|
const QByteArray digest = QCryptographicHash::hash(path.toUtf8(), QCryptographicHash::Sha1).toHex();
|
||||||
|
|
@ -46,6 +94,7 @@ RunnerManagerDaemon::RunnerManagerDaemon(QObject *parent)
|
||||||
bool RunnerManagerDaemon::init()
|
bool RunnerManagerDaemon::init()
|
||||||
{
|
{
|
||||||
loadRegistry();
|
loadRegistry();
|
||||||
|
loadGameProfiles();
|
||||||
m_discovered = discoverRunners();
|
m_discovered = discoverRunners();
|
||||||
|
|
||||||
QDBusConnection bus = QDBusConnection::sessionBus();
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
|
@ -181,6 +230,76 @@ bool RunnerManagerDaemon::saveRegistry() const
|
||||||
return true;
|
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()
|
QStringList RunnerManagerDaemon::steamCandidateRoots()
|
||||||
{
|
{
|
||||||
const QString home = QDir::homePath();
|
const QString home = QDir::homePath();
|
||||||
|
|
@ -998,14 +1117,192 @@ QVariantMap RunnerManagerDaemon::DeletePrefix(const QVariantMap &spec)
|
||||||
return out;
|
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 RunnerManagerDaemon::ResolveLaunch(const QVariantMap &spec) const
|
||||||
{
|
{
|
||||||
QVariantMap out;
|
QVariantMap out;
|
||||||
out.insert(QStringLiteral("ok"), false);
|
out.insert(QStringLiteral("ok"), false);
|
||||||
|
|
||||||
const QString runnerId = spec.value(QStringLiteral("runnerId")).toString();
|
const QString gameId = spec.value(QStringLiteral("gameId")).toString();
|
||||||
QString runner = spec.value(QStringLiteral("runner")).toString();
|
|
||||||
QString runnerPath = spec.value(QStringLiteral("runnerPath")).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()) {
|
if (!runnerId.isEmpty()) {
|
||||||
const RunnerInfo info = runnerById(runnerId);
|
const RunnerInfo info = runnerById(runnerId);
|
||||||
|
|
@ -1017,17 +1314,84 @@ QVariantMap RunnerManagerDaemon::ResolveLaunch(const QVariantMap &spec) const
|
||||||
runnerPath = info.path;
|
runnerPath = info.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString gameId = spec.value(QStringLiteral("gameId")).toString();
|
|
||||||
const QString program = spec.value(QStringLiteral("program")).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();
|
QString prefixPath = spec.value(QStringLiteral("prefixPath")).toString();
|
||||||
|
if (prefixPath.isEmpty() && profile.contains(QStringLiteral("prefixPath"))) {
|
||||||
|
prefixPath = profile.value(QStringLiteral("prefixPath")).toString();
|
||||||
|
}
|
||||||
|
|
||||||
if (program.isEmpty()) {
|
if (program.isEmpty()) {
|
||||||
out.insert(QStringLiteral("error"), QStringLiteral("missing program"));
|
out.insert(QStringLiteral("error"), QStringLiteral("missing program"));
|
||||||
return out;
|
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 ((runner == QLatin1String("wine")) || (runner == QLatin1String("proton"))) {
|
||||||
if (prefixPath.isEmpty()) {
|
if (prefixPath.isEmpty()) {
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,18 @@ public Q_SLOTS:
|
||||||
QVariantMap DeletePrefix(const QVariantMap &spec);
|
QVariantMap DeletePrefix(const QVariantMap &spec);
|
||||||
QVariantMap ResolveLaunch(const QVariantMap &spec) const;
|
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:
|
Q_SIGNALS:
|
||||||
void InstallStarted(const QString &installId, const QVariantMap &spec);
|
void InstallStarted(const QString &installId, const QVariantMap &spec);
|
||||||
void InstallProgress(const QString &installId, qint64 receivedBytes, qint64 totalBytes);
|
void InstallProgress(const QString &installId, qint64 receivedBytes, qint64 totalBytes);
|
||||||
void InstallFinished(const QString &installId, const QVariantMap &result);
|
void InstallFinished(const QString &installId, const QVariantMap &result);
|
||||||
|
|
||||||
|
void GameProfilesChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct RunnerInfo {
|
struct RunnerInfo {
|
||||||
QString id;
|
QString id;
|
||||||
|
|
@ -65,6 +72,11 @@ private:
|
||||||
void loadRegistry();
|
void loadRegistry();
|
||||||
bool saveRegistry() const;
|
bool saveRegistry() const;
|
||||||
|
|
||||||
|
QString gameProfilesPath() const;
|
||||||
|
void loadGameProfiles();
|
||||||
|
bool saveGameProfiles() const;
|
||||||
|
QVariantMap gameProfileForGameId(const QString &gameId) const;
|
||||||
|
|
||||||
QList<RunnerInfo> discoverRunners() const;
|
QList<RunnerInfo> discoverRunners() const;
|
||||||
QString defaultProtonExecutable() const;
|
QString defaultProtonExecutable() const;
|
||||||
|
|
||||||
|
|
@ -81,4 +93,6 @@ private:
|
||||||
|
|
||||||
QHash<QString, QPointer<QNetworkReply>> m_installReplies;
|
QHash<QString, QPointer<QNetworkReply>> m_installReplies;
|
||||||
QHash<QString, QPointer<QProcess>> m_installProcesses;
|
QHash<QString, QPointer<QProcess>> m_installProcesses;
|
||||||
|
|
||||||
|
QHash<QString, QVariantMap> m_gameProfiles;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
#include <QDBusPendingCall>
|
#include <QDBusPendingCall>
|
||||||
#include <QDBusPendingCallWatcher>
|
#include <QDBusPendingCallWatcher>
|
||||||
#include <QDBusReply>
|
#include <QDBusReply>
|
||||||
|
#include <QDBusVariant>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
|
|
@ -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 kRunnerPath = QStringLiteral("/org/kde/ALaKarte/Runner1");
|
||||||
static const QString kRunnerInterface = QStringLiteral("org.kde.ALaKarte.Runner1");
|
static const QString kRunnerInterface = QStringLiteral("org.kde.ALaKarte.Runner1");
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
static QVariant unwrapDbusVariant(QVariant v)
|
||||||
|
{
|
||||||
|
if (v.canConvert<QDBusVariant>()) {
|
||||||
|
v = v.value<QDBusVariant>().variant();
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QVariantMap unwrapVariantMap(QVariant v)
|
||||||
|
{
|
||||||
|
v = unwrapDbusVariant(v);
|
||||||
|
if (v.metaType() == QMetaType::fromType<QDBusArgument>()) {
|
||||||
|
const QDBusArgument arg = v.value<QDBusArgument>();
|
||||||
|
return qdbus_cast<QVariantMap>(arg);
|
||||||
|
}
|
||||||
|
if (v.canConvert<QVariantMap>()) {
|
||||||
|
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)
|
static QString stableIdForUrl(const QString &type, const QString &url)
|
||||||
{
|
{
|
||||||
const QByteArray digest = QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Sha1).toHex();
|
const QByteArray digest = QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Sha1).toHex();
|
||||||
|
|
@ -80,6 +127,8 @@ RunnerManagerClient::RunnerManagerClient(QObject *parent)
|
||||||
SLOT(onInstallProgress(QString, qlonglong, qlonglong)));
|
SLOT(onInstallProgress(QString, qlonglong, qlonglong)));
|
||||||
|
|
||||||
bus.connect(kRunnerService, kRunnerPath, kRunnerInterface, QStringLiteral("InstallFinished"), this, SLOT(onInstallFinished(QString, QVariantMap)));
|
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()) {
|
if (QCoreApplication::instance()) {
|
||||||
|
|
@ -89,6 +138,7 @@ RunnerManagerClient::RunnerManagerClient(QObject *parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshRunners();
|
refreshRunners();
|
||||||
|
refreshGameProfiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
RunnerManagerClient::~RunnerManagerClient()
|
RunnerManagerClient::~RunnerManagerClient()
|
||||||
|
|
@ -213,6 +263,11 @@ QVariantList RunnerManagerClient::runners() const
|
||||||
return m_runners;
|
return m_runners;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QVariantList RunnerManagerClient::gameProfiles() const
|
||||||
|
{
|
||||||
|
return m_gameProfiles;
|
||||||
|
}
|
||||||
|
|
||||||
static QString normalizeHex(QString s)
|
static QString normalizeHex(QString s)
|
||||||
{
|
{
|
||||||
s = s.trimmed().toLower();
|
s = s.trimmed().toLower();
|
||||||
|
|
@ -417,6 +472,11 @@ void RunnerManagerClient::onInstallFinished(const QString &installId, const QVar
|
||||||
refreshRunners();
|
refreshRunners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::onGameProfilesChanged()
|
||||||
|
{
|
||||||
|
refreshGameProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
void RunnerManagerClient::refreshRunners()
|
void RunnerManagerClient::refreshRunners()
|
||||||
{
|
{
|
||||||
ensureRunnerDaemon();
|
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<QVariantList> 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<QVariantMap>()) {
|
||||||
|
map = v.toMap();
|
||||||
|
} else if (v.metaType() == QMetaType::fromType<QDBusArgument>()) {
|
||||||
|
const QDBusArgument arg = v.value<QDBusArgument>();
|
||||||
|
map = qdbus_cast<QVariantMap>(arg);
|
||||||
|
} else if (v.canConvert<QVariantMap>()) {
|
||||||
|
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<int>::create(0);
|
||||||
|
auto callPtr = QSharedPointer<std::function<void()>>::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<QVariantMap> 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<int>::create(0);
|
||||||
|
auto callPtr = QSharedPointer<std::function<void()>>::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<QVariantMap> 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<int>::create(0);
|
||||||
|
auto callPtr = QSharedPointer<std::function<void()>>::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<QVariantMap> 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)
|
void RunnerManagerClient::uninstallRunner(const QString &runnerId)
|
||||||
{
|
{
|
||||||
const QString id = runnerId.trimmed();
|
const QString id = runnerId.trimmed();
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ class RunnerManagerClient : public QObject
|
||||||
Q_PROPERTY(QString status READ status NOTIFY statusChanged)
|
Q_PROPERTY(QString status READ status NOTIFY statusChanged)
|
||||||
Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
|
Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
|
||||||
Q_PROPERTY(QVariantList runners READ runners NOTIFY runnersChanged)
|
Q_PROPERTY(QVariantList runners READ runners NOTIFY runnersChanged)
|
||||||
|
Q_PROPERTY(QVariantList gameProfiles READ gameProfiles NOTIFY gameProfilesChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit RunnerManagerClient(QObject *parent = nullptr);
|
explicit RunnerManagerClient(QObject *parent = nullptr);
|
||||||
|
|
@ -29,6 +30,7 @@ public:
|
||||||
QString status() const;
|
QString status() const;
|
||||||
QString lastError() const;
|
QString lastError() const;
|
||||||
QVariantList runners() const;
|
QVariantList runners() const;
|
||||||
|
QVariantList gameProfiles() const;
|
||||||
|
|
||||||
Q_INVOKABLE void installRunnerFromUrl(const QString &url,
|
Q_INVOKABLE void installRunnerFromUrl(const QString &url,
|
||||||
const QString &sha256 = QString(),
|
const QString &sha256 = QString(),
|
||||||
|
|
@ -41,6 +43,11 @@ public:
|
||||||
Q_INVOKABLE void refreshRunners();
|
Q_INVOKABLE void refreshRunners();
|
||||||
Q_INVOKABLE void uninstallRunner(const QString &runnerId);
|
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 ensurePrefix(const QString &gameId, const QString &runner, const QString &prefixPath = QString());
|
||||||
|
|
||||||
Q_INVOKABLE void deletePrefix(const QString &gameId, const QString &prefixPath = QString());
|
Q_INVOKABLE void deletePrefix(const QString &gameId, const QString &prefixPath = QString());
|
||||||
|
|
@ -52,14 +59,21 @@ Q_SIGNALS:
|
||||||
void statusChanged();
|
void statusChanged();
|
||||||
void lastErrorChanged();
|
void lastErrorChanged();
|
||||||
void runnersChanged();
|
void runnersChanged();
|
||||||
|
void gameProfilesChanged();
|
||||||
void prefixEnsured(const QString &gameId, const QString &prefixPath);
|
void prefixEnsured(const QString &gameId, const QString &prefixPath);
|
||||||
void prefixDeleted(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:
|
private Q_SLOTS:
|
||||||
void onInstallStarted(const QString &installId, const QVariantMap &spec);
|
void onInstallStarted(const QString &installId, const QVariantMap &spec);
|
||||||
void onInstallProgress(const QString &installId, qlonglong received, qlonglong total);
|
void onInstallProgress(const QString &installId, qlonglong received, qlonglong total);
|
||||||
void onInstallFinished(const QString &installId, const QVariantMap &result);
|
void onInstallFinished(const QString &installId, const QVariantMap &result);
|
||||||
|
|
||||||
|
void onGameProfilesChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void ensureRunnerDaemon();
|
void ensureRunnerDaemon();
|
||||||
void shutdownSpawnedRunnerDaemon();
|
void shutdownSpawnedRunnerDaemon();
|
||||||
|
|
@ -77,8 +91,10 @@ private:
|
||||||
QString m_lastError;
|
QString m_lastError;
|
||||||
|
|
||||||
QVariantList m_runners;
|
QVariantList m_runners;
|
||||||
|
QVariantList m_gameProfiles;
|
||||||
|
|
||||||
bool m_refreshRetryPending = false;
|
bool m_refreshRetryPending = false;
|
||||||
|
bool m_refreshProfilesRetryPending = false;
|
||||||
|
|
||||||
class QProcess *m_runnerdProcess = nullptr;
|
class QProcess *m_runnerdProcess = nullptr;
|
||||||
bool m_runnerdSpawnAttempted = false;
|
bool m_runnerdSpawnAttempted = false;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue