Merge remote-tracking branch 'upstream/develop' into develop

This commit is contained in:
Sebastian-byte
2021-02-12 12:20:47 -05:00
47 changed files with 2102 additions and 126 deletions

View File

@@ -303,9 +303,11 @@ set(MINECRAFT_SOURCES
minecraft/AssetsUtils.h
minecraft/AssetsUtils.cpp
# Skin upload utilities
minecraft/SkinUpload.cpp
minecraft/SkinUpload.h
# Minecraft services
minecraft/services/SkinUpload.cpp
minecraft/services/SkinUpload.h
minecraft/services/SkinDelete.cpp
minecraft/services/SkinDelete.h
mojang/PackageManifest.h
mojang/PackageManifest.cpp
@@ -486,6 +488,15 @@ set(TECHNIC_SOURCES
modplatform/technic/TechnicPackProcessor.cpp
)
set(ATLAUNCHER_SOURCES
modplatform/atlauncher/ATLPackIndex.cpp
modplatform/atlauncher/ATLPackIndex.h
modplatform/atlauncher/ATLPackInstallTask.cpp
modplatform/atlauncher/ATLPackInstallTask.h
modplatform/atlauncher/ATLPackManifest.cpp
modplatform/atlauncher/ATLPackManifest.h
)
add_unit_test(Index
SOURCES meta/Index_test.cpp
LIBS MultiMC_logic
@@ -518,6 +529,7 @@ set(LOGIC_SOURCES
${FLAME_SOURCES}
${MODPACKSCH_SOURCES}
${TECHNIC_SOURCES}
${ATLAUNCHER_SOURCES}
)
add_library(MultiMC_logic SHARED ${LOGIC_SOURCES})

View File

@@ -96,6 +96,7 @@ void Env::initHttpMetaCache()
m_metacache->addBase("fmllibs", QDir("mods/minecraftforge/libs").absolutePath());
m_metacache->addBase("liteloader", QDir("mods/liteloader").absolutePath());
m_metacache->addBase("general", QDir("cache").absolutePath());
m_metacache->addBase("ATLauncherPacks", QDir("cache/ATLauncherPacks").absolutePath());
m_metacache->addBase("FTBPacks", QDir("cache/FTBPacks").absolutePath());
m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath());
m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath());

View File

