new builds menu and modloaders implementation
This commit is contained in:
@@ -0,0 +1,520 @@
|
||||
#include "modloaderversionservice.h"
|
||||
|
||||
#include "launcherpaths.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QRegularExpression>
|
||||
#include <QSaveFile>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
#include <QXmlStreamReader>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
// Списки лоадеров меняются реже манифеста Mojang, но шесть часов держим те же:
|
||||
// одна цифра на весь лаунчер понятнее двух разных.
|
||||
constexpr qint64 kCacheTtlSeconds = 6 * 60 * 60;
|
||||
constexpr int kTransferTimeoutMs = 20000;
|
||||
|
||||
const QString kFabricMeta = QStringLiteral("https://meta.fabricmc.net/v2/versions/loader/");
|
||||
const QString kQuiltMeta = QStringLiteral("https://meta.quiltmc.org/v3/versions/loader/");
|
||||
|
||||
const QString kForgeMaven = QStringLiteral("https://maven.minecraftforge.net/net/minecraftforge/forge/");
|
||||
const QString kForgePromotions =
|
||||
QStringLiteral("https://files.minecraftforge.net/maven/net/minecraftforge/forge/promotions_slim.json");
|
||||
|
||||
const QString kNeoForgeMaven = QStringLiteral("https://maven.neoforged.net/releases/net/neoforged/neoforge/");
|
||||
// Под 1.20.1 NeoForge выходил ещё под старым именем артефакта.
|
||||
const QString kNeoForgeLegacyMaven =
|
||||
QStringLiteral("https://maven.neoforged.net/releases/net/neoforged/forge/");
|
||||
|
||||
bool looksUnstable(const QString &version)
|
||||
{
|
||||
const QString lower = version.toLower();
|
||||
return lower.contains(QLatin1String("alpha")) || lower.contains(QLatin1String("beta"))
|
||||
|| lower.contains(QLatin1String("-rc")) || lower.contains(QLatin1String("-pre"))
|
||||
|| lower.contains(u'+');
|
||||
}
|
||||
|
||||
// Сравнение версий по числовым компонентам: «47.4.23» новее «47.4.2», хотя как
|
||||
// строка стоит раньше. Нечисловые хвосты (alpha, +snapshot) сравниваются как есть.
|
||||
bool isNewer(const QString &a, const QString &b)
|
||||
{
|
||||
const QStringList pa = a.split(QRegularExpression(QStringLiteral("[.\\-+]")), Qt::SkipEmptyParts);
|
||||
const QStringList pb = b.split(QRegularExpression(QStringLiteral("[.\\-+]")), Qt::SkipEmptyParts);
|
||||
for (int i = 0; i < std::max(pa.size(), pb.size()); ++i) {
|
||||
const QString sa = i < pa.size() ? pa.at(i) : QString();
|
||||
const QString sb = i < pb.size() ? pb.at(i) : QString();
|
||||
bool okA = false, okB = false;
|
||||
const int na = sa.toInt(&okA);
|
||||
const int nb = sb.toInt(&okB);
|
||||
if (okA && okB) {
|
||||
if (na != nb)
|
||||
return na > nb;
|
||||
continue;
|
||||
}
|
||||
// Число всегда «новее» суффикса: 26.1.0.0 идёт впереди 26.1.0.0-alpha.1.
|
||||
if (okA != okB)
|
||||
return okA;
|
||||
if (sa != sb)
|
||||
return sa > sb;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// «21.1.248» -> «1.21.1», «21.0.167» -> «1.21», «26.1.2.97» -> «26.1.2»,
|
||||
// «26.2.0.68» -> «26.2». До 1.20.2 Minecraft нумеровался как 1.x.y, с 26.1 —
|
||||
// как x.y, поэтому у NeoForge две схемы: три компонента и четыре.
|
||||
QString neoForgeGameVersion(const QString &neoVersion)
|
||||
{
|
||||
const QString head = neoVersion.section(u'-', 0, 0).section(u'+', 0, 0);
|
||||
const QStringList parts = head.split(u'.');
|
||||
if (parts.size() == 3)
|
||||
return parts.at(1) == QLatin1String("0") ? QStringLiteral("1.%1").arg(parts.at(0))
|
||||
: QStringLiteral("1.%1.%2").arg(parts.at(0), parts.at(1));
|
||||
if (parts.size() >= 4)
|
||||
return parts.at(2) == QLatin1String("0")
|
||||
? QStringLiteral("%1.%2").arg(parts.at(0), parts.at(1))
|
||||
: QStringLiteral("%1.%2.%3").arg(parts.at(0), parts.at(1), parts.at(2));
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonObject entryToJson(const LoaderVersionEntry &entry)
|
||||
{
|
||||
return {{QStringLiteral("loaderVersion"), entry.loaderVersion},
|
||||
{QStringLiteral("gameVersion"), entry.gameVersion},
|
||||
{QStringLiteral("versionId"), entry.versionId},
|
||||
{QStringLiteral("installerUrl"), entry.installerUrl.toString()},
|
||||
{QStringLiteral("recommended"), entry.recommended},
|
||||
{QStringLiteral("stable"), entry.stable}};
|
||||
}
|
||||
|
||||
LoaderVersionEntry entryFromJson(const QJsonObject &o)
|
||||
{
|
||||
LoaderVersionEntry entry;
|
||||
entry.loaderVersion = o.value(QStringLiteral("loaderVersion")).toString();
|
||||
entry.gameVersion = o.value(QStringLiteral("gameVersion")).toString();
|
||||
entry.versionId = o.value(QStringLiteral("versionId")).toString();
|
||||
entry.installerUrl = QUrl(o.value(QStringLiteral("installerUrl")).toString());
|
||||
entry.recommended = o.value(QStringLiteral("recommended")).toBool();
|
||||
entry.stable = o.value(QStringLiteral("stable")).toBool(true);
|
||||
return entry;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ModLoaderVersionService::ModLoaderVersionService(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_network(new QNetworkAccessManager(this))
|
||||
{
|
||||
}
|
||||
|
||||
bool ModLoaderVersionService::isPerGameVersion(ModLoader loader)
|
||||
{
|
||||
return loader == ModLoader::Fabric || loader == ModLoader::Quilt;
|
||||
}
|
||||
|
||||
ModLoaderVersionService::Bucket &ModLoaderVersionService::bucket(ModLoader loader)
|
||||
{
|
||||
return m_buckets[static_cast<int>(loader)];
|
||||
}
|
||||
|
||||
const ModLoaderVersionService::Bucket &ModLoaderVersionService::bucket(ModLoader loader) const
|
||||
{
|
||||
static const Bucket empty;
|
||||
const auto it = m_buckets.constFind(static_cast<int>(loader));
|
||||
return it == m_buckets.constEnd() ? empty : it.value();
|
||||
}
|
||||
|
||||
QString ModLoaderVersionService::requestKey(ModLoader loader, const QString &gameVersion)
|
||||
{
|
||||
// У Forge и NeoForge один запрос покрывает все версии игры — они делят
|
||||
// очередь колбэков, чтобы не качать maven-metadata по разу на версию.
|
||||
return isPerGameVersion(loader) ? loaderKey(loader) + u'/' + gameVersion : loaderKey(loader);
|
||||
}
|
||||
|
||||
bool ModLoaderVersionService::isFresh(ModLoader loader, const QString &gameVersion) const
|
||||
{
|
||||
const Bucket &b = bucket(loader);
|
||||
const QDateTime fetched = isPerGameVersion(loader)
|
||||
? b.fetchedAtByGame.value(gameVersion)
|
||||
: b.fetchedAt;
|
||||
return fetched.isValid() && fetched.secsTo(QDateTime::currentDateTimeUtc()) < kCacheTtlSeconds;
|
||||
}
|
||||
|
||||
QList<LoaderVersionEntry> ModLoaderVersionService::versions(ModLoader loader,
|
||||
const QString &gameVersion) const
|
||||
{
|
||||
return bucket(loader).byGame.value(gameVersion);
|
||||
}
|
||||
|
||||
bool ModLoaderVersionService::isRefreshing(ModLoader loader, const QString &gameVersion) const
|
||||
{
|
||||
return m_inFlight.contains(requestKey(loader, gameVersion));
|
||||
}
|
||||
|
||||
std::optional<LoaderVersionEntry> ModLoaderVersionService::find(ModLoader loader,
|
||||
const QString &gameVersion,
|
||||
const QString &loaderVersion) const
|
||||
{
|
||||
const QList<LoaderVersionEntry> list = versions(loader, gameVersion);
|
||||
for (const LoaderVersionEntry &entry : list) {
|
||||
if (entry.loaderVersion == loaderVersion)
|
||||
return entry;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void ModLoaderVersionService::ensureLoaded(ModLoader loader,
|
||||
const QString &gameVersion,
|
||||
Callback callback,
|
||||
bool forceRefresh)
|
||||
{
|
||||
if (gameVersion.trimmed().isEmpty()) {
|
||||
if (callback)
|
||||
QTimer::singleShot(0, this, [callback = std::move(callback)] { callback(true, {}); });
|
||||
return;
|
||||
}
|
||||
|
||||
Bucket &b = bucket(loader);
|
||||
if (!b.cacheRead) {
|
||||
b.cacheRead = true;
|
||||
if (readCache(loader))
|
||||
emit versionsChanged(loaderKey(loader), gameVersion);
|
||||
}
|
||||
|
||||
if (!forceRefresh && isFresh(loader, gameVersion)) {
|
||||
// Колбэк, который то синхронный, то нет, ломает вызывающий код —
|
||||
// на попадании в кэш отвечаем через событийный цикл.
|
||||
if (callback)
|
||||
QTimer::singleShot(0, this, [callback = std::move(callback)] { callback(true, {}); });
|
||||
return;
|
||||
}
|
||||
|
||||
const QString key = requestKey(loader, gameVersion);
|
||||
if (callback)
|
||||
m_pending[key] << std::move(callback);
|
||||
if (m_inFlight.contains(key))
|
||||
return;
|
||||
startRefresh(loader, gameVersion);
|
||||
}
|
||||
|
||||
void ModLoaderVersionService::startRefresh(ModLoader loader, const QString &gameVersion)
|
||||
{
|
||||
const QString key = requestKey(loader, gameVersion);
|
||||
m_inFlight.insert(key);
|
||||
emit refreshingChanged();
|
||||
|
||||
const auto request = [](const QUrl &url) {
|
||||
QNetworkRequest req{url};
|
||||
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
|
||||
QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
req.setTransferTimeout(kTransferTimeoutMs);
|
||||
return req;
|
||||
};
|
||||
|
||||
if (isPerGameVersion(loader)) {
|
||||
const QString base = loader == ModLoader::Fabric ? kFabricMeta : kQuiltMeta;
|
||||
QNetworkReply *reply = m_network->get(request(QUrl(base + gameVersion)));
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, loader, gameVersion] {
|
||||
reply->deleteLater();
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
finishRefresh(loader, gameVersion, false,
|
||||
tr("Не удалось получить список версий %1: %2")
|
||||
.arg(loaderTitle(loader), reply->errorString()));
|
||||
return;
|
||||
}
|
||||
|
||||
const QByteArray payload = reply->readAll();
|
||||
// Пустой список — законный ответ: этот лоадер не поддерживает такую
|
||||
// версию игры. Отличаем его от испорченного ответа по самому JSON.
|
||||
const QJsonDocument document = QJsonDocument::fromJson(payload);
|
||||
if (!document.isArray()) {
|
||||
finishRefresh(loader, gameVersion, false,
|
||||
tr("Список версий %1 не удалось разобрать").arg(loaderTitle(loader)));
|
||||
return;
|
||||
}
|
||||
|
||||
Bucket &b = bucket(loader);
|
||||
b.byGame.insert(gameVersion, parseFabricLike(payload, loader, gameVersion));
|
||||
b.fetchedAtByGame.insert(gameVersion, QDateTime::currentDateTimeUtc());
|
||||
writeCache(loader);
|
||||
emit versionsChanged(loaderKey(loader), gameVersion);
|
||||
finishRefresh(loader, gameVersion, true, {});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Forge и NeoForge: основной maven-metadata плюс вспомогательный запрос
|
||||
// (рекомендованные сборки у Forge, старая схема 1.20.1 у NeoForge).
|
||||
// Считаем оба и сводим результат, когда придёт последний.
|
||||
struct Pending
|
||||
{
|
||||
int outstanding = 2;
|
||||
QHash<QString, QList<LoaderVersionEntry>> merged;
|
||||
QHash<QString, QString> recommended; // версия игры -> версия лоадера
|
||||
bool primaryOk = false;
|
||||
QString warning;
|
||||
};
|
||||
auto state = std::make_shared<Pending>();
|
||||
|
||||
const auto settle = [this, state, loader, gameVersion] {
|
||||
if (--state->outstanding > 0)
|
||||
return;
|
||||
if (!state->primaryOk) {
|
||||
finishRefresh(loader, gameVersion, false, state->warning);
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto it = state->merged.begin(); it != state->merged.end(); ++it) {
|
||||
const QString promoted = state->recommended.value(it.key());
|
||||
if (promoted.isEmpty())
|
||||
continue;
|
||||
for (LoaderVersionEntry &entry : it.value())
|
||||
entry.recommended = entry.loaderVersion == promoted;
|
||||
}
|
||||
|
||||
Bucket &b = bucket(loader);
|
||||
b.byGame = state->merged;
|
||||
b.fetchedAt = QDateTime::currentDateTimeUtc();
|
||||
writeCache(loader);
|
||||
emit versionsChanged(loaderKey(loader), gameVersion);
|
||||
finishRefresh(loader, gameVersion, true, {});
|
||||
};
|
||||
|
||||
const QString primaryUrl = loader == ModLoader::Forge ? kForgeMaven : kNeoForgeMaven;
|
||||
QNetworkReply *primary = m_network->get(request(QUrl(primaryUrl + QStringLiteral("maven-metadata.xml"))));
|
||||
connect(primary, &QNetworkReply::finished, this, [this, primary, state, loader, settle] {
|
||||
primary->deleteLater();
|
||||
if (primary->error() != QNetworkReply::NoError) {
|
||||
state->warning = tr("Не удалось получить список версий %1: %2")
|
||||
.arg(loaderTitle(loader), primary->errorString());
|
||||
settle();
|
||||
return;
|
||||
}
|
||||
auto parsed = parseMavenMetadata(primary->readAll(), loader, /*legacyNeoForge=*/false);
|
||||
if (parsed.isEmpty()) {
|
||||
state->warning = tr("Список версий %1 не удалось разобрать").arg(loaderTitle(loader));
|
||||
settle();
|
||||
return;
|
||||
}
|
||||
state->primaryOk = true;
|
||||
state->merged = std::move(parsed);
|
||||
settle();
|
||||
});
|
||||
|
||||
if (loader == ModLoader::Forge) {
|
||||
QNetworkReply *promos = m_network->get(request(QUrl(kForgePromotions)));
|
||||
connect(promos, &QNetworkReply::finished, this, [promos, state, settle] {
|
||||
promos->deleteLater();
|
||||
if (promos->error() == QNetworkReply::NoError) {
|
||||
const QJsonObject root = QJsonDocument::fromJson(promos->readAll()).object();
|
||||
const QJsonObject promoted = root.value(QStringLiteral("promos")).toObject();
|
||||
for (auto it = promoted.constBegin(); it != promoted.constEnd(); ++it) {
|
||||
if (!it.key().endsWith(QLatin1String("-recommended")))
|
||||
continue;
|
||||
state->recommended.insert(it.key().chopped(int(sizeof("-recommended") - 1)),
|
||||
it.value().toString());
|
||||
}
|
||||
}
|
||||
// Метка «рекомендовано» необязательна: без неё список всё равно рабочий.
|
||||
settle();
|
||||
});
|
||||
} else {
|
||||
QNetworkReply *legacy =
|
||||
m_network->get(request(QUrl(kNeoForgeLegacyMaven + QStringLiteral("maven-metadata.xml"))));
|
||||
connect(legacy, &QNetworkReply::finished, this, [legacy, state, settle] {
|
||||
legacy->deleteLater();
|
||||
if (legacy->error() == QNetworkReply::NoError) {
|
||||
const auto extra = parseMavenMetadata(legacy->readAll(), ModLoader::NeoForge,
|
||||
/*legacyNeoForge=*/true);
|
||||
for (auto it = extra.constBegin(); it != extra.constEnd(); ++it)
|
||||
state->merged[it.key()] += it.value();
|
||||
}
|
||||
settle();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ModLoaderVersionService::finishRefresh(ModLoader loader,
|
||||
const QString &gameVersion,
|
||||
bool ok,
|
||||
const QString &warning)
|
||||
{
|
||||
const QString key = requestKey(loader, gameVersion);
|
||||
m_inFlight.remove(key);
|
||||
emit refreshingChanged();
|
||||
|
||||
// Колбэк может снова позвать ensureLoaded — список забираем себе заранее.
|
||||
const QList<Callback> pending = m_pending.take(key);
|
||||
const bool haveData = !bucket(loader).byGame.isEmpty();
|
||||
for (const Callback &callback : pending) {
|
||||
// Сеть недоступна, но старые данные есть — работаем на них.
|
||||
if (!ok && haveData)
|
||||
callback(true, warning);
|
||||
else
|
||||
callback(ok, warning);
|
||||
}
|
||||
}
|
||||
|
||||
QList<LoaderVersionEntry> ModLoaderVersionService::parseFabricLike(const QByteArray &payload,
|
||||
ModLoader loader,
|
||||
const QString &gameVersion)
|
||||
{
|
||||
const QString prefix = loader == ModLoader::Fabric ? QStringLiteral("fabric-loader-")
|
||||
: QStringLiteral("quilt-loader-");
|
||||
QList<LoaderVersionEntry> list;
|
||||
const QJsonArray array = QJsonDocument::fromJson(payload).array();
|
||||
list.reserve(array.size());
|
||||
|
||||
for (const QJsonValue &value : array) {
|
||||
const QJsonObject object = value.toObject().value(QStringLiteral("loader")).toObject();
|
||||
LoaderVersionEntry entry;
|
||||
entry.loaderVersion = object.value(QStringLiteral("version")).toString();
|
||||
if (entry.loaderVersion.isEmpty())
|
||||
continue;
|
||||
entry.gameVersion = gameVersion;
|
||||
entry.versionId = prefix + entry.loaderVersion + u'-' + gameVersion;
|
||||
entry.stable = object.value(QStringLiteral("stable")).toBool(!looksUnstable(entry.loaderVersion));
|
||||
list << entry;
|
||||
}
|
||||
|
||||
// Fabric и Quilt отдают новые сверху, но зависеть от этого не стоит.
|
||||
std::stable_sort(list.begin(), list.end(),
|
||||
[](const LoaderVersionEntry &a, const LoaderVersionEntry &b) {
|
||||
return isNewer(a.loaderVersion, b.loaderVersion);
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
QHash<QString, QList<LoaderVersionEntry>> ModLoaderVersionService::parseMavenMetadata(
|
||||
const QByteArray &xml, ModLoader loader, bool legacyNeoForge)
|
||||
{
|
||||
// Версия игры зашита в саму строку версии лоадера, поэтому раскладка по
|
||||
// версиям игры — это и есть проверка совместимости: несовместимой пары в
|
||||
// получившемся отображении просто не существует.
|
||||
const bool splitOnDash = loader == ModLoader::Forge || legacyNeoForge;
|
||||
const QString base = loader == ModLoader::Forge ? kForgeMaven
|
||||
: legacyNeoForge ? kNeoForgeLegacyMaven
|
||||
: kNeoForgeMaven;
|
||||
const QString artifact = loader == ModLoader::Forge ? QStringLiteral("forge")
|
||||
: legacyNeoForge ? QStringLiteral("forge")
|
||||
: QStringLiteral("neoforge");
|
||||
|
||||
QHash<QString, QList<LoaderVersionEntry>> byGame;
|
||||
QXmlStreamReader reader(xml);
|
||||
while (!reader.atEnd()) {
|
||||
if (reader.readNext() != QXmlStreamReader::StartElement)
|
||||
continue;
|
||||
if (reader.name() != QLatin1String("version"))
|
||||
continue;
|
||||
|
||||
const QString full = reader.readElementText().trimmed();
|
||||
if (full.isEmpty())
|
||||
continue;
|
||||
|
||||
LoaderVersionEntry entry;
|
||||
if (splitOnDash) {
|
||||
const int dash = full.indexOf(u'-');
|
||||
if (dash <= 0 || dash + 1 >= full.size())
|
||||
continue;
|
||||
entry.gameVersion = full.left(dash);
|
||||
entry.loaderVersion = full.mid(dash + 1);
|
||||
} else {
|
||||
entry.gameVersion = neoForgeGameVersion(full);
|
||||
entry.loaderVersion = full;
|
||||
}
|
||||
if (entry.gameVersion.isEmpty())
|
||||
continue;
|
||||
|
||||
entry.stable = !looksUnstable(full);
|
||||
entry.installerUrl = QUrl(base + full + u'/' + artifact + u'-' + full
|
||||
+ QStringLiteral("-installer.jar"));
|
||||
byGame[entry.gameVersion] << entry;
|
||||
}
|
||||
if (reader.hasError())
|
||||
return {};
|
||||
|
||||
// maven-metadata приходит неотсортированным: 1.20.1-47.4.23 стоит между
|
||||
// версиями 26.x. Без сортировки «самой новой» оказалась бы случайная сборка.
|
||||
for (auto it = byGame.begin(); it != byGame.end(); ++it) {
|
||||
std::stable_sort(it.value().begin(), it.value().end(),
|
||||
[](const LoaderVersionEntry &a, const LoaderVersionEntry &b) {
|
||||
return isNewer(a.loaderVersion, b.loaderVersion);
|
||||
});
|
||||
}
|
||||
return byGame;
|
||||
}
|
||||
|
||||
bool ModLoaderVersionService::readCache(ModLoader loader)
|
||||
{
|
||||
QFile file(LauncherPaths::loaderCacheFile(loaderKey(loader)));
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return false;
|
||||
|
||||
const QJsonObject root = QJsonDocument::fromJson(file.readAll()).object();
|
||||
const QJsonObject games = root.value(QStringLiteral("games")).toObject();
|
||||
if (games.isEmpty())
|
||||
return false;
|
||||
|
||||
Bucket &b = bucket(loader);
|
||||
for (auto it = games.constBegin(); it != games.constEnd(); ++it) {
|
||||
const QJsonObject game = it.value().toObject();
|
||||
QList<LoaderVersionEntry> entries;
|
||||
const QJsonArray array = game.value(QStringLiteral("entries")).toArray();
|
||||
entries.reserve(array.size());
|
||||
for (const QJsonValue &value : array)
|
||||
entries << entryFromJson(value.toObject());
|
||||
b.byGame.insert(it.key(), entries);
|
||||
const QDateTime fetched =
|
||||
QDateTime::fromString(game.value(QStringLiteral("fetchedAt")).toString(), Qt::ISODate);
|
||||
if (fetched.isValid())
|
||||
b.fetchedAtByGame.insert(it.key(), fetched);
|
||||
}
|
||||
b.fetchedAt = QDateTime::fromString(root.value(QStringLiteral("fetchedAt")).toString(),
|
||||
Qt::ISODate);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ModLoaderVersionService::writeCache(ModLoader loader)
|
||||
{
|
||||
// Папку кэша могли удалить уже после запуска лаунчера.
|
||||
if (!LauncherPaths::ensureRootExists())
|
||||
return;
|
||||
|
||||
const Bucket &b = bucket(loader);
|
||||
QJsonObject games;
|
||||
for (auto it = b.byGame.constBegin(); it != b.byGame.constEnd(); ++it) {
|
||||
QJsonArray entries;
|
||||
for (const LoaderVersionEntry &entry : it.value())
|
||||
entries << entryToJson(entry);
|
||||
games.insert(it.key(),
|
||||
QJsonObject{{QStringLiteral("fetchedAt"),
|
||||
b.fetchedAtByGame.value(it.key()).toString(Qt::ISODate)},
|
||||
{QStringLiteral("entries"), entries}});
|
||||
}
|
||||
|
||||
const QJsonObject root{{QStringLiteral("fetchedAt"), b.fetchedAt.toString(Qt::ISODate)},
|
||||
{QStringLiteral("games"), games}};
|
||||
|
||||
// QSaveFile: оборванная запись не оставит обрезанный кэш, который потом
|
||||
// пришлось бы отличать от настоящего.
|
||||
QSaveFile file(LauncherPaths::loaderCacheFile(loaderKey(loader)));
|
||||
if (!file.open(QIODevice::WriteOnly))
|
||||
return;
|
||||
const QByteArray payload = QJsonDocument(root).toJson(QJsonDocument::Compact);
|
||||
if (file.write(payload) != payload.size()) {
|
||||
file.cancelWriting();
|
||||
return;
|
||||
}
|
||||
file.commit();
|
||||
}
|
||||
Reference in New Issue
Block a user