a-la-karte/src/game.cpp

731 lines
21 KiB
C++
Raw Normal View History

// 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);
}