diff --git a/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt index c57d5783..fa1a9f57 100644 --- a/api/logic/CMakeLists.txt +++ b/api/logic/CMakeLists.txt @@ -291,6 +291,12 @@ set(MINECRAFT_SOURCES minecraft/ftb/FTBPlugin.h minecraft/ftb/FTBPlugin.cpp + # Curse + minecraft/curse/PackManifest.h + minecraft/curse/PackManifest.cpp + minecraft/curse/FileResolvingTask.h + minecraft/curse/FileResolvingTask.cpp + # Assets minecraft/AssetsUtils.h minecraft/AssetsUtils.cpp diff --git a/api/logic/InstanceImportTask.cpp b/api/logic/InstanceImportTask.cpp index 23897778..1b44ec7a 100644 --- a/api/logic/InstanceImportTask.cpp +++ b/api/logic/InstanceImportTask.cpp @@ -1,3 +1,4 @@ +#include "minecraft/onesix/OneSixInstance.h" #include "InstanceImportTask.h" #include "BaseInstance.h" @@ -9,6 +10,9 @@ #include "settings/INISettingsObject.h" #include "icons/IIconList.h" #include +#include "minecraft/curse/FileResolvingTask.h" +#include "minecraft/curse/PackManifest.h" +#include "Json.h" InstanceImportTask::InstanceImportTask(SettingsObjectPtr settings, const QUrl sourceUrl, BaseInstanceProvider * target, const QString &instName, const QString &instIcon, const QString &instGroup) @@ -107,18 +111,87 @@ void InstanceImportTask::extractFinished() } QDir extractDir(m_stagingPath); const QFileInfo instanceCfgFile = findRecursive(extractDir.absolutePath(), "instance.cfg"); - if (!instanceCfgFile.isFile() || !instanceCfgFile.exists()) + const QFileInfo curseJson = findRecursive(extractDir.absolutePath(), "manifest.json"); + if (instanceCfgFile.isFile()) + { + processMultiMC(instanceCfgFile); + } + else if (curseJson.isFile()) + { + processCurse(curseJson); + } + else { m_target->destroyStagingPath(m_stagingPath); - emitFailed(tr("Archive does not contain instance.cfg")); + emitFailed(tr("Archive does not contain a recognized modpack type.")); + } +} + +void InstanceImportTask::extractAborted() +{ + m_target->destroyStagingPath(m_stagingPath); + emitFailed(tr("Instance import has been aborted.")); + return; +} + +void InstanceImportTask::processCurse(const QFileInfo & manifest) +{ + Curse::Manifest pack; + try + { + Curse::loadManifest(pack, manifest.absoluteFilePath()); + } + catch (JSONValidationError & e) + { + emitFailed(tr("Could not understand curse manifest:\n") + e.cause()); return; } + m_packRoot = manifest.absolutePath(); + QString configPath = FS::PathCombine(m_packRoot, "instance.cfg"); + auto instanceSettings = std::make_shared(configPath); + instanceSettings->registerSetting("InstanceType", "Legacy"); + instanceSettings->set("InstanceType", "OneSix"); + OneSixInstance instance(m_globalSettings, instanceSettings, m_packRoot); + instance.setIntendedVersionId(pack.minecraft.version); + instance.setName(m_instName); + instance.setIconKey(m_instIcon); + m_curseResolver.reset(new Curse::FileResolvingTask(pack.files)); + connect(m_curseResolver.get(), &Curse::FileResolvingTask::succeeded, this, &InstanceImportTask::curseResolvingSucceeded); + connect(m_curseResolver.get(), &Curse::FileResolvingTask::failed, this, &InstanceImportTask::curseResolvingFailed); + m_curseResolver->start(); +} +void InstanceImportTask::curseResolvingFailed(QString reason) +{ + m_target->destroyStagingPath(m_stagingPath); + m_curseResolver.reset(); + emitFailed(tr("Unable to resolve Curse mod IDs:\n") + reason); +} + +void InstanceImportTask::curseResolvingSucceeded() +{ + auto results = m_curseResolver->getResults(); + for(auto result: results) + { + qDebug() << result.fileName << " = " << result.url; + } + m_curseResolver.reset(); + if (!m_target->commitStagedInstance(m_stagingPath, m_packRoot, m_instName, m_instGroup)) + { + m_target->destroyStagingPath(m_stagingPath); + emitFailed(tr("Unable to commit instance")); + return; + } + emitSucceeded(); +} + +void InstanceImportTask::processMultiMC(const QFileInfo & config) +{ // FIXME: copy from FolderInstanceProvider!!! FIX IT!!! - auto instanceSettings = std::make_shared(instanceCfgFile.absoluteFilePath()); + auto instanceSettings = std::make_shared(config.absoluteFilePath()); instanceSettings->registerSetting("InstanceType", "Legacy"); - QString actualDir = instanceCfgFile.absolutePath(); + QString actualDir = config.absolutePath(); NullInstance instance(m_globalSettings, instanceSettings, actualDir); // reset time played on import... because packs. @@ -155,10 +228,3 @@ void InstanceImportTask::extractFinished() } emitSucceeded(); } - -void InstanceImportTask::extractAborted() -{ - m_target->destroyStagingPath(m_stagingPath); - emitFailed(tr("Instance import has been aborted.")); - return; -} diff --git a/api/logic/InstanceImportTask.h b/api/logic/InstanceImportTask.h index fe227e07..0bd7ddf8 100644 --- a/api/logic/InstanceImportTask.h +++ b/api/logic/InstanceImportTask.h @@ -7,8 +7,13 @@ #include #include #include "settings/SettingsObject.h" +#include "QObjectPtr.h" class BaseInstanceProvider; +namespace Curse +{ + class FileResolvingTask; +} class MULTIMC_LOGIC_EXPORT InstanceImportTask : public Task { @@ -23,6 +28,8 @@ protected: private: void extractAndTweak(); + void processMultiMC(const QFileInfo &config); + void processCurse(const QFileInfo &manifest); private slots: void downloadSucceeded(); @@ -30,14 +37,18 @@ private slots: void downloadProgressChanged(qint64 current, qint64 total); void extractFinished(); void extractAborted(); + void curseResolvingSucceeded(); + void curseResolvingFailed(QString reason); private: /* data */ SettingsObjectPtr m_globalSettings; NetJobPtr m_filesNetJob; + shared_qobject_ptr m_curseResolver; QUrl m_sourceUrl; BaseInstanceProvider * m_target; QString m_archivePath; bool m_downloadRequired = false; + QString m_packRoot; QString m_instName; QString m_instIcon; QString m_instGroup; diff --git a/api/logic/minecraft/curse/FileResolvingTask.cpp b/api/logic/minecraft/curse/FileResolvingTask.cpp new file mode 100644 index 00000000..13308202 --- /dev/null +++ b/api/logic/minecraft/curse/FileResolvingTask.cpp @@ -0,0 +1,64 @@ +#include "FileResolvingTask.h" +#include "Json.h" + +const char * metabase = "https://cursemeta.dries007.net"; + +Curse::FileResolvingTask::FileResolvingTask(QVector& toProcess) + : m_toProcess(toProcess) +{ +} + +void Curse::FileResolvingTask::executeTask() +{ + m_dljob.reset(new NetJob("Curse file resolver")); + results.resize(m_toProcess.size()); + int index = 0; + for(auto & file: m_toProcess) + { + 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, &Curse::FileResolvingTask::netJobFinished); + m_dljob->start(); +} + +void Curse::FileResolvingTask::netJobFinished() +{ + bool failed = false; + int index = 0; + for(auto & bytes: results) + { + try + { + auto doc = Json::requireDocument(bytes); + auto obj = Json::requireObject(doc); + // result code signifies true failure. + if(obj.contains("code")) + { + failed = true; + continue; + } + auto & out = m_toProcess[index]; + out.fileName = Json::requireString(obj, "FileNameOnDisk"); + out.url = Json::requireString(obj, "DownloadURL"); + out.resolved = true; + } + catch(JSONValidationError & e) + { + failed = true; + } + index++; + } + if(!failed) + { + emitSucceeded(); + } + else + { + emitFailed(tr("Some curse ID resolving tasks failed.")); + } +} diff --git a/api/logic/minecraft/curse/FileResolvingTask.h b/api/logic/minecraft/curse/FileResolvingTask.h new file mode 100644 index 00000000..b7ca85d3 --- /dev/null +++ b/api/logic/minecraft/curse/FileResolvingTask.h @@ -0,0 +1,32 @@ +#pragma once + +#include "tasks/Task.h" +#include "net/NetJob.h" +#include "PackManifest.h" + +#include "multimc_logic_export.h" + +namespace Curse +{ +class MULTIMC_LOGIC_EXPORT FileResolvingTask : public Task +{ + Q_OBJECT +public: + explicit FileResolvingTask(QVector &toProcess); + const QVector &getResults() const + { + return m_toProcess; + } + +protected: + virtual void executeTask() override; + +protected slots: + void netJobFinished(); + +private: /* data */ + QVector m_toProcess; + QVector results; + NetJobPtr m_dljob; +}; +} diff --git a/api/logic/minecraft/curse/PackManifest.cpp b/api/logic/minecraft/curse/PackManifest.cpp new file mode 100644 index 00000000..a4ea703b --- /dev/null +++ b/api/logic/minecraft/curse/PackManifest.cpp @@ -0,0 +1,63 @@ +#include "PackManifest.h" +#include "Json.h" + +static void loadFileV1(Curse::File & f, QJsonObject & file) +{ + f.projectId = Json::requireInteger(file, "projectID"); + f.fileId = Json::requireInteger(file, "fileID"); + f.required = Json::requireBoolean(file, "required"); +} + +static void loadModloaderV1(Curse::Modloader & m, QJsonObject & modLoader) +{ + m.id = Json::requireString(modLoader, "id"); + m.primary = Json::ensureBoolean(modLoader, "primary", false); +} + +static void loadMinecraftV1(Curse::Minecraft & m, QJsonObject & minecraft) +{ + m.version = Json::requireString(minecraft, "version"); + auto arr = Json::ensureArray(minecraft, "modLoaders", QJsonArray()); + for (const auto & item : arr) + { + auto obj = Json::requireObject(item); + Curse::Modloader loader; + loadModloaderV1(loader, obj); + m.modLoaders.append(loader); + } +} + +static void loadManifestV1(Curse::Manifest & m, QJsonObject & manifest) +{ + auto mc = Json::requireObject(manifest, "minecraft"); + loadMinecraftV1(m.minecraft, mc); + m.name = Json::requireString(manifest, "name"); + m.version = Json::requireString(manifest, "version"); + m.author = Json::requireString(manifest, "author"); + auto arr = Json::ensureArray(manifest, "files", QJsonArray()); + for (const auto & item : arr) + { + auto obj = Json::requireObject(item); + Curse::File file; + loadFileV1(file, obj); + m.files.append(file); + } + m.overrides = Json::ensureString(manifest, "overrides", "overrides"); +} + +void Curse::loadManifest(Curse::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 Curse 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); +} diff --git a/api/logic/minecraft/curse/PackManifest.h b/api/logic/minecraft/curse/PackManifest.h new file mode 100644 index 00000000..8b9602a4 --- /dev/null +++ b/api/logic/minecraft/curse/PackManifest.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +namespace Curse +{ +struct File +{ + int projectId = 0; + int fileId = 0; + bool required = true; + + // our + bool resolved = false; + QString fileName; + QString url; +}; + +struct Modloader +{ + QString id; + bool primary = false; +}; + +struct Minecraft +{ + QString version; + QVector modLoaders; +}; + +struct Manifest +{ + QString manifestType; + int manifestVersion = 0; + Curse::Minecraft minecraft; + QString name; + QString version; + QString author; + QVector files; + QString overrides; +}; + +void loadManifest(Curse::Manifest & m, const QString &filepath); +}