Files
BiLauncher/minecraftversion.cpp
T

477 lines
19 KiB
C++
Raw 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 "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;
}