@@ -138,7 +138,7 @@ void InstanceImportTask::processZipPack()
void InstanceImportTask::extractFinished()
{
m_packZip.reset();
if (m_extractFuture.result().isEmpty())
if (!m_extractFuture.result())
{
emitFailed(tr("Failed to extract modpack"));
return;

View File

@@ -24,6 +24,8 @@
#include "settings/SettingsObject.h"
#include "QObjectPtr.h"
#include <nonstd/optional>
class QuaZip;
namespace Flame
{
@@ -60,8 +62,8 @@ private: /* data */
QString m_archivePath;
bool m_downloadRequired = false;
std::unique_ptr<QuaZip> m_packZip;
QFuture<QStringList> m_extractFuture;
QFutureWatcher<QStringList> m_extractFutureWatcher;
QFuture<nonstd::optional<QStringList>> m_extractFuture;
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
enum class ModpackType{
Unknown,
MultiMC,

View File

@@ -208,16 +208,27 @@ bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & re
// ours
QStringList MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target)
nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target)
{
QDir directory(target);
QStringList extracted;
qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target;
if (!zip->goToFirstFile())
auto numEntries = zip->getEntriesCount();
if(numEntries < 0) {
qWarning() << "Failed to enumerate files in archive";
return nonstd::nullopt;
}
else if(numEntries == 0) {
qDebug() << "Extracting empty archives seems odd...";
return extracted;
}
else if (!zip->goToFirstFile())
{
qWarning() << "Failed to seek to first file in zip";
return QStringList();
return nonstd::nullopt;
}
do
{
QString name = zip->getCurrentFileName();
@@ -235,7 +246,7 @@ QStringList MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QSt
{
qWarning() << "Failed to extract file" << name << "to" << absFilePath;
JlCompress::removeFile(extracted);
return QStringList();
return nonstd::nullopt;
}
extracted.append(absFilePath);
qDebug() << "Extracted file" << name;
@@ -244,12 +255,58 @@ QStringList MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QSt
}
// ours
QStringList MMCZip::extractDir(QString fileCompressed, QString dir)
bool MMCZip::extractRelFile(QuaZip *zip, const QString &file, const QString &target)
{
return JlCompress::extractFile(zip, file, target);
}
// ours
nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString dir)
{
QuaZip zip(fileCompressed);
if (!zip.open(QuaZip::mdUnzip))
{
return {};
// check if this is a minimum size empty zip file...
QFileInfo fileInfo(fileCompressed);
if(fileInfo.size() == 22) {
return QStringList();
}
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();;
return nonstd::nullopt;
}
return MMCZip::extractSubDir(&zip, "", dir);
}
// ours
nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir)
{
QuaZip zip(fileCompressed);
if (!zip.open(QuaZip::mdUnzip))
{
// check if this is a minimum size empty zip file...
QFileInfo fileInfo(fileCompressed);
if(fileInfo.size() == 22) {
return QStringList();
}
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();;
return nonstd::nullopt;
}
return MMCZip::extractSubDir(&zip, subdir, dir);
}
// ours
bool MMCZip::extractFile(QString fileCompressed, QString file, QString target)
{
QuaZip zip(fileCompressed);
if (!zip.open(QuaZip::mdUnzip))
{
// check if this is a minimum size empty zip file...
QFileInfo fileInfo(fileCompressed);
if(fileInfo.size() == 22) {
return true;
}
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();
return false;
}
return MMCZip::extractRelFile(&zip, file, target);
}

View File

@@ -24,6 +24,7 @@
#include "multimc_logic_export.h"
#include <JlCompress.h>
#include <nonstd/optional>
namespace MMCZip
{
@@ -57,7 +58,9 @@ namespace MMCZip
/**
* Extract a subdirectory from an archive
*/
QStringList MULTIMC_LOGIC_EXPORT extractSubDir(QuaZip *zip, const QString & subdir, const QString &target);
nonstd::optional<QStringList> MULTIMC_LOGIC_EXPORT extractSubDir(QuaZip *zip, const QString & subdir, const QString &target);
bool MULTIMC_LOGIC_EXPORT extractRelFile(QuaZip *zip, const QString & file, const QString &target);
/**
* Extract a whole archive.
@@ -66,5 +69,26 @@ namespace MMCZip
* \param dir The directory to extract to, the current directory if left empty.
* \return The list of the full paths of the files extracted, empty on failure.
*/
QStringList MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString dir);
nonstd::optional<QStringList> MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString dir);
/**
* Extract a subdirectory from an archive
*
* \param fileCompressed The name of the archive.
* \param subdir The directory within the archive to extract
* \param dir The directory to extract to, the current directory if left empty.
* \return The list of the full paths of the files extracted, empty on failure.
*/
nonstd::optional<QStringList> MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString subdir, QString dir);
/**
* Extract a single file from an archive into a directory
*
* \param fileCompressed The name of the archive.
* \param file The file within the archive to extract
* \param dir The directory to extract to, the current directory if left empty.
* \return true for success or false for failure
*/
bool MULTIMC_LOGIC_EXPORT extractFile(QString fileCompressed, QString file, QString dir);
}

View File

@@ -114,6 +114,10 @@ public:
/// get the profile component by index
Component * getComponent(int index);
/// Add the component to the internal list of patches
// todo(merged): is this the best approach
void appendComponent(ComponentPtr component);
private:
void scheduleSave();
bool saveIsScheduled() const;
@@ -121,8 +125,6 @@ private:
/// apply the component patches. Catches all the errors and returns true/false for success/failure
void invalidateLaunchProfile();
/// Add the component to the internal list of patches
void appendComponent(ComponentPtr component);
/// insert component so that its index is ideally the specified one (returns real index)
void insertComponent(size_t index, ComponentPtr component);

View File

@@ -289,7 +289,7 @@ bool World::install(const QString &to, const QString &name)
{
return false;
}
ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath).isEmpty();
ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath);
}
else if(m_containerFile.isDir())
{

View File

@@ -0,0 +1,42 @@
#include "SkinDelete.h"
#include <QNetworkRequest>
#include <QHttpMultiPart>
#include <Env.h>
SkinDelete::SkinDelete(QObject *parent, AuthSessionPtr session)
: Task(parent), m_session(session)
{
}
void SkinDelete::executeTask()
{
QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active"));
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit());
QNetworkReply *rep = ENV.qnam().deleteResource(request);
m_reply = std::shared_ptr<QNetworkReply>(rep);
setStatus(tr("Deleting skin"));
connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress);
connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError)));
connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished()));
}
void SkinDelete::downloadError(QNetworkReply::NetworkError error)
{
// error happened during download.
qCritical() << "Network error: " << error;
emitFailed(m_reply->errorString());
}
void SkinDelete::downloadFinished()
{
// if the download failed
if (m_reply->error() != QNetworkReply::NetworkError::NoError)
{
emitFailed(QString("Network error: %1").arg(m_reply->errorString()));
m_reply.reset();
return;
}
emitSucceeded();
}

