Files
minecraft-launcher/minecraftversion.cpp
T

467 lines
19 KiB
C++
Raw Normal View History

2026-08-22 18:19:13 +03:00
#include "minecraftversion.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QHash>
#include <QJsonDocument>
#include <QJsonObject>
#include <QObject>
#include <QRegularExpression>
#include <QSysInfo>
#include <algorithm>
#include <utility>
namespace {
// Больше десяти уровней inheritsFrom не бывает даже у Forge поверх Fabric-подобных сборок.
constexpr int kMaxInheritanceDepth = 10;
QString versionJsonPath(const QString &gameDir, const QString &id)
{
return gameDir + "/versions/" + id + "/" + id + ".json";
}
QString versionJarPath(const QString &gameDir, const QString &id)
{
return gameDir + "/versions/" + id + "/" + id + ".jar";
}
// org.lwjgl:lwjgl:3.3.1:natives-windows -> org/lwjgl/lwjgl/3.3.1/lwjgl-3.3.1-natives-windows.jar
QString mavenToPath(const QString &name)
{
QString coords = name;
QString extension = QStringLiteral("jar");
const int at = coords.indexOf(u'@');
if (at >= 0) {
extension = coords.mid(at + 1);
coords.truncate(at);
}
const QStringList parts = coords.split(u':');
if (parts.size() < 3)
return {};
QString file = parts[1] + u'-' + parts[2];
if (parts.size() > 3)
file += u'-' + parts[3];
return QString(parts[0]).replace(u'.', u'/') + u'/' + parts[1] + u'/' + parts[2]
+ u'/' + file + u'.' + extension;
}
// Ключ для дедупликации: у наследника и родителя одна и та же библиотека часто
// разных версий, и побеждать должна версия наследника.
QString libraryKey(const QString &name)
{
const QStringList parts = name.split(u':');
if (parts.size() < 2)
return name;
QString key = parts[0] + u':' + parts[1];
if (parts.size() > 3)
key += u':' + parts[3];
return key;
}
QString mavenClassifier(const QString &name)
{
const QStringList parts = name.split(u':');
if (parts.size() < 4)
return {};
QString classifier = parts[3];
const int at = classifier.indexOf(u'@');
if (at >= 0)
classifier.truncate(at);
return classifier;
}
bool ruleMatches(const QJsonObject &rule, const QSet<QString> &features)
{
if (rule.contains(QStringLiteral("features"))) {
const QJsonObject wanted = rule.value(QStringLiteral("features")).toObject();
for (auto it = wanted.constBegin(); it != wanted.constEnd(); ++it) {
if (it.value().toBool() != features.contains(it.key()))
return false;
}
}
const QJsonObject os = rule.value(QStringLiteral("os")).toObject();
if (!os.isEmpty()) {
if (os.contains(QStringLiteral("name"))
&& os.value(QStringLiteral("name")).toString() != VersionLoader::osName())
return false;
if (os.contains(QStringLiteral("arch"))
&& os.value(QStringLiteral("arch")).toString() != VersionLoader::osArch())
return false;
if (os.contains(QStringLiteral("version"))) {
const QRegularExpression re(os.value(QStringLiteral("version")).toString());
if (!re.isValid() || !re.match(QSysInfo::productVersion()).hasMatch())
return false;
}
}
return true;
}
void appendArguments(const QJsonArray &source, QStringList &target, const QSet<QString> &features)
{
for (const QJsonValue &entry : source) {
if (entry.isString()) {
target << entry.toString();
continue;
}
const QJsonObject object = entry.toObject();
if (!VersionLoader::rulesAllow(object.value(QStringLiteral("rules")).toArray(), features))
continue;
const QJsonValue value = object.value(QStringLiteral("value"));
if (value.isString())
target << value.toString();
else
for (const QJsonValue &item : value.toArray())
target << item.toString();
}
}
MinecraftLibrary libraryFromDownload(const QString &name,
const QJsonObject &download,
const QString &fallbackPath)
{
MinecraftLibrary library;
library.name = name;
library.path = download.value(QStringLiteral("path")).toString();
if (library.path.isEmpty())
library.path = fallbackPath;
library.url = download.value(QStringLiteral("url")).toString();
library.sha1 = download.value(QStringLiteral("sha1")).toString();
library.size = static_cast<qint64>(download.value(QStringLiteral("size")).toDouble());
return library;
}
void parseLibraries(const QJsonObject &root,
QList<MinecraftLibrary> &target,
const QSet<QString> &features)
{
for (const QJsonValue &entry : root.value(QStringLiteral("libraries")).toArray()) {
const QJsonObject object = entry.toObject();
if (!VersionLoader::rulesAllow(object.value(QStringLiteral("rules")).toArray(), features))
continue;
const QString name = object.value(QStringLiteral("name")).toString();
if (name.isEmpty())
continue;
const QString relativePath = mavenToPath(name);
const QJsonObject downloads = object.value(QStringLiteral("downloads")).toObject();
const QJsonObject extract = object.value(QStringLiteral("extract")).toObject();
QStringList exclude;
for (const QJsonValue &value : extract.value(QStringLiteral("exclude")).toArray())
exclude << value.toString();
// Основной артефакт. В новом формате (1.19+) натив-библиотеки — это
// отдельные записи с классификатором natives-*, у них тоже есть artifact.
if (downloads.contains(QStringLiteral("artifact")) || !downloads.contains(QStringLiteral("classifiers"))) {
MinecraftLibrary library = libraryFromDownload(
name, downloads.value(QStringLiteral("artifact")).toObject(), relativePath);
// У Fabric у части библиотек нет блока downloads вообще — только
// name и адрес Maven-репозитория, путь собираем из координат.
if (library.url.isEmpty()) {
const QString repository = object.value(QStringLiteral("url")).toString();
if (!repository.isEmpty())
library.url = repository.endsWith(u'/') ? repository + library.path
: repository + u'/' + library.path;
}
if (!library.path.isEmpty()) {
library.native = mavenClassifier(name).startsWith(QStringLiteral("natives-"));
library.extractExclude = exclude;
target << library;
}
}
// Старый формат (до 1.18): отдельная карта natives + downloads.classifiers.
const QJsonObject natives = object.value(QStringLiteral("natives")).toObject();
if (natives.isEmpty())
continue;
QString classifier = natives.value(VersionLoader::osName()).toString();
if (classifier.isEmpty())
continue;
classifier.replace(QStringLiteral("${arch}"),
VersionLoader::osArch() == QStringLiteral("x86") ? QStringLiteral("32")
: QStringLiteral("64"));
const QJsonObject classifiers = downloads.value(QStringLiteral("classifiers")).toObject();
MinecraftLibrary library = libraryFromDownload(
name + u':' + classifier,
classifiers.value(classifier).toObject(),
mavenToPath(name + u':' + classifier));
if (library.path.isEmpty())
continue;
library.native = true;
library.extractExclude = exclude;
target << library;
}
}
// Для 1.19+ в списке лежат natives-windows, natives-windows-x86 и natives-windows-arm64
// одновременно — правила фильтруют только по ОС. Оставляем по одной на артефакт.
void pickNativesForCurrentArch(QList<MinecraftLibrary> &libraries)
{
const QString exact = VersionLoader::nativeClassifier();
const QString osPart = VersionLoader::osName() == QStringLiteral("osx")
? QStringLiteral("macos")
: VersionLoader::osName();
const QString base = QStringLiteral("natives-") + osPart;
QHash<QString, QString> chosen; // group:artifact -> классификатор
for (const MinecraftLibrary &library : libraries) {
if (!library.native)
continue;
const QStringList parts = library.name.split(u':');
if (parts.size() < 4)
continue;
const QString artifact = parts[0] + u':' + parts[1];
const QString classifier = mavenClassifier(library.name);
const QString current = chosen.value(artifact);
if (current == exact)
continue;
if (classifier == exact || current.isEmpty() || classifier == base)
chosen.insert(artifact, classifier);
}
libraries.removeIf([&](const MinecraftLibrary &library) {
if (!library.native)
return false;
const QStringList parts = library.name.split(u':');
if (parts.size() < 4)
return false;
return chosen.value(parts[0] + u':' + parts[1]) != mavenClassifier(library.name);
});
}
} // namespace
QString VersionLoader::osName()
{
#if defined(Q_OS_WIN)
return QStringLiteral("windows");
#elif defined(Q_OS_MACOS)
return QStringLiteral("osx");
#else
return QStringLiteral("linux");
#endif
}
QString VersionLoader::osArch()
{
const QString arch = QSysInfo::currentCpuArchitecture();
if (arch == QLatin1String("i386") || arch == QLatin1String("i686"))
return QStringLiteral("x86");
return arch; // x86_64 / arm64 — совпадают с тем, что пишет Mojang
}
QString VersionLoader::nativeClassifier()
{
const QString arch = osArch();
const QString os = osName();
if (os == QLatin1String("windows")) {
if (arch == QLatin1String("arm64"))
return QStringLiteral("natives-windows-arm64");
if (arch == QLatin1String("x86"))
return QStringLiteral("natives-windows-x86");
return QStringLiteral("natives-windows");
}
if (os == QLatin1String("osx"))
return arch == QLatin1String("arm64") ? QStringLiteral("natives-macos-arm64")
: QStringLiteral("natives-macos");
return arch == QLatin1String("arm64") ? QStringLiteral("natives-linux-arm64")
: QStringLiteral("natives-linux");
}
bool VersionLoader::rulesAllow(const QJsonArray &rules, const QSet<QString> &features)
{
if (rules.isEmpty())
return true;
bool allowed = false;
for (const QJsonValue &entry : rules) {
const QJsonObject rule = entry.toObject();
if (ruleMatches(rule, features))
allowed = rule.value(QStringLiteral("action")).toString() != QLatin1String("disallow");
}
return allowed;
}
QStringList VersionLoader::installedVersions(const QString &gameDir)
{
QStringList result;
const QDir versions(gameDir + QStringLiteral("/versions"));
const QStringList entries = versions.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
for (const QString &id : entries) {
if (QFile::exists(versionJsonPath(gameDir, id)))
result << id;
}
return result;
}
MinecraftVersion VersionLoader::load(const QString &gameDir,
const QString &versionId,
const QSet<QString> &features,
QString *error)
{
MinecraftVersion version;
// Цепочка от самой производной версии к базовой: Fabric -> 1.20.1.
QList<QJsonObject> chain;
QStringList chainIds;
QString currentId = versionId;
while (!currentId.isEmpty()) {
if (chainIds.contains(currentId)) {
if (error)
*error = QObject::tr("Циклическая зависимость inheritsFrom в версии «%1»").arg(currentId);
return version;
}
if (chain.size() >= kMaxInheritanceDepth) {
if (error)
*error = QObject::tr("Слишком длинная цепочка inheritsFrom у версии «%1»").arg(versionId);
return version;
}
const QString path = versionJsonPath(gameDir, currentId);
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
if (error)
*error = QObject::tr("Не найден файл версии: %1").arg(QDir::toNativeSeparators(path));
return version;
}
QJsonParseError parseError;
const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &parseError);
if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
if (error)
*error = QObject::tr("Повреждён файл версии %1: %2")
.arg(QDir::toNativeSeparators(path), parseError.errorString());
return version;
}
const QJsonObject object = document.object();
chain << object;
chainIds << currentId;
currentId = object.value(QStringLiteral("inheritsFrom")).toString();
}
version.id = versionId;
// Скаляры: идём от базовой версии к производной, наследник перекрывает родителя.
for (int i = chain.size() - 1; i >= 0; --i) {
const QJsonObject &object = chain[i];
if (object.contains(QStringLiteral("mainClass")))
version.mainClass = object.value(QStringLiteral("mainClass")).toString();
if (object.contains(QStringLiteral("type")))
version.type = object.value(QStringLiteral("type")).toString();
if (object.contains(QStringLiteral("assets")))
version.assetsKind = object.value(QStringLiteral("assets")).toString();
if (object.contains(QStringLiteral("assetIndex")))
version.assetIndexId = object.value(QStringLiteral("assetIndex"))
.toObject()
.value(QStringLiteral("id"))
.toString();
if (object.contains(QStringLiteral("javaVersion")))
version.javaMajor = object.value(QStringLiteral("javaVersion"))
.toObject()
.value(QStringLiteral("majorVersion"))
.toInt(version.javaMajor);
const QJsonObject logging = object.value(QStringLiteral("logging"))
.toObject()
.value(QStringLiteral("client"))
.toObject();
if (!logging.isEmpty()) {
version.loggingArgument = logging.value(QStringLiteral("argument")).toString();
const QString fileId = logging.value(QStringLiteral("file"))
.toObject()
.value(QStringLiteral("id"))
.toString();
if (!fileId.isEmpty())
version.loggingConfigPath = gameDir + QStringLiteral("/assets/log_configs/") + fileId;
}
// minecraftArguments (до 1.13) заменяет список целиком, arguments.game — дополняет.
if (object.contains(QStringLiteral("minecraftArguments"))) {
version.gameArgs = object.value(QStringLiteral("minecraftArguments"))
.toString()
.split(QRegularExpression(QStringLiteral("\\s+")),
Qt::SkipEmptyParts);
}
const QJsonObject arguments = object.value(QStringLiteral("arguments")).toObject();
appendArguments(arguments.value(QStringLiteral("jvm")).toArray(), version.jvmArgs, features);
appendArguments(arguments.value(QStringLiteral("game")).toArray(), version.gameArgs, features);
const QByteArray raw = QJsonDocument(arguments.value(QStringLiteral("game")).toArray()).toJson();
if (raw.contains("quickPlayMultiplayer"))
version.supportsQuickPlay = true;
if (raw.contains("resolution_width"))
version.hasCustomResolutionArgs = true;
}
if (version.assetIndexId.isEmpty())
version.assetIndexId = version.assetsKind;
if (version.assetsKind.isEmpty())
version.assetsKind = version.assetIndexId;
if (version.type.isEmpty())
version.type = QStringLiteral("release");
// Библиотеки: наследник первым, дубли по group:artifact:classifier отбрасываем.
QList<MinecraftLibrary> collected;
for (const QJsonObject &object : chain)
parseLibraries(object, collected, features);
QSet<QString> seen;
for (const MinecraftLibrary &library : std::as_const(collected)) {
const QString key = libraryKey(library.name);
if (seen.contains(key))
continue;
seen.insert(key);
version.libraries << library;
}
pickNativesForCurrentArch(version.libraries);
// До 1.13 jvm-аргументов в json нет — собираем их сами.
const bool hasClasspathArg = std::any_of(
version.jvmArgs.cbegin(), version.jvmArgs.cend(), [](const QString &argument) {
return argument == QLatin1String("-cp") || argument == QLatin1String("-classpath");
});
if (!hasClasspathArg) {
version.jvmArgs << QStringLiteral("-Djava.library.path=${natives_directory}")
<< QStringLiteral("-Dminecraft.launcher.brand=${launcher_name}")
<< QStringLiteral("-Dminecraft.launcher.version=${launcher_version}")
<< QStringLiteral("-cp") << QStringLiteral("${classpath}");
}
// Клиентский jar: у Fabric-версии он свой, у Forge обычно только у родителя.
for (const QString &id : std::as_const(chainIds)) {
const QString jar = versionJarPath(gameDir, id);
if (QFile::exists(jar)) {
version.clientJarPath = jar;
break;
}
}
if (version.clientJarPath.isEmpty())
version.clientJarPath = versionJarPath(gameDir, chainIds.constLast());
if (version.mainClass.isEmpty() && error)
*error = QObject::tr("В файле версии «%1» не указан mainClass").arg(versionId);
return version;
}