aboutsummaryrefslogtreecommitdiff
path: root/launcher/modplatform
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/modplatform')
-rw-r--r--launcher/modplatform/atlauncher/ATLPackIndex.cpp33
-rw-r--r--launcher/modplatform/atlauncher/ATLPackIndex.h34
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.cpp764
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.h101
-rw-r--r--launcher/modplatform/atlauncher/ATLPackManifest.cpp218
-rw-r--r--launcher/modplatform/atlauncher/ATLPackManifest.h125
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.cpp63
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.h32
-rw-r--r--launcher/modplatform/flame/FlamePackIndex.cpp92
-rw-r--r--launcher/modplatform/flame/FlamePackIndex.h41
-rw-r--r--launcher/modplatform/flame/PackManifest.cpp126
-rw-r--r--launcher/modplatform/flame/PackManifest.h62
-rw-r--r--launcher/modplatform/legacy_ftb/PackFetchTask.cpp172
-rw-r--r--launcher/modplatform/legacy_ftb/PackFetchTask.h44
-rw-r--r--launcher/modplatform/legacy_ftb/PackHelpers.h45
-rw-r--r--launcher/modplatform/legacy_ftb/PackInstallTask.cpp214
-rw-r--r--launcher/modplatform/legacy_ftb/PackInstallTask.h55
-rw-r--r--launcher/modplatform/legacy_ftb/PrivatePackManager.cpp41
-rw-r--r--launcher/modplatform/legacy_ftb/PrivatePackManager.h43
-rw-r--r--launcher/modplatform/modpacksch/FTBPackInstallTask.cpp209
-rw-r--r--launcher/modplatform/modpacksch/FTBPackInstallTask.h46
-rw-r--r--launcher/modplatform/modpacksch/FTBPackManifest.cpp156
-rw-r--r--launcher/modplatform/modpacksch/FTBPackManifest.h125
-rw-r--r--launcher/modplatform/technic/SingleZipPackInstallTask.cpp141
-rw-r--r--launcher/modplatform/technic/SingleZipPackInstallTask.h64
-rw-r--r--launcher/modplatform/technic/SolderPackInstallTask.cpp207
-rw-r--r--launcher/modplatform/technic/SolderPackInstallTask.h60
-rw-r--r--launcher/modplatform/technic/TechnicPackProcessor.cpp208
-rw-r--r--launcher/modplatform/technic/TechnicPackProcessor.h35
29 files changed, 3556 insertions, 0 deletions
diff --git a/launcher/modplatform/atlauncher/ATLPackIndex.cpp b/launcher/modplatform/atlauncher/ATLPackIndex.cpp
new file mode 100644
index 00000000..35f50b18
--- /dev/null
+++ b/launcher/modplatform/atlauncher/ATLPackIndex.cpp
@@ -0,0 +1,33 @@
+#include "ATLPackIndex.h"
+
+#include <QRegularExpression>
+
+#include "Json.h"
+
+static void loadIndexedVersion(ATLauncher::IndexedVersion & v, QJsonObject & obj)
+{
+ v.version = Json::requireString(obj, "version");
+ v.minecraft = Json::requireString(obj, "minecraft");
+}
+
+void ATLauncher::loadIndexedPack(ATLauncher::IndexedPack & m, QJsonObject & obj)
+{
+ m.id = Json::requireInteger(obj, "id");
+ m.position = Json::requireInteger(obj, "position");
+ m.name = Json::requireString(obj, "name");
+ m.type = Json::requireString(obj, "type") == "private" ?
+ ATLauncher::PackType::Private :
+ ATLauncher::PackType::Public;
+ auto versionsArr = Json::requireArray(obj, "versions");
+ for (const auto versionRaw : versionsArr)
+ {
+ auto versionObj = Json::requireObject(versionRaw);
+ ATLauncher::IndexedVersion version;
+ loadIndexedVersion(version, versionObj);
+ m.versions.append(version);
+ }
+ m.system = Json::ensureBoolean(obj, QString("system"), false);
+ m.description = Json::ensureString(obj, "description", "");
+
+ m.safeName = Json::requireString(obj, "name").replace(QRegularExpression("[^A-Za-z0-9]"), "");
+}
diff --git a/launcher/modplatform/atlauncher/ATLPackIndex.h b/launcher/modplatform/atlauncher/ATLPackIndex.h
new file mode 100644
index 00000000..405a3448
--- /dev/null
+++ b/launcher/modplatform/atlauncher/ATLPackIndex.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "ATLPackManifest.h"
+
+#include <QString>
+#include <QVector>
+#include <QMetaType>
+
+namespace ATLauncher
+{
+
+struct IndexedVersion
+{
+ QString version;
+ QString minecraft;
+};
+
+struct IndexedPack
+{
+ int id;
+ int position;
+ QString name;
+ PackType type;
+ QVector<IndexedVersion> versions;
+ bool system;
+ QString description;
+
+ QString safeName;
+};
+
+void loadIndexedPack(IndexedPack & m, QJsonObject & obj);
+}
+
+Q_DECLARE_METATYPE(ATLauncher::IndexedPack)
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
new file mode 100644
index 00000000..55087a27
--- /dev/null
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
@@ -0,0 +1,764 @@
+#include <Env.h>
+#include <quazip.h>
+#include <QtConcurrent/QtConcurrent>
+#include <MMCZip.h>
+#include <minecraft/OneSixVersionFormat.h>
+#include <Version.h>
+#include <net/ChecksumValidator.h>
+#include "ATLPackInstallTask.h"
+
+#include "BuildConfig.h"
+#include "FileSystem.h"
+#include "Json.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "settings/INISettingsObject.h"
+#include "meta/Index.h"
+#include "meta/Version.h"
+#include "meta/VersionList.h"
+
+namespace ATLauncher {
+
+PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString pack, QString version)
+{
+ m_support = support;
+ m_pack = pack;
+ m_version_name = version;
+}
+
+bool PackInstallTask::abort()
+{
+ if(abortable)
+ {
+ return jobPtr->abort();
+ }
+ return false;
+}
+
+void PackInstallTask::executeTask()
+{
+ qDebug() << "PackInstallTask::executeTask: " << QThread::currentThreadId();
+ auto *netJob = new NetJob("ATLauncher::VersionFetch");
+ auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json")
+ .arg(m_pack).arg(m_version_name);
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
+ jobPtr = netJob;
+ jobPtr->start();
+
+ QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded);
+ QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed);
+}
+
+void PackInstallTask::onDownloadSucceeded()
+{
+ qDebug() << "PackInstallTask::onDownloadSucceeded: " << QThread::currentThreadId();
+ jobPtr.reset();
+
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+ if(parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ auto obj = doc.object();
+
+ ATLauncher::PackVersion version;
+ try
+ {
+ ATLauncher::loadVersion(version, obj);
+ }
+ catch (const JSONValidationError &e)
+ {
+ emitFailed(tr("Could not understand pack manifest:\n") + e.cause());
+ return;
+ }
+ m_version = version;
+
+ auto vlist = ENV.metadataIndex()->get("net.minecraft");
+ if(!vlist)
+ {
+ emitFailed(tr("Failed to get local metadata index for %1").arg("net.minecraft"));
+ return;
+ }
+
+ auto ver = vlist->getVersion(m_version.minecraft);
+ if (!ver) {
+ emitFailed(tr("Failed to get local metadata index for '%1' v%2").arg("net.minecraft").arg(m_version.minecraft));
+ return;
+ }
+ ver->load(Net::Mode::Online);
+ minecraftVersion = ver;
+
+ if(m_version.noConfigs) {
+ downloadMods();
+ }
+ else {
+ installConfigs();
+ }
+}
+
+void PackInstallTask::onDownloadFailed(QString reason)
+{
+ qDebug() << "PackInstallTask::onDownloadFailed: " << QThread::currentThreadId();
+ jobPtr.reset();
+ emitFailed(reason);
+}
+
+QString PackInstallTask::getDirForModType(ModType type, QString raw)
+{
+ switch (type) {
+ // Mod types that can either be ignored at this stage, or ignored
+ // completely.
+ case ModType::Root:
+ case ModType::Extract:
+ case ModType::Decomp:
+ case ModType::TexturePackExtract:
+ case ModType::ResourcePackExtract:
+ case ModType::MCPC:
+ return Q_NULLPTR;
+ case ModType::Forge:
+ // Forge detection happens later on, if it cannot be detected it will
+ // install a jarmod component.
+ case ModType::Jar:
+ return "jarmods";
+ case ModType::Mods:
+ return "mods";
+ case ModType::Flan:
+ return "Flan";
+ case ModType::Dependency:
+ return FS::PathCombine("mods", m_version.minecraft);
+ case ModType::Ic2Lib:
+ return FS::PathCombine("mods", "ic2");
+ case ModType::DenLib:
+ return FS::PathCombine("mods", "denlib");
+ case ModType::Coremods:
+ return "coremods";
+ case ModType::Plugins:
+ return "plugins";
+ case ModType::TexturePack:
+ return "texturepacks";
+ case ModType::ResourcePack:
+ return "resourcepacks";
+ case ModType::ShaderPack:
+ return "shaderpacks";
+ case ModType::Millenaire:
+ qWarning() << "Unsupported mod type: " + raw;
+ return Q_NULLPTR;
+ case ModType::Unknown:
+ emitFailed(tr("Unknown mod type: %1").arg(raw));
+ return Q_NULLPTR;
+ }
+
+ return Q_NULLPTR;
+}
+
+QString PackInstallTask::getVersionForLoader(QString uid)
+{
+ if(m_version.loader.recommended || m_version.loader.latest || m_version.loader.choose) {
+ auto vlist = ENV.metadataIndex()->get(uid);
+ if(!vlist)
+ {
+ emitFailed(tr("Failed to get local metadata index for %1").arg(uid));
+ return Q_NULLPTR;
+ }
+
+ if(!vlist->isLoaded()) {
+ vlist->load(Net::Mode::Online);
+ }
+
+ if(m_version.loader.recommended || m_version.loader.latest) {
+ for (int i = 0; i < vlist->versions().size(); i++) {
+ auto version = vlist->versions().at(i);
+ auto reqs = version->requires();
+
+ // filter by minecraft version, if the loader depends on a certain version.
+ // not all mod loaders depend on a given Minecraft version, so we won't do this
+ // filtering for those loaders.
+ if (m_version.loader.type != "fabric") {
+ auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require &req) {
+ return req.uid == "net.minecraft";
+ });
+ if (iter == reqs.end()) continue;
+ if (iter->equalsVersion != m_version.minecraft) continue;
+ }
+
+ if (m_version.loader.recommended) {
+ // first recommended build we find, we use.
+ if (!version->isRecommended()) continue;
+ }
+
+ return version->descriptor();
+ }
+
+ emitFailed(tr("Failed to find version for %1 loader").arg(m_version.loader.type));
+ return Q_NULLPTR;
+ }
+ else if(m_version.loader.choose) {
+ // Fabric Loader doesn't depend on a given Minecraft version.
+ if (m_version.loader.type == "fabric") {
+ return m_support->chooseVersion(vlist, Q_NULLPTR);
+ }
+
+ return m_support->chooseVersion(vlist, m_version.minecraft);
+ }
+ }
+
+ if (m_version.loader.version == Q_NULLPTR || m_version.loader.version.isEmpty()) {
+ emitFailed(tr("No loader version set for modpack!"));
+ return Q_NULLPTR;
+ }
+
+ return m_version.loader.version;
+}
+
+QString PackInstallTask::detectLibrary(VersionLibrary library)
+{
+ // Try to detect what the library is
+ if (!library.server.isEmpty() && library.server.split("/").length() >= 3) {
+ auto lastSlash = library.server.lastIndexOf("/");
+ auto locationAndVersion = library.server.mid(0, lastSlash);
+ auto fileName = library.server.mid(lastSlash + 1);
+
+ lastSlash = locationAndVersion.lastIndexOf("/");
+ auto location = locationAndVersion.mid(0, lastSlash);
+ auto version = locationAndVersion.mid(lastSlash + 1);
+
+ lastSlash = location.lastIndexOf("/");
+ auto group = location.mid(0, lastSlash).replace("/", ".");
+ auto artefact = location.mid(lastSlash + 1);
+
+ return group + ":" + artefact + ":" + version;
+ }
+
+ if(library.file.contains("-")) {
+ auto lastSlash = library.file.lastIndexOf("-");
+ auto name = library.file.mid(0, lastSlash);
+ auto version = library.file.mid(lastSlash + 1).remove(".jar");
+
+ if(name == QString("guava")) {
+ return "com.google.guava:guava:" + version;
+ }
+ else if(name == QString("commons-lang3")) {
+ return "org.apache.commons:commons-lang3:" + version;
+ }
+ }
+
+ return "org.multimc.atlauncher:" + library.md5 + ":1";
+}
+
+bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile)
+{
+ if(m_version.libraries.isEmpty()) {
+ return true;
+ }
+
+ QList<GradleSpecifier> exempt;
+ for(const auto & componentUid : componentsToInstall.keys()) {
+ auto componentVersion = componentsToInstall.value(componentUid);
+
+ for(const auto & library : componentVersion->data()->libraries) {
+ GradleSpecifier lib(library->rawName());
+ exempt.append(lib);
+ }
+ }
+
+ {
+ for(const auto & library : minecraftVersion->data()->libraries) {
+ GradleSpecifier lib(library->rawName());
+ exempt.append(lib);
+ }
+ }
+
+ auto uuid = QUuid::createUuid();
+ auto id = uuid.toString().remove('{').remove('}');
+ auto target_id = "org.multimc.atlauncher." + id;
+
+ auto patchDir = FS::PathCombine(instanceRoot, "patches");
+ if(!FS::ensureFolderPathExists(patchDir))
+ {
+ return false;
+ }
+ auto patchFileName = FS::PathCombine(patchDir, target_id + ".json");
+
+ auto f = std::make_shared<VersionFile>();
+ f->name = m_pack + " " + m_version_name + " (libraries)";
+
+ for(const auto & lib : m_version.libraries) {
+ auto libName = detectLibrary(lib);
+ GradleSpecifier libSpecifier(libName);
+
+ bool libExempt = false;
+ for(const auto & existingLib : exempt) {
+ if(libSpecifier.matchName(existingLib)) {
+ // If the pack specifies a newer version of the lib, use that!
+ libExempt = Version(libSpecifier.version()) >= Version(existingLib.version());
+ }
+ }
+ if(libExempt) continue;
+
+ auto library = std::make_shared<Library>();
+ library->setRawName(libName);
+
+ switch(lib.download) {
+ case DownloadType::Server:
+ library->setAbsoluteUrl(BuildConfig.ATL_DOWNLOAD_SERVER_URL + lib.url);
+ break;
+ case DownloadType::Direct:
+ library->setAbsoluteUrl(lib.url);
+ break;
+ case DownloadType::Browser:
+ case DownloadType::Unknown:
+ emitFailed(tr("Unknown or unsupported download type: %1").arg(lib.download_raw));
+ return false;
+ }
+
+ f->libraries.append(library);
+ }
+
+ if(f->libraries.isEmpty()) {
+ return true;
+ }
+
+ QFile file(patchFileName);
+ if (!file.open(QFile::WriteOnly))
+ {
+ qCritical() << "Error opening" << file.fileName()
+ << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
+ file.close();
+
+ profile->appendComponent(new Component(profile.get(), target_id, f));
+ return true;
+}
+
+bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile)
+{
+ if(m_version.mainClass == QString() && m_version.extraArguments == QString()) {
+ return true;
+ }
+
+ auto uuid = QUuid::createUuid();
+ auto id = uuid.toString().remove('{').remove('}');
+ auto target_id = "org.multimc.atlauncher." + id;
+
+ auto patchDir = FS::PathCombine(instanceRoot, "patches");
+ if(!FS::ensureFolderPathExists(patchDir))
+ {
+ return false;
+ }
+ auto patchFileName = FS::PathCombine(patchDir, target_id + ".json");
+
+ QStringList mainClasses;
+ QStringList tweakers;
+ for(const auto & componentUid : componentsToInstall.keys()) {
+ auto componentVersion = componentsToInstall.value(componentUid);
+
+ if(componentVersion->data()->mainClass != QString("")) {
+ mainClasses.append(componentVersion->data()->mainClass);
+ }
+ tweakers.append(componentVersion->data()->addTweakers);
+ }
+
+ auto f = std::make_shared<VersionFile>();
+ f->name = m_pack + " " + m_version_name;
+ if(m_version.mainClass != QString() && !mainClasses.contains(m_version.mainClass)) {
+ f->mainClass = m_version.mainClass;
+ }
+
+ // Parse out tweakers
+ auto args = m_version.extraArguments.split(" ");
+ QString previous;
+ for(auto arg : args) {
+ if(arg.startsWith("--tweakClass=") || previous == "--tweakClass") {
+ auto tweakClass = arg.remove("--tweakClass=");
+ if(tweakers.contains(tweakClass)) continue;
+
+ f->addTweakers.append(tweakClass);
+ }
+ previous = arg;
+ }
+
+ if(f->mainClass == QString() && f->addTweakers.isEmpty()) {
+ return true;
+ }
+
+ QFile file(patchFileName);
+ if (!file.open(QFile::WriteOnly))
+ {
+ qCritical() << "Error opening" << file.fileName()
+ << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
+ file.close();
+
+ profile->appendComponent(new Component(profile.get(), target_id, f));
+ return true;
+}
+
+void PackInstallTask::installConfigs()
+{
+ qDebug() << "PackInstallTask::installConfigs: " << QThread::currentThreadId();
+ setStatus(tr("Downloading configs..."));
+ jobPtr.reset(new NetJob(tr("Config download")));
+
+ auto path = QString("Configs/%1/%2.zip").arg(m_pack).arg(m_version_name);
+ auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.zip")
+ .arg(m_pack).arg(m_version_name);
+ auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", path);
+ entry->setStale(true);
+
+ auto dl = Net::Download::makeCached(url, entry);
+ if (!m_version.configs.sha1.isEmpty()) {
+ auto rawSha1 = QByteArray::fromHex(m_version.configs.sha1.toLatin1());
+ dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1));
+ }
+ jobPtr->addNetAction(dl);
+ archivePath = entry->getFullPath();
+
+ connect(jobPtr.get(), &NetJob::succeeded, this, [&]()
+ {
+ abortable = false;
+ jobPtr.reset();
+ extractConfigs();
+ });
+ connect(jobPtr.get(), &NetJob::failed, [&](QString reason)
+ {
+ abortable = false;
+ jobPtr.reset();
+ emitFailed(reason);
+ });
+ connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total)
+ {
+ abortable = true;
+ setProgress(current, total);
+ });
+
+ jobPtr->start();
+}
+
+void PackInstallTask::extractConfigs()
+{
+ qDebug() << "PackInstallTask::extractConfigs: " << QThread::currentThreadId();
+ setStatus(tr("Extracting configs..."));
+
+ QDir extractDir(m_stagingPath);
+
+ QuaZip packZip(archivePath);
+ if(!packZip.open(QuaZip::mdUnzip))
+ {
+ emitFailed(tr("Failed to open pack configs %1!").arg(archivePath));
+ return;
+ }
+
+ m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/minecraft");
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, [&]()
+ {
+ downloadMods();
+ });
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, [&]()
+ {
+ emitAborted();
+ });
+ m_extractFutureWatcher.setFuture(m_extractFuture);
+}
+
+void PackInstallTask::downloadMods()
+{
+ qDebug() << "PackInstallTask::installMods: " << QThread::currentThreadId();
+
+ QVector<ATLauncher::VersionMod> optionalMods;
+ for (const auto& mod : m_version.mods) {
+ if (mod.optional) {
+ optionalMods.push_back(mod);
+ }
+ }
+
+ // Select optional mods, if pack contains any
+ QVector<QString> selectedMods;
+ if (!optionalMods.isEmpty()) {
+ setStatus(tr("Selecting optional mods..."));
+ selectedMods = m_support->chooseOptionalMods(optionalMods);
+ }
+
+ setStatus(tr("Downloading mods..."));
+
+ jarmods.clear();
+ jobPtr.reset(new NetJob(tr("Mod download")));
+ for(const auto& mod : m_version.mods) {
+ // skip non-client mods
+ if(!mod.client) continue;
+
+ // skip optional mods that were not selected
+ if(mod.optional && !selectedMods.contains(mod.name)) continue;
+
+ QString url;
+ switch(mod.download) {
+ case DownloadType::Server:
+ url = BuildConfig.ATL_DOWNLOAD_SERVER_URL + mod.url;
+ break;
+ case DownloadType::Browser:
+ emitFailed(tr("Unsupported download type: %1").arg(mod.download_raw));
+ return;
+ case DownloadType::Direct:
+ url = mod.url;
+ break;
+ case DownloadType::Unknown:
+ emitFailed(tr("Unknown download type: %1").arg(mod.download_raw));
+ return;
+ }
+
+ QFileInfo fileName(mod.file);
+ auto cacheName = fileName.completeBaseName() + "-" + mod.md5 + "." + fileName.suffix();
+
+ if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract || mod.type == ModType::ResourcePackExtract) {
+ auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", cacheName);
+ entry->setStale(true);
+ modsToExtract.insert(entry->getFullPath(), mod);
+
+ auto dl = Net::Download::makeCached(url, entry);
+ if (!mod.md5.isEmpty()) {
+ auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
+ dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5));
+ }
+ jobPtr->addNetAction(dl);
+ }
+ else if(mod.type == ModType::Decomp) {
+ auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", cacheName);
+ entry->setStale(true);
+ modsToDecomp.insert(entry->getFullPath(), mod);
+
+ auto dl = Net::Download::makeCached(url, entry);
+ if (!mod.md5.isEmpty()) {
+ auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
+ dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5));
+ }
+ jobPtr->addNetAction(dl);
+ }
+ else {
+ auto relpath = getDirForModType(mod.type, mod.type_raw);
+ if(relpath == Q_NULLPTR) continue;
+
+ auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", cacheName);
+ entry->setStale(true);
+
+ auto dl = Net::Download::makeCached(url, entry);
+ if (!mod.md5.isEmpty()) {
+ auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
+ dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5));
+ }
+ jobPtr->addNetAction(dl);
+
+ auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file);
+ qDebug() << "Will download" << url << "to" << path;
+ modsToCopy[entry->getFullPath()] = path;
+
+ if(mod.type == ModType::Forge) {
+ auto vlist = ENV.metadataIndex()->get("net.minecraftforge");
+ if(vlist)
+ {
+ auto ver = vlist->getVersion(mod.version);
+ if(ver) {
+ ver->load(Net::Mode::Online);
+ componentsToInstall.insert("net.minecraftforge", ver);
+ continue;
+ }
+ }
+
+ qDebug() << "Jarmod: " + path;
+ jarmods.push_back(path);
+ }
+
+ if(mod.type == ModType::Jar) {
+ qDebug() << "Jarmod: " + path;
+ jarmods.push_back(path);
+ }
+ }
+ }
+
+ connect(jobPtr.get(), &NetJob::succeeded, this, &PackInstallTask::onModsDownloaded);
+ connect(jobPtr.get(), &NetJob::failed, [&](QString reason)
+ {
+ abortable = false;
+ jobPtr.reset();
+ emitFailed(reason);
+ });
+ connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total)
+ {
+ abortable = true;
+ setProgress(current, total);
+ });
+
+ jobPtr->start();
+}
+
+void PackInstallTask::onModsDownloaded() {
+ abortable = false;
+
+ qDebug() << "PackInstallTask::onModsDownloaded: " << QThread::currentThreadId();
+ jobPtr.reset();
+
+ if(!modsToExtract.empty() || !modsToDecomp.empty() || !modsToCopy.empty()) {
+ m_modExtractFuture = QtConcurrent::run(QThreadPool::globalInstance(), this, &PackInstallTask::extractMods, modsToExtract, modsToDecomp, modsToCopy);
+ connect(&m_modExtractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onModsExtracted);
+ connect(&m_modExtractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, [&]()
+ {
+ emitAborted();
+ });
+ m_modExtractFutureWatcher.setFuture(m_modExtractFuture);
+ }
+ else {
+ install();
+ }
+}
+
+void PackInstallTask::onModsExtracted() {
+ qDebug() << "PackInstallTask::onModsExtracted: " << QThread::currentThreadId();
+ if(m_modExtractFuture.result()) {
+ install();
+ }
+ else {
+ emitFailed(tr("Failed to extract mods..."));
+ }
+}
+
+bool PackInstallTask::extractMods(
+ const QMap<QString, VersionMod> &toExtract,
+ const QMap<QString, VersionMod> &toDecomp,
+ const QMap<QString, QString> &toCopy
+) {
+ qDebug() << "PackInstallTask::extractMods: " << QThread::currentThreadId();
+
+ setStatus(tr("Extracting mods..."));
+ for (auto iter = toExtract.begin(); iter != toExtract.end(); iter++) {
+ auto &modPath = iter.key();
+ auto &mod = iter.value();
+
+ QString extractToDir;
+ if(mod.type == ModType::Extract) {
+ extractToDir = getDirForModType(mod.extractTo, mod.extractTo_raw);
+ }
+ else if(mod.type == ModType::TexturePackExtract) {
+ extractToDir = FS::PathCombine("texturepacks", "extracted");
+ }
+ else if(mod.type == ModType::ResourcePackExtract) {
+ extractToDir = FS::PathCombine("resourcepacks", "extracted");
+ }
+
+ QDir extractDir(m_stagingPath);
+ auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir);
+
+ QString folderToExtract = "";
+ if(mod.type == ModType::Extract) {
+ folderToExtract = mod.extractFolder;
+ folderToExtract.remove(QRegExp("^/"));
+ }
+
+ qDebug() << "Extracting " + mod.file + " to " + extractToDir;
+ if(!MMCZip::extractDir(modPath, folderToExtract, extractToPath)) {
+ // assume error
+ return false;
+ }
+ }
+
+ for (auto iter = toDecomp.begin(); iter != toDecomp.end(); iter++) {
+ auto &modPath = iter.key();
+ auto &mod = iter.value();
+ auto extractToDir = getDirForModType(mod.decompType, mod.decompType_raw);
+
+ QDir extractDir(m_stagingPath);
+ auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir, mod.decompFile);
+
+ qDebug() << "Extracting " + mod.decompFile + " to " + extractToDir;
+ if(!MMCZip::extractFile(modPath, mod.decompFile, extractToPath)) {
+ qWarning() << "Failed to extract" << mod.decompFile;
+ return false;
+ }
+ }
+
+ for (auto iter = toCopy.begin(); iter != toCopy.end(); iter++) {
+ auto &from = iter.key();
+ auto &to = iter.value();
+ FS::copy fileCopyOperation(from, to);
+ if(!fileCopyOperation()) {
+ qWarning() << "Failed to copy" << from << "to" << to;
+ return false;
+ }
+ }
+ return true;
+}
+
+void PackInstallTask::install()
+{
+ qDebug() << "PackInstallTask::install: " << QThread::currentThreadId();
+ setStatus(tr("Installing modpack"));
+
+ auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg");
+ auto instanceSettings = std::make_shared<INISettingsObject>(instanceConfigPath);
+ instanceSettings->suspendSave();
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+ instanceSettings->set("InstanceType", "OneSix");
+
+ MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
+ auto components = instance.getPackProfile();
+ components->buildingFromScratch();
+
+ // Use a component to add libraries BEFORE Minecraft
+ if(!createLibrariesComponent(instance.instanceRoot(), components)) {
+ emitFailed(tr("Failed to create libraries component"));
+ return;
+ }
+
+ // Minecraft
+ components->setComponentVersion("net.minecraft", m_version.minecraft, true);
+
+ // Loader
+ if(m_version.loader.type == QString("forge"))
+ {
+ auto version = getVersionForLoader("net.minecraftforge");
+ if(version == Q_NULLPTR) return;
+
+ components->setComponentVersion("net.minecraftforge", version, true);
+ }
+ else if(m_version.loader.type == QString("fabric"))
+ {
+ auto version = getVersionForLoader("net.fabricmc.fabric-loader");
+ if(version == Q_NULLPTR) return;
+
+ components->setComponentVersion("net.fabricmc.fabric-loader", version, true);
+ }
+ else if(m_version.loader.type != QString())
+ {
+ emitFailed(tr("Unknown loader type: ") + m_version.loader.type);
+ return;
+ }
+
+ for(const auto & componentUid : componentsToInstall.keys()) {
+ auto version = componentsToInstall.value(componentUid);
+ components->setComponentVersion(componentUid, version->version());
+ }
+
+ components->installJarMods(jarmods);
+
+ // Use a component to fill in the rest of the data
+ // todo: use more detection
+ if(!createPackComponent(instance.instanceRoot(), components)) {
+ emitFailed(tr("Failed to create pack component"));
+ return;
+ }
+
+ components->saveNow();
+
+ instance.setName(m_instName);
+ instance.setIconKey(m_instIcon);
+ instanceSettings->resumeSave();
+
+ jarmods.clear();
+ emitSucceeded();
+}
+
+}
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h
new file mode 100644
index 00000000..39e2b013
--- /dev/null
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h
@@ -0,0 +1,101 @@
+#pragma once
+
+#include <meta/VersionList.h>
+#include "ATLPackManifest.h"
+
+#include "InstanceTask.h"
+#include "net/NetJob.h"
+#include "settings/INISettingsObject.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "meta/Version.h"
+
+#include <nonstd/optional>
+
+namespace ATLauncher {
+
+class UserInteractionSupport {
+
+public:
+ /**
+ * Requests a user interaction to select which optional mods should be installed.
+ */
+ virtual QVector<QString> chooseOptionalMods(QVector<ATLauncher::VersionMod> mods) = 0;
+
+ /**
+ * Requests a user interaction to select a component version from a given version list
+ * and constrained to a given Minecraft version.
+ */
+ virtual QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) = 0;
+
+};
+
+class PackInstallTask : public InstanceTask
+{
+Q_OBJECT
+
+public:
+ explicit PackInstallTask(UserInteractionSupport *support, QString pack, QString version);
+ virtual ~PackInstallTask(){}
+
+ bool canAbort() const override { return true; }
+ bool abort() override;
+
+protected:
+ virtual void executeTask() override;
+
+private slots:
+ void onDownloadSucceeded();
+ void onDownloadFailed(QString reason);
+
+ void onModsDownloaded();
+ void onModsExtracted();
+
+private:
+ QString getDirForModType(ModType type, QString raw);
+ QString getVersionForLoader(QString uid);
+ QString detectLibrary(VersionLibrary library);
+
+ bool createLibrariesComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile);
+ bool createPackComponent(QString instanceRoot, std::shared_ptr<PackProfile> profile);
+
+ void installConfigs();
+ void extractConfigs();
+ void downloadMods();
+ bool extractMods(
+ const QMap<QString, VersionMod> &toExtract,
+ const QMap<QString, VersionMod> &toDecomp,
+ const QMap<QString, QString> &toCopy
+ );
+ void install();
+
+private:
+ UserInteractionSupport *m_support;
+
+ bool abortable = false;
+
+ NetJobPtr jobPtr;
+ QByteArray response;
+
+ QString m_pack;
+ QString m_version_name;
+ PackVersion m_version;
+
+ QMap<QString, VersionMod> modsToExtract;
+ QMap<QString, VersionMod> modsToDecomp;
+ QMap<QString, QString> modsToCopy;
+
+ QString archivePath;
+ QStringList jarmods;
+ Meta::VersionPtr minecraftVersion;
+ QMap<QString, Meta::VersionPtr> componentsToInstall;
+
+ QFuture<nonstd::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
+
+ QFuture<bool> m_modExtractFuture;
+ QFutureWatcher<bool> m_modExtractFutureWatcher;
+
+};
+
+}
diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/launcher/modplatform/atlauncher/ATLPackManifest.cpp
new file mode 100644
index 00000000..e25d8346
--- /dev/null
+++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp
@@ -0,0 +1,218 @@
+#include "ATLPackManifest.h"
+
+#include "Json.h"
+
+static ATLauncher::DownloadType parseDownloadType(QString rawType) {
+ if(rawType == QString("server")) {
+ return ATLauncher::DownloadType::Server;
+ }
+ else if(rawType == QString("browser")) {
+ return ATLauncher::DownloadType::Browser;
+ }
+ else if(rawType == QString("direct")) {
+ return ATLauncher::DownloadType::Direct;
+ }
+
+ return ATLauncher::DownloadType::Unknown;
+}
+
+static ATLauncher::ModType parseModType(QString rawType) {
+ // See https://wiki.atlauncher.com/mod_types
+ if(rawType == QString("root")) {
+ return ATLauncher::ModType::Root;
+ }
+ else if(rawType == QString("forge")) {
+ return ATLauncher::ModType::Forge;
+ }
+ else if(rawType == QString("jar")) {
+ return ATLauncher::ModType::Jar;
+ }
+ else if(rawType == QString("mods")) {
+ return ATLauncher::ModType::Mods;
+ }
+ else if(rawType == QString("flan")) {
+ return ATLauncher::ModType::Flan;
+ }
+ else if(rawType == QString("dependency") || rawType == QString("depandency")) {
+ return ATLauncher::ModType::Dependency;
+ }
+ else if(rawType == QString("ic2lib")) {
+ return ATLauncher::ModType::Ic2Lib;
+ }
+ else if(rawType == QString("denlib")) {
+ return ATLauncher::ModType::DenLib;
+ }
+ else if(rawType == QString("coremods")) {
+ return ATLauncher::ModType::Coremods;
+ }
+ else if(rawType == QString("mcpc")) {
+ return ATLauncher::ModType::MCPC;
+ }
+ else if(rawType == QString("plugins")) {
+ return ATLauncher::ModType::Plugins;
+ }
+ else if(rawType == QString("extract")) {
+ return ATLauncher::ModType::Extract;
+ }
+ else if(rawType == QString("decomp")) {
+ return ATLauncher::ModType::Decomp;
+ }
+ else if(rawType == QString("texturepack")) {
+ return ATLauncher::ModType::TexturePack;
+ }
+ else if(rawType == QString("resourcepack")) {
+ return ATLauncher::ModType::ResourcePack;
+ }
+ else if(rawType == QString("shaderpack")) {
+ return ATLauncher::ModType::ShaderPack;
+ }
+ else if(rawType == QString("texturepackextract")) {
+ return ATLauncher::ModType::TexturePackExtract;
+ }
+ else if(rawType == QString("resourcepackextract")) {
+ return ATLauncher::ModType::ResourcePackExtract;
+ }
+ else if(rawType == QString("millenaire")) {
+ return ATLauncher::ModType::Millenaire;
+ }
+
+ return ATLauncher::ModType::Unknown;
+}
+
+static void loadVersionLoader(ATLauncher::VersionLoader & p, QJsonObject & obj) {
+ p.type = Json::requireString(obj, "type");
+ p.choose = Json::ensureBoolean(obj, QString("choose"), false);
+
+ auto metadata = Json::requireObject(obj, "metadata");
+ p.latest = Json::ensureBoolean(metadata, QString("latest"), false);
+ p.recommended = Json::ensureBoolean(metadata, QString("recommended"), false);
+
+ // Minecraft Forge
+ if (p.type == "forge") {
+ p.version = Json::ensureString(metadata, "version", "");
+ }
+
+ // Fabric Loader
+ if (p.type == "fabric") {
+ p.version = Json::ensureString(metadata, "loader", "");
+ }
+}
+
+static void loadVersionLibrary(ATLauncher::VersionLibrary & p, QJsonObject & obj) {
+ p.url = Json::requireString(obj, "url");
+ p.file = Json::requireString(obj, "file");
+ p.md5 = Json::requireString(obj, "md5");
+
+ p.download_raw = Json::requireString(obj, "download");
+ p.download = parseDownloadType(p.download_raw);
+
+ p.server = Json::ensureString(obj, "server", "");
+}
+
+static void loadVersionConfigs(ATLauncher::VersionConfigs & p, QJsonObject & obj) {
+ p.filesize = Json::requireInteger(obj, "filesize");
+ p.sha1 = Json::requireString(obj, "sha1");
+}
+
+static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) {
+ p.name = Json::requireString(obj, "name");
+ p.version = Json::requireString(obj, "version");
+ p.url = Json::requireString(obj, "url");
+ p.file = Json::requireString(obj, "file");
+ p.md5 = Json::ensureString(obj, "md5", "");
+
+ p.download_raw = Json::requireString(obj, "download");
+ p.download = parseDownloadType(p.download_raw);
+
+ p.type_raw = Json::requireString(obj, "type");
+ p.type = parseModType(p.type_raw);
+
+ // This contributes to the Minecraft Forge detection, where we rely on mod.type being "Forge"
+ // when the mod represents Forge. As there is little difference between "Jar" and "Forge, some
+ // packs regretfully use "Jar". This will correct the type to "Forge" in these cases (as best
+ // it can).
+ if(p.name == QString("Minecraft Forge") && p.type == ATLauncher::ModType::Jar) {
+ p.type_raw = "forge";
+ p.type = ATLauncher::ModType::Forge;
+ }
+
+ if(obj.contains("extractTo")) {
+ p.extractTo_raw = Json::requireString(obj, "extractTo");
+ p.extractTo = parseModType(p.extractTo_raw);
+ p.extractFolder = Json::ensureString(obj, "extractFolder", "").replace("%s%", "/");
+ }
+
+ if(obj.contains("decompType")) {
+ p.decompType_raw = Json::requireString(obj, "decompType");
+ p.decompType = parseModType(p.decompType_raw);
+ p.decompFile = Json::requireString(obj, "decompFile");
+ }
+
+ p.description = Json::ensureString(obj, QString("description"), "");
+ p.optional = Json::ensureBoolean(obj, QString("optional"), false);
+ p.recommended = Json::ensureBoolean(obj, QString("recommended"), false);
+ p.selected = Json::ensureBoolean(obj, QString("selected"), false);
+ p.hidden = Json::ensureBoolean(obj, QString("hidden"), false);
+ p.library = Json::ensureBoolean(obj, QString("library"), false);
+ p.group = Json::ensureString(obj, QString("group"), "");
+ if(obj.contains("depends")) {
+ auto dependsArr = Json::requireArray(obj, "depends");
+ for (const auto depends : dependsArr) {
+ p.depends.append(Json::requireString(depends));
+ }
+ }
+
+ p.client = Json::ensureBoolean(obj, QString("client"), false);
+
+ // computed
+ p.effectively_hidden = p.hidden || p.library;
+}
+
+void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj)
+{
+ v.version = Json::requireString(obj, "version");
+ v.minecraft = Json::requireString(obj, "minecraft");
+ v.noConfigs = Json::ensureBoolean(obj, QString("noConfigs"), false);
+
+ if(obj.contains("mainClass")) {
+ auto main = Json::requireObject(obj, "mainClass");
+ v.mainClass = Json::ensureString(main, "mainClass", "");
+ }
+
+ if(obj.contains("extraArguments")) {
+ auto arguments = Json::requireObject(obj, "extraArguments");
+ v.extraArguments = Json::ensureString(arguments, "arguments", "");
+ }
+
+ if(obj.contains("loader")) {
+ auto loader = Json::requireObject(obj, "loader");
+ loadVersionLoader(v.loader, loader);
+ }
+
+ if(obj.contains("libraries")) {
+ auto libraries = Json::requireArray(obj, "libraries");
+ for (const auto libraryRaw : libraries)
+ {
+ auto libraryObj = Json::requireObject(libraryRaw);
+ ATLauncher::VersionLibrary target;
+ loadVersionLibrary(target, libraryObj);
+ v.libraries.append(target);
+ }
+ }
+
+ if(obj.contains("mods")) {
+ auto mods = Json::requireArray(obj, "mods");
+ for (const auto modRaw : mods)
+ {
+ auto modObj = Json::requireObject(modRaw);
+ ATLauncher::VersionMod mod;
+ loadVersionMod(mod, modObj);
+ v.mods.append(mod);
+ }
+ }
+
+ if(obj.contains("configs")) {
+ auto configsObj = Json::requireObject(obj, "configs");
+ loadVersionConfigs(v.configs, configsObj);
+ }
+}
diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h
new file mode 100644
index 00000000..ead216a5
--- /dev/null
+++ b/launcher/modplatform/atlauncher/ATLPackManifest.h
@@ -0,0 +1,125 @@
+#pragma once
+
+#include <QString>
+#include <QVector>
+#include <QJsonObject>
+
+namespace ATLauncher
+{
+
+enum class PackType
+{
+ Public,
+ Private
+};
+
+enum class ModType
+{
+ Root,
+ Forge,
+ Jar,
+ Mods,
+ Flan,
+ Dependency,
+ Ic2Lib,
+ DenLib,
+ Coremods,
+ MCPC,
+ Plugins,
+ Extract,
+ Decomp,
+ TexturePack,
+ ResourcePack,
+ ShaderPack,
+ TexturePackExtract,
+ ResourcePackExtract,
+ Millenaire,
+ Unknown
+};
+
+enum class DownloadType
+{
+ Server,
+ Browser,
+ Direct,
+ Unknown
+};
+
+struct VersionLoader
+{
+ QString type;
+ bool latest;
+ bool recommended;
+ bool choose;
+
+ QString version;
+};
+
+struct VersionLibrary
+{
+ QString url;
+ QString file;
+ QString server;
+ QString md5;
+ DownloadType download;
+ QString download_raw;
+};
+
+struct VersionMod
+{
+ QString name;
+ QString version;
+ QString url;
+ QString file;
+ QString md5;
+ DownloadType download;
+ QString download_raw;
+ ModType type;
+ QString type_raw;
+
+ ModType extractTo;
+ QString extractTo_raw;
+ QString extractFolder;
+
+ ModType decompType;
+ QString decompType_raw;
+ QString decompFile;
+
+ QString description;
+ bool optional;
+ bool recommended;
+ bool selected;
+ bool hidden;
+ bool library;
+ QString group;
+ QVector<QString> depends;
+
+ bool client;
+
+ // computed
+ bool effectively_hidden;
+};
+
+struct VersionConfigs
+{
+ int filesize;
+ QString sha1;
+};
+
+struct PackVersion
+{
+ QString version;
+ QString minecraft;
+ bool noConfigs;
+ QString mainClass;
+ QString extraArguments;
+
+ VersionLoader loader;
+ QVector<VersionLibrary> libraries;
+ QVector<VersionMod> mods;
+ VersionConfigs configs;
+};
+
+void loadVersion(PackVersion & v, QJsonObject & obj);
+
+}
diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp
new file mode 100644
index 00000000..295574f0
--- /dev/null
+++ b/launcher/modplatform/flame/FileResolvingTask.cpp
@@ -0,0 +1,63 @@
+#include "FileResolvingTask.h"
+#include "Json.h"
+
+namespace {
+ const char * metabase = "https://cursemeta.dries007.net";
+}
+
+Flame::FileResolvingTask::FileResolvingTask(Flame::Manifest& toProcess)
+ : m_toProcess(toProcess)
+{
+}
+
+void Flame::FileResolvingTask::executeTask()
+{
+ setStatus(tr("Resolving mod IDs..."));
+ setProgress(0, m_toProcess.files.size());
+ m_dljob.reset(new NetJob("Mod id resolver"));
+ results.resize(m_toProcess.files.size());
+ int index = 0;
+ for(auto & file: m_toProcess.files)
+ {
+ auto projectIdStr = QString::number(file.projectId);
+ auto fileIdStr = QString::number(file.fileId);
+ QString metaurl = QString("%1/%2/%3.json").arg(metabase, projectIdStr, fileIdStr);
+ auto dl = Net::Download::makeByteArray(QUrl(metaurl), &results[index]);
+ m_dljob->addNetAction(dl);
+ index ++;
+ }
+ connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished);
+ m_dljob->start();
+}
+
+void Flame::FileResolvingTask::netJobFinished()
+{
+ bool failed = false;
+ int index = 0;
+ for(auto & bytes: results)
+ {
+ auto & out = m_toProcess.files[index];
+ try
+ {
+ failed &= (!out.parseFromBytes(bytes));
+ }
+ catch (const JSONValidationError &e)
+ {
+
+ qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:";
+ qCritical() << e.cause();
+ qCritical() << "JSON:";
+ qCritical() << bytes;
+ failed = true;
+ }
+ index++;
+ }
+ if(!failed)
+ {
+ emitSucceeded();
+ }
+ else
+ {
+ emitFailed(tr("Some mod ID resolving tasks failed."));
+ }
+}
diff --git a/launcher/modplatform/flame/FileResolvingTask.h b/launcher/modplatform/flame/FileResolvingTask.h
new file mode 100644
index 00000000..78a38fcb
--- /dev/null
+++ b/launcher/modplatform/flame/FileResolvingTask.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "tasks/Task.h"
+#include "net/NetJob.h"
+#include "PackManifest.h"
+
+namespace Flame
+{
+class FileResolvingTask : public Task
+{
+ Q_OBJECT
+public:
+ explicit FileResolvingTask(Flame::Manifest &toProcess);
+ virtual ~FileResolvingTask() {};
+
+ const Flame::Manifest &getResults() const
+ {
+ return m_toProcess;
+ }
+
+protected:
+ virtual void executeTask() override;
+
+protected slots:
+ void netJobFinished();
+
+private: /* data */
+ Flame::Manifest m_toProcess;
+ QVector<QByteArray> results;
+ NetJobPtr m_dljob;
+};
+}
diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp
new file mode 100644
index 00000000..3d8ea22a
--- /dev/null
+++ b/launcher/modplatform/flame/FlamePackIndex.cpp
@@ -0,0 +1,92 @@
+#include "FlamePackIndex.h"
+
+#include "Json.h"
+
+void Flame::loadIndexedPack(Flame::IndexedPack & pack, QJsonObject & obj)
+{
+ pack.addonId = Json::requireInteger(obj, "id");
+ pack.name = Json::requireString(obj, "name");
+ pack.websiteUrl = Json::ensureString(obj, "websiteUrl", "");
+ pack.description = Json::ensureString(obj, "summary", "");
+
+ bool thumbnailFound = false;
+ auto attachments = Json::requireArray(obj, "attachments");
+ for(auto attachmentRaw: attachments) {
+ auto attachmentObj = Json::requireObject(attachmentRaw);
+ bool isDefault = attachmentObj.value("isDefault").toBool(false);
+ if(isDefault) {
+ thumbnailFound = true;
+ pack.logoName = Json::requireString(attachmentObj, "title");
+ pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl");
+ break;
+ }
+ }
+
+ if(!thumbnailFound) {
+ throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name));
+ }
+
+ auto authors = Json::requireArray(obj, "authors");
+ for(auto authorIter: authors) {
+ auto author = Json::requireObject(authorIter);
+ Flame::ModpackAuthor packAuthor;
+ packAuthor.name = Json::requireString(author, "name");
+ packAuthor.url = Json::requireString(author, "url");
+ pack.authors.append(packAuthor);
+ }
+ int defaultFileId = Json::requireInteger(obj, "defaultFileId");
+
+ bool found = false;
+ // check if there are some files before adding the pack
+ auto files = Json::requireArray(obj, "latestFiles");
+ for(auto fileIter: files) {
+ auto file = Json::requireObject(fileIter);
+ int id = Json::requireInteger(file, "id");
+
+ // NOTE: for now, ignore everything that's not the default...
+ if(id != defaultFileId) {
+ continue;
+ }
+
+ auto versionArray = Json::requireArray(file, "gameVersion");
+ if(versionArray.size() < 1) {
+ continue;
+ }
+
+ found = true;
+ break;
+ }
+ if(!found) {
+ throw JSONValidationError(QString("Pack with no good file, skipping: %1").arg(pack.name));
+ }
+}
+
+void Flame::loadIndexedPackVersions(Flame::IndexedPack & pack, QJsonArray & arr)
+{
+ QVector<Flame::IndexedVersion> unsortedVersions;
+ for(auto versionIter: arr) {
+ auto version = Json::requireObject(versionIter);
+ Flame::IndexedVersion file;
+
+ file.addonId = pack.addonId;
+ file.fileId = Json::requireInteger(version, "id");
+ auto versionArray = Json::requireArray(version, "gameVersion");
+ if(versionArray.size() < 1) {
+ continue;
+ }
+
+ // pick the latest version supported
+ file.mcVersion = versionArray[0].toString();
+ file.version = Json::requireString(version, "displayName");
+ file.downloadUrl = Json::requireString(version, "downloadUrl");
+ unsortedVersions.append(file);
+ }
+
+ auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool
+ {
+ return a.fileId > b.fileId;
+ };
+ std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);
+ pack.versions = unsortedVersions;
+ pack.versionsLoaded = true;
+}
diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h
new file mode 100644
index 00000000..7ffa29c3
--- /dev/null
+++ b/launcher/modplatform/flame/FlamePackIndex.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include <QList>
+#include <QMetaType>
+#include <QString>
+#include <QVector>
+
+namespace Flame {
+
+struct ModpackAuthor {
+ QString name;
+ QString url;
+};
+
+struct IndexedVersion {
+ int addonId;
+ int fileId;
+ QString version;
+ QString mcVersion;
+ QString downloadUrl;
+};
+
+struct IndexedPack
+{
+ int addonId;
+ QString name;
+ QString description;
+ QList<ModpackAuthor> authors;
+ QString logoName;
+ QString logoUrl;
+ QString websiteUrl;
+
+ bool versionsLoaded = false;
+ QVector<IndexedVersion> versions;
+};
+
+void loadIndexedPack(IndexedPack & m, QJsonObject & obj);
+void loadIndexedPackVersions(IndexedPack & m, QJsonArray & arr);
+}
+
+Q_DECLARE_METATYPE(Flame::IndexedPack)
diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp
new file mode 100644
index 00000000..b928fd16
--- /dev/null
+++ b/launcher/modplatform/flame/PackManifest.cpp
@@ -0,0 +1,126 @@
+#include "PackManifest.h"
+#include "Json.h"
+
+static void loadFileV1(Flame::File & f, QJsonObject & file)
+{
+ f.projectId = Json::requireInteger(file, "projectID");
+ f.fileId = Json::requireInteger(file, "fileID");
+ f.required = Json::ensureBoolean(file, QString("required"), true);
+}
+
+static void loadModloaderV1(Flame::Modloader & m, QJsonObject & modLoader)
+{
+ m.id = Json::requireString(modLoader, "id");
+ m.primary = Json::ensureBoolean(modLoader, QString("primary"), false);
+}
+
+static void loadMinecraftV1(Flame::Minecraft & m, QJsonObject & minecraft)
+{
+ m.version = Json::requireString(minecraft, "version");
+ // extra libraries... apparently only used for a custom Minecraft launcher in the 1.2.5 FTB retro pack
+ // intended use is likely hardcoded in the 'Flame' client, the manifest says nothing
+ m.libraries = Json::ensureString(minecraft, QString("libraries"), QString());
+ auto arr = Json::ensureArray(minecraft, "modLoaders", QJsonArray());
+ for (QJsonValueRef item : arr)
+ {
+ auto obj = Json::requireObject(item);
+ Flame::Modloader loader;
+ loadModloaderV1(loader, obj);
+ m.modLoaders.append(loader);
+ }
+}
+
+static void loadManifestV1(Flame::Manifest & m, QJsonObject & manifest)
+{
+ auto mc = Json::requireObject(manifest, "minecraft");
+ loadMinecraftV1(m.minecraft, mc);
+ m.name = Json::ensureString(manifest, QString("name"), "Unnamed");
+ m.version = Json::ensureString(manifest, QString("version"), QString());
+ m.author = Json::ensureString(manifest, QString("author"), "Anonymous Coward");
+ auto arr = Json::ensureArray(manifest, "files", QJsonArray());
+ for (QJsonValueRef item : arr)
+ {
+ auto obj = Json::requireObject(item);
+ Flame::File file;
+ loadFileV1(file, obj);
+ m.files.append(file);
+ }
+ m.overrides = Json::ensureString(manifest, "overrides", "overrides");
+}
+
+void Flame::loadManifest(Flame::Manifest & m, const QString &filepath)
+{
+ auto doc = Json::requireDocument(filepath);
+ auto obj = Json::requireObject(doc);
+ m.manifestType = Json::requireString(obj, "manifestType");
+ if(m.manifestType != "minecraftModpack")
+ {
+ throw JSONValidationError("Not a modpack manifest!");
+ }
+ m.manifestVersion = Json::requireInteger(obj, "manifestVersion");
+ if(m.manifestVersion != 1)
+ {
+ throw JSONValidationError(QString("Unknown manifest version (%1)").arg(m.manifestVersion));
+ }
+ loadManifestV1(m, obj);
+}
+
+bool Flame::File::parseFromBytes(const QByteArray& bytes)
+{
+ auto doc = Json::requireDocument(bytes);
+ auto obj = Json::requireObject(doc);
+ // result code signifies true failure.
+ if(obj.contains("code"))
+ {
+ qCritical() << "Resolving of" << projectId << fileId << "failed because of a negative result:";
+ qCritical() << bytes;
+ return false;
+ }
+ fileName = Json::requireString(obj, "FileNameOnDisk");
+ QString rawUrl = Json::requireString(obj, "DownloadURL");
+ url = QUrl(rawUrl, QUrl::TolerantMode);
+ if(!url.isValid())
+ {
+ throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl));
+ }
+ // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience
+ // It is also optional
+ QJsonObject projObj = Json::ensureObject(obj, "_Project", {});
+ if(!projObj.isEmpty())
+ {
+ QString strType = Json::ensureString(projObj, "PackageType", "mod").toLower();
+ if(strType == "singlefile")
+ {
+ type = File::Type::SingleFile;
+ }
+ else if(strType == "ctoc")
+ {
+ type = File::Type::Ctoc;
+ }
+ else if(strType == "cmod2")
+ {
+ type = File::Type::Cmod2;
+ }
+ else if(strType == "mod")
+ {
+ type = File::Type::Mod;
+ }
+ else if(strType == "folder")
+ {
+ type = File::Type::Folder;
+ }
+ else if(strType == "modpack")
+ {
+ type = File::Type::Modpack;
+ }
+ else
+ {
+ qCritical() << "Resolving of" << projectId << fileId << "failed because of unknown file type:" << strType;
+ type = File::Type::Unknown;
+ return false;
+ }
+ targetFolder = Json::ensureString(projObj, "Path", "mods");
+ }
+ resolved = true;
+ return true;
+}
diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h
new file mode 100644
index 00000000..02f39f0e
--- /dev/null
+++ b/launcher/modplatform/flame/PackManifest.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include <QString>
+#include <QVector>
+#include <QUrl>
+
+namespace Flame
+{
+struct File
+{
+ // NOTE: throws JSONValidationError
+ bool parseFromBytes(const QByteArray &bytes);
+
+ int projectId = 0;
+ int fileId = 0;
+ // NOTE: the opposite to 'optional'. This is at the time of writing unused.
+ bool required = true;
+
+ // our
+ bool resolved = false;
+ QString fileName;
+ QUrl url;
+ QString targetFolder = QLatin1Literal("mods");
+ enum class Type
+ {
+ Unknown,
+ Folder,
+ Ctoc,
+ SingleFile,
+ Cmod2,
+ Modpack,
+ Mod
+ } type = Type::Mod;
+};
+
+struct Modloader
+{
+ QString id;
+ bool primary = false;
+};
+
+struct Minecraft
+{
+ QString version;
+ QString libraries;
+ QVector<Flame::Modloader> modLoaders;
+};
+
+struct Manifest
+{
+ QString manifestType;
+ int manifestVersion = 0;
+ Flame::Minecraft minecraft;
+ QString name;
+ QString version;
+ QString author;
+ QVector<Flame::File> files;
+ QString overrides;
+};
+
+void loadManifest(Flame::Manifest & m, const QString &filepath);
+}
diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp
new file mode 100644
index 00000000..c2ef6436
--- /dev/null
+++ b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp
@@ -0,0 +1,172 @@
+#include "PackFetchTask.h"
+#include "PrivatePackManager.h"
+
+#include <QDomDocument>
+#include <BuildConfig.h>
+
+namespace LegacyFTB {
+
+void PackFetchTask::fetch()
+{
+ publicPacks.clear();
+ thirdPartyPacks.clear();
+
+ NetJob *netJob = new NetJob("LegacyFTB::ModpackFetch");
+
+ QUrl publicPacksUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/modpacks.xml");
+ qDebug() << "Downloading public version info from" << publicPacksUrl.toString();
+ netJob->addNetAction(Net::Download::makeByteArray(publicPacksUrl, &publicModpacksXmlFileData));
+
+ QUrl thirdPartyUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/thirdparty.xml");
+ qDebug() << "Downloading thirdparty version info from" << thirdPartyUrl.toString();
+ netJob->addNetAction(Net::Download::makeByteArray(thirdPartyUrl, &thirdPartyModpacksXmlFileData));
+
+ QObject::connect(netJob, &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished);
+ QObject::connect(netJob, &NetJob::failed, this, &PackFetchTask::fileDownloadFailed);
+
+ jobPtr.reset(netJob);
+ netJob->start();
+}
+
+void PackFetchTask::fetchPrivate(const QStringList & toFetch)
+{
+ QString privatePackBaseUrl = BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1.xml";
+
+ for (auto &packCode: toFetch)
+ {
+ QByteArray *data = new QByteArray();
+ NetJob *job = new NetJob("Fetching private pack");
+ job->addNetAction(Net::Download::makeByteArray(privatePackBaseUrl.arg(packCode), data));
+
+ QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode]
+ {
+ ModpackList packs;
+ parseAndAddPacks(*data, PackType::Private, packs);
+ foreach(Modpack currentPack, packs)
+ {
+ currentPack.packCode = packCode;
+ emit privateFileDownloadFinished(currentPack);
+ }
+
+ job->deleteLater();
+
+ data->clear();
+ delete data;
+ });
+
+ QObject::connect(job, &NetJob::failed, this, [this, job, packCode, data](QString reason)
+ {
+ emit privateFileDownloadFailed(reason, packCode);
+ job->deleteLater();
+
+ data->clear();
+ delete data;
+ });
+
+ job->start();
+ }
+}
+
+void PackFetchTask::fileDownloadFinished()
+{
+ jobPtr.reset();
+
+ QStringList failedLists;
+
+ if(!parseAndAddPacks(publicModpacksXmlFileData, PackType::Public, publicPacks))
+ {
+ failedLists.append(tr("Public Packs"));
+ }
+
+ if(!parseAndAddPacks(thirdPartyModpacksXmlFileData, PackType::ThirdParty, thirdPartyPacks))
+ {
+ failedLists.append(tr("Third Party Packs"));
+ }
+
+ if(failedLists.size() > 0)
+ {
+ emit failed(tr("Failed to download some pack lists: %1").arg(failedLists.join("\n- ")));
+ }
+ else
+ {
+ emit finished(publicPacks, thirdPartyPacks);
+ }
+}
+
+bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, ModpackList &list)
+{
+ QDomDocument doc;
+
+ QString errorMsg = "Unknown error.";
+ int errorLine = -1;
+ int errorCol = -1;
+
+ if(!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol))
+ {
+ auto fullErrMsg = QString("Failed to fetch modpack data: %1 %2:3d!").arg(errorMsg, errorLine, errorCol);
+ qWarning() << fullErrMsg;
+ data.clear();
+ return false;
+ }
+
+ QDomNodeList nodes = doc.elementsByTagName("modpack");
+ for(int i = 0; i < nodes.length(); i++)
+ {
+ QDomElement element = nodes.at(i).toElement();
+
+ Modpack modpack;
+ modpack.name = element.attribute("name");
+ modpack.currentVersion = element.attribute("version");
+ modpack.mcVersion = element.attribute("mcVersion");
+ modpack.description = element.attribute("description");
+ modpack.mods = element.attribute("mods");
+ modpack.logo = element.attribute("logo");
+ modpack.oldVersions = element.attribute("oldVersions").split(";");
+ modpack.broken = false;
+ modpack.bugged = false;
+
+ //remove empty if the xml is bugged
+ for(QString curr : modpack.oldVersions)
+ {
+ if(curr.isNull() || curr.isEmpty())
+ {
+ modpack.oldVersions.removeAll(curr);
+ modpack.bugged = true;
+ qWarning() << "Removed some empty versions from" << modpack.name;
+ }
+ }
+
+ if(modpack.oldVersions.size() < 1)
+ {
+ if(!modpack.currentVersion.isNull() && !modpack.currentVersion.isEmpty())
+ {
+ modpack.oldVersions.append(modpack.currentVersion);
+ qWarning() << "Added current version to oldVersions because oldVersions was empty! (" + modpack.name + ")";
+ }
+ else
+ {
+ modpack.broken = true;
+ qWarning() << "Broken pack:" << modpack.name << " => No valid version!";
+ }
+ }
+
+ modpack.author = element.attribute("author");
+
+ modpack.dir = element.attribute("dir");
+ modpack.file = element.attribute("url");
+
+ modpack.type = packType;
+
+ list.append(modpack);
+ }
+
+ return true;
+}
+
+void PackFetchTask::fileDownloadFailed(QString reason)
+{
+ qWarning() << "Fetching FTBPacks failed:" << reason;
+ emit failed(reason);
+}
+
+}
diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.h b/launcher/modplatform/legacy_ftb/PackFetchTask.h
new file mode 100644
index 00000000..3ab32fab
--- /dev/null
+++ b/launcher/modplatform/legacy_ftb/PackFetchTask.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "net/NetJob.h"
+#include <QTemporaryDir>
+#include <QByteArray>
+#include <QObject>
+#include "PackHelpers.h"
+
+namespace LegacyFTB {
+
+class PackFetchTask : public QObject {
+
+ Q_OBJECT
+
+public:
+ PackFetchTask() = default;
+ virtual ~PackFetchTask() = default;
+
+ void fetch();
+ void fetchPrivate(const QStringList &toFetch);
+
+private:
+ NetJobPtr jobPtr;
+
+ QByteArray publicModpacksXmlFileData;
+ QByteArray thirdPartyModpacksXmlFileData;
+
+ bool parseAndAddPacks(QByteArray &data, PackType packType, ModpackList &list);
+ ModpackList publicPacks;
+ ModpackList thirdPartyPacks;
+
+protected slots:
+ void fileDownloadFinished();
+ void fileDownloadFailed(QString reason);
+
+signals:
+ void finished(ModpackList publicPacks, ModpackList thirdPartyPacks);
+ void failed(QString reason);
+
+ void privateFileDownloadFinished(Modpack modpack);
+ void privateFileDownloadFailed(QString reason, QString packCode);
+};
+
+}
diff --git a/launcher/modplatform/legacy_ftb/PackHelpers.h b/launcher/modplatform/legacy_ftb/PackHelpers.h
new file mode 100644
index 00000000..566210d0
--- /dev/null
+++ b/launcher/modplatform/legacy_ftb/PackHelpers.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include <QList>
+#include <QString>
+#include <QStringList>
+#include <QMetaType>
+
+namespace LegacyFTB {
+
+//Header for structs etc...
+enum class PackType
+{
+ Public,
+ ThirdParty,
+ Private
+};
+
+struct Modpack
+{
+ QString name;
+ QString description;
+ QString author;
+ QStringList oldVersions;
+ QString currentVersion;
+ QString mcVersion;
+ QString mods;
+ QString logo;
+
+ //Technical data
+ QString dir;
+ QString file; //<- Url in the xml, but doesn't make much sense
+
+ bool bugged = false;
+ bool broken = false;
+
+ PackType type;
+ QString packCode;
+};
+
+typedef QList<Modpack> ModpackList;
+
+}
+
+//We need it for the proxy model
+Q_DECLARE_METATYPE(LegacyFTB::Modpack)
diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
new file mode 100644
index 00000000..c77f3250
--- /dev/null
+++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
@@ -0,0 +1,214 @@
+#include "PackInstallTask.h"
+
+#include "Env.h"
+#include "MMCZip.h"
+
+#include "BaseInstance.h"
+#include "FileSystem.h"
+#include "settings/INISettingsObject.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "minecraft/GradleSpecifier.h"
+#include "BuildConfig.h"
+
+#include <QtConcurrent>
+
+namespace LegacyFTB {
+
+PackInstallTask::PackInstallTask(Modpack pack, QString version)
+{
+ m_pack = pack;
+ m_version = version;
+}
+
+void PackInstallTask::executeTask()
+{
+ downloadPack();
+}
+
+void PackInstallTask::downloadPack()
+{
+ setStatus(tr("Downloading zip for %1").arg(m_pack.name));
+
+ auto packoffset = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file);
+ auto entry = ENV.metacache()->resolveEntry("FTBPacks", packoffset);
+ NetJob *job = new NetJob("Download FTB Pack");
+
+ entry->setStale(true);
+ QString url;
+ if(m_pack.type == PackType::Private)
+ {
+ url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "privatepacks/%1").arg(packoffset);
+ }
+ else
+ {
+ url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "modpacks/%1").arg(packoffset);
+ }
+ job->addNetAction(Net::Download::makeCached(url, entry));
+ archivePath = entry->getFullPath();
+
+ netJobContainer.reset(job);
+ connect(job, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded);
+ connect(job, &NetJob::failed, this, &PackInstallTask::onDownloadFailed);
+ connect(job, &NetJob::progress, this, &PackInstallTask::onDownloadProgress);
+ job->start();
+
+ progress(1, 4);
+}
+
+void PackInstallTask::onDownloadSucceeded()
+{
+ abortable = false;
+ unzip();
+}
+
+void PackInstallTask::onDownloadFailed(QString reason)
+{
+ abortable = false;
+ emitFailed(reason);
+}
+
+void PackInstallTask::onDownloadProgress(qint64 current, qint64 total)
+{
+ abortable = true;
+ progress(current, total * 4);
+ setStatus(tr("Downloading zip for %1 (%2%)").arg(m_pack.name).arg(current / 10));
+}
+
+void PackInstallTask::unzip()
+{
+ progress(2, 4);
+ setStatus(tr("Extracting modpack"));
+ QDir extractDir(m_stagingPath);
+
+ m_packZip.reset(new QuaZip(archivePath));
+ if(!m_packZip->open(QuaZip::mdUnzip))
+ {
+ emitFailed(tr("Failed to open modpack file %1!").arg(archivePath));
+ return;
+ }
+
+ m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip");
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &PackInstallTask::onUnzipFinished);
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &PackInstallTask::onUnzipCanceled);
+ m_extractFutureWatcher.setFuture(m_extractFuture);
+}
+
+void PackInstallTask::onUnzipFinished()
+{
+ install();
+}
+
+void PackInstallTask::onUnzipCanceled()
+{
+ emitAborted();
+}
+
+void PackInstallTask::install()
+{
+ progress(3, 4);
+ setStatus(tr("Installing modpack"));
+ QDir unzipMcDir(m_stagingPath + "/unzip/minecraft");
+ if(unzipMcDir.exists())
+ {
+ //ok, found minecraft dir, move contents to instance dir
+ if(!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/.minecraft"))
+ {
+ emitFailed(tr("Failed to move unzipped minecraft!"));
+ return;
+ }
+ }
+
+ QString instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg");
+ auto instanceSettings = std::make_shared<INISettingsObject>(instanceConfigPath);
+ instanceSettings->suspendSave();
+ 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", m_pack.mcVersion, true);
+
+ bool fallback = true;
+
+ //handle different versions
+ QFile packJson(m_stagingPath + "/.minecraft/pack.json");
+ QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods");
+ if(packJson.exists())
+ {
+ packJson.open(QIODevice::ReadOnly | QIODevice::Text);
+ QJsonDocument doc = QJsonDocument::fromJson(packJson.readAll());
+ packJson.close();
+
+ //we only care about the libs
+ QJsonArray libs = doc.object().value("libraries").toArray();
+
+ foreach (const QJsonValue &value, libs)
+ {
+ QString nameValue = value.toObject().value("name").toString();
+ if(!nameValue.startsWith("net.minecraftforge"))
+ {
+ continue;
+ }
+
+ GradleSpecifier forgeVersion(nameValue);
+
+ components->setComponentVersion("net.minecraftforge", forgeVersion.version().replace(m_pack.mcVersion, "").replace("-", ""));
+ packJson.remove();
+ fallback = false;
+ break;
+ }
+
+ }
+
+ if(jarmodDir.exists())
+ {
+ qDebug() << "Found jarmods, installing...";
+
+ QStringList jarmods;
+ for (auto info: jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files))
+ {
+ qDebug() << "Jarmod:" << info.fileName();
+ jarmods.push_back(info.absoluteFilePath());
+ }
+
+ components->installJarMods(jarmods);
+ fallback = false;
+ }
+
+ //just nuke unzip directory, it s not needed anymore
+ FS::deletePath(m_stagingPath + "/unzip");
+
+ if(fallback)
+ {
+ //TODO: Some fallback mechanism... or just keep failing!
+ emitFailed(tr("No installation method found!"));
+ return;
+ }
+
+ components->saveNow();
+
+ progress(4, 4);
+
+ instance.setName(m_instName);
+ if(m_instIcon == "default")
+ {
+ m_instIcon = "ftb_logo";
+ }
+ instance.setIconKey(m_instIcon);
+ instanceSettings->resumeSave();
+
+ emitSucceeded();
+}
+
+bool PackInstallTask::abort()
+{
+ if(abortable)
+ {
+ return netJobContainer->abort();
+ }
+ return false;
+}
+
+}
diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.h b/launcher/modplatform/legacy_ftb/PackInstallTask.h
new file mode 100644
index 00000000..600f72e7
--- /dev/null
+++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h
@@ -0,0 +1,55 @@
+#pragma once
+#include "InstanceTask.h"
+#include "net/NetJob.h"
+#include "quazip.h"
+#include "quazipdir.h"
+#include "meta/Index.h"
+#include "meta/Version.h"
+#include "meta/VersionList.h"
+#include "PackHelpers.h"
+
+#include <nonstd/optional>
+
+namespace LegacyFTB {
+
+class PackInstallTask : public InstanceTask
+{
+ Q_OBJECT
+
+public:
+ explicit PackInstallTask(Modpack pack, QString version);
+ virtual ~PackInstallTask(){}
+
+ bool canAbort() const override { return true; }
+ bool abort() override;
+
+protected:
+ //! Entry point for tasks.
+ virtual void executeTask() override;
+
+private:
+ void downloadPack();
+ void unzip();
+ void install();
+
+private slots:
+ void onDownloadSucceeded();
+ void onDownloadFailed(QString reason);
+ void onDownloadProgress(qint64 current, qint64 total);
+
+ void onUnzipFinished();
+ void onUnzipCanceled();
+
+private: /* data */
+ bool abortable = false;
+ std::unique_ptr<QuaZip> m_packZip;
+ QFuture<nonstd::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
+ NetJobPtr netJobContainer;
+ QString archivePath;
+
+ Modpack m_pack;
+ QString m_version;
+};
+
+}
diff --git a/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp b/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp
new file mode 100644
index 00000000..501e6003
--- /dev/null
+++ b/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp
@@ -0,0 +1,41 @@
+#include "PrivatePackManager.h"
+
+#include <QDebug>
+
+#include "FileSystem.h"
+
+namespace LegacyFTB {
+
+void PrivatePackManager::load()
+{
+ try
+ {
+ currentPacks = QString::fromUtf8(FS::read(m_filename)).split('\n', QString::SkipEmptyParts).toSet();
+ dirty = false;
+ }
+ catch(...)
+ {
+ currentPacks = {};
+ qWarning() << "Failed to read third party FTB pack codes from" << m_filename;
+ }
+}
+
+void PrivatePackManager::save() const
+{
+ if(!dirty)
+ {
+ return;
+ }
+ try
+ {
+ QStringList list = currentPacks.toList();
+ FS::write(m_filename, list.join('\n').toUtf8());
+ dirty = false;
+ }
+ catch(...)
+ {
+ qWarning() << "Failed to write third party FTB pack codes to" << m_filename;
+ }
+}
+
+}
diff --git a/launcher/modplatform/legacy_ftb/PrivatePackManager.h b/launcher/modplatform/legacy_ftb/PrivatePackManager.h
new file mode 100644
index 00000000..0e814646
--- /dev/null
+++ b/launcher/modplatform/legacy_ftb/PrivatePackManager.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include <QSet>
+#include <QString>
+#include <QFile>
+
+namespace LegacyFTB {
+
+class PrivatePackManager
+{
+public:
+ ~PrivatePackManager()
+ {
+ save();
+ }
+ void load();
+ void save() const;
+ bool empty() const
+ {
+ return currentPacks.empty();
+ }
+ const QSet<QString> &getCurrentPackCodes() const
+ {
+ return currentPacks;
+ }
+ void add(const QString &code)
+ {
+ currentPacks.insert(code);
+ dirty = true;
+ }
+ void remove(const QString &code)
+ {
+ currentPacks.remove(code);
+ dirty = true;
+ }
+
+private:
+ QSet<QString> currentPacks;
+ QString m_filename = "private_packs.txt";
+ mutable bool dirty = false;
+};
+
+}
diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
new file mode 100644
index 00000000..f22373bc
--- /dev/null
+++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
@@ -0,0 +1,209 @@
+#include "FTBPackInstallTask.h"
+
+#include "BuildConfig.h"
+#include "Env.h"
+#include "FileSystem.h"
+#include "Json.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "net/ChecksumValidator.h"
+#include "settings/INISettingsObject.h"
+
+namespace ModpacksCH {
+
+PackInstallTask::PackInstallTask(Modpack pack, QString version)
+{
+ m_pack = pack;
+ m_version_name = version;
+}
+
+bool PackInstallTask::abort()
+{
+ if(abortable)
+ {
+ return jobPtr->abort();
+ }
+ return false;
+}
+
+void PackInstallTask::executeTask()
+{
+ // Find pack version
+ bool found = false;
+ VersionInfo version;
+
+ for(auto vInfo : m_pack.versions) {
+ if (vInfo.name == m_version_name) {
+ found = true;
+ version = vInfo;
+ break;
+ }
+ }
+
+ if(!found) {
+ emitFailed(tr("Failed to find pack version %1").arg(m_version_name));
+ return;
+ }
+
+ auto *netJob = new NetJob("ModpacksCH::VersionFetch");
+ auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1/%2")
+ .arg(m_pack.id).arg(version.id);
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
+ jobPtr = netJob;
+ jobPtr->start();
+
+ QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded);
+ QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed);
+}
+
+void PackInstallTask::onDownloadSucceeded()
+{
+ jobPtr.reset();
+
+ QJsonParseError parse_error;
+ QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
+ if(parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << response;
+ return;
+ }
+
+ auto obj = doc.object();
+
+ ModpacksCH::Version version;
+ try
+ {
+ ModpacksCH::loadVersion(version, obj);
+ }
+ catch (const JSONValidationError &e)
+ {
+ emitFailed(tr("Could not understand pack manifest:\n") + e.cause());
+ return;
+ }
+ m_version = version;
+
+ downloadPack();
+}
+
+void PackInstallTask::onDownloadFailed(QString reason)
+{
+ jobPtr.reset();
+ emitFailed(reason);
+}
+
+void PackInstallTask::downloadPack()
+{
+ setStatus(tr("Downloading mods..."));
+
+ jobPtr.reset(new NetJob(tr("Mod download")));
+ for(auto file : m_version.files) {
+ if(file.serverOnly) continue;
+
+ QFileInfo fileName(file.name);
+ auto cacheName = fileName.completeBaseName() + "-" + file.sha1 + "." + fileName.suffix();
+
+ auto entry = ENV.metacache()->resolveEntry("ModpacksCHPacks", cacheName);
+ entry->setStale(true);
+
+ auto relpath = FS::PathCombine("minecraft", file.path, file.name);
+ auto path = FS::PathCombine(m_stagingPath, relpath);
+
+ qDebug() << "Will download" << file.url << "to" << path;
+ filesToCopy[entry->getFullPath()] = path;
+
+ auto dl = Net::Download::makeCached(file.url, entry);
+ if (!file.sha1.isEmpty()) {
+ auto rawSha1 = QByteArray::fromHex(file.sha1.toLatin1());
+ dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1));
+ }
+ jobPtr->addNetAction(dl);
+ }
+
+ connect(jobPtr.get(), &NetJob::succeeded, this, [&]()
+ {
+ abortable = false;
+ jobPtr.reset();
+ install();
+ });
+ connect(jobPtr.get(), &NetJob::failed, [&](QString reason)
+ {
+ abortable = false;
+ jobPtr.reset();
+ emitFailed(reason);
+ });
+ connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total)
+ {
+ abortable = true;
+ setProgress(current, total);
+ });
+
+ jobPtr->start();
+}
+
+void PackInstallTask::install()
+{
+ setStatus(tr("Copying modpack files"));
+
+ for (auto iter = filesToCopy.begin(); iter != filesToCopy.end(); iter++) {
+ auto &from = iter.key();
+ auto &to = iter.value();
+ FS::copy fileCopyOperation(from, to);
+ if(!fileCopyOperation()) {
+ qWarning() << "Failed to copy" << from << "to" << to;
+ emitFailed(tr("Failed to copy files"));
+ return;
+ }
+ }
+
+ setStatus(tr("Installing modpack"));
+
+ auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg");
+ auto instanceSettings = std::make_shared<INISettingsObject>(instanceConfigPath);
+ instanceSettings->suspendSave();
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+ instanceSettings->set("InstanceType", "OneSix");
+
+ MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath);
+ auto components = instance.getPackProfile();
+ components->buildingFromScratch();
+
+ for(auto target : m_version.targets) {
+ if(target.type == "game" && target.name == "minecraft") {
+ components->setComponentVersion("net.minecraft", target.version, true);
+ break;
+ }
+ }
+
+ for(auto target : m_version.targets) {
+ if(target.type != "modloader") continue;
+
+ if(target.name == "forge") {
+ components->setComponentVersion("net.minecraftforge", target.version, true);
+ }
+ else if(target.name == "fabric") {
+ components->setComponentVersion("net.fabricmc.fabric-loader", target.version, true);
+ }
+ }
+
+ // install any jar mods
+ QDir jarModsDir(FS::PathCombine(m_stagingPath, "minecraft", "jarmods"));
+ if (jarModsDir.exists()) {
+ QStringList jarMods;
+
+ for (const auto& info : jarModsDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) {
+ jarMods.push_back(info.absoluteFilePath());
+ }
+
+ components->installJarMods(jarMods);
+ }
+
+ components->saveNow();
+
+ instance.setName(m_instName);
+ instance.setIconKey(m_instIcon);
+ instanceSettings->resumeSave();
+
+ emitSucceeded();
+}
+
+}
diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.h b/launcher/modplatform/modpacksch/FTBPackInstallTask.h
new file mode 100644
index 00000000..fdd84c4e
--- /dev/null
+++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#include "FTBPackManifest.h"
+
+#include "InstanceTask.h"
+#include "net/NetJob.h"
+
+namespace ModpacksCH {
+
+class PackInstallTask : public InstanceTask
+{
+ Q_OBJECT
+
+public:
+ explicit PackInstallTask(Modpack pack, QString version);
+ virtual ~PackInstallTask(){}
+
+ bool canAbort() const override { return true; }
+ bool abort() override;
+
+protected:
+ virtual void executeTask() override;
+
+private slots:
+ void onDownloadSucceeded();
+ void onDownloadFailed(QString reason);
+
+private:
+ void downloadPack();
+ void install();
+
+private:
+ bool abortable = false;
+
+ NetJobPtr jobPtr;
+ QByteArray response;
+
+ Modpack m_pack;
+ QString m_version_name;
+ Version m_version;
+
+ QMap<QString, QString> filesToCopy;
+
+};
+
+}
diff --git a/launcher/modplatform/modpacksch/FTBPackManifest.cpp b/launcher/modplatform/modpacksch/FTBPackManifest.cpp
new file mode 100644
index 00000000..fd99d332
--- /dev/null
+++ b/launcher/modplatform/modpacksch/FTBPackManifest.cpp
@@ -0,0 +1,156 @@
+#include "FTBPackManifest.h"
+
+#include "Json.h"
+
+static void loadSpecs(ModpacksCH::Specs & s, QJsonObject & obj)
+{
+ s.id = Json::requireInteger(obj, "id");
+ s.minimum = Json::requireInteger(obj, "minimum");
+ s.recommended = Json::requireInteger(obj, "recommended");
+}
+
+static void loadTag(ModpacksCH::Tag & t, QJsonObject & obj)
+{
+ t.id = Json::requireInteger(obj, "id");
+ t.name = Json::requireString(obj, "name");
+}
+
+static void loadArt(ModpacksCH::Art & a, QJsonObject & obj)
+{
+ a.id = Json::requireInteger(obj, "id");
+ a.url = Json::requireString(obj, "url");
+ a.type = Json::requireString(obj, "type");
+ a.width = Json::requireInteger(obj, "width");
+ a.height = Json::requireInteger(obj, "height");
+ a.compressed = Json::requireBoolean(obj, "compressed");
+ a.sha1 = Json::requireString(obj, "sha1");
+ a.size = Json::requireInteger(obj, "size");
+ a.updated = Json::requireInteger(obj, "updated");
+}
+
+static void loadAuthor(ModpacksCH::Author & a, QJsonObject & obj)
+{
+ a.id = Json::requireInteger(obj, "id");
+ a.name = Json::requireString(obj, "name");
+ a.type = Json::requireString(obj, "type");
+ a.website = Json::requireString(obj, "website");
+ a.updated = Json::requireInteger(obj, "updated");
+}
+
+static void loadVersionInfo(ModpacksCH::VersionInfo & v, QJsonObject & obj)
+{
+ v.id = Json::requireInteger(obj, "id");
+ v.name = Json::requireString(obj, "name");
+ v.type = Json::requireString(obj, "type");
+ v.updated = Json::requireInteger(obj, "updated");
+ auto specs = Json::requireObject(obj, "specs");
+ loadSpecs(v.specs, specs);
+}
+
+void ModpacksCH::loadModpack(ModpacksCH::Modpack & m, QJsonObject & obj)
+{
+ m.id = Json::requireInteger(obj, "id");
+ m.name = Json::requireString(obj, "name");
+ m.synopsis = Json::requireString(obj, "synopsis");
+ m.description = Json::requireString(obj, "description");
+ m.type = Json::requireString(obj, "type");
+ m.featured = Json::requireBoolean(obj, "featured");
+ m.installs = Json::requireInteger(obj, "installs");
+ m.plays = Json::requireInteger(obj, "plays");
+ m.updated = Json::requireInteger(obj, "updated");
+ m.refreshed = Json::requireInteger(obj, "refreshed");
+ auto artArr = Json::requireArray(obj, "art");
+ for (QJsonValueRef artRaw : artArr)
+ {
+ auto artObj = Json::requireObject(artRaw);
+ ModpacksCH::Art art;
+ loadArt(art, artObj);
+ m.art.append(art);
+ }
+ auto authorArr = Json::requireArray(obj, "authors");
+ for (QJsonValueRef authorRaw : authorArr)
+ {
+ auto authorObj = Json::requireObject(authorRaw);
+ ModpacksCH::Author author;
+ loadAuthor(author, authorObj);
+ m.authors.append(author);
+ }
+ auto versionArr = Json::requireArray(obj, "versions");
+ for (QJsonValueRef versionRaw : versionArr)
+ {
+ auto versionObj = Json::requireObject(versionRaw);
+ ModpacksCH::VersionInfo version;
+ loadVersionInfo(version, versionObj);
+ m.versions.append(version);
+ }
+ auto tagArr = Json::requireArray(obj, "tags");
+ for (QJsonValueRef tagRaw : tagArr)
+ {
+ auto tagObj = Json::requireObject(tagRaw);
+ ModpacksCH::Tag tag;
+ loadTag(tag, tagObj);
+ m.tags.append(tag);
+ }
+ m.updated = Json::requireInteger(obj, "updated");
+}
+
+static void loadVersionTarget(ModpacksCH::VersionTarget & a, QJsonObject & obj)
+{
+ a.id = Json::requireInteger(obj, "id");
+ a.name = Json::requireString(obj, "name");
+ a.type = Json::requireString(obj, "type");
+ a.version = Json::requireString(obj, "version");
+ a.updated = Json::requireInteger(obj, "updated");
+}
+
+static void loadVersionFile(ModpacksCH::VersionFile & a, QJsonObject & obj)
+{
+ a.id = Json::requireInteger(obj, "id");
+ a.type = Json::requireString(obj, "type");
+ a.path = Json::requireString(obj, "path");
+ a.name = Json::requireString(obj, "name");
+ a.version = Json::requireString(obj, "version");
+ a.url = Json::requireString(obj, "url");
+ a.sha1 = Json::requireString(obj, "sha1");
+ a.size = Json::requireInteger(obj, "size");
+ a.clientOnly = Json::requireBoolean(obj, "clientonly");
+ a.serverOnly = Json::requireBoolean(obj, "serveronly");
+ a.optional = Json::requireBoolean(obj, "optional");
+ a.updated = Json::requireInteger(obj, "updated");
+}
+
+void ModpacksCH::loadVersion(ModpacksCH::Version & m, QJsonObject & obj)
+{
+ m.id = Json::requireInteger(obj, "id");
+ m.parent = Json::requireInteger(obj, "parent");
+ m.name = Json::requireString(obj, "name");
+ m.type = Json::requireString(obj, "type");
+ m.installs = Json::requireInteger(obj, "installs");
+ m.plays = Json::requireInteger(obj, "plays");
+ m.updated = Json::requireInteger(obj, "updated");
+ m.refreshed = Json::requireInteger(obj, "refreshed");
+ auto specs = Json::requireObject(obj, "specs");
+ loadSpecs(m.specs, specs);
+ auto targetArr = Json::requireArray(obj, "targets");
+ for (QJsonValueRef targetRaw : targetArr)
+ {
+ auto versionObj = Json::requireObject(targetRaw);
+ ModpacksCH::VersionTarget target;
+ loadVersionTarget(target, versionObj);
+ m.targets.append(target);
+ }
+ auto fileArr = Json::requireArray(obj, "files");
+ for (QJsonValueRef fileRaw : fileArr)
+ {
+ auto fileObj = Json::requireObject(fileRaw);
+ ModpacksCH::VersionFile file;
+ loadVersionFile(file, fileObj);
+ m.files.append(file);
+ }
+}
+
+//static void loadVersionChangelog(ModpacksCH::VersionChangelog & m, QJsonObject & obj)
+//{
+// m.content = Json::requireString(obj, "content");
+// m.updated = Json::requireInteger(obj, "updated");
+//}
diff --git a/launcher/modplatform/modpacksch/FTBPackManifest.h b/launcher/modplatform/modpacksch/FTBPackManifest.h
new file mode 100644
index 00000000..7818b36d
--- /dev/null
+++ b/launcher/modplatform/modpacksch/FTBPackManifest.h
@@ -0,0 +1,125 @@
+#pragma once
+
+#include <QString>
+#include <QVector>
+#include <QUrl>
+#include <QJsonObject>
+#include <QMetaType>
+
+namespace ModpacksCH
+{
+
+struct Specs
+{
+ int id;
+ int minimum;
+ int recommended;
+};
+
+struct Tag
+{
+ int id;
+ QString name;
+};
+
+struct Art
+{
+ int id;
+ QString url;
+ QString type;
+ int width;
+ int height;
+ bool compressed;
+ QString sha1;
+ int size;
+ int64_t updated;
+};
+
+struct Author
+{
+ int id;
+ QString name;
+ QString type;
+ QString website;
+ int64_t updated;
+};
+
+struct VersionInfo
+{
+ int id;
+ QString name;
+ QString type;
+ int64_t updated;
+ Specs specs;
+};
+
+struct Modpack
+{
+ int id;
+ QString name;
+ QString synopsis;
+ QString description;
+ QString type;
+ bool featured;
+ int installs;
+ int plays;
+ int64_t updated;
+ int64_t refreshed;
+ QVector<Art> art;
+ QVector<Author> authors;
+ QVector<VersionInfo> versions;
+ QVector<Tag> tags;
+};
+
+struct VersionTarget
+{
+ int id;
+ QString type;
+ QString name;
+ QString version;
+ int64_t updated;
+};
+
+struct VersionFile
+{
+ int id;
+ QString type;
+ QString path;
+ QString name;
+ QString version;
+ QString url;
+ QString sha1;
+ int size;
+ bool clientOnly;
+ bool serverOnly;
+ bool optional;
+ int64_t updated;
+};
+
+struct Version
+{
+ int id;
+ int parent;
+ QString name;
+ QString type;
+ int installs;
+ int plays;
+ int64_t updated;
+ int64_t refreshed;
+ Specs specs;
+ QVector<VersionTarget> targets;
+ QVector<VersionFile> files;
+};
+
+struct VersionChangelog
+{
+ QString content;
+ int64_t updated;
+};
+
+void loadModpack(Modpack & m, QJsonObject & obj);
+
+void loadVersion(Version & m, QJsonObject & obj);
+}
+
+Q_DECLARE_METATYPE(ModpacksCH::Modpack)
diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp
new file mode 100644
index 00000000..dbce8e53
--- /dev/null
+++ b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp
@@ -0,0 +1,141 @@
+/* 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 "SingleZipPackInstallTask.h"
+
+#include "Env.h"
+#include "MMCZip.h"
+#include "TechnicPackProcessor.h"
+
+#include <QtConcurrent>
+#include <FileSystem.h>
+
+Technic::SingleZipPackInstallTask::SingleZipPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion)
+{
+ m_sourceUrl = sourceUrl;
+ m_minecraftVersion = minecraftVersion;
+}
+
+bool Technic::SingleZipPackInstallTask::abort() {
+ if(m_abortable)
+ {
+ return m_filesNetJob->abort();
+ }
+ return false;
+}
+
+void Technic::SingleZipPackInstallTask::executeTask()
+{
+ setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString()));
+
+ const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path();
+ auto entry = ENV.metacache()->resolveEntry("general", path);
+ entry->setStale(true);
+ m_filesNetJob.reset(new NetJob(tr("Modpack download")));
+ m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry));
+ m_archivePath = entry->getFullPath();
+ auto job = m_filesNetJob.get();
+ connect(job, &NetJob::succeeded, this, &Technic::SingleZipPackInstallTask::downloadSucceeded);
+ connect(job, &NetJob::progress, this, &Technic::SingleZipPackInstallTask::downloadProgressChanged);
+ connect(job, &NetJob::failed, this, &Technic::SingleZipPackInstallTask::downloadFailed);
+ m_filesNetJob->start();
+}
+
+void Technic::SingleZipPackInstallTask::downloadSucceeded()
+{
+ m_abortable = false;
+
+ setStatus(tr("Extracting modpack"));
+ QDir extractDir(FS::PathCombine(m_stagingPath, ".minecraft"));
+ qDebug() << "Attempting to create instance from" << m_archivePath;
+
+ // open the zip and find relevant files in it
+ m_packZip.reset(new QuaZip(m_archivePath));
+ if (!m_packZip->open(QuaZip::mdUnzip))
+ {
+ emitFailed(tr("Unable to open supplied modpack zip file."));
+ return;
+ }
+ m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), QString(""), extractDir.absolutePath());
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &Technic::SingleZipPackInstallTask::extractFinished);
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &Technic::SingleZipPackInstallTask::extractAborted);
+ m_extractFutureWatcher.setFuture(m_extractFuture);
+ m_filesNetJob.reset();
+}
+
+void Technic::SingleZipPackInstallTask::downloadFailed(QString reason)
+{
+ m_abortable = false;
+ emitFailed(reason);
+ m_filesNetJob.reset();
+}
+
+void Technic::SingleZipPackInstallTask::downloadProgressChanged(qint64 current, qint64 total)
+{
+ m_abortable = true;
+ setProgress(current / 2, total);
+}
+
+void Technic::SingleZipPackInstallTask::extractFinished()
+{
+ m_packZip.reset();
+ if (!m_extractFuture.result())
+ {
+ emitFailed(tr("Failed to extract modpack"));
+ return;
+ }
+ QDir extractDir(m_stagingPath);
+
+ qDebug() << "Fixing permissions for extracted pack files...";
+ QDirIterator it(extractDir, QDirIterator::Subdirectories);
+ while (it.hasNext())
+ {
+ auto filepath = it.next();
+ QFileInfo file(filepath);
+ auto permissions = QFile::permissions(filepath);
+ auto origPermissions = permissions;
+ if (file.isDir())
+ {
+ // Folder +rwx for current user
+ permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser;
+ }
+ else
+ {
+ // File +rw for current user
+ permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser;
+ }
+ if (origPermissions != permissions)
+ {
+ if (!QFile::setPermissions(filepath, permissions))
+ {
+ logWarning(tr("Could not fix permissions for %1").arg(filepath));
+ }
+ else
+ {
+ qDebug() << "Fixed" << filepath;
+ }
+ }
+ }
+
+ shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
+ connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SingleZipPackInstallTask::emitSucceeded);
+ connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SingleZipPackInstallTask::emitFailed);
+ packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath, m_minecraftVersion);
+}
+
+void Technic::SingleZipPackInstallTask::extractAborted()
+{
+ emitFailed(tr("Instance import has been aborted."));
+}
diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.h b/launcher/modplatform/technic/SingleZipPackInstallTask.h
new file mode 100644
index 00000000..80f10a98
--- /dev/null
+++ b/launcher/modplatform/technic/SingleZipPackInstallTask.h
@@ -0,0 +1,64 @@
+/* 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.
+ */
+
+#pragma once
+
+#include "InstanceTask.h"
+#include "net/NetJob.h"
+
+#include "quazip.h"
+
+#include <QFutureWatcher>
+#include <QStringList>
+#include <QUrl>
+
+#include <nonstd/optional>
+
+namespace Technic {
+
+class SingleZipPackInstallTask : public InstanceTask
+{
+ Q_OBJECT
+
+public:
+ SingleZipPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion);
+
+ bool canAbort() const override { return true; }
+ bool abort() override;
+
+protected:
+ void executeTask() override;
+
+
+private slots:
+ void downloadSucceeded();
+ void downloadFailed(QString reason);
+ void downloadProgressChanged(qint64 current, qint64 total);
+ void extractFinished();
+ void extractAborted();
+
+private:
+ bool m_abortable = false;
+
+ QUrl m_sourceUrl;
+ QString m_minecraftVersion;
+ QString m_archivePath;
+ NetJobPtr m_filesNetJob;
+ std::unique_ptr<QuaZip> m_packZip;
+ QFuture<nonstd::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
+};
+
+} // namespace Technic
diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp
new file mode 100644
index 00000000..1b4186d4
--- /dev/null
+++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp
@@ -0,0 +1,207 @@
+/* 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 "SolderPackInstallTask.h"
+
+#include <FileSystem.h>
+#include <Json.h>
+#include <QtConcurrentRun>
+#include <MMCZip.h>
+#include "TechnicPackProcessor.h"
+
+Technic::SolderPackInstallTask::SolderPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion)
+{
+ m_sourceUrl = sourceUrl;
+ m_minecraftVersion = minecraftVersion;
+}
+
+bool Technic::SolderPackInstallTask::abort() {
+ if(m_abortable)
+ {
+ return m_filesNetJob->abort();
+ }
+ return false;
+}
+
+void Technic::SolderPackInstallTask::executeTask()
+{
+ setStatus(tr("Finding recommended version:\n%1").arg(m_sourceUrl.toString()));
+ m_filesNetJob.reset(new NetJob(tr("Finding recommended version")));
+ m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response));
+ auto job = m_filesNetJob.get();
+ connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::versionSucceeded);
+ connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
+ m_filesNetJob->start();
+}
+
+void Technic::SolderPackInstallTask::versionSucceeded()
+{
+ try
+ {
+ QJsonDocument doc = Json::requireDocument(m_response);
+ QJsonObject obj = Json::requireObject(doc);
+ QString version = Json::requireString(obj, "recommended", "__placeholder__");
+ m_sourceUrl = m_sourceUrl.toString() + '/' + version;
+ }
+ catch (const JSONValidationError &e)
+ {
+ emitFailed(e.cause());
+ m_filesNetJob.reset();
+ return;
+ }
+
+ setStatus(tr("Resolving modpack files:\n%1").arg(m_sourceUrl.toString()));
+ m_filesNetJob.reset(new NetJob(tr("Resolving modpack files")));
+ m_filesNetJob->addNetAction(Net::Download::makeByteArray(m_sourceUrl, &m_response));
+ auto job = m_filesNetJob.get();
+ connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded);
+ connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
+ m_filesNetJob->start();
+}
+
+void Technic::SolderPackInstallTask::fileListSucceeded()
+{
+ setStatus(tr("Downloading modpack:"));
+ QStringList modUrls;
+ try
+ {
+ QJsonDocument doc = Json::requireDocument(m_response);
+ QJsonObject obj = Json::requireObject(doc);
+ QString minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__");
+ if (!minecraftVersion.isEmpty())
+ m_minecraftVersion = minecraftVersion;
+ QJsonArray mods = Json::requireArray(obj, "mods", "'mods'");
+ for (auto mod: mods)
+ {
+ QJsonObject modObject = Json::requireObject(mod);
+ modUrls.append(Json::requireString(modObject, "url", "'url'"));
+ }
+ }
+ catch (const JSONValidationError &e)
+ {
+ emitFailed(e.cause());
+ m_filesNetJob.reset();
+ return;
+ }
+ m_filesNetJob.reset(new NetJob(tr("Downloading modpack")));
+ int i = 0;
+ for (auto &modUrl: modUrls)
+ {
+ auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i));
+ m_filesNetJob->addNetAction(Net::Download::makeFile(modUrl, path));
+ i++;
+ }
+
+ m_modCount = modUrls.size();
+
+ connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded);
+ connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged);
+ connect(m_filesNetJob.get(), &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed);
+ m_filesNetJob->start();
+}
+
+void Technic::SolderPackInstallTask::downloadSucceeded()
+{
+ m_abortable = false;
+
+ setStatus(tr("Extracting modpack"));
+ m_filesNetJob.reset();
+ m_extractFuture = QtConcurrent::run([this]()
+ {
+ int i = 0;
+ QString extractDir = FS::PathCombine(m_stagingPath, ".minecraft");
+ FS::ensureFolderPathExists(extractDir);
+
+ while (m_modCount > i)
+ {
+ auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i));
+ if (!MMCZip::extractDir(path, extractDir))
+ {
+ return false;
+ }
+ i++;
+ }
+ return true;
+ });
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &Technic::SolderPackInstallTask::extractFinished);
+ connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &Technic::SolderPackInstallTask::extractAborted);
+ m_extractFutureWatcher.setFuture(m_extractFuture);
+}
+
+void Technic::SolderPackInstallTask::downloadFailed(QString reason)
+{
+ m_abortable = false;
+ emitFailed(reason);
+ m_filesNetJob.reset();
+}
+
+void Technic::SolderPackInstallTask::downloadProgressChanged(qint64 current, qint64 total)
+{
+ m_abortable = true;
+ setProgress(current / 2, total);
+}
+
+void Technic::SolderPackInstallTask::extractFinished()
+{
+ if (!m_extractFuture.result())
+ {
+ emitFailed(tr("Failed to extract modpack"));
+ return;
+ }
+ QDir extractDir(m_stagingPath);
+
+ qDebug() << "Fixing permissions for extracted pack files...";
+ QDirIterator it(extractDir, QDirIterator::Subdirectories);
+ while (it.hasNext())
+ {
+ auto filepath = it.next();
+ QFileInfo file(filepath);
+ auto permissions = QFile::permissions(filepath);
+ auto origPermissions = permissions;
+ if(file.isDir())
+ {
+ // Folder +rwx for current user
+ permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser;
+ }
+ else
+ {
+ // File +rw for current user
+ permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser;
+ }
+ if(origPermissions != permissions)
+ {
+ if(!QFile::setPermissions(filepath, permissions))
+ {
+ logWarning(tr("Could not fix permissions for %1").arg(filepath));
+ }
+ else
+ {
+ qDebug() << "Fixed" << filepath;
+ }
+ }
+ }
+
+ shared_qobject_ptr<Technic::TechnicPackProcessor> packProcessor = new Technic::TechnicPackProcessor();
+ connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SolderPackInstallTask::emitSucceeded);
+ connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SolderPackInstallTask::emitFailed);
+ packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath, m_minecraftVersion, true);
+}
+
+void Technic::SolderPackInstallTask::extractAborted()
+{
+ emitFailed(tr("Instance import has been aborted."));
+ return;
+}
+
diff --git a/launcher/modplatform/technic/SolderPackInstallTask.h b/launcher/modplatform/technic/SolderPackInstallTask.h
new file mode 100644
index 00000000..6e1057eb
--- /dev/null
+++ b/launcher/modplatform/technic/SolderPackInstallTask.h
@@ -0,0 +1,60 @@
+/* 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.
+ */
+
+#pragma once
+
+#include <InstanceTask.h>
+#include <net/NetJob.h>
+#include <tasks/Task.h>
+
+#include <QUrl>
+
+namespace Technic
+{
+ class SolderPackInstallTask : public InstanceTask
+ {
+ Q_OBJECT
+ public:
+ explicit SolderPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion);
+
+ bool canAbort() const override { return true; }
+ bool abort() override;
+
+ protected:
+ //! Entry point for tasks.
+ virtual void executeTask() override;
+
+ private slots:
+ void versionSucceeded();
+ void fileListSucceeded();
+ void downloadSucceeded();
+ void downloadFailed(QString reason);
+ void downloadProgressChanged(qint64 current, qint64 total);
+ void extractFinished();
+ void extractAborted();
+
+ private:
+ bool m_abortable = false;
+
+ NetJobPtr m_filesNetJob;
+ QUrl m_sourceUrl;
+ QString m_minecraftVersion;
+ QByteArray m_response;
+ QTemporaryDir m_outputDir;
+ int m_modCount;
+ QFuture<bool> m_extractFuture;
+ QFutureWatcher<bool> m_extractFutureWatcher;
+ };
+}
diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp
new file mode 100644
index 00000000..52979b7c
--- /dev/null
+++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp
@@ -0,0 +1,208 @@
+/* Copyright 2020-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 "TechnicPackProcessor.h"
+
+#include <FileSystem.h>
+#include <Json.h>
+#include <minecraft/MinecraftInstance.h>
+#include <minecraft/PackProfile.h>
+#include <quazip.h>
+#include <quazipdir.h>
+#include <quazipfile.h>
+#include <settings/INISettingsObject.h>
+
+#include <memory>
+
+void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const QString &instName, const QString &instIcon, const QString &stagingPath, const QString &minecraftVersion, const bool isSolder)
+{
+ QString minecraftPath = FS::PathCombine(stagingPath, ".minecraft");
+ QString configPath = FS::PathCombine(stagingPath, "instance.cfg");
+ auto instanceSettings = std::make_shared<INISettingsObject>(configPath);
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+ instanceSettings->set("InstanceType", "OneSix");
+ MinecraftInstance instance(globalSettings, instanceSettings, stagingPath);
+
+ instance.setName(instName);
+
+ if (instIcon != "default")
+ {
+ instance.setIconKey(instIcon);
+ }
+
+ auto components = instance.getPackProfile();
+ components->buildingFromScratch();
+
+ QByteArray data;
+
+ QString modpackJar = FS::PathCombine(minecraftPath, "bin", "modpack.jar");
+ QString versionJson = FS::PathCombine(minecraftPath, "bin", "version.json");
+ QString fmlMinecraftVersion;
+ if (QFile::exists(modpackJar))
+ {
+ QuaZip zipFile(modpackJar);
+ if (!zipFile.open(QuaZip::mdUnzip))
+ {
+ emit failed(tr("Unable to open \"bin/modpack.jar\" file!"));
+ return;
+ }
+ QuaZipDir zipFileRoot(&zipFile, "/");
+ if (zipFileRoot.exists("/version.json"))
+ {
+ if (zipFileRoot.exists("/fmlversion.properties"))
+ {
+ zipFile.setCurrentFile("fmlversion.properties");
+ QuaZipFile file(&zipFile);
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ emit failed(tr("Unable to open \"fmlversion.properties\"!"));
+ return;
+ }
+ QByteArray fmlVersionData = file.readAll();
+ file.close();
+ INIFile iniFile;
+ iniFile.loadFile(fmlVersionData);
+ // If not present, this evaluates to a null string
+ fmlMinecraftVersion = iniFile["fmlbuild.mcversion"].toString();
+ }
+ zipFile.setCurrentFile("version.json", QuaZip::csSensitive);
+ QuaZipFile file(&zipFile);
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ emit failed(tr("Unable to open \"version.json\"!"));
+ return;
+ }
+ data = file.readAll();
+ file.close();
+ }
+ else
+ {
+ if (minecraftVersion.isEmpty())
+ emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but minecraft version is unknown"));
+ components->setComponentVersion("net.minecraft", minecraftVersion, true);
+ components->installJarMods({modpackJar});
+
+ // Forge for 1.4.7 and for 1.5.2 require extra libraries.
+ // Figure out the forge version and add it as a component
+ // (the code still comes from the jar mod installed above)
+ if (zipFileRoot.exists("/forgeversion.properties"))
+ {
+ zipFile.setCurrentFile("forgeversion.properties", QuaZip::csSensitive);
+ QuaZipFile file(&zipFile);
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ // Really shouldn't happen, but error handling shall not be forgotten
+ emit failed(tr("Unable to open \"forgeversion.properties\""));
+ return;
+ }
+ QByteArray forgeVersionData = file.readAll();
+ file.close();
+ INIFile iniFile;
+ iniFile.loadFile(forgeVersionData);
+ QString major, minor, revision, build;
+ major = iniFile["forge.major.number"].toString();
+ minor = iniFile["forge.minor.number"].toString();
+ revision = iniFile["forge.revision.number"].toString();
+ build = iniFile["forge.build.number"].toString();
+
+ if (major.isEmpty() || minor.isEmpty() || revision.isEmpty() || build.isEmpty())
+ {
+ emit failed(tr("Invalid \"forgeversion.properties\"!"));
+ return;
+ }
+
+ components->setComponentVersion("net.minecraftforge", major + '.' + minor + '.' + revision + '.' + build);
+ }
+
+ components->saveNow();
+ emit succeeded();
+ return;
+ }
+ }
+ else if (QFile::exists(versionJson))
+ {
+ QFile file(versionJson);
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ emit failed(tr("Unable to open \"version.json\"!"));
+ return;
+ }
+ data = file.readAll();
+ file.close();
+ }
+ else
+ {
+ // This is the "Vanilla" modpack, excluded by the search code
+ emit failed(tr("Unable to find a \"version.json\"!"));
+ return;
+ }
+
+ try
+ {
+ QJsonDocument doc = Json::requireDocument(data);
+ QJsonObject root = Json::requireObject(doc, "version.json");
+ QString minecraftVersion = Json::ensureString(root, "inheritsFrom", QString(), "");
+ if (minecraftVersion.isEmpty())
+ {
+ if (fmlMinecraftVersion.isEmpty())
+ {
+ emit failed(tr("Could not understand \"version.json\":\ninheritsFrom is missing"));
+ return;
+ }
+ minecraftVersion = fmlMinecraftVersion;
+ }
+ components->setComponentVersion("net.minecraft", minecraftVersion, true);
+ for (auto library: Json::ensureArray(root, "libraries", {}))
+ {
+ if (!library.isObject())
+ {
+ continue;
+ }
+
+ auto libraryObject = Json::ensureObject(library, {}, "");
+ auto libraryName = Json::ensureString(libraryObject, "name", "", "");
+
+ if (libraryName.startsWith("net.minecraftforge:forge:") && libraryName.contains('-'))
+ {
+ QString libraryVersion = libraryName.section(':', 2);
+ if (!libraryVersion.startsWith("1.7.10-"))
+ {
+ components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1));
+ }
+ else
+ {
+ // 1.7.10 versions sometimes look like 1.7.10-10.13.4.1614-1.7.10, this filters out the 10.13.4.1614 part
+ components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1, 1));
+ }
+ }
+ else if (libraryName.startsWith("net.minecraftforge:minecraftforge:"))
+ {
+ components->setComponentVersion("net.minecraftforge", libraryName.section(':', 2));
+ }
+ else if (libraryName.startsWith("net.fabricmc:fabric-loader:"))
+ {
+ components->setComponentVersion("net.fabricmc.fabric-loader", libraryName.section(':', 2));
+ }
+ }
+ }
+ catch (const JSONValidationError &e)
+ {
+ emit failed(tr("Could not understand \"version.json\":\n") + e.cause());
+ return;
+ }
+
+ components->saveNow();
+ emit succeeded();
+}
diff --git a/launcher/modplatform/technic/TechnicPackProcessor.h b/launcher/modplatform/technic/TechnicPackProcessor.h
new file mode 100644
index 00000000..2ad803b3
--- /dev/null
+++ b/launcher/modplatform/technic/TechnicPackProcessor.h
@@ -0,0 +1,35 @@
+/* Copyright 2020-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.
+ */
+
+#pragma once
+
+#include <QString>
+#include "settings/SettingsObject.h"
+
+namespace Technic
+{
+ // not exporting it, only used in SingleZipPackInstallTask, InstanceImportTask and SolderPackInstallTask
+ class TechnicPackProcessor : public QObject
+ {
+ Q_OBJECT
+
+ signals:
+ void succeeded();
+ void failed(QString reason);
+
+ public:
+ void run(SettingsObjectPtr globalSettings, const QString &instName, const QString &instIcon, const QString &stagingPath, const QString &minecraftVersion=QString(), const bool isSolder = false);
+ };
+}