From 16cf56b7a49e1affbad3aa2fc2d60ff690823a79 Mon Sep 17 00:00:00 2001 From: arthomnix Date: Sat, 4 Feb 2023 21:41:24 +0000 Subject: [PATCH] GH-4699 Modrinth pack exporter (WIP) --- launcher/CMakeLists.txt | 8 + launcher/ModrinthInstanceExportTask.cpp | 221 ++++++++++++++ launcher/ModrinthInstanceExportTask.h | 69 +++++ launcher/ui/MainWindow.cpp | 4 +- launcher/ui/dialogs/ModrinthExportDialog.cpp | 114 +++++++ launcher/ui/dialogs/ModrinthExportDialog.h | 37 +++ launcher/ui/dialogs/ModrinthExportDialog.ui | 277 ++++++++++++++++++ .../SelectInstanceExportFormatDialog.cpp | 37 +++ .../SelectInstanceExportFormatDialog.h | 36 +++ .../SelectInstanceExportFormatDialog.ui | 95 ++++++ 10 files changed, 896 insertions(+), 2 deletions(-) create mode 100644 launcher/ModrinthInstanceExportTask.cpp create mode 100644 launcher/ModrinthInstanceExportTask.h create mode 100644 launcher/ui/dialogs/ModrinthExportDialog.cpp create mode 100644 launcher/ui/dialogs/ModrinthExportDialog.h create mode 100644 launcher/ui/dialogs/ModrinthExportDialog.ui create mode 100644 launcher/ui/dialogs/SelectInstanceExportFormatDialog.cpp create mode 100644 launcher/ui/dialogs/SelectInstanceExportFormatDialog.h create mode 100644 launcher/ui/dialogs/SelectInstanceExportFormatDialog.ui diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 85f669eb..be12d08c 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -36,6 +36,8 @@ set(CORE_SOURCES InstanceCopyTask.cpp InstanceImportTask.h InstanceImportTask.cpp + ModrinthInstanceExportTask.h + ModrinthInstanceExportTask.cpp # Use tracking separate from memory management Usable.h @@ -784,6 +786,10 @@ SET(LAUNCHER_SOURCES ui/dialogs/SkinUploadDialog.h ui/dialogs/CreateShortcutDialog.cpp ui/dialogs/CreateShortcutDialog.h + ui/dialogs/SelectInstanceExportFormatDialog.cpp + ui/dialogs/SelectInstanceExportFormatDialog.h + ui/dialogs/ModrinthExportDialog.cpp + ui/dialogs/ModrinthExportDialog.h # GUI - widgets ui/widgets/Common.cpp @@ -882,6 +888,8 @@ qt5_wrap_ui(LAUNCHER_UI ui/dialogs/LoginDialog.ui ui/dialogs/EditAccountDialog.ui ui/dialogs/CreateShortcutDialog.ui + ui/dialogs/SelectInstanceExportFormatDialog.ui + ui/dialogs/ModrinthExportDialog.ui ) qt5_add_resources(LAUNCHER_RESOURCES diff --git a/launcher/ModrinthInstanceExportTask.cpp b/launcher/ModrinthInstanceExportTask.cpp new file mode 100644 index 00000000..943294c6 --- /dev/null +++ b/launcher/ModrinthInstanceExportTask.cpp @@ -0,0 +1,221 @@ +/* + * Copyright 2023 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include +#include +#include +#include +#include "Json.h" +#include "ModrinthInstanceExportTask.h" +#include "net/NetJob.h" +#include "Application.h" +#include "ui/dialogs/ModrinthExportDialog.h" +#include "JlCompress.h" +#include "FileSystem.h" + +ModrinthInstanceExportTask::ModrinthInstanceExportTask(InstancePtr instance, ModrinthExportSettings settings) : m_instance(instance), m_settings(settings) {} + +void ModrinthInstanceExportTask::executeTask() +{ + QDir modsDir(m_instance->gameRoot() + "/mods"); + modsDir.setFilter(QDir::Files); + modsDir.setNameFilters(QStringList() << "*.jar"); + + QDir resourcePacksDir(m_instance->gameRoot() + "/resourcepacks"); + resourcePacksDir.setFilter(QDir::Files); + resourcePacksDir.setNameFilters(QStringList() << "*.zip"); + + QDir shaderPacksDir(m_instance->gameRoot() + "/shaderpacks"); + shaderPacksDir.setFilter(QDir::Files); + shaderPacksDir.setNameFilters(QStringList() << "*.zip"); + + QStringList filesToResolve; + + if (modsDir.exists()) { + QDirIterator modsIterator(modsDir); + while (modsIterator.hasNext()) { + filesToResolve << modsIterator.next(); + } + } + + if (m_settings.includeResourcePacks && resourcePacksDir.exists()) { + QDirIterator resourcePacksIterator(resourcePacksDir); + while (resourcePacksIterator.hasNext()) { + filesToResolve << resourcePacksIterator.next(); + } + } + + if (m_settings.includeShaderPacks && shaderPacksDir.exists()) { + QDirIterator shaderPacksIterator(shaderPacksDir); + while (shaderPacksIterator.hasNext()) { + filesToResolve << shaderPacksIterator.next(); + } + } + + m_netJob = new NetJob(tr("Modrinth pack export"), APPLICATION->network()); + + for (QString filePath: filesToResolve) { + QFile file(filePath); + + if (file.open(QFile::ReadOnly)) { + QByteArray contents = file.readAll(); + QCryptographicHash hasher(QCryptographicHash::Sha512); + hasher.addData(contents); + QString hash = hasher.result().toHex(); + + m_responses.append(ModrinthLookupData { + QFileInfo(file), + QByteArray() + }); + + m_netJob->addNetAction(Net::Download::makeByteArray( + QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha512").arg(hash), + &m_responses.last().response + )); + } + } + + connect(m_netJob.get(), &NetJob::succeeded, this, &ModrinthInstanceExportTask::lookupSucceeded); + connect(m_netJob.get(), &NetJob::failed, this, &ModrinthInstanceExportTask::lookupFailed); + connect(m_netJob.get(), &NetJob::progress, this, &ModrinthInstanceExportTask::lookupProgress); + + m_netJob->start(); +} + +void ModrinthInstanceExportTask::lookupSucceeded() +{ + QList resolvedFiles; + QFileInfoList failedFiles; + + for (const auto &data : m_responses) { + try { + auto document = Json::requireDocument(data.response); + auto object = Json::requireObject(document); + auto file = Json::requireIsArrayOf(object, "files").first(); + auto url = Json::requireString(file, "url"); + auto hashes = Json::requireObject(file, "hashes"); + + QString sha512Hash = Json::requireString(hashes, "sha512"); + QString sha1Hash = Json::requireString(hashes, "sha1"); + + ModrinthFile fileData; + + QDir gameDir(m_instance->gameRoot()); + + fileData.path = gameDir.relativeFilePath(data.fileInfo.absoluteFilePath()); + fileData.download = url; + fileData.sha512 = sha512Hash; + fileData.sha1 = sha1Hash; + fileData.fileSize = data.fileInfo.size(); + + resolvedFiles << fileData; + } catch (const Json::JsonException &e) { + qDebug() << "File " << data.fileInfo.path() << " failed to process for reason " << e.cause() << ", adding to overrides"; + failedFiles << data.fileInfo; + } + } + + qDebug() << "Failed files: " << failedFiles; + + QJsonObject indexJson; + indexJson.insert("formatVersion", QJsonValue(1)); + indexJson.insert("game", QJsonValue("minecraft")); + indexJson.insert("versionId", QJsonValue(m_settings.version)); + indexJson.insert("name", QJsonValue(m_settings.name)); + + if (!m_settings.description.isEmpty()) { + indexJson.insert("summary", QJsonValue(m_settings.description)); + } + + QJsonArray files; + + for (const auto &file : resolvedFiles) { + QJsonObject fileObj; + fileObj.insert("path", file.path); + + QJsonObject hashes; + hashes.insert("sha512", file.sha512); + hashes.insert("sha1", file.sha1); + fileObj.insert("hashes", hashes); + + QJsonArray downloads; + downloads.append(file.download); + fileObj.insert("downloads", downloads); + + fileObj.insert("fileSize", QJsonValue(file.fileSize)); + + files.append(fileObj); + } + + indexJson.insert("files", files); + + QJsonObject dependencies; + dependencies.insert("minecraft", m_settings.gameVersion); + if (!m_settings.forgeVersion.isEmpty()) { + dependencies.insert("forge", m_settings.forgeVersion); + } + if (!m_settings.fabricVersion.isEmpty()) { + dependencies.insert("fabric-loader", m_settings.fabricVersion); + } + if (!m_settings.quiltVersion.isEmpty()) { + dependencies.insert("quilt-loader", m_settings.quiltVersion); + } + + indexJson.insert("dependencies", dependencies); + + QTemporaryDir tmp; + if (tmp.isValid()) { + Json::write(indexJson, tmp.filePath("modrinth.index.json")); + + if (!failedFiles.isEmpty()) { + QDir tmpDir(tmp.path()); + QDir gameDir(m_instance->gameRoot()); + for (const auto &file : failedFiles) { + QString src = file.absoluteFilePath(); + tmpDir.mkpath("overrides/" + gameDir.relativeFilePath(file.absolutePath())); + QString dest = tmpDir.path() + "/overrides/" + gameDir.relativeFilePath(src); + qDebug() << dest; + if (!QFile::copy(file.absoluteFilePath(), dest)) { + emitFailed(tr("Failed to copy file %1 to overrides").arg(src)); + return; + } + } + + if (m_settings.includeGameConfig) { + tmpDir.mkdir("overrides"); + QFile::copy(gameDir.absoluteFilePath("options.txt"), tmpDir.absoluteFilePath("overrides/options.txt")); + } + + if (m_settings.includeModConfigs) { + tmpDir.mkdir("overrides"); + FS::copy copy(m_instance->gameRoot() + "/config", tmpDir.absoluteFilePath("overrides/config")); + copy(); + } + } + + if (!JlCompress::compressDir(m_settings.exportPath, tmp.path())) { + emitFailed(tr("Failed to create zip file")); + return; + } + } else { + emitFailed(tr("Failed to create temporary directory")); + return; + } + + emitSucceeded(); +} + +void ModrinthInstanceExportTask::lookupFailed(const QString &) +{ + lookupSucceeded(); // the NetJob will fail if some files were not found on Modrinth, we still want to continue in that case + // FIXME: the NetJob will retry each download 3 times if it fails, we should probably stop it from doing that +} + +void ModrinthInstanceExportTask::lookupProgress(qint64 current, qint64 total) +{ + setProgress(current, total); +} \ No newline at end of file diff --git a/launcher/ModrinthInstanceExportTask.h b/launcher/ModrinthInstanceExportTask.h new file mode 100644 index 00000000..8749ded4 --- /dev/null +++ b/launcher/ModrinthInstanceExportTask.h @@ -0,0 +1,69 @@ +/* + * Copyright 2023 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include "tasks/Task.h" +#include "BaseInstance.h" +#include "net/NetJob.h" +#include "ui/dialogs/ModrinthExportDialog.h" + +struct ModrinthExportSettings { + QString version; + QString name; + QString description; + + bool includeGameConfig; + bool includeModConfigs; + bool includeResourcePacks; + bool includeShaderPacks; + + QString gameVersion; + QString forgeVersion; + QString fabricVersion; + QString quiltVersion; + + QString exportPath; +}; + +struct ModrinthLookupData { + QFileInfo fileInfo; + QByteArray response; +}; + +// Using the existing Modrinth::File struct from the importer doesn't actually make much sense here (doesn't support multiple hashes, hash is a byte array rather than a string, no file size, etc) +struct ModrinthFile +{ + QString path; + QString sha512; + QString sha1; + QString download; + qint64 fileSize; +}; + +class ModrinthInstanceExportTask : public Task +{ +Q_OBJECT + +public: + explicit ModrinthInstanceExportTask(InstancePtr instance, ModrinthExportSettings settings); + +protected: + //! Entry point for tasks. + virtual void executeTask() override; + +private slots: + void lookupSucceeded(); + void lookupFailed(const QString &); + void lookupProgress(qint64 current, qint64 total); + +private: + InstancePtr m_instance; + ModrinthExportSettings m_settings; + QList m_responses; + NetJob::Ptr m_netJob; +}; \ No newline at end of file diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 44cb504b..9dcbabea 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -84,7 +84,7 @@ #include "ui/dialogs/EditAccountDialog.h" #include "ui/dialogs/NotificationDialog.h" #include "ui/dialogs/CreateShortcutDialog.h" -#include "ui/dialogs/ExportInstanceDialog.h" +#include "ui/dialogs/SelectInstanceExportFormatDialog.h" #include "UpdateController.h" #include "KonamiCode.h" @@ -1756,7 +1756,7 @@ void MainWindow::on_actionExportInstance_triggered() { if (m_selectedInstance) { - ExportInstanceDialog dlg(m_selectedInstance, this); + SelectInstanceExportFormatDialog dlg(m_selectedInstance, this); dlg.exec(); } } diff --git a/launcher/ui/dialogs/ModrinthExportDialog.cpp b/launcher/ui/dialogs/ModrinthExportDialog.cpp new file mode 100644 index 00000000..decde9ec --- /dev/null +++ b/launcher/ui/dialogs/ModrinthExportDialog.cpp @@ -0,0 +1,114 @@ +/* + * Copyright 2023 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include +#include +#include +#include +#include "ModrinthExportDialog.h" +#include "ui_ModrinthExportDialog.h" +#include "BaseInstance.h" +#include "ModrinthInstanceExportTask.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "ProgressDialog.h" +#include "CustomMessageBox.h" + + +ModrinthExportDialog::ModrinthExportDialog(InstancePtr instance, QWidget *parent) : + QDialog(parent), ui(new Ui::ModrinthExportDialog), m_instance(instance) +{ + ui->setupUi(this); + ui->name->setText(m_instance->name()); + ui->version->setText("1.0"); +} + +void ModrinthExportDialog::updateDialogState() +{ + ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->setEnabled( + !ui->name->text().isEmpty() + && !ui->version->text().isEmpty() + && !ui->file->text().isEmpty() + ); +} + +void ModrinthExportDialog::on_fileBrowseButton_clicked() +{ + QFileDialog dialog(this, tr("Select modpack file"), QStandardPaths::writableLocation(QStandardPaths::HomeLocation)); + dialog.setDefaultSuffix("mrpack"); + dialog.setAcceptMode(QFileDialog::AcceptSave); + dialog.setFileMode(QFileDialog::AnyFile); + dialog.selectFile(ui->name->text() + ".mrpack"); + + if (dialog.exec()) { + ui->file->setText(dialog.selectedFiles().at(0)); + } + + updateDialogState(); +} + +void ModrinthExportDialog::accept() +{ + ModrinthExportSettings settings; + + settings.name = ui->name->text(); + settings.version = ui->version->text(); + settings.description = ui->description->text(); + + settings.includeGameConfig = ui->includeGameConfig->isChecked(); + settings.includeModConfigs = ui->includeModConfigs->isChecked(); + settings.includeResourcePacks = ui->includeResourcePacks->isChecked(); + settings.includeShaderPacks = ui->includeShaderPacks->isChecked(); + + MinecraftInstancePtr minecraftInstance = std::dynamic_pointer_cast(m_instance); + minecraftInstance->getPackProfile()->reload(Net::Mode::Offline); + + auto minecraftComponent = minecraftInstance->getPackProfile()->getComponent("net.minecraft"); + auto forgeComponent = minecraftInstance->getPackProfile()->getComponent("net.minecraftforge"); + auto fabricComponent = minecraftInstance->getPackProfile()->getComponent("net.fabricmc.fabric-loader"); + auto quiltComponent = minecraftInstance->getPackProfile()->getComponent("org.quiltmc.quilt-loader"); + + if (minecraftComponent) { + settings.gameVersion = minecraftComponent->getVersion(); + } + if (forgeComponent) { + settings.forgeVersion = forgeComponent->getVersion(); + } + if (fabricComponent) { + settings.fabricVersion = fabricComponent->getVersion(); + } + if (quiltComponent) { + settings.quiltVersion = quiltComponent->getVersion(); + } + + settings.exportPath = ui->file->text(); + + auto *task = new ModrinthInstanceExportTask(m_instance, settings); + + connect(task, &Task::failed, [this](QString reason) + { + CustomMessageBox::selectable(parentWidget(), tr("Error"), reason, QMessageBox::Critical)->show(); + }); + connect(task, &Task::succeeded, [this, task]() + { + QStringList warnings = task->warnings(); + if(warnings.count()) + { + CustomMessageBox::selectable(parentWidget(), tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } + }); + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(task); + + QDialog::accept(); +} + +ModrinthExportDialog::~ModrinthExportDialog() +{ + delete ui; +} diff --git a/launcher/ui/dialogs/ModrinthExportDialog.h b/launcher/ui/dialogs/ModrinthExportDialog.h new file mode 100644 index 00000000..3ebc9436 --- /dev/null +++ b/launcher/ui/dialogs/ModrinthExportDialog.h @@ -0,0 +1,37 @@ +/* + * Copyright 2023 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include +#include "ExportInstanceDialog.h" + +QT_BEGIN_NAMESPACE +namespace Ui +{ + class ModrinthExportDialog; +} +QT_END_NAMESPACE + +class ModrinthExportDialog : public QDialog +{ +Q_OBJECT + +public: + explicit ModrinthExportDialog(InstancePtr instance, QWidget *parent = nullptr); + + ~ModrinthExportDialog() override; + +private slots: + void on_fileBrowseButton_clicked(); + void accept() override; + void updateDialogState(); + +private: + Ui::ModrinthExportDialog *ui; + InstancePtr m_instance; +}; \ No newline at end of file diff --git a/launcher/ui/dialogs/ModrinthExportDialog.ui b/launcher/ui/dialogs/ModrinthExportDialog.ui new file mode 100644 index 00000000..7aa18bfc --- /dev/null +++ b/launcher/ui/dialogs/ModrinthExportDialog.ui @@ -0,0 +1,277 @@ + + + ModrinthExportDialog + + + + 0 + 0 + 679 + 559 + + + + + 0 + 0 + + + + ModrinthExportDialog + + + + + 10 + 10 + 661 + 541 + + + + + + + + 16777215 + 25 + + + + Export Modrinth modpack + + + + + + + + 16777215 + 200 + + + + Metadata + + + + + 10 + 30 + 641 + 151 + + + + + + + + + Name + + + + + + + + + + Version + + + + + + + Description + + + + + + + + + + + + + + + + + + + Export Options + + + + + 9 + 29 + 641 + 221 + + + + + + + QLayout::SetFixedSize + + + + + File + + + + + + + + + + Browse... + + + + + + + + + Include Minecraft config + + + true + + + + + + + Include mod configs + + + true + + + + + + + Include resource packs + + + + + + + Include shader packs + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + buttonBox + accepted() + ModrinthExportDialog + accept() + + + 340 + 532 + + + 338 + 279 + + + + + buttonBox + rejected() + ModrinthExportDialog + reject() + + + 340 + 532 + + + 338 + 279 + + + + + name + textChanged(QString) + ModrinthExportDialog + updateDialogState() + + + 395 + 90 + + + 339 + 279 + + + + + version + textChanged(QString) + ModrinthExportDialog + updateDialogState() + + + 395 + 129 + + + 339 + 279 + + + + + file + textChanged(QString) + ModrinthExportDialog + updateDialogState() + + + 309 + 329 + + + 339 + 279 + + + + + + updateDialogState() + + diff --git a/launcher/ui/dialogs/SelectInstanceExportFormatDialog.cpp b/launcher/ui/dialogs/SelectInstanceExportFormatDialog.cpp new file mode 100644 index 00000000..2a3c6f4b --- /dev/null +++ b/launcher/ui/dialogs/SelectInstanceExportFormatDialog.cpp @@ -0,0 +1,37 @@ +/* + * Copyright 2023 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include "SelectInstanceExportFormatDialog.h" +#include "ui_SelectInstanceExportFormatDialog.h" +#include "BuildConfig.h" +#include "ModrinthExportDialog.h" + + +SelectInstanceExportFormatDialog::SelectInstanceExportFormatDialog(InstancePtr instance, QWidget *parent) : + QDialog(parent), ui(new Ui::SelectInstanceExportFormatDialog), m_instance(instance) +{ + ui->setupUi(this); + ui->mmcFormat->setText(BuildConfig.LAUNCHER_NAME); +} + +void SelectInstanceExportFormatDialog::accept() +{ + if (ui->mmcFormat->isChecked()) { + ExportInstanceDialog dlg(m_instance, parentWidget()); + QDialog::accept(); + dlg.exec(); + } else if (ui->modrinthFormat->isChecked()) { + ModrinthExportDialog dlg(m_instance, parentWidget()); + QDialog::accept(); + dlg.exec(); + } +} + +SelectInstanceExportFormatDialog::~SelectInstanceExportFormatDialog() +{ + delete ui; +} diff --git a/launcher/ui/dialogs/SelectInstanceExportFormatDialog.h b/launcher/ui/dialogs/SelectInstanceExportFormatDialog.h new file mode 100644 index 00000000..2c286fad --- /dev/null +++ b/launcher/ui/dialogs/SelectInstanceExportFormatDialog.h @@ -0,0 +1,36 @@ +/* + * Copyright 2023 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include +#include "ExportInstanceDialog.h" + + +QT_BEGIN_NAMESPACE +namespace Ui +{ + class SelectInstanceExportFormatDialog; +} +QT_END_NAMESPACE + +class SelectInstanceExportFormatDialog : public QDialog +{ +Q_OBJECT + +public: + explicit SelectInstanceExportFormatDialog(InstancePtr instance, QWidget *parent = nullptr); + + ~SelectInstanceExportFormatDialog() override; + +private slots: + void accept() override; + +private: + Ui::SelectInstanceExportFormatDialog *ui; + InstancePtr m_instance; +}; \ No newline at end of file diff --git a/launcher/ui/dialogs/SelectInstanceExportFormatDialog.ui b/launcher/ui/dialogs/SelectInstanceExportFormatDialog.ui new file mode 100644 index 00000000..5b779a4e --- /dev/null +++ b/launcher/ui/dialogs/SelectInstanceExportFormatDialog.ui @@ -0,0 +1,95 @@ + + + SelectInstanceExportFormatDialog + + + + 0 + 0 + 446 + 181 + + + + Select Instance Export Format + + + + + 10 + 10 + 421 + 161 + + + + + + + Select export format + + + + + + + Launcher + + + true + + + + + + + Modrinth (WIP) + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + buttonBox + accepted() + SelectInstanceExportFormatDialog + accept() + + + 220 + 152 + + + 222 + 90 + + + + + buttonBox + rejected() + SelectInstanceExportFormatDialog + reject() + + + 220 + 152 + + + 222 + 90 + + + + +