View File

@@ -0,0 +1,30 @@
#pragma once
#include <QFile>
#include <QtNetwork/QtNetwork>
#include <memory>
#include <minecraft/auth/AuthSession.h>
#include "tasks/Task.h"
#include "multimc_logic_export.h"
typedef std::shared_ptr<class SkinDelete> SkinDeletePtr;
class MULTIMC_LOGIC_EXPORT SkinDelete : public Task
{
Q_OBJECT
public:
SkinDelete(QObject *parent, AuthSessionPtr session);
virtual ~SkinDelete() = default;
private:
AuthSessionPtr m_session;
std::shared_ptr<QNetworkReply> m_reply;
protected:
virtual void executeTask();
public slots:
void downloadError(QNetworkReply::NetworkError);
void downloadFinished();
};

View File

@@ -3,15 +3,14 @@
#include <QHttpMultiPart>
#include <Env.h>
QByteArray getModelString(SkinUpload::Model model) {
QByteArray getVariant(SkinUpload::Model model) {
switch (model) {
case SkinUpload::STEVE:
return "";
case SkinUpload::ALEX:
return "slim";
default:
qDebug() << "Unknown skin type!";
return "";
case SkinUpload::STEVE:
return "CLASSIC";
case SkinUpload::ALEX:
return "SLIM";
}
}
@@ -22,25 +21,23 @@ SkinUpload::SkinUpload(QObject *parent, AuthSessionPtr session, QByteArray skin,
void SkinUpload::executeTask()
{
QNetworkRequest request(QUrl(QString("https://api.mojang.com/user/profile/%1/skin").arg(m_session->uuid)));
request.setRawHeader("Authorization", QString("Bearer: %1").arg(m_session->access_token).toLocal8Bit());
QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins"));
request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit());
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart model;
model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"model\""));
model.setBody(getModelString(m_model));
QHttpPart skin;
skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png"));
skin.setHeader(QNetworkRequest::ContentDispositionHeader,
QVariant("form-data; name=\"file\"; filename=\"skin.png\""));
skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\""));
skin.setBody(m_skin);
multiPart->append(model);
multiPart->append(skin);
QHttpPart model;
model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\""));
model.setBody(getVariant(m_model));
QNetworkReply *rep = ENV.qnam().put(request, multiPart);
multiPart->append(skin);
multiPart->append(model);
QNetworkReply *rep = ENV.qnam().post(request, multiPart);
m_reply = std::shared_ptr<QNetworkReply>(rep);
setStatus(tr("Uploading skin"));

View File

@@ -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]"), "");
}

View File

@@ -0,0 +1,36 @@
#pragma once
#include "ATLPackManifest.h"
#include <QString>
#include <QVector>
#include <QMetaType>
#include "multimc_logic_export.h"
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;
};
MULTIMC_LOGIC_EXPORT void loadIndexedPack(IndexedPack & m, QJsonObject & obj);
}
Q_DECLARE_METATYPE(ATLauncher::IndexedPack)

View File

@@ -0,0 +1,682 @@
#include <Env.h>
#include <quazip.h>
#include <QtConcurrent/QtConcurrent>
#include <MMCZip.h>
#include <minecraft/OneSixVersionFormat.h>
#include <Version.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(QString pack, QString version)
{
m_pack = pack;
m_version_name = version;
}
bool PackInstallTask::abort()
{
return true;
}
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 ") + "net.minecraft");
return;
}
auto ver = vlist->getVersion(m_version.minecraft);
if (!ver) {
emitFailed(tr("Failed to get local metadata index for ") + "net.minecraft" + " " + 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: ") + 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 ") + uid);
return Q_NULLPTR;
}
// todo: filter by Minecraft version
if(m_version.loader.recommended) {
return vlist.get()->getRecommended().get()->descriptor();
}
else if(m_version.loader.latest) {
return vlist.get()->at(0)->descriptor();
}
else if(m_version.loader.choose) {
// todo: implement
}
}
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: ") + 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);
jobPtr->addNetAction(Net::Download::makeCached(url, entry));
archivePath = entry->getFullPath();
connect(jobPtr.get(), &NetJob::succeeded, this, [&]()
{
jobPtr.reset();
extractConfigs();
});
connect(jobPtr.get(), &NetJob::failed, [&](QString reason)
{
jobPtr.reset();
emitFailed(reason);
});
connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total)
{
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();
setStatus(tr("Downloading mods..."));
jarmods.clear();
jobPtr.reset(new NetJob(tr("Mod download")));
for(const auto& mod : m_version.mods) {
// skip optional mods for now
if(mod.optional) 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: ") + mod.download_raw);
return;
case DownloadType::Direct:
url = mod.url;
break;
case DownloadType::Unknown:
emitFailed(tr("Unknown download type: ") + 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);
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);
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);
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)
{
jobPtr.reset();
emitFailed(reason);
});
connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total)
{
setProgress(current, total);
});
jobPtr->start();
}
void PackInstallTask::onModsDownloaded() {
qDebug() << "PackInstallTask::onModsDownloaded: " << QThread::currentThreadId();
jobPtr.reset();
if(modsToExtract.size() || modsToDecomp.size() || modsToCopy.size()) {
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)) {
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)) {
return;
}
components->saveNow();
instance.setName(m_instName);
instance.setIconKey(m_instIcon);
instanceSettings->resumeSave();
jarmods.clear();
emitSucceeded();
}
}

