version tracking and auto installing

This commit is contained in:
2026-08-24 21:09:52 +03:00
parent 671fbe169d
commit 5bc59f8018
10 changed files with 1937 additions and 24 deletions
+238 -1
View File
@@ -4,6 +4,8 @@
#include "gamelauncher.h"
#include "javalocator.h"
#include "launcherpaths.h"
#include "versioninstaller.h"
#include "versionmanifestservice.h"
#include <QDesktopServices>
#include <QDir>
@@ -11,7 +13,9 @@
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QHash>
#include <QRegularExpression>
#include <QSet>
#include <QTimer>
#include <QUrl>
#include <algorithm>
@@ -35,6 +39,8 @@ LauncherBackend::LauncherBackend(QObject *parent)
: QObject(parent)
, m_auth(new AuthService(this))
, m_launcher(new GameLauncher(this))
, m_manifest(new VersionManifestService(this))
, m_installer(new VersionInstaller(m_manifest, this))
{
// Папка лаунчера создаётся при первом запуске и проверяется при каждом
// следующем — если её удалили, она появится снова.
@@ -58,6 +64,44 @@ LauncherBackend::LauncherBackend(QObject *parent)
emit gameFinished(exitCode, crashed);
});
connect(m_manifest, &VersionManifestService::versionsChanged,
this, &LauncherBackend::invalidateCatalog);
connect(m_manifest, &VersionManifestService::refreshingChanged,
this, &LauncherBackend::catalogLoadingChanged);
connect(m_installer, &VersionInstaller::started, this, [this](const QString &id) {
emit downloadChanged();
emit busyChanged();
emit launchProgress(tr("Загрузка версии «%1»…").arg(id));
});
connect(m_installer, &VersionInstaller::progressChanged,
this, &LauncherBackend::downloadChanged);
// Порядок важен: на installedVersionsChanged интерфейс перечитывает каталог,
// и устаревший кэш вернулся бы наружу.
connect(m_installer, &VersionInstaller::finished, this, [this](const QString &id) {
invalidateCatalog();
emit installedVersionsChanged();
emit downloadChanged();
emit busyChanged();
emit launchProgress(tr("Версия «%1» загружена").arg(id));
});
connect(m_installer, &VersionInstaller::failed, this, [this](const QString &id,
const QString &message) {
// Часть файлов всё же появилась — список установленного мог измениться.
invalidateCatalog();
emit installedVersionsChanged();
emit downloadChanged();
emit busyChanged();
emit launchError(tr("Не удалось загрузить версию «%1»: %2").arg(id, message));
});
connect(m_installer, &VersionInstaller::canceled, this, [this](const QString &id) {
invalidateCatalog();
emit installedVersionsChanged();
emit downloadChanged();
emit busyChanged();
emit launchProgress(tr("Загрузка версии «%1» отменена").arg(id));
});
// Интерфейс подключается к сигналам уже после конструктора — сообщаем отложенно.
if (!m_storageIssues.isEmpty()) {
QTimer::singleShot(0, this, [this] {
@@ -116,6 +160,7 @@ void LauncherBackend::addVersion(const QString &name, const QString &serverUrl,
m_versions.append({m_nextVersionId++, name, serverUrl, versionId});
saveVersions();
emit versionsChanged();
maybeInstall(versionIdOf(m_versions.last()));
}
void LauncherBackend::updateProfile(int index,
@@ -157,6 +202,7 @@ void LauncherBackend::updateVersion(int index,
m_versions[index].versionId = versionId;
saveVersions();
emit versionsChanged();
maybeInstall(versionIdOf(m_versions[index]));
}
QVariantMap LauncherBackend::profileAt(int index) const
@@ -230,8 +276,192 @@ QStringList LauncherBackend::checkInstallation(int versionIndex) const
return GameLauncher::missingFiles(options, version);
}
// ── Каталог версий ─────────────────────────────────────────────────────────
void LauncherBackend::invalidateCatalog()
{
m_catalogDirty = true;
emit versionCatalogChanged();
}
bool LauncherBackend::catalogLoading() const
{
return m_manifest->isRefreshing();
}
void LauncherBackend::refreshVersionCatalog(bool force)
{
// Список установленного мог поменяться и без нас (папку добавили руками).
invalidateCatalog();
m_manifest->ensureLoaded([this](bool ok, const QString &warning) {
if (!ok && !warning.isEmpty())
emit launchError(warning);
}, force);
}
bool LauncherBackend::isVersionInstalled(const QString &versionId) const
{
if (versionId.trimmed().isEmpty())
return false;
return VersionLoader::installedVersions(gameDir()).contains(versionId);
}
QVariantList LauncherBackend::versionCatalog() const
{
if (m_catalogDirty) {
rebuildCatalog();
m_catalogDirty = false;
}
return m_catalog;
}
void LauncherBackend::rebuildCatalog() const
{
m_catalog.clear();
const QList<RemoteVersionEntry> remote = m_manifest->versions();
QHash<QString, int> remoteIndex;
remoteIndex.reserve(remote.size());
for (int i = 0; i < remote.size(); ++i)
remoteIndex.insert(remote.at(i).id, i);
const auto makeVersionRow = [](const QString &id, const QString &type, bool installed) {
return QVariantMap{{QStringLiteral("kind"),
installed ? QStringLiteral("installed") : QStringLiteral("remote")},
{QStringLiteral("id"), id},
{QStringLiteral("type"), type},
{QStringLiteral("label"), id},
{QStringLiteral("installed"), installed},
{QStringLiteral("selectable"), true},
{QStringLiteral("search"), (id + u' ' + type).toLower()}};
};
// Установленные: сначала знакомые манифесту, свежие сверху, затем сборки
// вроде fabric-loader-…, о которых манифест ничего не знает.
QStringList installed = VersionLoader::installedVersions(gameDir());
std::stable_sort(installed.begin(), installed.end(),
[&remoteIndex, &remote](const QString &a, const QString &b) {
const auto ia = remoteIndex.constFind(a);
const auto ib = remoteIndex.constFind(b);
const bool knownA = ia != remoteIndex.constEnd();
const bool knownB = ib != remoteIndex.constEnd();
if (knownA != knownB)
return knownA;
if (knownA)
return ia.value() < ib.value(); // манифест уже отсортирован
return a < b;
});
QSet<QString> installedSet;
for (const QString &id : std::as_const(installed)) {
const auto it = remoteIndex.constFind(id);
const QString type = it != remoteIndex.constEnd() ? remote.at(it.value()).type : QString();
installedSet.insert(id);
m_catalog.append(makeVersionRow(id, type, true));
}
m_catalog.append(QVariantMap{{QStringLiteral("kind"), QStringLiteral("separator")},
{QStringLiteral("id"), QString()},
{QStringLiteral("type"), QString()},
{QStringLiteral("label"), tr("Все версии Minecraft")},
{QStringLiteral("installed"), false},
{QStringLiteral("selectable"), false},
{QStringLiteral("search"), QString()}});
if (remote.isEmpty()) {
m_catalog.append(
QVariantMap{{QStringLiteral("kind"), QStringLiteral("status")},
{QStringLiteral("id"), QString()},
{QStringLiteral("type"), QString()},
{QStringLiteral("label"),
m_manifest->isRefreshing()
? tr("Загрузка списка версий…")
: tr("Список версий недоступен — проверьте соединение")},
{QStringLiteral("installed"), false},
{QStringLiteral("selectable"), false},
{QStringLiteral("search"), QString()}});
return;
}
for (const RemoteVersionEntry &entry : remote) {
if (installedSet.contains(entry.id))
continue;
m_catalog.append(makeVersionRow(entry.id, entry.type, false));
}
}
// ── Загрузка версий ────────────────────────────────────────────────────────
void LauncherBackend::maybeInstall(const QString &versionId)
{
const QString id = versionId.trimmed();
if (id.isEmpty() || isVersionInstalled(id))
return;
if (m_installer->versionId() == id || m_installer->isQueued(id))
return;
// Манифест здесь не спрашиваем: установщик сам скажет, что версии в нём нет,
// и это правильный ответ на вручную вписанный id сборки.
m_installer->install(gameDir(), id);
}
void LauncherBackend::installVersion(const QString &versionId)
{
const QString id = versionId.trimmed();
if (id.isEmpty())
return;
if (m_installer->versionId() == id || m_installer->isQueued(id))
return;
m_installer->install(gameDir(), id);
}
void LauncherBackend::cancelDownload()
{
m_installer->cancel();
}
bool LauncherBackend::downloading() const
{
return m_installer->isRunning();
}
double LauncherBackend::downloadProgress() const
{
return m_installer->fraction();
}
QString LauncherBackend::downloadVersion() const
{
return m_installer->versionId();
}
QString LauncherBackend::downloadStatus() const
{
const QString stage = m_installer->stage();
const QString file = m_installer->currentFile();
if (stage.isEmpty())
return file;
if (file.isEmpty())
return stage;
return QStringLiteral("%1 — %2").arg(stage, file);
}
qint64 LauncherBackend::downloadBytesDone() const
{
return m_installer->bytesDone();
}
qint64 LauncherBackend::downloadBytesTotal() const
{
return m_installer->bytesTotal();
}
void LauncherBackend::launchGame(int profileIndex, int versionIndex)
{
if (m_installer->isRunning()) {
emit launchError(tr("Идёт загрузка версии «%1» — дождитесь окончания")
.arg(m_installer->versionId()));
return;
}
if (m_busy) {
emit launchError(tr("Запуск уже выполняется"));
return;
@@ -475,6 +705,11 @@ void LauncherBackend::failLaunch(const QString &message)
emit launchError(message);
}
bool LauncherBackend::busy() const
{
return m_busy || m_installer->isRunning();
}
void LauncherBackend::setBusy(bool busy)
{
if (m_busy == busy)
@@ -504,8 +739,10 @@ void LauncherBackend::updateSettings(const QVariantMap &values)
m_settings.insert(it.key(), it.value());
saveSettings();
emit settingsChanged();
if (gameDir() != previousGameDir)
if (gameDir() != previousGameDir) {
invalidateCatalog();
emit installedVersionsChanged();
}
}
QStringList LauncherBackend::detectedJava() const