From ab19b863417d7cfca7ff1a5121c2f41ed0a722d9 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Mon, 24 Aug 2020 23:13:43 +0100 Subject: [PATCH] GH-405 ATLauncher Support --- api/logic/CMakeLists.txt | 10 + api/logic/Env.cpp | 1 + api/logic/MMCZip.cpp | 28 + api/logic/MMCZip.h | 23 + api/logic/minecraft/PackProfile.h | 6 +- .../modplatform/atlauncher/ATLPackIndex.cpp | 33 + .../modplatform/atlauncher/ATLPackIndex.h | 36 + .../atlauncher/ATLPackInstallTask.cpp | 655 ++++++++++++++++++ .../atlauncher/ATLPackInstallTask.h | 72 ++ .../atlauncher/ATLPackManifest.cpp | 180 +++++ .../modplatform/atlauncher/ATLPackManifest.h | 107 +++ application/CMakeLists.txt | 14 + application/dialogs/NewInstanceDialog.cpp | 2 + .../modplatform/atlauncher/AtlFilterModel.cpp | 67 ++ .../modplatform/atlauncher/AtlFilterModel.h | 32 + .../pages/modplatform/atlauncher/AtlModel.cpp | 185 +++++ .../pages/modplatform/atlauncher/AtlModel.h | 52 ++ .../pages/modplatform/atlauncher/AtlPage.cpp | 100 +++ .../pages/modplatform/atlauncher/AtlPage.h | 78 +++ .../pages/modplatform/atlauncher/AtlPage.ui | 55 ++ application/resources/multimc/multimc.qrc | 4 + .../scalable/atlauncher-placeholder.png | Bin 0 -> 13542 bytes .../resources/multimc/scalable/atlauncher.svg | 15 + buildconfig/BuildConfig.h | 2 + 24 files changed, 1755 insertions(+), 2 deletions(-) create mode 100644 api/logic/modplatform/atlauncher/ATLPackIndex.cpp create mode 100644 api/logic/modplatform/atlauncher/ATLPackIndex.h create mode 100644 api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp create mode 100644 api/logic/modplatform/atlauncher/ATLPackInstallTask.h create mode 100644 api/logic/modplatform/atlauncher/ATLPackManifest.cpp create mode 100644 api/logic/modplatform/atlauncher/ATLPackManifest.h create mode 100644 application/pages/modplatform/atlauncher/AtlFilterModel.cpp create mode 100644 application/pages/modplatform/atlauncher/AtlFilterModel.h create mode 100644 application/pages/modplatform/atlauncher/AtlModel.cpp create mode 100644 application/pages/modplatform/atlauncher/AtlModel.h create mode 100644 application/pages/modplatform/atlauncher/AtlPage.cpp create mode 100644 application/pages/modplatform/atlauncher/AtlPage.h create mode 100644 application/pages/modplatform/atlauncher/AtlPage.ui create mode 100644 application/resources/multimc/scalable/atlauncher-placeholder.png create mode 100644 application/resources/multimc/scalable/atlauncher.svg diff --git a/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt index be4318a85..3d385b1ca 100644 --- a/api/logic/CMakeLists.txt +++ b/api/logic/CMakeLists.txt @@ -486,6 +486,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 +527,7 @@ set(LOGIC_SOURCES ${FLAME_SOURCES} ${MODPACKSCH_SOURCES} ${TECHNIC_SOURCES} + ${ATLAUNCHER_SOURCES} ) add_library(MultiMC_logic SHARED ${LOGIC_SOURCES}) diff --git a/api/logic/Env.cpp b/api/logic/Env.cpp index 14e434ec4..42a1cff7a 100644 --- a/api/logic/Env.cpp +++ b/api/logic/Env.cpp @@ -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()); diff --git a/api/logic/MMCZip.cpp b/api/logic/MMCZip.cpp index 2d18b2b80..50b95c8ea 100644 --- a/api/logic/MMCZip.cpp +++ b/api/logic/MMCZip.cpp @@ -243,6 +243,12 @@ QStringList MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QSt return extracted; } +// ours +bool MMCZip::extractRelFile(QuaZip *zip, const QString &file, const QString &target) +{ + return JlCompress::extractFile(zip, file, target); +} + // ours QStringList MMCZip::extractDir(QString fileCompressed, QString dir) { @@ -253,3 +259,25 @@ QStringList MMCZip::extractDir(QString fileCompressed, QString dir) } return MMCZip::extractSubDir(&zip, "", dir); } + +// ours +QStringList MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir) +{ + QuaZip zip(fileCompressed); + if (!zip.open(QuaZip::mdUnzip)) + { + return {}; + } + return MMCZip::extractSubDir(&zip, subdir, dir); +} + +// ours +bool MMCZip::extractFile(QString fileCompressed, QString file, QString target) +{ + QuaZip zip(fileCompressed); + if (!zip.open(QuaZip::mdUnzip)) + { + return {}; + } + return MMCZip::extractRelFile(&zip, file, target); +} diff --git a/api/logic/MMCZip.h b/api/logic/MMCZip.h index fca7dde06..beff2e4d7 100644 --- a/api/logic/MMCZip.h +++ b/api/logic/MMCZip.h @@ -59,6 +59,8 @@ namespace MMCZip */ 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. * @@ -67,4 +69,25 @@ namespace MMCZip * \return The list of the full paths of the files extracted, empty on failure. */ 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. + */ + 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); + } diff --git a/api/logic/minecraft/PackProfile.h b/api/logic/minecraft/PackProfile.h index 6a2a21ec4..e55e6a580 100644 --- a/api/logic/minecraft/PackProfile.h +++ b/api/logic/minecraft/PackProfile.h @@ -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); diff --git a/api/logic/modplatform/atlauncher/ATLPackIndex.cpp b/api/logic/modplatform/atlauncher/ATLPackIndex.cpp new file mode 100644 index 000000000..4d2cf1539 --- /dev/null +++ b/api/logic/modplatform/atlauncher/ATLPackIndex.cpp @@ -0,0 +1,33 @@ +#include "ATLPackIndex.h" + +#include + +#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, "system", false); + m.description = Json::ensureString(obj, "description", ""); + + m.safeName = Json::requireString(obj, "name").replace(QRegularExpression("[^A-Za-z0-9]"), ""); +} diff --git a/api/logic/modplatform/atlauncher/ATLPackIndex.h b/api/logic/modplatform/atlauncher/ATLPackIndex.h new file mode 100644 index 000000000..5e2e6487c --- /dev/null +++ b/api/logic/modplatform/atlauncher/ATLPackIndex.h @@ -0,0 +1,36 @@ +#pragma once + +#include "ATLPackManifest.h" + +#include +#include +#include + +#include "multimc_logic_export.h" + +namespace ATLauncher +{ + +struct IndexedVersion +{ + QString version; + QString minecraft; +}; + +struct IndexedPack +{ + int id; + int position; + QString name; + PackType type; + QVector versions; + bool system; + QString description; + + QString safeName; +}; + +MULTIMC_LOGIC_EXPORT void loadIndexedPack(IndexedPack & m, QJsonObject & obj); +} + +Q_DECLARE_METATYPE(ATLauncher::IndexedPack) diff --git a/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp b/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp new file mode 100644 index 000000000..5498ce386 --- /dev/null +++ b/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -0,0 +1,655 @@ +#include +#include +#include +#include +#include +#include +#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() +{ + 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() +{ + 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) { + installMods(); + } + else { + installConfigs(); + } +} + +void PackInstallTask::onDownloadFailed(QString reason) +{ + 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 profile) +{ + if(m_version.libraries.isEmpty()) { + return true; + } + + QList 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(); + 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->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 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(); + 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() +{ + setStatus(tr("Downloading configs...")); + jobPtr.reset(new NetJob(tr("Config download"))); + + auto path = QString("Configs/%1/%2").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() +{ + 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::finished, this, [&]() + { + installMods(); + }); + connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, [&]() + { + emitAborted(); + }); + m_extractFutureWatcher.setFuture(m_extractFuture); +} + +void PackInstallTask::installMods() +{ + 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; + } + + if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract || mod.type == ModType::ResourcePackExtract) { + auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", mod.url); + 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", mod.url); + 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 path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); + + qDebug() << "Will download" << url << "to" << path; + auto dl = Net::Download::makeFile(url, path); + jobPtr->addNetAction(dl); + + 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, [&]() + { + jobPtr.reset(); + extractMods(); + }); + 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::extractMods() +{ + setStatus(tr("Extracting mods...")); + + if(modsToExtract.isEmpty()) { + decompMods(); + return; + } + + auto modPath = modsToExtract.firstKey(); + auto mod = modsToExtract.value(modPath); + + 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"); + } + + qDebug() << "Extracting " + mod.file + " to " + extractToDir; + + QDir extractDir(m_stagingPath); + auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir); + + QString folderToExtract = ""; + if(mod.type == ModType::Extract) { + folderToExtract = mod.extractFolder; + } + + m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, modPath, folderToExtract, extractToPath); + connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, [&]() + { + extractMods(); + }); + connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, [&]() + { + emitAborted(); + }); + m_extractFutureWatcher.setFuture(m_extractFuture); + + modsToExtract.remove(modPath); +} + +void PackInstallTask::decompMods() +{ + setStatus(tr("Extracting 'decomp' mods...")); + + if(modsToDecomp.isEmpty()) { + install(); + return; + } + + auto modPath = modsToDecomp.firstKey(); + auto mod = modsToDecomp.value(modPath); + + auto extractToDir = getDirForModType(mod.decompType, mod.decompType_raw); + + QDir extractDir(m_stagingPath); + auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir, mod.decompFile); + + qWarning() << "Extracting " + mod.decompFile + " to " + extractToDir; + + m_decompFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractFile, modPath, mod.decompFile, extractToPath); + connect(&m_decompFutureWatcher, &QFutureWatcher::finished, this, [&]() + { + install(); + }); + connect(&m_decompFutureWatcher, &QFutureWatcher::canceled, this, [&]() + { + emitAborted(); + }); + m_decompFutureWatcher.setFuture(m_decompFuture); + + modsToDecomp.remove(modPath); +} + +void PackInstallTask::install() +{ + setStatus(tr("Installing modpack")); + + auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared(instanceConfigPath); + 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(); +} + +} diff --git a/api/logic/modplatform/atlauncher/ATLPackInstallTask.h b/api/logic/modplatform/atlauncher/ATLPackInstallTask.h new file mode 100644 index 000000000..12e6bcf57 --- /dev/null +++ b/api/logic/modplatform/atlauncher/ATLPackInstallTask.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#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" + +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); + +private: + QString getDirForModType(ModType type, QString raw); + QString getVersionForLoader(QString uid); + QString detectLibrary(VersionLibrary library); + + bool createLibrariesComponent(QString instanceRoot, std::shared_ptr profile); + bool createPackComponent(QString instanceRoot, std::shared_ptr profile); + + void installConfigs(); + void extractConfigs(); + void installMods(); + void extractMods(); + void decompMods(); + void install(); + +private: + NetJobPtr jobPtr; + QByteArray response; + + QString m_pack; + QString m_version_name; + PackVersion m_version; + + QMap modsToExtract; + QMap modsToDecomp; + + QString archivePath; + QStringList jarmods; + Meta::VersionPtr minecraftVersion; + QMap componentsToInstall; + + QFuture m_extractFuture; + QFutureWatcher m_extractFutureWatcher; + + QFuture m_decompFuture; + QFutureWatcher m_decompFutureWatcher; + +}; + +} diff --git a/api/logic/modplatform/atlauncher/ATLPackManifest.cpp b/api/logic/modplatform/atlauncher/ATLPackManifest.cpp new file mode 100644 index 000000000..de3ec2329 --- /dev/null +++ b/api/logic/modplatform/atlauncher/ATLPackManifest.cpp @@ -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, "latest", false); + p.choose = Json::ensureBoolean(obj, "choose", false); + p.recommended = Json::ensureBoolean(obj, "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, "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, "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); + } +} diff --git a/api/logic/modplatform/atlauncher/ATLPackManifest.h b/api/logic/modplatform/atlauncher/ATLPackManifest.h new file mode 100644 index 000000000..1adf889bc --- /dev/null +++ b/api/logic/modplatform/atlauncher/ATLPackManifest.h @@ -0,0 +1,107 @@ +#pragma once + +#include +#include +#include +#include + +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 libraries; + QVector mods; +}; + +MULTIMC_LOGIC_EXPORT void loadVersion(PackVersion & v, QJsonObject & obj); + +} diff --git a/application/CMakeLists.txt b/application/CMakeLists.txt index 1a3bd1c32..a81327e34 100644 --- a/application/CMakeLists.txt +++ b/application/CMakeLists.txt @@ -124,25 +124,38 @@ SET(MULTIMC_SOURCES # GUI - platform pages pages/modplatform/VanillaPage.cpp pages/modplatform/VanillaPage.h + + pages/modplatform/atlauncher/AtlModel.cpp + pages/modplatform/atlauncher/AtlModel.h + pages/modplatform/atlauncher/AtlFilterModel.cpp + pages/modplatform/atlauncher/AtlFilterModel.h + pages/modplatform/atlauncher/AtlPage.cpp + pages/modplatform/atlauncher/AtlPage.h + pages/modplatform/atlauncher/AtlPage.h + pages/modplatform/ftb/FtbFilterModel.cpp pages/modplatform/ftb/FtbFilterModel.h pages/modplatform/ftb/FtbListModel.cpp pages/modplatform/ftb/FtbListModel.h pages/modplatform/ftb/FtbPage.cpp pages/modplatform/ftb/FtbPage.h + pages/modplatform/legacy_ftb/Page.cpp pages/modplatform/legacy_ftb/Page.h pages/modplatform/legacy_ftb/ListModel.h pages/modplatform/legacy_ftb/ListModel.cpp + pages/modplatform/twitch/TwitchData.h pages/modplatform/twitch/TwitchModel.cpp pages/modplatform/twitch/TwitchModel.h pages/modplatform/twitch/TwitchPage.cpp pages/modplatform/twitch/TwitchPage.h + pages/modplatform/technic/TechnicModel.cpp pages/modplatform/technic/TechnicModel.h pages/modplatform/technic/TechnicPage.cpp pages/modplatform/technic/TechnicPage.h + pages/modplatform/ImportPage.cpp pages/modplatform/ImportPage.h @@ -260,6 +273,7 @@ SET(MULTIMC_UIS # Platform pages pages/modplatform/VanillaPage.ui + pages/modplatform/atlauncher/AtlPage.ui pages/modplatform/ftb/FtbPage.ui pages/modplatform/legacy_ftb/Page.ui pages/modplatform/twitch/TwitchPage.ui diff --git a/application/dialogs/NewInstanceDialog.cpp b/application/dialogs/NewInstanceDialog.cpp index 4035cb9f2..d70cbffed 100644 --- a/application/dialogs/NewInstanceDialog.cpp +++ b/application/dialogs/NewInstanceDialog.cpp @@ -34,6 +34,7 @@ #include "widgets/PageContainer.h" #include +#include #include #include #include @@ -129,6 +130,7 @@ QList NewInstanceDialog::getPages() { new VanillaPage(this), importPage, + new AtlPage(this), new FtbPage(this), new LegacyFTB::Page(this), technicPage, diff --git a/application/pages/modplatform/atlauncher/AtlFilterModel.cpp b/application/pages/modplatform/atlauncher/AtlFilterModel.cpp new file mode 100644 index 000000000..8ea1546ae --- /dev/null +++ b/application/pages/modplatform/atlauncher/AtlFilterModel.cpp @@ -0,0 +1,67 @@ +#include "AtlFilterModel.h" + +#include + +#include +#include +#include + +namespace Atl { + +FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent) +{ + currentSorting = Sorting::ByPopularity; + sortings.insert(tr("Sort by popularity"), Sorting::ByPopularity); + sortings.insert(tr("Sort by name"), Sorting::ByName); + sortings.insert(tr("Sort by game version"), Sorting::ByGameVersion); +} + +const QMap FilterModel::getAvailableSortings() +{ + return sortings; +} + +QString FilterModel::translateCurrentSorting() +{ + return sortings.key(currentSorting); +} + +void FilterModel::setSorting(Sorting sorting) +{ + currentSorting = sorting; + invalidate(); +} + +FilterModel::Sorting FilterModel::getCurrentSorting() +{ + return currentSorting; +} + +bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + return true; +} + +bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + ATLauncher::IndexedPack leftPack = sourceModel()->data(left, Qt::UserRole).value(); + ATLauncher::IndexedPack rightPack = sourceModel()->data(right, Qt::UserRole).value(); + + if (currentSorting == ByPopularity) { + return leftPack.position > rightPack.position; + } + else if (currentSorting == ByGameVersion) { + Version lv(leftPack.versions.at(0).minecraft); + Version rv(rightPack.versions.at(0).minecraft); + return lv < rv; + } + else if (currentSorting == ByName) { + return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; + } + + // Invalid sorting set, somehow... + qWarning() << "Invalid sorting set!"; + return true; +} + +} diff --git a/application/pages/modplatform/atlauncher/AtlFilterModel.h b/application/pages/modplatform/atlauncher/AtlFilterModel.h new file mode 100644 index 000000000..2aef81fb6 --- /dev/null +++ b/application/pages/modplatform/atlauncher/AtlFilterModel.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +namespace Atl { + +class FilterModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + FilterModel(QObject* parent = Q_NULLPTR); + enum Sorting { + ByPopularity, + ByGameVersion, + ByName, + }; + const QMap getAvailableSortings(); + QString translateCurrentSorting(); + void setSorting(Sorting sorting); + Sorting getCurrentSorting(); + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + +private: + QMap sortings; + Sorting currentSorting; + +}; + +} diff --git a/application/pages/modplatform/atlauncher/AtlModel.cpp b/application/pages/modplatform/atlauncher/AtlModel.cpp new file mode 100644 index 000000000..46e35ec62 --- /dev/null +++ b/application/pages/modplatform/atlauncher/AtlModel.cpp @@ -0,0 +1,185 @@ +#include "AtlModel.h" + +#include +#include +#include + +namespace Atl { + +ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +ListModel::~ListModel() +{ +} + +int ListModel::rowCount(const QModelIndex &parent) const +{ + return modpacks.size(); +} + +int ListModel::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +QVariant ListModel::data(const QModelIndex &index, int role) const +{ + int pos = index.row(); + if(pos >= modpacks.size() || pos < 0 || !index.isValid()) + { + return QString("INVALID INDEX %1").arg(pos); + } + + ATLauncher::IndexedPack pack = modpacks.at(pos); + if(role == Qt::DisplayRole) + { + return pack.name; + } + else if (role == Qt::ToolTipRole) + { + return pack.description; + } + else if(role == Qt::DecorationRole) + { + if(m_logoMap.contains(pack.safeName)) + { + return (m_logoMap.value(pack.safeName)); + } + auto icon = MMC->getThemedIcon("atlauncher-placeholder"); + + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(pack.safeName.toLower()); + ((ListModel *)this)->requestLogo(pack.safeName, url); + + return icon; + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + + return QVariant(); +} + +void ListModel::request() +{ + beginResetModel(); + modpacks.clear(); + endResetModel(); + + auto *netJob = new NetJob("Atl::Request"); + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/json/packsnew.json"); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); + jobPtr = netJob; + jobPtr->start(); + + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed); +} + +void ListModel::requestFinished() +{ + 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 ATL at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + QList newList; + + auto packs = doc.array(); + for(auto packRaw : packs) { + auto packObj = packRaw.toObject(); + + ATLauncher::IndexedPack pack; + ATLauncher::loadIndexedPack(pack, packObj); + + // ignore packs without a published version + if(pack.versions.length() == 0) continue; + // only display public packs (for now) + if(pack.type != ATLauncher::PackType::Public) continue; + // ignore "system" packs (Vanilla, Vanilla with Forge, etc) + if(pack.system) continue; + + newList.append(pack); + } + + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); + modpacks.append(newList); + endInsertRows(); +} + +void ListModel::requestFailed(QString reason) +{ + jobPtr.reset(); +} + +void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) +{ + if(m_logoMap.contains(logo)) + { + callback(ENV.metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + } + else + { + requestLogo(logo, logoUrl); + } +} + +void ListModel::logoFailed(QString logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + +void ListModel::logoLoaded(QString logo, QIcon out) +{ + m_loadingLogos.removeAll(logo); + m_logoMap.insert(logo, out); + + for(int i = 0; i < modpacks.size(); i++) { + if(modpacks[i].safeName == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + } + } +} + +void ListModel::requestLogo(QString file, QString url) +{ + if(m_loadingLogos.contains(file) || m_failedLogos.contains(file)) + { + return; + } + + MetaEntryPtr entry = ENV.metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(file.section(".", 0, 0))); + NetJob *job = new NetJob(QString("ATLauncher Icon Download %1").arg(file)); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job, &NetJob::succeeded, this, [this, file, fullPath] + { + emit logoLoaded(file, QIcon(fullPath)); + if(waitingCallbacks.contains(file)) + { + waitingCallbacks.value(file)(fullPath); + } + }); + + QObject::connect(job, &NetJob::failed, this, [this, file] + { + emit logoFailed(file); + }); + + job->start(); + + m_loadingLogos.append(file); +} + +} diff --git a/application/pages/modplatform/atlauncher/AtlModel.h b/application/pages/modplatform/atlauncher/AtlModel.h new file mode 100644 index 000000000..2d30a64e6 --- /dev/null +++ b/application/pages/modplatform/atlauncher/AtlModel.h @@ -0,0 +1,52 @@ +#pragma once + +#include + +#include "net/NetJob.h" +#include +#include + +namespace Atl { + +typedef QMap LogoMap; +typedef std::function LogoCallback; + +class ListModel : public QAbstractListModel +{ + Q_OBJECT + +public: + ListModel(QObject *parent); + virtual ~ListModel(); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + + void request(); + + void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); + +private slots: + void requestFinished(); + void requestFailed(QString reason); + + void logoFailed(QString logo); + void logoLoaded(QString logo, QIcon out); + +private: + void requestLogo(QString file, QString url); + +private: + QList modpacks; + + QStringList m_failedLogos; + QStringList m_loadingLogos; + LogoMap m_logoMap; + QMap waitingCallbacks; + + NetJobPtr jobPtr; + QByteArray response; +}; + +} diff --git a/application/pages/modplatform/atlauncher/AtlPage.cpp b/application/pages/modplatform/atlauncher/AtlPage.cpp new file mode 100644 index 000000000..cfc61e8d4 --- /dev/null +++ b/application/pages/modplatform/atlauncher/AtlPage.cpp @@ -0,0 +1,100 @@ +#include "AtlPage.h" +#include "ui_AtlPage.h" + +#include "dialogs/NewInstanceDialog.h" +#include +#include + +AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), ui(new Ui::AtlPage), dialog(dialog) +{ + ui->setupUi(this); + + filterModel = new Atl::FilterModel(this); + listModel = new Atl::ListModel(this); + filterModel->setSourceModel(listModel); + ui->packView->setModel(filterModel); + ui->packView->setSortingEnabled(true); + + ui->packView->header()->hide(); + ui->packView->setIndentation(0); + + for(int i = 0; i < filterModel->getAvailableSortings().size(); i++) + { + ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i)); + } + ui->sortByBox->setCurrentText(filterModel->translateCurrentSorting()); + + connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &AtlPage::onSortingSelectionChanged); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &AtlPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &AtlPage::onVersionSelectionChanged); +} + +AtlPage::~AtlPage() +{ + delete ui; +} + +bool AtlPage::shouldDisplay() const +{ + return true; +} + +void AtlPage::openedImpl() +{ + listModel->request(); +} + +void AtlPage::suggestCurrent() +{ + if(isOpened) { + dialog->setSuggestedPack(selected.name, new ATLauncher::PackInstallTask(selected.safeName, selectedVersion)); + } + + auto editedLogoName = selected.safeName; + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(selected.safeName.toLower()); + listModel->getLogo(selected.safeName, url, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); +} + +void AtlPage::onSortingSelectionChanged(QString data) +{ + auto toSet = filterModel->getAvailableSortings().value(data); + filterModel->setSorting(toSet); +} + +void AtlPage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + ui->versionSelectionBox->clear(); + + if(!first.isValid()) + { + if(isOpened) + { + dialog->setSuggestedPack(); + } + return; + } + + selected = filterModel->data(first, Qt::UserRole).value(); + + for(const auto& version : selected.versions) { + ui->versionSelectionBox->addItem(version.version); + } + + suggestCurrent(); +} + +void AtlPage::onVersionSelectionChanged(QString data) +{ + if(data.isNull() || data.isEmpty()) + { + selectedVersion = ""; + return; + } + + selectedVersion = data; + suggestCurrent(); +} diff --git a/application/pages/modplatform/atlauncher/AtlPage.h b/application/pages/modplatform/atlauncher/AtlPage.h new file mode 100644 index 000000000..fceb0abfe --- /dev/null +++ b/application/pages/modplatform/atlauncher/AtlPage.h @@ -0,0 +1,78 @@ +/* Copyright 2013-2019 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 "AtlFilterModel.h" +#include "AtlModel.h" + +#include + +#include "MultiMC.h" +#include "pages/BasePage.h" +#include "tasks/Task.h" + +namespace Ui +{ + class AtlPage; +} + +class NewInstanceDialog; + +class AtlPage : public QWidget, public BasePage +{ +Q_OBJECT + +public: + explicit AtlPage(NewInstanceDialog* dialog, QWidget *parent = 0); + virtual ~AtlPage(); + virtual QString displayName() const override + { + return tr("ATLauncher"); + } + virtual QIcon icon() const override + { + return MMC->getThemedIcon("atlauncher"); + } + virtual QString id() const override + { + return "atl"; + } + virtual QString helpPage() const override + { + return "ATL-platform"; + } + virtual bool shouldDisplay() const override; + + void openedImpl() override; + +private: + void suggestCurrent(); + +private slots: + void onSortingSelectionChanged(QString data); + + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString data); + +private: + Ui::AtlPage *ui = nullptr; + NewInstanceDialog* dialog = nullptr; + Atl::ListModel* listModel = nullptr; + Atl::FilterModel* filterModel = nullptr; + + ATLauncher::IndexedPack selected; + QString selectedVersion; +}; diff --git a/application/pages/modplatform/atlauncher/AtlPage.ui b/application/pages/modplatform/atlauncher/AtlPage.ui new file mode 100644 index 000000000..fa88597e6 --- /dev/null +++ b/application/pages/modplatform/atlauncher/AtlPage.ui @@ -0,0 +1,55 @@ + + + AtlPage + + + + 0 + 0 + 875 + 745 + + + + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + true + + + + 96 + 48 + + + + + + + + packView + versionSelectionBox + + + + diff --git a/application/resources/multimc/multimc.qrc b/application/resources/multimc/multimc.qrc index 4f039a99a..4e95869ea 100644 --- a/application/resources/multimc/multimc.qrc +++ b/application/resources/multimc/multimc.qrc @@ -17,6 +17,10 @@ scalable/technic.svg + + scalable/atlauncher.svg + scalable/atlauncher-placeholder.png + scalable/proxy.svg diff --git a/application/resources/multimc/scalable/atlauncher-placeholder.png b/application/resources/multimc/scalable/atlauncher-placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..f4314c4344536aefeaebb89d1d44d7a6ecd97ef0 GIT binary patch literal 13542 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 aA7IE(WK~u6+Q1CjdF1Kp=d#Wzp$Pz5uYSS+ literal 0 HcmV?d00001 diff --git a/application/resources/multimc/scalable/atlauncher.svg b/application/resources/multimc/scalable/atlauncher.svg new file mode 100644 index 000000000..1bb5f3598 --- /dev/null +++ b/application/resources/multimc/scalable/atlauncher.svg @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 7e85aa5ee..02a9297a9 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -79,6 +79,8 @@ public: QString LEGACY_FTB_CDN_BASE_URL = "https://dist.creeper.host/FTB2/"; + QString ATL_DOWNLOAD_SERVER_URL = "https://download.nodecdn.net/containers/atl/"; + /** * \brief Converts the Version to a string. * \return The version number in string format (major.minor.revision.build).