java implementation and downloading

This commit is contained in:
2026-08-27 19:35:11 +03:00
parent 08e0dc4965
commit dddc0261d4
14 changed files with 2907 additions and 10 deletions
+293 -8
View File
@@ -2,7 +2,10 @@
#include "authservice.h"
#include "gamelauncher.h"
#include "javainstaller.h"
#include "javalocator.h"
#include "javaruntime.h"
#include "javaruntimeservice.h"
#include "launcherpaths.h"
#include "versioninstaller.h"
#include "versionmanifestservice.h"
@@ -47,6 +50,8 @@ LauncherBackend::LauncherBackend(QObject *parent)
, m_loaderMeta(new ModLoaderVersionService(this))
, m_loaderInstaller(new ModLoaderInstaller(m_loaderMeta, m_installer, this))
, m_switcher(new BuildSwitcher(this))
, m_javaMeta(new JavaRuntimeService(this))
, m_javaInstaller(new JavaInstaller(this))
{
// Папка лаунчера создаётся при первом запуске и проверяется при каждом
// следующем — если её удалили, она появится снова.
@@ -88,6 +93,41 @@ LauncherBackend::LauncherBackend(QObject *parent)
connect(m_manifest, &VersionManifestService::refreshingChanged,
this, &LauncherBackend::catalogLoadingChanged);
connect(m_javaMeta, &JavaRuntimeService::catalogChanged,
this, &LauncherBackend::invalidateJavaCatalog);
connect(m_javaMeta, &JavaRuntimeService::refreshingChanged,
this, &LauncherBackend::javaCatalogLoadingChanged);
connect(m_javaInstaller, &JavaInstaller::started, this, [this](const QString &label) {
emit downloadChanged();
emit busyChanged();
emit launchProgress(tr("Загрузка Java «%1»…").arg(label));
});
connect(m_javaInstaller, &JavaInstaller::progressChanged,
this, &LauncherBackend::downloadChanged);
connect(m_javaInstaller, &JavaInstaller::finished, this,
[this](const QString &runtimeId, const QString &javaPath) {
invalidateJavaCatalog();
emit downloadChanged();
emit busyChanged();
emit javaRuntimeInstalled(runtimeId);
emit launchProgress(tr("Java установлена: %1")
.arg(QDir::toNativeSeparators(javaPath)));
});
connect(m_javaInstaller, &JavaInstaller::failed, this,
[this](const QString &label, const QString &message) {
invalidateJavaCatalog();
emit downloadChanged();
emit busyChanged();
emit launchError(tr("Не удалось установить Java «%1»: %2").arg(label, message));
});
connect(m_javaInstaller, &JavaInstaller::canceled, this, [this](const QString &label) {
invalidateJavaCatalog();
emit downloadChanged();
emit busyChanged();
emit launchProgress(tr("Загрузка Java «%1» отменена").arg(label));
});
connect(m_installer, &VersionInstaller::started, this, [this](const QString &id) {
emit downloadChanged();
emit busyChanged();
@@ -653,19 +693,32 @@ void LauncherBackend::cancelDownload()
// Установщик лоадера сам остановит вложенную установку версии.
if (m_loaderInstaller->isRunning())
m_loaderInstaller->cancel();
else
else if (m_installer->isRunning())
m_installer->cancel();
else
m_javaInstaller->cancel();
}
// Панель загрузки одна на два установщика: пока идёт установка модлоадера, она
// показывает его стадии, включая вложенную загрузку файлов версии.
// Панель загрузки одна на три установщика: пока идёт установка модлоадера, она
// показывает его стадии, включая вложенную загрузку файлов версии. Установка
// Java начинается только когда остальные не заняты, поэтому за панель они не спорят.
bool LauncherBackend::downloading() const
{
return m_loaderInstaller->isRunning() || m_installer->isRunning();
return m_loaderInstaller->isRunning() || m_installer->isRunning()
|| m_javaInstaller->isRunning();
}
// Java качается сама по себе, а версия и лоадер — вместе; отсюда и порядок проверок.
bool LauncherBackend::javaDownloadShown() const
{
return m_javaInstaller->isRunning() && !m_installer->isRunning()
&& !m_loaderInstaller->isRunning();
}
double LauncherBackend::downloadProgress() const
{
if (javaDownloadShown())
return m_javaInstaller->fraction();
if (!m_loaderInstaller->isRunning())
return m_installer->fraction();
// Внутри установки лоадера файлы качает VersionInstaller — его доля точнее.
@@ -674,6 +727,8 @@ double LauncherBackend::downloadProgress() const
QString LauncherBackend::downloadVersion() const
{
if (javaDownloadShown())
return m_javaInstaller->label();
if (m_loaderInstaller->isRunning())
return m_loaderInstaller->label();
return m_installer->versionId();
@@ -683,7 +738,10 @@ QString LauncherBackend::downloadStatus() const
{
QString stage = m_installer->stage();
QString file = m_installer->currentFile();
if (m_loaderInstaller->isRunning() && !m_installer->isRunning()) {
if (javaDownloadShown()) {
stage = m_javaInstaller->stage();
file = m_javaInstaller->currentFile();
} else if (m_loaderInstaller->isRunning() && !m_installer->isRunning()) {
stage = m_loaderInstaller->stage();
file = m_loaderInstaller->currentFile();
}
@@ -696,6 +754,8 @@ QString LauncherBackend::downloadStatus() const
qint64 LauncherBackend::downloadBytesDone() const
{
if (javaDownloadShown())
return m_javaInstaller->bytesDone();
if (m_loaderInstaller->isRunning() && !m_installer->isRunning())
return m_loaderInstaller->bytesDone();
return m_installer->bytesDone();
@@ -703,6 +763,8 @@ qint64 LauncherBackend::downloadBytesDone() const
qint64 LauncherBackend::downloadBytesTotal() const
{
if (javaDownloadShown())
return m_javaInstaller->bytesTotal();
if (m_loaderInstaller->isRunning() && !m_installer->isRunning())
return m_loaderInstaller->bytesTotal();
return m_installer->bytesTotal();
@@ -791,7 +853,9 @@ void LauncherBackend::installLoaderForBuild(int index)
*loader,
build.minecraftVersion,
build.loaderVersion,
m_settings.value(QStringLiteral("javaPath")).toString());
managedJavaPath().isEmpty()
? m_settings.value(QStringLiteral("javaPath")).toString()
: managedJavaPath());
}
void LauncherBackend::launchGame(int profileIndex, int buildIndex)
@@ -806,6 +870,13 @@ void LauncherBackend::launchGame(int profileIndex, int buildIndex)
.arg(m_installer->versionId()));
return;
}
// Игра ищет java при запуске: стартовать посреди её установки — значит
// найти половину распакованного дерева.
if (m_javaInstaller->isRunning()) {
emit launchError(tr("Идёт установка Java «%1» — дождитесь окончания")
.arg(m_javaInstaller->label()));
return;
}
if (m_busy) {
emit launchError(tr("Запуск уже выполняется"));
return;
@@ -1013,7 +1084,15 @@ void LauncherBackend::continueLaunch(const AuthResult &auth)
options.clientToken = auth.clientToken;
options.userType = auth.userType;
options.javaPath = m_settings.value(QStringLiteral("javaPath")).toString();
// Скачанная лаунчером сборка важнее пути из настроек: её выбрали явно в
// окне выбора версии Java.
options.javaPath = managedJavaPath();
if (options.javaPath.isEmpty()) {
if (!m_settings.value(QStringLiteral("javaRuntime")).toString().trimmed().isEmpty())
emit launchProgress(tr("Выбранная сборка Java не найдена в папке лаунчера — "
"ищем установленную в системе"));
options.javaPath = m_settings.value(QStringLiteral("javaPath")).toString();
}
options.minMemoryMb = settingInt(m_settings, QStringLiteral("minMemoryMb"), 512);
options.maxMemoryMb = settingInt(m_settings, QStringLiteral("maxMemoryMb"), 4096);
options.windowWidth = settingInt(m_settings, QStringLiteral("windowWidth"), 0);
@@ -1081,7 +1160,7 @@ QString LauncherBackend::switchStatus() const
bool LauncherBackend::busy() const
{
return m_busy || m_installer->isRunning() || m_loaderInstaller->isRunning()
|| m_switcher->isRunning();
|| m_javaInstaller->isRunning() || m_switcher->isRunning();
}
void LauncherBackend::setBusy(bool busy)
@@ -1133,6 +1212,210 @@ QStringList LauncherBackend::detectedJava() const
return result;
}
// ── Сборки Java ────────────────────────────────────────────────────────────
void LauncherBackend::invalidateJavaCatalog()
{
m_javaCatalogDirty = true;
emit javaCatalogChanged();
}
bool LauncherBackend::javaCatalogLoading() const
{
return m_javaMeta->isRefreshing();
}
void LauncherBackend::refreshJavaCatalog(bool force)
{
// Список установленного мог поменяться и без нас (папку удалили руками).
invalidateJavaCatalog();
m_javaMeta->ensureLoaded([this](bool ok, const QString &warning) {
if (!ok && !warning.isEmpty())
emit launchError(warning);
}, force);
}
QVariantList LauncherBackend::javaCatalog() const
{
if (m_javaCatalogDirty) {
rebuildJavaCatalog();
m_javaCatalogDirty = false;
}
return m_javaCatalog;
}
void LauncherBackend::rebuildJavaCatalog() const
{
m_javaCatalog.clear();
const QList<InstalledJavaRuntime> installed = JavaRuntimeStore::installed();
QHash<QString, qint64> installedSizes;
for (const InstalledJavaRuntime &runtime : installed)
installedSizes.insert(runtime.id, runtime.size);
const auto megabytes = [](qint64 bytes) {
return bytes > 0 ? int((bytes + 512 * 1024) / (1024 * 1024)) : 0;
};
const auto makeRow = [&megabytes](const QString &id,
const QString &kind,
int major,
const QString &version,
const QString &detail,
qint64 size,
bool isInstalled,
bool lts,
bool downloadable) {
return QVariantMap{
{QStringLiteral("id"), id},
{QStringLiteral("kind"), kind},
{QStringLiteral("major"), major},
{QStringLiteral("version"), version},
{QStringLiteral("label"), QStringLiteral("Java %1").arg(version)},
{QStringLiteral("detail"), detail},
{QStringLiteral("coverage"), JavaRequirement::coverage(major)},
{QStringLiteral("sizeMb"), megabytes(size)},
{QStringLiteral("installed"), isInstalled},
{QStringLiteral("lts"), lts},
{QStringLiteral("downloadable"), downloadable},
{QStringLiteral("search"),
QStringLiteral("%1 %2 %3 %4").arg(version, QString::number(major), kind, detail).toLower()}};
};
// Скачанное — сверху: этим уже можно пользоваться прямо сейчас.
QSet<QString> listed;
for (const InstalledJavaRuntime &runtime : installed) {
listed.insert(runtime.id);
const auto remote = m_javaMeta->find(runtime.id);
const QString detail = remote ? (remote->kind == JavaRuntimeKind::Mojang
? tr("Mojang · %1").arg(remote->component)
: tr("Temurin · %1").arg(remote->architecture))
: tr("скачана лаунчером");
m_javaCatalog.append(makeRow(runtime.id,
runtime.kind,
runtime.major,
runtime.version,
detail,
runtime.size,
true,
remote && remote->lts,
remote.has_value()));
}
const QList<JavaRuntimeKind> kinds{JavaRuntimeKind::Mojang, JavaRuntimeKind::Jdk,
JavaRuntimeKind::Jre};
for (const JavaRuntimeKind kind : kinds) {
const QList<JavaRuntimeEntry> entries = m_javaMeta->entries(kind);
for (const JavaRuntimeEntry &entry : entries) {
if (listed.contains(entry.id))
continue;
const QString detail = kind == JavaRuntimeKind::Mojang
? tr("Mojang · %1").arg(entry.component)
: tr("Temurin · %1").arg(entry.architecture);
m_javaCatalog.append(makeRow(entry.id,
javaKindKey(kind),
entry.major,
entry.version,
detail,
entry.size,
false,
entry.lts,
true));
}
}
}
void LauncherBackend::installJavaRuntime(const QString &runtimeId)
{
if (m_javaInstaller->isRunning()) {
emit launchError(tr("Идёт установка Java «%1» — дождитесь окончания")
.arg(m_javaInstaller->label()));
return;
}
if (m_installer->isRunning() || m_loaderInstaller->isRunning() || m_switcher->isRunning()) {
emit launchError(tr("Дождитесь окончания текущей операции"));
return;
}
const auto entry = m_javaMeta->find(runtimeId);
if (!entry) {
emit launchError(tr("Сборка Java «%1» не найдена в каталоге").arg(runtimeId));
return;
}
m_javaInstaller->install(*entry);
}
void LauncherBackend::removeJavaRuntime(const QString &runtimeId)
{
if (m_javaInstaller->isRunning() && m_javaInstaller->runtimeId() == runtimeId) {
emit launchError(tr("Эта сборка Java сейчас скачивается"));
return;
}
QString error;
if (!JavaRuntimeStore::remove(runtimeId, &error)) {
emit launchError(tr("Не удалось удалить сборку Java: %1").arg(error));
return;
}
// Удалили выбранную — настройка указывала бы в пустоту.
if (m_settings.value(QStringLiteral("javaRuntime")).toString() == runtimeId) {
m_settings.insert(QStringLiteral("javaRuntime"), QString());
saveSettings();
emit settingsChanged();
}
invalidateJavaCatalog();
emit launchProgress(tr("Сборка Java удалена"));
}
QVariantMap LauncherBackend::javaRuntimeInfo(const QString &runtimeId) const
{
if (runtimeId.trimmed().isEmpty())
return {};
const auto runtime = JavaRuntimeStore::find(runtimeId);
if (runtime) {
return QVariantMap{{QStringLiteral("id"), runtime->id},
{QStringLiteral("kind"), runtime->kind},
{QStringLiteral("major"), runtime->major},
{QStringLiteral("version"), runtime->version},
{QStringLiteral("label"), QStringLiteral("Java %1").arg(runtime->version)},
{QStringLiteral("javaPath"), QDir::toNativeSeparators(runtime->javaPath)},
{QStringLiteral("installed"), true}};
}
// Выбрана, но ещё не скачана: подпись всё равно нужна.
const auto entry = m_javaMeta->find(runtimeId);
if (!entry)
return QVariantMap{{QStringLiteral("id"), runtimeId},
{QStringLiteral("label"), runtimeId},
{QStringLiteral("installed"), false}};
return QVariantMap{{QStringLiteral("id"), entry->id},
{QStringLiteral("kind"), javaKindKey(entry->kind)},
{QStringLiteral("major"), entry->major},
{QStringLiteral("version"), entry->version},
{QStringLiteral("label"), QStringLiteral("Java %1").arg(entry->version)},
{QStringLiteral("javaPath"), QString()},
{QStringLiteral("installed"), false}};
}
int LauncherBackend::requiredJavaMajor(int buildIndex) const
{
if (buildIndex < 0 || buildIndex >= m_builds.size())
return 0;
return JavaRequirement::minimumFor(m_builds.at(buildIndex).minecraftVersion);
}
QString LauncherBackend::managedJavaPath() const
{
const QString id = m_settings.value(QStringLiteral("javaRuntime")).toString();
if (id.trimmed().isEmpty())
return {};
const auto runtime = JavaRuntimeStore::find(id);
return runtime ? runtime->javaPath : QString();
}
void LauncherBackend::openMinecraftFolder()
{
const QString modsDir = gameDir() + QStringLiteral("/mods");
@@ -1396,6 +1679,8 @@ void LauncherBackend::loadSettings()
{
m_settings = {{QStringLiteral("gameDir"), QString()},
{QStringLiteral("javaPath"), QString()},
// id сборки Java из <root>/java. Пусто — java ищется сама.
{QStringLiteral("javaRuntime"), QString()},
{QStringLiteral("minMemoryMb"), 512},
{QStringLiteral("maxMemoryMb"), 4096},
{QStringLiteral("jvmArgs"), QString()},