From db038463581400005f045a277a249ab07175ab2b Mon Sep 17 00:00:00 2001 From: kb1000 Date: Mon, 31 Jan 2022 15:25:36 +0100 Subject: [PATCH] Add support for importing Modrinth packs from files --- launcher/CMakeLists.txt | 10 ++ launcher/InstanceImportTask.cpp | 170 +++++++++++++++++- launcher/InstanceImportTask.h | 6 +- .../modrinth/ModrinthPackManifest.cpp | 16 ++ .../modrinth/ModrinthPackManifest.h | 32 ++++ launcher/resources/multimc/multimc.qrc | 3 + .../resources/multimc/scalable/modrinth.svg | 4 + launcher/ui/dialogs/NewInstanceDialog.cpp | 2 + launcher/ui/pages/modplatform/ImportPage.cpp | 4 +- .../modplatform/modrinth/ModrinthPage.cpp | 55 ++++++ .../pages/modplatform/modrinth/ModrinthPage.h | 62 +++++++ .../modplatform/modrinth/ModrinthPage.ui | 94 ++++++++++ 12 files changed, 452 insertions(+), 6 deletions(-) create mode 100644 launcher/modplatform/modrinth/ModrinthPackManifest.cpp create mode 100644 launcher/modplatform/modrinth/ModrinthPackManifest.h create mode 100644 launcher/resources/multimc/scalable/modrinth.svg create mode 100644 launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp create mode 100644 launcher/ui/pages/modplatform/modrinth/ModrinthPage.h create mode 100644 launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 16ec4c04..cbe135e2 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -563,6 +563,11 @@ set(ATLAUNCHER_SOURCES modplatform/atlauncher/ATLShareCode.h ) +set(MODRINTH_SOURCES + modplatform/modrinth/ModrinthPackManifest.cpp + modplatform/modrinth/ModrinthPackManifest.h +) + add_unit_test(Index SOURCES meta/Index_test.cpp LIBS Launcher_logic @@ -596,6 +601,7 @@ set(LOGIC_SOURCES ${MODPACKSCH_SOURCES} ${TECHNIC_SOURCES} ${ATLAUNCHER_SOURCES} + ${MODRINTH_SOURCES} ) SET(LAUNCHER_SOURCES @@ -774,6 +780,9 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/flame/FlameModPage.cpp ui/pages/modplatform/flame/FlameModPage.h + ui/pages/modplatform/modrinth/ModrinthPage.cpp + ui/pages/modplatform/modrinth/ModrinthPage.h + ui/pages/modplatform/technic/TechnicModel.cpp ui/pages/modplatform/technic/TechnicModel.h ui/pages/modplatform/technic/TechnicPage.cpp @@ -908,6 +917,7 @@ qt5_wrap_ui(LAUNCHER_UI ui/pages/modplatform/legacy_ftb/Page.ui ui/pages/modplatform/ImportPage.ui ui/pages/modplatform/ftb/FtbPage.ui + ui/pages/modplatform/modrinth/ModrinthPage.ui ui/pages/modplatform/technic/TechnicPage.ui ui/widgets/InstanceCardWidget.ui ui/widgets/CustomCommands.ui diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 1a13c997..51715581 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -30,10 +30,15 @@ #include "modplatform/flame/PackManifest.h" #include "Json.h" #include +#include "modplatform/modrinth/ModrinthPackManifest.h" #include "modplatform/technic/TechnicPackProcessor.h" #include "icons/IconList.h" #include "Application.h" +#include "net/ChecksumValidator.h" + +#include +#include InstanceImportTask::InstanceImportTask(const QUrl sourceUrl) { @@ -109,6 +114,7 @@ void InstanceImportTask::processZipPack() QString mmcFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg"); bool technicFound = QuaZipDir(m_packZip.get()).exists("/bin/modpack.jar") || QuaZipDir(m_packZip.get()).exists("/bin/version.json"); QString flameFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json"); + QString modrinthFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "modrinth.index.json"); QString root; if(!mmcFound.isNull()) { @@ -132,6 +138,13 @@ void InstanceImportTask::processZipPack() root = flameFound; m_modpackType = ModpackType::Flame; } + else if(!modrinthFound.isNull()) + { + // process as Modrinth pack + qDebug() << "Modrinth:" << modrinthFound; + root = modrinthFound; + m_modpackType = ModpackType::Modrinth; + } if(m_modpackType == ModpackType::Unknown) { emitFailed(tr("Archive does not contain a recognized modpack type.")); @@ -188,15 +201,18 @@ void InstanceImportTask::extractFinished() switch(m_modpackType) { - case ModpackType::Flame: - processFlame(); - return; case ModpackType::MultiMC: processMultiMC(); return; case ModpackType::Technic: processTechnic(); return; + case ModpackType::Flame: + processFlame(); + return; + case ModpackType::Modrinth: + processModrinth(); + return; case ModpackType::Unknown: emitFailed(tr("Archive does not contain a recognized modpack type.")); return; @@ -461,3 +477,151 @@ void InstanceImportTask::processMultiMC() } emitSucceeded(); } + +void InstanceImportTask::processModrinth() { + std::vector files; + QString minecraftVersion, fabricVersion, forgeVersion; + try + { + QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); + auto doc = Json::requireDocument(indexPath); + auto obj = Json::requireObject(doc, "modrinth.index.json"); + int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json"); + if (formatVersion == 1) + { + auto game = Json::requireString(obj, "game", "modrinth.index.json"); + if (game != "minecraft") + { + throw JSONValidationError("Unknown game: " + game); + } + + auto jsonFiles = Json::requireIsArrayOf(obj, "files", "modrinth.index.json"); + std::transform(jsonFiles.begin(), jsonFiles.end(), std::back_inserter(files), [](const QJsonObject& obj) + { + Modrinth::File file; + file.path = Json::requireString(obj, "path"); + QString supported = Json::ensureString(Json::ensureObject(obj, "env")); + QJsonObject hashes = Json::requireObject(obj, "hashes"); + QString hash; + QCryptographicHash::Algorithm hashAlgorithm; + hash = Json::ensureString(hashes, "sha256"); + hashAlgorithm = QCryptographicHash::Sha256; + if (hash.isEmpty()) + { + hash = Json::ensureString(hashes, "sha512"); + hashAlgorithm = QCryptographicHash::Sha512; + if (hash.isEmpty()) + { + hash = Json::ensureString(hashes, "sha1"); + hashAlgorithm = QCryptographicHash::Sha1; + if (hash.isEmpty()) + { + throw JSONValidationError("No hash found for: " + file.path); + } + } + } + file.hash = QByteArray::fromHex(hash.toLatin1()); + file.hashAlgorithm = hashAlgorithm; + // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode (as Modrinth seems to incorrectly handle spaces) + file.download = Json::requireString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path); + if (!file.download.isValid()) + { + throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL"); + } + return file; + }); + + auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); + for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) + { + QString name = it.key(); + if (name == "minecraft") + { + if (!minecraftVersion.isEmpty()) + throw JSONValidationError("Duplicate Minecraft version"); + minecraftVersion = Json::requireString(*it, "Minecraft version"); + } + else if (name == "fabric-loader") + { + if (!fabricVersion.isEmpty()) + throw JSONValidationError("Duplicate Fabric Loader version"); + fabricVersion = Json::requireString(*it, "Fabric Loader version"); + } + else if (name == "forge") + { + if (!forgeVersion.isEmpty()) + throw JSONValidationError("Duplicate Forge version"); + forgeVersion = Json::requireString(*it, "Forge version"); + } + else + { + throw JSONValidationError("Unknown dependency type: " + name); + } + } + } + else + { + throw JSONValidationError(QStringLiteral("Unknown format version: %s").arg(formatVersion)); + } + QFile::remove(indexPath); + } + catch (const JSONValidationError &e) + { + emitFailed(tr("Could not understand pack index:\n") + e.cause()); + return; + } + QString overridePath = FS::PathCombine(m_stagingPath, "overrides"); + if (QFile::exists(overridePath)) { + QString mcPath = FS::PathCombine(m_stagingPath, ".minecraft"); + if (!QFile::rename(overridePath, mcPath)) { + emitFailed(tr("Could not rename the overrides folder:\n") + "overrides"); + return; + } + } + + QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared(configPath); + instanceSettings->registerSetting("InstanceType", "Legacy"); + instanceSettings->set("InstanceType", "OneSix"); + MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", minecraftVersion, true); + if (!fabricVersion.isEmpty()) + components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion, true); + if (!forgeVersion.isEmpty()) + components->setComponentVersion("net.minecraftforge", forgeVersion, true); + if (m_instIcon != "default") + { + instance.setIconKey(m_instIcon); + } + instance.setName(m_instName); + instance.saveNow(); + + m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network()); + for (auto &file : files) + { + auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path); + qDebug() << "Will download" << file.download << "to" << path; + auto dl = Net::Download::makeFile(file.download, path); + dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); + m_filesNetJob->addNetAction(dl); + } + connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() + { + m_filesNetJob.reset(); + emitSucceeded(); + } + ); + connect(m_filesNetJob.get(), &NetJob::failed, [&](const QString &reason) + { + m_filesNetJob.reset(); + emitFailed(reason); + }); + connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) + { + setProgress(current, total); + }); + setStatus(tr("Downloading mods...")); + m_filesNetJob->start(); +} diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index 365c3dc4..317562d9 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -47,8 +47,9 @@ protected: private: void processZipPack(); void processMultiMC(); - void processFlame(); void processTechnic(); + void processFlame(); + void processModrinth(); private slots: void downloadSucceeded(); @@ -69,7 +70,8 @@ private: /* data */ enum class ModpackType{ Unknown, MultiMC, + Technic, Flame, - Technic + Modrinth, } m_modpackType = ModpackType::Unknown; }; diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp new file mode 100644 index 00000000..2100aaf9 --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -0,0 +1,16 @@ +/* Copyright 2022 kb1000 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ModrinthPackManifest.h" diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h new file mode 100644 index 00000000..9742aeb2 --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -0,0 +1,32 @@ +/* Copyright 2022 kb1000 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +namespace Modrinth { +struct File +{ + QString path; + QCryptographicHash::Algorithm hashAlgorithm; + QByteArray hash; + // TODO: should this support multiple download URLs, like the JSON does? + QUrl download; +}; +} diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index 0fe673ff..1671093d 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -20,6 +20,9 @@ scalable/atlauncher.svg scalable/atlauncher-placeholder.png + + scalable/modrinth.svg + scalable/proxy.svg diff --git a/launcher/resources/multimc/scalable/modrinth.svg b/launcher/resources/multimc/scalable/modrinth.svg new file mode 100644 index 00000000..32715f5c --- /dev/null +++ b/launcher/resources/multimc/scalable/modrinth.svg @@ -0,0 +1,4 @@ + + + + diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index b402839c..05ea091d 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -39,6 +39,7 @@ #include "ui/pages/modplatform/legacy_ftb/Page.h" #include "ui/pages/modplatform/flame/FlamePage.h" #include "ui/pages/modplatform/ImportPage.h" +#include "ui/pages/modplatform/modrinth/ModrinthPage.h" #include "ui/pages/modplatform/technic/TechnicPage.h" @@ -134,6 +135,7 @@ QList NewInstanceDialog::getPages() flamePage, new FtbPage(this), new LegacyFTB::Page(this), + new ModrinthPage(this), technicPage }; } diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index 1b53dd40..8ae38f8d 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -109,7 +109,8 @@ void ImportPage::updateState() { // FIXME: actually do some validation of what's inside here... this is fake AF QFileInfo fi(input); - if(fi.exists() && fi.suffix() == "zip") + // mrpack is a modrinth pack + if(fi.exists() && (fi.suffix() == "zip" || fi.suffix() == "mrpack")) { QFileInfo fi(url.fileName()); dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url)); @@ -143,6 +144,7 @@ void ImportPage::setUrl(const QString& url) void ImportPage::on_modpackBtn_clicked() { + // TODO: Add .mrpack filter auto filter = QMimeDatabase().mimeTypeForName("application/zip").filterString(); const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), filter); if (url.isValid()) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp new file mode 100644 index 00000000..93b1ca02 --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2021 MultiMC Contributors + * Copyright 2021-2022 kb1000 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ModrinthPage.h" + +#include "ui_ModrinthPage.h" + +#include + +ModrinthPage::ModrinthPage(NewInstanceDialog *dialog, QWidget *parent) : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog) +{ + ui->setupUi(this); +} + +ModrinthPage::~ModrinthPage() +{ + delete ui; +} + +void ModrinthPage::openedImpl() +{ + BasePage::openedImpl(); + triggerSearch(); +} + +bool ModrinthPage::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + auto *keyEvent = reinterpret_cast(event); + if (keyEvent->key() == Qt::Key_Return) { + this->triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QObject::eventFilter(watched, event); +} + +void ModrinthPage::triggerSearch() { + +} diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h new file mode 100644 index 00000000..6c75b60d --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -0,0 +1,62 @@ +/* + * Copyright 2013-2021 MultiMC Contributors + * Copyright 2021-2022 kb1000 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "Application.h" +#include "ui/dialogs/NewInstanceDialog.h" +#include "ui/pages/BasePage.h" + +#include + +namespace Ui +{ + class ModrinthPage; +} + +class ModrinthPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit ModrinthPage(NewInstanceDialog *dialog, QWidget *parent = nullptr); + ~ModrinthPage() override; + + QString displayName() const override + { + return tr("Modrinth"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("modrinth"); + } + QString id() const override + { + return "modrinth"; + } + + void openedImpl() override; + + bool eventFilter(QObject *watched, QEvent *event) override; + +private slots: + void triggerSearch(); + +private: + Ui::ModrinthPage *ui; + NewInstanceDialog *dialog; +}; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui new file mode 100644 index 00000000..7ef099d3 --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -0,0 +1,94 @@ + + + ModrinthPage + + + + 0 + 0 + 837 + 685 + + + + + + + + + Search and filter ... + + + + + + + Search + + + + + + + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + 48 + 48 + + + + + + + + true + + + true + + + + + + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + searchEdit + searchButton + packView + packDescription + sortByBox + versionSelectionBox + + + +