View File

@@ -0,0 +1,84 @@
#pragma once
#include <meta/VersionList.h>
#include "ATLPackManifest.h"
#include "InstanceTask.h"
#include "multimc_logic_export.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 MULTIMC_LOGIC_EXPORT PackInstallTask : public InstanceTask
{
Q_OBJECT
public:
explicit PackInstallTask(QString pack, QString version);
virtual ~PackInstallTask(){}
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:
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;
QFuture<bool> m_decompFuture;
QFutureWatcher<bool> m_decompFutureWatcher;
};
}

View File

@@ -0,0 +1,180 @@
#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.latest = Json::ensureBoolean(obj, QString("latest"), false);
p.choose = Json::ensureBoolean(obj, QString("choose"), false);
p.recommended = Json::ensureBoolean(obj, QString("recommended"), false);
auto metadata = Json::requireObject(obj, "metadata");
p.version = Json::requireString(metadata, "version");
}
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 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.optional = Json::ensureBoolean(obj, QString("optional"), false);
}
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);
}
}
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);
}
}

View File

@@ -0,0 +1,107 @@
#pragma once
#include <QString>
#include <QVector>
#include <QJsonObject>
#include <multimc_logic_export.h>
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;
bool optional;
};
struct PackVersion
{
QString version;
QString minecraft;
bool noConfigs;
QString mainClass;
QString extraArguments;
VersionLoader loader;
QVector<VersionLibrary> libraries;
QVector<VersionMod> mods;
};
MULTIMC_LOGIC_EXPORT void loadVersion(PackVersion & v, QJsonObject & obj);
}

View File

@@ -121,6 +121,7 @@ void PackInstallTask::install()
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");

View File

@@ -8,6 +8,8 @@
#include "meta/VersionList.h"
#include "PackHelpers.h"
#include <nonstd/optional>
namespace LegacyFTB {
class MULTIMC_LOGIC_EXPORT PackInstallTask : public InstanceTask
@@ -40,8 +42,8 @@ private slots:
private: /* data */
bool abortable = false;
std::unique_ptr<QuaZip> m_packZip;
QFuture<QStringList> m_extractFuture;
QFutureWatcher<QStringList> m_extractFutureWatcher;
QFuture<nonstd::optional<QStringList>> m_extractFuture;
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
NetJobPtr netJobContainer;
QString archivePath;

View File

@@ -91,6 +91,7 @@ void PackInstallTask::install()
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");

View File

@@ -79,7 +79,7 @@ void Technic::SingleZipPackInstallTask::downloadProgressChanged(qint64 current,
void Technic::SingleZipPackInstallTask::extractFinished()
{
m_packZip.reset();
if (m_extractFuture.result().isEmpty())
if (!m_extractFuture.result())
{
emitFailed(tr("Failed to extract modpack"));
return;

View File

@@ -25,6 +25,8 @@
#include <QStringList>
#include <QUrl>
#include <nonstd/optional>
namespace Technic {
class MULTIMC_LOGIC_EXPORT SingleZipPackInstallTask : public InstanceTask
@@ -51,8 +53,8 @@ private:
QString m_archivePath;
NetJobPtr m_filesNetJob;
std::unique_ptr<QuaZip> m_packZip;
QFuture<QStringList> m_extractFuture;
QFutureWatcher<QStringList> m_extractFutureWatcher;
QFuture<nonstd::optional<QStringList>> m_extractFuture;
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
};
} // namespace Technic

View File

@@ -117,7 +117,7 @@ void Technic::SolderPackInstallTask::downloadSucceeded()
while (m_modCount > i)
{
auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i));
if (MMCZip::extractDir(path, extractDir).isEmpty())
if (!MMCZip::extractDir(path, extractDir))
{
return false;
}