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 be4318a8..3d385b1c 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 14e434ec..42a1cff7 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 2d18b2b8..50b95c8e 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 fca7dde0..beff2e4d 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 6a2a21ec..e55e6a58 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 00000000..4d2cf153 --- /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 00000000..5e2e6487 --- /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 00000000..5498ce38 --- /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 00000000..12e6bcf5 --- /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 00000000..de3ec232 --- /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 00000000..1adf889b --- /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 1a3bd1c3..a81327e3 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 4035cb9f..d70cbffe 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 00000000..8ea1546a --- /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 00000000..2aef81fb --- /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 00000000..46e35ec6 --- /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 00000000..2d30a64e --- /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 00000000..cfc61e8d --- /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 00000000..fceb0abf --- /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 00000000..fa88597e --- /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 4f039a99..4e95869e 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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 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 00000000..1bb5f359 --- /dev/null +++ b/application/resources/multimc/scalable/atlauncher.svg @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 7e85aa5e..02a9297a 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).