aboutsummaryrefslogtreecommitdiff
path: root/launcher/InstanceImportTask.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/InstanceImportTask.cpp')
-rw-r--r--launcher/InstanceImportTask.cpp249
1 files changed, 219 insertions, 30 deletions
diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp
index 1a13c997..8f68b95f 100644
--- a/launcher/InstanceImportTask.cpp
+++ b/launcher/InstanceImportTask.cpp
@@ -1,43 +1,72 @@
-/* Copyright 2013-2021 MultiMC Contributors
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
*
- * 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
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
*
- * 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.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * 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 "InstanceImportTask.h"
+#include <QtConcurrentRun>
+#include "Application.h"
#include "BaseInstance.h"
#include "FileSystem.h"
-#include "Application.h"
#include "MMCZip.h"
#include "NullInstance.h"
-#include "settings/INISettingsObject.h"
#include "icons/IconUtils.h"
-#include <QtConcurrentRun>
+#include "settings/INISettingsObject.h"
// FIXME: this does not belong here, it's Minecraft/Flame specific
+#include <quazip/quazipdir.h>
+#include "Json.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "modplatform/flame/FileResolvingTask.h"
#include "modplatform/flame/PackManifest.h"
-#include "Json.h"
-#include <quazip/quazipdir.h>
+#include "modplatform/modrinth/ModrinthPackManifest.h"
#include "modplatform/technic/TechnicPackProcessor.h"
-#include "icons/IconList.h"
#include "Application.h"
+#include "icons/IconList.h"
+#include "net/ChecksumValidator.h"
-InstanceImportTask::InstanceImportTask(const QUrl sourceUrl)
+#include "ui/dialogs/CustomMessageBox.h"
+
+#include <algorithm>
+#include <iterator>
+
+InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent)
{
m_sourceUrl = sourceUrl;
+ m_parent = parent;
}
bool InstanceImportTask::abort()
@@ -109,6 +138,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 +162,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 +225,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;
@@ -439,25 +479,174 @@ void InstanceImportTask::processMultiMC()
instance.setName(m_instName);
// if the icon was specified by user, use that. otherwise pull icon from the pack
- if (m_instIcon != "default")
- {
+ if (m_instIcon != "default") {
instance.setIconKey(m_instIcon);
- }
- else
- {
+ } else {
m_instIcon = instance.iconKey();
auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon);
- if (!importIconPath.isNull() && QFile::exists(importIconPath))
- {
+ if (!importIconPath.isNull() && QFile::exists(importIconPath)) {
// import icon
auto iconList = APPLICATION->icons();
- if (iconList->iconFileExists(m_instIcon))
- {
+ if (iconList->iconFileExists(m_instIcon)) {
iconList->deleteIcon(m_instIcon);
}
- iconList->installIcons({importIconPath});
+ iconList->installIcons({ importIconPath });
}
}
emitSucceeded();
}
+
+void InstanceImportTask::processModrinth()
+{
+ std::vector<Modrinth::File> files;
+ QString minecraftVersion, fabricVersion, quiltVersion, 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<QJsonObject>(obj, "files", "modrinth.index.json");
+ bool had_optional = false;
+ for (auto& obj : jsonFiles) {
+ Modrinth::File file;
+ file.path = Json::requireString(obj, "path");
+
+ auto env = Json::ensureObject(obj, "env");
+ QString support = Json::ensureString(env, "client", "unsupported");
+ if (support == "unsupported") {
+ continue;
+ } else if (support == "optional") {
+ // TODO: Make a review dialog for choosing which ones the user wants!
+ if (!had_optional) {
+ had_optional = true;
+ auto info = CustomMessageBox::selectable(
+ m_parent, tr("Optional mod detected!"),
+ tr("One or more mods from this modpack are optional. They will be downloaded, but disabled by default!"), QMessageBox::Information);
+ info->exec();
+ }
+
+ if (file.path.endsWith(".jar"))
+ file.path += ".disabled";
+ }
+
+ QJsonObject hashes = Json::requireObject(obj, "hashes");
+ QString hash;
+ QCryptographicHash::Algorithm hashAlgorithm;
+ hash = Json::ensureString(hashes, "sha1");
+ hashAlgorithm = QCryptographicHash::Sha1;
+ if (hash.isEmpty()) {
+ hash = Json::ensureString(hashes, "sha512");
+ hashAlgorithm = QCryptographicHash::Sha512;
+ if (hash.isEmpty()) {
+ hash = Json::ensureString(hashes, "sha256");
+ hashAlgorithm = QCryptographicHash::Sha256;
+ 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() || !Modrinth::validateDownloadUrl(file.download)) {
+ throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL");
+ }
+ files.push_back(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") {
+ minecraftVersion = Json::requireString(*it, "Minecraft version");
+ }
+ else if (name == "fabric-loader") {
+ fabricVersion = Json::requireString(*it, "Fabric Loader version");
+ }
+ else if (name == "quilt-loader") {
+ quiltVersion = Json::requireString(*it, "Quilt Loader version");
+ }
+ else if (name == "forge") {
+ 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<INISettingsObject>(configPath);
+ 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 (!quiltVersion.isEmpty())
+ components->setComponentVersion("org.quiltmc.quilt-loader", quiltVersion, true);
+ if (!forgeVersion.isEmpty())
+ components->setComponentVersion("net.minecraftforge", forgeVersion, true);
+ if (m_instIcon != "default")
+ {
+ instance.setIconKey(m_instIcon);
+ }
+ else
+ {
+ instance.setIconKey("modrinth");
+ }
+ 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();
+}