Add support for importing Modrinth packs from files

This commit is contained in:
kb1000 2022-01-31 15:25:36 +01:00 committed by Sefa Eyeoglu
parent c6b3eccbdf
commit db03846358
No known key found for this signature in database
GPG Key ID: C10411294912A422
12 changed files with 452 additions and 6 deletions

View File

@ -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

View File

@ -30,10 +30,15 @@
#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 "net/ChecksumValidator.h"
#include <algorithm>
#include <iterator>
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<Modrinth::File> 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<QJsonObject>(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<INISettingsObject>(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();
}

View File

@ -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;
};

View File

@ -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"

View File

@ -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 <QByteArray>
#include <QCryptographicHash>
#include <QString>
#include <QUrl>
namespace Modrinth {
struct File
{
QString path;
QCryptographicHash::Algorithm hashAlgorithm;
QByteArray hash;
// TODO: should this support multiple download URLs, like the JSON does?
QUrl download;
};
}

View File

@ -20,6 +20,9 @@
<file>scalable/atlauncher.svg</file>
<file>scalable/atlauncher-placeholder.png</file>
<!-- Modrinth logo icon -->
<file>scalable/modrinth.svg</file>
<!-- A proxy icon. Our own. SSSsss -->
<file>scalable/proxy.svg</file>

View File

@ -0,0 +1,4 @@
<svg width="512" height="514" viewBox="0 0 512 514" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M503.16 323.56C514.55 281.47 515.32 235.91 503.2 190.76C466.57 54.2299 326.04 -26.8001 189.33 9.77991C83.8101 38.0199 11.3899 128.07 0.689941 230.47H43.99C54.29 147.33 113.74 74.7298 199.75 51.7098C306.05 23.2598 415.13 80.6699 453.17 181.38L411.03 192.65C391.64 145.8 352.57 111.45 306.3 96.8198L298.56 140.66C335.09 154.13 364.72 184.5 375.56 224.91C391.36 283.8 361.94 344.14 308.56 369.17L320.09 412.16C390.25 383.21 432.4 310.3 422.43 235.14L464.41 223.91C468.91 252.62 467.35 281.16 460.55 308.07L503.16 323.56Z" fill="#5DA426"/>
<path d="M321.99 504.22C185.27 540.8 44.7501 459.77 8.11011 323.24C3.84011 307.31 1.17 291.33 0 275.46H43.27C44.36 287.37 46.4699 299.35 49.6799 311.29C53.0399 323.8 57.45 335.75 62.79 347.07L101.38 323.92C98.1299 316.42 95.39 308.6 93.21 300.47C69.17 210.87 122.41 118.77 212.13 94.7601C229.13 90.2101 246.23 88.4401 262.93 89.1501L255.19 133C244.73 133.05 234.11 134.42 223.53 137.25C157.31 154.98 118.01 222.95 135.75 289.09C136.85 293.16 138.13 297.13 139.59 300.99L188.94 271.38L174.07 231.95L220.67 184.08L279.57 171.39L296.62 192.38L269.47 219.88L245.79 227.33L228.87 244.72L237.16 267.79C237.16 267.79 253.95 285.63 253.98 285.64L277.7 279.33L294.58 260.79L331.44 249.12L342.42 273.82L304.39 320.45L240.66 340.63L212.08 308.81L162.26 338.7C187.8 367.78 226.2 383.93 266.01 380.56L277.54 423.55C218.13 431.41 160.1 406.82 124.05 361.64L85.6399 384.68C136.25 451.17 223.84 484.11 309.61 461.16C371.35 444.64 419.4 402.56 445.42 349.38L488.06 364.88C457.17 431.16 398.22 483.82 321.99 504.22Z" fill="#5DA426"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -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<BasePage *> NewInstanceDialog::getPages()
flamePage,
new FtbPage(this),
new LegacyFTB::Page(this),
new ModrinthPage(this),
technicPage
};
}

View File

@ -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())

View File

@ -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 <QKeyEvent>
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<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Return) {
this->triggerSearch();
keyEvent->accept();
return true;
}
}
return QObject::eventFilter(watched, event);
}
void ModrinthPage::triggerSearch() {
}

View File

@ -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 <QWidget>
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;
};

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ModrinthPage</class>
<widget class="QWidget" name="ModrinthPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>837</width>
<height>685</height>
</rect>
</property>
<layout class="QVBoxLayout">
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>Search and filter ...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="searchButton">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QListView" name="packView">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QTextBrowser" name="packDescription">
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="openLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QComboBox" name="sortByBox"/>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Version selected:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="versionSelectionBox"/>
</item>
</layout>
</item>
</layout>
</widget>
<tabstops>
<tabstop>searchEdit</tabstop>
<tabstop>searchButton</tabstop>
<tabstop>packView</tabstop>
<tabstop>packDescription</tabstop>
<tabstop>sortByBox</tabstop>
<tabstop>versionSelectionBox</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>