new builds menu and modloaders implementation

This commit is contained in:
2026-08-27 09:21:51 +03:00
parent 5bc59f8018
commit 168884127b
23 changed files with 3979 additions and 1081 deletions
+492 -88
View File
@@ -6,6 +6,9 @@
#include "launcherpaths.h"
#include "versioninstaller.h"
#include "versionmanifestservice.h"
#include "modloaderinstaller.h"
#include "modloaderversionservice.h"
#include "buildswitcher.h"
#include <QDesktopServices>
#include <QDir>
@@ -41,6 +44,9 @@ LauncherBackend::LauncherBackend(QObject *parent)
, m_launcher(new GameLauncher(this))
, m_manifest(new VersionManifestService(this))
, m_installer(new VersionInstaller(m_manifest, this))
, m_loaderMeta(new ModLoaderVersionService(this))
, m_loaderInstaller(new ModLoaderInstaller(m_loaderMeta, m_installer, this))
, m_switcher(new BuildSwitcher(this))
{
// Папка лаунчера создаётся при первом запуске и проверяется при каждом
// следующем — если её удалили, она появится снова.
@@ -49,9 +55,22 @@ LauncherBackend::LauncherBackend(QObject *parent)
m_storageIssues << tr("Не удалось создать папку лаунчера: %1").arg(storageError);
loadSettings();
// Порядок важен: миграция должна успеть до того, как ensureDataFiles()
// создаст пустой customBuilds.json поверх старых данных.
migrateLegacyBuildsFile();
loadData();
ensureDataFiles();
// Прошлое переключение оборвалось на середине. Доигрываем его сами, а не
// предлагаем пользователю переключиться заново: архив прежней сборки уже
// записан целиком, а вот повторное переключение сначала заархивировало бы
// поверх него нынешнее — наполовину разобранное — содержимое .minecraft.
const QString interrupted = m_switcher->interruptedSwitchWarning();
if (!interrupted.isEmpty()) {
m_storageIssues << interrupted;
QTimer::singleShot(0, this, [this] { m_switcher->resumeInterrupted(gameDir()); });
}
connect(m_auth, &AuthService::progress, this, &LauncherBackend::launchProgress);
connect(m_launcher, &GameLauncher::progress, this, &LauncherBackend::launchProgress);
connect(m_launcher, &GameLauncher::output, this, &LauncherBackend::gameOutput);
@@ -94,6 +113,88 @@ LauncherBackend::LauncherBackend(QObject *parent)
emit busyChanged();
emit launchError(tr("Не удалось загрузить версию «%1»: %2").arg(id, message));
});
connect(m_switcher, &BuildSwitcher::progressChanged, this, [this] {
emit switchChanged();
emit busyChanged();
});
connect(m_switcher, &BuildSwitcher::finished, this, [this](int toBuildId) {
// Активной сборка становится только здесь: провалившееся переключение
// оставляет активной прежнюю.
m_settings.insert(QStringLiteral("activeBuildId"), toBuildId);
saveSettings();
invalidateCatalog();
emit activeBuildChanged();
emit installedVersionsChanged();
emit switchChanged();
emit busyChanged();
const int index = indexOfBuildId(toBuildId);
emit launchProgress(tr("Активна сборка «%1»")
.arg(index >= 0 ? m_builds[index].name : QString()));
});
connect(m_switcher, &BuildSwitcher::failed, this,
[this](int, const QString &message, bool gameDirIntact) {
invalidateCatalog();
emit installedVersionsChanged();
emit switchChanged();
emit busyChanged();
emit launchError(gameDirIntact
? tr("Сборка не переключена: %1").arg(message)
: tr("Сборка не переключена: %1\nСодержимое .minecraft "
"осталось разобранным — повторите переключение.")
.arg(message));
});
connect(m_loaderMeta, &ModLoaderVersionService::versionsChanged,
this, &LauncherBackend::loaderVersionsChanged);
connect(m_loaderMeta, &ModLoaderVersionService::refreshingChanged,
this, &LauncherBackend::downloadChanged);
// Установщик лоадера делит панель прогресса и флаг занятости с обычным
// установщиком версий — сигналы у них одни и те же.
connect(m_loaderInstaller, &ModLoaderInstaller::started, this, [this](const QString &label) {
emit busyChanged();
emit downloadChanged();
emit launchProgress(tr("Установка %1…").arg(label));
});
connect(m_loaderInstaller, &ModLoaderInstaller::progressChanged,
this, &LauncherBackend::downloadChanged);
connect(m_loaderInstaller, &ModLoaderInstaller::log, this, &LauncherBackend::gameOutput);
connect(m_loaderInstaller, &ModLoaderInstaller::finished, this,
[this](const QString &loader, const QString &gameVersion,
const QString &loaderVersion, const QString &producedVersionId) {
const int index = indexOfBuildId(m_loaderInstallBuildId);
m_loaderInstallBuildId = -1;
if (index >= 0) {
updateCustomBuild(index,
{{QStringLiteral("loader"), loader},
{QStringLiteral("loaderVersion"), loaderVersion},
{QStringLiteral("resolvedVersionId"), producedVersionId}});
}
invalidateCatalog();
emit installedVersionsChanged();
emit busyChanged();
emit downloadChanged();
emit launchProgress(tr("Готово: %1 для Minecraft %2")
.arg(producedVersionId, gameVersion));
});
connect(m_loaderInstaller, &ModLoaderInstaller::failed, this,
[this](const QString &label, const QString &message) {
m_loaderInstallBuildId = -1;
invalidateCatalog();
emit installedVersionsChanged();
emit busyChanged();
emit downloadChanged();
emit launchError(tr("%1: %2").arg(label, message));
});
connect(m_loaderInstaller, &ModLoaderInstaller::canceled, this, [this](const QString &label) {
m_loaderInstallBuildId = -1;
invalidateCatalog();
emit installedVersionsChanged();
emit busyChanged();
emit downloadChanged();
emit launchProgress(tr("Установка %1 отменена").arg(label));
});
connect(m_installer, &VersionInstaller::canceled, this, [this](const QString &id) {
invalidateCatalog();
emit installedVersionsChanged();
@@ -120,11 +221,11 @@ QStringList LauncherBackend::profileNames() const
return names;
}
QStringList LauncherBackend::versionNames() const
QStringList LauncherBackend::customBuildNames() const
{
QStringList names;
for (const auto &v : m_versions)
names << v.name;
for (const auto &b : m_builds)
names << b.name;
return names;
}
@@ -155,12 +256,19 @@ void LauncherBackend::addProfile(const QString &name,
emit profilesChanged();
}
void LauncherBackend::addVersion(const QString &name, const QString &serverUrl, const QString &versionId)
void LauncherBackend::addCustomBuild(const QString &name,
const QString &serverUrl,
const QString &minecraftVersion)
{
m_versions.append({m_nextVersionId++, name, serverUrl, versionId});
saveVersions();
emit versionsChanged();
maybeInstall(versionIdOf(m_versions.last()));
CustomBuild build;
build.id = m_nextBuildId++;
build.name = name;
build.serverUrl = serverUrl;
build.minecraftVersion = minecraftVersion;
m_builds.append(build);
saveCustomBuilds();
emit customBuildsChanged();
maybeInstall(launchVersionIdOf(m_builds.last()));
}
void LauncherBackend::updateProfile(int index,
@@ -190,19 +298,43 @@ void LauncherBackend::updateProfile(int index,
emit profilesChanged();
}
void LauncherBackend::updateVersion(int index,
const QString &name,
const QString &serverUrl,
const QString &versionId)
void LauncherBackend::updateCustomBuild(int index, const QVariantMap &fields)
{
if (index < 0 || index >= m_versions.size())
if (index < 0 || index >= m_builds.size())
return;
m_versions[index].name = name;
m_versions[index].serverUrl = serverUrl;
m_versions[index].versionId = versionId;
saveVersions();
emit versionsChanged();
maybeInstall(versionIdOf(m_versions[index]));
CustomBuild &build = m_builds[index];
const auto take = [&fields](const char *key, QString &target) {
const auto it = fields.constFind(QLatin1String(key));
if (it == fields.constEnd())
return false;
target = it->toString();
return true;
};
// Сменилась версия игры — выбранная сборка модлоадера к ней уже не подходит.
// Сам лоадер оставляем: чекбокс не должен слетать, он просто перевыберет версию.
const QString previousMinecraft = build.minecraftVersion;
take("name", build.name);
take("serverUrl", build.serverUrl);
take("minecraftVersion", build.minecraftVersion);
take("loader", build.loader);
take("loaderVersion", build.loaderVersion);
take("resolvedVersionId", build.resolvedVersionId);
if (build.minecraftVersion != previousMinecraft
&& !fields.contains(QStringLiteral("loaderVersion"))) {
build.loaderVersion.clear();
build.resolvedVersionId.clear();
}
saveCustomBuilds();
emit customBuildsChanged();
emit activeBuildChanged(); // имя активной сборки могло измениться
// maybeInstall() здесь намеренно нет: карточка сборки сохраняется по ходу
// правки, и загрузка стартовала бы почти на каждое действие. Для этого есть
// installCustomBuild().
}
QVariantMap LauncherBackend::profileAt(int index) const
@@ -216,12 +348,18 @@ QVariantMap LauncherBackend::profileAt(int index) const
{"authType", p.authType}};
}
QVariantMap LauncherBackend::versionAt(int index) const
QVariantMap LauncherBackend::customBuildAt(int index) const
{
if (index < 0 || index >= m_versions.size())
if (index < 0 || index >= m_builds.size())
return {};
const auto &v = m_versions[index];
return {{"name", v.name}, {"serverUrl", v.serverUrl}, {"versionId", versionIdOf(v)}};
const auto &b = m_builds[index];
return {{"name", b.name},
{"serverUrl", b.serverUrl},
{"minecraftVersion", b.minecraftVersion},
{"loader", b.loader},
{"loaderVersion", b.loaderVersion},
{"resolvedVersionId", b.resolvedVersionId},
{"launchVersionId", launchVersionIdOf(b)}};
}
void LauncherBackend::removeProfile(int index)
@@ -233,37 +371,105 @@ void LauncherBackend::removeProfile(int index)
emit profilesChanged();
}
void LauncherBackend::removeVersion(int index)
void LauncherBackend::removeCustomBuild(int index)
{
if (index < 0 || index >= m_versions.size())
if (index < 0 || index >= m_builds.size())
return;
m_versions.removeAt(index);
saveVersions();
emit versionsChanged();
m_builds.removeAt(index);
saveCustomBuilds();
emit customBuildsChanged();
emit activeBuildChanged();
}
QString LauncherBackend::versionIdOf(const Version &version) const
int LauncherBackend::indexOfBuildId(int buildId) const
{
// Пользователь мог назвать запись как угодно — тогда версией считаем само имя.
return version.versionId.isEmpty() ? version.name : version.versionId;
for (int i = 0; i < m_builds.size(); ++i) {
if (m_builds[i].id == buildId)
return i;
}
return -1;
}
QStringList LauncherBackend::checkInstallation(int versionIndex) const
int LauncherBackend::activeBuildIndex() const
{
if (versionIndex < 0 || versionIndex >= m_versions.size())
return {tr("Версия не выбрана")};
if (m_builds.isEmpty())
return -1;
const int index = indexOfBuildId(m_settings.value(QStringLiteral("activeBuildId")).toInt());
// Настройка могла указывать на удалённую сборку — тогда активна первая.
return index >= 0 ? index : 0;
}
const Version &entry = m_versions[versionIndex];
const QString id = versionIdOf(entry);
void LauncherBackend::setActiveBuildIndex(int index)
{
const int current = activeBuildIndex();
if (index < 0 || index >= m_builds.size() || index == current)
return;
if (m_switcher->isRunning()) {
emit launchError(tr("Смена сборки уже выполняется"));
return;
}
if (m_launcher->isRunning()) {
emit launchError(tr("Сначала закройте игру — её файлы сейчас переносятся"));
return;
}
if (m_busy || m_installer->isRunning() || m_loaderInstaller->isRunning()) {
emit launchError(tr("Дождитесь окончания текущей операции"));
return;
}
// Содержимое .minecraft принадлежит прежней сборке: сначала оно уезжает в
// её архив, и только потом на его место разворачивается выбранная.
const CustomBuild &target = m_builds[index];
emit launchProgress(tr("Смена сборки на «%1»…").arg(target.name));
m_switcher->switchTo(current >= 0 ? m_builds[current].id : -1,
current >= 0 ? m_builds[current].name : QString(),
target.id,
target.name,
gameDir());
emit switchChanged();
emit busyChanged();
}
QString LauncherBackend::activeBuildName() const
{
const int index = activeBuildIndex();
return index < 0 ? QString() : m_builds[index].name;
}
void LauncherBackend::installCustomBuild(int index)
{
// Установка сборки — это установка её версии игры и, если выбран,
// модлоадера: они ставятся одной цепочкой.
installLoaderForBuild(index);
}
QString LauncherBackend::launchVersionIdOf(const CustomBuild &build) const
{
// resolvedVersionId появляется после установки модлоадера. Пользователь мог
// назвать сборку как угодно — тогда версией считаем само имя: так работали
// записи, созданные до появления каталога версий.
if (!build.resolvedVersionId.isEmpty())
return build.resolvedVersionId;
return build.minecraftVersion.isEmpty() ? build.name : build.minecraftVersion;
}
QStringList LauncherBackend::checkInstallation(int buildIndex) const
{
if (buildIndex < 0 || buildIndex >= m_builds.size())
return {tr("Сборка не выбрана")};
const CustomBuild &entry = m_builds[buildIndex];
const QString id = launchVersionIdOf(entry);
if (id.trimmed().isEmpty())
return {tr("У записи лаунчера не указан ID версии. Проверьте %1")
.arg(QDir::toNativeSeparators(LauncherPaths::versionsFile()))};
return {tr("У сборки не выбрана версия Minecraft. Проверьте %1")
.arg(QDir::toNativeSeparators(LauncherPaths::customBuildsFile()))};
QString error;
const MinecraftVersion version = VersionLoader::load(gameDir(), id, {}, &error);
if (!version.isValid()) {
return {error.isEmpty()
? tr("Версия «%1» из записи «%2» не установлена в %3")
? tr("Версия «%1» из сборки «%2» не установлена в %3")
.arg(id,
entry.name,
QDir::toNativeSeparators(gameDir() + QStringLiteral("/versions")))
@@ -416,28 +622,43 @@ void LauncherBackend::installVersion(const QString &versionId)
void LauncherBackend::cancelDownload()
{
m_installer->cancel();
// Установщик лоадера сам остановит вложенную установку версии.
if (m_loaderInstaller->isRunning())
m_loaderInstaller->cancel();
else
m_installer->cancel();
}
// Панель загрузки одна на два установщика: пока идёт установка модлоадера, она
// показывает его стадии, включая вложенную загрузку файлов версии.
bool LauncherBackend::downloading() const
{
return m_installer->isRunning();
return m_loaderInstaller->isRunning() || m_installer->isRunning();
}
double LauncherBackend::downloadProgress() const
{
return m_installer->fraction();
if (!m_loaderInstaller->isRunning())
return m_installer->fraction();
// Внутри установки лоадера файлы качает VersionInstaller — его доля точнее.
return m_installer->isRunning() ? m_installer->fraction() : m_loaderInstaller->fraction();
}
QString LauncherBackend::downloadVersion() const
{
if (m_loaderInstaller->isRunning())
return m_loaderInstaller->label();
return m_installer->versionId();
}
QString LauncherBackend::downloadStatus() const
{
const QString stage = m_installer->stage();
const QString file = m_installer->currentFile();
QString stage = m_installer->stage();
QString file = m_installer->currentFile();
if (m_loaderInstaller->isRunning() && !m_installer->isRunning()) {
stage = m_loaderInstaller->stage();
file = m_loaderInstaller->currentFile();
}
if (stage.isEmpty())
return file;
if (file.isEmpty())
@@ -447,16 +668,111 @@ QString LauncherBackend::downloadStatus() const
qint64 LauncherBackend::downloadBytesDone() const
{
if (m_loaderInstaller->isRunning() && !m_installer->isRunning())
return m_loaderInstaller->bytesDone();
return m_installer->bytesDone();
}
qint64 LauncherBackend::downloadBytesTotal() const
{
if (m_loaderInstaller->isRunning() && !m_installer->isRunning())
return m_loaderInstaller->bytesTotal();
return m_installer->bytesTotal();
}
void LauncherBackend::launchGame(int profileIndex, int versionIndex)
// ── Модлоадеры ─────────────────────────────────────────────────────────────
QVariantList LauncherBackend::loaderVersions(const QString &loaderKey,
const QString &gameVersion) const
{
const auto loader = loaderFromKey(loaderKey);
if (!loader)
return {};
QVariantList result;
const QList<LoaderVersionEntry> entries = m_loaderMeta->versions(*loader, gameVersion);
result.reserve(entries.size());
for (const LoaderVersionEntry &entry : entries) {
QString label = entry.loaderVersion;
if (entry.recommended)
label += tr(" — рекомендуемая");
else if (!entry.stable)
label += tr(" — нестабильная");
result.append(QVariantMap{{QStringLiteral("version"), entry.loaderVersion},
{QStringLiteral("label"), label},
{QStringLiteral("recommended"), entry.recommended},
{QStringLiteral("stable"), entry.stable}});
}
return result;
}
void LauncherBackend::refreshLoaderVersions(const QString &loaderKey,
const QString &gameVersion,
bool force)
{
const auto loader = loaderFromKey(loaderKey);
if (!loader)
return;
m_loaderMeta->ensureLoaded(*loader, gameVersion, [this, loaderKey, gameVersion](
bool ok, const QString &warning) {
if (!ok && !warning.isEmpty())
emit launchError(warning);
// Кэш мог ответить сразу, без сигнала versionsChanged.
emit loaderVersionsChanged(loaderKey, gameVersion);
}, force);
}
bool LauncherBackend::loaderVersionsLoading(const QString &loaderKey,
const QString &gameVersion) const
{
const auto loader = loaderFromKey(loaderKey);
return loader && m_loaderMeta->isRefreshing(*loader, gameVersion);
}
void LauncherBackend::installLoaderForBuild(int index)
{
if (index < 0 || index >= m_builds.size())
return;
if (busy()) {
emit launchError(tr("Дождитесь окончания текущей загрузки"));
return;
}
const CustomBuild &build = m_builds[index];
if (build.minecraftVersion.trimmed().isEmpty()) {
emit launchError(tr("Сначала выберите версию Minecraft для сборки «%1»").arg(build.name));
return;
}
// Лоадер не выбран — сборка ванильная, ставим саму версию игры.
const auto loader = loaderFromKey(build.loader);
if (!loader) {
if (build.resolvedVersionId != build.minecraftVersion)
updateCustomBuild(index, {{QStringLiteral("resolvedVersionId"), QString()}});
maybeInstall(build.minecraftVersion);
return;
}
if (build.loaderVersion.trimmed().isEmpty()) {
emit launchError(tr("Выберите версию %1 для сборки «%2»")
.arg(loaderTitle(*loader), build.name));
return;
}
m_loaderInstallBuildId = build.id;
m_loaderInstaller->install(gameDir(),
*loader,
build.minecraftVersion,
build.loaderVersion,
m_settings.value(QStringLiteral("javaPath")).toString());
}
void LauncherBackend::launchGame(int profileIndex, int buildIndex)
{
if (m_loaderInstaller->isRunning()) {
emit launchError(tr("Идёт установка «%1» — дождитесь окончания")
.arg(m_loaderInstaller->label()));
return;
}
if (m_installer->isRunning()) {
emit launchError(tr("Идёт загрузка версии «%1» — дождитесь окончания")
.arg(m_installer->versionId()));
@@ -474,8 +790,8 @@ void LauncherBackend::launchGame(int profileIndex, int versionIndex)
emit launchError(tr("Выберите профиль перед запуском"));
return;
}
if (versionIndex < 0 || versionIndex >= m_versions.size()) {
emit launchError(tr("Выберите версию перед запуском"));
if (buildIndex < 0 || buildIndex >= m_builds.size()) {
emit launchError(tr("Выберите сборку перед запуском"));
return;
}
@@ -488,11 +804,12 @@ void LauncherBackend::launchGame(int profileIndex, int versionIndex)
return;
}
const Version &entry = m_versions[versionIndex];
const QString id = versionIdOf(entry);
const CustomBuild &entry = m_builds[buildIndex];
const QString id = launchVersionIdOf(entry);
if (id.trimmed().isEmpty()) {
emit launchError(tr("У записи лаунчера не указан ID версии. Проверьте %1")
.arg(QDir::toNativeSeparators(LauncherPaths::versionsFile())));
emit launchError(tr("У сборки «%1» не выбрана версия Minecraft. Проверьте %2")
.arg(entry.name,
QDir::toNativeSeparators(LauncherPaths::customBuildsFile())));
return;
}
@@ -501,7 +818,7 @@ void LauncherBackend::launchGame(int profileIndex, int versionIndex)
if (!installed.contains(id)) {
emit launchError(installed.isEmpty()
? tr("В %1 нет ни одной установленной версии").arg(versionsDir)
: tr("Версия «%1» из записи «%2» не установлена в %3. Доступны: %4")
: tr("Версия «%1» из сборки «%2» не установлена в %3. Доступны: %4")
.arg(id,
entry.name,
versionsDir,
@@ -536,7 +853,9 @@ void LauncherBackend::launchGame(int profileIndex, int versionIndex)
}
m_pendingProfile = profileIndex;
m_pendingVersion = versionIndex;
// Держим id, а не индекс: пока идёт авторизация, сборку могли удалить из
// модального окна, и индекс уехал бы на чужую запись.
m_pendingBuild = entry.id;
m_pendingInjectorPath.clear();
beginAuthentication();
}
@@ -621,20 +940,26 @@ void LauncherBackend::submitTwoFactorCode(const QString &code)
void LauncherBackend::cancelPendingLaunch()
{
m_pendingProfile = -1;
m_pendingVersion = -1;
m_pendingBuild = -1;
m_pendingInjectorPath.clear();
setBusy(false);
}
void LauncherBackend::continueLaunch(const AuthResult &auth)
{
if (m_pendingProfile < 0 || m_pendingVersion < 0) {
if (m_pendingProfile < 0 || m_pendingProfile >= m_profiles.size() || m_pendingBuild < 0) {
failLaunch(tr("Запуск был отменён"));
return;
}
const int buildIndex = indexOfBuildId(m_pendingBuild);
if (buildIndex < 0) {
failLaunch(tr("Сборка была удалена, пока шла авторизация"));
return;
}
Profile &profile = m_profiles[m_pendingProfile];
const Version &version = m_versions[m_pendingVersion];
const CustomBuild &build = m_builds[buildIndex];
// Ely.by возвращает игровой ник и UUID аккаунта — их и запоминаем,
// чтобы в следующий раз обойтись без пароля.
@@ -666,7 +991,7 @@ void LauncherBackend::continueLaunch(const AuthResult &auth)
options.windowWidth = settingInt(m_settings, QStringLiteral("windowWidth"), 0);
options.windowHeight = settingInt(m_settings, QStringLiteral("windowHeight"), 0);
options.fullscreen = m_settings.value(QStringLiteral("fullscreen")).toBool();
options.serverAddress = version.serverUrl.trimmed();
options.serverAddress = build.serverUrl.trimmed();
options.launcherName = m_settings.value(QStringLiteral("launcherName")).toString();
options.launcherVersion = m_settings.value(QStringLiteral("launcherVersion")).toString();
@@ -682,8 +1007,8 @@ void LauncherBackend::continueLaunch(const AuthResult &auth)
const bool started = m_launcher->launch(options, m_pendingVersionData, &error);
const QString profileName = profile.name;
const QString versionName = version.name;
const QString serverUrl = version.serverUrl;
const QString buildName = build.name;
const QString serverUrl = build.serverUrl;
cancelPendingLaunch();
if (!started) {
@@ -691,7 +1016,7 @@ void LauncherBackend::continueLaunch(const AuthResult &auth)
return;
}
emit gameRunningChanged();
emit launched(profileName, versionName, serverUrl);
emit launched(profileName, buildName, serverUrl);
}
void LauncherBackend::stopGame()
@@ -705,9 +1030,30 @@ void LauncherBackend::failLaunch(const QString &message)
emit launchError(message);
}
bool LauncherBackend::switching() const
{
return m_switcher->isRunning();
}
double LauncherBackend::switchProgress() const
{
return m_switcher->fraction();
}
QString LauncherBackend::switchStage() const
{
return m_switcher->stage();
}
QString LauncherBackend::switchStatus() const
{
return m_switcher->status();
}
bool LauncherBackend::busy() const
{
return m_busy || m_installer->isRunning();
return m_busy || m_installer->isRunning() || m_loaderInstaller->isRunning()
|| m_switcher->isRunning();
}
void LauncherBackend::setBusy(bool busy)
@@ -896,27 +1242,81 @@ void LauncherBackend::loadData()
std::sort(m_profiles.begin(), m_profiles.end(),
[](const Profile &a, const Profile &b) { return a.id < b.id; });
int skippedVersions = 0;
loadFile(LauncherPaths::versionsFile(), [this, &skippedVersions](const QJsonObject &o) {
const int id = o["id"].toInt();
const QString name = o["name"].toString();
const QString versionId = o.value("versionId").toString();
// Без имени и без ID запись нерабочая: versionIdOf() вернёт пустую строку.
if (name.trimmed().isEmpty() && versionId.trimmed().isEmpty()) {
++skippedVersions;
int skippedBuilds = 0;
loadFile(LauncherPaths::customBuildsFile(), [this, &skippedBuilds](const QJsonObject &o) {
CustomBuild build;
build.id = o["id"].toInt();
build.name = o["name"].toString();
build.serverUrl = o["serverUrl"].toString();
build.minecraftVersion = o.value("minecraftVersion").toString();
// Файл мог остаться от версии лаунчера до переименования или быть
// поправлен руками — старый ключ тоже принимаем.
if (build.minecraftVersion.isEmpty())
build.minecraftVersion = o.value("versionId").toString();
build.loader = o.value("loader").toString();
build.loaderVersion = o.value("loaderVersion").toString();
build.resolvedVersionId = o.value("resolvedVersionId").toString();
// Без имени и без версии запись нерабочая: launchVersionIdOf() вернёт
// пустую строку.
if (build.name.trimmed().isEmpty() && build.minecraftVersion.trimmed().isEmpty()) {
++skippedBuilds;
return;
}
m_versions.append({id, name, o["serverUrl"].toString(), versionId});
if (id >= m_nextVersionId)
m_nextVersionId = id + 1;
m_builds.append(build);
if (build.id >= m_nextBuildId)
m_nextBuildId = build.id + 1;
});
if (skippedVersions > 0) {
m_storageIssues << tr("В %1 пропущены записи без имени и ID версии: %2")
.arg(QDir::toNativeSeparators(LauncherPaths::versionsFile()))
.arg(skippedVersions);
if (skippedBuilds > 0) {
m_storageIssues << tr("В %1 пропущены сборки без имени и версии Minecraft: %2")
.arg(QDir::toNativeSeparators(LauncherPaths::customBuildsFile()))
.arg(skippedBuilds);
}
std::sort(m_versions.begin(), m_versions.end(),
[](const Version &a, const Version &b) { return a.id < b.id; });
std::sort(m_builds.begin(), m_builds.end(),
[](const CustomBuild &a, const CustomBuild &b) { return a.id < b.id; });
}
// Первый запуск после переименования «версий» в «сборки»: перекладываем
// <root>/versions.json в customBuilds.json. Старый файл не удаляем, а
// переименовываем — он остаётся резервной копией.
bool LauncherBackend::migrateLegacyBuildsFile()
{
if (QFile::exists(LauncherPaths::customBuildsFile()))
return false;
if (!QFile::exists(LauncherPaths::legacyCustomBuildsFile()))
return false;
// readArrayFile() сам отводит битый файл в .bak — тогда мигрировать нечего.
const QJsonArray legacy = readArrayFile(LauncherPaths::legacyCustomBuildsFile());
if (legacy.isEmpty() && !QFile::exists(LauncherPaths::legacyCustomBuildsFile()))
return false;
QJsonArray migrated;
for (const auto &value : legacy) {
const QJsonObject o = value.toObject();
migrated.append(QJsonObject{{"id", o.value("id").toInt()},
{"name", o.value("name").toString()},
{"serverUrl", o.value("serverUrl").toString()},
{"minecraftVersion", o.value("versionId").toString()},
{"loader", QString()},
{"loaderVersion", QString()},
{"resolvedVersionId", QString()}});
}
// Не удалось записать — молча читаем в этой сессии из старого файла.
// Без данных пользователь не остаётся ни при каком исходе.
if (!writeJsonFile(LauncherPaths::customBuildsFile(), QJsonDocument(migrated)))
return false;
const QString retired = LauncherPaths::legacyCustomBuildsFile() + QStringLiteral(".migrated");
QFile::remove(retired);
if (!QFile::rename(LauncherPaths::legacyCustomBuildsFile(), retired)) {
m_storageIssues << tr("Сборки перенесены в %1, но старый файл %2 удалить не удалось")
.arg(QDir::toNativeSeparators(LauncherPaths::customBuildsFile()),
QDir::toNativeSeparators(
LauncherPaths::legacyCustomBuildsFile()));
}
return true;
}
// Файлы могли не появиться при первом запуске, быть удалены или отложены в .bak —
@@ -927,8 +1327,8 @@ void LauncherBackend::ensureDataFiles()
saveSettings();
if (!QFile::exists(LauncherPaths::profilesFile()))
saveProfiles();
if (!QFile::exists(LauncherPaths::versionsFile()))
saveVersions();
if (!QFile::exists(LauncherPaths::customBuildsFile()))
saveCustomBuilds();
}
void LauncherBackend::saveProfiles()
@@ -949,16 +1349,19 @@ void LauncherBackend::saveProfiles()
writeJsonFile(LauncherPaths::profilesFile(), QJsonDocument(arr));
}
void LauncherBackend::saveVersions()
void LauncherBackend::saveCustomBuilds()
{
QJsonArray arr;
for (const auto &v : m_versions)
arr.append(QJsonObject{{"id", v.id},
{"name", v.name},
{"serverUrl", v.serverUrl},
{"versionId", v.versionId}});
for (const auto &b : m_builds)
arr.append(QJsonObject{{"id", b.id},
{"name", b.name},
{"serverUrl", b.serverUrl},
{"minecraftVersion", b.minecraftVersion},
{"loader", b.loader},
{"loaderVersion", b.loaderVersion},
{"resolvedVersionId", b.resolvedVersionId}});
writeJsonFile(LauncherPaths::versionsFile(), QJsonDocument(arr));
writeJsonFile(LauncherPaths::customBuildsFile(), QJsonDocument(arr));
}
void LauncherBackend::loadSettings()
@@ -972,7 +1375,8 @@ void LauncherBackend::loadSettings()
{QStringLiteral("windowHeight"), 0},
{QStringLiteral("fullscreen"), false},
{QStringLiteral("launcherName"), QStringLiteral("KishkaLauncher")},
{QStringLiteral("launcherVersion"), QStringLiteral("1.0")}};
{QStringLiteral("launcherVersion"), QStringLiteral("1.0")},
{QStringLiteral("activeBuildId"), 0}};
const QJsonObject stored = readObjectFile(LauncherPaths::settingsFile());
for (auto it = stored.constBegin(); it != stored.constEnd(); ++it)