From 4dfc01899a94ce365284ee1ba57c11afa823c55a Mon Sep 17 00:00:00 2001 From: Gingeh <39150378+Gingeh@users.noreply.github.com> Date: Fri, 15 Jul 2022 11:35:03 +1000 Subject: [PATCH 01/38] Make skin upload optional Signed-off-by: Gingeh <39150378+Gingeh@users.noreply.github.com> --- launcher/ui/dialogs/SkinUploadDialog.cpp | 90 +++++++++++++----------- launcher/ui/dialogs/SkinUploadDialog.ui | 6 +- 2 files changed, 52 insertions(+), 44 deletions(-) diff --git a/launcher/ui/dialogs/SkinUploadDialog.cpp b/launcher/ui/dialogs/SkinUploadDialog.cpp index b5b78690..8180ac1f 100644 --- a/launcher/ui/dialogs/SkinUploadDialog.cpp +++ b/launcher/ui/dialogs/SkinUploadDialog.cpp @@ -57,68 +57,72 @@ void SkinUploadDialog::on_buttonBox_accepted() { QString fileName; QString input = ui->skinPathTextBox->text(); - QRegularExpression urlPrefixMatcher(QRegularExpression::anchoredPattern("^([a-z]+)://.+$")); - bool isLocalFile = false; - // it has an URL prefix -> it is an URL - if(urlPrefixMatcher.match(input).hasMatch()) - { - QUrl fileURL = input; - if(fileURL.isValid()) + ProgressDialog prog(this); + SequentialTask skinUpload; + + if (!input.isEmpty()) { + QRegularExpression urlPrefixMatcher(QRegularExpression::anchoredPattern("^([a-z]+)://.+$")); + bool isLocalFile = false; + // it has an URL prefix -> it is an URL + if(urlPrefixMatcher.match(input).hasMatch()) { - // local? - if(fileURL.isLocalFile()) + QUrl fileURL = input; + if(fileURL.isValid()) { - isLocalFile = true; - fileName = fileURL.toLocalFile(); + // local? + if(fileURL.isLocalFile()) + { + isLocalFile = true; + fileName = fileURL.toLocalFile(); + } + else + { + CustomMessageBox::selectable( + this, + tr("Skin Upload"), + tr("Using remote URLs for setting skins is not implemented yet."), + QMessageBox::Warning + )->exec(); + close(); + return; + } } else { CustomMessageBox::selectable( this, tr("Skin Upload"), - tr("Using remote URLs for setting skins is not implemented yet."), + tr("You cannot use an invalid URL for uploading skins."), QMessageBox::Warning - )->exec(); + )->exec(); close(); return; } } else { - CustomMessageBox::selectable( - this, - tr("Skin Upload"), - tr("You cannot use an invalid URL for uploading skins."), - QMessageBox::Warning - )->exec(); + // just assume it's a path then + isLocalFile = true; + fileName = ui->skinPathTextBox->text(); + } + if (isLocalFile && !QFile::exists(fileName)) + { + CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec(); close(); return; } + SkinUpload::Model model = SkinUpload::STEVE; + if (ui->steveBtn->isChecked()) + { + model = SkinUpload::STEVE; + } + else if (ui->alexBtn->isChecked()) + { + model = SkinUpload::ALEX; + } + skinUpload.addTask(shared_qobject_ptr(new SkinUpload(this, m_acct->accessToken(), FS::read(fileName), model))); } - else - { - // just assume it's a path then - isLocalFile = true; - fileName = ui->skinPathTextBox->text(); - } - if (isLocalFile && !QFile::exists(fileName)) - { - CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec(); - close(); - return; - } - SkinUpload::Model model = SkinUpload::STEVE; - if (ui->steveBtn->isChecked()) - { - model = SkinUpload::STEVE; - } - else if (ui->alexBtn->isChecked()) - { - model = SkinUpload::ALEX; - } - ProgressDialog prog(this); - SequentialTask skinUpload; - skinUpload.addTask(shared_qobject_ptr(new SkinUpload(this, m_acct->accessToken(), FS::read(fileName), model))); + auto selectedCape = ui->capeCombo->currentData().toString(); if(selectedCape != m_acct->accountData()->minecraftProfile.currentCape) { skinUpload.addTask(shared_qobject_ptr(new CapeChange(this, m_acct->accessToken(), selectedCape))); diff --git a/launcher/ui/dialogs/SkinUploadDialog.ui b/launcher/ui/dialogs/SkinUploadDialog.ui index f4b0ed0a..c7b16645 100644 --- a/launcher/ui/dialogs/SkinUploadDialog.ui +++ b/launcher/ui/dialogs/SkinUploadDialog.ui @@ -21,7 +21,11 @@ - + + + Leave empty to keep current skin + + From 05fa266e6b423ce5cdd5da2ed9035917777459b0 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 1 Jul 2022 08:48:06 -0300 Subject: [PATCH 02/38] fix: provide default value to is_indexed in ModDownloadTask Signed-off-by: flow --- launcher/ModDownloadTask.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ModDownloadTask.h b/launcher/ModDownloadTask.h index b3c25909..6e204e70 100644 --- a/launcher/ModDownloadTask.h +++ b/launcher/ModDownloadTask.h @@ -30,7 +30,7 @@ class ModFolderModel; class ModDownloadTask : public SequentialTask { Q_OBJECT public: - explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr mods, bool is_indexed); + explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr mods, bool is_indexed = true); const QString& getFilename() const { return m_mod_version.fileName; } private: From 032ceefa1d4c147477fd432cea64f6cab88b8699 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 22 Apr 2022 11:36:00 -0300 Subject: [PATCH 03/38] feat: add some helping methods to WideBar Signed-off-by: flow --- launcher/ui/widgets/WideBar.cpp | 49 ++++++++++++++++++++++++++------- launcher/ui/widgets/WideBar.h | 31 +++++++++++++-------- 2 files changed, 58 insertions(+), 22 deletions(-) diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp index 8d5bd12d..79f1e0c9 100644 --- a/launcher/ui/widgets/WideBar.cpp +++ b/launcher/ui/widgets/WideBar.cpp @@ -76,13 +76,20 @@ void WideBar::addSeparator() m_entries.push_back(entry); } -void WideBar::insertActionBefore(QAction* before, QAction* action){ - auto iter = std::find_if(m_entries.begin(), m_entries.end(), [before](BarEntry * entry) { - return entry->wideAction == before; +auto WideBar::getMatching(QAction* act) -> QList::iterator +{ + auto iter = std::find_if(m_entries.begin(), m_entries.end(), [act](BarEntry * entry) { + return entry->wideAction == act; }); - if(iter == m_entries.end()) { + + return iter; +} + +void WideBar::insertActionBefore(QAction* before, QAction* action){ + auto iter = getMatching(before); + if(iter == m_entries.end()) return; - } + auto entry = new BarEntry(); entry->qAction = insertWidget((*iter)->qAction, new ActionButton(action, this)); entry->wideAction = action; @@ -90,14 +97,24 @@ void WideBar::insertActionBefore(QAction* before, QAction* action){ m_entries.insert(iter, entry); } +void WideBar::insertActionAfter(QAction* after, QAction* action){ + auto iter = getMatching(after); + if(iter == m_entries.end()) + return; + + auto entry = new BarEntry(); + entry->qAction = insertWidget((*(iter+1))->qAction, new ActionButton(action, this)); + entry->wideAction = action; + entry->type = BarEntry::Action; + m_entries.insert(iter + 1, entry); +} + void WideBar::insertSpacer(QAction* action) { - auto iter = std::find_if(m_entries.begin(), m_entries.end(), [action](BarEntry * entry) { - return entry->wideAction == action; - }); - if(iter == m_entries.end()) { + auto iter = getMatching(action); + if(iter == m_entries.end()) return; - } + QWidget* spacer = new QWidget(); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); @@ -107,6 +124,18 @@ void WideBar::insertSpacer(QAction* action) m_entries.insert(iter, entry); } +void WideBar::insertSeparator(QAction* before) +{ + auto iter = getMatching(before); + if(iter == m_entries.end()) + return; + + auto entry = new BarEntry(); + entry->qAction = QToolBar::insertSeparator(before); + entry->type = BarEntry::Separator; + m_entries.insert(iter, entry); +} + QMenu * WideBar::createContextMenu(QWidget *parent, const QString & title) { QMenu *contextMenu = new QMenu(title, parent); diff --git a/launcher/ui/widgets/WideBar.h b/launcher/ui/widgets/WideBar.h index 2b676a8c..8ff62ef2 100644 --- a/launcher/ui/widgets/WideBar.h +++ b/launcher/ui/widgets/WideBar.h @@ -1,27 +1,34 @@ #pragma once -#include #include #include +#include class QMenu; -class WideBar : public QToolBar -{ +class WideBar : public QToolBar { Q_OBJECT -public: - explicit WideBar(const QString &title, QWidget * parent = nullptr); - explicit WideBar(QWidget * parent = nullptr); + public: + explicit WideBar(const QString& title, QWidget* parent = nullptr); + explicit WideBar(QWidget* parent = nullptr); virtual ~WideBar(); - void addAction(QAction *action); + void addAction(QAction* action); void addSeparator(); - void insertSpacer(QAction *action); - void insertActionBefore(QAction *before, QAction *action); - QMenu *createContextMenu(QWidget *parent = nullptr, const QString & title = QString()); -private: + void insertSpacer(QAction* action); + void insertSeparator(QAction* before); + void insertActionBefore(QAction* before, QAction* action); + void insertActionAfter(QAction* after, QAction* action); + + QMenu* createContextMenu(QWidget* parent = nullptr, const QString& title = QString()); + + private: struct BarEntry; - QList m_entries; + + auto getMatching(QAction* act) -> QList::iterator; + + private: + QList m_entries; }; From 43b9db6e45a2ea74384f7851722952b9a1547213 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 10 Jun 2022 19:27:25 -0300 Subject: [PATCH 04/38] change: allow deleting mods while preserving their metadata Signed-off-by: flow --- launcher/minecraft/mod/Mod.cpp | 5 +++-- launcher/minecraft/mod/Mod.h | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 742709e3..37ec8eca 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -162,13 +162,14 @@ void Mod::setMetadata(Metadata::ModStruct* metadata) } } -auto Mod::destroy(QDir& index_dir) -> bool +auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool { auto n = name(); // FIXME: This can fail to remove the metadata if the // "ModMetadataDisabled" setting is on, since there could // be a name mismatch! - Metadata::remove(index_dir, n); + if(!preserve_metadata) + Metadata::remove(index_dir, n); m_type = MOD_UNKNOWN; return FS::deletePath(m_file.filePath()); diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 5f9c4684..abb8a52d 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -82,7 +82,7 @@ public: auto enable(bool value) -> bool; // delete all the files of this mod - auto destroy(QDir& index_dir) -> bool; + auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool; // change the mod's filesystem path (used by mod lists for *MAGIC* purposes) void repath(const QFileInfo &file); From 882c82f82c0f5a3b634c5784d0968174cfdb8960 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 9 May 2022 10:53:52 -0300 Subject: [PATCH 05/38] fix: always update global progress of sequential tasks Previously, it would not update the global counter if the subTask didn't update its progress, even though progress was being made. This also prevents a segmentation fault while aborting the task. Signed-off-by: flow --- launcher/tasks/SequentialTask.cpp | 20 ++++++++++++++------ launcher/tasks/SequentialTask.h | 2 ++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp index 7f03ad2e..f1e1a889 100644 --- a/launcher/tasks/SequentialTask.cpp +++ b/launcher/tasks/SequentialTask.cpp @@ -1,5 +1,7 @@ #include "SequentialTask.h" +#include + SequentialTask::SequentialTask(QObject* parent, const QString& task_name) : Task(parent), m_name(task_name), m_currentIndex(-1) {} SequentialTask::~SequentialTask() @@ -39,14 +41,15 @@ bool SequentialTask::abort() emit aborted(); emit finished(); } - m_queue.clear(); + + m_aborted = true; return true; } bool succeeded = m_queue[m_currentIndex]->abort(); - m_queue.clear(); + m_aborted = succeeded; - if(succeeded) + if (succeeded) emitAborted(); return succeeded; @@ -54,10 +57,14 @@ bool SequentialTask::abort() void SequentialTask::startNext() { - if (m_currentIndex != -1) { - Task::Ptr previous = m_queue[m_currentIndex]; + if (m_aborted) + return; + + if (m_currentIndex != -1 && m_currentIndex < m_queue.size()) { + Task::Ptr previous = m_queue.at(m_currentIndex); disconnect(previous.get(), 0, this, 0); } + m_currentIndex++; if (m_queue.isEmpty() || m_currentIndex >= m_queue.size()) { emitSucceeded(); @@ -76,6 +83,8 @@ void SequentialTask::startNext() setStatus(tr("Executing task %1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size())); setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus()); + setProgress(m_currentIndex + 1, m_queue.count()); + next->start(); } @@ -93,7 +102,6 @@ void SequentialTask::subTaskProgress(qint64 current, qint64 total) setProgress(0, 100); return; } - setProgress(m_currentIndex + 1, m_queue.count()); m_stepProgress = current; m_stepTotalProgress = total; diff --git a/launcher/tasks/SequentialTask.h b/launcher/tasks/SequentialTask.h index e10cb6f7..942ebec2 100644 --- a/launcher/tasks/SequentialTask.h +++ b/launcher/tasks/SequentialTask.h @@ -44,4 +44,6 @@ protected: qint64 m_stepProgress = 0; qint64 m_stepTotalProgress = 100; + + bool m_aborted = false; }; From 91776311c7faa5062bdfa0e543b513119d903002 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 6 Jun 2022 20:16:13 -0300 Subject: [PATCH 06/38] fix: allow aborting upload tasks This maintains the same behaviour as the Download task. Signed-off-by: flow --- launcher/net/Upload.cpp | 10 ++++++++++ launcher/net/Upload.h | 2 ++ 2 files changed, 12 insertions(+) diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp index 12dd1e78..cfda4b4e 100644 --- a/launcher/net/Upload.cpp +++ b/launcher/net/Upload.cpp @@ -43,6 +43,16 @@ namespace Net { + bool Upload::abort() + { + if (m_reply) { + m_reply->abort(); + } else { + m_state = State::AbortedByUser; + } + return true; + } + void Upload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { setProgress(bytesReceived, bytesTotal); } diff --git a/launcher/net/Upload.h b/launcher/net/Upload.h index 56687a31..7c194bbc 100644 --- a/launcher/net/Upload.h +++ b/launcher/net/Upload.h @@ -46,6 +46,8 @@ namespace Net { public: static Upload::Ptr makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data); + auto abort() -> bool override; + auto canAbort() const -> bool override { return true; }; protected slots: void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; From dd6aabf9ab2c974816aef4e889e059fa0cdad53b Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 30 May 2022 13:30:39 -0300 Subject: [PATCH 07/38] feat: add ChooseProviderDialog Allows you to prompt the user for choosing a (mod) provider. This should be fairly independent of the mod updater logic, so it can be used for other ends later down the road :^) Signed-off-by: flow --- launcher/CMakeLists.txt | 3 + launcher/ui/dialogs/ChooseProviderDialog.cpp | 96 ++++++++++++++++++++ launcher/ui/dialogs/ChooseProviderDialog.h | 56 ++++++++++++ launcher/ui/dialogs/ChooseProviderDialog.ui | 89 ++++++++++++++++++ 4 files changed, 244 insertions(+) create mode 100644 launcher/ui/dialogs/ChooseProviderDialog.cpp create mode 100644 launcher/ui/dialogs/ChooseProviderDialog.h create mode 100644 launcher/ui/dialogs/ChooseProviderDialog.ui diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index ecdeaac0..7b8dd9c5 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -836,6 +836,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/ModDownloadDialog.h ui/dialogs/ScrollMessageBox.cpp ui/dialogs/ScrollMessageBox.h + ui/dialogs/ChooseProviderDialog.h + ui/dialogs/ChooseProviderDialog.cpp # GUI - widgets ui/widgets/Common.cpp @@ -941,6 +943,7 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/EditAccountDialog.ui ui/dialogs/ReviewMessageBox.ui ui/dialogs/ScrollMessageBox.ui + ui/dialogs/ChooseProviderDialog.ui ) qt_add_resources(LAUNCHER_RESOURCES diff --git a/launcher/ui/dialogs/ChooseProviderDialog.cpp b/launcher/ui/dialogs/ChooseProviderDialog.cpp new file mode 100644 index 00000000..89935d9a --- /dev/null +++ b/launcher/ui/dialogs/ChooseProviderDialog.cpp @@ -0,0 +1,96 @@ +#include "ChooseProviderDialog.h" +#include "ui_ChooseProviderDialog.h" + +#include +#include + +#include "modplatform/ModIndex.h" + +static ModPlatform::ProviderCapabilities ProviderCaps; + +ChooseProviderDialog::ChooseProviderDialog(QWidget* parent, bool single_choice, bool allow_skipping) + : QDialog(parent), ui(new Ui::ChooseProviderDialog) +{ + ui->setupUi(this); + + addProviders(); + m_providers.button(0)->click(); + + connect(ui->skipOneButton, &QPushButton::clicked, this, &ChooseProviderDialog::skipOne); + connect(ui->skipAllButton, &QPushButton::clicked, this, &ChooseProviderDialog::skipAll); + + connect(ui->confirmOneButton, &QPushButton::clicked, this, &ChooseProviderDialog::confirmOne); + connect(ui->confirmAllButton, &QPushButton::clicked, this, &ChooseProviderDialog::confirmAll); + + if (single_choice) { + ui->providersLayout->removeWidget(ui->skipAllButton); + ui->providersLayout->removeWidget(ui->confirmAllButton); + } + + if (!allow_skipping) { + ui->providersLayout->removeWidget(ui->skipOneButton); + ui->providersLayout->removeWidget(ui->skipAllButton); + } +} + +ChooseProviderDialog::~ChooseProviderDialog() +{ + delete ui; +} + +void ChooseProviderDialog::setDescription(QString desc) +{ + ui->explanationLabel->setText(desc); +} + +void ChooseProviderDialog::skipOne() +{ + reject(); +} +void ChooseProviderDialog::skipAll() +{ + m_response.skip_all = true; + reject(); +} + +void ChooseProviderDialog::confirmOne() +{ + m_response.chosen = getSelectedProvider(); + m_response.try_others = ui->tryOthersCheckbox->isChecked(); + accept(); +} +void ChooseProviderDialog::confirmAll() +{ + m_response.chosen = getSelectedProvider(); + m_response.confirm_all = true; + m_response.try_others = ui->tryOthersCheckbox->isChecked(); + accept(); +} + +auto ChooseProviderDialog::getSelectedProvider() const -> ModPlatform::Provider +{ + return ModPlatform::Provider(m_providers.checkedId()); +} + +void ChooseProviderDialog::addProviders() +{ + int btn_index = 0; + QRadioButton* btn; + + for (auto& provider : { ModPlatform::Provider::MODRINTH, ModPlatform::Provider::FLAME }) { + btn = new QRadioButton(ProviderCaps.readableName(provider), this); + m_providers.addButton(btn, btn_index++); + ui->providersLayout->addWidget(btn); + } +} + +void ChooseProviderDialog::disableInput() +{ + for (auto& btn : m_providers.buttons()) + btn->setEnabled(false); + + ui->skipOneButton->setEnabled(false); + ui->skipAllButton->setEnabled(false); + ui->confirmOneButton->setEnabled(false); + ui->confirmAllButton->setEnabled(false); +} diff --git a/launcher/ui/dialogs/ChooseProviderDialog.h b/launcher/ui/dialogs/ChooseProviderDialog.h new file mode 100644 index 00000000..4a3b9f29 --- /dev/null +++ b/launcher/ui/dialogs/ChooseProviderDialog.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +namespace Ui { +class ChooseProviderDialog; +} + +namespace ModPlatform { +enum class Provider; +} + +class Mod; +class NetJob; +class ModUpdateDialog; + +class ChooseProviderDialog : public QDialog { + Q_OBJECT + + struct Response { + bool skip_all = false; + bool confirm_all = false; + + bool try_others = false; + + ModPlatform::Provider chosen; + }; + + public: + explicit ChooseProviderDialog(QWidget* parent, bool single_choice = false, bool allow_skipping = true); + ~ChooseProviderDialog(); + + auto getResponse() const -> Response { return m_response; } + + void setDescription(QString desc); + + private slots: + void skipOne(); + void skipAll(); + void confirmOne(); + void confirmAll(); + + private: + void addProviders(); + void disableInput(); + + auto getSelectedProvider() const -> ModPlatform::Provider; + + private: + Ui::ChooseProviderDialog* ui; + + QButtonGroup m_providers; + + Response m_response; +}; diff --git a/launcher/ui/dialogs/ChooseProviderDialog.ui b/launcher/ui/dialogs/ChooseProviderDialog.ui new file mode 100644 index 00000000..78cd9613 --- /dev/null +++ b/launcher/ui/dialogs/ChooseProviderDialog.ui @@ -0,0 +1,89 @@ + + + ChooseProviderDialog + + + + 0 + 0 + 453 + 197 + + + + Choose a mod provider + + + + + + Qt::AlignJustify|Qt::AlignTop + + + true + + + -1 + + + + + + + Qt::AlignHCenter|Qt::AlignTop + + + Qt::AlignHCenter|Qt::AlignTop + + + + + + + + + Skip this mod + + + + + + + Skip all + + + + + + + Confirm for all + + + + + + + Confirm + + + true + + + + + + + + + Try to automatically use other providers if the chosen one fails + + + true + + + + + + + + From 9a44c9221139428fa4e3bdf560f6bfdc6fcbe75d Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 2 Jun 2022 19:34:08 -0300 Subject: [PATCH 08/38] feat: add MultipleOptionsTask This is a variation of a Sequential Task, in which a subtask failing will prompt the next one to execute, and a subtask being successful will stop the task. This way, this can be used for easily managing fallbacks with tasks. :D Signed-off-by: flow --- launcher/CMakeLists.txt | 2 ++ launcher/tasks/MultipleOptionsTask.cpp | 48 ++++++++++++++++++++++++++ launcher/tasks/MultipleOptionsTask.h | 19 ++++++++++ launcher/tasks/SequentialTask.h | 14 ++++---- 4 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 launcher/tasks/MultipleOptionsTask.cpp create mode 100644 launcher/tasks/MultipleOptionsTask.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 7b8dd9c5..3be161be 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -397,6 +397,8 @@ set(TASKS_SOURCES tasks/ConcurrentTask.cpp tasks/SequentialTask.h tasks/SequentialTask.cpp + tasks/MultipleOptionsTask.h + tasks/MultipleOptionsTask.cpp ) ecm_add_test(tasks/Task_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test diff --git a/launcher/tasks/MultipleOptionsTask.cpp b/launcher/tasks/MultipleOptionsTask.cpp new file mode 100644 index 00000000..6e853568 --- /dev/null +++ b/launcher/tasks/MultipleOptionsTask.cpp @@ -0,0 +1,48 @@ +#include "MultipleOptionsTask.h" + +#include + +MultipleOptionsTask::MultipleOptionsTask(QObject* parent, const QString& task_name) : SequentialTask(parent, task_name) {} + +void MultipleOptionsTask::startNext() +{ + Task* previous = nullptr; + if (m_currentIndex != -1) { + previous = m_queue[m_currentIndex].get(); + disconnect(previous, 0, this, 0); + } + + m_currentIndex++; + if ((previous && previous->wasSuccessful())) { + emitSucceeded(); + return; + } + + Task::Ptr next = m_queue[m_currentIndex]; + + connect(next.get(), &Task::failed, this, &MultipleOptionsTask::subTaskFailed); + connect(next.get(), &Task::succeeded, this, &MultipleOptionsTask::startNext); + + connect(next.get(), &Task::status, this, &MultipleOptionsTask::subTaskStatus); + connect(next.get(), &Task::stepStatus, this, &MultipleOptionsTask::subTaskStatus); + + connect(next.get(), &Task::progress, this, &MultipleOptionsTask::subTaskProgress); + + qDebug() << QString("Making attemp %1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size()); + setStatus(tr("Making attempt #%1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size())); + setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus()); + + next->start(); +} + +void MultipleOptionsTask::subTaskFailed(QString const& reason) +{ + qDebug() << QString("Failed attempt #%1 of %2. Reason: %3").arg(m_currentIndex + 1).arg(m_queue.size()).arg(reason); + if(m_currentIndex < m_queue.size() - 1) { + startNext(); + return; + } + + qWarning() << QString("All attempts have failed!"); + emitFailed(); +} diff --git a/launcher/tasks/MultipleOptionsTask.h b/launcher/tasks/MultipleOptionsTask.h new file mode 100644 index 00000000..7c508b00 --- /dev/null +++ b/launcher/tasks/MultipleOptionsTask.h @@ -0,0 +1,19 @@ +#pragma once + +#include "SequentialTask.h" + +/* This task type will attempt to do run each of it's subtasks in sequence, + * until one of them succeeds. When that happens, the remaining tasks will not run. + * */ +class MultipleOptionsTask : public SequentialTask +{ + Q_OBJECT +public: + explicit MultipleOptionsTask(QObject *parent = nullptr, const QString& task_name = ""); + virtual ~MultipleOptionsTask() = default; + +private +slots: + void startNext() override; + void subTaskFailed(const QString &msg) override; +}; diff --git a/launcher/tasks/SequentialTask.h b/launcher/tasks/SequentialTask.h index 942ebec2..f5a58b1b 100644 --- a/launcher/tasks/SequentialTask.h +++ b/launcher/tasks/SequentialTask.h @@ -20,17 +20,17 @@ public: void addTask(Task::Ptr task); -protected slots: - void executeTask() override; public slots: bool abort() override; -private +protected slots: - void startNext(); - void subTaskFailed(const QString &msg); - void subTaskStatus(const QString &msg); - void subTaskProgress(qint64 current, qint64 total); + void executeTask() override; + + virtual void startNext(); + virtual void subTaskFailed(const QString &msg); + virtual void subTaskStatus(const QString &msg); + virtual void subTaskProgress(qint64 current, qint64 total); protected: void setStepStatus(QString status) { m_step_status = status; emit stepStatus(status); }; From 32a9545360b10058cf84b951ee88959adf3bf374 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 3 Jun 2022 19:02:11 -0300 Subject: [PATCH 09/38] libs: add murmur2 library Signed-off-by: flow --- CMakeLists.txt | 1 + launcher/CMakeLists.txt | 1 + libraries/README.md | 6 ++ libraries/murmur2/CMakeLists.txt | 12 ++++ libraries/murmur2/src/MurmurHash2.cpp | 86 +++++++++++++++++++++++++++ libraries/murmur2/src/MurmurHash2.h | 30 ++++++++++ 6 files changed, 136 insertions(+) create mode 100644 libraries/murmur2/CMakeLists.txt create mode 100644 libraries/murmur2/src/MurmurHash2.cpp create mode 100644 libraries/murmur2/src/MurmurHash2.h diff --git a/CMakeLists.txt b/CMakeLists.txt index c88a32ba..33c53b82 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -323,6 +323,7 @@ add_subdirectory(libraries/optional-bare) add_subdirectory(libraries/tomlc99) # toml parser add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much add_subdirectory(libraries/gamemode) +add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API ############################### Built Artifacts ############################### diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 3be161be..c02480dd 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -972,6 +972,7 @@ add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHE target_link_libraries(Launcher_logic systeminfo Launcher_classparser + Launcher_murmur2 nbt++ ${ZLIB_LIBRARIES} optional-bare diff --git a/libraries/README.md b/libraries/README.md index 360c34b1..946e34d8 100644 --- a/libraries/README.md +++ b/libraries/README.md @@ -149,6 +149,12 @@ BSD licensed, derived from [QtSingleApplication](https://github.com/qtproject/qt Changes are made to make the code more generic and useful in less usual conditions. +## murmur2 + +Canonical implementation of the murmur2 hash, taken from [SMHasher](https://github.com/aappleby/smhasher). + +Public domain (the author disclaimed the copyright). + ## optional-bare A simple single-file header-only version of a C++17-like optional for default-constructible, copyable types, for C++98 and later. diff --git a/libraries/murmur2/CMakeLists.txt b/libraries/murmur2/CMakeLists.txt new file mode 100644 index 00000000..f3068201 --- /dev/null +++ b/libraries/murmur2/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.9.4) +project(murmur2) + +set(MURMUR_SOURCES + src/MurmurHash2.h + src/MurmurHash2.cpp +) + +add_library(Launcher_murmur2 STATIC ${MURMUR_SOURCES}) +target_include_directories(Launcher_murmur2 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} "src" ) + +generate_export_header(Launcher_murmur2) diff --git a/libraries/murmur2/src/MurmurHash2.cpp b/libraries/murmur2/src/MurmurHash2.cpp new file mode 100644 index 00000000..3e52e6d1 --- /dev/null +++ b/libraries/murmur2/src/MurmurHash2.cpp @@ -0,0 +1,86 @@ +//----------------------------------------------------------------------------- +// MurmurHash2 was written by Austin Appleby, and is placed in the public +// domain. The author hereby disclaims copyright to this source code. + +// Note - This code makes a few assumptions about how your machine behaves - + +// 1. We can read a 4-byte value from any address without crashing +// 2. sizeof(int) == 4 + +// And it has a few limitations - + +// 1. It will not work incrementally. +// 2. It will not produce the same results on little-endian and big-endian +// machines. + +#include "MurmurHash2.h" + +//----------------------------------------------------------------------------- +// Platform-specific functions and macros + +// Microsoft Visual Studio + +#if defined(_MSC_VER) + +#define BIG_CONSTANT(x) (x) + +// Other compilers + +#else // defined(_MSC_VER) + +#define BIG_CONSTANT(x) (x##LLU) + +#endif // !defined(_MSC_VER) + +//----------------------------------------------------------------------------- + +uint64_t MurmurHash2 ( const void* key, int len, uint32_t seed ) +{ + // 'm' and 'r' are mixing constants generated offline. + // They're not really 'magic', they just happen to work well. + + const uint32_t m = 0x5bd1e995; + const int r = 24; + + // Initialize the hash to a 'random' value + + uint32_t h = seed ^ len; + + // Mix 4 bytes at a time into the hash + const auto* data = (const unsigned char*) key; + while(len >= 4) + { + uint32_t k = *(uint32_t*)data; + + k *= m; + k ^= k >> r; + k *= m; + + h *= m; + h ^= k; + + data += 4*sizeof(char); + len -= 4; + } + + // Handle the last few bytes of the input array + + switch(len) + { + case 3: h ^= data[2] << 16; + case 2: h ^= data[1] << 8; + case 1: h ^= data[0]; + h *= m; + }; + + // Do a few final mixes of the hash to ensure the last few + // bytes are well-incorporated. + + h ^= h >> 13; + h *= m; + h ^= h >> 15; + + return h; +} + +//----------------------------------------------------------------------------- diff --git a/libraries/murmur2/src/MurmurHash2.h b/libraries/murmur2/src/MurmurHash2.h new file mode 100644 index 00000000..c7b83bca --- /dev/null +++ b/libraries/murmur2/src/MurmurHash2.h @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------------- +// MurmurHash2 was written by Austin Appleby, and is placed in the public +// domain. The author hereby disclaims copyright to this source code. + +#pragma once + +//----------------------------------------------------------------------------- +// Platform-specific functions and macros + +// Microsoft Visual Studio + +#if defined(_MSC_VER) && (_MSC_VER < 1600) + +typedef unsigned char uint8_t; +typedef unsigned int uint32_t; +typedef unsigned __int64 uint64_t; + +// Other compilers + +#else // defined(_MSC_VER) + +#include + +#endif // !defined(_MSC_VER) + +//----------------------------------------------------------------------------- + +uint64_t MurmurHash2 ( const void* key, int len, uint32_t seed = 1 ); + +//----------------------------------------------------------------------------- From 0e52112016fe9942f7448cd83914f6266904c311 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 3 Jun 2022 19:04:49 -0300 Subject: [PATCH 10/38] feat: add some api calls to modrinth Calls added: - Get version from hash - Get versions from hashes - Latest version of a project from a hash, loader(s), and game version(s) - Latest versions of multiple project from hashes, loader(s), and game version(s) Some of those are not used yet, but may be of use later on, so we have it if we need it :) Signed-off-by: flow --- launcher/CMakeLists.txt | 2 +- launcher/modplatform/ModIndex.h | 1 + launcher/modplatform/modrinth/ModrinthAPI.cpp | 97 +++++++++++++++++++ launcher/modplatform/modrinth/ModrinthAPI.h | 22 +++++ .../modrinth/ModrinthPackIndex.cpp | 26 +++-- .../modplatform/modrinth/ModrinthPackIndex.h | 2 +- 6 files changed, 141 insertions(+), 9 deletions(-) create mode 100644 launcher/modplatform/modrinth/ModrinthAPI.cpp diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index c02480dd..085cc211 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -484,7 +484,7 @@ set(API_SOURCES modplatform/flame/FlameAPI.h modplatform/modrinth/ModrinthAPI.h - + modplatform/modrinth/ModrinthAPI.cpp modplatform/helpers/NetworkModAPI.h modplatform/helpers/NetworkModAPI.cpp ) diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index c27643af..459eb261 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -61,6 +61,7 @@ struct IndexedVersion { QVector loaders = {}; QString hash_type; QString hash; + bool is_preferred = true; }; struct ExtraPackData { diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp new file mode 100644 index 00000000..0d5d4bab --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -0,0 +1,97 @@ +#include "ModrinthAPI.h" + +#include "Application.h" +#include "Json.h" +#include "net/Upload.h" + +auto ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* response) -> NetJob::Ptr +{ + auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersion"), APPLICATION->network()); + + netJob->addNetAction(Net::Download::makeByteArray( + QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1?algorithm=%2").arg(hash, hash_format), response)); + + QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + + return netJob; +} + +auto ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response) -> NetJob::Ptr +{ + auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersions"), APPLICATION->network()); + + QJsonObject body_obj; + + Json::writeStringList(body_obj, "hashes", hashes); + Json::writeString(body_obj, "algorithm", hash_format); + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw)); + + QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + + return netJob; +} + +auto ModrinthAPI::latestVersion(QString hash, + QString hash_format, + std::list mcVersions, + ModLoaderTypes loaders, + QByteArray* response) -> NetJob::Ptr +{ + auto* netJob = new NetJob(QString("Modrinth::GetLatestVersion"), APPLICATION->network()); + + QJsonObject body_obj; + + Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders)); + + QStringList game_versions; + for (auto& ver : mcVersions) { + game_versions.append(ver.toString()); + } + Json::writeStringList(body_obj, "game_versions", game_versions); + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction(Net::Upload::makeByteArray( + QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1/update?algorithm=%2").arg(hash, hash_format), response, body_raw)); + + QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + + return netJob; +} + +auto ModrinthAPI::latestVersions(const QStringList& hashes, + QString hash_format, + std::list mcVersions, + ModLoaderTypes loaders, + QByteArray* response) -> NetJob::Ptr +{ + auto* netJob = new NetJob(QString("Modrinth::GetLatestVersions"), APPLICATION->network()); + + QJsonObject body_obj; + + Json::writeStringList(body_obj, "hashes", hashes); + Json::writeString(body_obj, "algorithm", hash_format); + + Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders)); + + QStringList game_versions; + for (auto& ver : mcVersions) { + game_versions.append(ver.toString()); + } + Json::writeStringList(body_obj, "game_versions", game_versions); + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction(Net::Upload::makeByteArray( + QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), response, body_raw)); + + QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + + return netJob; +} diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 89e52d6c..9694b85e 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -22,10 +22,32 @@ #include "modplatform/ModAPI.h" #include "modplatform/ModIndex.h" #include "modplatform/helpers/NetworkModAPI.h" +#include "net/NetJob.h" #include class ModrinthAPI : public NetworkModAPI { + public: + auto currentVersion(QString hash, + QString hash_format, + QByteArray* response) -> NetJob::Ptr; + + auto currentVersions(const QStringList& hashes, + QString hash_format, + QByteArray* response) -> NetJob::Ptr; + + auto latestVersion(QString hash, + QString hash_format, + std::list mcVersions, + ModLoaderTypes loaders, + QByteArray* response) -> NetJob::Ptr; + + auto latestVersions(const QStringList& hashes, + QString hash_format, + std::list mcVersions, + ModLoaderTypes loaders, + QByteArray* response) -> NetJob::Ptr; + public: inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index b6f5490a..4e738819 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -111,7 +111,7 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, pack.versionsLoaded = true; } -auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedVersion +auto Modrinth::loadIndexedPackVersion(QJsonObject &obj, QString preferred_hash_type, QString preferred_file_name) -> ModPlatform::IndexedVersion { ModPlatform::IndexedVersion file; @@ -142,6 +142,11 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV auto parent = files[i].toObject(); auto fileName = Json::requireString(parent, "filename"); + if (!preferred_file_name.isEmpty() && fileName.contains(preferred_file_name)) { + file.is_preferred = true; + break; + } + // Grab the primary file, if available if (Json::requireBoolean(parent, "primary")) break; @@ -153,13 +158,20 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV if (parent.contains("url")) { file.downloadUrl = Json::requireString(parent, "url"); file.fileName = Json::requireString(parent, "filename"); + file.is_preferred = Json::requireBoolean(parent, "primary"); auto hash_list = Json::requireObject(parent, "hashes"); - auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH); - for (auto& hash_type : hash_types) { - if (hash_list.contains(hash_type)) { - file.hash = Json::requireString(hash_list, hash_type); - file.hash_type = hash_type; - break; + + if (hash_list.contains(preferred_hash_type)) { + file.hash = Json::requireString(hash_list, preferred_hash_type); + file.hash_type = preferred_hash_type; + } else { + auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH); + for (auto& hash_type : hash_types) { + if (hash_list.contains(hash_type)) { + file.hash = Json::requireString(hash_list, hash_type); + file.hash_type = hash_type; + break; + } } } diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index b7936204..31881414 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -30,6 +30,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr& network, BaseInstance* inst); -auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion; +auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion; } // namespace Modrinth From 4bcf8e6975d4f314654062dde01d627ed7495866 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 3 Jun 2022 19:06:51 -0300 Subject: [PATCH 11/38] feat: add api call to flame Call added: - Get Fingerprints Matches - Get Mod File Changelog Signed-off-by: flow --- launcher/CMakeLists.txt | 1 + launcher/modplatform/flame/FlameAPI.cpp | 125 +++++++++++++++++++ launcher/modplatform/flame/FlameAPI.h | 8 ++ launcher/modplatform/flame/FlameModIndex.cpp | 2 +- 4 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 launcher/modplatform/flame/FlameAPI.cpp diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 085cc211..6dcea8e2 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -483,6 +483,7 @@ set(API_SOURCES modplatform/ModAPI.h modplatform/flame/FlameAPI.h + modplatform/flame/FlameAPI.cpp modplatform/modrinth/ModrinthAPI.h modplatform/modrinth/ModrinthAPI.cpp modplatform/helpers/NetworkModAPI.h diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp new file mode 100644 index 00000000..983e09fd --- /dev/null +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -0,0 +1,125 @@ +#include "FlameAPI.h" +#include "FlameModIndex.h" + +#include "Application.h" +#include "BuildConfig.h" +#include "Json.h" + +#include "net/Upload.h" + +auto FlameAPI::matchFingerprints(const std::list& fingerprints, QByteArray* response) -> NetJob::Ptr +{ + auto* netJob = new NetJob(QString("Flame::MatchFingerprints"), APPLICATION->network()); + + QJsonObject body_obj; + QJsonArray fingerprints_arr; + for (auto& fp : fingerprints) { + fingerprints_arr.append(QString("%1").arg(fp)); + } + + body_obj["fingerprints"] = fingerprints_arr; + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/fingerprints"), response, body_raw)); + + QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + + return netJob; +} + +auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString +{ + QEventLoop lock; + QString changelog; + + auto* netJob = new NetJob(QString("Flame::FileChangelog"), APPLICATION->network()); + auto* response = new QByteArray(); + netJob->addNetAction(Net::Download::makeByteArray( + QString("https://api.curseforge.com/v1/mods/%1/files/%2/changelog") + .arg(QString::fromStdString(std::to_string(modId)), QString::fromStdString(std::to_string(fileId))), + response)); + + QObject::connect(netJob, &NetJob::succeeded, [netJob, response, &changelog] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Flame::FileChangelog at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + netJob->failed(parse_error.errorString()); + return; + } + + changelog = Json::ensureString(doc.object(), "data"); + }); + + QObject::connect(netJob, &NetJob::finished, [response, &lock] { + delete response; + lock.quit(); + }); + + netJob->start(); + lock.exec(); + + return changelog; +} + +auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion +{ + QEventLoop loop; + + auto netJob = new NetJob(QString("Flame::GetLatestVersion(%1)").arg(args.addonId), APPLICATION->network()); + auto response = new QByteArray(); + ModPlatform::IndexedVersion ver; + + netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response)); + + QObject::connect(netJob, &NetJob::succeeded, [response, args, &ver] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from latest mod version at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + try { + auto obj = Json::requireObject(doc); + auto arr = Json::requireArray(obj, "data"); + + QJsonObject latest_file_obj; + ModPlatform::IndexedVersion ver_tmp; + + for (auto file : arr) { + auto file_obj = Json::requireObject(file); + auto file_tmp = FlameMod::loadIndexedPackVersion(file_obj); + if(file_tmp.date > ver_tmp.date) { + ver_tmp = file_tmp; + latest_file_obj = file_obj; + } + } + + ver = FlameMod::loadIndexedPackVersion(latest_file_obj); + } catch (Json::JsonException& e) { + qCritical() << "Failed to parse response from a version request."; + qCritical() << e.what(); + qDebug() << doc; + } + }); + + QObject::connect(netJob, &NetJob::finished, [response, netJob, &loop] { + netJob->deleteLater(); + delete response; + loop.quit(); + }); + + netJob->start(); + + loop.exec(); + + return ver; +} diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index aea76ff1..e45b5cb1 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -3,7 +3,15 @@ #include "modplatform/ModIndex.h" #include "modplatform/helpers/NetworkModAPI.h" +#include "net/NetJob.h" + class FlameAPI : public NetworkModAPI { + public: + auto matchFingerprints(const std::list& fingerprints, QByteArray* response) -> NetJob::Ptr; + auto getModFileChangelog(int modId, int fileId) -> QString; + + auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion; + private: inline auto getSortFieldInt(QString sortString) const -> int { diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index b99bfb26..0ad1d4ba 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -110,7 +110,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedV file.fileId = Json::requireInteger(obj, "id"); file.date = Json::requireString(obj, "fileDate"); file.version = Json::requireString(obj, "displayName"); - file.downloadUrl = Json::requireString(obj, "downloadUrl"); + file.downloadUrl = Json::ensureString(obj, "downloadUrl"); file.fileName = Json::requireString(obj, "fileName"); auto hash_list = Json::ensureArray(obj, "hashes"); From 844b2457769d61131f97b5e82bb134568dfd42ed Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 3 Jun 2022 19:08:01 -0300 Subject: [PATCH 12/38] feat: add EnsureMetadataTask This task is responsible for checking if the mod has metadata for a specific provider, and create it if it doesn't. In the context of the mod updater, this is not the best architecture, since we do a single task for each mod. However, this way of structuring it allows us to use it later on in more diverse scenarios. This way we decouple this task from the mod updater, trading off some performance (though that will be mitigated when we have a way of running arbitrary tasks concurrently). Signed-off-by: flow --- launcher/CMakeLists.txt | 3 + launcher/modplatform/EnsureMetadataTask.cpp | 244 ++++++++++++++++++++ launcher/modplatform/EnsureMetadataTask.h | 41 ++++ 3 files changed, 288 insertions(+) create mode 100644 launcher/modplatform/EnsureMetadataTask.cpp create mode 100644 launcher/modplatform/EnsureMetadataTask.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 6dcea8e2..25546c38 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -482,6 +482,9 @@ set(API_SOURCES modplatform/ModAPI.h + modplatform/EnsureMetadataTask.h + modplatform/EnsureMetadataTask.cpp + modplatform/flame/FlameAPI.h modplatform/flame/FlameAPI.cpp modplatform/modrinth/ModrinthAPI.h diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp new file mode 100644 index 00000000..dc92d8ab --- /dev/null +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -0,0 +1,244 @@ +#include "EnsureMetadataTask.h" + +#include +#include + +#include "FileSystem.h" +#include "Json.h" +#include "minecraft/mod/Mod.h" +#include "minecraft/mod/tasks/LocalModUpdateTask.h" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlameModIndex.h" +#include "modplatform/modrinth/ModrinthAPI.h" +#include "modplatform/modrinth/ModrinthPackIndex.h" +#include "net/NetJob.h" +#include "tasks/MultipleOptionsTask.h" + +static ModPlatform::ProviderCapabilities ProviderCaps; + +static ModrinthAPI modrinth_api; +static FlameAPI flame_api; + +EnsureMetadataTask::EnsureMetadataTask(Mod& mod, QDir& dir, bool try_all, ModPlatform::Provider prov) + : m_mod(mod), m_index_dir(dir), m_provider(prov), m_try_all(try_all) +{} + +bool EnsureMetadataTask::abort() +{ + return m_task_handler->abort(); +} + +void EnsureMetadataTask::executeTask() +{ + // They already have the right metadata :o + if (m_mod.status() != ModStatus::NoMetadata && m_mod.metadata() && m_mod.metadata()->provider == m_provider) { + emitReady(); + return; + } + + // Folders don't have metadata + if (m_mod.type() == Mod::MOD_FOLDER) { + emitReady(); + return; + } + + setStatus(tr("Generating %1's metadata...").arg(m_mod.name())); + qDebug() << QString("Generating %1's metadata...").arg(m_mod.name()); + + QByteArray jar_data; + + try { + jar_data = FS::read(m_mod.fileinfo().absoluteFilePath()); + } catch (FS::FileSystemException& e) { + qCritical() << QString("Failed to open / read JAR file of %1").arg(m_mod.name()); + qCritical() << QString("Reason: ") << e.cause(); + + emitFail(); + return; + } + + auto tsk = new MultipleOptionsTask(nullptr, "GetMetadataTask"); + + switch (m_provider) { + case (ModPlatform::Provider::MODRINTH): + modrinthEnsureMetadata(*tsk, jar_data); + if (m_try_all) + flameEnsureMetadata(*tsk, jar_data); + + break; + case (ModPlatform::Provider::FLAME): + flameEnsureMetadata(*tsk, jar_data); + if (m_try_all) + modrinthEnsureMetadata(*tsk, jar_data); + + break; + } + + connect(tsk, &MultipleOptionsTask::finished, this, [tsk] { tsk->deleteLater(); }); + connect(tsk, &MultipleOptionsTask::failed, [this] { + qCritical() << QString("Download of %1's metadata failed").arg(m_mod.name()); + + emitFail(); + }); + connect(tsk, &MultipleOptionsTask::succeeded, this, &EnsureMetadataTask::emitReady); + + m_task_handler = tsk; + + tsk->start(); +} + +void EnsureMetadataTask::emitReady() +{ + emit metadataReady(); + emitSucceeded(); +} + +void EnsureMetadataTask::emitFail() +{ + qDebug() << QString("Failed to generate metadata for %1").arg(m_mod.name()); + emit metadataFailed(); + //emitFailed(tr("Failed to generate metadata for %1").arg(m_mod.name())); + emitSucceeded(); +} + +void EnsureMetadataTask::modrinthEnsureMetadata(SequentialTask& tsk, QByteArray& jar_data) +{ + // Modrinth currently garantees that some hash types will always be present. + // But let's be sure and cover all cases anyways :) + for (auto hash_type : ProviderCaps.hashType(ModPlatform::Provider::MODRINTH)) { + auto* response = new QByteArray(); + auto hash = QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, hash_type).toHex()); + auto ver_task = modrinth_api.currentVersion(hash, hash_type, response); + + // Prevents unfortunate timings when aborting the task + if (!ver_task) + return; + + connect(ver_task.get(), &NetJob::succeeded, this, [this, ver_task, response] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from " << m_mod.name() << " at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + ver_task->failed(parse_error.errorString()); + return; + } + + auto doc_obj = Json::requireObject(doc); + auto ver = Modrinth::loadIndexedPackVersion(doc_obj, {}, m_mod.fileinfo().fileName()); + + // Minimal IndexedPack to create the metadata + ModPlatform::IndexedPack pack; + pack.name = m_mod.name(); + pack.provider = ModPlatform::Provider::MODRINTH; + pack.addonId = ver.addonId; + + // Prevent file name mismatch + ver.fileName = m_mod.fileinfo().fileName(); + + QDir tmp_index_dir(m_index_dir); + + { + LocalModUpdateTask update_metadata(m_index_dir, pack, ver); + QEventLoop loop; + QTimer timeout; + + QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); + QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + + update_metadata.start(); + timeout.start(100); + + loop.exec(); + } + + auto mod_name = m_mod.name(); + auto meta = new Metadata::ModStruct(Metadata::get(tmp_index_dir, mod_name)); + m_mod.setMetadata(meta); + }); + + tsk.addTask(ver_task); + } +} + +void EnsureMetadataTask::flameEnsureMetadata(SequentialTask& tsk, QByteArray& jar_data) +{ + QByteArray jar_data_treated; + for (char c : jar_data) { + // CF-specific + if (!(c == 9 || c == 10 || c == 13 || c == 32)) + jar_data_treated.push_back(c); + } + + auto* response = new QByteArray(); + + std::list fingerprints; + auto murmur = MurmurHash2(jar_data_treated, jar_data_treated.length()); + fingerprints.push_back(murmur); + + auto ver_task = flame_api.matchFingerprints(fingerprints, response); + + connect(ver_task.get(), &Task::succeeded, this, [this, ver_task, response] { + QDir tmp_index_dir(m_index_dir); + + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from " << m_mod.name() << " at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + ver_task->failed(parse_error.errorString()); + return; + } + + try { + auto doc_obj = Json::requireObject(doc); + auto data_obj = Json::ensureObject(doc_obj, "data"); + auto match_obj = Json::ensureObject(Json::ensureArray(data_obj, "exactMatches")[0], {}); + if (match_obj.isEmpty()) { + qCritical() << "Fingerprint match is empty!"; + + ver_task->failed(parse_error.errorString()); + return; + } + + auto file_obj = Json::ensureObject(match_obj, "file"); + + ModPlatform::IndexedPack pack; + pack.name = m_mod.name(); + pack.provider = ModPlatform::Provider::FLAME; + pack.addonId = Json::requireInteger(file_obj, "modId"); + + ModPlatform::IndexedVersion ver = FlameMod::loadIndexedPackVersion(file_obj); + + // Prevent file name mismatch + ver.fileName = m_mod.fileinfo().fileName(); + + { + LocalModUpdateTask update_metadata(m_index_dir, pack, ver); + QEventLoop loop; + QTimer timeout; + + QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); + QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + + update_metadata.start(); + timeout.start(100); + + loop.exec(); + } + + auto mod_name = m_mod.name(); + auto meta = new Metadata::ModStruct(Metadata::get(tmp_index_dir, mod_name)); + m_mod.setMetadata(meta); + + } catch (Json::JsonException& e) { + emitFailed(e.cause() + " : " + e.what()); + } + }); + + tsk.addTask(ver_task); +} diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h new file mode 100644 index 00000000..624e253a --- /dev/null +++ b/launcher/modplatform/EnsureMetadataTask.h @@ -0,0 +1,41 @@ +#pragma once + +#include "ModIndex.h" +#include "tasks/SequentialTask.h" + +class Mod; +class QDir; +class MultipleOptionsTask; + +class EnsureMetadataTask : public Task { + Q_OBJECT + + public: + EnsureMetadataTask(Mod&, QDir&, bool try_all, ModPlatform::Provider = ModPlatform::Provider::MODRINTH); + + public slots: + bool abort() override; + protected slots: + void executeTask() override; + + private: + // FIXME: Move to their own namespace + void modrinthEnsureMetadata(SequentialTask&, QByteArray&); + void flameEnsureMetadata(SequentialTask&, QByteArray&); + + // Helpers + void emitReady(); + void emitFail(); + + signals: + void metadataReady(); + void metadataFailed(); + + private: + Mod& m_mod; + QDir& m_index_dir; + ModPlatform::Provider m_provider; + bool m_try_all; + + MultipleOptionsTask* m_task_handler = nullptr; +}; From c3f6c3dd8228b23b403af6b091a888ea3cc436ca Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 3 Jun 2022 20:45:44 -0300 Subject: [PATCH 13/38] feat: add changelog to mod providers The Modrinth changelog is fairly straight-forward, as it's given to us directly with the API call we already did. Flame, on the other hand, requires us to do another call to get the changelog, so it can introduce quite a heavy performance impact. This way, we make it optional to get such changelog. Signed-off-by: flow --- launcher/modplatform/ModIndex.h | 1 + launcher/modplatform/flame/FlameModIndex.cpp | 7 ++++++- launcher/modplatform/flame/FlameModIndex.h | 2 +- launcher/modplatform/modrinth/ModrinthPackIndex.cpp | 1 + 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 459eb261..966082ab 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -62,6 +62,7 @@ struct IndexedVersion { QString hash_type; QString hash; bool is_preferred = true; + QString changelog; }; struct ExtraPackData { diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 0ad1d4ba..a3222f44 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -7,6 +7,7 @@ #include "net/NetJob.h" static ModPlatform::ProviderCapabilities ProviderCaps; +static FlameAPI api; void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { @@ -91,7 +92,7 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, pack.versionsLoaded = true; } -auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion +auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> ModPlatform::IndexedVersion { auto versionArray = Json::requireArray(obj, "gameVersions"); if (versionArray.isEmpty()) { @@ -124,5 +125,9 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedV break; } } + + if(load_changelog) + file.changelog = api.getModFileChangelog(file.addonId.toInt(), file.fileId.toInt()); + return file; } diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index 9c6c1c6c..a839dd83 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -17,6 +17,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr& network, BaseInstance* inst); -auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion; +auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion; } // namespace FlameMod diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 4e738819..9736e861 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -130,6 +130,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj, QString preferred_hash_t file.loaders.append(loader.toString()); } file.version = Json::requireString(obj, "name"); + file.changelog = Json::requireString(obj, "changelog"); auto files = Json::requireArray(obj, "files"); int i = 0; From b8b71c7dd29fbdc6c98d60ec54c57cff74f4cbfd Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 3 Jun 2022 21:26:26 -0300 Subject: [PATCH 14/38] feat: add mod update check tasks Those tasks take a list of mods and check on the mod providers for updates. They assume that the mods have metadata already. Signed-off-by: flow --- launcher/CMakeLists.txt | 6 + launcher/modplatform/CheckUpdateTask.h | 51 ++++ launcher/modplatform/ModIndex.h | 1 + .../modplatform/flame/FlameCheckUpdate.cpp | 228 ++++++++++++++++++ launcher/modplatform/flame/FlameCheckUpdate.h | 25 ++ .../modrinth/ModrinthCheckUpdate.cpp | 163 +++++++++++++ .../modrinth/ModrinthCheckUpdate.h | 23 ++ .../modrinth/ModrinthPackIndex.cpp | 3 +- 8 files changed, 499 insertions(+), 1 deletion(-) create mode 100644 launcher/modplatform/CheckUpdateTask.h create mode 100644 launcher/modplatform/flame/FlameCheckUpdate.cpp create mode 100644 launcher/modplatform/flame/FlameCheckUpdate.h create mode 100644 launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp create mode 100644 launcher/modplatform/modrinth/ModrinthCheckUpdate.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 25546c38..2313f0e4 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -485,6 +485,8 @@ set(API_SOURCES modplatform/EnsureMetadataTask.h modplatform/EnsureMetadataTask.cpp + modplatform/CheckUpdateTask.h + modplatform/flame/FlameAPI.h modplatform/flame/FlameAPI.cpp modplatform/modrinth/ModrinthAPI.h @@ -514,6 +516,8 @@ set(FLAME_SOURCES modplatform/flame/PackManifest.cpp modplatform/flame/FileResolvingTask.h modplatform/flame/FileResolvingTask.cpp + modplatform/flame/FlameCheckUpdate.cpp + modplatform/flame/FlameCheckUpdate.h ) set(MODRINTH_SOURCES @@ -521,6 +525,8 @@ set(MODRINTH_SOURCES modplatform/modrinth/ModrinthPackIndex.h modplatform/modrinth/ModrinthPackManifest.cpp modplatform/modrinth/ModrinthPackManifest.h + modplatform/modrinth/ModrinthCheckUpdate.cpp + modplatform/modrinth/ModrinthCheckUpdate.h ) set(MODPACKSCH_SOURCES diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h new file mode 100644 index 00000000..8c701e46 --- /dev/null +++ b/launcher/modplatform/CheckUpdateTask.h @@ -0,0 +1,51 @@ +#pragma once + +#include "minecraft/mod/Mod.h" +#include "modplatform/ModAPI.h" +#include "modplatform/ModIndex.h" +#include "tasks/Task.h" + +class ModDownloadTask; +class ModFolderModel; + +class CheckUpdateTask : public Task { + Q_OBJECT + + public: + CheckUpdateTask(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) + : m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder) {}; + + struct UpdatableMod { + QString name; + QString old_hash; + QString old_version; + QString new_version; + QString changelog; + ModPlatform::Provider provider; + ModDownloadTask* download; + + public: + UpdatableMod(QString name, QString old_h, QString old_v, QString new_v, QString changelog, ModPlatform::Provider p, ModDownloadTask* t) + : name(name), old_hash(old_h), old_version(old_v), new_version(new_v), changelog(changelog), provider(p), download(t) + {} + }; + + auto getUpdatable() -> std::vector&& { return std::move(m_updatable); } + + public slots: + bool abort() override = 0; + + protected slots: + void executeTask() override = 0; + + signals: + void checkFailed(Mod failed, QString reason, QUrl recover_url = {}); + + protected: + std::list& m_mods; + std::list& m_game_versions; + ModAPI::ModLoaderTypes m_loaders; + std::shared_ptr m_mods_folder; + + std::vector m_updatable; +}; diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 966082ab..f8ef211e 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -54,6 +54,7 @@ struct IndexedVersion { QVariant addonId; QVariant fileId; QString version; + QString version_number = {}; QVector mcVersion; QString downloadUrl; QString date; diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp new file mode 100644 index 00000000..f1983fa4 --- /dev/null +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -0,0 +1,228 @@ +#include "FlameCheckUpdate.h" +#include "FlameAPI.h" +#include "FlameModIndex.h" + +#include + +#include "FileSystem.h" +#include "Json.h" + +#include "ModDownloadTask.h" + +static FlameAPI api; +static ModPlatform::ProviderCapabilities ProviderCaps; + +bool FlameCheckUpdate::abort() +{ + m_was_aborted = true; + if (m_net_job) + return m_net_job->abort(); + return true; +} + +ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info) +{ + ModPlatform::IndexedPack pack; + + QEventLoop loop; + + auto get_project_job = new NetJob("Flame::GetProjectJob", APPLICATION->network()); + + auto response = new QByteArray(); + auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(ver_info.addonId.toString()); + auto dl = Net::Download::makeByteArray(url, response); + get_project_job->addNetAction(dl); + + QObject::connect(get_project_job, &NetJob::succeeded, [response, &pack]() { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + try { + auto doc_obj = Json::requireObject(doc); + auto data_obj = Json::requireObject(doc_obj, "data"); + FlameMod::loadIndexedPack(pack, data_obj); + } catch (Json::JsonException& e) { + qWarning() << e.cause(); + qDebug() << doc; + } + }); + + QObject::connect(get_project_job, &NetJob::finished, [&loop, get_project_job] { + get_project_job->deleteLater(); + loop.quit(); + }); + + get_project_job->start(); + loop.exec(); + + return pack; +} + +/* Check for update: + * - Get latest version available + * - Compare hash of the latest version with the current hash + * - If equal, no updates, else, there's updates, so add to the list + * */ +void FlameCheckUpdate::executeTask() +{ + setStatus(tr("Preparing mods for CurseForge...")); + setProgress(0, 5); + + QHash mappings; + + // Create all hashes + QStringList hashes; + std::list murmur_hashes; + + auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::FLAME).first(); + for (auto mod : m_mods) { + auto hash = mod.metadata()->hash; + + QByteArray jar_data; + + try { + jar_data = FS::read(mod.fileinfo().absoluteFilePath()); + } catch (FS::FileSystemException& e) { + qCritical() << QString("Failed to open / read JAR file of %1").arg(mod.name()); + qCritical() << QString("Reason: ") << e.cause(); + + failed(e.what()); + return; + } + + QByteArray jar_data_treated; + for (char c : jar_data) { + // CF-specific + if (!(c == 9 || c == 10 || c == 13 || c == 32)) + jar_data_treated.push_back(c); + } + + auto murmur_hash = MurmurHash2(jar_data_treated, jar_data_treated.length()); + murmur_hashes.emplace_back(murmur_hash); + + // Sadly the API can only handle one hash type per call, se we + // need to generate a new hash if the current one is innadequate + // (though it will rarely happen, if at all) + if (mod.metadata()->hash_format != best_hash_type) + hash = QString(ProviderCaps.hash(ModPlatform::Provider::FLAME, jar_data, best_hash_type).toHex()); + + hashes.append(hash); + mappings.insert(hash, mod); + } + + auto* response = new QByteArray(); + auto job = api.matchFingerprints(murmur_hashes, response); + + QEventLoop lock; + + connect(job.get(), &Task::succeeded, this, [this, response, &mappings] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + failed(parse_error.errorString()); + return; + } + + setStatus(tr("Parsing the first API response from CurseForge...")); + setProgress(2, 5); + + try { + auto doc_obj = Json::requireObject(doc); + auto data_obj = Json::ensureObject(doc_obj, "data"); + auto match_arr = Json::ensureArray(data_obj, "exactMatches"); + for (auto match : match_arr) { + auto match_obj = Json::ensureObject(match); + + ModPlatform::IndexedVersion current_ver; + try { + auto file_obj = Json::requireObject(match_obj, "file"); + current_ver = FlameMod::loadIndexedPackVersion(file_obj); + } catch (Json::JsonException& e) { + qCritical() << "Error while parsing Flame indexed version"; + qCritical() << e.what(); + failed(tr("An error occured while parsing a CurseForge indexed version!")); + return; + } + + auto mod_iter = mappings.find(current_ver.hash); + if (mod_iter == mappings.end()) { + qCritical() << "Failed to remap mod from Flame!"; + continue; + } + + auto mod = mod_iter.value(); + + setStatus(tr("Waiting for the API response from CurseForge for '%1'...").arg(mod.name())); + setProgress(3, 5); + + auto latest_ver = api.getLatestVersion({ current_ver.addonId.toString(), m_game_versions, m_loaders }); + + // Check if we were aborted while getting the latest version + if (m_was_aborted) { + aborted(); + return; + } + + setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(mod.name())); + setProgress(4, 5); + + if (!latest_ver.addonId.isValid()) { + emit checkFailed( + mod, + tr("No valid version found for this mod. It's probably unavailable for the current game version / mod loader.")); + continue; + } + + if (latest_ver.downloadUrl.isEmpty() && latest_ver.fileId != current_ver.fileId) { + auto pack = getProjectInfo(latest_ver); + auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver.fileId.toString()); + emit checkFailed(mod, tr("Mod has a new update available, but is opted-out on CurseForge"), recover_url); + + continue; + } + + if (!latest_ver.hash.isEmpty() && current_ver.hash != latest_ver.hash) { + // Fake pack with the necessary info to pass to the download task :) + ModPlatform::IndexedPack pack; + pack.name = mod.name(); + pack.addonId = mod.metadata()->project_id; + pack.websiteUrl = mod.homeurl(); + for (auto& author : mod.authors()) + pack.authors.append({ author }); + pack.description = mod.description(); + pack.provider = ModPlatform::Provider::FLAME; + + auto download_task = new ModDownloadTask(pack, latest_ver, m_mods_folder); + m_updatable.emplace_back(mod.name(), current_ver.hash, current_ver.version, latest_ver.version, + api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()), + ModPlatform::Provider::FLAME, download_task); + } + } + + } catch (Json::JsonException& e) { + failed(e.cause() + " : " + e.what()); + } + }); + + connect(job.get(), &Task::finished, &lock, &QEventLoop::quit); + + setStatus(tr("Waiting for the first API response from CurseForge...")); + setProgress(1, 5); + + m_net_job = job.get(); + job->start(); + + lock.exec(); + + emitSucceeded(); +} diff --git a/launcher/modplatform/flame/FlameCheckUpdate.h b/launcher/modplatform/flame/FlameCheckUpdate.h new file mode 100644 index 00000000..f068f08f --- /dev/null +++ b/launcher/modplatform/flame/FlameCheckUpdate.h @@ -0,0 +1,25 @@ +#pragma once + +#include "Application.h" +#include "modplatform/CheckUpdateTask.h" +#include "net/NetJob.h" + +class FlameCheckUpdate : public CheckUpdateTask { + Q_OBJECT + + public: + FlameCheckUpdate(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) + : CheckUpdateTask(mods, mcVersions, loaders, mods_folder) + {} + + public slots: + bool abort() override; + + protected slots: + void executeTask() override; + + private: + NetJob* m_net_job = nullptr; + + bool m_was_aborted = false; +}; diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp new file mode 100644 index 00000000..81a2652a --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -0,0 +1,163 @@ +#include "ModrinthCheckUpdate.h" +#include "ModrinthAPI.h" +#include "ModrinthPackIndex.h" + +#include "FileSystem.h" +#include "Json.h" + +#include "ModDownloadTask.h" + +static ModrinthAPI api; +static ModPlatform::ProviderCapabilities ProviderCaps; + +bool ModrinthCheckUpdate::abort() +{ + if (m_net_job) + return m_net_job->abort(); + return true; +} + +/* Check for update: + * - Get latest version available + * - Compare hash of the latest version with the current hash + * - If equal, no updates, else, there's updates, so add to the list + * */ +void ModrinthCheckUpdate::executeTask() +{ + setStatus(tr("Preparing mods for Modrinth...")); + setProgress(0, 3); + + QHash mappings; + + // Create all hashes + QStringList hashes; + auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first(); + for (auto mod : m_mods) { + auto hash = mod.metadata()->hash; + + // Sadly the API can only handle one hash type per call, se we + // need to generate a new hash if the current one is innadequate + // (though it will rarely happen, if at all) + if (mod.metadata()->hash_format != best_hash_type) { + QByteArray jar_data; + + try { + jar_data = FS::read(mod.fileinfo().absoluteFilePath()); + } catch (FS::FileSystemException& e) { + qCritical() << QString("Failed to open / read JAR file of %1").arg(mod.name()); + qCritical() << QString("Reason: ") << e.cause(); + + failed(e.what()); + return; + } + + hash = QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, best_hash_type).toHex()); + } + + hashes.append(hash); + mappings.insert(hash, mod); + } + + auto* response = new QByteArray(); + auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response); + + QEventLoop lock; + + connect(job.get(), &Task::succeeded, this, [this, response, &mappings, best_hash_type, job] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + failed(parse_error.errorString()); + return; + } + + setStatus(tr("Parsing the API response from Modrinth...")); + setProgress(2, 3); + + try { + for (auto hash : mappings.keys()) { + auto project_obj = doc[hash].toObject(); + if (project_obj.isEmpty()) { + qDebug() << "Mod " << mappings.find(hash).value().name() << " got an empty response."; + qDebug() << "Hash: " << hash; + + emit checkFailed(mappings.find(hash).value(), tr("Couldn't find mod in Modrinth")); + + continue; + } + + // Sometimes a version may have multiple files, one with "forge" and one with "fabric", + // so we may want to filter it + QString loader_filter; + static auto flags = { ModAPI::ModLoaderType::Forge, ModAPI::ModLoaderType::Fabric, ModAPI::ModLoaderType::Quilt }; + for (auto flag : flags) { + if (m_loaders.testFlag(flag)) { + loader_filter = api.getModLoaderString(flag); + break; + } + } + + // Currently, we rely on a couple heuristics to determine whether an update is actually available or not: + // - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the loader_filter + // - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case) + // Such is the pain of having arbitrary files for a given version .-. + + auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, best_hash_type, loader_filter); + if (project_ver.downloadUrl.isEmpty()) { + qCritical() << "Modrinth mod without download url!"; + qCritical() << project_ver.fileName; + + emit checkFailed(mappings.find(hash).value(), tr("Mod has an empty download URL")); + + continue; + } + + auto mod_iter = mappings.find(hash); + if (mod_iter == mappings.end()) { + qCritical() << "Failed to remap mod from Modrinth!"; + continue; + } + auto mod = *mod_iter; + + auto key = project_ver.hash; + if ((key != hash && project_ver.is_preferred) || (mod.status() == ModStatus::NotInstalled)) { + if (mod.version() == project_ver.version_number) + continue; + + // Fake pack with the necessary info to pass to the download task :) + ModPlatform::IndexedPack pack; + pack.name = mod.name(); + pack.addonId = mod.metadata()->project_id; + pack.websiteUrl = mod.homeurl(); + for (auto& author : mod.authors()) + pack.authors.append({ author }); + pack.description = mod.description(); + pack.provider = ModPlatform::Provider::MODRINTH; + + auto download_task = new ModDownloadTask(pack, project_ver, m_mods_folder); + + m_updatable.emplace_back(mod.name(), hash, mod.version(), project_ver.version_number, project_ver.changelog, + ModPlatform::Provider::MODRINTH, download_task); + } + } + } catch (Json::JsonException& e) { + failed(e.cause() + " : " + e.what()); + } + }); + + connect(job.get(), &Task::finished, &lock, &QEventLoop::quit); + + setStatus(tr("Waiting for the API response from Modrinth...")); + setProgress(1, 3); + + m_net_job = job.get(); + job->start(); + + lock.exec(); + + emitSucceeded(); +} diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h new file mode 100644 index 00000000..7e685a6d --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Application.h" +#include "modplatform/CheckUpdateTask.h" +#include "net/NetJob.h" + +class ModrinthCheckUpdate : public CheckUpdateTask { + Q_OBJECT + + public: + ModrinthCheckUpdate(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) + : CheckUpdateTask(mods, mcVersions, loaders, mods_folder) + {} + + public slots: + bool abort() override; + + protected slots: + void executeTask() override; + + private: + NetJob* m_net_job = nullptr; +}; diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 9736e861..1910c9be 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -130,6 +130,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj, QString preferred_hash_t file.loaders.append(loader.toString()); } file.version = Json::requireString(obj, "name"); + file.version_number = Json::requireString(obj, "version_number"); file.changelog = Json::requireString(obj, "changelog"); auto files = Json::requireArray(obj, "files"); @@ -159,7 +160,7 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj, QString preferred_hash_t if (parent.contains("url")) { file.downloadUrl = Json::requireString(parent, "url"); file.fileName = Json::requireString(parent, "filename"); - file.is_preferred = Json::requireBoolean(parent, "primary"); + file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1); auto hash_list = Json::requireObject(parent, "hashes"); if (hash_list.contains(preferred_hash_type)) { From 6e2869834f59ae4863e63a16de97aa3019723b26 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 4 Jun 2022 16:22:46 -0300 Subject: [PATCH 15/38] feat: add mod update dialog This subclasses the Review mods dialog to make a "Update review" one. Also, all the necessary components built until now are put together in a coherent unity that checks and generates metadata on-the-fly and checks for mod updates, while giving and receiving feedback to the user. Signed-off-by: flow --- launcher/CMakeLists.txt | 2 + .../modplatform/flame/FlameCheckUpdate.cpp | 19 +- .../modrinth/ModrinthCheckUpdate.cpp | 5 +- launcher/ui/dialogs/ModUpdateDialog.cpp | 317 ++++++++++++++++++ launcher/ui/dialogs/ModUpdateDialog.h | 61 ++++ launcher/ui/dialogs/ScrollMessageBox.ui | 2 +- 6 files changed, 390 insertions(+), 16 deletions(-) create mode 100644 launcher/ui/dialogs/ModUpdateDialog.cpp create mode 100644 launcher/ui/dialogs/ModUpdateDialog.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 2313f0e4..3c9aee6a 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -850,6 +850,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/ScrollMessageBox.h ui/dialogs/ChooseProviderDialog.h ui/dialogs/ChooseProviderDialog.cpp + ui/dialogs/ModUpdateDialog.cpp + ui/dialogs/ModUpdateDialog.h # GUI - widgets ui/widgets/Common.cpp diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index f1983fa4..12d99029 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -74,16 +74,13 @@ void FlameCheckUpdate::executeTask() setStatus(tr("Preparing mods for CurseForge...")); setProgress(0, 5); - QHash mappings; + QHash mappings; // Create all hashes - QStringList hashes; std::list murmur_hashes; auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::FLAME).first(); for (auto mod : m_mods) { - auto hash = mod.metadata()->hash; - QByteArray jar_data; try { @@ -106,14 +103,7 @@ void FlameCheckUpdate::executeTask() auto murmur_hash = MurmurHash2(jar_data_treated, jar_data_treated.length()); murmur_hashes.emplace_back(murmur_hash); - // Sadly the API can only handle one hash type per call, se we - // need to generate a new hash if the current one is innadequate - // (though it will rarely happen, if at all) - if (mod.metadata()->hash_format != best_hash_type) - hash = QString(ProviderCaps.hash(ModPlatform::Provider::FLAME, jar_data, best_hash_type).toHex()); - - hashes.append(hash); - mappings.insert(hash, mod); + mappings.insert(mod.metadata()->mod_id().toInt(), mod); } auto* response = new QByteArray(); @@ -154,9 +144,10 @@ void FlameCheckUpdate::executeTask() return; } - auto mod_iter = mappings.find(current_ver.hash); + auto mod_iter = mappings.find(current_ver.addonId.toInt()); if (mod_iter == mappings.end()) { qCritical() << "Failed to remap mod from Flame!"; + qDebug() << match_obj; continue; } @@ -191,7 +182,7 @@ void FlameCheckUpdate::executeTask() continue; } - if (!latest_ver.hash.isEmpty() && current_ver.hash != latest_ver.hash) { + if (!latest_ver.hash.isEmpty() && (current_ver.hash != latest_ver.hash || mod.status() == ModStatus::NotInstalled)) { // Fake pack with the necessary info to pass to the download task :) ModPlatform::IndexedPack pack; pack.name = mod.name(); diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index 81a2652a..981c4216 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -81,11 +81,14 @@ void ModrinthCheckUpdate::executeTask() try { for (auto hash : mappings.keys()) { auto project_obj = doc[hash].toObject(); + + // If the returned project is empty, but we have Modrinth metadata, + // it means this specific version is not available if (project_obj.isEmpty()) { qDebug() << "Mod " << mappings.find(hash).value().name() << " got an empty response."; qDebug() << "Hash: " << hash; - emit checkFailed(mappings.find(hash).value(), tr("Couldn't find mod in Modrinth")); + emit checkFailed(mappings.find(hash).value(), tr("Couldn't find the latest version of this mod with the correct mod loader and game version.")); continue; } diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp new file mode 100644 index 00000000..b60fd304 --- /dev/null +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -0,0 +1,317 @@ +#include "ModUpdateDialog.h" +#include "ChooseProviderDialog.h" +#include "CustomMessageBox.h" +#include "ProgressDialog.h" +#include "ScrollMessageBox.h" +#include "ui_ReviewMessageBox.h" + +#include "FileSystem.h" +#include "Json.h" + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +#include "modplatform/EnsureMetadataTask.h" +#include "modplatform/flame/FlameCheckUpdate.h" +#include "modplatform/modrinth/ModrinthCheckUpdate.h" + +#include + +#include +#include + +static ModPlatform::ProviderCapabilities ProviderCaps; + +static std::list mcVersions(BaseInstance* inst) +{ + return { static_cast(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() }; +} + +static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst) +{ + return { static_cast(inst)->getPackProfile()->getModLoaders() }; +} + +ModUpdateDialog::ModUpdateDialog(QWidget* parent, + BaseInstance* instance, + const std::shared_ptr& mods, + std::list& search_for) + : ReviewMessageBox(parent, tr("Confirm mods to update"), "") + , m_parent(parent) + , m_mod_model(mods) + , m_candidates(search_for) + , m_instance(instance) +{ + ReviewMessageBox::setGeometry(0, 0, 800, 600); + + ui->explainLabel->setText(tr("You're about to update the following mods:")); + ui->onlyCheckedLabel->setText(tr("Only mods with a check will be updated!")); + + connect(&m_check_task, &Task::failed, this, + [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + + connect(&m_check_task, &Task::succeeded, this, [&]() { + QStringList warnings = m_check_task.warnings(); + if (warnings.count()) { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); + } + }); +} + +void ModUpdateDialog::checkCandidates() +{ + // Ensure mods have valid metadata + auto went_well = ensureMetadata(); + if (!went_well) { + m_aborted = true; + return; + } + + // Report failed metadata generation + if (!m_failed_metadata.empty()) { + QString text; + for (const auto& mod : m_failed_metadata) { + text += tr("Mod name: %1
File name: %2

").arg(mod.name(), mod.fileinfo().fileName()); + } + + ScrollMessageBox message_dialog(m_parent, tr("Metadata generation failed"), + tr("Could not generate metadata for the following mods:
" + "Do you wish to proceed without those mods?"), + text); + message_dialog.setModal(true); + if (message_dialog.exec() == QDialog::Rejected) { + m_aborted = true; + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); + return; + } + } + + auto versions = mcVersions(m_instance); + auto loaders = mcLoaders(m_instance); + + if (!m_modrinth_to_update.empty()) { + m_modrinth_check_task = new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_mod_model); + connect(m_modrinth_check_task, &CheckUpdateTask::checkFailed, this, + [this](Mod mod, QString reason, QUrl recover_url) { m_failed_check_update.emplace_back(mod, reason, recover_url); }); + m_check_task.addTask(m_modrinth_check_task); + } + + if (!m_flame_to_update.empty()) { + m_flame_check_task = new FlameCheckUpdate(m_flame_to_update, versions, loaders, m_mod_model); + connect(m_flame_check_task, &CheckUpdateTask::checkFailed, this, + [this](Mod mod, QString reason, QUrl recover_url) { m_failed_check_update.emplace_back(mod, reason, recover_url); }); + m_check_task.addTask(m_flame_check_task); + } + + // Check for updates + ProgressDialog progress_dialog(m_parent); + progress_dialog.setSkipButton(true, tr("Abort")); + progress_dialog.setVisible(true); + progress_dialog.setWindowTitle(tr("Checking for updates...")); + auto ret = progress_dialog.execWithTask(&m_check_task); + + // If the dialog was skipped / some download error happened + if (ret == QDialog::DialogCode::Rejected) { + m_aborted = true; + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); + return; + } + + // Add found updates for Modrinth + if (m_modrinth_check_task) { + auto modrinth_updates = m_modrinth_check_task->getUpdatable(); + for (auto& updatable : modrinth_updates) { + qDebug() << QString("Mod %1 has an update available!").arg(updatable.name); + + appendMod(updatable); + m_tasks.insert(updatable.name, updatable.download); + } + } + + // Add found updated for Flame + if (m_flame_check_task) { + auto flame_updates = m_flame_check_task->getUpdatable(); + for (auto& updatable : flame_updates) { + qDebug() << QString("Mod %1 has an update available!").arg(updatable.name); + + appendMod(updatable); + m_tasks.insert(updatable.name, updatable.download); + } + } + + // Report failed update checking + if (!m_failed_check_update.empty()) { + QString text; + for (const auto& failed : m_failed_check_update) { + const auto& mod = std::get<0>(failed); + const auto& reason = std::get<1>(failed); + const auto& recover_url = std::get<2>(failed); + + qDebug() << mod.name() << " failed to check for updates!"; + + text += tr("Mod name: %1").arg(mod.name()) + "
"; + if (!reason.isEmpty()) + text += tr("Reason: %1").arg(reason) + "
"; + if (!recover_url.isEmpty()) + text += tr("Possible solution: ") + tr("Getting the latest version manually:") + "
" + + QString("").arg(recover_url.toString()) + recover_url.toString() + "
"; + text += "
"; + } + + ScrollMessageBox message_dialog(m_parent, tr("Failed to check for updates"), + tr("Could not check or get the following mods for updates:
" + "Do you wish to proceed without those mods?"), + text); + message_dialog.setModal(true); + if (message_dialog.exec() == QDialog::Rejected) { + m_aborted = true; + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); + return; + } + } + + // If there's no mod to be updated + if (ui->modTreeWidget->topLevelItemCount() == 0) + m_no_updates = true; + + if (m_aborted || m_no_updates) + QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); +} + +// Part 1: Ensure we have a valid metadata +auto ModUpdateDialog::ensureMetadata() -> bool +{ + auto index_dir = indexDir(); + + auto* seq = new SequentialTask(m_parent, tr("Looking for metadata")); + + bool confirm_rest = false; + bool try_others_rest = false; + bool skip_rest = false; + ModPlatform::Provider provider_rest = ModPlatform::Provider::MODRINTH; + + for (auto& candidate : m_candidates) { + if (candidate.status() != ModStatus::NoMetadata) { + onMetadataEnsured(candidate); + continue; + } + + if (skip_rest) + continue; + + if (confirm_rest) { + auto* task = new EnsureMetadataTask(candidate, index_dir, try_others_rest, provider_rest); + connect(task, &EnsureMetadataTask::metadataReady, [this, &candidate] { onMetadataEnsured(candidate); }); + connect(task, &EnsureMetadataTask::metadataFailed, [this, &candidate] { onMetadataFailed(candidate); }); + seq->addTask(task); + continue; + } + + ChooseProviderDialog chooser(this); + chooser.setDescription(tr("This mod (%1) does not have a metadata yet. We need to create one in order to keep relevant " + "information on how to update this " + "mod. To do this, please select a mod provider from which we can search for updates for %1.") + .arg(candidate.name())); + auto confirmed = chooser.exec() == QDialog::DialogCode::Accepted; + + auto response = chooser.getResponse(); + + if (response.skip_all) + skip_rest = true; + if (response.confirm_all) { + confirm_rest = true; + provider_rest = response.chosen; + try_others_rest = response.try_others; + } + + if (confirmed) { + auto* task = new EnsureMetadataTask(candidate, index_dir, response.try_others, response.chosen); + connect(task, &EnsureMetadataTask::metadataReady, [this, &candidate] { onMetadataEnsured(candidate); }); + connect(task, &EnsureMetadataTask::metadataFailed, [this, &candidate] { onMetadataFailed(candidate); }); + seq->addTask(task); + } + } + + ProgressDialog checking_dialog(m_parent); + checking_dialog.setSkipButton(true, tr("Abort")); + checking_dialog.setWindowTitle(tr("Generating metadata...")); + auto ret_metadata = checking_dialog.execWithTask(seq); + + return (ret_metadata != QDialog::DialogCode::Rejected); +} + +void ModUpdateDialog::onMetadataEnsured(Mod& mod) +{ + // When the mod is a folder, for instance + if (!mod.metadata()) + return; + + switch (mod.metadata()->provider) { + case ModPlatform::Provider::MODRINTH: + m_modrinth_to_update.push_back(mod); + break; + case ModPlatform::Provider::FLAME: + m_flame_to_update.push_back(mod); + break; + } +} + +void ModUpdateDialog::onMetadataFailed(Mod& mod) +{ + m_failed_metadata.push_back(mod); +} + +void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info) +{ + auto item_top = new QTreeWidgetItem(ui->modTreeWidget); + item_top->setCheckState(0, Qt::CheckState::Checked); + item_top->setText(0, info.name); + item_top->setExpanded(true); + + auto provider_item = new QTreeWidgetItem(item_top); + provider_item->setText(0, tr("Provider: %1").arg(ProviderCaps.readableName(info.provider))); + + auto old_version_item = new QTreeWidgetItem(item_top); + old_version_item->setText(0, tr("Old version: %1").arg(info.old_version.isEmpty() ? tr("Not installed") : info.old_version)); + + auto new_version_item = new QTreeWidgetItem(item_top); + new_version_item->setText(0, tr("New version: %1").arg(info.new_version)); + + auto changelog_item = new QTreeWidgetItem(item_top); + changelog_item->setText(0, tr("Changelog of the latest version")); + + auto changelog = new QTreeWidgetItem(changelog_item); + + auto changelog_area = new QTextEdit(); + HoeDown h; + changelog_area->setText(h.process(info.changelog.toUtf8())); + changelog_area->setReadOnly(true); + if (info.changelog.size() < 250) // heuristic + changelog_area->setSizeAdjustPolicy(QTextEdit::SizeAdjustPolicy::AdjustToContents); + + ui->modTreeWidget->setItemWidget(changelog, 0, changelog_area); + changelog_item->insertChildren(0, { changelog }); + + item_top->insertChildren(0, { old_version_item }); + item_top->insertChildren(1, { new_version_item }); + item_top->insertChildren(2, { changelog_item }); + + ui->modTreeWidget->addTopLevelItem(item_top); +} + +auto ModUpdateDialog::getTasks() -> const std::list +{ + std::list list; + + auto* item = ui->modTreeWidget->topLevelItem(0); + + for (int i = 0; item != nullptr; ++i) { + if (item->checkState(0) == Qt::CheckState::Checked) { + list.push_back(m_tasks.find(item->text(0)).value()); + } + + item = ui->modTreeWidget->topLevelItem(i); + } + + return list; +} diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h new file mode 100644 index 00000000..30cd5cbd --- /dev/null +++ b/launcher/ui/dialogs/ModUpdateDialog.h @@ -0,0 +1,61 @@ +#pragma once + +#include "BaseInstance.h" +#include "ModDownloadTask.h" +#include "ReviewMessageBox.h" + +#include "minecraft/mod/ModFolderModel.h" + +#include "modplatform/CheckUpdateTask.h" + +class Mod; +class ModrinthCheckUpdate; +class FlameCheckUpdate; + +class ModUpdateDialog final : public ReviewMessageBox { + Q_OBJECT + public: + explicit ModUpdateDialog(QWidget* parent, + BaseInstance* instance, + const std::shared_ptr& mod_model, + std::list& search_for); + + void checkCandidates(); + + void appendMod(const CheckUpdateTask::UpdatableMod& info); + + const std::list getTasks(); + auto indexDir() const -> QDir { return m_mod_model->indexDir(); } + + auto noUpdates() const -> bool { return m_no_updates; }; + auto aborted() const -> bool { return m_aborted; }; + + private: + auto ensureMetadata() -> bool; + + private slots: + void onMetadataEnsured(Mod&); + void onMetadataFailed(Mod&); + + private: + QWidget* m_parent; + + SequentialTask m_check_task; + ModrinthCheckUpdate* m_modrinth_check_task = nullptr; + FlameCheckUpdate* m_flame_check_task = nullptr; + + const std::shared_ptr& m_mod_model; + + std::list& m_candidates; + std::list m_modrinth_to_update; + std::list m_flame_to_update; + + std::list m_failed_metadata; + std::list> m_failed_check_update; + + QHash m_tasks; + BaseInstance* m_instance; + + bool m_no_updates = false; + bool m_aborted = false; +}; diff --git a/launcher/ui/dialogs/ScrollMessageBox.ui b/launcher/ui/dialogs/ScrollMessageBox.ui index 299d2ecc..e684185f 100644 --- a/launcher/ui/dialogs/ScrollMessageBox.ui +++ b/launcher/ui/dialogs/ScrollMessageBox.ui @@ -6,7 +6,7 @@ 0 0 - 400 + 500 455 From 2d10c246a8e80a5af7535e2ff94442c6db178486 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 4 Jun 2022 21:18:51 -0300 Subject: [PATCH 16/38] feat: add update mods to the ui / mod model Signed-off-by: flow --- launcher/minecraft/mod/ModFolderModel.cpp | 12 +++ launcher/minecraft/mod/ModFolderModel.h | 2 + .../pages/instance/ExternalResourcesPage.ui | 11 +++ launcher/ui/pages/instance/ModFolderPage.cpp | 78 ++++++++++++++++++- launcher/ui/pages/instance/ModFolderPage.h | 1 + 5 files changed, 101 insertions(+), 3 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 5ee08cbf..e72eb13e 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -259,6 +259,18 @@ bool ModFolderModel::isValid() return m_dir.exists() && m_dir.isReadable(); } +auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> std::list +{ + std::list selected_mods; + for (auto i : indexes) { + if(i.column() != 0) + continue; + + selected_mods.push_back(mods[i.row()]); + } + return selected_mods; +} + // FIXME: this does not take disabled mod (with extra .disable extension) into account... bool ModFolderModel::installMod(const QString &filename) { diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 24b4d358..c3b493b8 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -144,6 +144,8 @@ public: return mods; } + auto selectedMods(QModelIndexList& indexes) -> std::list; + public slots: void disableInteraction(bool disabled); diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui index 17bf455a..8edcfd64 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -147,6 +147,17 @@ Download a new resource + + + false + + + Check for &Updates + + + "Tries to find / update all selected resources (all resources if none is selected)" + +
diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 4432ccc8..a40cef77 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -49,6 +49,7 @@ #include "ui/GuiUtil.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ModDownloadDialog.h" +#include "ui/dialogs/ModUpdateDialog.h" #include "DesktopServices.h" @@ -78,6 +79,23 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); connect(ui->actionDownloadItem, &QAction::triggered, this, &ModFolderPage::installMods); + + ui->actionUpdateItem->setToolTip(tr("Tries to find / update all selected mods (all mods if none is selected)")); + ui->actionsToolbar->insertActionAfter(ui->actionAddItem, ui->actionUpdateItem); + connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods); + + connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, + [this] { ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); }); + + connect(mods.get(), &ModFolderModel::rowsInserted, this, + [this] { ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); }); + + connect(mods.get(), &ModFolderModel::updateFinished, this, [this, mods] { + ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); + + // Prevent a weird crash when trying to open the mods page twice in a session o.O + disconnect(mods.get(), &ModFolderModel::updateFinished, this, 0); + }); } } @@ -107,7 +125,6 @@ bool CoreModFolderPage::shouldDisplay() const return false; if (version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate) return true; - } return false; } @@ -118,7 +135,7 @@ void ModFolderPage::installMods() return; 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() == ModAPI::Unspecified) { QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!")); @@ -140,7 +157,7 @@ void ModFolderPage::installMods() QStringList warnings = tasks->warnings(); if (warnings.count()) CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); - + tasks->deleteLater(); }); @@ -155,3 +172,58 @@ void ModFolderPage::installMods() m_model->update(); } } + +void ModFolderPage::updateMods() +{ + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + + auto mods_list = m_model->selectedMods(selection); + bool use_all = mods_list.empty(); + if (use_all) + mods_list = m_model->allMods().toStdList(); + + ModUpdateDialog update_dialog(this, m_instance, m_model, mods_list); + update_dialog.checkCandidates(); + + if (update_dialog.aborted()) { + CustomMessageBox::selectable(this, tr("Aborted"), tr("The mod updater was aborted!"), QMessageBox::Warning)->show(); + return; + } + if (update_dialog.noUpdates()) { + CustomMessageBox::selectable(this, tr("Update checker"), + (mods_list.size() == 1) + ? tr("'%1' is up-to-date! :)").arg(mods_list.front().name()) + : tr("All %1mods are up-to-date! :)").arg(use_all ? "" : (tr("selected") + " "))) + ->exec(); + return; + } + + if (update_dialog.exec()) { + ConcurrentTask* tasks = new ConcurrentTask(this); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::aborted, [this, tasks]() { + CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } + tasks->deleteLater(); + }); + + for (auto task : update_dialog.getTasks()) { + tasks->addTask(task); + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } +} diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 19caa732..0a7fc9fa 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -56,6 +56,7 @@ class ModFolderPage : public ExternalResourcesPage { private slots: void installMods(); + void updateMods(); }; class CoreModFolderPage : public ModFolderPage { From 1709b47bb7fd325c1b6dd482524fffa428b0f5a9 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 10 Jun 2022 16:40:39 -0300 Subject: [PATCH 17/38] fix: don't double add mods in mod downloader/updater Signed-off-by: flow --- launcher/ui/dialogs/ModUpdateDialog.cpp | 2 +- launcher/ui/dialogs/ReviewMessageBox.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index b60fd304..a4d83483 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -305,7 +305,7 @@ auto ModUpdateDialog::getTasks() -> const std::list auto* item = ui->modTreeWidget->topLevelItem(0); - for (int i = 0; item != nullptr; ++i) { + for (int i = 1; item != nullptr; ++i) { if (item->checkState(0) == Qt::CheckState::Checked) { list.push_back(m_tasks.find(item->text(0)).value()); } diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index c92234a4..e664e566 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -40,7 +40,7 @@ auto ReviewMessageBox::deselectedMods() -> QStringList auto* item = ui->modTreeWidget->topLevelItem(0); - for (int i = 0; item != nullptr; ++i) { + for (int i = 1; item != nullptr; ++i) { if (item->checkState(0) == Qt::CheckState::Unchecked) { list.append(item->text(0)); } From dfab55112b783d191ac9b596df9c2972b5fe74cb Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 10 Jun 2022 16:43:01 -0300 Subject: [PATCH 18/38] feat: remove existing mod when updating/redownloading it Signed-off-by: flow --- launcher/ModDownloadTask.cpp | 11 ++++++++++- launcher/ModDownloadTask.h | 5 +++++ launcher/minecraft/mod/Mod.cpp | 7 +++++-- launcher/minecraft/mod/ModFolderModel.cpp | 14 ++++++++++++++ launcher/minecraft/mod/ModFolderModel.h | 2 ++ .../minecraft/mod/tasks/LocalModUpdateTask.cpp | 5 +++++ launcher/minecraft/mod/tasks/LocalModUpdateTask.h | 3 +++ 7 files changed, 44 insertions(+), 3 deletions(-) diff --git a/launcher/ModDownloadTask.cpp b/launcher/ModDownloadTask.cpp index 41856fb5..b1dd88d3 100644 --- a/launcher/ModDownloadTask.cpp +++ b/launcher/ModDownloadTask.cpp @@ -27,6 +27,7 @@ ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::Inde { if (is_indexed) { m_update_task.reset(new LocalModUpdateTask(mods->indexDir(), m_mod, m_mod_version)); + connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ModDownloadTask::hasOldMod); addTask(m_update_task); } @@ -40,12 +41,13 @@ ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::Inde connect(m_filesNetJob.get(), &NetJob::failed, this, &ModDownloadTask::downloadFailed); addTask(m_filesNetJob); - } void ModDownloadTask::downloadSucceeded() { m_filesNetJob.reset(); + if (!std::get<0>(to_delete).isEmpty()) + mods->uninstallMod(std::get<1>(to_delete), true); } void ModDownloadTask::downloadFailed(QString reason) @@ -58,3 +60,10 @@ void ModDownloadTask::downloadProgressChanged(qint64 current, qint64 total) { emit progress(current, total); } + +// This indirection is done so that we don't delete a mod before being sure it was +// downloaded successfully! +void ModDownloadTask::hasOldMod(QString name, QString filename) +{ + to_delete = {name, filename}; +} diff --git a/launcher/ModDownloadTask.h b/launcher/ModDownloadTask.h index 6e204e70..95020470 100644 --- a/launcher/ModDownloadTask.h +++ b/launcher/ModDownloadTask.h @@ -46,6 +46,11 @@ private: void downloadFailed(QString reason); void downloadSucceeded(); + + std::tuple to_delete {"", ""}; + +private slots: + void hasOldMod(QString name, QString filename); }; diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 37ec8eca..81bb902f 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -183,9 +183,12 @@ auto Mod::details() const -> const ModDetails& auto Mod::name() const -> QString { auto d_name = details().name; - if (!d_name.isEmpty()) { + if (!d_name.isEmpty()) return d_name; - } + + if (status() != ModStatus::NoMetadata) + return metadata()->name; + return m_name; } diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index e72eb13e..adc828c2 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -356,6 +356,20 @@ bool ModFolderModel::installMod(const QString &filename) return false; } +bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata) +{ + + for(auto mod : allMods()){ + if(mod.fileinfo().fileName() == filename){ + auto index_dir = indexDir(); + mod.destroy(index_dir, preserve_metadata); + return true; + } + } + + return false; +} + bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusAction enable) { if(interaction_disabled) { diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index c3b493b8..10289f8d 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -118,6 +118,8 @@ public: */ bool installMod(const QString& filename); + bool uninstallMod(const QString& filename, bool preserve_metadata = false); + /// Deletes all the selected mods bool deleteMods(const QModelIndexList &indexes); diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp index 1bdecb8c..c73e855e 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp @@ -44,6 +44,11 @@ void LocalModUpdateTask::executeTask() { setStatus(tr("Updating index for mod:\n%1").arg(m_mod.name)); + auto old_metadata = Metadata::get(m_index_dir, m_mod.name); + if (old_metadata.isValid()) { + emit hasOldMod(old_metadata.name, old_metadata.filename); + } + auto pw_mod = Metadata::create(m_index_dir, m_mod, m_mod_version); Metadata::update(m_index_dir, pw_mod); diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h index 2db183e0..1d2f06a6 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h @@ -37,6 +37,9 @@ class LocalModUpdateTask : public Task { //! Entry point for tasks. void executeTask() override; + signals: + void hasOldMod(QString name, QString filename); + private: QDir m_index_dir; ModPlatform::IndexedPack& m_mod; From 9a07ede615869e3df87c41a689ebbba16a433849 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 11 Jun 2022 05:38:40 -0300 Subject: [PATCH 19/38] fix: filter out opted-out mods in mod downloader Maintains Pre-Updater behaviour Signed-off-by: flow --- launcher/ui/pages/modplatform/flame/FlameModPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 10d34218..772fd2e0 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -64,7 +64,7 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool { Q_UNUSED(loaders); - return ver.mcVersion.contains(mineVer); + return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty(); } // I don't know why, but doing this on the parent class makes it so that From 91a5c4bdcbd3ae18139b85899f051fb3d9cbd1fc Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 11 Jun 2022 17:19:34 -0300 Subject: [PATCH 20/38] feat: add metadata get/delete via mod id This is, in many cases, more reliable than name comparisons, so it's useful specially in cases where a mod changes name between versions Signed-off-by: flow --- launcher/ModDownloadTask.cpp | 8 +++-- launcher/minecraft/mod/MetadataHandler.h | 10 ++++++ launcher/minecraft/mod/Mod.cpp | 8 ++--- .../mod/tasks/LocalModUpdateTask.cpp | 2 +- launcher/modplatform/packwiz/Packwiz.cpp | 33 +++++++++++++++++-- launcher/modplatform/packwiz/Packwiz.h | 8 +++++ 6 files changed, 57 insertions(+), 12 deletions(-) diff --git a/launcher/ModDownloadTask.cpp b/launcher/ModDownloadTask.cpp index b1dd88d3..7d35ff69 100644 --- a/launcher/ModDownloadTask.cpp +++ b/launcher/ModDownloadTask.cpp @@ -46,8 +46,12 @@ ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::Inde void ModDownloadTask::downloadSucceeded() { m_filesNetJob.reset(); - if (!std::get<0>(to_delete).isEmpty()) - mods->uninstallMod(std::get<1>(to_delete), true); + auto name = std::get<0>(to_delete); + if (!name.isEmpty()) { + // If they have the same name, we keep the metadata. + // This is a workaround for mods that change names between versions ;c + mods->uninstallMod(std::get<1>(to_delete), name == m_mod.name); + } } void ModDownloadTask::downloadFailed(QString reason) diff --git a/launcher/minecraft/mod/MetadataHandler.h b/launcher/minecraft/mod/MetadataHandler.h index 56962818..d5f01c42 100644 --- a/launcher/minecraft/mod/MetadataHandler.h +++ b/launcher/minecraft/mod/MetadataHandler.h @@ -52,8 +52,18 @@ class Metadata { Packwiz::V1::deleteModIndex(index_dir, mod_name); } + static void remove(QDir& index_dir, QVariant& mod_id) + { + Packwiz::V1::deleteModIndex(index_dir, mod_id); + } + static auto get(QDir& index_dir, QString& mod_name) -> ModStruct { return Packwiz::V1::getIndexForMod(index_dir, mod_name); } + + static auto get(QDir& index_dir, QVariant& mod_id) -> ModStruct + { + return Packwiz::V1::getIndexForMod(index_dir, mod_id); + } }; diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 81bb902f..bba7b342 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -164,12 +164,8 @@ void Mod::setMetadata(Metadata::ModStruct* metadata) auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool { - auto n = name(); - // FIXME: This can fail to remove the metadata if the - // "ModMetadataDisabled" setting is on, since there could - // be a name mismatch! - if(!preserve_metadata) - Metadata::remove(index_dir, n); + if (!preserve_metadata && status() != ModStatus::NoMetadata) + Metadata::remove(index_dir, metadata()->mod_id()); m_type = MOD_UNKNOWN; return FS::deletePath(m_file.filePath()); diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp index c73e855e..f0ef795d 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp @@ -44,7 +44,7 @@ void LocalModUpdateTask::executeTask() { setStatus(tr("Updating index for mod:\n%1").arg(m_mod.name)); - auto old_metadata = Metadata::get(m_index_dir, m_mod.name); + auto old_metadata = Metadata::get(m_index_dir, m_mod.addonId); if (old_metadata.isValid()) { emit hasOldMod(old_metadata.name, old_metadata.filename); } diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 0782b9f4..8bd66088 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -144,6 +144,9 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) QFile index_file(index_dir.absoluteFilePath(real_fname)); + if (real_fname != normalized_fname) + index_file.rename(normalized_fname); + // There's already data on there! // TODO: We should do more stuff here, as the user is likely trying to // override a file. In this case, check versions and ask the user what @@ -196,16 +199,28 @@ void V1::deleteModIndex(QDir& index_dir, QString& mod_name) QFile index_file(index_dir.absoluteFilePath(real_fname)); - if(!index_file.exists()){ + if (!index_file.exists()) { qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_name); return; } - if(!index_file.remove()){ + if (!index_file.remove()) { qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_name); } } +void V1::deleteModIndex(QDir& index_dir, QVariant& mod_id) +{ + for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) { + auto mod = getIndexForMod(index_dir, file_name); + + if (mod.mod_id() == mod_id) { + deleteModIndex(index_dir, mod.name); + break; + } + } +} + auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod { Mod mod; @@ -286,4 +301,16 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod return mod; } -} // namespace Packwiz +auto V1::getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod +{ + for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) { + auto mod = getIndexForMod(index_dir, file_name); + + if (mod.mod_id() == mod_id) + return mod; + } + + return {}; +} + +} // namespace Packwiz diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 3c99769c..9d643703 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -84,10 +84,18 @@ class V1 { /* Deletes the metadata for the mod with the given name. If the metadata doesn't exist, it does nothing. */ static void deleteModIndex(QDir& index_dir, QString& mod_name); + /* Deletes the metadata for the mod with the given id. If the metadata doesn't exist, it does nothing. */ + static void deleteModIndex(QDir& index_dir, QVariant& mod_id); + /* Gets the metadata for a mod with a particular name. * If the mod doesn't have a metadata, it simply returns an empty Mod object. * */ static auto getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod; + + /* Gets the metadata for a mod with a particular id. + * If the mod doesn't have a metadata, it simply returns an empty Mod object. + * */ + static auto getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod; }; } // namespace Packwiz From 4e6978ff6f61777a2e2e989cba58a9f0c48d2782 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 16 Jun 2022 11:45:29 -0300 Subject: [PATCH 21/38] feat: improve metadata gen. networking and performance This makes the metadata generation code a lot messier and harder to use, but there's not really much else that can be done about it while preserving all it's capabilities :( At least we now have speed Signed-off-by: flow --- launcher/modplatform/CheckUpdateTask.h | 2 +- launcher/modplatform/EnsureMetadataTask.cpp | 604 +++++++++++++----- launcher/modplatform/EnsureMetadataTask.h | 35 +- .../modplatform/flame/FlameCheckUpdate.cpp | 209 +++--- .../modrinth/ModrinthCheckUpdate.cpp | 5 +- launcher/ui/dialogs/ModUpdateDialog.cpp | 113 +++- launcher/ui/dialogs/ModUpdateDialog.h | 8 +- 7 files changed, 641 insertions(+), 335 deletions(-) diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h index 8c701e46..d96fc340 100644 --- a/launcher/modplatform/CheckUpdateTask.h +++ b/launcher/modplatform/CheckUpdateTask.h @@ -13,7 +13,7 @@ class CheckUpdateTask : public Task { public: CheckUpdateTask(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) - : m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder) {}; + : Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder) {}; struct UpdatableMod { QString name; diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index dc92d8ab..cf4e55b9 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -19,226 +19,502 @@ static ModPlatform::ProviderCapabilities ProviderCaps; static ModrinthAPI modrinth_api; static FlameAPI flame_api; -EnsureMetadataTask::EnsureMetadataTask(Mod& mod, QDir& dir, bool try_all, ModPlatform::Provider prov) - : m_mod(mod), m_index_dir(dir), m_provider(prov), m_try_all(try_all) -{} +EnsureMetadataTask::EnsureMetadataTask(Mod& mod, QDir dir, ModPlatform::Provider prov) : Task(nullptr), m_index_dir(dir), m_provider(prov) +{ + auto hash = getHash(mod); + if (hash.isEmpty()) + emitFail(mod); + else + m_mods.insert(hash, mod); +} + +EnsureMetadataTask::EnsureMetadataTask(std::list& mods, QDir dir, ModPlatform::Provider prov) + : Task(nullptr), m_index_dir(dir), m_provider(prov) +{ + for (auto& mod : mods) { + if (!mod.valid()) { + emitFail(mod); + continue; + } + + auto hash = getHash(mod); + if (hash.isEmpty()) { + emitFail(mod); + continue; + } + + m_mods.insert(hash, mod); + } +} + +QString EnsureMetadataTask::getHash(Mod& mod) +{ + /* Here we create a mapping hash -> mod, because we need that relationship to parse the API routes */ + QByteArray jar_data; + try { + jar_data = FS::read(mod.fileinfo().absoluteFilePath()); + } catch (FS::FileSystemException& e) { + qCritical() << QString("Failed to open / read JAR file of %1").arg(mod.name()); + qCritical() << QString("Reason: ") << e.cause(); + + return {}; + } + + switch (m_provider) { + case ModPlatform::Provider::MODRINTH: { + auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first(); + + return QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, hash_type).toHex()); + } + case ModPlatform::Provider::FLAME: { + QByteArray jar_data_treated; + for (char c : jar_data) { + // CF-specific + if (!(c == 9 || c == 10 || c == 13 || c == 32)) + jar_data_treated.push_back(c); + } + + return QString::number(MurmurHash2(jar_data_treated, jar_data_treated.length())); + } + } + + return {}; +} bool EnsureMetadataTask::abort() { - return m_task_handler->abort(); + // Prevent sending signals to a dead object + disconnect(this, 0, 0, 0); + + if (m_current_task) + return m_current_task->abort(); + return true; } void EnsureMetadataTask::executeTask() { - // They already have the right metadata :o - if (m_mod.status() != ModStatus::NoMetadata && m_mod.metadata() && m_mod.metadata()->provider == m_provider) { - emitReady(); - return; + setStatus(tr("Checking if mods have metadata...")); + + for (auto mod : m_mods) { + if (!mod.valid()) + continue; + + // They already have the right metadata :o + if (mod.status() != ModStatus::NoMetadata && mod.metadata() && mod.metadata()->provider == m_provider) { + qDebug() << "Mod" << mod.name() << "already has metadata!"; + emitReady(mod); + return; + } + + // Folders don't have metadata + if (mod.type() == Mod::MOD_FOLDER) { + emitReady(mod); + return; + } } - // Folders don't have metadata - if (m_mod.type() == Mod::MOD_FOLDER) { - emitReady(); - return; - } - - setStatus(tr("Generating %1's metadata...").arg(m_mod.name())); - qDebug() << QString("Generating %1's metadata...").arg(m_mod.name()); - - QByteArray jar_data; - - try { - jar_data = FS::read(m_mod.fileinfo().absoluteFilePath()); - } catch (FS::FileSystemException& e) { - qCritical() << QString("Failed to open / read JAR file of %1").arg(m_mod.name()); - qCritical() << QString("Reason: ") << e.cause(); - - emitFail(); - return; - } - - auto tsk = new MultipleOptionsTask(nullptr, "GetMetadataTask"); + NetJob::Ptr version_task; switch (m_provider) { case (ModPlatform::Provider::MODRINTH): - modrinthEnsureMetadata(*tsk, jar_data); - if (m_try_all) - flameEnsureMetadata(*tsk, jar_data); - + version_task = modrinthVersionsTask(); break; case (ModPlatform::Provider::FLAME): - flameEnsureMetadata(*tsk, jar_data); - if (m_try_all) - modrinthEnsureMetadata(*tsk, jar_data); - + version_task = flameVersionsTask(); break; } - connect(tsk, &MultipleOptionsTask::finished, this, [tsk] { tsk->deleteLater(); }); - connect(tsk, &MultipleOptionsTask::failed, [this] { - qCritical() << QString("Download of %1's metadata failed").arg(m_mod.name()); + auto invalidade_leftover = [this] { + QMutableHashIterator mods_iter(m_mods); + while (mods_iter.hasNext()) { + auto mod = mods_iter.next(); + emitFail(mod.value()); + } - emitFail(); - }); - connect(tsk, &MultipleOptionsTask::succeeded, this, &EnsureMetadataTask::emitReady); + emitSucceeded(); + }; - m_task_handler = tsk; + connect(version_task.get(), &Task::finished, this, [this, invalidade_leftover] { + NetJob::Ptr project_task; - tsk->start(); -} + switch (m_provider) { + case (ModPlatform::Provider::MODRINTH): + project_task = modrinthProjectsTask(); + break; + case (ModPlatform::Provider::FLAME): + project_task = flameProjectsTask(); + break; + } -void EnsureMetadataTask::emitReady() -{ - emit metadataReady(); - emitSucceeded(); -} - -void EnsureMetadataTask::emitFail() -{ - qDebug() << QString("Failed to generate metadata for %1").arg(m_mod.name()); - emit metadataFailed(); - //emitFailed(tr("Failed to generate metadata for %1").arg(m_mod.name())); - emitSucceeded(); -} - -void EnsureMetadataTask::modrinthEnsureMetadata(SequentialTask& tsk, QByteArray& jar_data) -{ - // Modrinth currently garantees that some hash types will always be present. - // But let's be sure and cover all cases anyways :) - for (auto hash_type : ProviderCaps.hashType(ModPlatform::Provider::MODRINTH)) { - auto* response = new QByteArray(); - auto hash = QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, hash_type).toHex()); - auto ver_task = modrinth_api.currentVersion(hash, hash_type, response); - - // Prevents unfortunate timings when aborting the task - if (!ver_task) + if (!project_task) { + invalidade_leftover(); return; + } - connect(ver_task.get(), &NetJob::succeeded, this, [this, ver_task, response] { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from " << m_mod.name() << " at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - - ver_task->failed(parse_error.errorString()); - return; - } - - auto doc_obj = Json::requireObject(doc); - auto ver = Modrinth::loadIndexedPackVersion(doc_obj, {}, m_mod.fileinfo().fileName()); - - // Minimal IndexedPack to create the metadata - ModPlatform::IndexedPack pack; - pack.name = m_mod.name(); - pack.provider = ModPlatform::Provider::MODRINTH; - pack.addonId = ver.addonId; - - // Prevent file name mismatch - ver.fileName = m_mod.fileinfo().fileName(); - - QDir tmp_index_dir(m_index_dir); - - { - LocalModUpdateTask update_metadata(m_index_dir, pack, ver); - QEventLoop loop; - QTimer timeout; - - QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); - QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); - - update_metadata.start(); - timeout.start(100); - - loop.exec(); - } - - auto mod_name = m_mod.name(); - auto meta = new Metadata::ModStruct(Metadata::get(tmp_index_dir, mod_name)); - m_mod.setMetadata(meta); + connect(project_task.get(), &Task::finished, this, [=] { + invalidade_leftover(); + project_task->deleteLater(); + m_current_task = nullptr; }); - tsk.addTask(ver_task); - } + m_current_task = project_task.get(); + project_task->start(); + }); + + connect(version_task.get(), &Task::finished, [=] { + version_task->deleteLater(); + m_current_task = nullptr; + }); + + if (m_mods.size() > 1) + setStatus(tr("Requesting metadata information from %1...").arg(ProviderCaps.readableName(m_provider))); + else if (!m_mods.empty()) + setStatus(tr("Requesting metadata information from %1 for '%2'...") + .arg(ProviderCaps.readableName(m_provider), m_mods.begin().value().name())); + + m_current_task = version_task.get(); + version_task->start(); } -void EnsureMetadataTask::flameEnsureMetadata(SequentialTask& tsk, QByteArray& jar_data) +void EnsureMetadataTask::emitReady(Mod& m) { - QByteArray jar_data_treated; - for (char c : jar_data) { - // CF-specific - if (!(c == 9 || c == 10 || c == 13 || c == 32)) - jar_data_treated.push_back(c); - } + qDebug() << QString("Generated metadata for %1").arg(m.name()); + emit metadataReady(m); + + m_mods.remove(getHash(m)); +} + +void EnsureMetadataTask::emitFail(Mod& m) +{ + qDebug() << QString("Failed to generate metadata for %1").arg(m.name()); + emit metadataFailed(m); + + m_mods.remove(getHash(m)); +} + +// Modrinth + +NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask() +{ + auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first(); auto* response = new QByteArray(); + auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response); - std::list fingerprints; - auto murmur = MurmurHash2(jar_data_treated, jar_data_treated.length()); - fingerprints.push_back(murmur); - - auto ver_task = flame_api.matchFingerprints(fingerprints, response); - - connect(ver_task.get(), &Task::succeeded, this, [this, ver_task, response] { - QDir tmp_index_dir(m_index_dir); + // Prevents unfortunate timings when aborting the task + if (!ver_task) + return {}; + connect(ver_task.get(), &NetJob::succeeded, this, [this, response] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from " << m_mod.name() << " at " << parse_error.offset + qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << *response; - ver_task->failed(parse_error.errorString()); + failed(parse_error.errorString()); + return; + } + + try { + auto entries = Json::requireObject(doc); + for (auto& hash : m_mods.keys()) { + auto mod = m_mods.find(hash).value(); + try { + auto entry = Json::requireObject(entries, hash); + + setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod.name())); + qDebug() << "Getting version for" << mod.name() << "from Modrinth"; + + m_temp_versions.insert(hash, Modrinth::loadIndexedPackVersion(entry)); + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << entries; + + emitFail(mod); + } + } + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << doc; + } + }); + + return ver_task; +} + +NetJob::Ptr EnsureMetadataTask::modrinthProjectsTask() +{ + QHash addonIds; + for (auto const& data : m_temp_versions) + addonIds.insert(data.addonId.toString(), data.hash); + + auto response = new QByteArray(); + NetJob::Ptr proj_task; + + if (addonIds.isEmpty()) { + qWarning() << "No addonId found!"; + } else if (addonIds.size() == 1) { + proj_task = modrinth_api.getProject(*addonIds.keyBegin(), response); + } else { + proj_task = modrinth_api.getProjects(addonIds.keys(), response); + } + + // Prevents unfortunate timings when aborting the task + if (!proj_task) + return {}; + + connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] { + QJsonParseError parse_error{}; + auto doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Modrinth projects task at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + try { + QJsonArray entries; + if (addonIds.size() == 1) + entries = { doc.object() }; + else + entries = Json::requireArray(doc); + + for (auto entry : entries) { + auto entry_obj = Json::requireObject(entry); + auto entry_id = Json::requireString(entry_obj, "id"); + + auto hash = addonIds.find(entry_id).value(); + + auto mod = m_mods.find(hash).value(); + + try { + setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod.name())); + + ModPlatform::IndexedPack pack; + Modrinth::loadIndexedPack(pack, entry_obj); + + modrinthCallback(pack, m_temp_versions.find(hash).value(), mod); + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << entries; + + emitFail(mod); + } + } + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << doc; + } + }); + + return proj_task; +} + +// Flame +NetJob::Ptr EnsureMetadataTask::flameVersionsTask() +{ + auto* response = new QByteArray(); + + std::list fingerprints; + for (auto& murmur : m_mods.keys()) { + fingerprints.push_back(murmur.toUInt()); + } + + auto ver_task = flame_api.matchFingerprints(fingerprints, response); + + connect(ver_task.get(), &Task::succeeded, this, [this, response] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + failed(parse_error.errorString()); return; } try { auto doc_obj = Json::requireObject(doc); - auto data_obj = Json::ensureObject(doc_obj, "data"); - auto match_obj = Json::ensureObject(Json::ensureArray(data_obj, "exactMatches")[0], {}); - if (match_obj.isEmpty()) { - qCritical() << "Fingerprint match is empty!"; + auto data_obj = Json::requireObject(doc_obj, "data"); + auto data_arr = Json::requireArray(data_obj, "exactMatches"); + + if (data_arr.isEmpty()) { + qWarning() << "No matches found for fingerprint search!"; - ver_task->failed(parse_error.errorString()); return; } - auto file_obj = Json::ensureObject(match_obj, "file"); + for (auto match : data_arr) { + auto match_obj = Json::ensureObject(match, {}); + auto file_obj = Json::ensureObject(match_obj, "file", {}); - ModPlatform::IndexedPack pack; - pack.name = m_mod.name(); - pack.provider = ModPlatform::Provider::FLAME; - pack.addonId = Json::requireInteger(file_obj, "modId"); + if (match_obj.isEmpty() || file_obj.isEmpty()) { + qWarning() << "Fingerprint match is empty!"; - ModPlatform::IndexedVersion ver = FlameMod::loadIndexedPackVersion(file_obj); + return; + } - // Prevent file name mismatch - ver.fileName = m_mod.fileinfo().fileName(); + auto fingerprint = QString::number(Json::ensureVariant(file_obj, "fileFingerprint").toUInt()); + auto mod = m_mods.find(fingerprint); + if (mod == m_mods.end()) { + qWarning() << "Invalid fingerprint from the API response."; + continue; + } - { - LocalModUpdateTask update_metadata(m_index_dir, pack, ver); - QEventLoop loop; - QTimer timeout; + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name())); - QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); - QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); - - update_metadata.start(); - timeout.start(100); - - loop.exec(); + m_temp_versions.insert(fingerprint, FlameMod::loadIndexedPackVersion(file_obj)); } - auto mod_name = m_mod.name(); - auto meta = new Metadata::ModStruct(Metadata::get(tmp_index_dir, mod_name)); - m_mod.setMetadata(meta); - } catch (Json::JsonException& e) { - emitFailed(e.cause() + " : " + e.what()); + qDebug() << e.cause(); + qDebug() << doc; } }); - tsk.addTask(ver_task); + return ver_task; +} + +NetJob::Ptr EnsureMetadataTask::flameProjectsTask() +{ + QHash addonIds; + for (auto const& hash : m_mods.keys()) { + if (m_temp_versions.contains(hash)) { + auto const& data = m_temp_versions.find(hash).value(); + addonIds.insert(data.addonId.toString(), hash); + } + } + + auto response = new QByteArray(); + NetJob::Ptr proj_task; + + if (addonIds.isEmpty()) { + qWarning() << "No addonId found!"; + } else if (addonIds.size() == 1) { + proj_task = flame_api.getProject(*addonIds.keyBegin(), response); + } else { + proj_task = flame_api.getProjects(addonIds.keys(), response); + } + + // Prevents unfortunate timings when aborting the task + if (!proj_task) + return {}; + + connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] { + QJsonParseError parse_error{}; + auto doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Modrinth projects task at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + try { + QJsonArray entries; + if (addonIds.size() == 1) + entries = { Json::requireObject(Json::requireObject(doc), "data") }; + else + entries = Json::requireArray(Json::requireObject(doc), "data"); + + for (auto entry : entries) { + auto entry_obj = Json::requireObject(entry); + + auto id = QString::number(Json::requireInteger(entry_obj, "id")); + auto hash = addonIds.find(id).value(); + auto mod = m_mods.find(hash).value(); + + try { + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod.name())); + + ModPlatform::IndexedPack pack; + FlameMod::loadIndexedPack(pack, entry_obj); + + flameCallback(pack, m_temp_versions.find(hash).value(), mod); + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << entries; + + emitFail(mod); + } + } + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << doc; + } + }); + + return proj_task; +} + +void EnsureMetadataTask::modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod& mod) +{ + // Prevent file name mismatch + ver.fileName = mod.fileinfo().fileName(); + + QDir tmp_index_dir(m_index_dir); + + { + LocalModUpdateTask update_metadata(m_index_dir, pack, ver); + QEventLoop loop; + + QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); + + update_metadata.start(); + + if (!update_metadata.isFinished()) + loop.exec(); + } + + auto metadata = Metadata::get(tmp_index_dir, pack.slug); + if (!metadata.isValid()) { + qCritical() << "Failed to generate metadata at last step!"; + emitFail(mod); + return; + } + + mod.setMetadata(metadata); + + emitReady(mod); +} + +void EnsureMetadataTask::flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod& mod) +{ + try { + // Prevent file name mismatch + ver.fileName = mod.fileinfo().fileName(); + + QDir tmp_index_dir(m_index_dir); + + { + LocalModUpdateTask update_metadata(m_index_dir, pack, ver); + QEventLoop loop; + + QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); + + update_metadata.start(); + + if (!update_metadata.isFinished()) + loop.exec(); + } + + auto metadata = Metadata::get(tmp_index_dir, pack.slug); + if (!metadata.isValid()) { + qCritical() << "Failed to generate metadata at last step!"; + emitFail(mod); + return; + } + + mod.setMetadata(metadata); + + emitReady(mod); + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + + emitFail(mod); + } } diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h index 624e253a..880503b9 100644 --- a/launcher/modplatform/EnsureMetadataTask.h +++ b/launcher/modplatform/EnsureMetadataTask.h @@ -2,6 +2,7 @@ #include "ModIndex.h" #include "tasks/SequentialTask.h" +#include "net/NetJob.h" class Mod; class QDir; @@ -11,7 +12,10 @@ class EnsureMetadataTask : public Task { Q_OBJECT public: - EnsureMetadataTask(Mod&, QDir&, bool try_all, ModPlatform::Provider = ModPlatform::Provider::MODRINTH); + EnsureMetadataTask(Mod&, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH); + EnsureMetadataTask(std::list&, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH); + + ~EnsureMetadataTask() = default; public slots: bool abort() override; @@ -20,22 +24,31 @@ class EnsureMetadataTask : public Task { private: // FIXME: Move to their own namespace - void modrinthEnsureMetadata(SequentialTask&, QByteArray&); - void flameEnsureMetadata(SequentialTask&, QByteArray&); + auto modrinthVersionsTask() -> NetJob::Ptr; + auto modrinthProjectsTask() -> NetJob::Ptr; + + auto flameVersionsTask() -> NetJob::Ptr; + auto flameProjectsTask() -> NetJob::Ptr; // Helpers - void emitReady(); - void emitFail(); + void emitReady(Mod&); + void emitFail(Mod&); + + auto getHash(Mod&) -> QString; + + private slots: + void modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod&); + void flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod&); signals: - void metadataReady(); - void metadataFailed(); + void metadataReady(Mod&); + void metadataFailed(Mod&); private: - Mod& m_mod; - QDir& m_index_dir; + QHash m_mods; + QDir m_index_dir; ModPlatform::Provider m_provider; - bool m_try_all; - MultipleOptionsTask* m_task_handler = nullptr; + QHash m_temp_versions; + NetJob* m_current_task; }; diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index 12d99029..3658bf8d 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -10,7 +10,6 @@ #include "ModDownloadTask.h" static FlameAPI api; -static ModPlatform::ProviderCapabilities ProviderCaps; bool FlameCheckUpdate::abort() { @@ -64,6 +63,50 @@ ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info) return pack; } +ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId) +{ + ModPlatform::IndexedVersion ver; + + QEventLoop loop; + + auto get_file_info_job = new NetJob("Flame::GetFileInfoJob", APPLICATION->network()); + + auto response = new QByteArray(); + auto url = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(QString::number(addonId), QString::number(fileId)); + auto dl = Net::Download::makeByteArray(url, response); + get_file_info_job->addNetAction(dl); + + QObject::connect(get_file_info_job, &NetJob::succeeded, [response, &ver]() { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + try { + auto doc_obj = Json::requireObject(doc); + auto data_obj = Json::requireObject(doc_obj, "data"); + ver = FlameMod::loadIndexedPackVersion(data_obj); + } catch (Json::JsonException& e) { + qWarning() << e.cause(); + qDebug() << doc; + } + }); + + QObject::connect(get_file_info_job, &NetJob::finished, [&loop, get_file_info_job] { + get_file_info_job->deleteLater(); + loop.quit(); + }); + + get_file_info_job->start(); + loop.exec(); + + return ver; +} + /* Check for update: * - Get latest version available * - Compare hash of the latest version with the current hash @@ -72,148 +115,60 @@ ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info) void FlameCheckUpdate::executeTask() { setStatus(tr("Preparing mods for CurseForge...")); - setProgress(0, 5); - QHash mappings; - - // Create all hashes - std::list murmur_hashes; - - auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::FLAME).first(); + int i = 0; for (auto mod : m_mods) { - QByteArray jar_data; + setStatus(tr("Getting API response from CurseForge for '%1'").arg(mod.name())); + setProgress(i++, m_mods.size()); - try { - jar_data = FS::read(mod.fileinfo().absoluteFilePath()); - } catch (FS::FileSystemException& e) { - qCritical() << QString("Failed to open / read JAR file of %1").arg(mod.name()); - qCritical() << QString("Reason: ") << e.cause(); + auto latest_ver = api.getLatestVersion({ mod.metadata()->project_id.toString(), m_game_versions, m_loaders }); - failed(e.what()); + // Check if we were aborted while getting the latest version + if (m_was_aborted) { + aborted(); return; } - QByteArray jar_data_treated; - for (char c : jar_data) { - // CF-specific - if (!(c == 9 || c == 10 || c == 13 || c == 32)) - jar_data_treated.push_back(c); + setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(mod.name())); + + if (!latest_ver.addonId.isValid()) { + emit checkFailed(mod, tr("No valid version found for this mod. It's probably unavailable for the current game " + "version / mod loader.")); + continue; } - auto murmur_hash = MurmurHash2(jar_data_treated, jar_data_treated.length()); - murmur_hashes.emplace_back(murmur_hash); + if (latest_ver.downloadUrl.isEmpty() && latest_ver.fileId != mod.metadata()->file_id) { + auto pack = getProjectInfo(latest_ver); + auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver.fileId.toString()); + emit checkFailed(mod, tr("Mod has a new update available, but is opted-out on CurseForge"), recover_url); - mappings.insert(mod.metadata()->mod_id().toInt(), mod); - } - - auto* response = new QByteArray(); - auto job = api.matchFingerprints(murmur_hashes, response); - - QEventLoop lock; - - connect(job.get(), &Task::succeeded, this, [this, response, &mappings] { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - - failed(parse_error.errorString()); - return; + continue; } - setStatus(tr("Parsing the first API response from CurseForge...")); - setProgress(2, 5); + if (!latest_ver.hash.isEmpty() && (mod.metadata()->hash != latest_ver.hash || mod.status() == ModStatus::NotInstalled)) { + // Fake pack with the necessary info to pass to the download task :) + ModPlatform::IndexedPack pack; + pack.name = mod.name(); + pack.slug = mod.metadata()->slug; + pack.addonId = mod.metadata()->project_id; + pack.websiteUrl = mod.homeurl(); + for (auto& author : mod.authors()) + pack.authors.append({ author }); + pack.description = mod.description(); + pack.provider = ModPlatform::Provider::FLAME; - try { - auto doc_obj = Json::requireObject(doc); - auto data_obj = Json::ensureObject(doc_obj, "data"); - auto match_arr = Json::ensureArray(data_obj, "exactMatches"); - for (auto match : match_arr) { - auto match_obj = Json::ensureObject(match); - - ModPlatform::IndexedVersion current_ver; - try { - auto file_obj = Json::requireObject(match_obj, "file"); - current_ver = FlameMod::loadIndexedPackVersion(file_obj); - } catch (Json::JsonException& e) { - qCritical() << "Error while parsing Flame indexed version"; - qCritical() << e.what(); - failed(tr("An error occured while parsing a CurseForge indexed version!")); - return; - } - - auto mod_iter = mappings.find(current_ver.addonId.toInt()); - if (mod_iter == mappings.end()) { - qCritical() << "Failed to remap mod from Flame!"; - qDebug() << match_obj; - continue; - } - - auto mod = mod_iter.value(); - - setStatus(tr("Waiting for the API response from CurseForge for '%1'...").arg(mod.name())); - setProgress(3, 5); - - auto latest_ver = api.getLatestVersion({ current_ver.addonId.toString(), m_game_versions, m_loaders }); - - // Check if we were aborted while getting the latest version - if (m_was_aborted) { - aborted(); - return; - } - - setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(mod.name())); - setProgress(4, 5); - - if (!latest_ver.addonId.isValid()) { - emit checkFailed( - mod, - tr("No valid version found for this mod. It's probably unavailable for the current game version / mod loader.")); - continue; - } - - if (latest_ver.downloadUrl.isEmpty() && latest_ver.fileId != current_ver.fileId) { - auto pack = getProjectInfo(latest_ver); - auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver.fileId.toString()); - emit checkFailed(mod, tr("Mod has a new update available, but is opted-out on CurseForge"), recover_url); - - continue; - } - - if (!latest_ver.hash.isEmpty() && (current_ver.hash != latest_ver.hash || mod.status() == ModStatus::NotInstalled)) { - // Fake pack with the necessary info to pass to the download task :) - ModPlatform::IndexedPack pack; - pack.name = mod.name(); - pack.addonId = mod.metadata()->project_id; - pack.websiteUrl = mod.homeurl(); - for (auto& author : mod.authors()) - pack.authors.append({ author }); - pack.description = mod.description(); - pack.provider = ModPlatform::Provider::FLAME; - - auto download_task = new ModDownloadTask(pack, latest_ver, m_mods_folder); - m_updatable.emplace_back(mod.name(), current_ver.hash, current_ver.version, latest_ver.version, - api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()), - ModPlatform::Provider::FLAME, download_task); - } + auto old_version = mod.version(); + if (old_version.isEmpty() && mod.status() != ModStatus::NotInstalled) { + auto current_ver = getFileInfo(latest_ver.addonId.toInt(), mod.metadata()->file_id.toInt()); + old_version = current_ver.version; } - } catch (Json::JsonException& e) { - failed(e.cause() + " : " + e.what()); + auto download_task = new ModDownloadTask(pack, latest_ver, m_mods_folder); + m_updatable.emplace_back(mod.name(), mod.metadata()->hash, old_version, latest_ver.version, + api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()), + ModPlatform::Provider::FLAME, download_task); } - }); - - connect(job.get(), &Task::finished, &lock, &QEventLoop::quit); - - setStatus(tr("Waiting for the first API response from CurseForge...")); - setProgress(1, 5); - - m_net_job = job.get(); - job->start(); - - lock.exec(); + } emitSucceeded(); } diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index 981c4216..78275cf0 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -88,7 +88,9 @@ void ModrinthCheckUpdate::executeTask() qDebug() << "Mod " << mappings.find(hash).value().name() << " got an empty response."; qDebug() << "Hash: " << hash; - emit checkFailed(mappings.find(hash).value(), tr("Couldn't find the latest version of this mod with the correct mod loader and game version.")); + emit checkFailed( + mappings.find(hash).value(), + tr("No valid version found for this mod. It's probably unavailable for the current game version / mod loader.")); continue; } @@ -134,6 +136,7 @@ void ModrinthCheckUpdate::executeTask() // Fake pack with the necessary info to pass to the download task :) ModPlatform::IndexedPack pack; pack.name = mod.name(); + pack.slug = mod.metadata()->slug; pack.addonId = mod.metadata()->project_id; pack.websiteUrl = mod.homeurl(); for (auto& author : mod.authors()) diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index a4d83483..7584621a 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -34,12 +34,13 @@ static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst) ModUpdateDialog::ModUpdateDialog(QWidget* parent, BaseInstance* instance, - const std::shared_ptr& mods, + const std::shared_ptr mods, std::list& search_for) : ReviewMessageBox(parent, tr("Confirm mods to update"), "") , m_parent(parent) , m_mod_model(mods) , m_candidates(search_for) + , m_second_try_metadata(new SequentialTask()) , m_instance(instance) { ReviewMessageBox::setGeometry(0, 0, 800, 600); @@ -47,15 +48,6 @@ ModUpdateDialog::ModUpdateDialog(QWidget* parent, ui->explainLabel->setText(tr("You're about to update the following mods:")); ui->onlyCheckedLabel->setText(tr("Only mods with a check will be updated!")); - connect(&m_check_task, &Task::failed, this, - [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); - - connect(&m_check_task, &Task::succeeded, this, [&]() { - QStringList warnings = m_check_task.warnings(); - if (warnings.count()) { - CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); - } - }); } void ModUpdateDialog::checkCandidates() @@ -89,26 +81,39 @@ void ModUpdateDialog::checkCandidates() auto versions = mcVersions(m_instance); auto loaders = mcLoaders(m_instance); + SequentialTask check_task (m_parent, tr("Checking for updates")); + if (!m_modrinth_to_update.empty()) { m_modrinth_check_task = new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_mod_model); connect(m_modrinth_check_task, &CheckUpdateTask::checkFailed, this, [this](Mod mod, QString reason, QUrl recover_url) { m_failed_check_update.emplace_back(mod, reason, recover_url); }); - m_check_task.addTask(m_modrinth_check_task); + check_task.addTask(m_modrinth_check_task); } if (!m_flame_to_update.empty()) { m_flame_check_task = new FlameCheckUpdate(m_flame_to_update, versions, loaders, m_mod_model); connect(m_flame_check_task, &CheckUpdateTask::checkFailed, this, [this](Mod mod, QString reason, QUrl recover_url) { m_failed_check_update.emplace_back(mod, reason, recover_url); }); - m_check_task.addTask(m_flame_check_task); + check_task.addTask(m_flame_check_task); } + connect(&check_task, &Task::failed, this, + [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + + connect(&check_task, &Task::succeeded, this, [&]() { + QStringList warnings = check_task.warnings(); + if (warnings.count()) { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); + } + }); + // Check for updates + // FIXME: SOMEHOW THIS IS NOT MODAL??????? ProgressDialog progress_dialog(m_parent); progress_dialog.setSkipButton(true, tr("Abort")); progress_dialog.setVisible(true); progress_dialog.setWindowTitle(tr("Checking for updates...")); - auto ret = progress_dialog.execWithTask(&m_check_task); + auto ret = progress_dialog.execWithTask(&check_task); // If the dialog was skipped / some download error happened if (ret == QDialog::DialogCode::Rejected) { @@ -183,13 +188,29 @@ auto ModUpdateDialog::ensureMetadata() -> bool { auto index_dir = indexDir(); - auto* seq = new SequentialTask(m_parent, tr("Looking for metadata")); + SequentialTask seq(m_parent, tr("Looking for metadata")); + + // A better use of data structures here could remove the need for this QHash + QHash should_try_others; + std::list modrinth_tmp; + std::list flame_tmp; bool confirm_rest = false; bool try_others_rest = false; bool skip_rest = false; ModPlatform::Provider provider_rest = ModPlatform::Provider::MODRINTH; + auto addToTmp = [&](Mod& m, ModPlatform::Provider p) { + switch (p) { + case ModPlatform::Provider::MODRINTH: + modrinth_tmp.push_back(m); + break; + case ModPlatform::Provider::FLAME: + flame_tmp.push_back(m); + break; + } + }; + for (auto& candidate : m_candidates) { if (candidate.status() != ModStatus::NoMetadata) { onMetadataEnsured(candidate); @@ -200,10 +221,8 @@ auto ModUpdateDialog::ensureMetadata() -> bool continue; if (confirm_rest) { - auto* task = new EnsureMetadataTask(candidate, index_dir, try_others_rest, provider_rest); - connect(task, &EnsureMetadataTask::metadataReady, [this, &candidate] { onMetadataEnsured(candidate); }); - connect(task, &EnsureMetadataTask::metadataFailed, [this, &candidate] { onMetadataFailed(candidate); }); - seq->addTask(task); + addToTmp(candidate, provider_rest); + should_try_others.insert(candidate.internal_id(), try_others_rest); continue; } @@ -224,18 +243,36 @@ auto ModUpdateDialog::ensureMetadata() -> bool try_others_rest = response.try_others; } - if (confirmed) { - auto* task = new EnsureMetadataTask(candidate, index_dir, response.try_others, response.chosen); - connect(task, &EnsureMetadataTask::metadataReady, [this, &candidate] { onMetadataEnsured(candidate); }); - connect(task, &EnsureMetadataTask::metadataFailed, [this, &candidate] { onMetadataFailed(candidate); }); - seq->addTask(task); - } + should_try_others.insert(candidate.internal_id(), response.try_others); + + if (confirmed) + addToTmp(candidate, response.chosen); } + if (!modrinth_tmp.empty()) { + auto* modrinth_task = new EnsureMetadataTask(modrinth_tmp, index_dir, ModPlatform::Provider::MODRINTH); + connect(modrinth_task, &EnsureMetadataTask::metadataReady, [this](Mod& candidate) { onMetadataEnsured(candidate); }); + connect(modrinth_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod& candidate) { + onMetadataFailed(candidate, should_try_others.find(candidate.internal_id()).value(), ModPlatform::Provider::MODRINTH); + }); + seq.addTask(modrinth_task); + } + + if (!flame_tmp.empty()) { + auto* flame_task = new EnsureMetadataTask(flame_tmp, index_dir, ModPlatform::Provider::FLAME); + connect(flame_task, &EnsureMetadataTask::metadataReady, [this](Mod& candidate) { onMetadataEnsured(candidate); }); + connect(flame_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod& candidate) { + onMetadataFailed(candidate, should_try_others.find(candidate.internal_id()).value(), ModPlatform::Provider::FLAME); + }); + seq.addTask(flame_task); + } + + seq.addTask(m_second_try_metadata); + ProgressDialog checking_dialog(m_parent); checking_dialog.setSkipButton(true, tr("Abort")); checking_dialog.setWindowTitle(tr("Generating metadata...")); - auto ret_metadata = checking_dialog.execWithTask(seq); + auto ret_metadata = checking_dialog.execWithTask(&seq); return (ret_metadata != QDialog::DialogCode::Rejected); } @@ -256,9 +293,31 @@ void ModUpdateDialog::onMetadataEnsured(Mod& mod) } } -void ModUpdateDialog::onMetadataFailed(Mod& mod) +ModPlatform::Provider next(ModPlatform::Provider p) { - m_failed_metadata.push_back(mod); + switch (p) { + case ModPlatform::Provider::MODRINTH: + return ModPlatform::Provider::FLAME; + case ModPlatform::Provider::FLAME: + return ModPlatform::Provider::MODRINTH; + } + + return ModPlatform::Provider::FLAME; +} + +void ModUpdateDialog::onMetadataFailed(Mod& mod, bool try_others, ModPlatform::Provider first_choice) +{ + if (try_others) { + auto index_dir = indexDir(); + + auto* task = new EnsureMetadataTask(mod, index_dir, next(first_choice)); + connect(task, &EnsureMetadataTask::metadataReady, [this](Mod& candidate) { onMetadataEnsured(candidate); }); + connect(task, &EnsureMetadataTask::metadataFailed, [this](Mod& candidate) { onMetadataFailed(candidate, false); }); + + m_second_try_metadata->addTask(task); + } else { + m_failed_metadata.push_back(mod); + } } void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info) diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h index 30cd5cbd..f40fc594 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.h +++ b/launcher/ui/dialogs/ModUpdateDialog.h @@ -17,7 +17,7 @@ class ModUpdateDialog final : public ReviewMessageBox { public: explicit ModUpdateDialog(QWidget* parent, BaseInstance* instance, - const std::shared_ptr& mod_model, + const std::shared_ptr mod_model, std::list& search_for); void checkCandidates(); @@ -35,21 +35,21 @@ class ModUpdateDialog final : public ReviewMessageBox { private slots: void onMetadataEnsured(Mod&); - void onMetadataFailed(Mod&); + void onMetadataFailed(Mod&, bool try_others = false, ModPlatform::Provider first_choice = ModPlatform::Provider::MODRINTH); private: QWidget* m_parent; - SequentialTask m_check_task; ModrinthCheckUpdate* m_modrinth_check_task = nullptr; FlameCheckUpdate* m_flame_check_task = nullptr; - const std::shared_ptr& m_mod_model; + const std::shared_ptr m_mod_model; std::list& m_candidates; std::list m_modrinth_to_update; std::list m_flame_to_update; + SequentialTask* m_second_try_metadata; std::list m_failed_metadata; std::list> m_failed_check_update; From a53ee2e35cafd36964663d632877badcf53d8786 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 17 Jun 2022 11:21:43 -0300 Subject: [PATCH 22/38] fix: mod parsing of 'String-fied' version (i.e. OpenBlocks) Signed-off-by: flow --- launcher/minecraft/mod/tasks/LocalModParseTask.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 3354732b..1519f49d 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -71,7 +72,13 @@ std::shared_ptr ReadMCModInfo(QByteArray contents) if(val.isUndefined()) { val = jsonDoc.object().value("modListVersion"); } - int version = val.toDouble(); + + int version = Json::ensureInteger(val, -1); + + // Some mods set the number with "", so it's a String instead + if (version < 0) + version = Json::ensureString(val, "").toInt(); + if (version != 2) { qCritical() << "BAD stuff happened to mod json:"; From fd6755c93f3f3f7551f9b7c11d1bbbb48c22e210 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 19 Jun 2022 14:26:15 -0300 Subject: [PATCH 23/38] change: mod metadata improvements - Use slug instead of name - Keep temporary status before having local details Signed-off-by: flow --- launcher/ModDownloadTask.cpp | 7 ++-- launcher/minecraft/mod/MetadataHandler.h | 12 +++--- launcher/minecraft/mod/Mod.cpp | 38 ++++++++++++------- launcher/minecraft/mod/Mod.h | 4 +- launcher/minecraft/mod/ModFolderModel.cpp | 30 ++++++++++----- .../mod/tasks/LocalModUpdateTask.cpp | 12 ++++-- .../minecraft/mod/tasks/ModFolderLoadTask.cpp | 2 +- launcher/modplatform/packwiz/Packwiz.cpp | 36 +++++++++--------- launcher/modplatform/packwiz/Packwiz.h | 15 ++++---- launcher/modplatform/packwiz/Packwiz_test.cpp | 7 ++-- 10 files changed, 96 insertions(+), 67 deletions(-) diff --git a/launcher/ModDownloadTask.cpp b/launcher/ModDownloadTask.cpp index 7d35ff69..2b0343f4 100644 --- a/launcher/ModDownloadTask.cpp +++ b/launcher/ModDownloadTask.cpp @@ -47,10 +47,9 @@ void ModDownloadTask::downloadSucceeded() { m_filesNetJob.reset(); auto name = std::get<0>(to_delete); - if (!name.isEmpty()) { - // If they have the same name, we keep the metadata. - // This is a workaround for mods that change names between versions ;c - mods->uninstallMod(std::get<1>(to_delete), name == m_mod.name); + auto filename = std::get<1>(to_delete); + if (!name.isEmpty() && filename != m_mod_version.fileName) { + mods->uninstallMod(filename, true); } } diff --git a/launcher/minecraft/mod/MetadataHandler.h b/launcher/minecraft/mod/MetadataHandler.h index d5f01c42..39723b49 100644 --- a/launcher/minecraft/mod/MetadataHandler.h +++ b/launcher/minecraft/mod/MetadataHandler.h @@ -37,9 +37,9 @@ class Metadata { return Packwiz::V1::createModFormat(index_dir, mod_pack, mod_version); } - static auto create(QDir& index_dir, Mod& internal_mod) -> ModStruct + static auto create(QDir& index_dir, Mod& internal_mod, QString mod_slug) -> ModStruct { - return Packwiz::V1::createModFormat(index_dir, internal_mod); + return Packwiz::V1::createModFormat(index_dir, internal_mod, mod_slug); } static void update(QDir& index_dir, ModStruct& mod) @@ -47,9 +47,9 @@ class Metadata { Packwiz::V1::updateModIndex(index_dir, mod); } - static void remove(QDir& index_dir, QString& mod_name) + static void remove(QDir& index_dir, QString mod_slug) { - Packwiz::V1::deleteModIndex(index_dir, mod_name); + Packwiz::V1::deleteModIndex(index_dir, mod_slug); } static void remove(QDir& index_dir, QVariant& mod_id) @@ -57,9 +57,9 @@ class Metadata { Packwiz::V1::deleteModIndex(index_dir, mod_id); } - static auto get(QDir& index_dir, QString& mod_name) -> ModStruct + static auto get(QDir& index_dir, QString mod_slug) -> ModStruct { - return Packwiz::V1::getIndexForMod(index_dir, mod_name); + return Packwiz::V1::getIndexForMod(index_dir, mod_slug); } static auto get(QDir& index_dir, QVariant& mod_id) -> ModStruct diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index bba7b342..7227fc94 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -147,25 +147,36 @@ void Mod::setStatus(ModStatus status) if (m_localDetails) { m_localDetails->status = status; } else { - m_temp_status = status; + if (!m_temp_status.get()) + m_temp_status.reset(new ModStatus()); + + *m_temp_status = status; } } -void Mod::setMetadata(Metadata::ModStruct* metadata) +void Mod::setMetadata(const Metadata::ModStruct& metadata) { if (status() == ModStatus::NoMetadata) setStatus(ModStatus::Installed); if (m_localDetails) { - m_localDetails->metadata.reset(metadata); + m_localDetails->metadata = std::make_shared(std::move(metadata)); } else { - m_temp_metadata.reset(metadata); + m_temp_metadata = std::make_shared(std::move(metadata)); } } auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool { - if (!preserve_metadata && status() != ModStatus::NoMetadata) - Metadata::remove(index_dir, metadata()->mod_id()); + if (!preserve_metadata) { + qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); + + if (metadata()) { + Metadata::remove(index_dir, metadata()->slug); + } else { + auto n = name(); + Metadata::remove(index_dir, n); + } + } m_type = MOD_UNKNOWN; return FS::deletePath(m_file.filePath()); @@ -182,7 +193,7 @@ auto Mod::name() const -> QString if (!d_name.isEmpty()) return d_name; - if (status() != ModStatus::NoMetadata) + if (metadata()) return metadata()->name; return m_name; @@ -211,7 +222,7 @@ auto Mod::authors() const -> QStringList auto Mod::status() const -> ModStatus { if (!m_localDetails) - return m_temp_status; + return m_temp_status ? *m_temp_status : ModStatus::NoMetadata; return details().status; } @@ -235,11 +246,10 @@ void Mod::finishResolvingWithDetails(std::shared_ptr details) m_resolved = true; m_localDetails = details; - if (m_localDetails && m_temp_metadata && m_temp_metadata->isValid()) { - m_localDetails->metadata = m_temp_metadata; - if (status() == ModStatus::NoMetadata) - setStatus(ModStatus::Installed); - } + setStatus(m_temp_status ? *m_temp_status : ModStatus::NoMetadata); - setStatus(m_temp_status); + if (m_localDetails && m_temp_metadata && m_temp_metadata->isValid()) { + setMetadata(*m_temp_metadata); + m_temp_metadata.reset(); + } } diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index abb8a52d..cbbbd362 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -77,7 +77,7 @@ public: auto metadata() const -> const std::shared_ptr; void setStatus(ModStatus status); - void setMetadata(Metadata::ModStruct* metadata); + void setMetadata(const Metadata::ModStruct& metadata); auto enable(bool value) -> bool; @@ -111,7 +111,7 @@ protected: std::shared_ptr m_temp_metadata; /* Set the mod status while it doesn't have local details just yet */ - ModStatus m_temp_status = ModStatus::NotInstalled; + std::shared_ptr m_temp_status; std::shared_ptr m_localDetails; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index adc828c2..d8170067 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -65,15 +65,21 @@ void ModFolderModel::startWatching() update(); + // Watch the mods folder is_watching = m_watcher->addPath(m_dir.absolutePath()); - if (is_watching) - { + if (is_watching) { qDebug() << "Started watching " << m_dir.absolutePath(); - } - else - { + } else { qDebug() << "Failed to start watching " << m_dir.absolutePath(); } + + // Watch the mods index folder + is_watching = m_watcher->addPath(indexDir().absolutePath()); + if (is_watching) { + qDebug() << "Started watching " << indexDir().absolutePath(); + } else { + qDebug() << "Failed to start watching " << indexDir().absolutePath(); + } } void ModFolderModel::stopWatching() @@ -82,14 +88,18 @@ void ModFolderModel::stopWatching() return; is_watching = !m_watcher->removePath(m_dir.absolutePath()); - if (!is_watching) - { + if (!is_watching) { qDebug() << "Stopped watching " << m_dir.absolutePath(); - } - else - { + } else { qDebug() << "Failed to stop watching " << m_dir.absolutePath(); } + + is_watching = !m_watcher->removePath(indexDir().absolutePath()); + if (!is_watching) { + qDebug() << "Stopped watching " << indexDir().absolutePath(); + } else { + qDebug() << "Failed to stop watching " << indexDir().absolutePath(); + } } bool ModFolderModel::update() diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp index f0ef795d..4b878918 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp @@ -47,12 +47,18 @@ void LocalModUpdateTask::executeTask() auto old_metadata = Metadata::get(m_index_dir, m_mod.addonId); if (old_metadata.isValid()) { emit hasOldMod(old_metadata.name, old_metadata.filename); + if (m_mod.slug.isEmpty()) + m_mod.slug = old_metadata.slug; } auto pw_mod = Metadata::create(m_index_dir, m_mod, m_mod_version); - Metadata::update(m_index_dir, pw_mod); - - emitSucceeded(); + if (pw_mod.isValid()) { + Metadata::update(m_index_dir, pw_mod); + emitSucceeded(); + } else { + qCritical() << "Tried to update an invalid mod!"; + emitFailed(tr("Invalid metadata")); + } } auto LocalModUpdateTask::abort() -> bool diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index 276414e4..4ffb626a 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -71,7 +71,7 @@ void ModFolderLoadTask::run() auto metadata = m_result->mods[chopped_id].metadata(); if (metadata) { - mod.setMetadata(new Metadata::ModStruct(*metadata)); + mod.setMetadata(*metadata); m_result->mods[mod.internal_id()].setStatus(ModStatus::Installed); m_result->mods.remove(chopped_id); diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 8bd66088..c3561093 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -55,11 +55,11 @@ auto getRealIndexName(QDir& index_dir, QString normalized_fname, bool should_fin } // Helpers -static inline auto indexFileName(QString const& mod_name) -> QString +static inline auto indexFileName(QString const& mod_slug) -> QString { - if(mod_name.endsWith(".pw.toml")) - return mod_name; - return QString("%1.pw.toml").arg(mod_name); + if(mod_slug.endsWith(".pw.toml")) + return mod_slug; + return QString("%1.pw.toml").arg(mod_slug); } static ModPlatform::ProviderCapabilities ProviderCaps; @@ -95,6 +95,7 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo { Mod mod; + mod.slug = mod_pack.slug; mod.name = mod_pack.name; mod.filename = mod_version.fileName; @@ -116,12 +117,10 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo return mod; } -auto V1::createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod +auto V1::createModFormat(QDir& index_dir, ::Mod& internal_mod, QString slug) -> Mod { - auto mod_name = internal_mod.name(); - // Try getting metadata if it exists - Mod mod { getIndexForMod(index_dir, mod_name) }; + Mod mod { getIndexForMod(index_dir, slug) }; if(mod.isValid()) return mod; @@ -139,7 +138,7 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) // Ensure the corresponding mod's info exists, and create it if not - auto normalized_fname = indexFileName(mod.name); + auto normalized_fname = indexFileName(mod.slug); auto real_fname = getRealIndexName(index_dir, normalized_fname); QFile index_file(index_dir.absoluteFilePath(real_fname)); @@ -187,12 +186,13 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) } } + index_file.flush(); index_file.close(); } -void V1::deleteModIndex(QDir& index_dir, QString& mod_name) +void V1::deleteModIndex(QDir& index_dir, QString& mod_slug) { - auto normalized_fname = indexFileName(mod_name); + auto normalized_fname = indexFileName(mod_slug); auto real_fname = getRealIndexName(index_dir, normalized_fname); if (real_fname.isEmpty()) return; @@ -200,12 +200,12 @@ void V1::deleteModIndex(QDir& index_dir, QString& mod_name) QFile index_file(index_dir.absoluteFilePath(real_fname)); if (!index_file.exists()) { - qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_name); + qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_slug); return; } if (!index_file.remove()) { - qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_name); + qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_slug); } } @@ -221,11 +221,11 @@ void V1::deleteModIndex(QDir& index_dir, QVariant& mod_id) } } -auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod +auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod { Mod mod; - auto normalized_fname = indexFileName(index_file_name); + auto normalized_fname = indexFileName(slug); auto real_fname = getRealIndexName(index_dir, normalized_fname, true); if (real_fname.isEmpty()) return {}; @@ -233,7 +233,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod QFile index_file(index_dir.absoluteFilePath(real_fname)); if (!index_file.open(QIODevice::ReadOnly)) { - qWarning() << QString("Failed to open mod metadata for %1").arg(index_file_name); + qWarning() << QString("Failed to open mod metadata for %1").arg(slug); return {}; } @@ -247,11 +247,13 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod index_file.close(); if (!table) { - qWarning() << QString("Could not open file %1!").arg(indexFileName(index_file_name)); + qWarning() << QString("Could not open file %1!").arg(normalized_fname); qWarning() << "Reason: " << QString(errbuf); return {}; } + mod.slug = slug; + { // Basic info mod.name = stringEntry(table, "name"); mod.filename = stringEntry(table, "filename"); diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 9d643703..3ec80377 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -40,6 +40,7 @@ auto intEntry(toml_table_t* parent, const char* entry_name) -> int; class V1 { public: struct Mod { + QString slug {}; QString name {}; QString filename {}; // FIXME: make side an enum @@ -58,7 +59,7 @@ class V1 { public: // This is a totally heuristic, but should work for now. - auto isValid() const -> bool { return !name.isEmpty() && !project_id.isNull(); } + auto isValid() const -> bool { return !slug.isEmpty() && !project_id.isNull(); } // Different providers can use different names for the same thing // Modrinth-specific @@ -71,9 +72,9 @@ class V1 { * */ static auto createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod; /* Generates the object representing the information in a mod.pw.toml file via - * its common representation in the launcher. + * its common representation in the launcher, plus a necessary slug. * */ - static auto createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod; + static auto createModFormat(QDir& index_dir, ::Mod& internal_mod, QString slug) -> Mod; /* Updates the mod index for the provided mod. * This creates a new index if one does not exist already @@ -81,16 +82,16 @@ class V1 { * */ static void updateModIndex(QDir& index_dir, Mod& mod); - /* Deletes the metadata for the mod with the given name. If the metadata doesn't exist, it does nothing. */ - static void deleteModIndex(QDir& index_dir, QString& mod_name); + /* Deletes the metadata for the mod with the given slug. If the metadata doesn't exist, it does nothing. */ + static void deleteModIndex(QDir& index_dir, QString& mod_slug); /* Deletes the metadata for the mod with the given id. If the metadata doesn't exist, it does nothing. */ static void deleteModIndex(QDir& index_dir, QVariant& mod_id); - /* Gets the metadata for a mod with a particular name. + /* Gets the metadata for a mod with a particular file name. * If the mod doesn't have a metadata, it simply returns an empty Mod object. * */ - static auto getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod; + static auto getIndexForMod(QDir& index_dir, QString slug) -> Mod; /* Gets the metadata for a mod with a particular id. * If the mod doesn't have a metadata, it simply returns an empty Mod object. diff --git a/launcher/modplatform/packwiz/Packwiz_test.cpp b/launcher/modplatform/packwiz/Packwiz_test.cpp index d6251148..aa0c35df 100644 --- a/launcher/modplatform/packwiz/Packwiz_test.cpp +++ b/launcher/modplatform/packwiz/Packwiz_test.cpp @@ -32,10 +32,11 @@ class PackwizTest : public QObject { QString source = QFINDTESTDATA("testdata"); QDir index_dir(source); - QString name_mod("borderless-mining.pw.toml"); - QVERIFY(index_dir.entryList().contains(name_mod)); + QString slug_mod("borderless-mining"); + QString file_name = slug_mod + ".pw.toml"; + QVERIFY(index_dir.entryList().contains(file_name)); - auto metadata = Packwiz::V1::getIndexForMod(index_dir, name_mod); + auto metadata = Packwiz::V1::getIndexForMod(index_dir, slug_mod); QVERIFY(metadata.isValid()); From 52c45c2d32e6bb10b2ca5db9b73cb91fac57c943 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 19 Jun 2022 14:29:21 -0300 Subject: [PATCH 24/38] feat: add some mod api calls - Get Project: Already existed but required a specific caller type. This is more general. - Get Projects: A single call to multiple of the above Both providers support these calls. Signed-off-by: flow --- launcher/modplatform/ModAPI.h | 4 +++ launcher/modplatform/flame/FlameAPI.cpp | 23 +++++++++++++++ launcher/modplatform/flame/FlameAPI.h | 4 +-- .../modplatform/helpers/NetworkModAPI.cpp | 29 +++++++++++++------ launcher/modplatform/helpers/NetworkModAPI.h | 2 ++ launcher/modplatform/modrinth/ModrinthAPI.cpp | 15 ++++++++-- launcher/modplatform/modrinth/ModrinthAPI.h | 8 ++++- 7 files changed, 71 insertions(+), 14 deletions(-) diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h index cf116353..26fe9d02 100644 --- a/launcher/modplatform/ModAPI.h +++ b/launcher/modplatform/ModAPI.h @@ -40,6 +40,7 @@ #include #include "Version.h" +#include "net/NetJob.h" namespace ModPlatform { class ListModel; @@ -74,6 +75,9 @@ class ModAPI { virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0; virtual void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) = 0; + virtual auto getProject(QString addonId, QByteArray* response) const -> NetJob::Ptr = 0; + virtual auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob::Ptr = 0; + struct VersionSearchArgs { QString addonId; diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index 983e09fd..065b4cc2 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -123,3 +123,26 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe return ver; } + +auto FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob::Ptr +{ + auto* netJob = new NetJob(QString("Flame::GetProjects"), APPLICATION->network()); + + QJsonObject body_obj; + QJsonArray addons_arr; + for (auto& addonId : addonIds) { + addons_arr.append(addonId); + } + + body_obj["modIds"] = addons_arr; + + QJsonDocument body(body_obj); + auto body_raw = body.toJson(); + + netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw)); + + QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + QObject::connect(netJob, &NetJob::failed, [body_raw] { qDebug() << body_raw; }); + + return netJob; +} diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index e45b5cb1..1646ec79 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -3,8 +3,6 @@ #include "modplatform/ModIndex.h" #include "modplatform/helpers/NetworkModAPI.h" -#include "net/NetJob.h" - class FlameAPI : public NetworkModAPI { public: auto matchFingerprints(const std::list& fingerprints, QByteArray* response) -> NetJob::Ptr; @@ -12,6 +10,8 @@ class FlameAPI : public NetworkModAPI { auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion; + auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob::Ptr override; + private: inline auto getSortFieldInt(QString sortString) const -> int { diff --git a/launcher/modplatform/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp index d7abd10f..e8cba12e 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.cpp +++ b/launcher/modplatform/helpers/NetworkModAPI.cpp @@ -33,18 +33,14 @@ void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const void NetworkModAPI::getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) { - auto id_str = pack.addonId.toString(); - auto netJob = new NetJob(QString("%1::ModInfo").arg(id_str), APPLICATION->network()); - auto searchUrl = getModInfoURL(id_str); - auto response = new QByteArray(); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); + auto job = getProject(pack.addonId.toString(), response); - QObject::connect(netJob, &NetJob::succeeded, [response, &pack, caller] { + QObject::connect(job.get(), &NetJob::succeeded, caller, [caller, &pack, response] { QJsonParseError parse_error{}; - auto doc = QJsonDocument::fromJson(*response, &parse_error); + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response for " << pack.name << " at " << parse_error.offset + qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << *response; return; @@ -53,7 +49,7 @@ void NetworkModAPI::getModInfo(CallerType* caller, ModPlatform::IndexedPack& pac caller->infoRequestFinished(doc, pack); }); - netJob->start(); + job->start(); } void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) const @@ -83,3 +79,18 @@ void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) co netJob->start(); } + +auto NetworkModAPI::getProject(QString addonId, QByteArray* response) const -> NetJob::Ptr +{ + auto netJob = new NetJob(QString("%1::GetProject").arg(addonId), APPLICATION->network()); + auto searchUrl = getModInfoURL(addonId); + + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); + + QObject::connect(netJob, &NetJob::finished, [response, netJob] { + netJob->deleteLater(); + delete response; + }); + + return netJob; +} diff --git a/launcher/modplatform/helpers/NetworkModAPI.h b/launcher/modplatform/helpers/NetworkModAPI.h index 87d77ad1..ea28c688 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.h +++ b/launcher/modplatform/helpers/NetworkModAPI.h @@ -8,6 +8,8 @@ class NetworkModAPI : public ModAPI { void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) override; void getVersions(CallerType* caller, VersionSearchArgs&& args) const override; + auto getProject(QString addonId, QByteArray* response) const -> NetJob::Ptr override; + protected: virtual auto getModSearchURL(SearchArgs& args) const -> QString = 0; virtual auto getModInfoURL(QString& id) const -> QString = 0; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp index 0d5d4bab..301c0be8 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.cpp +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -88,8 +88,19 @@ auto ModrinthAPI::latestVersions(const QStringList& hashes, QJsonDocument body(body_obj); auto body_raw = body.toJson(); - netJob->addNetAction(Net::Upload::makeByteArray( - QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), response, body_raw)); + netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), response, body_raw)); + + QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + + return netJob; +} + +auto ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob::Ptr +{ + auto netJob = new NetJob(QString("Modrinth::GetProjects"), APPLICATION->network()); + auto searchUrl = getMultipleModInfoURL(addonIds); + + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 9694b85e..2909daf9 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -22,7 +22,6 @@ #include "modplatform/ModAPI.h" #include "modplatform/ModIndex.h" #include "modplatform/helpers/NetworkModAPI.h" -#include "net/NetJob.h" #include @@ -48,6 +47,8 @@ class ModrinthAPI : public NetworkModAPI { ModLoaderTypes loaders, QByteArray* response) -> NetJob::Ptr; + auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob::Ptr override; + public: inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; @@ -103,6 +104,11 @@ class ModrinthAPI : public NetworkModAPI { return BuildConfig.MODRINTH_PROD_URL + "/project/" + id; }; + inline auto getMultipleModInfoURL(QStringList ids) const -> QString + { + return BuildConfig.MODRINTH_PROD_URL + QString("/projects?ids=[\"%1\"]").arg(ids.join("\",\"")); + }; + inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override { return QString(BuildConfig.MODRINTH_PROD_URL + From a7648d60ce1d1567cd1c878aaa55dae3696a0210 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 19 Jun 2022 14:31:44 -0300 Subject: [PATCH 25/38] fix: don't require non-essential items in mod index Also adds slug field. Signed-off-by: flow --- launcher/modplatform/ModIndex.h | 5 +++-- launcher/modplatform/flame/FlameModIndex.cpp | 9 +++++---- launcher/modplatform/modrinth/ModrinthPackIndex.cpp | 13 ++++++++----- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index f8ef211e..dc297d03 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -55,11 +55,11 @@ struct IndexedVersion { QVariant fileId; QString version; QString version_number = {}; - QVector mcVersion; + QStringList mcVersion; QString downloadUrl; QString date; QString fileName; - QVector loaders = {}; + QStringList loaders = {}; QString hash_type; QString hash; bool is_preferred = true; @@ -79,6 +79,7 @@ struct IndexedPack { QVariant addonId; Provider provider; QString name; + QString slug; QString description; QList authors; QString logoName; diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index a3222f44..746018e2 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -14,14 +14,15 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) pack.addonId = Json::requireInteger(obj, "id"); pack.provider = ModPlatform::Provider::FLAME; pack.name = Json::requireString(obj, "name"); + pack.slug = Json::requireString(obj, "slug"); pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", ""); pack.description = Json::ensureString(obj, "summary", ""); - QJsonObject logo = Json::requireObject(obj, "logo"); - pack.logoName = Json::requireString(logo, "title"); - pack.logoUrl = Json::requireString(logo, "thumbnailUrl"); + QJsonObject logo = Json::ensureObject(obj, "logo"); + pack.logoName = Json::ensureString(logo, "title"); + pack.logoUrl = Json::ensureString(logo, "thumbnailUrl"); - auto authors = Json::requireArray(obj, "authors"); + auto authors = Json::ensureArray(obj, "authors"); for (auto authorIter : authors) { auto author = Json::requireObject(authorIter); ModPlatform::ModpackAuthor packAuthor; diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 1910c9be..e50dd96d 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -29,13 +29,16 @@ static ModPlatform::ProviderCapabilities ProviderCaps; void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { - pack.addonId = Json::requireString(obj, "project_id"); + pack.addonId = Json::ensureString(obj, "project_id"); + if (pack.addonId.toString().isEmpty()) + pack.addonId = Json::requireString(obj, "id"); + pack.provider = ModPlatform::Provider::MODRINTH; pack.name = Json::requireString(obj, "title"); - QString slug = Json::ensureString(obj, "slug", ""); - if (!slug.isEmpty()) - pack.websiteUrl = "https://modrinth.com/mod/" + Json::ensureString(obj, "slug", ""); + pack.slug = Json::ensureString(obj, "slug", ""); + if (!pack.slug.isEmpty()) + pack.websiteUrl = "https://modrinth.com/mod/" + pack.slug; else pack.websiteUrl = ""; @@ -45,7 +48,7 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) pack.logoName = pack.addonId.toString(); ModPlatform::ModpackAuthor modAuthor; - modAuthor.name = Json::requireString(obj, "author"); + modAuthor.name = Json::ensureString(obj, "author", QObject::tr("No author(s)")); modAuthor.url = api.getAuthorURL(modAuthor.name); pack.authors.append(modAuthor); From 5f75e531e61e1f2cb5d602e084e9a0ddd1c85a5c Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 20 Jun 2022 08:55:35 -0300 Subject: [PATCH 26/38] fix: handling around disabled mods Don't update disabled mods to prevent mod duplication. Also, chop filename in the metadata with a '.disabled'. Signed-off-by: flow --- launcher/modplatform/EnsureMetadataTask.cpp | 4 ++++ launcher/modplatform/flame/FlameCheckUpdate.cpp | 5 +++++ launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp | 5 +++++ launcher/ui/dialogs/ModUpdateDialog.cpp | 10 +++++++--- launcher/ui/dialogs/ModUpdateDialog.h | 2 +- 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index cf4e55b9..19e44ce0 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -455,6 +455,8 @@ void EnsureMetadataTask::modrinthCallback(ModPlatform::IndexedPack& pack, ModPla { // Prevent file name mismatch ver.fileName = mod.fileinfo().fileName(); + if (ver.fileName.endsWith(".disabled")) + ver.fileName.chop(9); QDir tmp_index_dir(m_index_dir); @@ -487,6 +489,8 @@ void EnsureMetadataTask::flameCallback(ModPlatform::IndexedPack& pack, ModPlatfo try { // Prevent file name mismatch ver.fileName = mod.fileinfo().fileName(); + if (ver.fileName.endsWith(".disabled")) + ver.fileName.chop(9); QDir tmp_index_dir(m_index_dir); diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index 3658bf8d..be12dee3 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -118,6 +118,11 @@ void FlameCheckUpdate::executeTask() int i = 0; for (auto mod : m_mods) { + if (!mod.enabled()) { + emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!")); + continue; + } + setStatus(tr("Getting API response from CurseForge for '%1'").arg(mod.name())); setProgress(i++, m_mods.size()); diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index 78275cf0..5d936fec 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -33,6 +33,11 @@ void ModrinthCheckUpdate::executeTask() QStringList hashes; auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first(); for (auto mod : m_mods) { + if (!mod.enabled()) { + emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!")); + continue; + } + auto hash = mod.metadata()->hash; // Sadly the API can only handle one hash type per call, se we diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index 7584621a..2e1fbb08 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -62,8 +62,10 @@ void ModUpdateDialog::checkCandidates() // Report failed metadata generation if (!m_failed_metadata.empty()) { QString text; - for (const auto& mod : m_failed_metadata) { - text += tr("Mod name: %1
File name: %2

").arg(mod.name(), mod.fileinfo().fileName()); + for (const auto& failed : m_failed_metadata) { + const auto& mod = std::get<0>(failed); + const auto& reason = std::get<1>(failed); + text += tr("Mod name: %1
File name: %2
Reason: %3

").arg(mod.name(), mod.fileinfo().fileName(), reason); } ScrollMessageBox message_dialog(m_parent, tr("Metadata generation failed"), @@ -316,7 +318,9 @@ void ModUpdateDialog::onMetadataFailed(Mod& mod, bool try_others, ModPlatform::P m_second_try_metadata->addTask(task); } else { - m_failed_metadata.push_back(mod); + QString reason { tr("Didn't find a valid version on the selected mod provider(s)") }; + + m_failed_metadata.emplace_back(mod, reason); } } diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h index f40fc594..336fbba2 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.h +++ b/launcher/ui/dialogs/ModUpdateDialog.h @@ -50,7 +50,7 @@ class ModUpdateDialog final : public ReviewMessageBox { std::list m_flame_to_update; SequentialTask* m_second_try_metadata; - std::list m_failed_metadata; + std::list> m_failed_metadata; std::list> m_failed_check_update; QHash m_tasks; From dfd6cb29be99ed9efb73a78eb7c31b6070c2d3c9 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 20 Jun 2022 13:01:49 -0300 Subject: [PATCH 27/38] feat: improve changelog and sort updatable mods Signed-off-by: flow --- launcher/ui/dialogs/ModUpdateDialog.cpp | 60 +++++++++++++++---------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index 2e1fbb08..2d969828 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -16,8 +16,7 @@ #include "modplatform/modrinth/ModrinthCheckUpdate.h" #include - -#include +#include #include static ModPlatform::ProviderCapabilities ProviderCaps; @@ -47,7 +46,6 @@ ModUpdateDialog::ModUpdateDialog(QWidget* parent, ui->explainLabel->setText(tr("You're about to update the following mods:")); ui->onlyCheckedLabel->setText(tr("Only mods with a check will be updated!")); - } void ModUpdateDialog::checkCandidates() @@ -69,9 +67,9 @@ void ModUpdateDialog::checkCandidates() } ScrollMessageBox message_dialog(m_parent, tr("Metadata generation failed"), - tr("Could not generate metadata for the following mods:
" - "Do you wish to proceed without those mods?"), - text); + tr("Could not generate metadata for the following mods:
" + "Do you wish to proceed without those mods?"), + text); message_dialog.setModal(true); if (message_dialog.exec() == QDialog::Rejected) { m_aborted = true; @@ -83,7 +81,7 @@ void ModUpdateDialog::checkCandidates() auto versions = mcVersions(m_instance); auto loaders = mcLoaders(m_instance); - SequentialTask check_task (m_parent, tr("Checking for updates")); + SequentialTask check_task(m_parent, tr("Checking for updates")); if (!m_modrinth_to_update.empty()) { m_modrinth_check_task = new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_mod_model); @@ -166,9 +164,9 @@ void ModUpdateDialog::checkCandidates() } ScrollMessageBox message_dialog(m_parent, tr("Failed to check for updates"), - tr("Could not check or get the following mods for updates:
" - "Do you wish to proceed without those mods?"), - text); + tr("Could not check or get the following mods for updates:
" + "Do you wish to proceed without those mods?"), + text); message_dialog.setModal(true); if (message_dialog.exec() == QDialog::Rejected) { m_aborted = true; @@ -178,8 +176,21 @@ void ModUpdateDialog::checkCandidates() } // If there's no mod to be updated - if (ui->modTreeWidget->topLevelItemCount() == 0) + if (ui->modTreeWidget->topLevelItemCount() == 0) { m_no_updates = true; + } else { + // FIXME: Find a more efficient way of doing this! + + // Sort major items in alphabetical order (also sorts the children unfortunately) + ui->modTreeWidget->sortItems(0, Qt::SortOrder::AscendingOrder); + + // Re-sort the children + auto* item = ui->modTreeWidget->topLevelItem(0); + for (int i = 1; item != nullptr; ++i) { + item->sortChildren(0, Qt::SortOrder::DescendingOrder); + item = ui->modTreeWidget->topLevelItem(i); + } + } if (m_aborted || m_no_updates) QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); @@ -318,7 +329,7 @@ void ModUpdateDialog::onMetadataFailed(Mod& mod, bool try_others, ModPlatform::P m_second_try_metadata->addTask(task); } else { - QString reason { tr("Didn't find a valid version on the selected mod provider(s)") }; + QString reason{ tr("Didn't find a valid version on the selected mod provider(s)") }; m_failed_metadata.emplace_back(mod, reason); } @@ -344,20 +355,23 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info) changelog_item->setText(0, tr("Changelog of the latest version")); auto changelog = new QTreeWidgetItem(changelog_item); + auto changelog_area = new QTextBrowser(); - auto changelog_area = new QTextEdit(); - HoeDown h; - changelog_area->setText(h.process(info.changelog.toUtf8())); - changelog_area->setReadOnly(true); - if (info.changelog.size() < 250) // heuristic - changelog_area->setSizeAdjustPolicy(QTextEdit::SizeAdjustPolicy::AdjustToContents); + switch (info.provider) { + case ModPlatform::Provider::MODRINTH: { + HoeDown h; + changelog_area->setHtml(h.process(info.changelog.toUtf8())); + break; + } + case ModPlatform::Provider::FLAME: { + changelog_area->setHtml(info.changelog); + break; + } + } + + changelog_area->setOpenExternalLinks(true); ui->modTreeWidget->setItemWidget(changelog, 0, changelog_area); - changelog_item->insertChildren(0, { changelog }); - - item_top->insertChildren(0, { old_version_item }); - item_top->insertChildren(1, { new_version_item }); - item_top->insertChildren(2, { changelog_item }); ui->modTreeWidget->addTopLevelItem(item_top); } From fac63541a4831414b052de6400e7543bbc611db0 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 24 Jun 2022 20:59:17 -0300 Subject: [PATCH 28/38] fix: work around HoeDown bug(?) in changelog line breaks Signed-off-by: flow --- launcher/ui/dialogs/ModUpdateDialog.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index 2d969828..51e37bcf 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -357,10 +357,12 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info) auto changelog = new QTreeWidgetItem(changelog_item); auto changelog_area = new QTextBrowser(); + switch (info.provider) { case ModPlatform::Provider::MODRINTH: { HoeDown h; - changelog_area->setHtml(h.process(info.changelog.toUtf8())); + // HoeDown bug?: \n aren't converted to
+ changelog_area->setHtml(h.process(info.changelog.toUtf8()).replace('\n', "
")); break; } case ModPlatform::Provider::FLAME: { From c4316e81e64ad4ac63b0b50106b324a73abdc150 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 26 Jun 2022 14:17:15 -0300 Subject: [PATCH 29/38] change: make Mod a QObject used as a pointer Prevents problems when copying it around! Signed-off-by: flow --- launcher/MMCZip.cpp | 30 ++++--- launcher/MMCZip.h | 2 +- launcher/minecraft/MinecraftInstance.cpp | 24 +++--- launcher/minecraft/MinecraftInstance.h | 2 +- launcher/minecraft/mod/Mod.h | 6 +- launcher/minecraft/mod/ModFolderModel.cpp | 76 +++++++++--------- launcher/minecraft/mod/ModFolderModel.h | 16 ++-- .../minecraft/mod/tasks/ModFolderLoadTask.cpp | 32 ++++---- .../minecraft/mod/tasks/ModFolderLoadTask.h | 2 +- launcher/modplatform/CheckUpdateTask.h | 6 +- launcher/modplatform/EnsureMetadataTask.cpp | 80 ++++++++++--------- launcher/modplatform/EnsureMetadataTask.h | 20 ++--- .../modplatform/flame/FlameCheckUpdate.cpp | 34 ++++---- launcher/modplatform/flame/FlameCheckUpdate.h | 2 +- .../modrinth/ModrinthCheckUpdate.cpp | 34 ++++---- .../modrinth/ModrinthCheckUpdate.h | 2 +- launcher/ui/dialogs/ModUpdateDialog.cpp | 59 +++++++------- launcher/ui/dialogs/ModUpdateDialog.h | 16 ++-- launcher/ui/pages/instance/ModFolderPage.cpp | 2 +- 19 files changed, 229 insertions(+), 216 deletions(-) diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index f20d6dff..1627ee07 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -127,7 +127,7 @@ bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList } // ours -bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods) +bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods) { QuaZip zipOut(targetJarPath); if (!zipOut.open(QuaZip::mdCreate)) @@ -141,42 +141,40 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const QSet addedFiles; // Modify the jar - QListIterator i(mods); - i.toBack(); - while (i.hasPrevious()) + for (auto i = mods.constEnd(); i != mods.constBegin(); --i) { - const Mod &mod = i.previous(); + const Mod* mod = *i; // do not merge disabled mods. - if (!mod.enabled()) + if (!mod->enabled()) continue; - if (mod.type() == Mod::MOD_ZIPFILE) + if (mod->type() == Mod::MOD_ZIPFILE) { - if (!mergeZipFiles(&zipOut, mod.fileinfo(), addedFiles)) + if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles)) { zipOut.close(); QFile::remove(targetJarPath); - qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar."; + qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; return false; } } - else if (mod.type() == Mod::MOD_SINGLEFILE) + else if (mod->type() == Mod::MOD_SINGLEFILE) { // FIXME: buggy - does not work with addedFiles - auto filename = mod.fileinfo(); + auto filename = mod->fileinfo(); if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) { zipOut.close(); QFile::remove(targetJarPath); - qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar."; + qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; return false; } addedFiles.insert(filename.fileName()); } - else if (mod.type() == Mod::MOD_FOLDER) + else if (mod->type() == Mod::MOD_FOLDER) { // untested, but seems to be unused / not possible to reach // FIXME: buggy - does not work with addedFiles - auto filename = mod.fileinfo(); + auto filename = mod->fileinfo(); QString what_to_zip = filename.absoluteFilePath(); QDir dir(what_to_zip); dir.cdUp(); @@ -193,7 +191,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const { zipOut.close(); QFile::remove(targetJarPath); - qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar."; + qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; return false; } qDebug() << "Adding folder " << filename.fileName() << " from " @@ -204,7 +202,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const // Make sure we do not continue launching when something is missing or undefined... zipOut.close(); QFile::remove(targetJarPath); - qCritical() << "Failed to add unknown mod type" << mod.fileinfo().fileName() << "to the jar."; + qCritical() << "Failed to add unknown mod type" << mod->fileinfo().fileName() << "to the jar."; return false; } } diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index bf90cd0b..7f43d158 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -75,7 +75,7 @@ namespace MMCZip /** * take a source jar, add mods to it, resulting in target jar */ - bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods); + bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods); /** * Find a single file in archive by file name (not path) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index abc022b6..360e754d 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -700,24 +700,24 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr { out << QString("%1:").arg(label); auto modList = model.allMods(); - std::sort(modList.begin(), modList.end(), [](Mod &a, Mod &b) { - auto aName = a.fileinfo().completeBaseName(); - auto bName = b.fileinfo().completeBaseName(); + std::sort(modList.begin(), modList.end(), [](Mod::Ptr a, Mod::Ptr b) { + auto aName = a->fileinfo().completeBaseName(); + auto bName = b->fileinfo().completeBaseName(); return aName.localeAwareCompare(bName) < 0; }); - for(auto & mod: modList) + for(auto mod: modList) { - if(mod.type() == Mod::MOD_FOLDER) + if(mod->type() == Mod::MOD_FOLDER) { - out << u8" [📁] " + mod.fileinfo().completeBaseName() + " (folder)"; + out << u8" [📁] " + mod->fileinfo().completeBaseName() + " (folder)"; continue; } - if(mod.enabled()) { - out << u8" [✔️] " + mod.fileinfo().completeBaseName(); + if(mod->enabled()) { + out << u8" [✔️]" + mod->fileinfo().completeBaseName(); } else { - out << u8" [❌] " + mod.fileinfo().completeBaseName() + " (disabled)"; + out << u8" [❌] " + mod->fileinfo().completeBaseName() + " (disabled)"; } } @@ -1136,16 +1136,16 @@ std::shared_ptr MinecraftInstance::gameOptionsModel() const return m_game_options; } -QList< Mod > MinecraftInstance::getJarMods() const +QList MinecraftInstance::getJarMods() const { auto profile = m_components->getProfile(); - QList mods; + QList mods; for (auto jarmod : profile->getJarMods()) { QStringList jar, temp1, temp2, temp3; jarmod->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, jarmodsPath().absolutePath()); // QString filePath = jarmodsPath().absoluteFilePath(jarmod->filename(currentSystem)); - mods.push_back(Mod(QFileInfo(jar[0]))); + mods.push_back(new Mod(QFileInfo(jar[0]))); } return mods; } diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index 05450d41..8e1c67f2 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -81,7 +81,7 @@ public: shared_qobject_ptr createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override; QStringList extraArguments() const override; QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override; - QList getJarMods() const; + QList getJarMods() const; QString createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin); /// get arguments passed to java QStringList javaArguments() const; diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index cbbbd362..3d3becd7 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -39,10 +39,12 @@ #include #include +#include "QObjectPtr.h" #include "ModDetails.h" -class Mod +class Mod : public QObject { + Q_OBJECT public: enum ModType { @@ -53,6 +55,8 @@ public: MOD_LITEMOD, //!< The mod is a litemod }; + using Ptr = shared_qobject_ptr; + Mod() = default; Mod(const QFileInfo &file); explicit Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata); diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index d8170067..e0391c01 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -134,7 +134,7 @@ void ModFolderModel::finishUpdate() QSet newSet(newList.begin(), newList.end()); #else QSet currentSet = modsIndex.keys().toSet(); - auto & newMods = m_update->mods; + auto& newMods = m_update->mods; QSet newSet = newMods.keys().toSet(); #endif @@ -142,19 +142,20 @@ void ModFolderModel::finishUpdate() { QSet kept = currentSet; kept.intersect(newSet); - for(auto & keptMod: kept) { - auto & newMod = newMods[keptMod]; + for(auto& keptMod : kept) { + auto* newMod = newMods[keptMod]; auto row = modsIndex[keptMod]; - auto & currentMod = mods[row]; - if(newMod.dateTimeChanged() == currentMod.dateTimeChanged()) { + auto currentMod = mods[row]; + if(newMod->dateTimeChanged() == currentMod->dateTimeChanged()) { // no significant change, ignore... continue; } - auto & oldMod = mods[row]; - if(oldMod.isResolving()) { - activeTickets.remove(oldMod.resolutionTicket()); + auto oldMod = mods[row]; + if(oldMod->isResolving()) { + activeTickets.remove(oldMod->resolutionTicket()); } - oldMod = newMod; + + mods[row] = newMod; resolveMod(mods[row]); emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); } @@ -173,9 +174,10 @@ void ModFolderModel::finishUpdate() int removedIndex = *iter; beginRemoveRows(QModelIndex(), removedIndex, removedIndex); auto removedIter = mods.begin() + removedIndex; - if(removedIter->isResolving()) { - activeTickets.remove(removedIter->resolutionTicket()); + if((*removedIter)->isResolving()) { + activeTickets.remove((*removedIter)->resolutionTicket()); } + mods.erase(removedIter); endRemoveRows(); } @@ -201,8 +203,8 @@ void ModFolderModel::finishUpdate() { modsIndex.clear(); int idx = 0; - for(auto & mod: mods) { - modsIndex[mod.internal_id()] = idx; + for(auto mod: mods) { + modsIndex[mod->internal_id()] = idx; idx++; } } @@ -217,17 +219,17 @@ void ModFolderModel::finishUpdate() } } -void ModFolderModel::resolveMod(Mod& m) +void ModFolderModel::resolveMod(Mod::Ptr m) { - if(!m.shouldResolve()) { + if(!m->shouldResolve()) { return; } - auto task = new LocalModParseTask(nextResolutionTicket, m.type(), m.fileinfo()); + auto task = new LocalModParseTask(nextResolutionTicket, m->type(), m->fileinfo()); auto result = task->result(); - result->id = m.internal_id(); + result->id = m->internal_id(); activeTickets.insert(nextResolutionTicket, result); - m.setResolving(true, nextResolutionTicket); + m->setResolving(true, nextResolutionTicket); nextResolutionTicket++; QThreadPool *threadPool = QThreadPool::globalInstance(); connect(task, &LocalModParseTask::finished, this, &ModFolderModel::finishModParse); @@ -243,8 +245,8 @@ void ModFolderModel::finishModParse(int token) auto result = *iter; activeTickets.remove(token); int row = modsIndex[result->id]; - auto & mod = mods[row]; - mod.finishResolvingWithDetails(result->details); + auto mod = mods[row]; + mod->finishResolvingWithDetails(result->details); emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } @@ -269,9 +271,9 @@ bool ModFolderModel::isValid() return m_dir.exists() && m_dir.isReadable(); } -auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> std::list +auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> std::list { - std::list selected_mods; + std::list selected_mods; for (auto i : indexes) { if(i.column() != 0) continue; @@ -370,9 +372,9 @@ bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadat { for(auto mod : allMods()){ - if(mod.fileinfo().fileName() == filename){ + if(mod->fileinfo().fileName() == filename){ auto index_dir = indexDir(); - mod.destroy(index_dir, preserve_metadata); + mod->destroy(index_dir, preserve_metadata); return true; } } @@ -413,9 +415,9 @@ bool ModFolderModel::deleteMods(const QModelIndexList& indexes) if(i.column() != 0) { continue; } - Mod &m = mods[i.row()]; + auto m = mods[i.row()]; auto index_dir = indexDir(); - m.destroy(index_dir); + m->destroy(index_dir); } return true; } @@ -442,9 +444,9 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const switch (column) { case NameColumn: - return mods[row].name(); + return mods[row]->name(); case VersionColumn: { - switch(mods[row].type()) { + switch(mods[row]->type()) { case Mod::MOD_FOLDER: return tr("Folder"); case Mod::MOD_SINGLEFILE: @@ -452,23 +454,23 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const default: break; } - return mods[row].version(); + return mods[row]->version(); } case DateColumn: - return mods[row].dateTimeChanged(); + return mods[row]->dateTimeChanged(); default: return QVariant(); } case Qt::ToolTipRole: - return mods[row].internal_id(); + return mods[row]->internal_id(); case Qt::CheckStateRole: switch (column) { case ActiveColumn: - return mods[row].enabled() ? Qt::Checked : Qt::Unchecked; + return mods[row]->enabled() ? Qt::Checked : Qt::Unchecked; default: return QVariant(); } @@ -508,20 +510,20 @@ bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction actio break; case Toggle: default: - desiredStatus = !mod.enabled(); + desiredStatus = !mod->enabled(); break; } - if(desiredStatus == mod.enabled()) { + if(desiredStatus == mod->enabled()) { return true; } // preserve the row, but change its ID - auto oldId = mod.internal_id(); - if(!mod.enable(!mod.enabled())) { + auto oldId = mod->internal_id(); + if(!mod->enable(!mod->enabled())) { return false; } - auto newId = mod.internal_id(); + auto newId = mod->internal_id(); if(modsIndex.contains(newId)) { // NOTE: this could handle a corner case, where we are overwriting a file, because the same 'mod' exists both enabled and disabled // But is it necessary? diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 10289f8d..04681879 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -101,13 +101,13 @@ public: { return size() == 0; } - Mod &operator[](size_t index) + Mod& operator[](size_t index) { - return mods[index]; + return *mods[index]; } - const Mod &at(size_t index) const + const Mod& at(size_t index) const { - return mods.at(index); + return *mods.at(index); } /// Reloads the mod list and returns true if the list changed. @@ -141,12 +141,12 @@ public: return { QString("%1/.index").arg(dir().absolutePath()) }; } - const QList & allMods() + const QList& allMods() { return mods; } - auto selectedMods(QModelIndexList& indexes) -> std::list; + auto selectedMods(QModelIndexList& indexes) -> std::list; public slots: void disableInteraction(bool disabled); @@ -161,7 +161,7 @@ signals: void updateFinished(); private: - void resolveMod(Mod& m); + void resolveMod(Mod::Ptr m); bool setModStatus(int index, ModStatusAction action); protected: @@ -175,5 +175,5 @@ protected: QMap modsIndex; QMap activeTickets; int nextResolutionTicket = 0; - QList mods; + QList mods; }; diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index 4ffb626a..63a6ca90 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -53,33 +53,33 @@ void ModFolderLoadTask::run() // Read JAR files that don't have metadata m_mods_dir.refresh(); for (auto entry : m_mods_dir.entryInfoList()) { - Mod mod(entry); + auto* mod = new Mod(entry); - if (mod.enabled()) { - if (m_result->mods.contains(mod.internal_id())) { - m_result->mods[mod.internal_id()].setStatus(ModStatus::Installed); + if (mod->enabled()) { + if (m_result->mods.contains(mod->internal_id())) { + m_result->mods[mod->internal_id()]->setStatus(ModStatus::Installed); } else { - m_result->mods[mod.internal_id()] = mod; - m_result->mods[mod.internal_id()].setStatus(ModStatus::NoMetadata); + m_result->mods[mod->internal_id()] = mod; + m_result->mods[mod->internal_id()]->setStatus(ModStatus::NoMetadata); } } else { - QString chopped_id = mod.internal_id().chopped(9); + QString chopped_id = mod->internal_id().chopped(9); if (m_result->mods.contains(chopped_id)) { - m_result->mods[mod.internal_id()] = mod; + m_result->mods[mod->internal_id()] = mod; - auto metadata = m_result->mods[chopped_id].metadata(); + auto metadata = m_result->mods[chopped_id]->metadata(); if (metadata) { - mod.setMetadata(*metadata); + mod->setMetadata(*metadata); - m_result->mods[mod.internal_id()].setStatus(ModStatus::Installed); + m_result->mods[mod->internal_id()]->setStatus(ModStatus::Installed); m_result->mods.remove(chopped_id); } } else { - m_result->mods[mod.internal_id()] = mod; - m_result->mods[mod.internal_id()].setStatus(ModStatus::NoMetadata); + m_result->mods[mod->internal_id()] = mod; + m_result->mods[mod->internal_id()]->setStatus(ModStatus::NoMetadata); } } } @@ -97,8 +97,8 @@ void ModFolderLoadTask::getFromMetadata() return; } - Mod mod(m_mods_dir, metadata); - mod.setStatus(ModStatus::NotInstalled); - m_result->mods[mod.internal_id()] = mod; + auto* mod = new Mod(m_mods_dir, metadata); + mod->setStatus(ModStatus::NotInstalled); + m_result->mods[mod->internal_id()] = mod; } } diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h index 088f873e..7568fdf5 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h @@ -48,7 +48,7 @@ class ModFolderLoadTask : public QObject, public QRunnable Q_OBJECT public: struct Result { - QMap mods; + QMap mods; }; using ResultPtr = std::shared_ptr; ResultPtr result() const { diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h index d96fc340..94ee19b3 100644 --- a/launcher/modplatform/CheckUpdateTask.h +++ b/launcher/modplatform/CheckUpdateTask.h @@ -12,7 +12,7 @@ class CheckUpdateTask : public Task { Q_OBJECT public: - CheckUpdateTask(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) + CheckUpdateTask(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) : Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder) {}; struct UpdatableMod { @@ -39,10 +39,10 @@ class CheckUpdateTask : public Task { void executeTask() override = 0; signals: - void checkFailed(Mod failed, QString reason, QUrl recover_url = {}); + void checkFailed(Mod* failed, QString reason, QUrl recover_url = {}); protected: - std::list& m_mods; + std::list& m_mods; std::list& m_game_versions; ModAPI::ModLoaderTypes m_loaders; std::shared_ptr m_mods_folder; diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index 19e44ce0..7c153511 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -19,7 +19,7 @@ static ModPlatform::ProviderCapabilities ProviderCaps; static ModrinthAPI modrinth_api; static FlameAPI flame_api; -EnsureMetadataTask::EnsureMetadataTask(Mod& mod, QDir dir, ModPlatform::Provider prov) : Task(nullptr), m_index_dir(dir), m_provider(prov) +EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider prov) : Task(nullptr), m_index_dir(dir), m_provider(prov) { auto hash = getHash(mod); if (hash.isEmpty()) @@ -28,11 +28,11 @@ EnsureMetadataTask::EnsureMetadataTask(Mod& mod, QDir dir, ModPlatform::Provider m_mods.insert(hash, mod); } -EnsureMetadataTask::EnsureMetadataTask(std::list& mods, QDir dir, ModPlatform::Provider prov) +EnsureMetadataTask::EnsureMetadataTask(std::list& mods, QDir dir, ModPlatform::Provider prov) : Task(nullptr), m_index_dir(dir), m_provider(prov) { - for (auto& mod : mods) { - if (!mod.valid()) { + for (auto* mod : mods) { + if (!mod->valid()) { emitFail(mod); continue; } @@ -47,14 +47,14 @@ EnsureMetadataTask::EnsureMetadataTask(std::list& mods, QDir dir, ModPlatfo } } -QString EnsureMetadataTask::getHash(Mod& mod) +QString EnsureMetadataTask::getHash(Mod* mod) { /* Here we create a mapping hash -> mod, because we need that relationship to parse the API routes */ QByteArray jar_data; try { - jar_data = FS::read(mod.fileinfo().absoluteFilePath()); + jar_data = FS::read(mod->fileinfo().absoluteFilePath()); } catch (FS::FileSystemException& e) { - qCritical() << QString("Failed to open / read JAR file of %1").arg(mod.name()); + qCritical() << QString("Failed to open / read JAR file of %1").arg(mod->name()); qCritical() << QString("Reason: ") << e.cause(); return {}; @@ -95,19 +95,19 @@ void EnsureMetadataTask::executeTask() { setStatus(tr("Checking if mods have metadata...")); - for (auto mod : m_mods) { - if (!mod.valid()) + for (auto* mod : m_mods) { + if (!mod->valid()) continue; // They already have the right metadata :o - if (mod.status() != ModStatus::NoMetadata && mod.metadata() && mod.metadata()->provider == m_provider) { - qDebug() << "Mod" << mod.name() << "already has metadata!"; + if (mod->status() != ModStatus::NoMetadata && mod->metadata() && mod->metadata()->provider == m_provider) { + qDebug() << "Mod" << mod->name() << "already has metadata!"; emitReady(mod); return; } // Folders don't have metadata - if (mod.type() == Mod::MOD_FOLDER) { + if (mod->type() == Mod::MOD_FOLDER) { emitReady(mod); return; } @@ -125,7 +125,7 @@ void EnsureMetadataTask::executeTask() } auto invalidade_leftover = [this] { - QMutableHashIterator mods_iter(m_mods); + QMutableHashIterator mods_iter(m_mods); while (mods_iter.hasNext()) { auto mod = mods_iter.next(); emitFail(mod.value()); @@ -170,23 +170,23 @@ void EnsureMetadataTask::executeTask() setStatus(tr("Requesting metadata information from %1...").arg(ProviderCaps.readableName(m_provider))); else if (!m_mods.empty()) setStatus(tr("Requesting metadata information from %1 for '%2'...") - .arg(ProviderCaps.readableName(m_provider), m_mods.begin().value().name())); + .arg(ProviderCaps.readableName(m_provider), m_mods.begin().value()->name())); m_current_task = version_task.get(); version_task->start(); } -void EnsureMetadataTask::emitReady(Mod& m) +void EnsureMetadataTask::emitReady(Mod* m) { - qDebug() << QString("Generated metadata for %1").arg(m.name()); + qDebug() << QString("Generated metadata for %1").arg(m->name()); emit metadataReady(m); m_mods.remove(getHash(m)); } -void EnsureMetadataTask::emitFail(Mod& m) +void EnsureMetadataTask::emitFail(Mod* m) { - qDebug() << QString("Failed to generate metadata for %1").arg(m.name()); + qDebug() << QString("Failed to generate metadata for %1").arg(m->name()); emit metadataFailed(m); m_mods.remove(getHash(m)); @@ -224,8 +224,8 @@ NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask() try { auto entry = Json::requireObject(entries, hash); - setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod.name())); - qDebug() << "Getting version for" << mod.name() << "from Modrinth"; + setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name())); + qDebug() << "Getting version for" << mod->name() << "from Modrinth"; m_temp_versions.insert(hash, Modrinth::loadIndexedPackVersion(entry)); } catch (Json::JsonException& e) { @@ -284,17 +284,22 @@ NetJob::Ptr EnsureMetadataTask::modrinthProjectsTask() for (auto entry : entries) { auto entry_obj = Json::requireObject(entry); - auto entry_id = Json::requireString(entry_obj, "id"); - auto hash = addonIds.find(entry_id).value(); + ModPlatform::IndexedPack pack; + Modrinth::loadIndexedPack(pack, entry_obj); - auto mod = m_mods.find(hash).value(); + auto hash = addonIds.find(pack.addonId.toString()).value(); + + auto mod_iter = m_mods.find(hash); + if (mod_iter == m_mods.end()) { + qWarning() << "Invalid project id from the API response."; + continue; + } + + auto* mod = mod_iter.value(); try { - setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod.name())); - - ModPlatform::IndexedPack pack; - Modrinth::loadIndexedPack(pack, entry_obj); + setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name())); modrinthCallback(pack, m_temp_versions.find(hash).value(), mod); } catch (Json::JsonException& e) { @@ -365,7 +370,7 @@ NetJob::Ptr EnsureMetadataTask::flameVersionsTask() continue; } - setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name())); + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg((*mod)->name())); m_temp_versions.insert(fingerprint, FlameMod::loadIndexedPackVersion(file_obj)); } @@ -385,7 +390,10 @@ NetJob::Ptr EnsureMetadataTask::flameProjectsTask() for (auto const& hash : m_mods.keys()) { if (m_temp_versions.contains(hash)) { auto const& data = m_temp_versions.find(hash).value(); - addonIds.insert(data.addonId.toString(), hash); + + auto id_str = data.addonId.toString(); + if (!id_str.isEmpty()) + addonIds.insert(data.addonId.toString(), hash); } } @@ -429,7 +437,7 @@ NetJob::Ptr EnsureMetadataTask::flameProjectsTask() auto mod = m_mods.find(hash).value(); try { - setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod.name())); + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name())); ModPlatform::IndexedPack pack; FlameMod::loadIndexedPack(pack, entry_obj); @@ -451,10 +459,10 @@ NetJob::Ptr EnsureMetadataTask::flameProjectsTask() return proj_task; } -void EnsureMetadataTask::modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod& mod) +void EnsureMetadataTask::modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod* mod) { // Prevent file name mismatch - ver.fileName = mod.fileinfo().fileName(); + ver.fileName = mod->fileinfo().fileName(); if (ver.fileName.endsWith(".disabled")) ver.fileName.chop(9); @@ -479,16 +487,16 @@ void EnsureMetadataTask::modrinthCallback(ModPlatform::IndexedPack& pack, ModPla return; } - mod.setMetadata(metadata); + mod->setMetadata(metadata); emitReady(mod); } -void EnsureMetadataTask::flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod& mod) +void EnsureMetadataTask::flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod* mod) { try { // Prevent file name mismatch - ver.fileName = mod.fileinfo().fileName(); + ver.fileName = mod->fileinfo().fileName(); if (ver.fileName.endsWith(".disabled")) ver.fileName.chop(9); @@ -513,7 +521,7 @@ void EnsureMetadataTask::flameCallback(ModPlatform::IndexedPack& pack, ModPlatfo return; } - mod.setMetadata(metadata); + mod->setMetadata(metadata); emitReady(mod); } catch (Json::JsonException& e) { diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h index 880503b9..d40a972e 100644 --- a/launcher/modplatform/EnsureMetadataTask.h +++ b/launcher/modplatform/EnsureMetadataTask.h @@ -12,8 +12,8 @@ class EnsureMetadataTask : public Task { Q_OBJECT public: - EnsureMetadataTask(Mod&, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH); - EnsureMetadataTask(std::list&, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH); + EnsureMetadataTask(Mod*, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH); + EnsureMetadataTask(std::list&, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH); ~EnsureMetadataTask() = default; @@ -31,21 +31,21 @@ class EnsureMetadataTask : public Task { auto flameProjectsTask() -> NetJob::Ptr; // Helpers - void emitReady(Mod&); - void emitFail(Mod&); + void emitReady(Mod*); + void emitFail(Mod*); - auto getHash(Mod&) -> QString; + auto getHash(Mod*) -> QString; private slots: - void modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod&); - void flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod&); + void modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*); + void flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*); signals: - void metadataReady(Mod&); - void metadataFailed(Mod&); + void metadataReady(Mod*); + void metadataFailed(Mod*); private: - QHash m_mods; + QHash m_mods; QDir m_index_dir; ModPlatform::Provider m_provider; diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index be12dee3..68a4589b 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -117,16 +117,16 @@ void FlameCheckUpdate::executeTask() setStatus(tr("Preparing mods for CurseForge...")); int i = 0; - for (auto mod : m_mods) { - if (!mod.enabled()) { + for (auto* mod : m_mods) { + if (!mod->enabled()) { emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!")); continue; } - setStatus(tr("Getting API response from CurseForge for '%1'").arg(mod.name())); + setStatus(tr("Getting API response from CurseForge for '%1'").arg(mod->name())); setProgress(i++, m_mods.size()); - auto latest_ver = api.getLatestVersion({ mod.metadata()->project_id.toString(), m_game_versions, m_loaders }); + auto latest_ver = api.getLatestVersion({ mod->metadata()->project_id.toString(), m_game_versions, m_loaders }); // Check if we were aborted while getting the latest version if (m_was_aborted) { @@ -134,7 +134,7 @@ void FlameCheckUpdate::executeTask() return; } - setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(mod.name())); + setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(mod->name())); if (!latest_ver.addonId.isValid()) { emit checkFailed(mod, tr("No valid version found for this mod. It's probably unavailable for the current game " @@ -142,7 +142,7 @@ void FlameCheckUpdate::executeTask() continue; } - if (latest_ver.downloadUrl.isEmpty() && latest_ver.fileId != mod.metadata()->file_id) { + if (latest_ver.downloadUrl.isEmpty() && latest_ver.fileId != mod->metadata()->file_id) { auto pack = getProjectInfo(latest_ver); auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver.fileId.toString()); emit checkFailed(mod, tr("Mod has a new update available, but is opted-out on CurseForge"), recover_url); @@ -150,26 +150,26 @@ void FlameCheckUpdate::executeTask() continue; } - if (!latest_ver.hash.isEmpty() && (mod.metadata()->hash != latest_ver.hash || mod.status() == ModStatus::NotInstalled)) { + if (!latest_ver.hash.isEmpty() && (mod->metadata()->hash != latest_ver.hash || mod->status() == ModStatus::NotInstalled)) { // Fake pack with the necessary info to pass to the download task :) ModPlatform::IndexedPack pack; - pack.name = mod.name(); - pack.slug = mod.metadata()->slug; - pack.addonId = mod.metadata()->project_id; - pack.websiteUrl = mod.homeurl(); - for (auto& author : mod.authors()) + pack.name = mod->name(); + pack.slug = mod->metadata()->slug; + pack.addonId = mod->metadata()->project_id; + pack.websiteUrl = mod->homeurl(); + for (auto& author : mod->authors()) pack.authors.append({ author }); - pack.description = mod.description(); + pack.description = mod->description(); pack.provider = ModPlatform::Provider::FLAME; - auto old_version = mod.version(); - if (old_version.isEmpty() && mod.status() != ModStatus::NotInstalled) { - auto current_ver = getFileInfo(latest_ver.addonId.toInt(), mod.metadata()->file_id.toInt()); + auto old_version = mod->version(); + if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) { + auto current_ver = getFileInfo(latest_ver.addonId.toInt(), mod->metadata()->file_id.toInt()); old_version = current_ver.version; } auto download_task = new ModDownloadTask(pack, latest_ver, m_mods_folder); - m_updatable.emplace_back(mod.name(), mod.metadata()->hash, old_version, latest_ver.version, + m_updatable.emplace_back(pack.name, mod->metadata()->hash, old_version, latest_ver.version, api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()), ModPlatform::Provider::FLAME, download_task); } diff --git a/launcher/modplatform/flame/FlameCheckUpdate.h b/launcher/modplatform/flame/FlameCheckUpdate.h index f068f08f..0891891d 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.h +++ b/launcher/modplatform/flame/FlameCheckUpdate.h @@ -8,7 +8,7 @@ class FlameCheckUpdate : public CheckUpdateTask { Q_OBJECT public: - FlameCheckUpdate(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) + FlameCheckUpdate(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) : CheckUpdateTask(mods, mcVersions, loaders, mods_folder) {} diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index 5d936fec..79d8edf7 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -27,29 +27,29 @@ void ModrinthCheckUpdate::executeTask() setStatus(tr("Preparing mods for Modrinth...")); setProgress(0, 3); - QHash mappings; + QHash mappings; // Create all hashes QStringList hashes; auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first(); - for (auto mod : m_mods) { - if (!mod.enabled()) { + for (auto* mod : m_mods) { + if (!mod->enabled()) { emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!")); continue; } - auto hash = mod.metadata()->hash; + auto hash = mod->metadata()->hash; // Sadly the API can only handle one hash type per call, se we // need to generate a new hash if the current one is innadequate // (though it will rarely happen, if at all) - if (mod.metadata()->hash_format != best_hash_type) { + if (mod->metadata()->hash_format != best_hash_type) { QByteArray jar_data; try { - jar_data = FS::read(mod.fileinfo().absoluteFilePath()); + jar_data = FS::read(mod->fileinfo().absoluteFilePath()); } catch (FS::FileSystemException& e) { - qCritical() << QString("Failed to open / read JAR file of %1").arg(mod.name()); + qCritical() << QString("Failed to open / read JAR file of %1").arg(mod->name()); qCritical() << QString("Reason: ") << e.cause(); failed(e.what()); @@ -90,7 +90,7 @@ void ModrinthCheckUpdate::executeTask() // If the returned project is empty, but we have Modrinth metadata, // it means this specific version is not available if (project_obj.isEmpty()) { - qDebug() << "Mod " << mappings.find(hash).value().name() << " got an empty response."; + qDebug() << "Mod " << mappings.find(hash).value()->name() << " got an empty response."; qDebug() << "Hash: " << hash; emit checkFailed( @@ -134,24 +134,24 @@ void ModrinthCheckUpdate::executeTask() auto mod = *mod_iter; auto key = project_ver.hash; - if ((key != hash && project_ver.is_preferred) || (mod.status() == ModStatus::NotInstalled)) { - if (mod.version() == project_ver.version_number) + if ((key != hash && project_ver.is_preferred) || (mod->status() == ModStatus::NotInstalled)) { + if (mod->version() == project_ver.version_number) continue; // Fake pack with the necessary info to pass to the download task :) ModPlatform::IndexedPack pack; - pack.name = mod.name(); - pack.slug = mod.metadata()->slug; - pack.addonId = mod.metadata()->project_id; - pack.websiteUrl = mod.homeurl(); - for (auto& author : mod.authors()) + pack.name = mod->name(); + pack.slug = mod->metadata()->slug; + pack.addonId = mod->metadata()->project_id; + pack.websiteUrl = mod->homeurl(); + for (auto& author : mod->authors()) pack.authors.append({ author }); - pack.description = mod.description(); + pack.description = mod->description(); pack.provider = ModPlatform::Provider::MODRINTH; auto download_task = new ModDownloadTask(pack, project_ver, m_mods_folder); - m_updatable.emplace_back(mod.name(), hash, mod.version(), project_ver.version_number, project_ver.changelog, + m_updatable.emplace_back(pack.name, hash, mod->version(), project_ver.version_number, project_ver.changelog, ModPlatform::Provider::MODRINTH, download_task); } } diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h index 7e685a6d..d61667f5 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h @@ -8,7 +8,7 @@ class ModrinthCheckUpdate : public CheckUpdateTask { Q_OBJECT public: - ModrinthCheckUpdate(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) + ModrinthCheckUpdate(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) : CheckUpdateTask(mods, mcVersions, loaders, mods_folder) {} diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index 51e37bcf..3dc2a85f 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -34,7 +34,7 @@ static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst) ModUpdateDialog::ModUpdateDialog(QWidget* parent, BaseInstance* instance, const std::shared_ptr mods, - std::list& search_for) + std::list& search_for) : ReviewMessageBox(parent, tr("Confirm mods to update"), "") , m_parent(parent) , m_mod_model(mods) @@ -63,7 +63,7 @@ void ModUpdateDialog::checkCandidates() for (const auto& failed : m_failed_metadata) { const auto& mod = std::get<0>(failed); const auto& reason = std::get<1>(failed); - text += tr("Mod name: %1
File name: %2
Reason: %3

").arg(mod.name(), mod.fileinfo().fileName(), reason); + text += tr("Mod name: %1
File name: %2
Reason: %3

").arg(mod->name(), mod->fileinfo().fileName(), reason); } ScrollMessageBox message_dialog(m_parent, tr("Metadata generation failed"), @@ -86,14 +86,14 @@ void ModUpdateDialog::checkCandidates() if (!m_modrinth_to_update.empty()) { m_modrinth_check_task = new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_mod_model); connect(m_modrinth_check_task, &CheckUpdateTask::checkFailed, this, - [this](Mod mod, QString reason, QUrl recover_url) { m_failed_check_update.emplace_back(mod, reason, recover_url); }); + [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.emplace_back(mod, reason, recover_url); }); check_task.addTask(m_modrinth_check_task); } if (!m_flame_to_update.empty()) { m_flame_check_task = new FlameCheckUpdate(m_flame_to_update, versions, loaders, m_mod_model); connect(m_flame_check_task, &CheckUpdateTask::checkFailed, this, - [this](Mod mod, QString reason, QUrl recover_url) { m_failed_check_update.emplace_back(mod, reason, recover_url); }); + [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.emplace_back(mod, reason, recover_url); }); check_task.addTask(m_flame_check_task); } @@ -152,9 +152,9 @@ void ModUpdateDialog::checkCandidates() const auto& reason = std::get<1>(failed); const auto& recover_url = std::get<2>(failed); - qDebug() << mod.name() << " failed to check for updates!"; + qDebug() << mod->name() << " failed to check for updates!"; - text += tr("Mod name: %1").arg(mod.name()) + "
"; + text += tr("Mod name: %1").arg(mod->name()) + "
"; if (!reason.isEmpty()) text += tr("Reason: %1").arg(reason) + "
"; if (!recover_url.isEmpty()) @@ -205,15 +205,15 @@ auto ModUpdateDialog::ensureMetadata() -> bool // A better use of data structures here could remove the need for this QHash QHash should_try_others; - std::list modrinth_tmp; - std::list flame_tmp; + std::list modrinth_tmp; + std::list flame_tmp; bool confirm_rest = false; bool try_others_rest = false; bool skip_rest = false; ModPlatform::Provider provider_rest = ModPlatform::Provider::MODRINTH; - auto addToTmp = [&](Mod& m, ModPlatform::Provider p) { + auto addToTmp = [&](Mod* m, ModPlatform::Provider p) { switch (p) { case ModPlatform::Provider::MODRINTH: modrinth_tmp.push_back(m); @@ -224,9 +224,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool } }; - for (auto& candidate : m_candidates) { - if (candidate.status() != ModStatus::NoMetadata) { - onMetadataEnsured(candidate); + for (auto candidate : m_candidates) { + auto* candidate_ptr = candidate.get(); + if (candidate->status() != ModStatus::NoMetadata) { + onMetadataEnsured(candidate_ptr); continue; } @@ -234,8 +235,8 @@ auto ModUpdateDialog::ensureMetadata() -> bool continue; if (confirm_rest) { - addToTmp(candidate, provider_rest); - should_try_others.insert(candidate.internal_id(), try_others_rest); + addToTmp(candidate_ptr, provider_rest); + should_try_others.insert(candidate->internal_id(), try_others_rest); continue; } @@ -243,7 +244,7 @@ auto ModUpdateDialog::ensureMetadata() -> bool chooser.setDescription(tr("This mod (%1) does not have a metadata yet. We need to create one in order to keep relevant " "information on how to update this " "mod. To do this, please select a mod provider from which we can search for updates for %1.") - .arg(candidate.name())); + .arg(candidate->name())); auto confirmed = chooser.exec() == QDialog::DialogCode::Accepted; auto response = chooser.getResponse(); @@ -256,26 +257,26 @@ auto ModUpdateDialog::ensureMetadata() -> bool try_others_rest = response.try_others; } - should_try_others.insert(candidate.internal_id(), response.try_others); + should_try_others.insert(candidate->internal_id(), response.try_others); if (confirmed) - addToTmp(candidate, response.chosen); + addToTmp(candidate_ptr, response.chosen); } if (!modrinth_tmp.empty()) { auto* modrinth_task = new EnsureMetadataTask(modrinth_tmp, index_dir, ModPlatform::Provider::MODRINTH); - connect(modrinth_task, &EnsureMetadataTask::metadataReady, [this](Mod& candidate) { onMetadataEnsured(candidate); }); - connect(modrinth_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod& candidate) { - onMetadataFailed(candidate, should_try_others.find(candidate.internal_id()).value(), ModPlatform::Provider::MODRINTH); + connect(modrinth_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); + connect(modrinth_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { + onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::MODRINTH); }); seq.addTask(modrinth_task); } if (!flame_tmp.empty()) { auto* flame_task = new EnsureMetadataTask(flame_tmp, index_dir, ModPlatform::Provider::FLAME); - connect(flame_task, &EnsureMetadataTask::metadataReady, [this](Mod& candidate) { onMetadataEnsured(candidate); }); - connect(flame_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod& candidate) { - onMetadataFailed(candidate, should_try_others.find(candidate.internal_id()).value(), ModPlatform::Provider::FLAME); + connect(flame_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); + connect(flame_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) { + onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::FLAME); }); seq.addTask(flame_task); } @@ -290,13 +291,13 @@ auto ModUpdateDialog::ensureMetadata() -> bool return (ret_metadata != QDialog::DialogCode::Rejected); } -void ModUpdateDialog::onMetadataEnsured(Mod& mod) +void ModUpdateDialog::onMetadataEnsured(Mod* mod) { // When the mod is a folder, for instance - if (!mod.metadata()) + if (!mod->metadata()) return; - switch (mod.metadata()->provider) { + switch (mod->metadata()->provider) { case ModPlatform::Provider::MODRINTH: m_modrinth_to_update.push_back(mod); break; @@ -318,14 +319,14 @@ ModPlatform::Provider next(ModPlatform::Provider p) return ModPlatform::Provider::FLAME; } -void ModUpdateDialog::onMetadataFailed(Mod& mod, bool try_others, ModPlatform::Provider first_choice) +void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::Provider first_choice) { if (try_others) { auto index_dir = indexDir(); auto* task = new EnsureMetadataTask(mod, index_dir, next(first_choice)); - connect(task, &EnsureMetadataTask::metadataReady, [this](Mod& candidate) { onMetadataEnsured(candidate); }); - connect(task, &EnsureMetadataTask::metadataFailed, [this](Mod& candidate) { onMetadataFailed(candidate, false); }); + connect(task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); }); + connect(task, &EnsureMetadataTask::metadataFailed, [this](Mod* candidate) { onMetadataFailed(candidate, false); }); m_second_try_metadata->addTask(task); } else { diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h index 336fbba2..b598447d 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.h +++ b/launcher/ui/dialogs/ModUpdateDialog.h @@ -18,7 +18,7 @@ class ModUpdateDialog final : public ReviewMessageBox { explicit ModUpdateDialog(QWidget* parent, BaseInstance* instance, const std::shared_ptr mod_model, - std::list& search_for); + std::list& search_for); void checkCandidates(); @@ -34,8 +34,8 @@ class ModUpdateDialog final : public ReviewMessageBox { auto ensureMetadata() -> bool; private slots: - void onMetadataEnsured(Mod&); - void onMetadataFailed(Mod&, bool try_others = false, ModPlatform::Provider first_choice = ModPlatform::Provider::MODRINTH); + void onMetadataEnsured(Mod*); + void onMetadataFailed(Mod*, bool try_others = false, ModPlatform::Provider first_choice = ModPlatform::Provider::MODRINTH); private: QWidget* m_parent; @@ -45,13 +45,13 @@ class ModUpdateDialog final : public ReviewMessageBox { const std::shared_ptr m_mod_model; - std::list& m_candidates; - std::list m_modrinth_to_update; - std::list m_flame_to_update; + std::list& m_candidates; + std::list m_modrinth_to_update; + std::list m_flame_to_update; SequentialTask* m_second_try_metadata; - std::list> m_failed_metadata; - std::list> m_failed_check_update; + std::list> m_failed_metadata; + std::list> m_failed_check_update; QHash m_tasks; BaseInstance* m_instance; diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index a40cef77..c21cdda4 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -192,7 +192,7 @@ void ModFolderPage::updateMods() if (update_dialog.noUpdates()) { CustomMessageBox::selectable(this, tr("Update checker"), (mods_list.size() == 1) - ? tr("'%1' is up-to-date! :)").arg(mods_list.front().name()) + ? tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) : tr("All %1mods are up-to-date! :)").arg(use_all ? "" : (tr("selected") + " "))) ->exec(); return; From 001bbef9eea845ec7b970c73e16d0f129fade18b Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 28 Jun 2022 07:09:58 -0300 Subject: [PATCH 30/38] fix: don't use shared_ptr for a background task T_T Signed-off-by: flow --- launcher/modplatform/ModAPI.h | 4 ++-- launcher/modplatform/flame/FlameAPI.cpp | 4 ++-- launcher/modplatform/flame/FlameAPI.h | 2 +- launcher/modplatform/helpers/NetworkModAPI.cpp | 4 ++-- launcher/modplatform/helpers/NetworkModAPI.h | 2 +- launcher/modplatform/modrinth/ModrinthAPI.cpp | 4 ++-- launcher/modplatform/modrinth/ModrinthAPI.h | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h index 26fe9d02..4114d83c 100644 --- a/launcher/modplatform/ModAPI.h +++ b/launcher/modplatform/ModAPI.h @@ -75,8 +75,8 @@ class ModAPI { virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0; virtual void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) = 0; - virtual auto getProject(QString addonId, QByteArray* response) const -> NetJob::Ptr = 0; - virtual auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob::Ptr = 0; + virtual auto getProject(QString addonId, QByteArray* response) const -> NetJob* = 0; + virtual auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* = 0; struct VersionSearchArgs { diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index 065b4cc2..e40d84f7 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -124,7 +124,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe return ver; } -auto FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob::Ptr +auto FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* { auto* netJob = new NetJob(QString("Flame::GetProjects"), APPLICATION->network()); @@ -141,7 +141,7 @@ auto FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const -> netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw)); - QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); }); QObject::connect(netJob, &NetJob::failed, [body_raw] { qDebug() << body_raw; }); return netJob; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 1646ec79..ec6706e5 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -10,7 +10,7 @@ class FlameAPI : public NetworkModAPI { auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion; - auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob::Ptr override; + auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override; private: inline auto getSortFieldInt(QString sortString) const -> int diff --git a/launcher/modplatform/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp index e8cba12e..90edfe31 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.cpp +++ b/launcher/modplatform/helpers/NetworkModAPI.cpp @@ -36,7 +36,7 @@ void NetworkModAPI::getModInfo(CallerType* caller, ModPlatform::IndexedPack& pac auto response = new QByteArray(); auto job = getProject(pack.addonId.toString(), response); - QObject::connect(job.get(), &NetJob::succeeded, caller, [caller, &pack, response] { + QObject::connect(job, &NetJob::succeeded, caller, [caller, &pack, response] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { @@ -80,7 +80,7 @@ void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) co netJob->start(); } -auto NetworkModAPI::getProject(QString addonId, QByteArray* response) const -> NetJob::Ptr +auto NetworkModAPI::getProject(QString addonId, QByteArray* response) const -> NetJob* { auto netJob = new NetJob(QString("%1::GetProject").arg(addonId), APPLICATION->network()); auto searchUrl = getModInfoURL(addonId); diff --git a/launcher/modplatform/helpers/NetworkModAPI.h b/launcher/modplatform/helpers/NetworkModAPI.h index ea28c688..989bcec4 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.h +++ b/launcher/modplatform/helpers/NetworkModAPI.h @@ -8,7 +8,7 @@ class NetworkModAPI : public ModAPI { void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) override; void getVersions(CallerType* caller, VersionSearchArgs&& args) const override; - auto getProject(QString addonId, QByteArray* response) const -> NetJob::Ptr override; + auto getProject(QString addonId, QByteArray* response) const -> NetJob* override; protected: virtual auto getModSearchURL(SearchArgs& args) const -> QString = 0; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp index 301c0be8..747cf4c3 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.cpp +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -95,14 +95,14 @@ auto ModrinthAPI::latestVersions(const QStringList& hashes, return netJob; } -auto ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob::Ptr +auto ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* { auto netJob = new NetJob(QString("Modrinth::GetProjects"), APPLICATION->network()); auto searchUrl = getMultipleModInfoURL(addonIds); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); - QObject::connect(netJob, &NetJob::finished, [response] { delete response; }); + QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); }); return netJob; } diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 2909daf9..e1a18681 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -47,7 +47,7 @@ class ModrinthAPI : public NetworkModAPI { ModLoaderTypes loaders, QByteArray* response) -> NetJob::Ptr; - auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob::Ptr override; + auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override; public: inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; From 2b65ee433fa594173205ceed104d5261052cac9a Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 1 Jul 2022 09:42:15 -0300 Subject: [PATCH 31/38] fix: changelogs with too much space between lines Signed-off-by: flow --- launcher/ui/dialogs/ModUpdateDialog.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index 3dc2a85f..24fa229d 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -363,7 +363,13 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info) case ModPlatform::Provider::MODRINTH: { HoeDown h; // HoeDown bug?: \n aren't converted to
- changelog_area->setHtml(h.process(info.changelog.toUtf8()).replace('\n', "
")); + auto text = h.process(info.changelog.toUtf8()); + + // Don't convert if there's an HTML tag right after (Qt rendering weirdness) + text.remove(QRegularExpression("(\n+)(?=<)")); + text.replace('\n', "
"); + + changelog_area->setHtml(text); break; } case ModPlatform::Provider::FLAME: { From 79b0a16f7a74aa8184c41f0574865e1cf6db0519 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 1 Jul 2022 11:54:32 -0300 Subject: [PATCH 32/38] fix: try finding a good height for short changelogs Signed-off-by: flow --- launcher/ui/dialogs/ModUpdateDialog.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index 24fa229d..4f83eeb9 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -358,7 +358,6 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info) auto changelog = new QTreeWidgetItem(changelog_item); auto changelog_area = new QTextBrowser(); - switch (info.provider) { case ModPlatform::Provider::MODRINTH: { HoeDown h; @@ -379,6 +378,12 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info) } changelog_area->setOpenExternalLinks(true); + changelog_area->setLineWrapMode(QTextBrowser::LineWrapMode::NoWrap); + changelog_area->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); + + // HACK: Is there a better way of achieving this? + auto font_height = QFontMetrics(changelog_area->font()).height(); + changelog_area->setMaximumHeight((changelog_area->toPlainText().count(QRegularExpression("\n|
")) + 2) * font_height); ui->modTreeWidget->setItemWidget(changelog, 0, changelog_area); From 58dc3e93d375a36c3c8c82fbbb7dc6322a475869 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 1 Jul 2022 11:59:34 -0300 Subject: [PATCH 33/38] fix: clean up execWithTask in Progress Dialog This prevents weird problems, such as dialogs being non-modal when they should be! Signed-off-by: flow --- launcher/ui/dialogs/ModUpdateDialog.cpp | 2 -- launcher/ui/dialogs/ProgressDialog.cpp | 34 +++++++++++-------------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index 4f83eeb9..c280c8da 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -108,10 +108,8 @@ void ModUpdateDialog::checkCandidates() }); // Check for updates - // FIXME: SOMEHOW THIS IS NOT MODAL??????? ProgressDialog progress_dialog(m_parent); progress_dialog.setSkipButton(true, tr("Abort")); - progress_dialog.setVisible(true); progress_dialog.setWindowTitle(tr("Checking for updates...")); auto ret = progress_dialog.execWithTask(&check_task); diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp index e5226016..a79bc837 100644 --- a/launcher/ui/dialogs/ProgressDialog.cpp +++ b/launcher/ui/dialogs/ProgressDialog.cpp @@ -62,24 +62,24 @@ void ProgressDialog::updateSize() int ProgressDialog::execWithTask(Task* task) { this->task = task; - QDialog::DialogCode result; if (!task) { - qDebug() << "Programmer error: progress dialog created with null task."; - return Accepted; + qDebug() << "Programmer error: Progress dialog created with null task."; + return QDialog::DialogCode::Accepted; } + QDialog::DialogCode result; if (handleImmediateResult(result)) { return result; } // Connect signals. - connect(task, SIGNAL(started()), SLOT(onTaskStarted())); - connect(task, SIGNAL(failed(QString)), SLOT(onTaskFailed(QString))); - connect(task, SIGNAL(succeeded()), SLOT(onTaskSucceeded())); - connect(task, SIGNAL(status(QString)), SLOT(changeStatus(const QString&))); - connect(task, SIGNAL(stepStatus(QString)), SLOT(changeStatus(const QString&))); - connect(task, SIGNAL(progress(qint64, qint64)), SLOT(changeProgress(qint64, qint64))); + connect(task, &Task::started, this, &ProgressDialog::onTaskStarted); + connect(task, &Task::failed, this, &ProgressDialog::onTaskFailed); + connect(task, &Task::succeeded, this, &ProgressDialog::onTaskSucceeded); + connect(task, &Task::status, this, &ProgressDialog::changeStatus); + connect(task, &Task::stepStatus, this, &ProgressDialog::changeStatus); + connect(task, &Task::progress, this, &ProgressDialog::changeProgress); connect(task, &Task::aborted, [this] { onTaskFailed(tr("Aborted by user")); }); @@ -89,19 +89,15 @@ int ProgressDialog::execWithTask(Task* task) ui->globalProgressBar->setHidden(true); } - // if this didn't connect to an already running task, invoke start + // It's a good idea to start the task after we entered the dialog's event loop :^) if (!task->isRunning()) { - task->start(); - } - if (task->isRunning()) { - changeProgress(task->getProgress(), task->getTotalProgress()); - changeStatus(task->getStatus()); - return QDialog::exec(); - } else if (handleImmediateResult(result)) { - return result; + QMetaObject::invokeMethod(task, &Task::start, Qt::QueuedConnection); } else { - return QDialog::Rejected; + changeStatus(task->getStatus()); + changeProgress(task->getProgress(), task->getTotalProgress()); } + + return QDialog::exec(); } // TODO: only provide the unique_ptr overloads From 47bdcb6050cc5eb106929f27115c9dd13f93d154 Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 1 Jul 2022 16:17:05 -0300 Subject: [PATCH 34/38] feat: make second metadata pass concurrent Signed-off-by: flow --- launcher/ui/dialogs/ModUpdateDialog.cpp | 4 +++- launcher/ui/dialogs/ModUpdateDialog.h | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index c280c8da..c54d178b 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -8,6 +8,8 @@ #include "FileSystem.h" #include "Json.h" +#include "tasks/ConcurrentTask.h" + #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" @@ -39,7 +41,7 @@ ModUpdateDialog::ModUpdateDialog(QWidget* parent, , m_parent(parent) , m_mod_model(mods) , m_candidates(search_for) - , m_second_try_metadata(new SequentialTask()) + , m_second_try_metadata(new ConcurrentTask()) , m_instance(instance) { ReviewMessageBox::setGeometry(0, 0, 800, 600); diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h index b598447d..638f64bc 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.h +++ b/launcher/ui/dialogs/ModUpdateDialog.h @@ -11,6 +11,7 @@ class Mod; class ModrinthCheckUpdate; class FlameCheckUpdate; +class ConcurrentTask; class ModUpdateDialog final : public ReviewMessageBox { Q_OBJECT @@ -49,7 +50,7 @@ class ModUpdateDialog final : public ReviewMessageBox { std::list m_modrinth_to_update; std::list m_flame_to_update; - SequentialTask* m_second_try_metadata; + ConcurrentTask* m_second_try_metadata; std::list> m_failed_metadata; std::list> m_failed_check_update; From 650af5eb64d3c560044cf1e806159f1d64985aa6 Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 6 Jul 2022 13:21:06 -0300 Subject: [PATCH 35/38] change: use ModStatus as a simple member instead of a pointer Signed-off-by: flow --- launcher/minecraft/mod/Mod.cpp | 9 +++------ launcher/minecraft/mod/Mod.h | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 7227fc94..588d76e3 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -147,10 +147,7 @@ void Mod::setStatus(ModStatus status) if (m_localDetails) { m_localDetails->status = status; } else { - if (!m_temp_status.get()) - m_temp_status.reset(new ModStatus()); - - *m_temp_status = status; + m_temp_status = status; } } void Mod::setMetadata(const Metadata::ModStruct& metadata) @@ -222,7 +219,7 @@ auto Mod::authors() const -> QStringList auto Mod::status() const -> ModStatus { if (!m_localDetails) - return m_temp_status ? *m_temp_status : ModStatus::NoMetadata; + return m_temp_status; return details().status; } @@ -246,7 +243,7 @@ void Mod::finishResolvingWithDetails(std::shared_ptr details) m_resolved = true; m_localDetails = details; - setStatus(m_temp_status ? *m_temp_status : ModStatus::NoMetadata); + setStatus(m_temp_status); if (m_localDetails && m_temp_metadata && m_temp_metadata->isValid()) { setMetadata(*m_temp_metadata); diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 3d3becd7..7a13e44b 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -115,7 +115,7 @@ protected: std::shared_ptr m_temp_metadata; /* Set the mod status while it doesn't have local details just yet */ - std::shared_ptr m_temp_status; + ModStatus m_temp_status = ModStatus::NoMetadata; std::shared_ptr m_localDetails; From de9e304236ac0c11dd2b6bfb8b6f55943349c0e9 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 10 Jul 2022 15:15:25 -0300 Subject: [PATCH 36/38] fix: std::list -> QList Qt6 removed Qlist::toStdList() :sob: Signed-off-by: flow --- launcher/minecraft/mod/ModFolderModel.cpp | 4 ++-- launcher/minecraft/mod/ModFolderModel.h | 2 +- launcher/modplatform/CheckUpdateTask.h | 4 ++-- launcher/modplatform/EnsureMetadataTask.cpp | 4 ++-- launcher/modplatform/EnsureMetadataTask.h | 2 +- launcher/modplatform/flame/FlameAPI.cpp | 2 +- launcher/modplatform/flame/FlameAPI.h | 2 +- launcher/modplatform/flame/FlameCheckUpdate.h | 2 +- .../modplatform/modrinth/ModrinthCheckUpdate.h | 2 +- launcher/ui/dialogs/ModUpdateDialog.cpp | 16 ++++++++-------- launcher/ui/dialogs/ModUpdateDialog.h | 14 +++++++------- launcher/ui/pages/instance/ModFolderPage.cpp | 2 +- 12 files changed, 28 insertions(+), 28 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index e0391c01..c4449b2a 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -271,9 +271,9 @@ bool ModFolderModel::isValid() return m_dir.exists() && m_dir.isReadable(); } -auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> std::list +auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList { - std::list selected_mods; + QList selected_mods; for (auto i : indexes) { if(i.column() != 0) continue; diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 04681879..a7d3ece0 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -146,7 +146,7 @@ public: return mods; } - auto selectedMods(QModelIndexList& indexes) -> std::list; + auto selectedMods(QModelIndexList& indexes) -> QList; public slots: void disableInteraction(bool disabled); diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h index 94ee19b3..91922034 100644 --- a/launcher/modplatform/CheckUpdateTask.h +++ b/launcher/modplatform/CheckUpdateTask.h @@ -12,7 +12,7 @@ class CheckUpdateTask : public Task { Q_OBJECT public: - CheckUpdateTask(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) + CheckUpdateTask(QList& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) : Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder) {}; struct UpdatableMod { @@ -42,7 +42,7 @@ class CheckUpdateTask : public Task { void checkFailed(Mod* failed, QString reason, QUrl recover_url = {}); protected: - std::list& m_mods; + QList& m_mods; std::list& m_game_versions; ModAPI::ModLoaderTypes m_loaders; std::shared_ptr m_mods_folder; diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index 7c153511..f0c1fa99 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -28,7 +28,7 @@ EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider m_mods.insert(hash, mod); } -EnsureMetadataTask::EnsureMetadataTask(std::list& mods, QDir dir, ModPlatform::Provider prov) +EnsureMetadataTask::EnsureMetadataTask(QList& mods, QDir dir, ModPlatform::Provider prov) : Task(nullptr), m_index_dir(dir), m_provider(prov) { for (auto* mod : mods) { @@ -323,7 +323,7 @@ NetJob::Ptr EnsureMetadataTask::flameVersionsTask() { auto* response = new QByteArray(); - std::list fingerprints; + QList fingerprints; for (auto& murmur : m_mods.keys()) { fingerprints.push_back(murmur.toUInt()); } diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h index d40a972e..79db6976 100644 --- a/launcher/modplatform/EnsureMetadataTask.h +++ b/launcher/modplatform/EnsureMetadataTask.h @@ -13,7 +13,7 @@ class EnsureMetadataTask : public Task { public: EnsureMetadataTask(Mod*, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH); - EnsureMetadataTask(std::list&, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH); + EnsureMetadataTask(QList&, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH); ~EnsureMetadataTask() = default; diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index e40d84f7..0ff04f72 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -7,7 +7,7 @@ #include "net/Upload.h" -auto FlameAPI::matchFingerprints(const std::list& fingerprints, QByteArray* response) -> NetJob::Ptr +auto FlameAPI::matchFingerprints(const QList& fingerprints, QByteArray* response) -> NetJob::Ptr { auto* netJob = new NetJob(QString("Flame::MatchFingerprints"), APPLICATION->network()); diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index ec6706e5..336df387 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -5,7 +5,7 @@ class FlameAPI : public NetworkModAPI { public: - auto matchFingerprints(const std::list& fingerprints, QByteArray* response) -> NetJob::Ptr; + auto matchFingerprints(const QList& fingerprints, QByteArray* response) -> NetJob::Ptr; auto getModFileChangelog(int modId, int fileId) -> QString; auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion; diff --git a/launcher/modplatform/flame/FlameCheckUpdate.h b/launcher/modplatform/flame/FlameCheckUpdate.h index 0891891d..163c706c 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.h +++ b/launcher/modplatform/flame/FlameCheckUpdate.h @@ -8,7 +8,7 @@ class FlameCheckUpdate : public CheckUpdateTask { Q_OBJECT public: - FlameCheckUpdate(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) + FlameCheckUpdate(QList& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) : CheckUpdateTask(mods, mcVersions, loaders, mods_folder) {} diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h index d61667f5..abf8ada1 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h @@ -8,7 +8,7 @@ class ModrinthCheckUpdate : public CheckUpdateTask { Q_OBJECT public: - ModrinthCheckUpdate(std::list& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) + ModrinthCheckUpdate(QList& mods, std::list& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr mods_folder) : CheckUpdateTask(mods, mcVersions, loaders, mods_folder) {} diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index c54d178b..b6e76ff1 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -36,7 +36,7 @@ static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst) ModUpdateDialog::ModUpdateDialog(QWidget* parent, BaseInstance* instance, const std::shared_ptr mods, - std::list& search_for) + QList& search_for) : ReviewMessageBox(parent, tr("Confirm mods to update"), "") , m_parent(parent) , m_mod_model(mods) @@ -88,14 +88,14 @@ void ModUpdateDialog::checkCandidates() if (!m_modrinth_to_update.empty()) { m_modrinth_check_task = new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_mod_model); connect(m_modrinth_check_task, &CheckUpdateTask::checkFailed, this, - [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.emplace_back(mod, reason, recover_url); }); + [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({mod, reason, recover_url}); }); check_task.addTask(m_modrinth_check_task); } if (!m_flame_to_update.empty()) { m_flame_check_task = new FlameCheckUpdate(m_flame_to_update, versions, loaders, m_mod_model); connect(m_flame_check_task, &CheckUpdateTask::checkFailed, this, - [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.emplace_back(mod, reason, recover_url); }); + [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({mod, reason, recover_url}); }); check_task.addTask(m_flame_check_task); } @@ -205,8 +205,8 @@ auto ModUpdateDialog::ensureMetadata() -> bool // A better use of data structures here could remove the need for this QHash QHash should_try_others; - std::list modrinth_tmp; - std::list flame_tmp; + QList modrinth_tmp; + QList flame_tmp; bool confirm_rest = false; bool try_others_rest = false; @@ -332,7 +332,7 @@ void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::P } else { QString reason{ tr("Didn't find a valid version on the selected mod provider(s)") }; - m_failed_metadata.emplace_back(mod, reason); + m_failed_metadata.append({mod, reason}); } } @@ -390,9 +390,9 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info) ui->modTreeWidget->addTopLevelItem(item_top); } -auto ModUpdateDialog::getTasks() -> const std::list +auto ModUpdateDialog::getTasks() -> const QList { - std::list list; + QList list; auto* item = ui->modTreeWidget->topLevelItem(0); diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h index 638f64bc..76aaab36 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.h +++ b/launcher/ui/dialogs/ModUpdateDialog.h @@ -19,13 +19,13 @@ class ModUpdateDialog final : public ReviewMessageBox { explicit ModUpdateDialog(QWidget* parent, BaseInstance* instance, const std::shared_ptr mod_model, - std::list& search_for); + QList& search_for); void checkCandidates(); void appendMod(const CheckUpdateTask::UpdatableMod& info); - const std::list getTasks(); + const QList getTasks(); auto indexDir() const -> QDir { return m_mod_model->indexDir(); } auto noUpdates() const -> bool { return m_no_updates; }; @@ -46,13 +46,13 @@ class ModUpdateDialog final : public ReviewMessageBox { const std::shared_ptr m_mod_model; - std::list& m_candidates; - std::list m_modrinth_to_update; - std::list m_flame_to_update; + QList& m_candidates; + QList m_modrinth_to_update; + QList m_flame_to_update; ConcurrentTask* m_second_try_metadata; - std::list> m_failed_metadata; - std::list> m_failed_check_update; + QList> m_failed_metadata; + QList> m_failed_check_update; QHash m_tasks; BaseInstance* m_instance; diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index c21cdda4..b190e51a 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -180,7 +180,7 @@ void ModFolderPage::updateMods() auto mods_list = m_model->selectedMods(selection); bool use_all = mods_list.empty(); if (use_all) - mods_list = m_model->allMods().toStdList(); + mods_list = m_model->allMods(); ModUpdateDialog update_dialog(this, m_instance, m_model, mods_list); update_dialog.checkCandidates(); From 6bb8332b4b8efbb3f21f31d465fa907c3db0a7ce Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 17 Jul 2022 11:43:12 -0300 Subject: [PATCH 37/38] fix: bogus returns in EnsureMetadataTask Signed-off-by: flow --- launcher/modplatform/EnsureMetadataTask.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index f0c1fa99..60c54c4e 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -96,20 +96,22 @@ void EnsureMetadataTask::executeTask() setStatus(tr("Checking if mods have metadata...")); for (auto* mod : m_mods) { - if (!mod->valid()) + if (!mod->valid()) { + qDebug() << "Mod" << mod->name() << "is invalid!"; + emitFail(mod); continue; + } // They already have the right metadata :o if (mod->status() != ModStatus::NoMetadata && mod->metadata() && mod->metadata()->provider == m_provider) { qDebug() << "Mod" << mod->name() << "already has metadata!"; emitReady(mod); - return; + continue; } // Folders don't have metadata if (mod->type() == Mod::MOD_FOLDER) { emitReady(mod); - return; } } From 54b335711acac5e57e94bc9cb81c751c9b2872c5 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 17 Jul 2022 11:56:23 -0300 Subject: [PATCH 38/38] fix: raw-pointers and leaks in ModFolderLoadTask Co-authored-by: timoreo Signed-off-by: flow --- launcher/minecraft/mod/ModFolderModel.cpp | 2 +- launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp | 3 +-- launcher/minecraft/mod/tasks/ModFolderLoadTask.h | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index c4449b2a..112d219e 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -143,7 +143,7 @@ void ModFolderModel::finishUpdate() QSet kept = currentSet; kept.intersect(newSet); for(auto& keptMod : kept) { - auto* newMod = newMods[keptMod]; + auto newMod = newMods[keptMod]; auto row = modsIndex[keptMod]; auto currentMod = mods[row]; if(newMod->dateTimeChanged() == currentMod->dateTimeChanged()) { diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index 63a6ca90..a2e055ba 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -36,7 +36,6 @@ #include "ModFolderLoadTask.h" -#include "Application.h" #include "minecraft/mod/MetadataHandler.h" ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed) @@ -53,7 +52,7 @@ void ModFolderLoadTask::run() // Read JAR files that don't have metadata m_mods_dir.refresh(); for (auto entry : m_mods_dir.entryInfoList()) { - auto* mod = new Mod(entry); + Mod::Ptr mod(new Mod(entry)); if (mod->enabled()) { if (m_result->mods.contains(mod->internal_id())) { diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h index 7568fdf5..0b6bb6cc 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h @@ -48,7 +48,7 @@ class ModFolderLoadTask : public QObject, public QRunnable Q_OBJECT public: struct Result { - QMap mods; + QMap mods; }; using ResultPtr = std::shared_ptr; ResultPtr result() const {