diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 7d4430fd2..c86990301 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -255,6 +255,8 @@ set(MINECRAFT_SOURCES minecraft/update/FoldersTask.h minecraft/update/LibrariesTask.cpp minecraft/update/LibrariesTask.h + minecraft/update/ModUpdateTask.cpp + minecraft/update/ModUpdateTask.h minecraft/launch/ClaimAccount.cpp minecraft/launch/ClaimAccount.h diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 8e98a2efe..b7f5b717d 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -73,6 +73,7 @@ #include "minecraft/update/FoldersTask.h" #include "minecraft/update/LegacyFMLLibrariesTask.h" #include "minecraft/update/LibrariesTask.h" +#include "minecraft/update/ModUpdateTask.h" #include "java/JavaUtils.h" @@ -263,10 +264,16 @@ void MinecraftInstance::loadSpecificSettings() connect(dataPacksEnabled.get(), &Setting::SettingChanged, this, [this] { m_data_pack_list.reset(); }); connect(dataPacksPath.get(), &Setting::SettingChanged, this, [this] { m_data_pack_list.reset(); }); - // Join server on launch, this does not have a global override + // Set mod downloaders, this does not have a global override m_settings->registerSetting("OverrideModDownloadLoaders", false); m_settings->registerSetting("ModDownloadLoaders", "[]"); + // Enable automatic mod updates, this does not have a global override + m_settings->registerSetting("AutomaticallyUpdateMods", false); + m_settings->registerSetting("AutomaticallyUpdateModsAll", false); + m_settings->registerSetting("AutomaticallyUpdateModsEnabled", true); + // m_settings->registerSetting("AutomaticallyUpdateModsSpecified", "[]"); + qDebug() << "Instance-type specific settings were loaded!"; setSpecificSettingsLoaded(true); @@ -1202,6 +1209,13 @@ LaunchTask* MinecraftInstance::createLaunchTask(AuthSessionPtr session, Minecraf process->appendStep(makeShared(pptr)); } + // Update mods if "AutomaticallyUpdateMods" is true. + // Must come after ScanModFolders to ensure mods are loaded. + if (settings()->get("AutomaticallyUpdateMods").toBool()) { + process->appendStep( + makeShared(pptr, makeShared(this, settings()->get("AutomaticallyUpdateModsEnabled").toBool()))); + } + // make sure we have enough RAM, warn the user if we don't { process->appendStep(makeShared(pptr, this)); diff --git a/launcher/minecraft/launch/ScanModFolders.cpp b/launcher/minecraft/launch/ScanModFolders.cpp index cbe1599cb..c5f769b7b 100644 --- a/launcher/minecraft/launch/ScanModFolders.cpp +++ b/launcher/minecraft/launch/ScanModFolders.cpp @@ -45,19 +45,19 @@ void ScanModFolders::executeTask() auto m_inst = m_parent->instance(); auto loaders = m_inst->loaderModList(); - connect(loaders, &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone); + connect(loaders, &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone, Qt::UniqueConnection); if (!loaders->update()) { m_modsDone = true; } auto cores = m_inst->coreModList(); - connect(cores, &ModFolderModel::updateFinished, this, &ScanModFolders::coreModsDone); + connect(cores, &ModFolderModel::updateFinished, this, &ScanModFolders::coreModsDone, Qt::UniqueConnection); if (!cores->update()) { m_coreModsDone = true; } auto nils = m_inst->nilModList(); - connect(nils, &ModFolderModel::updateFinished, this, &ScanModFolders::nilModsDone); + connect(nils, &ModFolderModel::updateFinished, this, &ScanModFolders::nilModsDone, Qt::UniqueConnection); if (!nils->update()) { m_nilModsDone = true; } @@ -66,25 +66,63 @@ void ScanModFolders::executeTask() void ScanModFolders::modsDone() { + qDebug() << "Check done in ScanModFolders modsDone..."; + if (!isRunning()) { + qDebug() << "ScanModFolders::modsDone called but step not running; ignoring."; + return; + } m_modsDone = true; checkDone(); } void ScanModFolders::coreModsDone() { + qDebug() << "Check done in ScanModFolders coreModsDone..."; + if (!isRunning()) { + qDebug() << "ScanModFolders::coreModsDone called but step not running; " + "ignoring."; + return; + } m_coreModsDone = true; checkDone(); } void ScanModFolders::nilModsDone() { + qDebug() << "Check done in ScanModFolders nilModsDone..."; + if (!isRunning()) { + qDebug() << "ScanModFolders::nilModsDone called but step not running; ignoring."; + return; + } m_nilModsDone = true; checkDone(); } void ScanModFolders::checkDone() { + qDebug() << "Check done in ScanModFolders..."; if (m_modsDone && m_coreModsDone && m_nilModsDone) { + // Disconnect model signals before finishing so the finished step doesn't + // receive later updateFinished() notifications and try to finish again. + auto m_inst = m_parent->instance(); + if (m_inst) { + disconnect(m_inst->loaderModList(), &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone); + disconnect(m_inst->coreModList(), &ModFolderModel::updateFinished, this, &ScanModFolders::coreModsDone); + disconnect(m_inst->nilModList(), &ModFolderModel::updateFinished, this, &ScanModFolders::nilModsDone); + } + emitSucceeded(); } } + +void ScanModFolders::finalize() +{ + auto m_inst = m_parent->instance(); + if (!m_inst) { + return; + } + + disconnect(m_inst->loaderModList(), &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone); + disconnect(m_inst->coreModList(), &ModFolderModel::updateFinished, this, &ScanModFolders::coreModsDone); + disconnect(m_inst->nilModList(), &ModFolderModel::updateFinished, this, &ScanModFolders::nilModsDone); +} diff --git a/launcher/minecraft/launch/ScanModFolders.h b/launcher/minecraft/launch/ScanModFolders.h index 5d9350952..16b9e38be 100644 --- a/launcher/minecraft/launch/ScanModFolders.h +++ b/launcher/minecraft/launch/ScanModFolders.h @@ -22,10 +22,12 @@ class ScanModFolders : public LaunchStep { Q_OBJECT public: explicit ScanModFolders(LaunchTask* parent) : LaunchStep(parent) {}; - virtual ~ScanModFolders() {}; + virtual ~ScanModFolders() = default; virtual void executeTask() override; virtual bool canAbort() const override { return false; } + virtual void finalize() override; + private slots: void coreModsDone(); void modsDone(); diff --git a/launcher/minecraft/update/ModUpdateTask.cpp b/launcher/minecraft/update/ModUpdateTask.cpp new file mode 100644 index 000000000..7a0416e43 --- /dev/null +++ b/launcher/minecraft/update/ModUpdateTask.cpp @@ -0,0 +1,125 @@ +#include "ModUpdateTask.h" + +#include +#include +#include "Application.h" +#include "BuildConfig.h" +#include "launch/LaunchStep.h" +#include "minecraft/AssetsUtils.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "minecraft/mod/Mod.h" +#include "minecraft/mod/ModDetails.h" +#include "minecraft/mod/ModFolderModel.h" +#include "net/ApiDownload.h" +#include "net/ChecksumValidator.h" +#include "ui/dialogs/CustomMessageBox.h" + +ModUpdateTask::ModUpdateTask(MinecraftInstance* inst, bool enabledModsOnly) +{ + m_instance = inst; + m_enabledModsOnly = enabledModsOnly; +} + +void ModUpdateTask::executeTask() +{ + setStatus(tr("Updating mods...")); + qDebug() << "Updating mods..."; + + if (m_instance->typeName() != "Minecraft") { + return; // this is a null instance or a legacy instance + } + + auto* profile = static_cast(m_instance)->getPackProfile(); + if (!profile->getModLoaders().has_value()) { + emitFailed(tr("Mod updates are unavailable when mod loader is missing!")); + return; + } + + if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { + emitFailed(tr("Mod updates are unavailable when metadata is disabled!")); + return; + } + + // ResourceUpdateDialog only accepts Resource, + // so we convert the Mod* list to a Resource* list with a simple static_cast + auto model = m_instance->loaderModList(); + auto _modsList = model->allMods(); + QList modsList; + modsList.reserve(_modsList.size()); + for (auto mod : _modsList) { + modsList.append(static_cast(mod)); + } + + // Filter out disabled mods if "AutomaticallyUpdateModsEnabled" is true + if (m_enabledModsOnly) { + modsList.erase(std::remove_if(modsList.begin(), modsList.end(), [](Resource* resource) { return !resource->enabled(); }), + modsList.end()); + } + + // Spawn ResourceUpdateDialog to handle mod updates + // 99% copied from ModFolderPage.cpp + auto parent = QApplication::activeWindow(); + ResourceUpdateDialog updateDialog = + ResourceUpdateDialog(parent, m_instance, model, modsList, m_includeDeps, profile->getModLoadersList()); + updateDialog.checkCandidates(); + + if (updateDialog.aborted()) { + CustomMessageBox::selectable(parent, tr("Aborted"), tr("The mod updater was aborted!"), QMessageBox::Warning)->show(); + emitAborted(); + return; + } + if (updateDialog.noUpdates()) { + QString message{ tr("'%1' is up-to-date! :)").arg(modsList.front()->name()) }; + if (modsList.size() > 1) { + if (!m_enabledModsOnly) { + message = tr("All mods are up-to-date! :)"); + } else { + message = tr("All enabled mods are up-to-date! :)"); + } + } + CustomMessageBox::selectable(parent, tr("Update checker"), message)->exec(); + emitSucceeded(); + return; + } + + if (updateDialog.exec() != 0) { + auto* tasks = new ConcurrentTask("Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, parent, tasks](const QString& reason) { + CustomMessageBox::selectable(parent, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::aborted, [this, parent, tasks]() { + CustomMessageBox::selectable(parent, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, parent, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) { + CustomMessageBox::selectable(parent, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } + tasks->deleteLater(); + }); + + for (const auto& task : updateDialog.getTasks()) { + tasks->addTask(task); + } + + ProgressDialog loadDialog(parent); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + } + model->update(); + qDebug() << "Finished updating mods..."; + emitSucceeded(); +} + +bool ModUpdateTask::canAbort() const +{ + return true; +} + +bool ModUpdateTask::abort() +{ + return true; +} diff --git a/launcher/minecraft/update/ModUpdateTask.h b/launcher/minecraft/update/ModUpdateTask.h new file mode 100644 index 000000000..012c60bff --- /dev/null +++ b/launcher/minecraft/update/ModUpdateTask.h @@ -0,0 +1,27 @@ +#pragma once +#include "net/NetJob.h" +#include "tasks/Task.h" +class MinecraftInstance; + +class ModUpdateTask : public Task { + Q_OBJECT + + public: + ModUpdateTask(MinecraftInstance* inst, bool enabled); + virtual ~ModUpdateTask() = default; + + void executeTask() override; + + bool canAbort() const override; + + public: + static QString resourceUrl(); + + public slots: + bool abort() override; + + private: + MinecraftInstance* m_instance; + bool m_enabledModsOnly; + bool m_includeDeps = true; +}; diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp index 460068bd3..29a4045d3 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -61,6 +61,7 @@ MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstance* instance, QW m_ui->serverJoinGroupBox->hide(); m_ui->globalDataPacksGroupBox->hide(); m_ui->loaderGroup->hide(); + m_ui->autoModUpdateGroup->hide(); } else { m_javaSettings = new JavaSettingsWidget(m_instance, this); m_ui->javaScrollArea->setWidget(m_javaSettings); @@ -120,6 +121,18 @@ MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstance* instance, QW m_ui->legacyFabric, m_ui->ornithe, m_ui->rift }) { connect(c, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::saveSelectedLoaders); } + + connect(m_ui->autoModUpdateGroup, &QGroupBox::toggled, this, [this](bool value) { + m_instance->settings()->set("AutomaticallyUpdateMods", value); + // if (!value) { + // m_instance->settings()->reset("AutomaticallyUpdateModsAll"); + // m_instance->settings()->reset("AutomaticallyUpdateModsEnabled"); + // } + }); + connect(m_ui->allMods, &QAbstractButton::toggled, this, + [this](bool value) { m_instance->settings()->set("AutomaticallyUpdateModsAll", value); }); + connect(m_ui->enabledMods, &QAbstractButton::toggled, this, + [this](bool value) { m_instance->settings()->set("AutomaticallyUpdateModsEnabled", value); }); } m_ui->maximizedWarning->hide(); @@ -248,6 +261,10 @@ void MinecraftSettingsWidget::loadSettings() m_ui->serverJoinGroupBox->setChecked(settings->get("JoinServerOnLaunch").toBool()); + m_ui->autoModUpdateGroup->setChecked(settings->get("AutomaticallyUpdateMods").toBool()); + m_ui->allMods->setChecked(settings->get("AutomaticallyUpdateModsAll").toBool()); + m_ui->enabledMods->setChecked(settings->get("AutomaticallyUpdateModsEnabled").toBool()); + m_ui->instanceAccountGroupBox->setChecked(settings->get("UseAccountForInstance").toBool()); updateAccountsMenu(*settings); @@ -455,6 +472,21 @@ void MinecraftSettingsWidget::saveSettings() settings->reset("JoinWorldOnLaunch"); } + bool automaticallyUpdateMods = m_ui->autoModUpdateGroup->isChecked(); + settings->set("AutomaticallyUpdateMods", automaticallyUpdateMods); + // if (automaticallyUpdateMods) { + if (m_ui->allMods->isChecked()) { + settings->set("AutomaticallyUpdateModsAll", true); + settings->set("AutomaticallyUpdateModsEnabled", false); + } else { + settings->set("AutomaticallyUpdateModsAll", false); + settings->set("AutomaticallyUpdateModsEnabled", true); + } + // } else { + // settings->reset("AutomaticallyUpdateModsAll"); + // settings->reset("AutomaticallyUpdateModsEnabled"); + // } + // Use an account for this instance bool useAccountForInstance = m_ui->instanceAccountGroupBox->isChecked(); settings->set("UseAccountForInstance", useAccountForInstance); diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.ui b/launcher/ui/widgets/MinecraftSettingsWidget.ui index a063f9660..16de653cf 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.ui +++ b/launcher/ui/widgets/MinecraftSettingsWidget.ui @@ -268,7 +268,7 @@ Allows installing data packs across all worlds if an applicable mod is installed. -It is most likely you will need to change the path - please refer to the mod's website. + It is most likely you will need to change the path - please refer to the mod's website. @@ -554,6 +554,35 @@ It is most likely you will need to change the path - please refer to the mod's w + + + + Automatically Update Mods + + + true + + + false + + + + + + All mods + + + + + + + Enabled mods only + + + + + +