From b0f5f4cb13d7d0f983b7813377405298de718b2e Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sun, 7 Jun 2020 16:14:47 +0100 Subject: [PATCH 1/6] GH-3095 New FTB platform support Models are based on the models from my go-modpacksch library. License: ======== The MIT License (MIT) Copyright (c) Jamie Mansfield Copyright (c) contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --- api/logic/CMakeLists.txt | 8 + api/logic/Env.cpp | 1 + .../modpacksch/PackInstallTask.cpp | 150 +++++++++ .../modplatform/modpacksch/PackInstallTask.h | 41 +++ .../modplatform/modpacksch/PackManifest.cpp | 156 +++++++++ .../modplatform/modpacksch/PackManifest.h | 127 ++++++++ application/CMakeLists.txt | 5 + application/dialogs/NewInstanceDialog.cpp | 2 + .../pages/modplatform/ftb/FtbModel.cpp | 302 ++++++++++++++++++ application/pages/modplatform/ftb/FtbModel.h | 68 ++++ application/pages/modplatform/ftb/FtbPage.cpp | 109 +++++++ application/pages/modplatform/ftb/FtbPage.h | 77 +++++ application/pages/modplatform/ftb/FtbPage.ui | 64 ++++ buildconfig/BuildConfig.h | 2 + 14 files changed, 1112 insertions(+) create mode 100644 api/logic/modplatform/modpacksch/PackInstallTask.cpp create mode 100644 api/logic/modplatform/modpacksch/PackInstallTask.h create mode 100644 api/logic/modplatform/modpacksch/PackManifest.cpp create mode 100644 api/logic/modplatform/modpacksch/PackManifest.h create mode 100644 application/pages/modplatform/ftb/FtbModel.cpp create mode 100644 application/pages/modplatform/ftb/FtbModel.h create mode 100644 application/pages/modplatform/ftb/FtbPage.cpp create mode 100644 application/pages/modplatform/ftb/FtbPage.h create mode 100644 application/pages/modplatform/ftb/FtbPage.ui diff --git a/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt index 50eff4ba..2a1e51fc 100644 --- a/api/logic/CMakeLists.txt +++ b/api/logic/CMakeLists.txt @@ -451,6 +451,13 @@ set(FLAME_SOURCES modplatform/flame/FileResolvingTask.cpp ) +set(MODPACKSCH_SOURCES + modplatform/modpacksch/PackInstallTask.h + modplatform/modpacksch/PackInstallTask.cpp + modplatform/modpacksch/PackManifest.h + modplatform/modpacksch/PackManifest.cpp +) + add_unit_test(Index SOURCES meta/Index_test.cpp LIBS MultiMC_logic @@ -481,6 +488,7 @@ set(LOGIC_SOURCES ${ICONS_SOURCES} ${FTB_SOURCES} ${FLAME_SOURCES} + ${MODPACKSCH_SOURCES} ) add_library(MultiMC_logic SHARED ${LOGIC_SOURCES}) diff --git a/api/logic/Env.cpp b/api/logic/Env.cpp index 0d496d4e..2043f982 100644 --- a/api/logic/Env.cpp +++ b/api/logic/Env.cpp @@ -97,6 +97,7 @@ void Env::initHttpMetaCache() m_metacache->addBase("liteloader", QDir("mods/liteloader").absolutePath()); m_metacache->addBase("general", QDir("cache").absolutePath()); m_metacache->addBase("FTBPacks", QDir("cache/FTBPacks").absolutePath()); + m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath()); m_metacache->addBase("TwitchPacks", QDir("cache/TwitchPacks").absolutePath()); m_metacache->addBase("skins", QDir("accounts/skins").absolutePath()); m_metacache->addBase("root", QDir::currentPath()); diff --git a/api/logic/modplatform/modpacksch/PackInstallTask.cpp b/api/logic/modplatform/modpacksch/PackInstallTask.cpp new file mode 100644 index 00000000..f8572a4d --- /dev/null +++ b/api/logic/modplatform/modpacksch/PackInstallTask.cpp @@ -0,0 +1,150 @@ +#include "PackInstallTask.h" + +#include "BuildConfig.h" +#include "FileSystem.h" +#include "Json.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "settings/INISettingsObject.h" + +namespace ModpacksCH { + +PackInstallTask::PackInstallTask(Modpack pack, QString version) +{ + m_pack = pack; + m_version_name = version; +} + +bool PackInstallTask::abort() +{ + return true; +} + +void PackInstallTask::executeTask() +{ + // Find pack version + bool found = false; + VersionInfo version; + + for(auto vInfo : m_pack.versions) { + if (vInfo.name == m_version_name) { + found = true; + version = vInfo; + continue; + } + } + + if(!found) { + emitFailed("failed to find pack version " + m_version_name); + return; + } + + auto *netJob = new NetJob("ModpacksCH::VersionFetch"); + auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1/%2") + .arg(m_pack.id).arg(version.id); + 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(); + + ModpacksCH::Version version; + try + { + ModpacksCH::loadVersion(version, obj); + } + catch (const JSONValidationError &e) + { + emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); + return; + } + m_version = version; + + install(); +} + +void PackInstallTask::onDownloadFailed(QString reason) +{ + jobPtr.reset(); + emitFailed(reason); +} + +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(); + + for(auto target : m_version.targets) { + if(target.type == "game" && target.name == "minecraft") { + components->setComponentVersion("net.minecraft", target.version, true); + continue; + } + } + + for(auto target : m_version.targets) { + if(target.type == "modloader" && target.name == "forge") { + components->setComponentVersion("net.minecraftforge", target.version, true); + } + } + components->saveNow(); + + jobPtr.reset(new NetJob(tr("Mod download"))); + for(auto file : m_version.files) { + if(file.serverOnly) continue; + + auto relpath = FS::PathCombine("minecraft", file.path, file.name); + auto path = FS::PathCombine(m_stagingPath , relpath); + + qDebug() << "Will download" << file.url << "to" << path; + auto dl = Net::Download::makeFile(file.url, path); + jobPtr->addNetAction(dl); + } + connect(jobPtr.get(), &NetJob::succeeded, this, [&]() + { + jobPtr.reset(); + emitSucceeded(); + }); + + connect(jobPtr.get(), &NetJob::failed, [&](QString reason) + { + jobPtr.reset(); + emitFailed(reason); + }); + connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) + { + setProgress(current, total); + }); + + setStatus(tr("Downloading mods...")); + jobPtr->start(); + + instance.setName(m_instName); + instance.setIconKey(m_instIcon); + instanceSettings->resumeSave(); +} + +} diff --git a/api/logic/modplatform/modpacksch/PackInstallTask.h b/api/logic/modplatform/modpacksch/PackInstallTask.h new file mode 100644 index 00000000..e5969dc8 --- /dev/null +++ b/api/logic/modplatform/modpacksch/PackInstallTask.h @@ -0,0 +1,41 @@ +#pragma once + +#include "PackManifest.h" + +#include "InstanceTask.h" +#include "multimc_logic_export.h" +#include "net/NetJob.h" + +namespace ModpacksCH { + +class MULTIMC_LOGIC_EXPORT PackInstallTask : public InstanceTask +{ + Q_OBJECT + +public: + explicit PackInstallTask(Modpack pack, QString version); + virtual ~PackInstallTask(){} + + bool abort() override; + +protected: + virtual void executeTask() override; + +private slots: + void onDownloadSucceeded(); + void onDownloadFailed(QString reason); + +private: + void install(); + +private: + NetJobPtr jobPtr; + QByteArray response; + + Modpack m_pack; + QString m_version_name; + Version m_version; + +}; + +} diff --git a/api/logic/modplatform/modpacksch/PackManifest.cpp b/api/logic/modplatform/modpacksch/PackManifest.cpp new file mode 100644 index 00000000..5ffd18de --- /dev/null +++ b/api/logic/modplatform/modpacksch/PackManifest.cpp @@ -0,0 +1,156 @@ +#include "PackManifest.h" + +#include "Json.h" + +static void loadSpecs(ModpacksCH::Specs & s, QJsonObject & obj) +{ + s.id = Json::requireInteger(obj, "id"); + s.minimum = Json::requireInteger(obj, "minimum"); + s.recommended = Json::requireInteger(obj, "recommended"); +} + +static void loadTag(ModpacksCH::Tag & t, QJsonObject & obj) +{ + t.id = Json::requireInteger(obj, "id"); + t.name = Json::requireString(obj, "name"); +} + +static void loadArt(ModpacksCH::Art & a, QJsonObject & obj) +{ + a.id = Json::requireInteger(obj, "id"); + a.url = Json::requireString(obj, "url"); + a.type = Json::requireString(obj, "type"); + a.width = Json::requireInteger(obj, "width"); + a.height = Json::requireInteger(obj, "height"); + a.compressed = Json::requireBoolean(obj, "compressed"); + a.sha1 = Json::requireString(obj, "sha1"); + a.size = Json::requireInteger(obj, "size"); + a.updated = Json::requireInteger(obj, "updated"); +} + +static void loadAuthor(ModpacksCH::Author & a, QJsonObject & obj) +{ + a.id = Json::requireInteger(obj, "id"); + a.name = Json::requireString(obj, "name"); + a.type = Json::requireString(obj, "type"); + a.website = Json::requireString(obj, "website"); + a.updated = Json::requireInteger(obj, "updated"); +} + +static void loadVersionInfo(ModpacksCH::VersionInfo & v, QJsonObject & obj) +{ + v.id = Json::requireInteger(obj, "id"); + v.name = Json::requireString(obj, "name"); + v.type = Json::requireString(obj, "type"); + v.updated = Json::requireInteger(obj, "updated"); + auto specs = Json::requireObject(obj, "specs"); + loadSpecs(v.specs, specs); +} + +void ModpacksCH::loadModpack(ModpacksCH::Modpack & m, QJsonObject & obj) +{ + m.id = Json::requireInteger(obj, "id"); + m.name = Json::requireString(obj, "name"); + m.synopsis = Json::requireString(obj, "synopsis"); + m.description = Json::requireString(obj, "description"); + m.type = Json::requireString(obj, "type"); + m.featured = Json::requireBoolean(obj, "featured"); + m.installs = Json::requireInteger(obj, "installs"); + m.plays = Json::requireInteger(obj, "plays"); + m.updated = Json::requireInteger(obj, "updated"); + m.refreshed = Json::requireInteger(obj, "refreshed"); + auto artArr = Json::requireArray(obj, "art"); + for (const auto & artRaw : artArr) + { + auto artObj = Json::requireObject(artRaw); + ModpacksCH::Art art; + loadArt(art, artObj); + m.art.append(art); + } + auto authorArr = Json::requireArray(obj, "authors"); + for (const auto & authorRaw : authorArr) + { + auto authorObj = Json::requireObject(authorRaw); + ModpacksCH::Author author; + loadAuthor(author, authorObj); + m.authors.append(author); + } + auto versionArr = Json::requireArray(obj, "versions"); + for (const auto & versionRaw : versionArr) + { + auto versionObj = Json::requireObject(versionRaw); + ModpacksCH::VersionInfo version; + loadVersionInfo(version, versionObj); + m.versions.append(version); + } + auto tagArr = Json::requireArray(obj, "tags"); + for (const auto & tagRaw : tagArr) + { + auto tagObj = Json::requireObject(tagRaw); + ModpacksCH::Tag tag; + loadTag(tag, tagObj); + m.tags.append(tag); + } + m.updated = Json::requireInteger(obj, "updated"); +} + +static void loadVersionTarget(ModpacksCH::VersionTarget & a, QJsonObject & obj) +{ + a.id = Json::requireInteger(obj, "id"); + a.name = Json::requireString(obj, "name"); + a.type = Json::requireString(obj, "type"); + a.version = Json::requireString(obj, "version"); + a.updated = Json::requireInteger(obj, "updated"); +} + +static void loadVersionFile(ModpacksCH::VersionFile & a, QJsonObject & obj) +{ + a.id = Json::requireInteger(obj, "id"); + a.type = Json::requireString(obj, "type"); + a.path = Json::requireString(obj, "path"); + a.name = Json::requireString(obj, "name"); + a.version = Json::requireString(obj, "version"); + a.url = Json::requireString(obj, "url"); + a.sha1 = Json::requireString(obj, "sha1"); + a.size = Json::requireInteger(obj, "size"); + a.clientOnly = Json::requireBoolean(obj, "clientonly"); + a.serverOnly = Json::requireBoolean(obj, "serveronly"); + a.optional = Json::requireBoolean(obj, "optional"); + a.updated = Json::requireInteger(obj, "updated"); +} + +void ModpacksCH::loadVersion(ModpacksCH::Version & m, QJsonObject & obj) +{ + m.id = Json::requireInteger(obj, "id"); + m.parent = Json::requireInteger(obj, "parent"); + m.name = Json::requireString(obj, "name"); + m.type = Json::requireString(obj, "type"); + m.installs = Json::requireInteger(obj, "installs"); + m.plays = Json::requireInteger(obj, "plays"); + m.updated = Json::requireInteger(obj, "updated"); + m.refreshed = Json::requireInteger(obj, "refreshed"); + auto specs = Json::requireObject(obj, "specs"); + loadSpecs(m.specs, specs); + auto targetArr = Json::requireArray(obj, "targets"); + for (const auto & targetRaw : targetArr) + { + auto versionObj = Json::requireObject(targetRaw); + ModpacksCH::VersionTarget target; + loadVersionTarget(target, versionObj); + m.targets.append(target); + } + auto fileArr = Json::requireArray(obj, "files"); + for (const auto & fileRaw : fileArr) + { + auto fileObj = Json::requireObject(fileRaw); + ModpacksCH::VersionFile file; + loadVersionFile(file, fileObj); + m.files.append(file); + } +} + +//static void loadVersionChangelog(ModpacksCH::VersionChangelog & m, QJsonObject & obj) +//{ +// m.content = Json::requireString(obj, "content"); +// m.updated = Json::requireInteger(obj, "updated"); +//} diff --git a/api/logic/modplatform/modpacksch/PackManifest.h b/api/logic/modplatform/modpacksch/PackManifest.h new file mode 100644 index 00000000..518fffbf --- /dev/null +++ b/api/logic/modplatform/modpacksch/PackManifest.h @@ -0,0 +1,127 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "multimc_logic_export.h" + +namespace ModpacksCH +{ + +struct Specs +{ + int id; + int minimum; + int recommended; +}; + +struct Tag +{ + int id; + QString name; +}; + +struct Art +{ + int id; + QString url; + QString type; + int width; + int height; + bool compressed; + QString sha1; + int size; + int64_t updated; +}; + +struct Author +{ + int id; + QString name; + QString type; + QString website; + int64_t updated; +}; + +struct VersionInfo +{ + int id; + QString name; + QString type; + int64_t updated; + Specs specs; +}; + +struct Modpack +{ + int id; + QString name; + QString synopsis; + QString description; + QString type; + bool featured; + int installs; + int plays; + int64_t updated; + int64_t refreshed; + QVector art; + QVector authors; + QVector versions; + QVector tags; +}; + +struct VersionTarget +{ + int id; + QString type; + QString name; + QString version; + int64_t updated; +}; + +struct VersionFile +{ + int id; + QString type; + QString path; + QString name; + QString version; + QString url; + QString sha1; + int size; + bool clientOnly; + bool serverOnly; + bool optional; + int64_t updated; +}; + +struct Version +{ + int id; + int parent; + QString name; + QString type; + int installs; + int plays; + int64_t updated; + int64_t refreshed; + Specs specs; + QVector targets; + QVector files; +}; + +struct VersionChangelog +{ + QString content; + int64_t updated; +}; + +MULTIMC_LOGIC_EXPORT void loadModpack(Modpack & m, QJsonObject & obj); + +MULTIMC_LOGIC_EXPORT void loadVersion(Version & m, QJsonObject & obj); +} + +Q_DECLARE_METATYPE(ModpacksCH::Modpack) diff --git a/application/CMakeLists.txt b/application/CMakeLists.txt index 1d5b8e04..802789a2 100644 --- a/application/CMakeLists.txt +++ b/application/CMakeLists.txt @@ -124,6 +124,10 @@ SET(MULTIMC_SOURCES # GUI - platform pages pages/modplatform/VanillaPage.cpp pages/modplatform/VanillaPage.h + pages/modplatform/ftb/FtbModel.cpp + pages/modplatform/ftb/FtbModel.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 @@ -250,6 +254,7 @@ SET(MULTIMC_UIS # Platform pages pages/modplatform/VanillaPage.ui + pages/modplatform/ftb/FtbPage.ui pages/modplatform/legacy_ftb/Page.ui pages/modplatform/twitch/TwitchPage.ui pages/modplatform/ImportPage.ui diff --git a/application/dialogs/NewInstanceDialog.cpp b/application/dialogs/NewInstanceDialog.cpp index 511f991e..d8abdbd4 100644 --- a/application/dialogs/NewInstanceDialog.cpp +++ b/application/dialogs/NewInstanceDialog.cpp @@ -34,6 +34,7 @@ #include "widgets/PageContainer.h" #include +#include #include #include #include @@ -125,6 +126,7 @@ QList NewInstanceDialog::getPages() { new VanillaPage(this), importPage, + new FtbPage(this), new LegacyFTB::Page(this), twitchPage }; diff --git a/application/pages/modplatform/ftb/FtbModel.cpp b/application/pages/modplatform/ftb/FtbModel.cpp new file mode 100644 index 00000000..ecdcb00b --- /dev/null +++ b/application/pages/modplatform/ftb/FtbModel.cpp @@ -0,0 +1,302 @@ +#include "FtbModel.h" + +#include "BuildConfig.h" +#include "Env.h" +#include "MultiMC.h" +#include "Json.h" + +#include + +namespace Ftb { + +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); + } + + ModpacksCH::Modpack pack = modpacks.at(pos); + if(role == Qt::DisplayRole) + { + return pack.name; + } + else if (role == Qt::ToolTipRole) + { + return pack.synopsis; + } + else if(role == Qt::DecorationRole) + { + QIcon placeholder = MMC->getThemedIcon("screenshot-placeholder"); + + auto iter = m_logoMap.find(pack.name); + if (iter != m_logoMap.end()) { + auto & logo = *iter; + if(!logo.result.isNull()) { + return logo.result; + } + return placeholder; + } + + for(auto art : pack.art) { + if(art.type == "square") { + ((ListModel *)this)->requestLogo(pack.name, art.url); + } + } + return placeholder; + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + + return QVariant(); +} + +void ListModel::performSearch() +{ + auto *netJob = new NetJob("Ftb::Search"); + QString searchUrl; + if(currentSearchTerm.isEmpty()) { + searchUrl = BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/popular/plays/100"; + } + else { + searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/search/25?term=%1") + .arg(currentSearchTerm); + } + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + jobPtr = netJob; + jobPtr->start(); + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); +} + +void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) +{ + if(m_logoMap.contains(logo)) + { + callback(ENV.metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + } + else + { + requestLogo(logo, logoUrl); + } +} + +void ListModel::searchWithTerm(const QString &term) +{ + if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) { + return; + } + currentSearchTerm = term; + if(jobPtr) { + jobPtr->abort(); + searchState = ResetRequested; + return; + } + else { + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + } + performSearch(); +} + +void ListModel::searchRequestFinished() +{ + jobPtr.reset(); + remainingPacks.clear(); + + 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 packs = doc.object().value("packs").toArray(); + for(auto pack : packs) { + auto packId = pack.toInt(); + remainingPacks.append(packId); + } + + if(!remainingPacks.isEmpty()) { + currentPack = remainingPacks.at(0); + requestPack(); + } +} + +void ListModel::searchRequestFailed(QString reason) +{ + jobPtr.reset(); + remainingPacks.clear(); + + if(searchState == ResetRequested) { + beginResetModel(); + modpacks.clear(); + endResetModel(); + + performSearch(); + } else { + searchState = Finished; + } +} + +void ListModel::requestPack() +{ + auto *netJob = new NetJob("Ftb::Search"); + auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1") + .arg(currentPack); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + jobPtr = netJob; + jobPtr->start(); + + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::packRequestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::packRequestFailed); +} + +void ListModel::packRequestFinished() +{ + jobPtr.reset(); + remainingPacks.removeOne(currentPack); + + 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(); + + ModpacksCH::Modpack pack; + try + { + ModpacksCH::loadModpack(pack, obj); + } + catch (const JSONValidationError &e) + { + qDebug() << QString::fromUtf8(response); + qWarning() << "Error while reading pack manifest from FTB: " << e.cause(); + return; + } + + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size()); + modpacks.append(pack); + endInsertRows(); + + if(!remainingPacks.isEmpty()) { + currentPack = remainingPacks.at(0); + requestPack(); + } +} + +void ListModel::packRequestFailed(QString reason) +{ + jobPtr.reset(); + remainingPacks.removeOne(currentPack); +} + +void ListModel::logoLoaded(QString logo, bool stale) +{ + auto & logoObj = m_logoMap[logo]; + logoObj.downloadJob.reset(); + QString smallPath = logoObj.fullpath + ".small"; + + QFileInfo smallInfo(smallPath); + + if(stale || !smallInfo.exists()) { + QImage image(logoObj.fullpath); + if (image.isNull()) + { + logoObj.failed = true; + return; + } + QImage small; + if (image.width() > image.height()) { + small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation); + } + else { + small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation); + } + QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2); + QImage square(QSize(256, 256), QImage::Format_ARGB32); + square.fill(Qt::transparent); + + QPainter painter(&square); + painter.drawImage(offset, small); + painter.end(); + + square.save(logoObj.fullpath + ".small", "PNG"); + } + + logoObj.result = QIcon(logoObj.fullpath + ".small"); + for(int i = 0; i < modpacks.size(); i++) { + if(modpacks[i].name == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + } + } +} + +void ListModel::logoFailed(QString logo) +{ + m_logoMap[logo].failed = true; + m_logoMap[logo].downloadJob.reset(); +} + +void ListModel::requestLogo(QString logo, QString url) +{ + if(m_logoMap.contains(logo)) { + return; + } + + MetaEntryPtr entry = ENV.metacache()->resolveEntry("ModpacksCHPacks", QString("logos/%1").arg(logo.section(".", 0, 0))); + + bool stale = entry->isStale(); + + NetJob *job = new NetJob(QString("FTB Icon Download %1").arg(logo)); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job, &NetJob::finished, this, [this, logo, fullPath, stale] + { + logoLoaded(logo, stale); + }); + + QObject::connect(job, &NetJob::failed, this, [this, logo] + { + logoFailed(logo); + }); + + auto &newLogoEntry = m_logoMap[logo]; + newLogoEntry.downloadJob = job; + newLogoEntry.fullpath = fullPath; + job->start(); +} + +} diff --git a/application/pages/modplatform/ftb/FtbModel.h b/application/pages/modplatform/ftb/FtbModel.h new file mode 100644 index 00000000..b480797f --- /dev/null +++ b/application/pages/modplatform/ftb/FtbModel.h @@ -0,0 +1,68 @@ +#pragma once + +#include + +#include "modplatform/modpacksch/PackManifest.h" +#include "net/NetJob.h" +#include + +namespace Ftb { + +struct Logo { + QString fullpath; + NetJobPtr downloadJob; + QIcon result; + bool failed = false; +}; + +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 getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); + void searchWithTerm(const QString & term); + +private slots: + void performSearch(); + void searchRequestFinished(); + void searchRequestFailed(QString reason); + + void requestPack(); + void packRequestFinished(); + void packRequestFailed(QString reason); + + void logoFailed(QString logo); + void logoLoaded(QString logo, bool stale); + +private: + void requestLogo(QString file, QString url); + +private: + QList modpacks; + LogoMap m_logoMap; + + QString currentSearchTerm; + enum SearchState { + None, + CanPossiblyFetchMore, + ResetRequested, + Finished + } searchState = None; + NetJobPtr jobPtr; + int currentPack; + QList remainingPacks; + QByteArray response; +}; + +} diff --git a/application/pages/modplatform/ftb/FtbPage.cpp b/application/pages/modplatform/ftb/FtbPage.cpp new file mode 100644 index 00000000..3a09780e --- /dev/null +++ b/application/pages/modplatform/ftb/FtbPage.cpp @@ -0,0 +1,109 @@ +#include "FtbPage.h" +#include "ui_FtbPage.h" + +#include + +#include "dialogs/NewInstanceDialog.h" +#include "modplatform/modpacksch/PackInstallTask.h" + +FtbPage::FtbPage(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), ui(new Ui::FtbPage), dialog(dialog) +{ + ui->setupUi(this); + connect(ui->searchButton, &QPushButton::clicked, this, &FtbPage::triggerSearch); + ui->searchEdit->installEventFilter(this); + model = new Ftb::ListModel(this); + ui->packView->setModel(model); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FtbPage::onSelectionChanged); + + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FtbPage::onVersionSelectionChanged); +} + +FtbPage::~FtbPage() +{ + delete ui; +} + +bool FtbPage::eventFilter(QObject* watched, QEvent* event) +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Return) { + triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QWidget::eventFilter(watched, event); +} + +bool FtbPage::shouldDisplay() const +{ + return true; +} + +void FtbPage::openedImpl() +{ + dialog->setSuggestedPack(); + triggerSearch(); +} + +void FtbPage::suggestCurrent() +{ + if(isOpened) + { + dialog->setSuggestedPack(selected.name, new ModpacksCH::PackInstallTask(selected, selectedVersion)); + + for(auto art : selected.art) { + if(art.type == "square") { + QString editedLogoName; + editedLogoName = selected.name; + + model->getLogo(selected.name, art.url, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo + ".small", editedLogoName); + }); + } + } + } +} + +void FtbPage::triggerSearch() +{ + model->searchWithTerm(ui->searchEdit->text()); +} + +void FtbPage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + ui->versionSelectionBox->clear(); + + if(!first.isValid()) + { + if(isOpened) + { + dialog->setSuggestedPack(); + } + return; + } + + selected = model->data(first, Qt::UserRole).value(); + + // reverse foreach, so that the newest versions are first + for (auto i = selected.versions.size(); i--;) { + ui->versionSelectionBox->addItem(selected.versions.at(i).name); + } + + suggestCurrent(); +} + +void FtbPage::onVersionSelectionChanged(QString data) +{ + if(data.isNull() || data.isEmpty()) + { + selectedVersion = ""; + return; + } + + selectedVersion = data; + suggestCurrent(); +} diff --git a/application/pages/modplatform/ftb/FtbPage.h b/application/pages/modplatform/ftb/FtbPage.h new file mode 100644 index 00000000..80f152c6 --- /dev/null +++ b/application/pages/modplatform/ftb/FtbPage.h @@ -0,0 +1,77 @@ +/* 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 "FtbModel.h" + +#include + +#include "MultiMC.h" +#include "pages/BasePage.h" +#include "tasks/Task.h" + +namespace Ui +{ + class FtbPage; +} + +class NewInstanceDialog; + +class FtbPage : public QWidget, public BasePage +{ +Q_OBJECT + +public: + explicit FtbPage(NewInstanceDialog* dialog, QWidget *parent = 0); + virtual ~FtbPage(); + virtual QString displayName() const override + { + return tr("FTB"); + } + virtual QIcon icon() const override + { + return MMC->getThemedIcon("ftb_logo"); + } + virtual QString id() const override + { + return "ftb"; + } + virtual QString helpPage() const override + { + return "FTB-platform"; + } + virtual bool shouldDisplay() const override; + + void openedImpl() override; + + bool eventFilter(QObject * watched, QEvent * event) override; + +private: + void suggestCurrent(); + +private slots: + void triggerSearch(); + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString data); + +private: + Ui::FtbPage *ui = nullptr; + NewInstanceDialog* dialog = nullptr; + Ftb::ListModel* model = nullptr; + + ModpacksCH::Modpack selected; + QString selectedVersion; +}; diff --git a/application/pages/modplatform/ftb/FtbPage.ui b/application/pages/modplatform/ftb/FtbPage.ui new file mode 100644 index 00000000..772b0276 --- /dev/null +++ b/application/pages/modplatform/ftb/FtbPage.ui @@ -0,0 +1,64 @@ + + + FtbPage + + + + 0 + 0 + 875 + 745 + + + + + + + true + + + + 48 + 48 + + + + true + + + + + + + Search + + + + + + + + + + + + + + + Version selected: + + + + + + + + + searchEdit + searchButton + packView + versionSelectionBox + + + + diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index a80af3d2..7e85aa5e 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -75,6 +75,8 @@ public: QString FMLLIBS_FORGE_BASE_URL = "https://files.minecraftforge.net/fmllibs/"; QString TRANSLATIONS_BASE_URL = "https://files.multimc.org/translations/"; + QString MODPACKSCH_API_BASE_URL = "https://api.modpacks.ch/"; + QString LEGACY_FTB_CDN_BASE_URL = "https://dist.creeper.host/FTB2/"; /** From c6c9feb3a2006f0b37736799f003a0fb87f68b18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Fri, 21 Aug 2020 02:40:19 +0200 Subject: [PATCH 2/6] NOISSUE attempt to fix build on macOS --- api/logic/CMakeLists.txt | 8 ++++---- .../{PackInstallTask.cpp => FTBPackInstallTask.cpp} | 2 +- .../{PackInstallTask.h => FTBPackInstallTask.h} | 2 +- .../modpacksch/{PackManifest.cpp => FTBPackManifest.cpp} | 2 +- .../modpacksch/{PackManifest.h => FTBPackManifest.h} | 0 application/pages/modplatform/ftb/FtbModel.h | 2 +- application/pages/modplatform/ftb/FtbPage.cpp | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) rename api/logic/modplatform/modpacksch/{PackInstallTask.cpp => FTBPackInstallTask.cpp} (99%) rename api/logic/modplatform/modpacksch/{PackInstallTask.h => FTBPackInstallTask.h} (95%) rename api/logic/modplatform/modpacksch/{PackManifest.cpp => FTBPackManifest.cpp} (99%) rename api/logic/modplatform/modpacksch/{PackManifest.h => FTBPackManifest.h} (100%) diff --git a/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt index 2a1e51fc..740cd886 100644 --- a/api/logic/CMakeLists.txt +++ b/api/logic/CMakeLists.txt @@ -452,10 +452,10 @@ set(FLAME_SOURCES ) set(MODPACKSCH_SOURCES - modplatform/modpacksch/PackInstallTask.h - modplatform/modpacksch/PackInstallTask.cpp - modplatform/modpacksch/PackManifest.h - modplatform/modpacksch/PackManifest.cpp + modplatform/modpacksch/FTBPackInstallTask.h + modplatform/modpacksch/FTBPackInstallTask.cpp + modplatform/modpacksch/FTBPackManifest.h + modplatform/modpacksch/FTBPackManifest.cpp ) add_unit_test(Index diff --git a/api/logic/modplatform/modpacksch/PackInstallTask.cpp b/api/logic/modplatform/modpacksch/FTBPackInstallTask.cpp similarity index 99% rename from api/logic/modplatform/modpacksch/PackInstallTask.cpp rename to api/logic/modplatform/modpacksch/FTBPackInstallTask.cpp index f8572a4d..fdfe560a 100644 --- a/api/logic/modplatform/modpacksch/PackInstallTask.cpp +++ b/api/logic/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -1,4 +1,4 @@ -#include "PackInstallTask.h" +#include "FTBPackInstallTask.h" #include "BuildConfig.h" #include "FileSystem.h" diff --git a/api/logic/modplatform/modpacksch/PackInstallTask.h b/api/logic/modplatform/modpacksch/FTBPackInstallTask.h similarity index 95% rename from api/logic/modplatform/modpacksch/PackInstallTask.h rename to api/logic/modplatform/modpacksch/FTBPackInstallTask.h index e5969dc8..c5a80751 100644 --- a/api/logic/modplatform/modpacksch/PackInstallTask.h +++ b/api/logic/modplatform/modpacksch/FTBPackInstallTask.h @@ -1,6 +1,6 @@ #pragma once -#include "PackManifest.h" +#include "FTBPackManifest.h" #include "InstanceTask.h" #include "multimc_logic_export.h" diff --git a/api/logic/modplatform/modpacksch/PackManifest.cpp b/api/logic/modplatform/modpacksch/FTBPackManifest.cpp similarity index 99% rename from api/logic/modplatform/modpacksch/PackManifest.cpp rename to api/logic/modplatform/modpacksch/FTBPackManifest.cpp index 5ffd18de..35626cb8 100644 --- a/api/logic/modplatform/modpacksch/PackManifest.cpp +++ b/api/logic/modplatform/modpacksch/FTBPackManifest.cpp @@ -1,4 +1,4 @@ -#include "PackManifest.h" +#include "FTBPackManifest.h" #include "Json.h" diff --git a/api/logic/modplatform/modpacksch/PackManifest.h b/api/logic/modplatform/modpacksch/FTBPackManifest.h similarity index 100% rename from api/logic/modplatform/modpacksch/PackManifest.h rename to api/logic/modplatform/modpacksch/FTBPackManifest.h diff --git a/application/pages/modplatform/ftb/FtbModel.h b/application/pages/modplatform/ftb/FtbModel.h index b480797f..9c057d73 100644 --- a/application/pages/modplatform/ftb/FtbModel.h +++ b/application/pages/modplatform/ftb/FtbModel.h @@ -2,7 +2,7 @@ #include -#include "modplatform/modpacksch/PackManifest.h" +#include "modplatform/modpacksch/FTBPackManifest.h" #include "net/NetJob.h" #include diff --git a/application/pages/modplatform/ftb/FtbPage.cpp b/application/pages/modplatform/ftb/FtbPage.cpp index 3a09780e..c82508b3 100644 --- a/application/pages/modplatform/ftb/FtbPage.cpp +++ b/application/pages/modplatform/ftb/FtbPage.cpp @@ -4,7 +4,7 @@ #include #include "dialogs/NewInstanceDialog.h" -#include "modplatform/modpacksch/PackInstallTask.h" +#include "modplatform/modpacksch/FTBPackInstallTask.h" FtbPage::FtbPage(NewInstanceDialog* dialog, QWidget *parent) : QWidget(parent), ui(new Ui::FtbPage), dialog(dialog) From 8a0027c73a755849bf5b58c1509c71a543ddb982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sat, 22 Aug 2020 01:34:55 +0200 Subject: [PATCH 3/6] NOISSUE Add world icons and world icon reset button --- api/logic/minecraft/World.cpp | 17 +++++++ api/logic/minecraft/World.h | 7 +++ api/logic/minecraft/WorldList.cpp | 17 +++++++ api/logic/minecraft/WorldList.h | 6 ++- application/pages/instance/WorldListPage.cpp | 47 +++++++++++++++++++- application/pages/instance/WorldListPage.h | 1 + application/pages/instance/WorldListPage.ui | 15 +++++++ 7 files changed, 108 insertions(+), 2 deletions(-) diff --git a/api/logic/minecraft/World.cpp b/api/logic/minecraft/World.cpp index 17dbf4ae..06a6080a 100644 --- a/api/logic/minecraft/World.cpp +++ b/api/logic/minecraft/World.cpp @@ -138,14 +138,31 @@ void World::repath(const QFileInfo &file) m_folderName = file.fileName(); if(file.isFile() && file.suffix() == "zip") { + m_iconFile = QString(); readFromZip(file); } else if(file.isDir()) { + QFileInfo assumedIconPath(file.absoluteFilePath() + "/icon.png"); + if(assumedIconPath.exists()) { + m_iconFile = assumedIconPath.absoluteFilePath(); + } readFromFS(file); } } +bool World::resetIcon() +{ + if(m_iconFile.isNull()) { + return false; + } + if(QFile(m_iconFile).remove()) { + m_iconFile = QString(); + return true; + } + return false; +} + void World::readFromFS(const QFileInfo &file) { auto bytes = getLevelDatDataFromFS(file); diff --git a/api/logic/minecraft/World.h b/api/logic/minecraft/World.h index 818701fa..d04c1040 100644 --- a/api/logic/minecraft/World.h +++ b/api/logic/minecraft/World.h @@ -40,6 +40,10 @@ public: { return m_actualName; } + QString iconFile() const + { + return m_iconFile; + } QDateTime lastPlayed() const { return m_lastPlayed; @@ -70,6 +74,8 @@ public: bool replace(World &with); // change the world's filesystem path (used by world lists for *MAGIC* purposes) void repath(const QFileInfo &file); + // remove the icon file, if any + bool resetIcon(); bool rename(const QString &to); bool install(const QString &to, const QString &name= QString()); @@ -88,6 +94,7 @@ protected: QString m_containerOffsetPath; QString m_folderName; QString m_actualName; + QString m_iconFile; QDateTime levelDatTime; QDateTime m_lastPlayed; int64_t m_randomSeed = 0; diff --git a/api/logic/minecraft/WorldList.cpp b/api/logic/minecraft/WorldList.cpp index b7a24434..94b59da4 100644 --- a/api/logic/minecraft/WorldList.cpp +++ b/api/logic/minecraft/WorldList.cpp @@ -136,6 +136,19 @@ bool WorldList::deleteWorlds(int first, int last) return true; } +bool WorldList::resetIcon(int row) +{ + if (row >= worlds.size() || row < 0) + return false; + World &m = worlds[row]; + if(m.resetIcon()) { + emit dataChanged(index(row), index(row), {WorldList::IconFileRole}); + return true; + } + return false; +} + + int WorldList::columnCount(const QModelIndex &parent) const { return 3; @@ -195,6 +208,10 @@ QVariant WorldList::data(const QModelIndex &index, int role) const { return world.lastPlayed(); } + case IconFileRole: + { + return world.iconFile(); + } default: return QVariant(); } diff --git a/api/logic/minecraft/WorldList.h b/api/logic/minecraft/WorldList.h index 37ad330a..db44daf9 100644 --- a/api/logic/minecraft/WorldList.h +++ b/api/logic/minecraft/WorldList.h @@ -44,7 +44,8 @@ public: SeedRole, NameRole, GameModeRole, - LastPlayedRole + LastPlayedRole, + IconFileRole }; WorldList(const QString &dir); @@ -81,6 +82,9 @@ public: /// Deletes the mod at the given index. virtual bool deleteWorld(int index); + /// Removes the world icon, if any + virtual bool resetIcon(int index); + /// Deletes all the selected mods virtual bool deleteWorlds(int first, int last); diff --git a/application/pages/instance/WorldListPage.cpp b/application/pages/instance/WorldListPage.cpp index 8358a0f1..75741d22 100644 --- a/application/pages/instance/WorldListPage.cpp +++ b/application/pages/instance/WorldListPage.cpp @@ -31,6 +31,33 @@ #include #include +class WorldListProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + WorldListProxyModel(QObject *parent) : QSortFilterProxyModel(parent) {} + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const + { + QModelIndex sourceIndex = mapToSource(index); + + if (index.column() == 0 && role == Qt::DecorationRole) + { + WorldList *worlds = qobject_cast(sourceModel()); + auto iconFile = worlds->data(sourceIndex, WorldList::IconFileRole).toString(); + if(iconFile.isNull()) { + // NOTE: Minecraft uses the same placeholder for servers AND worlds + return MMC->getThemedIcon("unknown_server"); + } + return QIcon(iconFile); + } + + return sourceIndex.data(role); + } +}; + + WorldListPage::WorldListPage(BaseInstance *inst, std::shared_ptr worlds, QWidget *parent) : QMainWindow(parent), m_inst(inst), ui(new Ui::WorldListPage), m_worlds(worlds) { @@ -38,13 +65,14 @@ WorldListPage::WorldListPage(BaseInstance *inst, std::shared_ptr worl ui->toolBar->insertSpacer(ui->actionRefresh); - QSortFilterProxyModel * proxy = new QSortFilterProxyModel(this); + WorldListProxyModel * proxy = new WorldListProxyModel(this); proxy->setSortCaseSensitivity(Qt::CaseInsensitive); proxy->setSourceModel(m_worlds.get()); ui->worldTreeView->setSortingEnabled(true); ui->worldTreeView->setModel(proxy); ui->worldTreeView->installEventFilter(this); ui->worldTreeView->setContextMenuPolicy(Qt::CustomContextMenu); + ui->worldTreeView->setIconSize(QSize(64,64)); connect(ui->worldTreeView, &QTreeView::customContextMenuRequested, this, &WorldListPage::ShowContextMenu); auto head = ui->worldTreeView->header(); @@ -142,6 +170,19 @@ void WorldListPage::on_actionView_Folder_triggered() DesktopServices::openDirectory(m_worlds->dir().absolutePath(), true); } +void WorldListPage::on_actionReset_Icon_triggered() +{ + auto proxiedIndex = getSelectedWorld(); + + if(!proxiedIndex.isValid()) + return; + + if(m_worlds->resetIcon(proxiedIndex.row())) { + ui->actionReset_Icon->setEnabled(false); + } +} + + QModelIndex WorldListPage::getSelectedWorld() { auto index = ui->worldTreeView->selectionModel()->currentIndex(); @@ -255,6 +296,8 @@ void WorldListPage::worldChanged(const QModelIndex ¤t, const QModelIndex & ui->actionRemove->setEnabled(enable); ui->actionCopy->setEnabled(enable); ui->actionRename->setEnabled(enable); + bool hasIcon = !index.data(WorldList::IconFileRole).isNull(); + ui->actionReset_Icon->setEnabled(enable && hasIcon); } void WorldListPage::on_actionAdd_triggered() @@ -342,3 +385,5 @@ void WorldListPage::on_actionRefresh_triggered() { m_worlds->update(); } + +#include "WorldListPage.moc" diff --git a/application/pages/instance/WorldListPage.h b/application/pages/instance/WorldListPage.h index c39420da..8ff14819 100644 --- a/application/pages/instance/WorldListPage.h +++ b/application/pages/instance/WorldListPage.h @@ -90,6 +90,7 @@ private slots: void on_actionRename_triggered(); void on_actionRefresh_triggered(); void on_actionView_Folder_triggered(); + void on_actionReset_Icon_triggered(); void worldChanged(const QModelIndex ¤t, const QModelIndex &previous); void mceditState(LoggedProcess::State state); diff --git a/application/pages/instance/WorldListPage.ui b/application/pages/instance/WorldListPage.ui index ddb3dfa9..8d00f8f4 100644 --- a/application/pages/instance/WorldListPage.ui +++ b/application/pages/instance/WorldListPage.ui @@ -41,6 +41,12 @@ true + + false + + + false + true @@ -79,6 +85,7 @@ + @@ -124,6 +131,14 @@ View Folder + + + Reset Icon + + + Remove world icon to make the game re-generate it on next load. + + From feae420450d92733f237d32dca9303ff51fe0525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Mon, 7 Sep 2020 22:28:41 +0200 Subject: [PATCH 4/6] NOISSUE add 'java.vendor' to the checker and display/log the value --- api/logic/java/JavaChecker.cpp | 4 +++- api/logic/java/JavaChecker.h | 1 + api/logic/java/launch/CheckJava.cpp | 13 ++++++++----- api/logic/java/launch/CheckJava.h | 2 +- application/JavaCommon.cpp | 3 ++- application/MultiMC.cpp | 1 + libraries/javacheck/JavaCheck.java | 2 +- 7 files changed, 17 insertions(+), 9 deletions(-) diff --git a/api/logic/java/JavaChecker.cpp b/api/logic/java/JavaChecker.cpp index ca0f4bde..d78d6505 100644 --- a/api/logic/java/JavaChecker.cpp +++ b/api/logic/java/JavaChecker.cpp @@ -115,7 +115,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) } } - if(!results.contains("os.arch") || !results.contains("java.version") || !success) + if(!results.contains("os.arch") || !results.contains("java.version") || !results.contains("java.vendor") || !success) { result.validity = JavaCheckResult::Validity::ReturnedInvalidData; emit checkFinished(result); @@ -124,6 +124,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) auto os_arch = results["os.arch"]; auto java_version = results["java.version"]; + auto java_vendor = results["java.vendor"]; bool is_64 = os_arch == "x86_64" || os_arch == "amd64"; @@ -132,6 +133,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) result.mojangPlatform = is_64 ? "64" : "32"; result.realPlatform = os_arch; result.javaVersion = java_version; + result.javaVendor = java_vendor; qDebug() << "Java checker succeeded."; emit checkFinished(result); } diff --git a/api/logic/java/JavaChecker.h b/api/logic/java/JavaChecker.h index af0dcb90..0a96249a 100644 --- a/api/logic/java/JavaChecker.h +++ b/api/logic/java/JavaChecker.h @@ -17,6 +17,7 @@ struct MULTIMC_LOGIC_EXPORT JavaCheckResult QString mojangPlatform; QString realPlatform; JavaVersion javaVersion; + QString javaVendor; QString outLog; QString errorLog; bool is_64bit = false; diff --git a/api/logic/java/launch/CheckJava.cpp b/api/logic/java/launch/CheckJava.cpp index b75c6dc6..d8be0581 100644 --- a/api/logic/java/launch/CheckJava.cpp +++ b/api/logic/java/launch/CheckJava.cpp @@ -56,9 +56,10 @@ void CheckJava::executeTask() auto storedUnixTime = settings->get("JavaTimestamp").toLongLong(); auto storedArchitecture = settings->get("JavaArchitecture").toString(); auto storedVersion = settings->get("JavaVersion").toString(); + auto storedVendor = settings->get("JavaVendor").toString(); m_javaUnixTime = javaUnixTime; // if timestamps are not the same, or something is missing, check! - if (javaUnixTime != storedUnixTime || storedVersion.size() == 0 || storedArchitecture.size() == 0) + if (javaUnixTime != storedUnixTime || storedVersion.size() == 0 || storedArchitecture.size() == 0 || storedVendor.size() == 0) { m_JavaChecker = new JavaChecker(); emit logLine(tr("Checking Java version..."), MessageLevel::MultiMC); @@ -71,7 +72,8 @@ void CheckJava::executeTask() { auto verString = instance->settings()->get("JavaVersion").toString(); auto archString = instance->settings()->get("JavaArchitecture").toString(); - printJavaInfo(verString, archString); + auto vendorString = instance->settings()->get("JavaVendor").toString(); + printJavaInfo(verString, archString, vendorString); } emitSucceeded(); } @@ -102,9 +104,10 @@ void CheckJava::checkJavaFinished(JavaCheckResult result) case JavaCheckResult::Validity::Valid: { auto instance = m_parent->instance(); - printJavaInfo(result.javaVersion.toString(), result.mojangPlatform); + printJavaInfo(result.javaVersion.toString(), result.mojangPlatform, result.javaVendor); instance->settings()->set("JavaVersion", result.javaVersion.toString()); instance->settings()->set("JavaArchitecture", result.mojangPlatform); + instance->settings()->set("JavaVendor", result.javaVendor); instance->settings()->set("JavaTimestamp", m_javaUnixTime); emitSucceeded(); return; @@ -112,9 +115,9 @@ void CheckJava::checkJavaFinished(JavaCheckResult result) } } -void CheckJava::printJavaInfo(const QString& version, const QString& architecture) +void CheckJava::printJavaInfo(const QString& version, const QString& architecture, const QString & vendor) { - emit logLine(tr("Java is version %1, using %2-bit architecture.\n\n").arg(version, architecture), MessageLevel::MultiMC); + emit logLine(tr("Java is version %1, using %2-bit architecture, from %3.\n\n").arg(version, architecture, vendor), MessageLevel::MultiMC); printSystemInfo(true, architecture == "64"); } diff --git a/api/logic/java/launch/CheckJava.h b/api/logic/java/launch/CheckJava.h index f0dd2308..fddfd791 100644 --- a/api/logic/java/launch/CheckJava.h +++ b/api/logic/java/launch/CheckJava.h @@ -35,7 +35,7 @@ private slots: void checkJavaFinished(JavaCheckResult result); private: - void printJavaInfo(const QString & version, const QString & architecture); + void printJavaInfo(const QString & version, const QString & architecture, const QString & vendor); void printSystemInfo(bool javaIsKnown, bool javaIs64bit); private: diff --git a/application/JavaCommon.cpp b/application/JavaCommon.cpp index 563dfb35..92a058f0 100644 --- a/application/JavaCommon.cpp +++ b/application/JavaCommon.cpp @@ -24,7 +24,8 @@ void JavaCommon::javaWasOk(QWidget *parent, JavaCheckResult result) { QString text; text += QObject::tr("Java test succeeded!
Platform reported: %1
Java version " - "reported: %2
").arg(result.realPlatform, result.javaVersion.toString()); + "reported: %2
Java vendor " + "reported: %3
").arg(result.realPlatform, result.javaVersion.toString(), result.javaVendor); if (result.errorLog.size()) { auto htmlError = result.errorLog; diff --git a/application/MultiMC.cpp b/application/MultiMC.cpp index eeab500e..8e2557d4 100644 --- a/application/MultiMC.cpp +++ b/application/MultiMC.cpp @@ -505,6 +505,7 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("JavaTimestamp", 0); m_settings->registerSetting("JavaArchitecture", ""); m_settings->registerSetting("JavaVersion", ""); + m_settings->registerSetting("JavaVendor", ""); m_settings->registerSetting("LastHostname", ""); m_settings->registerSetting("JvmArgs", ""); diff --git a/libraries/javacheck/JavaCheck.java b/libraries/javacheck/JavaCheck.java index 69933040..560abbc0 100644 --- a/libraries/javacheck/JavaCheck.java +++ b/libraries/javacheck/JavaCheck.java @@ -2,7 +2,7 @@ import java.lang.Integer; public class JavaCheck { - private static final String[] keys = {"os.arch", "java.version"}; + private static final String[] keys = {"os.arch", "java.version", "java.vendor"}; public static void main (String [] args) { int ret = 0; From 27e43d037e3b75862542c6437ccb59e1d42452d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Mon, 7 Sep 2020 22:33:57 +0200 Subject: [PATCH 5/6] NOISSUE remove translations from CheckJava launch step --- api/logic/java/launch/CheckJava.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/api/logic/java/launch/CheckJava.cpp b/api/logic/java/launch/CheckJava.cpp index d8be0581..fb7641d6 100644 --- a/api/logic/java/launch/CheckJava.cpp +++ b/api/logic/java/launch/CheckJava.cpp @@ -33,17 +33,17 @@ void CheckJava::executeTask() if (perInstance) { emit logLine( - tr("The java binary \"%1\" couldn't be found. Please fix the java path " + QString("The java binary \"%1\" couldn't be found. Please fix the java path " "override in the instance's settings or disable it.").arg(m_javaPath), MessageLevel::Warning); } else { - emit logLine(tr("The java binary \"%1\" couldn't be found. Please set up java in " + emit logLine(QString("The java binary \"%1\" couldn't be found. Please set up java in " "the settings.").arg(m_javaPath), MessageLevel::Warning); } - emitFailed(tr("Java path is not valid.")); + emitFailed(QString("Java path is not valid.")); return; } else @@ -62,7 +62,7 @@ void CheckJava::executeTask() if (javaUnixTime != storedUnixTime || storedVersion.size() == 0 || storedArchitecture.size() == 0 || storedVendor.size() == 0) { m_JavaChecker = new JavaChecker(); - emit logLine(tr("Checking Java version..."), MessageLevel::MultiMC); + emit logLine(QString("Checking Java version..."), MessageLevel::MultiMC); connect(m_JavaChecker.get(), &JavaChecker::checkFinished, this, &CheckJava::checkJavaFinished); m_JavaChecker->m_path = realJavaPath; m_JavaChecker->performCheck(); @@ -85,16 +85,16 @@ void CheckJava::checkJavaFinished(JavaCheckResult result) case JavaCheckResult::Validity::Errored: { // Error message displayed if java can't start - emit logLine(tr("Could not start java:"), MessageLevel::Error); + emit logLine(QString("Could not start java:"), MessageLevel::Error); emit logLines(result.errorLog.split('\n'), MessageLevel::Error); emit logLine("\nCheck your MultiMC Java settings.", MessageLevel::MultiMC); printSystemInfo(false, false); - emitFailed(tr("Could not start java!")); + emitFailed(QString("Could not start java!")); return; } case JavaCheckResult::Validity::ReturnedInvalidData: { - emit logLine(tr("Java checker returned some invalid data MultiMC doesn't understand:"), MessageLevel::Error); + emit logLine(QString("Java checker returned some invalid data MultiMC doesn't understand:"), MessageLevel::Error); emit logLines(result.outLog.split('\n'), MessageLevel::Warning); emit logLine("\nMinecraft might not start properly.", MessageLevel::MultiMC); printSystemInfo(false, false); @@ -117,7 +117,7 @@ void CheckJava::checkJavaFinished(JavaCheckResult result) void CheckJava::printJavaInfo(const QString& version, const QString& architecture, const QString & vendor) { - emit logLine(tr("Java is version %1, using %2-bit architecture, from %3.\n\n").arg(version, architecture, vendor), MessageLevel::MultiMC); + emit logLine(QString("Java is version %1, using %2-bit architecture, from %3.\n\n").arg(version, architecture, vendor), MessageLevel::MultiMC); printSystemInfo(true, architecture == "64"); } @@ -127,13 +127,13 @@ void CheckJava::printSystemInfo(bool javaIsKnown, bool javaIs64bit) auto system64 = Sys::isSystem64bit(); if(cpu64 != system64) { - emit logLine(tr("Your CPU architecture is not matching your system architecture. You might want to install a 64bit Operating System.\n\n"), MessageLevel::Error); + emit logLine(QString("Your CPU architecture is not matching your system architecture. You might want to install a 64bit Operating System.\n\n"), MessageLevel::Error); } if(javaIsKnown) { if(javaIs64bit != system64) { - emit logLine(tr("Your Java architecture is not matching your system architecture. You might want to install a 64bit Java version.\n\n"), MessageLevel::Error); + emit logLine(QString("Your Java architecture is not matching your system architecture. You might want to install a 64bit Java version.\n\n"), MessageLevel::Error); } } } From 5180536cc3a96ab2e6894fb1a8a5922dae6bcd21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Thu, 10 Sep 2020 23:10:17 +0200 Subject: [PATCH 6/6] NOISSUE add a way to use native system versions of OpenAL and GLFW If your OS comes with patched/fixed/newer versions of those, you can now check the checkboxes and stop using the old ones shipped by Mojang. --- api/logic/minecraft/MinecraftInstance.cpp | 5 ++ api/logic/minecraft/launch/ExtractNatives.cpp | 15 ++++- application/MultiMC.cpp | 4 ++ application/pages/global/MinecraftPage.cpp | 7 +++ application/pages/global/MinecraftPage.ui | 29 ++++++++- .../pages/instance/InstanceSettingsPage.cpp | 19 ++++++ .../pages/instance/InstanceSettingsPage.ui | 59 ++++++++++++++++++- 7 files changed, 132 insertions(+), 6 deletions(-) diff --git a/api/logic/minecraft/MinecraftInstance.cpp b/api/logic/minecraft/MinecraftInstance.cpp index db259395..5fda2f4b 100644 --- a/api/logic/minecraft/MinecraftInstance.cpp +++ b/api/logic/minecraft/MinecraftInstance.cpp @@ -100,6 +100,11 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO auto launchMethodOverride = m_settings->registerSetting("OverrideMCLaunchMethod", false); m_settings->registerOverride(globalSettings->getSetting("MCLaunchMethod"), launchMethodOverride); + // Native library workarounds + auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false); + m_settings->registerOverride(globalSettings->getSetting("UseNativeOpenAL"), nativeLibraryWorkaroundsOverride); + m_settings->registerOverride(globalSettings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride); + // DEPRECATED: Read what versions the user configuration thinks should be used m_settings->registerSetting({"IntendedVersion", "MinecraftVersion"}, ""); m_settings->registerSetting("LWJGLVersion", ""); diff --git a/api/logic/minecraft/launch/ExtractNatives.cpp b/api/logic/minecraft/launch/ExtractNatives.cpp index 253d13bc..31fd01f8 100644 --- a/api/logic/minecraft/launch/ExtractNatives.cpp +++ b/api/logic/minecraft/launch/ExtractNatives.cpp @@ -33,7 +33,7 @@ static QString replaceSuffix (QString target, const QString &suffix, const QStri return target + replacement; } -static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibHack) +static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibHack, bool nativeOpenAL, bool nativeGLFW) { QuaZip zip(source); if(!zip.open(QuaZip::mdUnzip)) @@ -48,6 +48,13 @@ static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibH do { QString name = zip.getCurrentFileName(); + auto lowercase = name.toLower(); + if (nativeGLFW && name.contains("glfw")) { + continue; + } + if (nativeOpenAL && name.contains("openal")) { + continue; + } if(applyJnilibHack) { name = replaceSuffix(name, ".jnilib", ".dylib"); @@ -76,12 +83,16 @@ void ExtractNatives::executeTask() emitSucceeded(); return; } + auto settings = minecraftInstance->settings(); + bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool(); + bool nativeGLFW = settings->get("UseNativeGLFW").toBool(); + auto outputPath = minecraftInstance->getNativePath(); auto javaVersion = minecraftInstance->getJavaVersion(); bool jniHackEnabled = javaVersion.major() >= 8; for(const auto &source: toExtract) { - if(!unzipNatives(source, outputPath, jniHackEnabled)) + if(!unzipNatives(source, outputPath, jniHackEnabled, nativeOpenAL, nativeGLFW)) { auto reason = tr("Couldn't extract native jar '%1' to destination '%2'").arg(source, outputPath); emit logLine(reason, MessageLevel::Fatal); diff --git a/application/MultiMC.cpp b/application/MultiMC.cpp index 8e2557d4..bca922ed 100644 --- a/application/MultiMC.cpp +++ b/application/MultiMC.cpp @@ -509,6 +509,10 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("LastHostname", ""); m_settings->registerSetting("JvmArgs", ""); + // Native library workarounds + m_settings->registerSetting("UseNativeOpenAL", false); + m_settings->registerSetting("UseNativeGLFW", false); + // Minecraft launch method m_settings->registerSetting("MCLaunchMethod", "LauncherPart"); diff --git a/application/pages/global/MinecraftPage.cpp b/application/pages/global/MinecraftPage.cpp index 1d7042ad..7a8e8a49 100644 --- a/application/pages/global/MinecraftPage.cpp +++ b/application/pages/global/MinecraftPage.cpp @@ -63,6 +63,10 @@ void MinecraftPage::applySettings() s->set("LaunchMaximized", ui->maximizedCheckBox->isChecked()); s->set("MinecraftWinWidth", ui->windowWidthSpinBox->value()); s->set("MinecraftWinHeight", ui->windowHeightSpinBox->value()); + + // Native library workarounds + s->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked()); + s->set("UseNativeGLFW", ui->useNativeGLFWCheck->isChecked()); } void MinecraftPage::loadSettings() @@ -73,4 +77,7 @@ void MinecraftPage::loadSettings() ui->maximizedCheckBox->setChecked(s->get("LaunchMaximized").toBool()); ui->windowWidthSpinBox->setValue(s->get("MinecraftWinWidth").toInt()); ui->windowHeightSpinBox->setValue(s->get("MinecraftWinHeight").toInt()); + + ui->useNativeOpenALCheck->setChecked(s->get("UseNativeOpenAL").toBool()); + ui->useNativeGLFWCheck->setChecked(s->get("UseNativeGLFW").toBool()); } diff --git a/application/pages/global/MinecraftPage.ui b/application/pages/global/MinecraftPage.ui index 9a18927a..c096c969 100644 --- a/application/pages/global/MinecraftPage.ui +++ b/application/pages/global/MinecraftPage.ui @@ -6,8 +6,8 @@ 0 0 - 545 - 195 + 936 + 1134 @@ -111,6 +111,29 @@ + + + + Native library workarounds + + + + + + Use system installation of GLFW + + + + + + + Use system installation of OpenAL + + + + + + @@ -135,6 +158,8 @@ maximizedCheckBox windowWidthSpinBox windowHeightSpinBox + useNativeGLFWCheck + useNativeOpenALCheck diff --git a/application/pages/instance/InstanceSettingsPage.cpp b/application/pages/instance/InstanceSettingsPage.cpp index b7b0a863..9a39b034 100644 --- a/application/pages/instance/InstanceSettingsPage.cpp +++ b/application/pages/instance/InstanceSettingsPage.cpp @@ -163,6 +163,20 @@ void InstanceSettingsPage::applySettings() m_settings->reset("WrapperCommand"); m_settings->reset("PostExitCommand"); } + + // Workarounds + bool workarounds = ui->nativeWorkaroundsGroupBox->isChecked(); + m_settings->set("OverrideNativeWorkarounds", workarounds); + if(workarounds) + { + m_settings->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked()); + m_settings->set("UseNativeGLFW", ui->useNativeGLFWCheck->isChecked()); + } + else + { + m_settings->reset("UseNativeOpenAL"); + m_settings->reset("UseNativeGLFW"); + } } void InstanceSettingsPage::loadSettings() @@ -219,6 +233,11 @@ void InstanceSettingsPage::loadSettings() m_settings->get("WrapperCommand").toString(), m_settings->get("PostExitCommand").toString() ); + + // Workarounds + ui->nativeWorkaroundsGroupBox->setChecked(m_settings->get("OverrideNativeWorkarounds").toBool()); + ui->useNativeGLFWCheck->setChecked(m_settings->get("UseNativeGLFW").toBool()); + ui->useNativeOpenALCheck->setChecked(m_settings->get("UseNativeOpenAL").toBool()); } void InstanceSettingsPage::on_javaDetectBtn_clicked() diff --git a/application/pages/instance/InstanceSettingsPage.ui b/application/pages/instance/InstanceSettingsPage.ui index d6de53ee..c91570c6 100644 --- a/application/pages/instance/InstanceSettingsPage.ui +++ b/application/pages/instance/InstanceSettingsPage.ui @@ -6,8 +6,8 @@ 0 0 - 738 - 804 + 691 + 581 @@ -364,6 +364,58 @@ + + + Workarounds + + + + + + true + + + Native libraries + + + true + + + false + + + + + + Use system installation of GLFW + + + + + + + Use system installation of OpenAL + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + @@ -398,6 +450,9 @@ showConsoleCheck autoCloseConsoleCheck showConsoleErrorCheck + nativeWorkaroundsGroupBox + useNativeGLFWCheck + useNativeOpenALCheck