From 0f2757f0004e20503b2c4ee587e16f6775ee14bf Mon Sep 17 00:00:00 2001 From: AbigailBuccaneer Date: Mon, 24 Aug 2020 17:43:58 +0100 Subject: [PATCH 01/15] Remove extra semicolons from big-endian handling Fixes #3296. --- libraries/classparser/src/javaendian.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/classparser/src/javaendian.h b/libraries/classparser/src/javaendian.h index 076bff5e..6eb4de63 100644 --- a/libraries/classparser/src/javaendian.h +++ b/libraries/classparser/src/javaendian.h @@ -11,32 +11,32 @@ inline uint64_t bigswap(uint64_t x) { return x; } -; + inline uint32_t bigswap(uint32_t x) { return x; } -; + inline uint16_t bigswap(uint16_t x) { return x; } -; + inline int64_t bigswap(int64_t x) { return x; } -; + inline int32_t bigswap(int32_t x) { return x; } -; + inline int16_t bigswap(int16_t x) { return x; } -; + #else inline uint64_t bigswap(uint64_t x) { From 6995a2e1bac6812ec4f48b98cfaf321f75c320d8 Mon Sep 17 00:00:00 2001 From: AbigailBuccaneer Date: Mon, 24 Aug 2020 18:04:37 +0100 Subject: [PATCH 02/15] Avoid undefined behaviour when byteswapping `a << b` is undefined when `a` is negative, and `a >> b` is implementation-defined. The correct thing to do here is to cast to unsigned, swap the bytes there and then swap back. This also improves performance on some compilers: Clang is smart enough to recognise that we're byteswapping here and reduce it to a single `bswap` instruction on x86_64, but only for the unsigned versions. --- libraries/classparser/src/javaendian.h | 27 +++++--------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/libraries/classparser/src/javaendian.h b/libraries/classparser/src/javaendian.h index 6eb4de63..5a6e107b 100644 --- a/libraries/classparser/src/javaendian.h +++ b/libraries/classparser/src/javaendian.h @@ -22,21 +22,6 @@ inline uint16_t bigswap(uint16_t x) return x; } -inline int64_t bigswap(int64_t x) -{ - return x; -} - -inline int32_t bigswap(int32_t x) -{ - return x; -} - -inline int16_t bigswap(int16_t x) -{ - return x; -} - #else inline uint64_t bigswap(uint64_t x) { @@ -55,22 +40,20 @@ inline uint16_t bigswap(uint16_t x) return (x >> 8) | (x << 8); } +#endif + inline int64_t bigswap(int64_t x) { - return (x >> 56) | ((x << 40) & 0x00FF000000000000) | ((x << 24) & 0x0000FF0000000000) | - ((x << 8) & 0x000000FF00000000) | ((x >> 8) & 0x00000000FF000000) | - ((x >> 24) & 0x0000000000FF0000) | ((x >> 40) & 0x000000000000FF00) | (x << 56); + return static_cast(bigswap(static_cast(x))); } inline int32_t bigswap(int32_t x) { - return (x >> 24) | ((x << 8) & 0x00FF0000) | ((x >> 8) & 0x0000FF00) | (x << 24); + return static_cast(bigswap(static_cast(x))); } inline int16_t bigswap(int16_t x) { - return (x >> 8) | (x << 8); + return static_cast(bigswap(static_cast(x))); } - -#endif } From ab19b863417d7cfca7ff1a5121c2f41ed0a722d9 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Mon, 24 Aug 2020 23:13:43 +0100 Subject: [PATCH 03/15] 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). From 85d5e9ff2385b7f96d295f7affb459fdb16e2145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 7 Feb 2021 23:51:10 +0100 Subject: [PATCH 04/15] NOISSUE fix some build fails on linux and windows --- api/logic/modplatform/atlauncher/ATLPackIndex.cpp | 2 +- api/logic/modplatform/atlauncher/ATLPackManifest.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/logic/modplatform/atlauncher/ATLPackIndex.cpp b/api/logic/modplatform/atlauncher/ATLPackIndex.cpp index 4d2cf153..35f50b18 100644 --- a/api/logic/modplatform/atlauncher/ATLPackIndex.cpp +++ b/api/logic/modplatform/atlauncher/ATLPackIndex.cpp @@ -26,7 +26,7 @@ void ATLauncher::loadIndexedPack(ATLauncher::IndexedPack & m, QJsonObject & obj) loadIndexedVersion(version, versionObj); m.versions.append(version); } - m.system = Json::ensureBoolean(obj, "system", false); + m.system = Json::ensureBoolean(obj, QString("system"), false); m.description = Json::ensureString(obj, "description", ""); m.safeName = Json::requireString(obj, "name").replace(QRegularExpression("[^A-Za-z0-9]"), ""); diff --git a/api/logic/modplatform/atlauncher/ATLPackManifest.cpp b/api/logic/modplatform/atlauncher/ATLPackManifest.cpp index de3ec232..a31530f8 100644 --- a/api/logic/modplatform/atlauncher/ATLPackManifest.cpp +++ b/api/logic/modplatform/atlauncher/ATLPackManifest.cpp @@ -81,9 +81,9 @@ static ATLauncher::ModType parseModType(QString rawType) { 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); + p.latest = Json::ensureBoolean(obj, QString("latest"), false); + p.choose = Json::ensureBoolean(obj, QString("choose"), false); + p.recommended = Json::ensureBoolean(obj, QString("recommended"), false); auto metadata = Json::requireObject(obj, "metadata"); p.version = Json::requireString(metadata, "version"); @@ -141,7 +141,7 @@ 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); + v.noConfigs = Json::ensureBoolean(obj, QString("noConfigs"), false); if(obj.contains("mainClass")) { auto main = Json::requireObject(obj, "mainClass"); From c54d922f2687c979804f309ff431a76d9ca0bd09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 7 Feb 2021 23:54:29 +0100 Subject: [PATCH 05/15] NOISSUE and fix one more build fail --- api/logic/modplatform/atlauncher/ATLPackManifest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/logic/modplatform/atlauncher/ATLPackManifest.cpp b/api/logic/modplatform/atlauncher/ATLPackManifest.cpp index a31530f8..84389330 100644 --- a/api/logic/modplatform/atlauncher/ATLPackManifest.cpp +++ b/api/logic/modplatform/atlauncher/ATLPackManifest.cpp @@ -134,7 +134,7 @@ static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) { p.decompFile = Json::requireString(obj, "decompFile"); } - p.optional = Json::ensureBoolean(obj, "optional", false); + p.optional = Json::ensureBoolean(obj, QString("optional"), false); } void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) From 434369ca7cc8e56181d75474ac099eba76f407dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Mon, 8 Feb 2021 02:23:35 +0100 Subject: [PATCH 06/15] NOISSUE tweak ATLauncher pack cache names to avoid filesystem issues --- .../modplatform/atlauncher/ATLPackInstallTask.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp b/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp index 5498ce38..9719cef3 100644 --- a/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -363,7 +363,7 @@ 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 path = QString("Configs/%1/%2.zip").arg(m_pack).arg(m_version_name); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.zip") .arg(m_pack).arg(m_version_name); auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", path); @@ -441,8 +441,12 @@ void PackInstallTask::installMods() return; } + QFileInfo fileName(mod.file); + auto cacheName = fileName.completeBaseName() + "-" + mod.md5 + "." + fileName.suffix(); + if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract || mod.type == ModType::ResourcePackExtract) { - auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", mod.url); + + auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", cacheName); entry->setStale(true); modsToExtract.insert(entry->getFullPath(), mod); @@ -450,7 +454,7 @@ void PackInstallTask::installMods() jobPtr->addNetAction(dl); } else if(mod.type == ModType::Decomp) { - auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", mod.url); + auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", cacheName); entry->setStale(true); modsToDecomp.insert(entry->getFullPath(), mod); From 13a7f8d3b7b7d92387099141fad81ca74adedf1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Tue, 9 Feb 2021 05:04:23 +0100 Subject: [PATCH 07/15] NOISSUE fix multiple issues in ATLauncher integration --- api/logic/InstanceImportTask.cpp | 2 +- api/logic/InstanceImportTask.h | 6 +- api/logic/MMCZip.cpp | 47 ++++- api/logic/MMCZip.h | 7 +- api/logic/minecraft/World.cpp | 2 +- .../atlauncher/ATLPackInstallTask.cpp | 178 ++++++++++-------- .../atlauncher/ATLPackInstallTask.h | 22 ++- .../modplatform/legacy_ftb/PackInstallTask.h | 6 +- .../technic/SingleZipPackInstallTask.cpp | 2 +- .../technic/SingleZipPackInstallTask.h | 6 +- .../technic/SolderPackInstallTask.cpp | 2 +- 11 files changed, 175 insertions(+), 105 deletions(-) diff --git a/api/logic/InstanceImportTask.cpp b/api/logic/InstanceImportTask.cpp index bd98f9d4..fe2cdd75 100644 --- a/api/logic/InstanceImportTask.cpp +++ b/api/logic/InstanceImportTask.cpp @@ -138,7 +138,7 @@ void InstanceImportTask::processZipPack() void InstanceImportTask::extractFinished() { m_packZip.reset(); - if (m_extractFuture.result().isEmpty()) + if (!m_extractFuture.result()) { emitFailed(tr("Failed to extract modpack")); return; diff --git a/api/logic/InstanceImportTask.h b/api/logic/InstanceImportTask.h index db658808..7291324d 100644 --- a/api/logic/InstanceImportTask.h +++ b/api/logic/InstanceImportTask.h @@ -24,6 +24,8 @@ #include "settings/SettingsObject.h" #include "QObjectPtr.h" +#include + class QuaZip; namespace Flame { @@ -60,8 +62,8 @@ private: /* data */ QString m_archivePath; bool m_downloadRequired = false; std::unique_ptr m_packZip; - QFuture m_extractFuture; - QFutureWatcher m_extractFutureWatcher; + QFuture> m_extractFuture; + QFutureWatcher> m_extractFutureWatcher; enum class ModpackType{ Unknown, MultiMC, diff --git a/api/logic/MMCZip.cpp b/api/logic/MMCZip.cpp index 50b95c8e..b25c61e7 100644 --- a/api/logic/MMCZip.cpp +++ b/api/logic/MMCZip.cpp @@ -208,16 +208,27 @@ bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & re // ours -QStringList MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target) +nonstd::optional MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target) { QDir directory(target); QStringList extracted; + qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target; - if (!zip->goToFirstFile()) + auto numEntries = zip->getEntriesCount(); + if(numEntries < 0) { + qWarning() << "Failed to enumerate files in archive"; + return nonstd::nullopt; + } + else if(numEntries == 0) { + qDebug() << "Extracting empty archives seems odd..."; + return extracted; + } + else if (!zip->goToFirstFile()) { qWarning() << "Failed to seek to first file in zip"; - return QStringList(); + return nonstd::nullopt; } + do { QString name = zip->getCurrentFileName(); @@ -235,7 +246,7 @@ QStringList MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QSt { qWarning() << "Failed to extract file" << name << "to" << absFilePath; JlCompress::removeFile(extracted); - return QStringList(); + return nonstd::nullopt; } extracted.append(absFilePath); qDebug() << "Extracted file" << name; @@ -250,23 +261,35 @@ bool MMCZip::extractRelFile(QuaZip *zip, const QString &file, const QString &tar } // ours -QStringList MMCZip::extractDir(QString fileCompressed, QString dir) +nonstd::optional MMCZip::extractDir(QString fileCompressed, QString dir) { QuaZip zip(fileCompressed); if (!zip.open(QuaZip::mdUnzip)) { - return {}; + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if(fileInfo.size() == 22) { + return QStringList(); + } + qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; + return nonstd::nullopt; } return MMCZip::extractSubDir(&zip, "", dir); } // ours -QStringList MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir) +nonstd::optional MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir) { QuaZip zip(fileCompressed); if (!zip.open(QuaZip::mdUnzip)) { - return {}; + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if(fileInfo.size() == 22) { + return QStringList(); + } + qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; + return nonstd::nullopt; } return MMCZip::extractSubDir(&zip, subdir, dir); } @@ -277,7 +300,13 @@ bool MMCZip::extractFile(QString fileCompressed, QString file, QString target) QuaZip zip(fileCompressed); if (!zip.open(QuaZip::mdUnzip)) { - return {}; + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if(fileInfo.size() == 22) { + return true; + } + qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError(); + return false; } return MMCZip::extractRelFile(&zip, file, target); } diff --git a/api/logic/MMCZip.h b/api/logic/MMCZip.h index beff2e4d..98d9cd5b 100644 --- a/api/logic/MMCZip.h +++ b/api/logic/MMCZip.h @@ -24,6 +24,7 @@ #include "multimc_logic_export.h" #include +#include namespace MMCZip { @@ -57,7 +58,7 @@ namespace MMCZip /** * Extract a subdirectory from an archive */ - QStringList MULTIMC_LOGIC_EXPORT extractSubDir(QuaZip *zip, const QString & subdir, const QString &target); + nonstd::optional 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); @@ -68,7 +69,7 @@ namespace MMCZip * \param dir The directory to extract to, the current directory if left empty. * \return The list of the full paths of the files extracted, empty on failure. */ - QStringList MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString dir); + nonstd::optional MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString dir); /** * Extract a subdirectory from an archive @@ -78,7 +79,7 @@ namespace MMCZip * \param dir The directory to extract to, the current directory if left empty. * \return The list of the full paths of the files extracted, empty on failure. */ - QStringList MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString subdir, QString dir); + nonstd::optional MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString subdir, QString dir); /** * Extract a single file from an archive into a directory diff --git a/api/logic/minecraft/World.cpp b/api/logic/minecraft/World.cpp index ddeaa3b6..a2b4dac7 100644 --- a/api/logic/minecraft/World.cpp +++ b/api/logic/minecraft/World.cpp @@ -289,7 +289,7 @@ bool World::install(const QString &to, const QString &name) { return false; } - ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath).isEmpty(); + ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath); } else if(m_containerFile.isDir()) { diff --git a/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp b/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp index 9719cef3..faa05e84 100644 --- a/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -31,6 +31,7 @@ bool PackInstallTask::abort() void PackInstallTask::executeTask() { + qDebug() << "PackInstallTask::executeTask: " << QThread::currentThreadId(); auto *netJob = new NetJob("ATLauncher::VersionFetch"); auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json") .arg(m_pack).arg(m_version_name); @@ -44,6 +45,7 @@ void PackInstallTask::executeTask() void PackInstallTask::onDownloadSucceeded() { + qDebug() << "PackInstallTask::onDownloadSucceeded: " << QThread::currentThreadId(); jobPtr.reset(); QJsonParseError parse_error; @@ -84,7 +86,7 @@ void PackInstallTask::onDownloadSucceeded() minecraftVersion = ver; if(m_version.noConfigs) { - installMods(); + downloadMods(); } else { installConfigs(); @@ -93,6 +95,7 @@ void PackInstallTask::onDownloadSucceeded() void PackInstallTask::onDownloadFailed(QString reason) { + qDebug() << "PackInstallTask::onDownloadFailed: " << QThread::currentThreadId(); jobPtr.reset(); emitFailed(reason); } @@ -360,6 +363,7 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr< void PackInstallTask::installConfigs() { + qDebug() << "PackInstallTask::installConfigs: " << QThread::currentThreadId(); setStatus(tr("Downloading configs...")); jobPtr.reset(new NetJob(tr("Config download"))); @@ -392,6 +396,7 @@ void PackInstallTask::installConfigs() void PackInstallTask::extractConfigs() { + qDebug() << "PackInstallTask::extractConfigs: " << QThread::currentThreadId(); setStatus(tr("Extracting configs...")); QDir extractDir(m_stagingPath); @@ -406,7 +411,7 @@ void PackInstallTask::extractConfigs() m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/minecraft"); connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, [&]() { - installMods(); + downloadMods(); }); connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, [&]() { @@ -415,8 +420,9 @@ void PackInstallTask::extractConfigs() m_extractFutureWatcher.setFuture(m_extractFuture); } -void PackInstallTask::installMods() +void PackInstallTask::downloadMods() { + qDebug() << "PackInstallTask::installMods: " << QThread::currentThreadId(); setStatus(tr("Downloading mods...")); jarmods.clear(); @@ -464,12 +470,17 @@ void PackInstallTask::installMods() 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); + auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", cacheName); + entry->setStale(true); + + auto dl = Net::Download::makeCached(url, entry); jobPtr->addNetAction(dl); + auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); + qDebug() << "Will download" << url << "to" << path; + modsToCopy[entry->getFullPath()] = path; + if(mod.type == ModType::Forge) { auto vlist = ENV.metadataIndex()->get("net.minecraftforge"); if(vlist) @@ -493,11 +504,7 @@ void PackInstallTask::installMods() } } - connect(jobPtr.get(), &NetJob::succeeded, this, [&]() - { - jobPtr.reset(); - extractMods(); - }); + connect(jobPtr.get(), &NetJob::succeeded, this, &PackInstallTask::onModsDownloaded); connect(jobPtr.get(), &NetJob::failed, [&](QString reason) { jobPtr.reset(); @@ -511,88 +518,103 @@ void PackInstallTask::installMods() jobPtr->start(); } -void PackInstallTask::extractMods() -{ - setStatus(tr("Extracting mods...")); +void PackInstallTask::onModsDownloaded() { + qDebug() << "PackInstallTask::onModsDownloaded: " << QThread::currentThreadId(); + jobPtr.reset(); - if(modsToExtract.isEmpty()) { - decompMods(); - return; + if(modsToExtract.size() || modsToDecomp.size() || modsToCopy.size()) { + m_modExtractFuture = QtConcurrent::run(QThreadPool::globalInstance(), this, &PackInstallTask::extractMods, modsToExtract, modsToDecomp, modsToCopy); + connect(&m_modExtractFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::onModsExtracted); + connect(&m_modExtractFutureWatcher, &QFutureWatcher::canceled, this, [&]() + { + emitAborted(); + }); + m_modExtractFutureWatcher.setFuture(m_modExtractFuture); } - - auto modPath = modsToExtract.firstKey(); - auto mod = modsToExtract.value(modPath); - - QString extractToDir; - if(mod.type == ModType::Extract) { - extractToDir = getDirForModType(mod.extractTo, mod.extractTo_raw); + else { + install(); } - 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()) { +void PackInstallTask::onModsExtracted() { + qDebug() << "PackInstallTask::onModsExtracted: " << QThread::currentThreadId(); + if(m_modExtractFuture.result()) { install(); - return; + } + else { + emitFailed(tr("Failed to extract mods...")); + } +} + +bool PackInstallTask::extractMods( + const QMap &toExtract, + const QMap &toDecomp, + const QMap &toCopy +) { + qDebug() << "PackInstallTask::extractMods: " << QThread::currentThreadId(); + + setStatus(tr("Extracting mods...")); + for (auto iter = toExtract.begin(); iter != toExtract.end(); iter++) { + auto &modPath = iter.key(); + auto &mod = iter.value(); + + QString extractToDir; + if(mod.type == ModType::Extract) { + extractToDir = getDirForModType(mod.extractTo, mod.extractTo_raw); + } + else if(mod.type == ModType::TexturePackExtract) { + extractToDir = FS::PathCombine("texturepacks", "extracted"); + } + else if(mod.type == ModType::ResourcePackExtract) { + extractToDir = FS::PathCombine("resourcepacks", "extracted"); + } + + QDir extractDir(m_stagingPath); + auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir); + + QString folderToExtract = ""; + if(mod.type == ModType::Extract) { + folderToExtract = mod.extractFolder; + folderToExtract.remove(QRegExp("^/")); + } + + qDebug() << "Extracting " + mod.file + " to " + extractToDir; + if(!MMCZip::extractDir(modPath, folderToExtract, extractToPath)) { + // assume error + return false; + } } - auto modPath = modsToDecomp.firstKey(); - auto mod = modsToDecomp.value(modPath); + for (auto iter = toDecomp.begin(); iter != toDecomp.end(); iter++) { + auto &modPath = iter.key(); + auto &mod = iter.value(); + auto extractToDir = getDirForModType(mod.decompType, mod.decompType_raw); - auto extractToDir = getDirForModType(mod.decompType, mod.decompType_raw); + QDir extractDir(m_stagingPath); + auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir, mod.decompFile); - QDir extractDir(m_stagingPath); - auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir, mod.decompFile); + qDebug() << "Extracting " + mod.decompFile + " to " + extractToDir; + if(!MMCZip::extractFile(modPath, mod.decompFile, extractToPath)) { + qWarning() << "Failed to extract" << mod.decompFile; + return false; + } + } - 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); + for (auto iter = toCopy.begin(); iter != toCopy.end(); iter++) { + auto &from = iter.key(); + auto &to = iter.value(); + FS::copy fileCopyOperation(from, to); + if(!fileCopyOperation()) { + qWarning() << "Failed to copy" << from << "to" << to; + return false; + } + } + return true; } void PackInstallTask::install() { + qDebug() << "PackInstallTask::install: " << QThread::currentThreadId(); setStatus(tr("Installing modpack")); auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); diff --git a/api/logic/modplatform/atlauncher/ATLPackInstallTask.h b/api/logic/modplatform/atlauncher/ATLPackInstallTask.h index 12e6bcf5..78544bab 100644 --- a/api/logic/modplatform/atlauncher/ATLPackInstallTask.h +++ b/api/logic/modplatform/atlauncher/ATLPackInstallTask.h @@ -11,6 +11,8 @@ #include "minecraft/PackProfile.h" #include "meta/Version.h" +#include + namespace ATLauncher { class MULTIMC_LOGIC_EXPORT PackInstallTask : public InstanceTask @@ -30,6 +32,9 @@ private slots: void onDownloadSucceeded(); void onDownloadFailed(QString reason); + void onModsDownloaded(); + void onModsExtracted(); + private: QString getDirForModType(ModType type, QString raw); QString getVersionForLoader(QString uid); @@ -40,9 +45,12 @@ private: void installConfigs(); void extractConfigs(); - void installMods(); - void extractMods(); - void decompMods(); + void downloadMods(); + bool extractMods( + const QMap &toExtract, + const QMap &toDecomp, + const QMap &toCopy + ); void install(); private: @@ -55,14 +63,18 @@ private: QMap modsToExtract; QMap modsToDecomp; + QMap modsToCopy; QString archivePath; QStringList jarmods; Meta::VersionPtr minecraftVersion; QMap componentsToInstall; - QFuture m_extractFuture; - QFutureWatcher m_extractFutureWatcher; + QFuture> m_extractFuture; + QFutureWatcher> m_extractFutureWatcher; + + QFuture m_modExtractFuture; + QFutureWatcher m_modExtractFutureWatcher; QFuture m_decompFuture; QFutureWatcher m_decompFutureWatcher; diff --git a/api/logic/modplatform/legacy_ftb/PackInstallTask.h b/api/logic/modplatform/legacy_ftb/PackInstallTask.h index 1eec1880..7868d1c4 100644 --- a/api/logic/modplatform/legacy_ftb/PackInstallTask.h +++ b/api/logic/modplatform/legacy_ftb/PackInstallTask.h @@ -8,6 +8,8 @@ #include "meta/VersionList.h" #include "PackHelpers.h" +#include + namespace LegacyFTB { class MULTIMC_LOGIC_EXPORT PackInstallTask : public InstanceTask @@ -40,8 +42,8 @@ private slots: private: /* data */ bool abortable = false; std::unique_ptr m_packZip; - QFuture m_extractFuture; - QFutureWatcher m_extractFutureWatcher; + QFuture> m_extractFuture; + QFutureWatcher> m_extractFutureWatcher; NetJobPtr netJobContainer; QString archivePath; diff --git a/api/logic/modplatform/technic/SingleZipPackInstallTask.cpp b/api/logic/modplatform/technic/SingleZipPackInstallTask.cpp index 9be99d06..96e1804d 100644 --- a/api/logic/modplatform/technic/SingleZipPackInstallTask.cpp +++ b/api/logic/modplatform/technic/SingleZipPackInstallTask.cpp @@ -79,7 +79,7 @@ void Technic::SingleZipPackInstallTask::downloadProgressChanged(qint64 current, void Technic::SingleZipPackInstallTask::extractFinished() { m_packZip.reset(); - if (m_extractFuture.result().isEmpty()) + if (!m_extractFuture.result()) { emitFailed(tr("Failed to extract modpack")); return; diff --git a/api/logic/modplatform/technic/SingleZipPackInstallTask.h b/api/logic/modplatform/technic/SingleZipPackInstallTask.h index ecf4445a..c56b9e46 100644 --- a/api/logic/modplatform/technic/SingleZipPackInstallTask.h +++ b/api/logic/modplatform/technic/SingleZipPackInstallTask.h @@ -25,6 +25,8 @@ #include #include +#include + namespace Technic { class MULTIMC_LOGIC_EXPORT SingleZipPackInstallTask : public InstanceTask @@ -51,8 +53,8 @@ private: QString m_archivePath; NetJobPtr m_filesNetJob; std::unique_ptr m_packZip; - QFuture m_extractFuture; - QFutureWatcher m_extractFutureWatcher; + QFuture> m_extractFuture; + QFutureWatcher> m_extractFutureWatcher; }; } // namespace Technic diff --git a/api/logic/modplatform/technic/SolderPackInstallTask.cpp b/api/logic/modplatform/technic/SolderPackInstallTask.cpp index a858de49..1d17073c 100644 --- a/api/logic/modplatform/technic/SolderPackInstallTask.cpp +++ b/api/logic/modplatform/technic/SolderPackInstallTask.cpp @@ -117,7 +117,7 @@ void Technic::SolderPackInstallTask::downloadSucceeded() while (m_modCount > i) { auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); - if (MMCZip::extractDir(path, extractDir).isEmpty()) + if (!MMCZip::extractDir(path, extractDir)) { return false; } From e8b9176736c8ab32731c4f01fe886de0c6377c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Tue, 9 Feb 2021 21:08:45 +0100 Subject: [PATCH 08/15] NOISSUE update macOS build instructions --- BUILD.md | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/BUILD.md b/BUILD.md index c120ec51..7aa1b497 100644 --- a/BUILD.md +++ b/BUILD.md @@ -7,7 +7,7 @@ Build Instructions * [Getting the source](#source) * [Linux](#linux) * [Windows](#windows) -* [OS X](#os-x) +* [macOS](#macos) # Note @@ -172,36 +172,32 @@ zlib1.dll 5. Run the command `mingw32-make install`, and it should install MultiMC, to whatever the `-DCMAKE_INSTALL_PREFIX` was. 6. In most cases, whenever compiling, the OpenSSL dll's aren't put into the directory to where MultiMC installs, meaning you cannot log in. The best way to fix this is just to do `copy C:\OpenSSL-Win32\*.dll C:\Where\you\installed\MultiMC\to`. This should copy the required OpenSSL dll's to log in. -# OS X +# macOS ### Install prerequisites: -* install homebrew -* then: - -``` -brew install qt5 -brew tap homebrew/versions -brew install gcc48 -brew install cmake -``` +- Install XCode and set it up to the point where you can build things from a terminal +- Install the official build of CMake (https://cmake.org/download/) +- Install JDK 8 (https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html) +- Get Qt 5.6 and install it (https://download.qt.io/new_archive/qt/5.6/5.6.3/) ### Build Pick an installation path - this is where the final `.app` will be constructed when you run `make install`. Supply it as the `CMAKE_INSTALL_PREFIX` argument during CMake configuration. ``` -git clone https://github.com/MultiMC/MultiMC5.git +git clone --recursive https://github.com/MultiMC/MultiMC5.git cd MultiMC5 -git submodule init -git submodule update mkdir build cd build -export CMAKE_PREFIX_PATH=/usr/local/opt/qt5 -export CC=/usr/local/bin/gcc-4.8 -export CXX=/usr/local/bin/g++-4.8 -cmake .. -DCMAKE_INSTALL_PREFIX:PATH=/Users/YOU/some/path/that/makes/sense/ -make +cmake \ + -DCMAKE_C_COMPILER=/usr/bin/clang \ + -DCMAKE_CXX_COMPILER=/usr/bin/clang++ \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX:PATH="../dist/" \ + -DCMAKE_PREFIX_PATH="/path/to/Qt5.6/" \ + -DQt5_DIR="/path/to/Qt5.6/" \ + -DMultiMC_LAYOUT=mac-bundle \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=10.7 \ + .. make install ``` - -**These build instructions were taken and adapted from https://gist.github.com/number5/7250865 If they don't work for you, let us know on IRC ([Esper/#MultiMC](http://webchat.esper.net/?nick=&channels=MultiMC))!** From f3205ebf25bad03021be824b9664409bf78f46dc Mon Sep 17 00:00:00 2001 From: AppleTheGolden Date: Tue, 9 Feb 2021 22:00:20 +0100 Subject: [PATCH 09/15] NOISSUE Swap discord invite with vanity url --- README.md | 2 +- application/MainWindow.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 20389ec4..ede8f88f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ The project uses C++ and Qt5 as the language and base framework. This might seem We can do more, with less, on worse hardware and leave more resources for the game while keeping the launcher running and providing extra features. -If you want to contribute, either talk to us on [Discord](https://discord.gg/0k2zsXGNHs0fE4Wm), [IRC](http://webchat.esper.net/?nick=&channels=MultiMC)(esper.net/#MultiMC) or pick up some item from the github issues [workflowy](https://github.com/MultiMC/MultiMC5/issues) - there is always plenty of ideas around. +If you want to contribute, either talk to us on [Discord](https://discord.gg/multimc), [IRC](http://webchat.esper.net/?nick=&channels=MultiMC)(esper.net/#MultiMC) or pick up some item from the github issues [workflowy](https://github.com/MultiMC/MultiMC5/issues) - there is always plenty of ideas around. ### Building If you want to build MultiMC yourself, check [BUILD.md](BUILD.md) for build instructions. diff --git a/application/MainWindow.cpp b/application/MainWindow.cpp index 24842e4f..1286007d 100644 --- a/application/MainWindow.cpp +++ b/application/MainWindow.cpp @@ -1460,7 +1460,7 @@ void MainWindow::on_actionREDDIT_triggered() void MainWindow::on_actionDISCORD_triggered() { - DesktopServices::openUrl(QUrl("https://discord.gg/0k2zsXGNHs0fE4Wm")); + DesktopServices::openUrl(QUrl("https://discord.gg/multimc")); } void MainWindow::on_actionChangeInstIcon_triggered() From 7265abf7636cc19b1b7c65f74b1111215914368c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Tue, 9 Feb 2021 22:46:51 +0100 Subject: [PATCH 10/15] NOISSUE sprinkle suspendSave all over pack import tasks --- api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp | 1 + api/logic/modplatform/legacy_ftb/PackInstallTask.cpp | 1 + api/logic/modplatform/modpacksch/FTBPackInstallTask.cpp | 1 + 3 files changed, 3 insertions(+) diff --git a/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp b/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp index faa05e84..25c6d58d 100644 --- a/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -619,6 +619,7 @@ void PackInstallTask::install() auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(instanceConfigPath); + instanceSettings->suspendSave(); instanceSettings->registerSetting("InstanceType", "Legacy"); instanceSettings->set("InstanceType", "OneSix"); diff --git a/api/logic/modplatform/legacy_ftb/PackInstallTask.cpp b/api/logic/modplatform/legacy_ftb/PackInstallTask.cpp index 9bf6c76a..c77f3250 100644 --- a/api/logic/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/api/logic/modplatform/legacy_ftb/PackInstallTask.cpp @@ -121,6 +121,7 @@ void PackInstallTask::install() QString instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(instanceConfigPath); + instanceSettings->suspendSave(); instanceSettings->registerSetting("InstanceType", "Legacy"); instanceSettings->set("InstanceType", "OneSix"); diff --git a/api/logic/modplatform/modpacksch/FTBPackInstallTask.cpp b/api/logic/modplatform/modpacksch/FTBPackInstallTask.cpp index b532af7f..dc2b05fe 100644 --- a/api/logic/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/api/logic/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -91,6 +91,7 @@ void PackInstallTask::install() auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(instanceConfigPath); + instanceSettings->suspendSave(); instanceSettings->registerSetting("InstanceType", "Legacy"); instanceSettings->set("InstanceType", "OneSix"); From 6b3b7ded2d9f30388219b97fb656313ef648e08e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Wed, 10 Feb 2021 03:32:17 +0100 Subject: [PATCH 11/15] GH-3130 fix uploading skins by using the new skins endpoint --- api/logic/minecraft/SkinUpload.cpp | 33 ++++++++++++++---------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/api/logic/minecraft/SkinUpload.cpp b/api/logic/minecraft/SkinUpload.cpp index 83bdf592..4e5a1698 100644 --- a/api/logic/minecraft/SkinUpload.cpp +++ b/api/logic/minecraft/SkinUpload.cpp @@ -3,15 +3,14 @@ #include #include -QByteArray getModelString(SkinUpload::Model model) { +QByteArray getVariant(SkinUpload::Model model) { switch (model) { - case SkinUpload::STEVE: - return ""; - case SkinUpload::ALEX: - return "slim"; default: qDebug() << "Unknown skin type!"; - return ""; + case SkinUpload::STEVE: + return "CLASSIC"; + case SkinUpload::ALEX: + return "SLIM"; } } @@ -22,25 +21,23 @@ SkinUpload::SkinUpload(QObject *parent, AuthSessionPtr session, QByteArray skin, void SkinUpload::executeTask() { - QNetworkRequest request(QUrl(QString("https://api.mojang.com/user/profile/%1/skin").arg(m_session->uuid))); - request.setRawHeader("Authorization", QString("Bearer: %1").arg(m_session->access_token).toLocal8Bit()); - + QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins")); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); - QHttpPart model; - model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"model\"")); - model.setBody(getModelString(m_model)); - QHttpPart skin; skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png")); - skin.setHeader(QNetworkRequest::ContentDispositionHeader, - QVariant("form-data; name=\"file\"; filename=\"skin.png\"")); + skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\"")); skin.setBody(m_skin); - multiPart->append(model); - multiPart->append(skin); + QHttpPart model; + model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\"")); + model.setBody(getVariant(m_model)); - QNetworkReply *rep = ENV.qnam().put(request, multiPart); + multiPart->append(skin); + multiPart->append(model); + + QNetworkReply *rep = ENV.qnam().post(request, multiPart); m_reply = std::shared_ptr(rep); setStatus(tr("Uploading skin")); From 003e0190482e7a8db669d82578c389b0339c2e4e Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Wed, 10 Feb 2021 16:11:59 +0000 Subject: [PATCH 12/15] NOISSUE Show ATLauncher pack descriptions in text browser --- .../pages/modplatform/atlauncher/AtlModel.cpp | 2 +- .../pages/modplatform/atlauncher/AtlPage.cpp | 2 + .../pages/modplatform/atlauncher/AtlPage.ui | 43 ++++++++++--------- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/application/pages/modplatform/atlauncher/AtlModel.cpp b/application/pages/modplatform/atlauncher/AtlModel.cpp index 46e35ec6..4b1b1c8e 100644 --- a/application/pages/modplatform/atlauncher/AtlModel.cpp +++ b/application/pages/modplatform/atlauncher/AtlModel.cpp @@ -39,7 +39,7 @@ QVariant ListModel::data(const QModelIndex &index, int role) const } else if (role == Qt::ToolTipRole) { - return pack.description; + return pack.name; } else if(role == Qt::DecorationRole) { diff --git a/application/pages/modplatform/atlauncher/AtlPage.cpp b/application/pages/modplatform/atlauncher/AtlPage.cpp index cfc61e8d..4518248d 100644 --- a/application/pages/modplatform/atlauncher/AtlPage.cpp +++ b/application/pages/modplatform/atlauncher/AtlPage.cpp @@ -80,6 +80,8 @@ void AtlPage::onSelectionChanged(QModelIndex first, QModelIndex second) selected = filterModel->data(first, Qt::UserRole).value(); + ui->packDescription->setHtml(selected.description.replace("\n", "
")); + for(const auto& version : selected.versions) { ui->versionSelectionBox->addItem(version.version); } diff --git a/application/pages/modplatform/atlauncher/AtlPage.ui b/application/pages/modplatform/atlauncher/AtlPage.ui index fa88597e..dd17283c 100644 --- a/application/pages/modplatform/atlauncher/AtlPage.ui +++ b/application/pages/modplatform/atlauncher/AtlPage.ui @@ -11,26 +11,6 @@ - - - - - - - - - Version selected: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - @@ -44,6 +24,29 @@ + + + + + + + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + From 6eabd336eef1855332573955a4ee225bd93c6bd8 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Wed, 10 Feb 2021 16:57:46 +0000 Subject: [PATCH 13/15] NOISSUE Show FTB pack descriptions in text browser --- application/pages/modplatform/ftb/FtbPage.cpp | 6 +++ application/pages/modplatform/ftb/FtbPage.ui | 52 +++++++++++-------- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/application/pages/modplatform/ftb/FtbPage.cpp b/application/pages/modplatform/ftb/FtbPage.cpp index 60294de0..dd2ff666 100644 --- a/application/pages/modplatform/ftb/FtbPage.cpp +++ b/application/pages/modplatform/ftb/FtbPage.cpp @@ -6,6 +6,8 @@ #include "dialogs/NewInstanceDialog.h" #include "modplatform/modpacksch/FTBPackInstallTask.h" +#include "HoeDown.h" + FtbPage::FtbPage(NewInstanceDialog* dialog, QWidget *parent) : QWidget(parent), ui(new Ui::FtbPage), dialog(dialog) { @@ -108,6 +110,10 @@ void FtbPage::onSelectionChanged(QModelIndex first, QModelIndex second) selected = filterModel->data(first, Qt::UserRole).value(); + HoeDown hoedown; + QString output = hoedown.process(selected.description.toUtf8()); + ui->packDescription->setHtml(output); + // reverse foreach, so that the newest versions are first for (auto i = selected.versions.size(); i--;) { ui->versionSelectionBox->addItem(selected.versions.at(i).name); diff --git a/application/pages/modplatform/ftb/FtbPage.ui b/application/pages/modplatform/ftb/FtbPage.ui index 3a2203db..475d78bb 100644 --- a/application/pages/modplatform/ftb/FtbPage.ui +++ b/application/pages/modplatform/ftb/FtbPage.ui @@ -11,16 +11,6 @@ - - - - Search - - - - - - @@ -41,25 +31,45 @@ - - - - true - - - - 48 - 48 - + + + + + + + Search + + + + + + + 48 + 48 + + + + + + + + true + + + true + + + + + searchEdit searchButton - packView versionSelectionBox From 152d476f20d06dff6aecca057c3d01776f3ac975 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Thu, 11 Feb 2021 00:19:49 +0000 Subject: [PATCH 14/15] NOISSUE Add search to ATLauncher --- .../modplatform/atlauncher/AtlFilterModel.cpp | 16 ++++- .../modplatform/atlauncher/AtlFilterModel.h | 2 + .../pages/modplatform/atlauncher/AtlPage.cpp | 12 ++++ .../pages/modplatform/atlauncher/AtlPage.h | 3 + .../pages/modplatform/atlauncher/AtlPage.ui | 63 ++++++++++++------- 5 files changed, 73 insertions(+), 23 deletions(-) diff --git a/application/pages/modplatform/atlauncher/AtlFilterModel.cpp b/application/pages/modplatform/atlauncher/AtlFilterModel.cpp index 8ea1546a..b5d8f22b 100644 --- a/application/pages/modplatform/atlauncher/AtlFilterModel.cpp +++ b/application/pages/modplatform/atlauncher/AtlFilterModel.cpp @@ -14,6 +14,8 @@ FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent) 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); + + searchTerm = ""; } const QMap FilterModel::getAvailableSortings() @@ -37,9 +39,21 @@ FilterModel::Sorting FilterModel::getCurrentSorting() return currentSorting; } +void FilterModel::setSearchTerm(const QString term) +{ + searchTerm = term.trimmed(); + invalidate(); +} + bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { - return true; + if (searchTerm.isEmpty()) { + return true; + } + + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + ATLauncher::IndexedPack pack = sourceModel()->data(index, Qt::UserRole).value(); + return pack.name.contains(searchTerm, Qt::CaseInsensitive); } bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const diff --git a/application/pages/modplatform/atlauncher/AtlFilterModel.h b/application/pages/modplatform/atlauncher/AtlFilterModel.h index 2aef81fb..bd72ad91 100644 --- a/application/pages/modplatform/atlauncher/AtlFilterModel.h +++ b/application/pages/modplatform/atlauncher/AtlFilterModel.h @@ -18,6 +18,7 @@ public: QString translateCurrentSorting(); void setSorting(Sorting sorting); Sorting getCurrentSorting(); + void setSearchTerm(QString term); protected: bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; @@ -26,6 +27,7 @@ protected: private: QMap sortings; Sorting currentSorting; + QString searchTerm; }; diff --git a/application/pages/modplatform/atlauncher/AtlPage.cpp b/application/pages/modplatform/atlauncher/AtlPage.cpp index 4518248d..f90d734c 100644 --- a/application/pages/modplatform/atlauncher/AtlPage.cpp +++ b/application/pages/modplatform/atlauncher/AtlPage.cpp @@ -25,6 +25,8 @@ AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget *parent) } ui->sortByBox->setCurrentText(filterModel->translateCurrentSorting()); + connect(ui->searchEdit, &QLineEdit::textChanged, this, &AtlPage::triggerSearch); + connect(ui->resetButton, &QPushButton::clicked, this, &AtlPage::resetSearch); 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); @@ -59,6 +61,16 @@ void AtlPage::suggestCurrent() }); } +void AtlPage::triggerSearch() +{ + filterModel->setSearchTerm(ui->searchEdit->text()); +} + +void AtlPage::resetSearch() +{ + ui->searchEdit->setText(""); +} + void AtlPage::onSortingSelectionChanged(QString data) { auto toSet = filterModel->getAvailableSortings().value(data); diff --git a/application/pages/modplatform/atlauncher/AtlPage.h b/application/pages/modplatform/atlauncher/AtlPage.h index fceb0abf..368de666 100644 --- a/application/pages/modplatform/atlauncher/AtlPage.h +++ b/application/pages/modplatform/atlauncher/AtlPage.h @@ -62,6 +62,9 @@ private: void suggestCurrent(); private slots: + void triggerSearch(); + void resetSearch(); + void onSortingSelectionChanged(QString data); void onSelectionChanged(QModelIndex first, QModelIndex second); diff --git a/application/pages/modplatform/atlauncher/AtlPage.ui b/application/pages/modplatform/atlauncher/AtlPage.ui index dd17283c..1a5a450d 100644 --- a/application/pages/modplatform/atlauncher/AtlPage.ui +++ b/application/pages/modplatform/atlauncher/AtlPage.ui @@ -11,30 +11,11 @@ - - - - true - - - - 96 - 48 - - - - - - - - - + + - - - @@ -45,12 +26,50 @@ + + + + + + + + + + + + Reset + + + + + + + + + + 96 + 48 + + + + + + + + true + + + true + + + - packView + searchEdit + resetButton versionSelectionBox From 1edcd9b86e1174ca236e24002650940c36124a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Thu, 11 Feb 2021 02:22:43 +0100 Subject: [PATCH 15/15] NOISSUE implement deleting skins --- api/logic/CMakeLists.txt | 8 ++-- api/logic/minecraft/services/SkinDelete.cpp | 42 +++++++++++++++++++ api/logic/minecraft/services/SkinDelete.h | 30 +++++++++++++ .../minecraft/{ => services}/SkinUpload.cpp | 0 .../minecraft/{ => services}/SkinUpload.h | 0 application/dialogs/SkinUploadDialog.cpp | 2 +- application/pages/global/AccountListPage.cpp | 24 +++++++++++ application/pages/global/AccountListPage.h | 21 +++------- application/pages/global/AccountListPage.ui | 10 +++++ 9 files changed, 118 insertions(+), 19 deletions(-) create mode 100644 api/logic/minecraft/services/SkinDelete.cpp create mode 100644 api/logic/minecraft/services/SkinDelete.h rename api/logic/minecraft/{ => services}/SkinUpload.cpp (100%) rename api/logic/minecraft/{ => services}/SkinUpload.h (100%) diff --git a/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt index 3d385b1c..84438a6b 100644 --- a/api/logic/CMakeLists.txt +++ b/api/logic/CMakeLists.txt @@ -303,9 +303,11 @@ set(MINECRAFT_SOURCES minecraft/AssetsUtils.h minecraft/AssetsUtils.cpp - # Skin upload utilities - minecraft/SkinUpload.cpp - minecraft/SkinUpload.h + # Minecraft services + minecraft/services/SkinUpload.cpp + minecraft/services/SkinUpload.h + minecraft/services/SkinDelete.cpp + minecraft/services/SkinDelete.h mojang/PackageManifest.h mojang/PackageManifest.cpp diff --git a/api/logic/minecraft/services/SkinDelete.cpp b/api/logic/minecraft/services/SkinDelete.cpp new file mode 100644 index 00000000..34977257 --- /dev/null +++ b/api/logic/minecraft/services/SkinDelete.cpp @@ -0,0 +1,42 @@ +#include "SkinDelete.h" +#include +#include +#include + +SkinDelete::SkinDelete(QObject *parent, AuthSessionPtr session) + : Task(parent), m_session(session) +{ +} + +void SkinDelete::executeTask() +{ + QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active")); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_session->access_token).toLocal8Bit()); + QNetworkReply *rep = ENV.qnam().deleteResource(request); + m_reply = std::shared_ptr(rep); + + setStatus(tr("Deleting skin")); + connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + +void SkinDelete::downloadError(QNetworkReply::NetworkError error) +{ + // error happened during download. + qCritical() << "Network error: " << error; + emitFailed(m_reply->errorString()); +} + +void SkinDelete::downloadFinished() +{ + // if the download failed + if (m_reply->error() != QNetworkReply::NetworkError::NoError) + { + emitFailed(QString("Network error: %1").arg(m_reply->errorString())); + m_reply.reset(); + return; + } + emitSucceeded(); +} + diff --git a/api/logic/minecraft/services/SkinDelete.h b/api/logic/minecraft/services/SkinDelete.h new file mode 100644 index 00000000..705ce8ef --- /dev/null +++ b/api/logic/minecraft/services/SkinDelete.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include +#include +#include "tasks/Task.h" +#include "multimc_logic_export.h" + +typedef std::shared_ptr SkinDeletePtr; + +class MULTIMC_LOGIC_EXPORT SkinDelete : public Task +{ + Q_OBJECT +public: + SkinDelete(QObject *parent, AuthSessionPtr session); + virtual ~SkinDelete() = default; + +private: + AuthSessionPtr m_session; + std::shared_ptr m_reply; + +protected: + virtual void executeTask(); + +public slots: + void downloadError(QNetworkReply::NetworkError); + void downloadFinished(); +}; + diff --git a/api/logic/minecraft/SkinUpload.cpp b/api/logic/minecraft/services/SkinUpload.cpp similarity index 100% rename from api/logic/minecraft/SkinUpload.cpp rename to api/logic/minecraft/services/SkinUpload.cpp diff --git a/api/logic/minecraft/SkinUpload.h b/api/logic/minecraft/services/SkinUpload.h similarity index 100% rename from api/logic/minecraft/SkinUpload.h rename to api/logic/minecraft/services/SkinUpload.h diff --git a/application/dialogs/SkinUploadDialog.cpp b/application/dialogs/SkinUploadDialog.cpp index 7d2ff829..56133529 100644 --- a/application/dialogs/SkinUploadDialog.cpp +++ b/application/dialogs/SkinUploadDialog.cpp @@ -1,7 +1,7 @@ #include #include #include -#include +#include #include "SkinUploadDialog.h" #include "ui_SkinUploadDialog.h" #include "ProgressDialog.h" diff --git a/application/pages/global/AccountListPage.cpp b/application/pages/global/AccountListPage.cpp index 5a508df4..ff3736ed 100644 --- a/application/pages/global/AccountListPage.cpp +++ b/application/pages/global/AccountListPage.cpp @@ -30,6 +30,7 @@ #include "dialogs/SkinUploadDialog.h" #include "tasks/Task.h" #include "minecraft/auth/YggdrasilTask.h" +#include "minecraft/services/SkinDelete.h" #include "MultiMC.h" @@ -142,6 +143,7 @@ void AccountListPage::updateButtonStates() ui->actionRemove->setEnabled(selection.size() > 0); ui->actionSetDefault->setEnabled(selection.size() > 0); ui->actionUploadSkin->setEnabled(selection.size() > 0); + ui->actionDeleteSkin->setEnabled(selection.size() > 0); if(m_accounts->activeAccount().get() == nullptr) { ui->actionNoDefault->setEnabled(false); @@ -191,3 +193,25 @@ void AccountListPage::on_actionUploadSkin_triggered() dialog.exec(); } } + +void AccountListPage::on_actionDeleteSkin_triggered() +{ + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() <= 0) + return; + + QModelIndex selected = selection.first(); + AuthSessionPtr session = std::make_shared(); + MojangAccountPtr account = selected.data(MojangAccountList::PointerRole).value(); + auto login = account->login(session); + ProgressDialog prog(this); + if (prog.execWithTask((Task*)login.get()) != QDialog::Accepted) { + CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to login!"), QMessageBox::Warning)->exec(); + return; + } + auto deleteSkinTask = std::make_shared(this, session); + if (prog.execWithTask((Task*)deleteSkinTask.get()) != QDialog::Accepted) { + CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to delete current skin!"), QMessageBox::Warning)->exec(); + return; + } +} diff --git a/application/pages/global/AccountListPage.h b/application/pages/global/AccountListPage.h index 364eab3d..fba1833f 100644 --- a/application/pages/global/AccountListPage.h +++ b/application/pages/global/AccountListPage.h @@ -59,35 +59,26 @@ public: return "Getting-Started#adding-an-account"; } -private: - void changeEvent(QEvent * event) override; - QMenu * createPopupMenu() override; - -public -slots: +public slots: void on_actionAdd_triggered(); - void on_actionRemove_triggered(); - void on_actionSetDefault_triggered(); - void on_actionNoDefault_triggered(); - void on_actionUploadSkin_triggered(); + void on_actionDeleteSkin_triggered(); void listChanged(); //! Updates the states of the dialog's buttons. void updateButtonStates(); -protected: - std::shared_ptr m_accounts; - -protected -slots: +protected slots: void ShowContextMenu(const QPoint &pos); void addAccount(const QString& errMsg=""); private: + void changeEvent(QEvent * event) override; + QMenu * createPopupMenu() override; + std::shared_ptr m_accounts; Ui::AccountListPage *ui; }; diff --git a/application/pages/global/AccountListPage.ui b/application/pages/global/AccountListPage.ui index ba07445e..71647db3 100644 --- a/application/pages/global/AccountListPage.ui +++ b/application/pages/global/AccountListPage.ui @@ -40,7 +40,9 @@ + + @@ -70,6 +72,14 @@ Upload Skin + + + Delete Skin + + + Delete the currently active skin and go back to the default one + +