a-la-karte/src/steamimporter.cpp

269 lines
7.9 KiB
C++
Raw Normal View History

// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024 A-La-Karte Contributors
#include "steamimporter.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QStringList>
#include <QTextStream>
SteamImporter::SteamImporter(QObject *parent)
: PlatformImporter(parent)
{
}
QString SteamImporter::platformName() const
{
return QStringLiteral("Steam");
}
QString SteamImporter::platformId() const
{
return QStringLiteral("steam");
}
bool SteamImporter::isAvailable() const
{
return !findSteamPath().isEmpty();
}
QString SteamImporter::findSteamPath() const
{
// Common Steam installation paths on Linux
QStringList possiblePaths = {expandPath(QStringLiteral("~/.steam/steam")),
expandPath(QStringLiteral("~/.local/share/Steam")),
expandPath(QStringLiteral("~/.var/app/com.valvesoftware.Steam/.steam/steam")),
expandPath(QStringLiteral("~/.var/app/com.valvesoftware.Steam/.local/share/Steam")),
QStringLiteral("/usr/share/steam"),
QStringLiteral("/usr/local/share/steam")};
for (const QString &path : possiblePaths) {
if (directoryExists(path)) {
return path;
}
}
return {};
}
QString SteamImporter::steamPath() const
{
return findSteamPath();
}
QStringList SteamImporter::findLibraryFolders() const
{
QStringList folders;
QString steamDir = findSteamPath();
if (steamDir.isEmpty()) {
return folders;
}
// Add main Steam directory
QString steamApps = steamDir + QStringLiteral("/steamapps");
if (directoryExists(steamApps)) {
folders.append(steamApps);
}
// Parse libraryfolders.vdf
QString libraryFoldersPath = steamApps + QStringLiteral("/libraryfolders.vdf");
QFile file(libraryFoldersPath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return folders;
}
QTextStream in(&file);
QString content = in.readAll();
// Simple VDF parser for library folders
static QRegularExpression pathRegex(QStringLiteral("\"path\"\\s+\"([^\"]+)\""));
QRegularExpressionMatchIterator it = pathRegex.globalMatch(content);
while (it.hasNext()) {
QRegularExpressionMatch match = it.next();
QString path = match.captured(1);
QString appsPath = path + QStringLiteral("/steamapps");
if (directoryExists(appsPath) && !folders.contains(appsPath)) {
folders.append(appsPath);
}
}
return folders;
}
Game *SteamImporter::parseAppManifest(const QString &path)
{
QFile file(path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return nullptr;
}
QTextStream in(&file);
QString content = in.readAll();
// Simple VDF parser
auto getValue = [&content](const QString &key) -> QString {
QRegularExpression regex(QStringLiteral("\"%1\"\\s+\"([^\"]+)\"").arg(key));
QRegularExpressionMatch match = regex.match(content);
return match.hasMatch() ? match.captured(1) : QString();
};
QString appId = getValue(QStringLiteral("appid"));
QString name = getValue(QStringLiteral("name"));
QString installDir = getValue(QStringLiteral("installdir"));
if (appId.isEmpty() || name.isEmpty()) {
return nullptr;
}
auto isNonGameEntry = [](const QString &title, const QString &dir) {
auto normalize = [](QString s) {
s = s.toLower();
s.remove(QLatin1Char(' '));
s.remove(QLatin1Char('_'));
s.remove(QLatin1Char('-'));
return s;
};
const QString t = normalize(title);
const QString d = normalize(dir);
static const QStringList patterns = {
QStringLiteral("steamlinuxruntime"),
QStringLiteral("steamworkscommonredistributables"),
QStringLiteral("steamworkssdkredist"),
QStringLiteral("shaderprecaching"),
QStringLiteral("proton"),
QStringLiteral("steamvr"),
};
for (const QString &p : patterns) {
if (t.contains(p) || d.contains(p)) {
return true;
}
}
return false;
};
if (isNonGameEntry(name, installDir)) {
return nullptr;
}
// Skip tools and other non-game content
QString stateFlags = getValue(QStringLiteral("StateFlags"));
if (!stateFlags.isEmpty()) {
bool ok = false;
const int flags = stateFlags.toInt(&ok);
if (ok) {
constexpr int InstalledFlag = 4;
if ((flags & InstalledFlag) == 0) {
return nullptr;
}
}
}
QFileInfo manifestInfo(path);
const QString gameDir = manifestInfo.absolutePath() + QStringLiteral("/common/") + installDir;
if (!QDir(gameDir).exists()) {
return nullptr;
}
Game *game = new Game(QStringLiteral("steam-%1").arg(appId), name);
game->setPlatform(platformName());
game->setPlatformId(appId);
game->setLaunchCommand(QStringLiteral("steam://rungameid/%1").arg(appId));
// Find cover image
QUrl coverUrl = findCoverImage(appId);
if (coverUrl.isValid()) {
game->setCoverUrl(coverUrl);
}
// Set installation directory
game->setWorkingDirectory(gameDir);
game->setInstalled(true);
return game;
}
QUrl SteamImporter::findCoverImage(const QString &appId) const
{
QString steamDir = findSteamPath();
if (steamDir.isEmpty()) {
return {};
}
// Check local cache first
QStringList coverNames = {QStringLiteral("library_600x900.jpg"),
QStringLiteral("library_600x900_2x.jpg"),
QStringLiteral("header.jpg"),
QStringLiteral("capsule_616x353.jpg")};
QString cacheDir = steamDir + QStringLiteral("/appcache/librarycache");
for (const QString &coverName : coverNames) {
QString coverPath = cacheDir + QStringLiteral("/%1_%2").arg(appId, coverName);
if (QFile::exists(coverPath)) {
return QUrl::fromLocalFile(coverPath);
}
}
// Check alternative grid path
QString gridDir = steamDir + QStringLiteral("/userdata");
QDir userDataDir(gridDir);
QStringList userDirs = userDataDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const QString &userDir : userDirs) {
QString gridPath = gridDir + QStringLiteral("/%1/config/grid").arg(userDir);
QDir grid(gridPath);
if (grid.exists()) {
QStringList filters = {appId + QStringLiteral("p.*"), appId + QStringLiteral(".*")};
QStringList images = grid.entryList(filters, QDir::Files);
if (!images.isEmpty()) {
return QUrl::fromLocalFile(gridPath + QStringLiteral("/") + images.first());
}
}
}
// Return Steam CDN URL as fallback
return QUrl(QStringLiteral("https://steamcdn-a.akamaihd.net/steam/apps/%1/library_600x900_2x.jpg").arg(appId));
}
QList<Game *> SteamImporter::importGames()
{
QList<Game *> games;
QStringList libraryFolders = findLibraryFolders();
int total = 0;
int current = 0;
// Count manifests first
for (const QString &folder : libraryFolders) {
QDir dir(folder);
total += dir.entryList({QStringLiteral("appmanifest_*.acf")}, QDir::Files).count();
}
for (const QString &folder : libraryFolders) {
QDir dir(folder);
QStringList manifests = dir.entryList({QStringLiteral("appmanifest_*.acf")}, QDir::Files);
for (const QString &manifest : manifests) {
Game *game = parseAppManifest(folder + QStringLiteral("/") + manifest);
if (game) {
games.append(game);
}
current++;
Q_EMIT importProgress(current, total);
}
}
return games;
}