#include "minecraftversion.h" #include #include #include #include #include #include #include #include #include #include #include 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 &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 &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(download.value(QStringLiteral("size")).toDouble()); return library; } void parseLibraries(const QJsonObject &root, QList &target, const QSet &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 &libraries) { const QString exact = VersionLoader::nativeClassifier(); const QString osPart = VersionLoader::osName() == QStringLiteral("osx") ? QStringLiteral("macos") : VersionLoader::osName(); const QString base = QStringLiteral("natives-") + osPart; QHash 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 &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 &features, QString *error) { MinecraftVersion version; // Цепочка от самой производной версии к базовой: Fabric -> 1.20.1. QList 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 collected; for (const QJsonObject &object : chain) parseLibraries(object, collected, features); QSet 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; }