version tracking and auto installing
This commit is contained in:
+238
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user