Files
BiLauncher/gamedirimport.cpp
T
galeon 1d781b5324
tests / package-smoke-linux (push) Skipped
tests / package-smoke-windows (push) Skipped
tests / package-smoke-macos (push) Skipped
tests / macos (push) Successful in 1m20s
tests / linux (push) Successful in 2m14s
tests / windows (push) Failing after 23m21s
fix #23 by adding validating on first startup and close #30 by adding
version number

Signed-off-by: galeon <dertiellis@gmail.com>
2026-10-23 09:55:15 +03:00

245 lines
9.3 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "gamedirimport.h"
#include "minecraftversion.h"
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QList>
#include <algorithm>
#include <utility>
namespace {
// Признаки того, что в папке играли: это принадлежит сборке. Служебные файлы
// чужих лаунчеров (launcher_log.txt, webcache2 и т. п.) сюда не входят, иначе
// пустая папка официального лаунчера давала бы сборку без версии.
const QStringList kContentMarkers = {QStringLiteral("mods"),
QStringLiteral("saves"),
QStringLiteral("resourcepacks"),
QStringLiteral("shaderpacks"),
QStringLiteral("config"),
QStringLiteral("options.txt"),
QStringLiteral("servers.dat")};
struct Candidate
{
QString id;
QJsonObject json;
bool inherits = false;
};
QJsonObject readObject(const QString &path)
{
QFile file(path);
if (!file.open(QIODevice::ReadOnly))
return {};
return QJsonDocument::fromJson(file.readAll()).object();
}
QString versionJsonPath(const QString &gameDir, const QString &id)
{
return gameDir + QStringLiteral("/versions/") + id + u'/' + id + QStringLiteral(".json");
}
bool hasContent(const QString &gameDir)
{
for (const QString &name : kContentMarkers) {
const QFileInfo info(gameDir + u'/' + name);
if (info.isFile())
return true;
if (info.isDir() && !QDir(info.filePath()).isEmpty())
return true;
}
return false;
}
bool hasMods(const QString &gameDir)
{
return !QDir(gameDir + QStringLiteral("/mods"))
.entryList({QStringLiteral("*.jar")}, QDir::Files)
.isEmpty();
}
QDateTime parseLastUsed(const QString &text)
{
QDateTime when = QDateTime::fromString(text, Qt::ISODateWithMs);
if (!when.isValid())
when = QDateTime::fromString(text, Qt::ISODate);
return when;
}
// Версии из launcher_profiles.json, которые есть в versions/, свежие сверху.
// Профили latest-release/latest-snapshot без lastVersionId пропускаются: без
// манифеста не узнать, во что они разворачивались.
QStringList profileVersions(const QString &gameDir, const QStringList &installed)
{
const QJsonObject profiles =
readObject(gameDir + QStringLiteral("/launcher_profiles.json"))
.value(QStringLiteral("profiles"))
.toObject();
struct Used
{
QString id;
QDateTime when;
};
QList<Used> used;
for (auto it = profiles.constBegin(); it != profiles.constEnd(); ++it) {
const QJsonObject profile = it.value().toObject();
const QString id = profile.value(QStringLiteral("lastVersionId")).toString();
if (id.isEmpty() || !installed.contains(id))
continue;
used.append({id, parseLastUsed(profile.value(QStringLiteral("lastUsed")).toString())});
}
// Профиль без даты считается самым старым.
std::stable_sort(used.begin(), used.end(), [](const Used &a, const Used &b) {
if (a.when.isValid() != b.when.isValid())
return a.when.isValid();
return a.when > b.when;
});
QStringList ids;
for (const Used &u : used) {
if (!ids.contains(u.id))
ids << u.id;
}
return ids;
}
// Установленные версии по времени правки описания, свежие сверху.
QStringList versionsByMtime(const QString &gameDir, QStringList installed)
{
std::stable_sort(installed.begin(), installed.end(), [&](const QString &a, const QString &b) {
return QFileInfo(versionJsonPath(gameDir, a)).lastModified()
> QFileInfo(versionJsonPath(gameDir, b)).lastModified();
});
return installed;
}
QList<Candidate> load(const QString &gameDir, const QStringList &ids)
{
QList<Candidate> out;
for (const QString &id : ids) {
const QJsonObject json = readObject(versionJsonPath(gameDir, id));
// Битое описание не запустится — такую версию не предлагаем.
if (json.isEmpty())
continue;
out.append({id, json, !json.value(QStringLiteral("inheritsFrom")).toString().isEmpty()});
}
return out;
}
// "1.20.1-47.2.0" → "47.2.0". Хвост "-1.7.10" у старых Forge остаётся: в
// каталоге модлоадеров версии записаны так же.
QString stripGamePrefix(const QString &version, const QString &game)
{
if (!game.isEmpty() && version.startsWith(game + u'-'))
return version.mid(game.size() + 1);
return version;
}
}
namespace GameDirImport {
LoaderInfo loaderOf(const QJsonObject &versionJson)
{
const QString game = versionJson.value(QStringLiteral("inheritsFrom")).toString();
for (const QJsonValue &value : versionJson.value(QStringLiteral("libraries")).toArray()) {
// group:artifact:version[:classifier]
const QStringList parts = value.toObject().value(QStringLiteral("name")).toString().split(u':');
if (parts.size() < 3)
continue;
const QString coord = parts[0] + u':' + parts[1];
const QString version = parts[2];
if (coord == QLatin1String("net.fabricmc:fabric-loader"))
return {QStringLiteral("fabric"), version};
if (coord == QLatin1String("org.quiltmc:quilt-loader"))
return {QStringLiteral("quilt"), version};
if (coord == QLatin1String("net.neoforged:neoforge"))
return {QStringLiteral("neoforge"), version};
// NeoForge для 1.20.1 публиковался под старым именем артефакта.
if (coord == QLatin1String("net.neoforged:forge"))
return {QStringLiteral("neoforge"), stripGamePrefix(version, game)};
if (coord == QLatin1String("net.minecraftforge:forge"))
return {QStringLiteral("forge"), stripGamePrefix(version, game)};
}
// Библиотек модлоадера в описании может не быть (их вынесли в родителя или
// описание урезали руками) — тогда смотрим на имя, которое дают установщики.
const QString id = versionJson.value(QStringLiteral("id")).toString();
for (const auto &[key, prefix] : {std::pair{QStringLiteral("fabric"), QStringLiteral("fabric-loader-")},
std::pair{QStringLiteral("quilt"), QStringLiteral("quilt-loader-")}}) {
const QString suffix = u'-' + game;
if (!game.isEmpty() && id.startsWith(prefix) && id.endsWith(suffix))
return {key, id.mid(prefix.size(), id.size() - prefix.size() - suffix.size())};
}
if (id.startsWith(QLatin1String("neoforge-")))
return {QStringLiteral("neoforge"), id.mid(9)};
const QString forgeInfix = game + QStringLiteral("-forge-");
if (!game.isEmpty() && id.startsWith(forgeInfix))
return {QStringLiteral("forge"), id.mid(forgeInfix.size())};
return {};
}
std::optional<Detected> detect(const QString &gameDir)
{
if (gameDir.isEmpty() || !QFileInfo(gameDir).isDir())
return std::nullopt;
const QStringList installed = VersionLoader::installedVersions(gameDir);
const bool content = hasContent(gameDir);
if (installed.isEmpty() && !content)
return std::nullopt;
const QList<Candidate> fromProfiles = load(gameDir, profileVersions(gameDir, installed));
const QList<Candidate> fromVersions = load(gameDir, versionsByMtime(gameDir, installed));
// С модами в папке ванильная версия почти наверняка не та, с которой играли:
// сначала ищем профиль модлоадера, и только потом берём что есть.
const Candidate *chosen = nullptr;
QString source;
auto pick = [&](const QList<Candidate> &list, const QString &from, bool needInherits) {
if (chosen)
return;
for (const Candidate &c : list) {
if (!needInherits || c.inherits) {
chosen = &c;
source = from;
return;
}
}
};
if (hasMods(gameDir)) {
pick(fromProfiles, QStringLiteral("launcher_profiles"), true);
pick(fromVersions, QStringLiteral("versions"), true);
}
pick(fromProfiles, QStringLiteral("launcher_profiles"), false);
pick(fromVersions, QStringLiteral("versions"), false);
Detected result;
if (!chosen) {
result.source = QStringLiteral("content");
return result;
}
result.source = source;
if (chosen->inherits) {
result.minecraftVersion = chosen->json.value(QStringLiteral("inheritsFrom")).toString();
result.resolvedVersionId = chosen->id;
const LoaderInfo loader = loaderOf(chosen->json);
result.loader = loader.key;
result.loaderVersion = loader.version;
} else {
result.minecraftVersion = chosen->id;
}
return result;
}
}