a-la-karte/src/game.cpp
Marco Allegretti 365c3b31cb Use global couch mode for profiles
Select the couch launch profile based on effective UI mode so profile choice is consistent across games.

Update details and edit UI to toggle and reflect couch mode globally, and fix duplicate QML property declarations.
2026-01-30 14:07:58 +01:00

730 lines
21 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024 A-La-Karte Contributors
#include "game.h"
#include <KLocalizedString>
#include <QCryptographicHash>
#include <QJsonValue>
#include <QLocale>
#include <QUuid>
Game::Game(QObject *parent)
: QObject(parent)
, m_id(QUuid::createUuid().toString(QUuid::WithoutBraces))
, m_dateAdded(QDateTime::currentDateTime())
{
}
Game::Game(const QString &id, const QString &name, QObject *parent)
: QObject(parent)
, m_id(id.isEmpty() ? QUuid::createUuid().toString(QUuid::WithoutBraces) : id)
, m_name(name)
, m_dateAdded(QDateTime::currentDateTime())
{
}
QString Game::id() const
{
return m_id;
}
QString Game::name() const
{
return m_name;
}
void Game::setName(const QString &name)
{
if (m_name != name) {
m_name = name;
Q_EMIT nameChanged();
}
}
QString Game::description() const
{
return m_description;
}
void Game::setDescription(const QString &description)
{
if (m_description != description) {
m_description = description;
Q_EMIT descriptionChanged();
}
}
QString Game::developer() const
{
return m_developer;
}
void Game::setDeveloper(const QString &developer)
{
if (m_developer != developer) {
m_developer = developer;
Q_EMIT developerChanged();
}
}
QString Game::publisher() const
{
return m_publisher;
}
void Game::setPublisher(const QString &publisher)
{
if (m_publisher != publisher) {
m_publisher = publisher;
Q_EMIT publisherChanged();
}
}
QUrl Game::coverUrl() const
{
return m_coverUrl;
}
void Game::setCoverUrl(const QUrl &url)
{
if (m_coverUrl != url) {
m_coverUrl = url;
Q_EMIT coverUrlChanged();
}
}
QUrl Game::iconUrl() const
{
return m_iconUrl;
}
void Game::setIconUrl(const QUrl &url)
{
if (m_iconUrl != url) {
m_iconUrl = url;
Q_EMIT iconUrlChanged();
}
}
QString Game::launchCommand() const
{
return m_launchCommand;
}
void Game::setLaunchCommand(const QString &command)
{
if (m_launchCommand != command) {
m_launchCommand = command;
Q_EMIT launchCommandChanged();
}
}
QString Game::workingDirectory() const
{
return m_workingDirectory;
}
void Game::setWorkingDirectory(const QString &dir)
{
if (m_workingDirectory != dir) {
m_workingDirectory = dir;
Q_EMIT workingDirectoryChanged();
}
}
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::activeLaunchProfile() const
{
return m_activeLaunchProfile;
}
void Game::setActiveLaunchProfile(const QString &profileId)
{
const QString normalized = profileId.trimmed().isEmpty() ? QStringLiteral("default") : profileId.trimmed();
if (m_activeLaunchProfile != normalized) {
m_activeLaunchProfile = normalized;
Q_EMIT activeLaunchProfileChanged();
}
}
QVariantMap Game::launchProfileConfig(const QString &profileId) const
{
const QString id = profileId.trimmed();
if (id.isEmpty() || id == QLatin1String("default")) {
return {};
}
const QVariant value = m_launchProfiles.value(id);
if (!value.isValid()) {
return {};
}
return value.toMap();
}
void Game::setLaunchProfileConfig(const QString &profileId, const QVariantMap &config)
{
const QString id = profileId.trimmed();
if (id.isEmpty() || id == QLatin1String("default")) {
return;
}
QVariantMap cleaned;
const QString runner = config.value(QStringLiteral("runner")).toString().trimmed();
if (!runner.isEmpty()) {
cleaned.insert(QStringLiteral("runner"), runner);
}
const QString runnerPath = config.value(QStringLiteral("runnerPath")).toString().trimmed();
if (!runnerPath.isEmpty()) {
cleaned.insert(QStringLiteral("runnerPath"), runnerPath);
}
const QString prefixPath = config.value(QStringLiteral("prefixPath")).toString().trimmed();
if (!prefixPath.isEmpty()) {
cleaned.insert(QStringLiteral("prefixPath"), prefixPath);
}
const QVariantMap env = config.value(QStringLiteral("env")).toMap();
if (!env.isEmpty()) {
cleaned.insert(QStringLiteral("env"), env);
}
if (cleaned.isEmpty()) {
m_launchProfiles.remove(id);
} else {
m_launchProfiles.insert(id, cleaned);
}
}
QVariantMap Game::effectiveLaunchConfig() const
{
return effectiveLaunchConfigForProfile(m_activeLaunchProfile);
}
QVariantMap Game::effectiveLaunchConfigForProfile(const QString &profileId) const
{
QVariantMap result;
if (!m_launchRunner.isEmpty()) {
result.insert(QStringLiteral("runner"), m_launchRunner);
}
if (!m_launchRunnerPath.isEmpty()) {
result.insert(QStringLiteral("runnerPath"), m_launchRunnerPath);
}
if (!m_launchPrefixPath.isEmpty()) {
result.insert(QStringLiteral("prefixPath"), m_launchPrefixPath);
}
if (!m_launchEnv.isEmpty()) {
result.insert(QStringLiteral("env"), m_launchEnv);
}
const QString id = profileId.trimmed();
if (id.isEmpty() || id == QLatin1String("default")) {
return result;
}
const QVariantMap profileConfig = launchProfileConfig(id);
if (profileConfig.isEmpty()) {
return result;
}
const QString runner = profileConfig.value(QStringLiteral("runner")).toString().trimmed();
if (!runner.isEmpty()) {
result.insert(QStringLiteral("runner"), runner);
}
const QString runnerPath = profileConfig.value(QStringLiteral("runnerPath")).toString().trimmed();
if (!runnerPath.isEmpty()) {
result.insert(QStringLiteral("runnerPath"), runnerPath);
}
const QString prefixPath = profileConfig.value(QStringLiteral("prefixPath")).toString().trimmed();
if (!prefixPath.isEmpty()) {
result.insert(QStringLiteral("prefixPath"), prefixPath);
}
const QVariantMap profileEnv = profileConfig.value(QStringLiteral("env")).toMap();
if (!profileEnv.isEmpty()) {
QVariantMap mergedEnv;
const QVariantMap baseEnv = result.value(QStringLiteral("env")).toMap();
for (auto it = baseEnv.constBegin(); it != baseEnv.constEnd(); ++it) {
mergedEnv.insert(it.key(), it.value());
}
for (auto it = profileEnv.constBegin(); it != profileEnv.constEnd(); ++it) {
mergedEnv.insert(it.key(), it.value());
}
result.insert(QStringLiteral("env"), mergedEnv);
}
return result;
}
QString Game::platform() const
{
return m_platform;
}
void Game::setPlatform(const QString &platform)
{
if (m_platform != platform) {
m_platform = platform;
Q_EMIT platformChanged();
}
}
QString Game::platformId() const
{
return m_platformId;
}
void Game::setPlatformId(const QString &id)
{
if (m_platformId != id) {
m_platformId = id;
Q_EMIT platformIdChanged();
}
}
QDateTime Game::dateAdded() const
{
return m_dateAdded;
}
void Game::setDateAdded(const QDateTime &dateTime)
{
if (m_dateAdded != dateTime) {
m_dateAdded = dateTime;
Q_EMIT dateAddedChanged();
}
}
QDateTime Game::lastPlayed() const
{
return m_lastPlayed;
}
void Game::setLastPlayed(const QDateTime &dateTime)
{
if (m_lastPlayed != dateTime) {
m_lastPlayed = dateTime;
Q_EMIT lastPlayedChanged();
}
}
qint64 Game::playTime() const
{
return m_playTime;
}
void Game::setPlayTime(qint64 seconds)
{
if (m_playTime != seconds) {
m_playTime = seconds;
Q_EMIT playTimeChanged();
}
}
bool Game::favorite() const
{
return m_favorite;
}
void Game::setFavorite(bool favorite)
{
if (m_favorite != favorite) {
m_favorite = favorite;
Q_EMIT favoriteChanged();
}
}
bool Game::hidden() const
{
return m_hidden;
}
void Game::setHidden(bool hidden)
{
if (m_hidden != hidden) {
m_hidden = hidden;
Q_EMIT hiddenChanged();
}
}
bool Game::installed() const
{
return m_installed;
}
void Game::setInstalled(bool installed)
{
if (m_installed != installed) {
m_installed = installed;
Q_EMIT installedChanged();
}
}
bool Game::running() const
{
return m_running;
}
void Game::setRunning(bool running)
{
if (m_running != running) {
m_running = running;
Q_EMIT runningChanged();
}
}
QJsonObject Game::toJson() const
{
QJsonObject obj;
obj[QStringLiteral("id")] = m_id;
obj[QStringLiteral("name")] = m_name;
obj[QStringLiteral("description")] = m_description;
obj[QStringLiteral("developer")] = m_developer;
obj[QStringLiteral("publisher")] = m_publisher;
obj[QStringLiteral("coverUrl")] = m_coverUrl.toString();
obj[QStringLiteral("iconUrl")] = m_iconUrl.toString();
obj[QStringLiteral("launchCommand")] = m_launchCommand;
obj[QStringLiteral("workingDirectory")] = m_workingDirectory;
obj[QStringLiteral("platform")] = m_platform;
obj[QStringLiteral("platformId")] = m_platformId;
obj[QStringLiteral("dateAdded")] = m_dateAdded.toString(Qt::ISODate);
obj[QStringLiteral("lastPlayed")] = m_lastPlayed.toString(Qt::ISODate);
obj[QStringLiteral("playTime")] = m_playTime;
obj[QStringLiteral("favorite")] = m_favorite;
obj[QStringLiteral("hidden")] = m_hidden;
obj[QStringLiteral("installed")] = m_installed;
const bool hasLaunchConfig = !m_launchEnv.isEmpty() || !m_launchRunner.isEmpty() || !m_launchRunnerPath.isEmpty() || !m_launchPrefixPath.isEmpty()
|| !m_launchProfiles.isEmpty() || (!m_activeLaunchProfile.isEmpty() && m_activeLaunchProfile != QLatin1String("default"));
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);
}
if (!m_activeLaunchProfile.isEmpty() && m_activeLaunchProfile != QLatin1String("default")) {
launchObj.insert(QStringLiteral("activeProfile"), m_activeLaunchProfile);
}
if (!m_launchProfiles.isEmpty()) {
QJsonObject profilesObj;
for (auto it = m_launchProfiles.constBegin(); it != m_launchProfiles.constEnd(); ++it) {
const QString profileId = it.key();
if (profileId.isEmpty() || profileId == QLatin1String("default")) {
continue;
}
const QVariantMap profileConfig = it.value().toMap();
if (profileConfig.isEmpty()) {
continue;
}
QJsonObject profileObj;
const QString runner = profileConfig.value(QStringLiteral("runner")).toString();
if (!runner.isEmpty()) {
profileObj.insert(QStringLiteral("runner"), runner);
}
const QString runnerPath = profileConfig.value(QStringLiteral("runnerPath")).toString();
if (!runnerPath.isEmpty()) {
profileObj.insert(QStringLiteral("runnerPath"), runnerPath);
}
const QString prefixPath = profileConfig.value(QStringLiteral("prefixPath")).toString();
if (!prefixPath.isEmpty()) {
profileObj.insert(QStringLiteral("prefixPath"), prefixPath);
}
const QVariantMap env = profileConfig.value(QStringLiteral("env")).toMap();
if (!env.isEmpty()) {
QJsonObject envObj;
for (auto envIt = env.constBegin(); envIt != env.constEnd(); ++envIt) {
envObj.insert(envIt.key(), QJsonValue::fromVariant(envIt.value()));
}
profileObj.insert(QStringLiteral("env"), envObj);
}
if (!profileObj.isEmpty()) {
profilesObj.insert(profileId, profileObj);
}
}
if (!profilesObj.isEmpty()) {
launchObj.insert(QStringLiteral("profiles"), profilesObj);
}
}
obj.insert(QStringLiteral("launch"), launchObj);
}
return obj;
}
Game *Game::fromJson(const QJsonObject &json, QObject *parent)
{
QString id = json[QStringLiteral("id")].toString();
QString name = json[QStringLiteral("name")].toString();
if (name.isEmpty()) {
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->setDescription(json[QStringLiteral("description")].toString());
game->setDeveloper(json[QStringLiteral("developer")].toString());
game->setPublisher(json[QStringLiteral("publisher")].toString());
game->setCoverUrl(QUrl(json[QStringLiteral("coverUrl")].toString()));
game->setIconUrl(QUrl(json[QStringLiteral("iconUrl")].toString()));
game->setLaunchCommand(launchCommand);
game->setWorkingDirectory(workingDirectory);
game->setPlatform(platform);
game->setPlatformId(platformId);
game->setDateAdded(QDateTime::fromString(json[QStringLiteral("dateAdded")].toString(), Qt::ISODate));
game->setLastPlayed(QDateTime::fromString(json[QStringLiteral("lastPlayed")].toString(), Qt::ISODate));
game->setPlayTime(json[QStringLiteral("playTime")].toInteger());
game->setFavorite(json[QStringLiteral("favorite")].toBool());
game->setHidden(json[QStringLiteral("hidden")].toBool());
game->setInstalled(json[QStringLiteral("installed")].toBool(true));
QVariantMap env;
QString runner;
QString runnerPath;
QString prefixPath;
QString activeProfile;
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();
activeProfile = launchObj.value(QStringLiteral("activeProfile")).toString();
const QJsonValue profilesValue = launchObj.value(QStringLiteral("profiles"));
if (profilesValue.isObject()) {
const QJsonObject profilesObj = profilesValue.toObject();
for (auto it = profilesObj.constBegin(); it != profilesObj.constEnd(); ++it) {
if (!it.value().isObject()) {
continue;
}
const QString profileId = it.key();
if (profileId.isEmpty() || profileId == QLatin1String("default")) {
continue;
}
const QJsonObject profileObj = it.value().toObject();
QVariantMap profileConfig;
const QString pRunner = profileObj.value(QStringLiteral("runner")).toString();
if (!pRunner.isEmpty()) {
profileConfig.insert(QStringLiteral("runner"), pRunner);
}
const QString pRunnerPath = profileObj.value(QStringLiteral("runnerPath")).toString();
if (!pRunnerPath.isEmpty()) {
profileConfig.insert(QStringLiteral("runnerPath"), pRunnerPath);
}
const QString pPrefixPath = profileObj.value(QStringLiteral("prefixPath")).toString();
if (!pPrefixPath.isEmpty()) {
profileConfig.insert(QStringLiteral("prefixPath"), pPrefixPath);
}
const QJsonValue pEnvValue = profileObj.value(QStringLiteral("env"));
if (pEnvValue.isObject()) {
const QVariantMap pEnv = pEnvValue.toObject().toVariantMap();
if (!pEnv.isEmpty()) {
profileConfig.insert(QStringLiteral("env"), pEnv);
}
}
if (!profileConfig.isEmpty()) {
game->setLaunchProfileConfig(profileId, profileConfig);
}
}
}
}
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);
}
if (!activeProfile.isEmpty()) {
game->setActiveLaunchProfile(activeProfile);
}
return game;
}
QString Game::playTimeFormatted() const
{
if (m_playTime == 0) {
return i18n("Never played");
}
qint64 hours = m_playTime / 3600;
qint64 minutes = (m_playTime % 3600) / 60;
if (hours > 0) {
return i18np("%1 hour", "%1 hours", hours) + QStringLiteral(" ") + i18np("%1 minute", "%1 minutes", minutes);
}
return i18np("%1 minute", "%1 minutes", minutes);
}
QString Game::lastPlayedFormatted() const
{
if (!m_lastPlayed.isValid()) {
return i18n("Never");
}
QDateTime now = QDateTime::currentDateTime();
qint64 days = m_lastPlayed.daysTo(now);
if (days == 0) {
return i18n("Today");
} else if (days == 1) {
return i18n("Yesterday");
} else if (days < 7) {
return i18np("%1 day ago", "%1 days ago", days);
} else if (days < 30) {
return i18np("%1 week ago", "%1 weeks ago", days / 7);
} else if (days < 365) {
return i18np("%1 month ago", "%1 months ago", days / 30);
}
return QLocale().toString(m_lastPlayed, QLocale::ShortFormat);
}
QString Game::dateAddedFormatted() const
{
if (!m_dateAdded.isValid()) {
return i18n("Unknown");
}
QDateTime now = QDateTime::currentDateTime();
qint64 days = m_dateAdded.daysTo(now);
if (days == 0) {
return i18n("Today");
} else if (days == 1) {
return i18n("Yesterday");
} else if (days < 7) {
return i18np("%1 day ago", "%1 days ago", days);
} else if (days < 30) {
return i18np("%1 week ago", "%1 weeks ago", days / 7);
} else if (days < 365) {
return i18np("%1 month ago", "%1 months ago", days / 30);
}
return QLocale().toString(m_dateAdded, QLocale::ShortFormat);
}