diff --git a/BUILD.md b/BUILD.md index ad8f5000..85a8348a 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))!** diff --git a/README.md b/README.md index 895beefd..86e818f2 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,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/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt index be4318a8..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 @@ -486,6 +488,15 @@ set(TECHNIC_SOURCES modplatform/technic/TechnicPackProcessor.cpp ) +set(ATLAUNCHER_SOURCES + modplatform/atlauncher/ATLPackIndex.cpp + modplatform/atlauncher/ATLPackIndex.h + modplatform/atlauncher/ATLPackInstallTask.cpp + modplatform/atlauncher/ATLPackInstallTask.h + modplatform/atlauncher/ATLPackManifest.cpp + modplatform/atlauncher/ATLPackManifest.h +) + add_unit_test(Index SOURCES meta/Index_test.cpp LIBS MultiMC_logic @@ -518,6 +529,7 @@ set(LOGIC_SOURCES ${FLAME_SOURCES} ${MODPACKSCH_SOURCES} ${TECHNIC_SOURCES} + ${ATLAUNCHER_SOURCES} ) add_library(MultiMC_logic SHARED ${LOGIC_SOURCES}) 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/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 2d18b2b8..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; @@ -244,12 +255,58 @@ QStringList MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QSt } // ours -QStringList MMCZip::extractDir(QString fileCompressed, QString dir) +bool MMCZip::extractRelFile(QuaZip *zip, const QString &file, const QString &target) +{ + return JlCompress::extractFile(zip, file, target); +} + +// ours +nonstd::optional MMCZip::extractDir(QString fileCompressed, QString dir) { QuaZip zip(fileCompressed); if (!zip.open(QuaZip::mdUnzip)) { - return {}; + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if(fileInfo.size() == 22) { + return QStringList(); + } + qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; + return nonstd::nullopt; } return MMCZip::extractSubDir(&zip, "", dir); } + +// ours +nonstd::optional MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir) +{ + QuaZip zip(fileCompressed); + if (!zip.open(QuaZip::mdUnzip)) + { + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if(fileInfo.size() == 22) { + return QStringList(); + } + qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; + return nonstd::nullopt; + } + return MMCZip::extractSubDir(&zip, subdir, dir); +} + +// ours +bool MMCZip::extractFile(QString fileCompressed, QString file, QString target) +{ + QuaZip zip(fileCompressed); + if (!zip.open(QuaZip::mdUnzip)) + { + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if(fileInfo.size() == 22) { + return true; + } + qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError(); + return false; + } + return MMCZip::extractRelFile(&zip, file, target); +} diff --git a/api/logic/MMCZip.h b/api/logic/MMCZip.h index fca7dde0..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,9 @@ 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); /** * Extract a whole archive. @@ -66,5 +69,26 @@ namespace MMCZip * \param dir The directory to extract to, the current directory if left empty. * \return The list of the full paths of the files extracted, empty on failure. */ - QStringList MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString dir); + nonstd::optional MULTIMC_LOGIC_EXPORT extractDir(QString fileCompressed, QString dir); + + /** + * Extract a subdirectory from an archive + * + * \param fileCompressed The name of the archive. + * \param subdir The directory within the archive to extract + * \param dir The directory to extract to, the current directory if left empty. + * \return The list of the full paths of the files extracted, empty on failure. + */ + nonstd::optional 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/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/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 72% rename from api/logic/minecraft/SkinUpload.cpp rename to api/logic/minecraft/services/SkinUpload.cpp index 83bdf592..4e5a1698 100644 --- a/api/logic/minecraft/SkinUpload.cpp +++ b/api/logic/minecraft/services/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")); 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/api/logic/modplatform/atlauncher/ATLPackIndex.cpp b/api/logic/modplatform/atlauncher/ATLPackIndex.cpp new file mode 100644 index 00000000..35f50b18 --- /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, 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/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..25c6d58d --- /dev/null +++ b/api/logic/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -0,0 +1,682 @@ +#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() +{ + qDebug() << "PackInstallTask::executeTask: " << QThread::currentThreadId(); + auto *netJob = new NetJob("ATLauncher::VersionFetch"); + auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json") + .arg(m_pack).arg(m_version_name); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + jobPtr = netJob; + jobPtr->start(); + + QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); + QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed); +} + +void PackInstallTask::onDownloadSucceeded() +{ + qDebug() << "PackInstallTask::onDownloadSucceeded: " << QThread::currentThreadId(); + jobPtr.reset(); + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + auto obj = doc.object(); + + ATLauncher::PackVersion version; + try + { + ATLauncher::loadVersion(version, obj); + } + catch (const JSONValidationError &e) + { + emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); + return; + } + m_version = version; + + auto vlist = ENV.metadataIndex()->get("net.minecraft"); + if(!vlist) + { + emitFailed(tr("Failed to get local metadata index for ") + "net.minecraft"); + return; + } + + auto ver = vlist->getVersion(m_version.minecraft); + if (!ver) { + emitFailed(tr("Failed to get local metadata index for ") + "net.minecraft" + " " + m_version.minecraft); + return; + } + ver->load(Net::Mode::Online); + minecraftVersion = ver; + + if(m_version.noConfigs) { + downloadMods(); + } + else { + installConfigs(); + } +} + +void PackInstallTask::onDownloadFailed(QString reason) +{ + qDebug() << "PackInstallTask::onDownloadFailed: " << QThread::currentThreadId(); + jobPtr.reset(); + emitFailed(reason); +} + +QString PackInstallTask::getDirForModType(ModType type, QString raw) +{ + switch (type) { + // Mod types that can either be ignored at this stage, or ignored + // completely. + case ModType::Root: + case ModType::Extract: + case ModType::Decomp: + case ModType::TexturePackExtract: + case ModType::ResourcePackExtract: + case ModType::MCPC: + return Q_NULLPTR; + case ModType::Forge: + // Forge detection happens later on, if it cannot be detected it will + // install a jarmod component. + case ModType::Jar: + return "jarmods"; + case ModType::Mods: + return "mods"; + case ModType::Flan: + return "Flan"; + case ModType::Dependency: + return FS::PathCombine("mods", m_version.minecraft); + case ModType::Ic2Lib: + return FS::PathCombine("mods", "ic2"); + case ModType::DenLib: + return FS::PathCombine("mods", "denlib"); + case ModType::Coremods: + return "coremods"; + case ModType::Plugins: + return "plugins"; + case ModType::TexturePack: + return "texturepacks"; + case ModType::ResourcePack: + return "resourcepacks"; + case ModType::ShaderPack: + return "shaderpacks"; + case ModType::Millenaire: + qWarning() << "Unsupported mod type: " + raw; + return Q_NULLPTR; + case ModType::Unknown: + emitFailed(tr("Unknown mod type: ") + raw); + return Q_NULLPTR; + } + + return Q_NULLPTR; +} + +QString PackInstallTask::getVersionForLoader(QString uid) +{ + if(m_version.loader.recommended || m_version.loader.latest || m_version.loader.choose) { + auto vlist = ENV.metadataIndex()->get(uid); + if(!vlist) + { + emitFailed(tr("Failed to get local metadata index for ") + uid); + return Q_NULLPTR; + } + + // todo: filter by Minecraft version + + if(m_version.loader.recommended) { + return vlist.get()->getRecommended().get()->descriptor(); + } + else if(m_version.loader.latest) { + return vlist.get()->at(0)->descriptor(); + } + else if(m_version.loader.choose) { + // todo: implement + } + } + + return m_version.loader.version; +} + +QString PackInstallTask::detectLibrary(VersionLibrary library) +{ + // Try to detect what the library is + if (!library.server.isEmpty() && library.server.split("/").length() >= 3) { + auto lastSlash = library.server.lastIndexOf("/"); + auto locationAndVersion = library.server.mid(0, lastSlash); + auto fileName = library.server.mid(lastSlash + 1); + + lastSlash = locationAndVersion.lastIndexOf("/"); + auto location = locationAndVersion.mid(0, lastSlash); + auto version = locationAndVersion.mid(lastSlash + 1); + + lastSlash = location.lastIndexOf("/"); + auto group = location.mid(0, lastSlash).replace("/", "."); + auto artefact = location.mid(lastSlash + 1); + + return group + ":" + artefact + ":" + version; + } + + if(library.file.contains("-")) { + auto lastSlash = library.file.lastIndexOf("-"); + auto name = library.file.mid(0, lastSlash); + auto version = library.file.mid(lastSlash + 1).remove(".jar"); + + if(name == QString("guava")) { + return "com.google.guava:guava:" + version; + } + else if(name == QString("commons-lang3")) { + return "org.apache.commons:commons-lang3:" + version; + } + } + + return "org.multimc.atlauncher:" + library.md5 + ":1"; +} + +bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared_ptr 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() +{ + qDebug() << "PackInstallTask::installConfigs: " << QThread::currentThreadId(); + setStatus(tr("Downloading configs...")); + jobPtr.reset(new NetJob(tr("Config download"))); + + auto path = QString("Configs/%1/%2.zip").arg(m_pack).arg(m_version_name); + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.zip") + .arg(m_pack).arg(m_version_name); + auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", path); + entry->setStale(true); + + jobPtr->addNetAction(Net::Download::makeCached(url, entry)); + archivePath = entry->getFullPath(); + + connect(jobPtr.get(), &NetJob::succeeded, this, [&]() + { + jobPtr.reset(); + extractConfigs(); + }); + connect(jobPtr.get(), &NetJob::failed, [&](QString reason) + { + jobPtr.reset(); + emitFailed(reason); + }); + connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) + { + setProgress(current, total); + }); + + jobPtr->start(); +} + +void PackInstallTask::extractConfigs() +{ + qDebug() << "PackInstallTask::extractConfigs: " << QThread::currentThreadId(); + setStatus(tr("Extracting configs...")); + + QDir extractDir(m_stagingPath); + + QuaZip packZip(archivePath); + if(!packZip.open(QuaZip::mdUnzip)) + { + emitFailed(tr("Failed to open pack configs %1!").arg(archivePath)); + return; + } + + m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/minecraft"); + connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, [&]() + { + downloadMods(); + }); + connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, [&]() + { + emitAborted(); + }); + m_extractFutureWatcher.setFuture(m_extractFuture); +} + +void PackInstallTask::downloadMods() +{ + qDebug() << "PackInstallTask::installMods: " << QThread::currentThreadId(); + setStatus(tr("Downloading mods...")); + + jarmods.clear(); + jobPtr.reset(new NetJob(tr("Mod download"))); + for(const auto& mod : m_version.mods) { + // skip optional mods for now + if(mod.optional) continue; + + QString url; + switch(mod.download) { + case DownloadType::Server: + url = BuildConfig.ATL_DOWNLOAD_SERVER_URL + mod.url; + break; + case DownloadType::Browser: + emitFailed(tr("Unsupported download type: ") + mod.download_raw); + return; + case DownloadType::Direct: + url = mod.url; + break; + case DownloadType::Unknown: + emitFailed(tr("Unknown download type: ") + mod.download_raw); + return; + } + + QFileInfo fileName(mod.file); + auto cacheName = fileName.completeBaseName() + "-" + mod.md5 + "." + fileName.suffix(); + + if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract || mod.type == ModType::ResourcePackExtract) { + + auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", cacheName); + entry->setStale(true); + modsToExtract.insert(entry->getFullPath(), mod); + + auto dl = Net::Download::makeCached(url, entry); + jobPtr->addNetAction(dl); + } + else if(mod.type == ModType::Decomp) { + auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", cacheName); + entry->setStale(true); + modsToDecomp.insert(entry->getFullPath(), mod); + + auto dl = Net::Download::makeCached(url, entry); + jobPtr->addNetAction(dl); + } + else { + auto relpath = getDirForModType(mod.type, mod.type_raw); + if(relpath == Q_NULLPTR) continue; + + auto entry = ENV.metacache()->resolveEntry("ATLauncherPacks", cacheName); + entry->setStale(true); + + auto dl = Net::Download::makeCached(url, entry); + jobPtr->addNetAction(dl); + + auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); + qDebug() << "Will download" << url << "to" << path; + modsToCopy[entry->getFullPath()] = path; + + if(mod.type == ModType::Forge) { + auto vlist = ENV.metadataIndex()->get("net.minecraftforge"); + if(vlist) + { + auto ver = vlist->getVersion(mod.version); + if(ver) { + ver->load(Net::Mode::Online); + componentsToInstall.insert("net.minecraftforge", ver); + continue; + } + } + + qDebug() << "Jarmod: " + path; + jarmods.push_back(path); + } + + if(mod.type == ModType::Jar) { + qDebug() << "Jarmod: " + path; + jarmods.push_back(path); + } + } + } + + connect(jobPtr.get(), &NetJob::succeeded, this, &PackInstallTask::onModsDownloaded); + connect(jobPtr.get(), &NetJob::failed, [&](QString reason) + { + jobPtr.reset(); + emitFailed(reason); + }); + connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) + { + setProgress(current, total); + }); + + jobPtr->start(); +} + +void PackInstallTask::onModsDownloaded() { + qDebug() << "PackInstallTask::onModsDownloaded: " << QThread::currentThreadId(); + jobPtr.reset(); + + if(modsToExtract.size() || modsToDecomp.size() || modsToCopy.size()) { + m_modExtractFuture = QtConcurrent::run(QThreadPool::globalInstance(), this, &PackInstallTask::extractMods, modsToExtract, modsToDecomp, modsToCopy); + connect(&m_modExtractFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::onModsExtracted); + connect(&m_modExtractFutureWatcher, &QFutureWatcher::canceled, this, [&]() + { + emitAborted(); + }); + m_modExtractFutureWatcher.setFuture(m_modExtractFuture); + } + else { + install(); + } +} + +void PackInstallTask::onModsExtracted() { + qDebug() << "PackInstallTask::onModsExtracted: " << QThread::currentThreadId(); + if(m_modExtractFuture.result()) { + install(); + } + else { + emitFailed(tr("Failed to extract mods...")); + } +} + +bool PackInstallTask::extractMods( + const QMap &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; + } + } + + for (auto iter = toDecomp.begin(); iter != toDecomp.end(); iter++) { + auto &modPath = iter.key(); + auto &mod = iter.value(); + auto extractToDir = getDirForModType(mod.decompType, mod.decompType_raw); + + QDir extractDir(m_stagingPath); + auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir, mod.decompFile); + + qDebug() << "Extracting " + mod.decompFile + " to " + extractToDir; + if(!MMCZip::extractFile(modPath, mod.decompFile, extractToPath)) { + qWarning() << "Failed to extract" << mod.decompFile; + return false; + } + } + + for (auto iter = toCopy.begin(); iter != toCopy.end(); iter++) { + auto &from = iter.key(); + auto &to = iter.value(); + FS::copy fileCopyOperation(from, to); + if(!fileCopyOperation()) { + qWarning() << "Failed to copy" << from << "to" << to; + return false; + } + } + return true; +} + +void PackInstallTask::install() +{ + qDebug() << "PackInstallTask::install: " << QThread::currentThreadId(); + setStatus(tr("Installing modpack")); + + auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared(instanceConfigPath); + instanceSettings->suspendSave(); + instanceSettings->registerSetting("InstanceType", "Legacy"); + instanceSettings->set("InstanceType", "OneSix"); + + MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + + // Use a component to add libraries BEFORE Minecraft + if(!createLibrariesComponent(instance.instanceRoot(), components)) { + return; + } + + // Minecraft + components->setComponentVersion("net.minecraft", m_version.minecraft, true); + + // Loader + if(m_version.loader.type == QString("forge")) + { + auto version = getVersionForLoader("net.minecraftforge"); + if(version == Q_NULLPTR) return; + + components->setComponentVersion("net.minecraftforge", version, true); + } + else if(m_version.loader.type == QString("fabric")) + { + auto version = getVersionForLoader("net.fabricmc.fabric-loader"); + if(version == Q_NULLPTR) return; + + components->setComponentVersion("net.fabricmc.fabric-loader", version, true); + } + else if(m_version.loader.type != QString()) + { + emitFailed(tr("Unknown loader type: ") + m_version.loader.type); + return; + } + + for(const auto & componentUid : componentsToInstall.keys()) { + auto version = componentsToInstall.value(componentUid); + components->setComponentVersion(componentUid, version->version()); + } + + components->installJarMods(jarmods); + + // Use a component to fill in the rest of the data + // todo: use more detection + if(!createPackComponent(instance.instanceRoot(), components)) { + return; + } + + components->saveNow(); + + instance.setName(m_instName); + instance.setIconKey(m_instIcon); + instanceSettings->resumeSave(); + + jarmods.clear(); + emitSucceeded(); +} + +} diff --git a/api/logic/modplatform/atlauncher/ATLPackInstallTask.h b/api/logic/modplatform/atlauncher/ATLPackInstallTask.h new file mode 100644 index 00000000..78544bab --- /dev/null +++ b/api/logic/modplatform/atlauncher/ATLPackInstallTask.h @@ -0,0 +1,84 @@ +#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" + +#include + +namespace ATLauncher { + +class MULTIMC_LOGIC_EXPORT PackInstallTask : public InstanceTask +{ +Q_OBJECT + +public: + explicit PackInstallTask(QString pack, QString version); + virtual ~PackInstallTask(){} + + bool abort() override; + +protected: + virtual void executeTask() override; + +private slots: + void onDownloadSucceeded(); + void onDownloadFailed(QString reason); + + void onModsDownloaded(); + void onModsExtracted(); + +private: + QString getDirForModType(ModType type, QString raw); + QString getVersionForLoader(QString uid); + QString detectLibrary(VersionLibrary library); + + bool createLibrariesComponent(QString instanceRoot, std::shared_ptr profile); + bool createPackComponent(QString instanceRoot, std::shared_ptr profile); + + void installConfigs(); + void extractConfigs(); + void downloadMods(); + bool extractMods( + const QMap &toExtract, + const QMap &toDecomp, + const QMap &toCopy + ); + void install(); + +private: + NetJobPtr jobPtr; + QByteArray response; + + QString m_pack; + QString m_version_name; + PackVersion m_version; + + QMap modsToExtract; + QMap modsToDecomp; + QMap modsToCopy; + + QString archivePath; + QStringList jarmods; + Meta::VersionPtr minecraftVersion; + QMap componentsToInstall; + + QFuture> m_extractFuture; + QFutureWatcher> m_extractFutureWatcher; + + QFuture m_modExtractFuture; + QFutureWatcher m_modExtractFutureWatcher; + + 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..84389330 --- /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, QString("latest"), false); + p.choose = Json::ensureBoolean(obj, QString("choose"), false); + p.recommended = Json::ensureBoolean(obj, QString("recommended"), false); + + auto metadata = Json::requireObject(obj, "metadata"); + p.version = Json::requireString(metadata, "version"); +} + +static void loadVersionLibrary(ATLauncher::VersionLibrary & p, QJsonObject & obj) { + p.url = Json::requireString(obj, "url"); + p.file = Json::requireString(obj, "file"); + p.md5 = Json::requireString(obj, "md5"); + + p.download_raw = Json::requireString(obj, "download"); + p.download = parseDownloadType(p.download_raw); + + p.server = Json::ensureString(obj, "server", ""); +} + +static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) { + p.name = Json::requireString(obj, "name"); + p.version = Json::requireString(obj, "version"); + p.url = Json::requireString(obj, "url"); + p.file = Json::requireString(obj, "file"); + p.md5 = Json::ensureString(obj, "md5", ""); + + p.download_raw = Json::requireString(obj, "download"); + p.download = parseDownloadType(p.download_raw); + + p.type_raw = Json::requireString(obj, "type"); + p.type = parseModType(p.type_raw); + + // This contributes to the Minecraft Forge detection, where we rely on mod.type being "Forge" + // when the mod represents Forge. As there is little difference between "Jar" and "Forge, some + // packs regretfully use "Jar". This will correct the type to "Forge" in these cases (as best + // it can). + if(p.name == QString("Minecraft Forge") && p.type == ATLauncher::ModType::Jar) { + p.type_raw = "forge"; + p.type = ATLauncher::ModType::Forge; + } + + if(obj.contains("extractTo")) { + p.extractTo_raw = Json::requireString(obj, "extractTo"); + p.extractTo = parseModType(p.extractTo_raw); + p.extractFolder = Json::ensureString(obj, "extractFolder", "").replace("%s%", "/"); + } + + if(obj.contains("decompType")) { + p.decompType_raw = Json::requireString(obj, "decompType"); + p.decompType = parseModType(p.decompType_raw); + p.decompFile = Json::requireString(obj, "decompFile"); + } + + p.optional = Json::ensureBoolean(obj, QString("optional"), false); +} + +void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) +{ + v.version = Json::requireString(obj, "version"); + v.minecraft = Json::requireString(obj, "minecraft"); + v.noConfigs = Json::ensureBoolean(obj, QString("noConfigs"), false); + + if(obj.contains("mainClass")) { + auto main = Json::requireObject(obj, "mainClass"); + v.mainClass = Json::ensureString(main, "mainClass", ""); + } + + if(obj.contains("extraArguments")) { + auto arguments = Json::requireObject(obj, "extraArguments"); + v.extraArguments = Json::ensureString(arguments, "arguments", ""); + } + + if(obj.contains("loader")) { + auto loader = Json::requireObject(obj, "loader"); + loadVersionLoader(v.loader, loader); + } + + if(obj.contains("libraries")) { + auto libraries = Json::requireArray(obj, "libraries"); + for (const auto libraryRaw : libraries) + { + auto libraryObj = Json::requireObject(libraryRaw); + ATLauncher::VersionLibrary target; + loadVersionLibrary(target, libraryObj); + v.libraries.append(target); + } + } + + auto mods = Json::requireArray(obj, "mods"); + for (const auto modRaw : mods) + { + auto modObj = Json::requireObject(modRaw); + ATLauncher::VersionMod mod; + loadVersionMod(mod, modObj); + v.mods.append(mod); + } +} 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/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/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/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"); 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; } 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/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() 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/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 + + diff --git a/application/pages/modplatform/atlauncher/AtlFilterModel.cpp b/application/pages/modplatform/atlauncher/AtlFilterModel.cpp new file mode 100644 index 00000000..b5d8f22b --- /dev/null +++ b/application/pages/modplatform/atlauncher/AtlFilterModel.cpp @@ -0,0 +1,81 @@ +#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); + + searchTerm = ""; +} + +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; +} + +void FilterModel::setSearchTerm(const QString term) +{ + searchTerm = term.trimmed(); + invalidate(); +} + +bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + 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 +{ + 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..bd72ad91 --- /dev/null +++ b/application/pages/modplatform/atlauncher/AtlFilterModel.h @@ -0,0 +1,34 @@ +#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(); + void setSearchTerm(QString term); + +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; + QString searchTerm; + +}; + +} diff --git a/application/pages/modplatform/atlauncher/AtlModel.cpp b/application/pages/modplatform/atlauncher/AtlModel.cpp new file mode 100644 index 00000000..4b1b1c8e --- /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.name; + } + 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..f90d734c --- /dev/null +++ b/application/pages/modplatform/atlauncher/AtlPage.cpp @@ -0,0 +1,114 @@ +#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->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); +} + +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::triggerSearch() +{ + filterModel->setSearchTerm(ui->searchEdit->text()); +} + +void AtlPage::resetSearch() +{ + ui->searchEdit->setText(""); +} + +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(); + + ui->packDescription->setHtml(selected.description.replace("\n", "
")); + + 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..368de666 --- /dev/null +++ b/application/pages/modplatform/atlauncher/AtlPage.h @@ -0,0 +1,81 @@ +/* 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 triggerSearch(); + void resetSearch(); + + 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..1a5a450d --- /dev/null +++ b/application/pages/modplatform/atlauncher/AtlPage.ui @@ -0,0 +1,77 @@ + + + AtlPage + + + + 0 + 0 + 875 + 745 + + + + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + Reset + + + + + + + + + + 96 + 48 + + + + + + + + true + + + true + + + + + + + + + searchEdit + resetButton + versionSelectionBox + + + + 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 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 00000000..f4314c43 Binary files /dev/null and b/application/resources/multimc/scalable/atlauncher-placeholder.png differ 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). diff --git a/libraries/classparser/src/javaendian.h b/libraries/classparser/src/javaendian.h index 076bff5e..5a6e107b 100644 --- a/libraries/classparser/src/javaendian.h +++ b/libraries/classparser/src/javaendian.h @@ -11,32 +11,17 @@ 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) { @@ -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 }