diff --git a/launcher/Application.cpp b/launcher/Application.cpp index a098eab14..80dc7beab 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -815,6 +815,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("RPDownloadGeometry", ""); m_settings->registerSetting("TPDownloadGeometry", ""); m_settings->registerSetting("ShaderDownloadGeometry", ""); + m_settings->registerSetting("DataPackDownloadGeometry", ""); + + // data pack window + // in future, more pages may be added - so this name is chosen to avoid needing migration + m_settings->registerSetting("WorldManagementGeometry", ""); // HACK: This code feels so stupid is there a less stupid way of doing this? { diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 0ef358120..a7ccb809d 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -338,6 +338,8 @@ set(MINECRAFT_SOURCES minecraft/mod/ResourceFolderModel.cpp minecraft/mod/DataPack.h minecraft/mod/DataPack.cpp + minecraft/mod/DataPackFolderModel.h + minecraft/mod/DataPackFolderModel.cpp minecraft/mod/ResourcePack.h minecraft/mod/ResourcePack.cpp minecraft/mod/ResourcePackFolderModel.h @@ -359,8 +361,6 @@ set(MINECRAFT_SOURCES minecraft/mod/tasks/LocalResourceUpdateTask.cpp minecraft/mod/tasks/LocalDataPackParseTask.h minecraft/mod/tasks/LocalDataPackParseTask.cpp - minecraft/mod/tasks/LocalResourcePackParseTask.h - minecraft/mod/tasks/LocalResourcePackParseTask.cpp minecraft/mod/tasks/LocalTexturePackParseTask.h minecraft/mod/tasks/LocalTexturePackParseTask.cpp minecraft/mod/tasks/LocalShaderPackParseTask.h @@ -917,6 +917,8 @@ SET(LAUNCHER_SOURCES ui/pages/instance/VersionPage.h ui/pages/instance/ManagedPackPage.cpp ui/pages/instance/ManagedPackPage.h + ui/pages/instance/DataPackPage.h + ui/pages/instance/DataPackPage.cpp ui/pages/instance/TexturePackPage.h ui/pages/instance/TexturePackPage.cpp ui/pages/instance/ResourcePackPage.h @@ -987,6 +989,8 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/ShaderPackPage.cpp ui/pages/modplatform/ShaderPackModel.cpp + ui/pages/modplatform/DataPackPage.cpp + ui/pages/modplatform/DataPackModel.cpp ui/pages/modplatform/ModpackProviderBasePage.h diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index 076341b0b..2c2b0b580 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include "minecraft/MinecraftInstance.h" #include "ui/pages/BasePage.h" #include "ui/pages/BasePageProvider.h" @@ -36,6 +37,7 @@ class InstancePageProvider : protected QObject, public BasePageProvider { values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList())); values.append(new NilModFolderPage(onesix.get(), onesix->nilModList())); values.append(new ResourcePackPage(onesix.get(), onesix->resourcePackList())); + values.append(new GlobalDataPackPage(onesix.get())); values.append(new TexturePackPage(onesix.get(), onesix->texturePackList())); values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList())); values.append(new NotesPage(onesix.get())); diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index fafe7bd37..fb15cb120 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -250,6 +250,12 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerSetting("ExportOptionalFiles", true); m_settings->registerSetting("ExportRecommendedRAM"); + auto dataPacksEnabled = m_settings->registerSetting("GlobalDataPacksEnabled", false); + auto dataPacksPath = m_settings->registerSetting("GlobalDataPacksPath", ""); + + connect(dataPacksEnabled.get(), &Setting::SettingChanged, this, [this] { m_data_pack_list.reset(); }); + connect(dataPacksPath.get(), &Setting::SettingChanged, this, [this] { m_data_pack_list.reset(); }); + // Join server on launch, this does not have a global override m_settings->registerSetting("OverrideModDownloadLoaders", false); m_settings->registerSetting("ModDownloadLoaders", "[]"); @@ -396,6 +402,16 @@ QString MinecraftInstance::nilModsDir() const return FS::PathCombine(gameRoot(), "nilmods"); } +QString MinecraftInstance::dataPacksDir() +{ + QString relativePath = settings()->get("GlobalDataPacksPath").toString(); + + if (relativePath.isEmpty()) + relativePath = "datapacks"; + + return QDir(gameRoot()).filePath(relativePath); +} + QString MinecraftInstance::resourcePacksDir() const { return FS::PathCombine(gameRoot(), "resourcepacks"); @@ -1247,9 +1263,18 @@ std::shared_ptr MinecraftInstance::shaderPackList() return m_shader_pack_list; } +std::shared_ptr MinecraftInstance::dataPackList() +{ + if (!m_data_pack_list && settings()->get("GlobalDataPacksEnabled").toBool()) { + bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_data_pack_list.reset(new DataPackFolderModel(dataPacksDir(), this, isIndexed, true)); + } + return m_data_pack_list; +} + QList> MinecraftInstance::resourceLists() { - return { loaderModList(), coreModList(), nilModList(), resourcePackList(), texturePackList(), shaderPackList() }; + return { loaderModList(), coreModList(), nilModList(), resourcePackList(), texturePackList(), shaderPackList(), dataPackList() }; } std::shared_ptr MinecraftInstance::worldList() diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index 5790e0619..a37164169 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -36,6 +36,7 @@ #pragma once #include +#include #include #include #include "BaseInstance.h" @@ -80,6 +81,7 @@ class MinecraftInstance : public BaseInstance { QString modsRoot() const override; QString coreModsDir() const; QString nilModsDir() const; + QString dataPacksDir(); QString modsCacheLocation() const; QString libDir() const; QString worldDir() const; @@ -116,6 +118,7 @@ class MinecraftInstance : public BaseInstance { std::shared_ptr resourcePackList(); std::shared_ptr texturePackList(); std::shared_ptr shaderPackList(); + std::shared_ptr dataPackList(); QList> resourceLists(); std::shared_ptr worldList(); std::shared_ptr gameOptionsModel(); @@ -166,6 +169,7 @@ class MinecraftInstance : public BaseInstance { mutable std::shared_ptr m_resource_pack_list; mutable std::shared_ptr m_shader_pack_list; mutable std::shared_ptr m_texture_pack_list; + mutable std::shared_ptr m_data_pack_list; mutable std::shared_ptr m_world_list; mutable std::shared_ptr m_game_options; }; diff --git a/launcher/minecraft/mod/DataPack.cpp b/launcher/minecraft/mod/DataPack.cpp index 580d5c714..e089f2f6e 100644 --- a/launcher/minecraft/mod/DataPack.cpp +++ b/launcher/minecraft/mod/DataPack.cpp @@ -25,7 +25,9 @@ #include #include +#include "MTPixmapCache.h" #include "Version.h" +#include "minecraft/mod/tasks/LocalDataPackParseTask.h" // Values taken from: // https://minecraft.wiki/w/Pack_format#List_of_data_pack_formats @@ -93,6 +95,51 @@ void DataPack::setDescription(QString new_description) m_description = new_description; } +void DataPack::setImage(QImage new_image) const +{ + QMutexLocker locker(&m_data_lock); + + Q_ASSERT(!new_image.isNull()); + + if (m_pack_image_cache_key.key.isValid()) + PixmapCache::instance().remove(m_pack_image_cache_key.key); + + // scale the image to avoid flooding the pixmapcache + auto pixmap = + QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); + + m_pack_image_cache_key.key = PixmapCache::instance().insert(pixmap); + m_pack_image_cache_key.was_ever_used = true; + + // This can happen if the pixmap is too big to fit in the cache :c + if (!m_pack_image_cache_key.key.isValid()) { + qWarning() << "Could not insert a image cache entry! Ignoring it."; + m_pack_image_cache_key.was_ever_used = false; + } +} + +QPixmap DataPack::image(QSize size, Qt::AspectRatioMode mode) const +{ + QPixmap cached_image; + if (PixmapCache::instance().find(m_pack_image_cache_key.key, &cached_image)) { + if (size.isNull()) + return cached_image; + return cached_image.scaled(size, mode, Qt::SmoothTransformation); + } + + // No valid image we can get + if (!m_pack_image_cache_key.was_ever_used) { + return {}; + } else { + qDebug() << "Data Pack" << name() << "Had it's image evicted from the cache. reloading..."; + PixmapCache::markCacheMissByEviciton(); + } + + // Imaged got evicted from the cache. Re-process it and retry. + DataPackUtils::processPackPNG(this); + return image(size); +} + std::pair DataPack::compatibleVersions() const { if (!s_pack_format_versions.contains(m_pack_format)) { diff --git a/launcher/minecraft/mod/DataPack.h b/launcher/minecraft/mod/DataPack.h index 266c2061b..4b56cb9d8 100644 --- a/launcher/minecraft/mod/DataPack.h +++ b/launcher/minecraft/mod/DataPack.h @@ -24,6 +24,7 @@ #include "Resource.h" #include +#include class Version; @@ -41,17 +42,23 @@ class DataPack : public Resource { /** Gets the numerical ID of the pack format. */ [[nodiscard]] int packFormat() const { return m_pack_format; } /** Gets, respectively, the lower and upper versions supported by the set pack format. */ - [[nodiscard]] std::pair compatibleVersions() const; + [[nodiscard]] virtual std::pair compatibleVersions() const; /** Gets the description of the data pack. */ [[nodiscard]] QString description() const { return m_description; } + /** Gets the image of the data pack, converted to a QPixmap for drawing, and scaled to size. */ + [[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; + /** Thread-safe. */ void setPackFormat(int new_format_id); /** Thread-safe. */ void setDescription(QString new_description); + /** Thread-safe. */ + void setImage(QImage new_image) const; + bool valid() const override; [[nodiscard]] int compare(Resource const& other, SortType type) const override; @@ -70,4 +77,14 @@ class DataPack : public Resource { /** The data pack's description, as defined in the pack.mcmeta file. */ QString m_description; + + /** The data pack's image file cache key, for access in the QPixmapCache global instance. + * + * The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true), + * so as to tell whether a cache entry is inexistent or if it was just evicted from the cache. + */ + struct { + QPixmapCache::Key key; + bool was_ever_used = false; + } mutable m_pack_image_cache_key; }; diff --git a/launcher/minecraft/mod/DataPackFolderModel.cpp b/launcher/minecraft/mod/DataPackFolderModel.cpp new file mode 100644 index 000000000..45cf1271f --- /dev/null +++ b/launcher/minecraft/mod/DataPackFolderModel.cpp @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 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. + */ + +#include "DataPackFolderModel.h" +#include +#include + +#include +#include + +#include "Application.h" +#include "Version.h" + +#include "minecraft/mod/tasks/LocalDataPackParseTask.h" + +DataPackFolderModel::DataPackFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) + : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) +{ + m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified" }); + m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE }; + m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, + QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true }; +} + +QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const +{ + if (!validateIndex(index)) + return {}; + + int row = index.row(); + int column = index.column(); + + switch (role) { + case Qt::DisplayRole: + switch (column) { + case NameColumn: + return m_resources[row]->name(); + case PackFormatColumn: { + auto& resource = at(row); + auto pack_format = resource.packFormat(); + if (pack_format == 0) + return tr("Unrecognized"); + + auto version_bounds = resource.compatibleVersions(); + if (version_bounds.first.toString().isEmpty()) + return QString::number(pack_format); + + return QString("%1 (%2 - %3)") + .arg(QString::number(pack_format), version_bounds.first.toString(), version_bounds.second.toString()); + } + case DateColumn: + return m_resources[row]->dateTimeChanged(); + + default: + return {}; + } + case Qt::DecorationRole: { + if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) + return APPLICATION->getThemedIcon("status-yellow"); + if (column == ImageColumn) { + return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); + } + return {}; + } + case Qt::ToolTipRole: { + if (column == PackFormatColumn) { + //: The string being explained by this is in the format: ID (Lower version - Upper version) + return tr("The data pack format ID, as well as the Minecraft versions it was designed for."); + } + if (column == NameColumn) { + if (at(row).isSymLinkUnder(instDirPath())) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." + "\nCanonical Path: %1") + .arg(at(row).fileinfo().canonicalFilePath()); + ; + } + if (at(row).isMoreThanOneHardLink()) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); + } + } + return m_resources[row]->internal_id(); + } + case Qt::SizeHintRole: + if (column == ImageColumn) { + return QSize(32, 32); + } + return {}; + case Qt::CheckStateRole: + if (column == ActiveColumn) + return at(row).enabled() ? Qt::Checked : Qt::Unchecked; + else + return {}; + default: + return {}; + } +} + +QVariant DataPackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const +{ + switch (role) { + case Qt::DisplayRole: + switch (section) { + case ActiveColumn: + case NameColumn: + case PackFormatColumn: + case DateColumn: + case ImageColumn: + return columnNames().at(section); + default: + return {}; + } + + case Qt::ToolTipRole: + switch (section) { + case ActiveColumn: + return tr("Is the data pack enabled? (Only valid for ZIPs)"); + case NameColumn: + return tr("The name of the data pack."); + case PackFormatColumn: + //: The string being explained by this is in the format: ID (Lower version - Upper version) + return tr("The data pack format ID, as well as the Minecraft versions it was designed for."); + case DateColumn: + return tr("The date and time this data pack was last changed (or added)."); + default: + return {}; + } + case Qt::SizeHintRole: + if (section == ImageColumn) { + return QSize(64, 0); + } + return {}; + default: + return {}; + } +} + +int DataPackFolderModel::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : NUM_COLUMNS; +} + +Resource* DataPackFolderModel::createResource(const QFileInfo& file) +{ + return new DataPack(file); +} + +Task* DataPackFolderModel::createParseTask(Resource& resource) +{ + return new LocalDataPackParseTask(m_next_resolution_ticket, static_cast(&resource)); +} diff --git a/launcher/minecraft/mod/DataPackFolderModel.h b/launcher/minecraft/mod/DataPackFolderModel.h new file mode 100644 index 000000000..026ae8b76 --- /dev/null +++ b/launcher/minecraft/mod/DataPackFolderModel.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 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 "ResourceFolderModel.h" + +#include "DataPack.h" +#include "ResourcePack.h" + +class DataPackFolderModel : public ResourceFolderModel { + Q_OBJECT + public: + enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, NUM_COLUMNS }; + + explicit DataPackFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); + + virtual QString id() const override { return "datapacks"; } + + [[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + + [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + [[nodiscard]] int columnCount(const QModelIndex& parent) const override; + + [[nodiscard]] Resource* createResource(const QFileInfo& file) override; + [[nodiscard]] Task* createParseTask(Resource&) override; + + RESOURCE_HELPERS(DataPack) +}; diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp index cf8ec871c..ccf52e8e4 100644 --- a/launcher/minecraft/mod/ResourcePack.cpp +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -6,8 +6,6 @@ #include "MTPixmapCache.h" #include "Version.h" -#include "minecraft/mod/tasks/LocalResourcePackParseTask.h" - // Values taken from: // https://minecraft.wiki/w/Pack_format#List_of_resource_pack_formats static const QMap> s_pack_format_versions = { @@ -29,62 +27,6 @@ static const QMap> s_pack_format_versions = { { 34, { Version("24w21a"), Version("1.21") } } }; -void ResourcePack::setPackFormat(int new_format_id) -{ - QMutexLocker locker(&m_data_lock); - - if (!s_pack_format_versions.contains(new_format_id)) { - qWarning() << "Pack format '" << new_format_id << "' is not a recognized resource pack id!"; - } - - m_pack_format = new_format_id; -} - -void ResourcePack::setImage(QImage new_image) const -{ - QMutexLocker locker(&m_data_lock); - - Q_ASSERT(!new_image.isNull()); - - if (m_pack_image_cache_key.key.isValid()) - PixmapCache::instance().remove(m_pack_image_cache_key.key); - - // scale the image to avoid flooding the pixmapcache - auto pixmap = - QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); - - m_pack_image_cache_key.key = PixmapCache::instance().insert(pixmap); - m_pack_image_cache_key.was_ever_used = true; - - // This can happen if the pixmap is too big to fit in the cache :c - if (!m_pack_image_cache_key.key.isValid()) { - qWarning() << "Could not insert a image cache entry! Ignoring it."; - m_pack_image_cache_key.was_ever_used = false; - } -} - -QPixmap ResourcePack::image(QSize size, Qt::AspectRatioMode mode) const -{ - QPixmap cached_image; - if (PixmapCache::instance().find(m_pack_image_cache_key.key, &cached_image)) { - if (size.isNull()) - return cached_image; - return cached_image.scaled(size, mode, Qt::SmoothTransformation); - } - - // No valid image we can get - if (!m_pack_image_cache_key.was_ever_used) { - return {}; - } else { - qDebug() << "Resource Pack" << name() << "Had it's image evicted from the cache. reloading..."; - PixmapCache::markCacheMissByEviciton(); - } - - // Imaged got evicted from the cache. Re-process it and retry. - ResourcePackUtils::processPackPNG(this); - return image(size); -} - std::pair ResourcePack::compatibleVersions() const { if (!s_pack_format_versions.contains(m_pack_format)) { diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h index bb5aeecb5..f214bedf2 100644 --- a/launcher/minecraft/mod/ResourcePack.h +++ b/launcher/minecraft/mod/ResourcePack.h @@ -22,27 +22,7 @@ class ResourcePack : public DataPack { ResourcePack(QFileInfo file_info) : DataPack(file_info) {} /** Gets, respectively, the lower and upper versions supported by the set pack format. */ - [[nodiscard]] std::pair compatibleVersions() const; - - /** Gets the image of the resource pack, converted to a QPixmap for drawing, and scaled to size. */ - [[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; - - /** Thread-safe. */ - void setImage(QImage new_image) const; - - /** Thread-safe. */ - void setPackFormat(int new_format_id); + [[nodiscard]] std::pair compatibleVersions() const override; virtual QString directory() { return "/assets"; } - - protected: - /** The resource pack's image file cache key, for access in the QPixmapCache global instance. - * - * The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true), - * so as to tell whether a cache entry is inexistent or if it was just evicted from the cache. - */ - struct { - QPixmapCache::Key key; - bool was_ever_used = false; - } mutable m_pack_image_cache_key; }; diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp index 530ce44cc..c37a25c21 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp @@ -24,7 +24,6 @@ #include "FileSystem.h" #include "Json.h" #include "minecraft/mod/ResourcePack.h" -#include "minecraft/mod/tasks/LocalResourcePackParseTask.h" #include #include @@ -82,29 +81,27 @@ bool processFolder(DataPack* pack, ProcessingLevel level) if (level == ProcessingLevel::BasicInfoOnly) { return true; // only need basic info already checked } - if (auto rp = dynamic_cast(pack)) { - auto png_invalid = [&pack]() { - qWarning() << "Resource pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; - return true; // the png is optional - }; + auto png_invalid = [&pack]() { + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; + return true; // the png is optional + }; - QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png")); - if (image_file_info.exists() && image_file_info.isFile()) { - QFile pack_png_file(image_file_info.filePath()); - if (!pack_png_file.open(QIODevice::ReadOnly)) - return png_invalid(); // can't open pack.png file + QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png")); + if (image_file_info.exists() && image_file_info.isFile()) { + QFile pack_png_file(image_file_info.filePath()); + if (!pack_png_file.open(QIODevice::ReadOnly)) + return png_invalid(); // can't open pack.png file - auto data = pack_png_file.readAll(); + auto data = pack_png_file.readAll(); - bool pack_png_result = ResourcePackUtils::processPackPNG(rp, std::move(data)); + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); - pack_png_file.close(); - if (!pack_png_result) { - return png_invalid(); // pack.png invalid - } - } else { - return png_invalid(); // pack.png does not exists or is not a valid file. + pack_png_file.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid } + } else { + return png_invalid(); // pack.png does not exists or is not a valid file. } return true; // all tests passed @@ -153,32 +150,31 @@ bool processZIP(DataPack* pack, ProcessingLevel level) zip.close(); return true; // only need basic info already checked } - if (auto rp = dynamic_cast(pack)) { - auto png_invalid = [&pack]() { - qWarning() << "Resource pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; - return true; // the png is optional - }; - if (zip.setCurrentFile("pack.png")) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return png_invalid(); - } + auto png_invalid = [&pack]() { + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; + return true; // the png is optional + }; - auto data = file.readAll(); - - bool pack_png_result = ResourcePackUtils::processPackPNG(rp, std::move(data)); - - file.close(); + if (zip.setCurrentFile("pack.png")) { + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file in zip."; zip.close(); - if (!pack_png_result) { - return png_invalid(); // pack.png invalid - } - } else { - zip.close(); - return png_invalid(); // could not set pack.mcmeta as current file. + return png_invalid(); } + + auto data = file.readAll(); + + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); + + file.close(); + zip.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid + } + } else { + zip.close(); + return png_invalid(); // could not set pack.mcmeta as current file. } zip.close(); @@ -195,7 +191,7 @@ bool processMCMeta(DataPack* pack, QByteArray&& raw_data) auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); pack->setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0)); - pack->setDescription(ResourcePackUtils::processComponent(pack_obj.value("description"))); + pack->setDescription(DataPackUtils::processComponent(pack_obj.value("description"))); } catch (Json::JsonException& e) { qWarning() << "JsonException: " << e.what() << e.cause(); return false; @@ -203,12 +199,171 @@ bool processMCMeta(DataPack* pack, QByteArray&& raw_data) return true; } +QString buildStyle(const QJsonObject& obj) +{ + QStringList styles; + if (auto color = Json::ensureString(obj, "color"); !color.isEmpty()) { + styles << QString("color: %1;").arg(color); + } + if (obj.contains("bold")) { + QString weight = "normal"; + if (Json::ensureBoolean(obj, "bold", false)) { + weight = "bold"; + } + styles << QString("font-weight: %1;").arg(weight); + } + if (obj.contains("italic")) { + QString style = "normal"; + if (Json::ensureBoolean(obj, "italic", false)) { + style = "italic"; + } + styles << QString("font-style: %1;").arg(style); + } + + return styles.isEmpty() ? "" : QString("style=\"%1\"").arg(styles.join(" ")); +} + +QString processComponent(const QJsonArray& value, bool strikethrough, bool underline) +{ + QString result; + for (auto current : value) + result += processComponent(current, strikethrough, underline); + return result; +} + +QString processComponent(const QJsonObject& obj, bool strikethrough, bool underline) +{ + underline = Json::ensureBoolean(obj, "underlined", underline); + strikethrough = Json::ensureBoolean(obj, "strikethrough", strikethrough); + + QString result = Json::ensureString(obj, "text"); + if (underline) { + result = QString("%1").arg(result); + } + if (strikethrough) { + result = QString("%1").arg(result); + } + // the extra needs to be a array + result += processComponent(Json::ensureArray(obj, "extra"), strikethrough, underline); + if (auto style = buildStyle(obj); !style.isEmpty()) { + result = QString("%2").arg(style, result); + } + if (obj.contains("clickEvent")) { + auto click_event = Json::ensureObject(obj, "clickEvent"); + auto action = Json::ensureString(click_event, "action"); + auto value = Json::ensureString(click_event, "value"); + if (action == "open_url" && !value.isEmpty()) { + result = QString("%2").arg(value, result); + } + } + return result; +} + +QString processComponent(const QJsonValue& value, bool strikethrough, bool underline) +{ + if (value.isString()) { + return value.toString(); + } + if (value.isBool()) { + return value.toBool() ? "true" : "false"; + } + if (value.isDouble()) { + return QString::number(value.toDouble()); + } + if (value.isArray()) { + return processComponent(value.toArray(), strikethrough, underline); + } + if (value.isObject()) { + return processComponent(value.toObject(), strikethrough, underline); + } + qWarning() << "Invalid component type!"; + return {}; +} + +bool processPackPNG(const DataPack* pack, QByteArray&& raw_data) +{ + auto img = QImage::fromData(raw_data); + if (!img.isNull()) { + pack->setImage(img); + } else { + qWarning() << "Failed to parse pack.png."; + return false; + } + return true; +} + +bool processPackPNG(const DataPack* pack) +{ + auto png_invalid = [&pack]() { + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; + return false; + }; + + switch (pack->type()) { + case ResourceType::FOLDER: { + QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png")); + if (image_file_info.exists() && image_file_info.isFile()) { + QFile pack_png_file(image_file_info.filePath()); + if (!pack_png_file.open(QIODevice::ReadOnly)) + return png_invalid(); // can't open pack.png file + + auto data = pack_png_file.readAll(); + + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); + + pack_png_file.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid + } + } else { + return png_invalid(); // pack.png does not exists or is not a valid file. + } + return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 + } + case ResourceType::ZIPFILE: { + QuaZip zip(pack->fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + QuaZipFile file(&zip); + if (zip.setCurrentFile("pack.png")) { + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file in zip."; + zip.close(); + return png_invalid(); + } + + auto data = file.readAll(); + + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); + + file.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid + } + } else { + return png_invalid(); // could not set pack.mcmeta as current file. + } + return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 + } + default: + qWarning() << "Invalid type for data pack parse task!"; + return false; + } +} + bool validate(QFileInfo file) { DataPack dp{ file }; return DataPackUtils::process(&dp, ProcessingLevel::BasicInfoOnly) && dp.valid(); } +bool validateResourcePack(QFileInfo file) +{ + ResourcePack rp{ file }; + return DataPackUtils::process(&rp, ProcessingLevel::BasicInfoOnly) && rp.valid(); +} + } // namespace DataPackUtils LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack* dp) : Task(false), m_token(token), m_data_pack(dp) {} @@ -221,4 +376,4 @@ void LocalDataPackParseTask::executeTask() } emitSucceeded(); -} +} \ No newline at end of file diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h index 7355783df..57591a0f4 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h @@ -39,9 +39,19 @@ bool processFolder(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full bool processMCMeta(DataPack* pack, QByteArray&& raw_data); +QString processComponent(const QJsonValue& value, bool strikethrough = false, bool underline = false); + +bool processPackPNG(const DataPack* pack, QByteArray&& raw_data); + +/// processes ONLY the pack.png (rest of the pack may be invalid) +bool processPackPNG(const DataPack* pack); + /** Checks whether a file is valid as a data pack or not. */ bool validate(QFileInfo file); +/** Checks whether a file is valid as a resource pack or not. */ +bool validateResourcePack(QFileInfo file); + } // namespace DataPackUtils class LocalDataPackParseTask : public Task { @@ -57,4 +67,4 @@ class LocalDataPackParseTask : public Task { int m_token; DataPack* m_data_pack; -}; +}; \ No newline at end of file diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp deleted file mode 100644 index db4b2e55c..000000000 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp +++ /dev/null @@ -1,192 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2022 flowln - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "LocalResourcePackParseTask.h" - -#include "FileSystem.h" -#include "Json.h" -#include "minecraft/mod/tasks/LocalDataPackParseTask.h" - -#include -#include -#include - -#include - -namespace ResourcePackUtils { - -QString buildStyle(const QJsonObject& obj) -{ - QStringList styles; - if (auto color = Json::ensureString(obj, "color"); !color.isEmpty()) { - styles << QString("color: %1;").arg(color); - } - if (obj.contains("bold")) { - QString weight = "normal"; - if (Json::ensureBoolean(obj, "bold", false)) { - weight = "bold"; - } - styles << QString("font-weight: %1;").arg(weight); - } - if (obj.contains("italic")) { - QString style = "normal"; - if (Json::ensureBoolean(obj, "italic", false)) { - style = "italic"; - } - styles << QString("font-style: %1;").arg(style); - } - - return styles.isEmpty() ? "" : QString("style=\"%1\"").arg(styles.join(" ")); -} - -QString processComponent(const QJsonArray& value, bool strikethrough, bool underline) -{ - QString result; - for (auto current : value) - result += processComponent(current, strikethrough, underline); - return result; -} - -QString processComponent(const QJsonObject& obj, bool strikethrough, bool underline) -{ - underline = Json::ensureBoolean(obj, "underlined", underline); - strikethrough = Json::ensureBoolean(obj, "strikethrough", strikethrough); - - QString result = Json::ensureString(obj, "text"); - if (underline) { - result = QString("%1").arg(result); - } - if (strikethrough) { - result = QString("%1").arg(result); - } - // the extra needs to be a array - result += processComponent(Json::ensureArray(obj, "extra"), strikethrough, underline); - if (auto style = buildStyle(obj); !style.isEmpty()) { - result = QString("%2").arg(style, result); - } - if (obj.contains("clickEvent")) { - auto click_event = Json::ensureObject(obj, "clickEvent"); - auto action = Json::ensureString(click_event, "action"); - auto value = Json::ensureString(click_event, "value"); - if (action == "open_url" && !value.isEmpty()) { - result = QString("%2").arg(value, result); - } - } - return result; -} - -QString processComponent(const QJsonValue& value, bool strikethrough, bool underline) -{ - if (value.isString()) { - return value.toString(); - } - if (value.isBool()) { - return value.toBool() ? "true" : "false"; - } - if (value.isDouble()) { - return QString::number(value.toDouble()); - } - if (value.isArray()) { - return processComponent(value.toArray(), strikethrough, underline); - } - if (value.isObject()) { - return processComponent(value.toObject(), strikethrough, underline); - } - qWarning() << "Invalid component type!"; - return {}; -} - -bool processPackPNG(const ResourcePack* pack, QByteArray&& raw_data) -{ - auto img = QImage::fromData(raw_data); - if (!img.isNull()) { - pack->setImage(img); - } else { - qWarning() << "Failed to parse pack.png."; - return false; - } - return true; -} - -bool processPackPNG(const ResourcePack* pack) -{ - auto png_invalid = [&pack]() { - qWarning() << "Resource pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; - return false; - }; - - switch (pack->type()) { - case ResourceType::FOLDER: { - QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png")); - if (image_file_info.exists() && image_file_info.isFile()) { - QFile pack_png_file(image_file_info.filePath()); - if (!pack_png_file.open(QIODevice::ReadOnly)) - return png_invalid(); // can't open pack.png file - - auto data = pack_png_file.readAll(); - - bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); - - pack_png_file.close(); - if (!pack_png_result) { - return png_invalid(); // pack.png invalid - } - } else { - return png_invalid(); // pack.png does not exists or is not a valid file. - } - return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 - } - case ResourceType::ZIPFILE: { - QuaZip zip(pack->fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return false; // can't open zip file - - QuaZipFile file(&zip); - if (zip.setCurrentFile("pack.png")) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return png_invalid(); - } - - auto data = file.readAll(); - - bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); - - file.close(); - if (!pack_png_result) { - return png_invalid(); // pack.png invalid - } - } else { - return png_invalid(); // could not set pack.mcmeta as current file. - } - return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 - } - default: - qWarning() << "Invalid type for resource pack parse task!"; - return false; - } -} - -bool validate(QFileInfo file) -{ - ResourcePack rp{ file }; - return DataPackUtils::process(&rp, DataPackUtils::ProcessingLevel::BasicInfoOnly) && rp.valid(); -} - -} // namespace ResourcePackUtils diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h deleted file mode 100644 index 6b4378aa6..000000000 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2022 flowln - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include -#include - -#include "minecraft/mod/ResourcePack.h" - -namespace ResourcePackUtils { - -QString processComponent(const QJsonValue& value, bool strikethrough = false, bool underline = false); -bool processPackPNG(const ResourcePack* pack, QByteArray&& raw_data); - -/// processes ONLY the pack.png (rest of the pack may be invalid) -bool processPackPNG(const ResourcePack* pack); - -/** Checks whether a file is valid as a resource pack or not. */ -bool validate(QFileInfo file); -} // namespace ResourcePackUtils diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp index d5a090832..e309b2105 100644 --- a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp @@ -25,7 +25,6 @@ #include "LocalDataPackParseTask.h" #include "LocalModParseTask.h" -#include "LocalResourcePackParseTask.h" #include "LocalShaderPackParseTask.h" #include "LocalTexturePackParseTask.h" #include "LocalWorldSaveParseTask.h" @@ -46,7 +45,7 @@ PackedResourceType identify(QFileInfo file) // mods can contain resource and data packs so they must be tested first qDebug() << file.fileName() << "is a mod"; return PackedResourceType::Mod; - } else if (ResourcePackUtils::validate(file)) { + } else if (DataPackUtils::validateResourcePack(file)) { qDebug() << file.fileName() << "is a resource pack"; return PackedResourceType::ResourcePack; } else if (TexturePackUtils::validate(file)) { diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index c3ecccf8e..380ff660f 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -122,11 +122,13 @@ auto getModLoaderAsString(ModLoaderType type) -> const QString case Cauldron: return "cauldron"; case LiteLoader: - return "liteloader"; + return "liteloader"; case Fabric: return "fabric"; case Quilt: return "quilt"; + case DataPack: + return "datapack"; default: break; } diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 523358e4e..ad2503ea7 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -29,13 +29,21 @@ class QIODevice; namespace ModPlatform { -enum ModLoaderType { NeoForge = 1 << 0, Forge = 1 << 1, Cauldron = 1 << 2, LiteLoader = 1 << 3, Fabric = 1 << 4, Quilt = 1 << 5 }; +enum ModLoaderType { + NeoForge = 1 << 0, + Forge = 1 << 1, + Cauldron = 1 << 2, + LiteLoader = 1 << 3, + Fabric = 1 << 4, + Quilt = 1 << 5, + DataPack = 1 << 6 +}; Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) QList modLoaderTypesToList(ModLoaderTypes flags); enum class ResourceProvider { MODRINTH, FLAME }; -enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK, MODPACK }; +enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK, MODPACK, DATA_PACK }; enum class DependencyType { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN }; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index f85a08eb1..71282f36a 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -48,6 +48,8 @@ class FlameAPI : public NetworkResourceAPI { return 6552; case ModPlatform::ResourceType::MODPACK: return 4471; + case ModPlatform::ResourceType::DATA_PACK: + return 6945; } } @@ -67,6 +69,8 @@ class FlameAPI : public NetworkResourceAPI { return 5; case ModPlatform::NeoForge: return 6; + case ModPlatform::DataPack: + break; // not supported } return 0; } @@ -96,8 +100,12 @@ class FlameAPI : public NetworkResourceAPI { if (args.sorting.has_value()) get_arguments.append(QString("sortField=%1").arg(args.sorting.value().index)); get_arguments.append("sortOrder=desc"); - if (args.loaders.has_value() && args.loaders.value() != 0) - get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(args.loaders.value()))); + if (args.loaders.has_value()) { + ModPlatform::ModLoaderTypes loaders = args.loaders.value(); + loaders &= ~ModPlatform::ModLoaderType::DataPack; + if (loaders != 0) + get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(loaders))); + } if (args.categoryIds.has_value() && !args.categoryIds->empty()) get_arguments.append(QString("categoryIds=[%1]").arg(args.categoryIds->join(","))); @@ -115,7 +123,7 @@ class FlameAPI : public NetworkResourceAPI { if (args.mcVersions.has_value()) url += QString("&gameVersion=%1").arg(args.mcVersions.value().front().toString()); - if (args.loaders.has_value() && ModPlatform::hasSingleModLoaderSelected(args.loaders.value())) { + if (args.loaders.has_value() && args.loaders.value() != ModPlatform::ModLoaderType::DataPack && ModPlatform::hasSingleModLoaderSelected(args.loaders.value())) { int mappedModLoader = getMappedModLoader(static_cast(static_cast(args.loaders.value()))); url += QString("&modLoaderType=%1").arg(mappedModLoader); } diff --git a/launcher/modplatform/import_ftb/PackInstallTask.cpp b/launcher/modplatform/import_ftb/PackInstallTask.cpp index 8046300e1..7cb8b6ebc 100644 --- a/launcher/modplatform/import_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/import_ftb/PackInstallTask.cpp @@ -89,6 +89,8 @@ void PackInstallTask::copySettings() break; case ModPlatform::LiteLoader: break; + case ModPlatform::DataPack: + break; } components->saveNow(); diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 3e9c4befd..17b23723b 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -42,8 +42,8 @@ class ModrinthAPI : public NetworkResourceAPI { static auto getModLoaderStrings(const ModPlatform::ModLoaderTypes types) -> const QStringList { QStringList l; - for (auto loader : - { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader }) { + for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader, + ModPlatform::DataPack }) { if (types & loader) { l << getModLoaderAsString(loader); } @@ -108,6 +108,8 @@ class ModrinthAPI : public NetworkResourceAPI { return "resourcepack"; case ModPlatform::ResourceType::SHADER_PACK: return "shader"; + case ModPlatform::ResourceType::DATA_PACK: + return "datapack"; case ModPlatform::ResourceType::MODPACK: return "modpack"; default: @@ -197,7 +199,8 @@ class ModrinthAPI : public NetworkResourceAPI { static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool { - return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt | ModPlatform::LiteLoader); + return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt | ModPlatform::LiteLoader | + ModPlatform::DataPack); } [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override diff --git a/launcher/resources/breeze_dark/breeze_dark.qrc b/launcher/resources/breeze_dark/breeze_dark.qrc index 8fdfdeeb2..585f2c60a 100644 --- a/launcher/resources/breeze_dark/breeze_dark.qrc +++ b/launcher/resources/breeze_dark/breeze_dark.qrc @@ -9,7 +9,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/discord.svg scalable/externaltools.svg scalable/help.svg diff --git a/launcher/resources/breeze_dark/scalable/environment-variables.svg b/launcher/resources/breeze_dark/scalable/datapacks.svg similarity index 100% rename from launcher/resources/breeze_dark/scalable/environment-variables.svg rename to launcher/resources/breeze_dark/scalable/datapacks.svg diff --git a/launcher/resources/breeze_light/breeze_light.qrc b/launcher/resources/breeze_light/breeze_light.qrc index 6a7120fed..2b0adba10 100644 --- a/launcher/resources/breeze_light/breeze_light.qrc +++ b/launcher/resources/breeze_light/breeze_light.qrc @@ -9,7 +9,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/discord.svg scalable/externaltools.svg scalable/help.svg diff --git a/launcher/resources/breeze_light/scalable/environment-variables.svg b/launcher/resources/breeze_light/scalable/datapacks.svg similarity index 100% rename from launcher/resources/breeze_light/scalable/environment-variables.svg rename to launcher/resources/breeze_light/scalable/datapacks.svg diff --git a/launcher/resources/flat/flat.qrc b/launcher/resources/flat/flat.qrc index b546faec0..2cc9f46f5 100644 --- a/launcher/resources/flat/flat.qrc +++ b/launcher/resources/flat/flat.qrc @@ -11,7 +11,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/discord.svg scalable/externaltools.svg scalable/help.svg diff --git a/launcher/resources/flat/scalable/environment-variables.svg b/launcher/resources/flat/scalable/datapacks.svg similarity index 100% rename from launcher/resources/flat/scalable/environment-variables.svg rename to launcher/resources/flat/scalable/datapacks.svg diff --git a/launcher/resources/flat_white/flat_white.qrc b/launcher/resources/flat_white/flat_white.qrc index c59bb2ba7..d873e40d6 100644 --- a/launcher/resources/flat_white/flat_white.qrc +++ b/launcher/resources/flat_white/flat_white.qrc @@ -11,7 +11,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/discord.svg scalable/externaltools.svg scalable/help.svg diff --git a/launcher/resources/flat_white/scalable/environment-variables.svg b/launcher/resources/flat_white/scalable/datapacks.svg similarity index 100% rename from launcher/resources/flat_white/scalable/environment-variables.svg rename to launcher/resources/flat_white/scalable/datapacks.svg diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index 80b472917..116dd73cd 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -74,7 +74,7 @@ scalable/screenshots.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg 16x16/cat.png diff --git a/launcher/resources/multimc/scalable/environment-variables.svg b/launcher/resources/multimc/scalable/datapacks.svg similarity index 100% rename from launcher/resources/multimc/scalable/environment-variables.svg rename to launcher/resources/multimc/scalable/datapacks.svg diff --git a/launcher/resources/pe_blue/pe_blue.qrc b/launcher/resources/pe_blue/pe_blue.qrc index 314fde1a8..1e6b5d3cc 100644 --- a/launcher/resources/pe_blue/pe_blue.qrc +++ b/launcher/resources/pe_blue/pe_blue.qrc @@ -10,7 +10,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/launcher/resources/pe_blue/scalable/environment-variables.svg b/launcher/resources/pe_blue/scalable/datapacks.svg similarity index 100% rename from launcher/resources/pe_blue/scalable/environment-variables.svg rename to launcher/resources/pe_blue/scalable/datapacks.svg diff --git a/launcher/resources/pe_colored/pe_colored.qrc b/launcher/resources/pe_colored/pe_colored.qrc index 484342534..71b38024a 100644 --- a/launcher/resources/pe_colored/pe_colored.qrc +++ b/launcher/resources/pe_colored/pe_colored.qrc @@ -10,7 +10,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/launcher/resources/pe_colored/scalable/environment-variables.svg b/launcher/resources/pe_colored/scalable/datapacks.svg similarity index 100% rename from launcher/resources/pe_colored/scalable/environment-variables.svg rename to launcher/resources/pe_colored/scalable/datapacks.svg diff --git a/launcher/resources/pe_dark/pe_dark.qrc b/launcher/resources/pe_dark/pe_dark.qrc index 06ba97df7..9ccfece1e 100644 --- a/launcher/resources/pe_dark/pe_dark.qrc +++ b/launcher/resources/pe_dark/pe_dark.qrc @@ -10,7 +10,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/launcher/resources/pe_dark/scalable/environment-variables.svg b/launcher/resources/pe_dark/scalable/datapacks.svg similarity index 100% rename from launcher/resources/pe_dark/scalable/environment-variables.svg rename to launcher/resources/pe_dark/scalable/datapacks.svg diff --git a/launcher/resources/pe_light/pe_light.qrc b/launcher/resources/pe_light/pe_light.qrc index a1081bcab..a6d49b803 100644 --- a/launcher/resources/pe_light/pe_light.qrc +++ b/launcher/resources/pe_light/pe_light.qrc @@ -10,7 +10,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/launcher/resources/pe_light/scalable/environment-variables.svg b/launcher/resources/pe_light/scalable/datapacks.svg similarity index 100% rename from launcher/resources/pe_light/scalable/environment-variables.svg rename to launcher/resources/pe_light/scalable/datapacks.svg diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index 718c0fe2c..fe9ee7bdb 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -57,7 +57,7 @@ ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::share { setObjectName(QStringLiteral("ResourceDownloadDialog")); - resize(std::max(0.5 * parent->width(), 400.0), std::max(0.75 * parent->height(), 400.0)); + resize(static_cast(std::max(0.5 * parent->width(), 400.0)), static_cast(std::max(0.75 * parent->height(), 400.0))); setWindowIcon(APPLICATION->getThemedIcon("new")); @@ -396,4 +396,28 @@ void ResourceDownloadDialog::setResourceMetadata(const std::shared_ptropenProject(meta->project_id); } + +DataPackDownloadDialog::DataPackDownloadDialog(QWidget* parent, + const std::shared_ptr& data_packs, + BaseInstance* instance) + : ResourceDownloadDialog(parent, data_packs), m_instance(instance) +{ + setWindowTitle(dialogTitle()); + + initializeContainer(); + connectButtons(); + + if (!geometrySaveKey().isEmpty()) + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray())); +} + +QList DataPackDownloadDialog::getPages() +{ + QList pages; + pages.append(ModrinthDataPackPage::create(this, *m_instance)); + if (APPLICATION->capabilities() & Application::SupportsFlame) + pages.append(FlameDataPackPage::create(this, *m_instance)); + return pages; +} + } // namespace ResourceDownload diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h index 0c3e314bc..181086d82 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.h +++ b/launcher/ui/dialogs/ResourceDownloadDialog.h @@ -25,6 +25,7 @@ #include #include "QObjectPtr.h" +#include "minecraft/mod/DataPackFolderModel.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "modplatform/ModIndex.h" #include "ui/pages/BasePageProvider.h" @@ -168,4 +169,21 @@ class ShaderPackDownloadDialog final : public ResourceDownloadDialog { BaseInstance* m_instance; }; +class DataPackDownloadDialog final : public ResourceDownloadDialog { + Q_OBJECT + + public: + explicit DataPackDownloadDialog(QWidget* parent, const std::shared_ptr& data_packs, BaseInstance* instance); + ~DataPackDownloadDialog() override = default; + + //: String that gets appended to the data pack download dialog title ("Download " + resourcesString()) + [[nodiscard]] QString resourcesString() const override { return tr("data packs"); } + [[nodiscard]] QString geometrySaveKey() const override { return "DataPackDownloadGeometry"; } + + QList getPages() override; + + private: + BaseInstance* m_instance; +}; + } // namespace ResourceDownload diff --git a/launcher/ui/dialogs/ResourceUpdateDialog.cpp b/launcher/ui/dialogs/ResourceUpdateDialog.cpp index aa4bbd294..774d3a339 100644 --- a/launcher/ui/dialogs/ResourceUpdateDialog.cpp +++ b/launcher/ui/dialogs/ResourceUpdateDialog.cpp @@ -32,17 +32,12 @@ static std::list mcVersions(BaseInstance* inst) return { static_cast(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() }; } -static QList mcLoadersList(BaseInstance* inst) -{ - return static_cast(inst)->getPackProfile()->getModLoadersList(); -} - ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent, BaseInstance* instance, const std::shared_ptr resource_model, QList& search_for, bool include_deps, - bool filter_loaders) + QList loadersList) : ReviewMessageBox(parent, tr("Confirm resources to update"), "") , m_parent(parent) , m_resource_model(resource_model) @@ -50,7 +45,7 @@ ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent, , m_second_try_metadata(new ConcurrentTask("Second Metadata Search", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())) , m_instance(instance) , m_include_deps(include_deps) - , m_filter_loaders(filter_loaders) + , m_loadersList(std::move(loadersList)) { ReviewMessageBox::setGeometry(0, 0, 800, 600); @@ -89,12 +84,10 @@ void ResourceUpdateDialog::checkCandidates() } auto versions = mcVersions(m_instance); - auto loadersList = m_filter_loaders ? mcLoadersList(m_instance) : QList(); - SequentialTask check_task(tr("Checking for updates")); if (!m_modrinth_to_update.empty()) { - m_modrinth_check_task.reset(new ModrinthCheckUpdate(m_modrinth_to_update, versions, loadersList, m_resource_model)); + m_modrinth_check_task.reset(new ModrinthCheckUpdate(m_modrinth_to_update, versions, m_loadersList, m_resource_model)); connect(m_modrinth_check_task.get(), &CheckUpdateTask::checkFailed, this, [this](Resource* resource, QString reason, QUrl recover_url) { m_failed_check_update.append({ resource, reason, recover_url }); @@ -103,7 +96,7 @@ void ResourceUpdateDialog::checkCandidates() } if (!m_flame_to_update.empty()) { - m_flame_check_task.reset(new FlameCheckUpdate(m_flame_to_update, versions, loadersList, m_resource_model)); + m_flame_check_task.reset(new FlameCheckUpdate(m_flame_to_update, versions, m_loadersList, m_resource_model)); connect(m_flame_check_task.get(), &CheckUpdateTask::checkFailed, this, [this](Resource* resource, QString reason, QUrl recover_url) { m_failed_check_update.append({ resource, reason, recover_url }); diff --git a/launcher/ui/dialogs/ResourceUpdateDialog.h b/launcher/ui/dialogs/ResourceUpdateDialog.h index de1d845d2..aef11c90f 100644 --- a/launcher/ui/dialogs/ResourceUpdateDialog.h +++ b/launcher/ui/dialogs/ResourceUpdateDialog.h @@ -21,7 +21,7 @@ class ResourceUpdateDialog final : public ReviewMessageBox { std::shared_ptr resource_model, QList& search_for, bool include_deps, - bool filter_loaders); + QList loadersList = {}); void checkCandidates(); @@ -64,5 +64,5 @@ class ResourceUpdateDialog final : public ReviewMessageBox { bool m_no_updates = false; bool m_aborted = false; bool m_include_deps = false; - bool m_filter_loaders = false; + QList m_loadersList; }; diff --git a/launcher/ui/pages/instance/DataPackPage.cpp b/launcher/ui/pages/instance/DataPackPage.cpp new file mode 100644 index 000000000..a56cc9b79 --- /dev/null +++ b/launcher/ui/pages/instance/DataPackPage.cpp @@ -0,0 +1,360 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "DataPackPage.h" +#include "minecraft/PackProfile.h" +#include "ui_ExternalResourcesPage.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" +#include "ui/dialogs/ResourceDownloadDialog.h" +#include "ui/dialogs/ResourceUpdateDialog.h" + +DataPackPage::DataPackPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent) + : ExternalResourcesPage(instance, model, parent), m_model(model) +{ + ui->actionDownloadItem->setText(tr("Download Packs")); + ui->actionDownloadItem->setToolTip(tr("Download data packs from online mod platforms")); + ui->actionDownloadItem->setEnabled(true); + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); + + connect(ui->actionDownloadItem, &QAction::triggered, this, &DataPackPage::downloadDataPacks); + + ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected data packs (all data packs if none are selected)")); + connect(ui->actionUpdateItem, &QAction::triggered, this, &DataPackPage::updateDataPacks); + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); + + auto updateMenu = new QMenu(this); + + auto update = updateMenu->addAction(ui->actionUpdateItem->text()); + connect(update, &QAction::triggered, this, &DataPackPage::updateDataPacks); + + updateMenu->addAction(ui->actionResetItemMetadata); + connect(ui->actionResetItemMetadata, &QAction::triggered, this, &DataPackPage::deleteDataPackMetadata); + + ui->actionUpdateItem->setMenu(updateMenu); + + ui->actionChangeVersion->setToolTip(tr("Change a data pack's version.")); + connect(ui->actionChangeVersion, &QAction::triggered, this, &DataPackPage::changeDataPackVersion); + ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, ui->actionChangeVersion); +} + +void DataPackPage::updateFrame(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) +{ + auto sourceCurrent = m_filterModel->mapToSource(current); + int row = sourceCurrent.row(); + auto& dp = m_model->at(row); + ui->frame->updateWithDataPack(dp); +} + +void DataPackPage::downloadDataPacks() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + auto profile = static_cast(m_instance)->getPackProfile(); + + m_downloadDialog = new ResourceDownload::DataPackDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &DataPackPage::downloadDialogFinished); + + m_downloadDialog->open(); +} + +void DataPackPage::downloadDialogFinished(int result) +{ + if (result) { + auto tasks = new ConcurrentTask(tr("Download Data Packs"), APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::aborted, [this, tasks]() { + CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + + tasks->deleteLater(); + }); + + if (m_downloadDialog) { + for (auto& task : m_downloadDialog->getTasks()) { + tasks->addTask(task); + } + } else { + qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } + if (m_downloadDialog) + m_downloadDialog->deleteLater(); +} + +void DataPackPage::updateDataPacks() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + auto profile = static_cast(m_instance)->getPackProfile(); + if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { + QMessageBox::critical(this, tr("Error"), tr("Data pack updates are unavailable when metadata is disabled!")); + return; + } + if (m_instance != nullptr && m_instance->isRunning()) { + auto response = + CustomMessageBox::selectable(this, tr("Confirm Update"), + tr("Updating data packs while the game is running may cause pack duplication and game crashes.\n" + "The old files may not be deleted as they are in use.\n" + "Are you sure you want to do this?"), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + + auto mods_list = m_model->selectedResources(selection); + bool use_all = mods_list.empty(); + if (use_all) + mods_list = m_model->allResources(); + + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false, { ModPlatform::ModLoaderType::DataPack }); + update_dialog.checkCandidates(); + + if (update_dialog.aborted()) { + CustomMessageBox::selectable(this, tr("Aborted"), tr("The data pack updater was aborted!"), QMessageBox::Warning)->show(); + return; + } + if (update_dialog.noUpdates()) { + QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) }; + if (mods_list.size() > 1) { + if (use_all) { + message = tr("All data packs are up-to-date! :)"); + } else { + message = tr("All selected data packs are up-to-date! :)"); + } + } + CustomMessageBox::selectable(this, tr("Update checker"), message)->exec(); + return; + } + + if (update_dialog.exec()) { + auto tasks = new ConcurrentTask("Download Data Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::aborted, [this, tasks]() { + CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } + tasks->deleteLater(); + }); + + for (auto task : update_dialog.getTasks()) { + tasks->addTask(task); + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } +} + +void DataPackPage::deleteDataPackMetadata() +{ + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + auto selectionCount = m_model->selectedDataPacks(selection).length(); + if (selectionCount == 0) + return; + if (selectionCount > 1) { + auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), + tr("You are about to remove the metadata for %1 data packs.\n" + "Are you sure?") + .arg(selectionCount), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + + m_model->deleteMetadata(selection); +} + +void DataPackPage::changeDataPackVersion() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { + QMessageBox::critical(this, tr("Error"), tr("Data pack updates are unavailable when metadata is disabled!")); + return; + } + + const QModelIndexList rows = ui->treeView->selectionModel()->selectedRows(); + + if (rows.count() != 1) + return; + + Resource& resource = m_model->at(m_filterModel->mapToSource(rows[0]).row()); + + if (resource.metadata() == nullptr) + return; + + ResourceDownload::DataPackDownloadDialog mdownload(this, m_model, m_instance); + mdownload.setResourceMetadata(resource.metadata()); + if (mdownload.exec()) { + auto tasks = new ConcurrentTask("Download Data Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::aborted, [this, tasks]() { + CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + + tasks->deleteLater(); + }); + + for (auto& task : mdownload.getTasks()) { + tasks->addTask(task); + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } +} + +GlobalDataPackPage::GlobalDataPackPage(MinecraftInstance* instance, QWidget* parent) : QWidget(parent), m_instance(instance) +{ + auto layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + setLayout(layout); + + connect(instance->settings()->getSetting("GlobalDataPacksEnabled").get(), &Setting::SettingChanged, this, [this] { + updateContent(); + if (m_container != nullptr) + m_container->refreshContainer(); + }); + + connect(instance->settings()->getSetting("GlobalDataPacksPath").get(), &Setting::SettingChanged, this, + &GlobalDataPackPage::updateContent); +} + +QString GlobalDataPackPage::displayName() const +{ + if (m_underlyingPage == nullptr) + return {}; + + return m_underlyingPage->displayName(); +} + +QIcon GlobalDataPackPage::icon() const +{ + if (m_underlyingPage == nullptr) + return {}; + + return m_underlyingPage->icon(); +} + +QString GlobalDataPackPage::helpPage() const +{ + if (m_underlyingPage == nullptr) + return {}; + + return m_underlyingPage->helpPage(); +} + +bool GlobalDataPackPage::shouldDisplay() const +{ + return m_instance->settings()->get("GlobalDataPacksEnabled").toBool(); +} + +bool GlobalDataPackPage::apply() +{ + return m_underlyingPage == nullptr || m_underlyingPage->apply(); +} + +void GlobalDataPackPage::openedImpl() +{ + if (m_underlyingPage != nullptr) + m_underlyingPage->openedImpl(); +} + +void GlobalDataPackPage::closedImpl() +{ + if (m_underlyingPage != nullptr) + m_underlyingPage->closedImpl(); +} + +void GlobalDataPackPage::updateContent() +{ + if (m_underlyingPage != nullptr) { + if (m_container->selectedPage() == this) + m_underlyingPage->closedImpl(); + + m_underlyingPage->apply(); + + layout()->removeWidget(m_underlyingPage); + + delete m_underlyingPage; + m_underlyingPage = nullptr; + } + + if (shouldDisplay()) { + m_underlyingPage = new DataPackPage(m_instance, m_instance->dataPackList()); + m_underlyingPage->setParentContainer(m_container); + m_underlyingPage->updateExtraInfo = [this](QString id, QString value) { updateExtraInfo(std::move(id), std::move(value)); }; + + if (m_container->selectedPage() == this) + m_underlyingPage->openedImpl(); + + layout()->addWidget(m_underlyingPage); + } +} +void GlobalDataPackPage::setParentContainer(BasePageContainer* container) +{ + BasePage::setParentContainer(container); + updateContent(); +} diff --git a/launcher/ui/pages/instance/DataPackPage.h b/launcher/ui/pages/instance/DataPackPage.h new file mode 100644 index 000000000..6676c165a --- /dev/null +++ b/launcher/ui/pages/instance/DataPackPage.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include "ExternalResourcesPage.h" +#include "minecraft/mod/DataPackFolderModel.h" +#include "ui/dialogs/ResourceDownloadDialog.h" + +class DataPackPage : public ExternalResourcesPage { + Q_OBJECT + public: + explicit DataPackPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent = nullptr); + + QString displayName() const override { return QObject::tr("Data packs"); } + QIcon icon() const override { return APPLICATION->getThemedIcon("datapacks"); } + QString id() const override { return "datapacks"; } + QString helpPage() const override { return "Data-packs"; } + bool shouldDisplay() const override { return true; } + + public slots: + void updateFrame(const QModelIndex& current, const QModelIndex& previous) override; + void downloadDataPacks(); + void downloadDialogFinished(int result); + void updateDataPacks(); + void deleteDataPackMetadata(); + void changeDataPackVersion(); + + private: + std::shared_ptr m_model; + QPointer m_downloadDialog; +}; + +/** + * Syncs DataPackPage with GlobalDataPacksPath and shows/hides based on GlobalDataPacksEnabled. + */ +class GlobalDataPackPage : public QWidget, public BasePage { + public: + explicit GlobalDataPackPage(MinecraftInstance* instance, QWidget* parent = nullptr); + + QString displayName() const override; + QIcon icon() const override; + QString id() const override { return "datapacks"; } + QString helpPage() const override; + + bool shouldDisplay() const override; + + bool apply() override; + void openedImpl() override; + void closedImpl() override; + + void setParentContainer(BasePageContainer* container) override; + + private: + void updateContent(); + QVBoxLayout* layout() { return static_cast(QWidget::layout()); } + + MinecraftInstance* m_instance; + DataPackPage* m_underlyingPage = nullptr; +}; \ No newline at end of file diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index dad2da8a4..7b79766ee 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -225,7 +225,7 @@ void ModFolderPage::updateMods(bool includeDeps) if (use_all) mods_list = m_model->allResources(); - ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, includeDeps, true); + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, includeDeps, profile->getModLoadersList()); update_dialog.checkCandidates(); if (update_dialog.aborted()) { diff --git a/launcher/ui/pages/instance/ResourcePackPage.cpp b/launcher/ui/pages/instance/ResourcePackPage.cpp index f37b3baf9..2b5a5a86c 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.cpp +++ b/launcher/ui/pages/instance/ResourcePackPage.cpp @@ -158,7 +158,7 @@ void ResourcePackPage::updateResourcePacks() if (use_all) mods_list = m_model->allResources(); - ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false, false); + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false); update_dialog.checkCandidates(); if (update_dialog.aborted()) { diff --git a/launcher/ui/pages/instance/ShaderPackPage.cpp b/launcher/ui/pages/instance/ShaderPackPage.cpp index 930b0b9da..baf18da52 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.cpp +++ b/launcher/ui/pages/instance/ShaderPackPage.cpp @@ -155,7 +155,7 @@ void ShaderPackPage::updateShaderPacks() if (use_all) mods_list = m_model->allResources(); - ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false, false); + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false); update_dialog.checkCandidates(); if (update_dialog.aborted()) { diff --git a/launcher/ui/pages/instance/TexturePackPage.cpp b/launcher/ui/pages/instance/TexturePackPage.cpp index 2886decb4..163a14c86 100644 --- a/launcher/ui/pages/instance/TexturePackPage.cpp +++ b/launcher/ui/pages/instance/TexturePackPage.cpp @@ -163,7 +163,7 @@ void TexturePackPage::updateTexturePacks() if (use_all) mods_list = m_model->allResources(); - ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false, false); + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false); update_dialog.checkCandidates(); if (update_dialog.aborted()) { diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 9e1a0fb55..6c10413e4 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -40,7 +40,9 @@ #include "ui/dialogs/CustomMessageBox.h" #include "ui_WorldListPage.h" +#include #include +#include #include #include #include @@ -49,6 +51,7 @@ #include #include #include +#include #include "FileSystem.h" #include "tools/MCEditTool.h" @@ -57,6 +60,7 @@ #include "ui/GuiUtil.h" #include "Application.h" +#include "DataPackPage.h" class WorldListProxyModel : public QSortFilterProxyModel { Q_OBJECT @@ -82,7 +86,7 @@ class WorldListProxyModel : public QSortFilterProxyModel { } }; -WorldListPage::WorldListPage(InstancePtr inst, std::shared_ptr worlds, QWidget* parent) +WorldListPage::WorldListPage(MinecraftInstancePtr inst, std::shared_ptr worlds, QWidget* parent) : QMainWindow(parent), m_inst(inst), ui(new Ui::WorldListPage), m_worlds(worlds) { ui->setupUi(this); @@ -209,7 +213,7 @@ void WorldListPage::on_actionView_Folder_triggered() DesktopServices::openPath(m_worlds->dir().absolutePath(), true); } -void WorldListPage::on_actionDatapacks_triggered() +void WorldListPage::on_actionData_Packs_triggered() { QModelIndex index = getSelectedWorld(); @@ -217,12 +221,49 @@ void WorldListPage::on_actionDatapacks_triggered() return; } - if (!worldSafetyNagQuestion(tr("Open World Datapacks Folder"))) + if (!worldSafetyNagQuestion(tr("Manage Data Packs"))) return; - auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString(); + const QString fullPath = m_worlds->data(index, WorldList::FolderRole).toString(); + const QString folder = FS::PathCombine(fullPath, "datapacks"); - DesktopServices::openPath(FS::PathCombine(fullPath, "datapacks"), true); + auto dialog = new QDialog(this); + dialog->setWindowTitle(tr("Data packs for %1").arg(m_worlds->data(index, WorldList::NameRole).toString())); + dialog->setWindowModality(Qt::WindowModal); + + dialog->resize(static_cast(std::max(0.5 * window()->width(), 400.0)), + static_cast(std::max(0.75 * window()->height(), 400.0))); + dialog->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("DataPackDownloadGeometry").toByteArray())); + + GenericPageProvider provider(dialog->windowTitle()); + + provider.addPageCreator([this, folder] { + bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + auto model = std::make_shared(folder, m_inst.get(), isIndexed, true); + return new DataPackPage(m_inst.get(), std::move(model)); + }); + + auto layout = new QVBoxLayout(dialog); + + auto focusStealer = new QPushButton(dialog); + layout->addWidget(focusStealer); + focusStealer->setDefault(true); + focusStealer->hide(); + + auto pageContainer = new PageContainer(&provider, {}, dialog); + pageContainer->hidePageList(); + layout->addWidget(pageContainer); + + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close | QDialogButtonBox::Help); + connect(buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::reject); + connect(buttonBox, &QDialogButtonBox::helpRequested, pageContainer, &PageContainer::help); + layout->addWidget(buttonBox); + + dialog->setLayout(layout); + + dialog->exec(); + + APPLICATION->settings()->set("DataPackDownloadGeometry", dialog->saveGeometry().toBase64()); } void WorldListPage::on_actionReset_Icon_triggered() @@ -335,7 +376,7 @@ void WorldListPage::worldChanged([[maybe_unused]] const QModelIndex& current, [[ ui->actionRemove->setEnabled(enable); ui->actionCopy->setEnabled(enable); ui->actionRename->setEnabled(enable); - ui->actionDatapacks->setEnabled(enable); + ui->actionData_Packs->setEnabled(enable); bool hasIcon = !index.data(WorldList::IconFileRole).isNull(); ui->actionReset_Icon->setEnabled(enable && hasIcon); diff --git a/launcher/ui/pages/instance/WorldListPage.h b/launcher/ui/pages/instance/WorldListPage.h index 84d9cd075..08cf7dc5f 100644 --- a/launcher/ui/pages/instance/WorldListPage.h +++ b/launcher/ui/pages/instance/WorldListPage.h @@ -53,7 +53,7 @@ class WorldListPage : public QMainWindow, public BasePage { Q_OBJECT public: - explicit WorldListPage(InstancePtr inst, std::shared_ptr worlds, QWidget* parent = 0); + explicit WorldListPage(MinecraftInstancePtr inst, std::shared_ptr worlds, QWidget* parent = 0); virtual ~WorldListPage(); virtual QString displayName() const override { return tr("Worlds"); } @@ -72,7 +72,7 @@ class WorldListPage : public QMainWindow, public BasePage { QMenu* createPopupMenu() override; protected: - InstancePtr m_inst; + MinecraftInstancePtr m_inst; private: QModelIndex getSelectedWorld(); @@ -97,7 +97,7 @@ class WorldListPage : public QMainWindow, public BasePage { void on_actionRename_triggered(); void on_actionRefresh_triggered(); void on_actionView_Folder_triggered(); - void on_actionDatapacks_triggered(); + void on_actionData_Packs_triggered(); void on_actionReset_Icon_triggered(); void worldChanged(const QModelIndex& current, const QModelIndex& previous); void mceditState(LoggedProcess::State state); diff --git a/launcher/ui/pages/instance/WorldListPage.ui b/launcher/ui/pages/instance/WorldListPage.ui index 04344b453..22c93256c 100644 --- a/launcher/ui/pages/instance/WorldListPage.ui +++ b/launcher/ui/pages/instance/WorldListPage.ui @@ -86,7 +86,7 @@ - + @@ -146,12 +146,12 @@ Remove world icon to make the game re-generate it on next load. - + - Datapacks + Data Packs - Manage datapacks inside the world. + Manage data packs inside the world. diff --git a/launcher/ui/pages/modplatform/DataPackModel.cpp b/launcher/ui/pages/modplatform/DataPackModel.cpp new file mode 100644 index 000000000..085bd2d53 --- /dev/null +++ b/launcher/ui/pages/modplatform/DataPackModel.cpp @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2023 flowln +// SPDX-FileCopyrightText: 2023 TheKodeToad +// +// SPDX-License-Identifier: GPL-3.0-only + +#include "DataPackModel.h" + +#include + +namespace ResourceDownload { + +DataPackResourceModel::DataPackResourceModel(BaseInstance const& base_inst, ResourceAPI* api) + : ResourceModel(api), m_base_instance(base_inst) +{} + +/******** Make data requests ********/ + +ResourceAPI::SearchArgs DataPackResourceModel::createSearchArguments() +{ + auto sort = getCurrentSortingMethodByIndex(); + return { ModPlatform::ResourceType::DATA_PACK, m_next_search_offset, m_search_term, sort, ModPlatform::ModLoaderType::DataPack }; +} + +ResourceAPI::VersionSearchArgs DataPackResourceModel::createVersionsArguments(const QModelIndex& entry) +{ + auto& pack = m_packs[entry.row()]; + return { *pack, {}, ModPlatform::ModLoaderType::DataPack }; +} + +ResourceAPI::ProjectInfoArgs DataPackResourceModel::createInfoArguments(const QModelIndex& entry) +{ + auto& pack = m_packs[entry.row()]; + return { *pack }; +} + +void DataPackResourceModel::searchWithTerm(const QString& term, unsigned int sort) +{ + if (m_search_term == term && m_search_term.isNull() == term.isNull() && m_current_sort_index == sort) { + return; + } + + setSearchTerm(term); + m_current_sort_index = sort; + + refresh(); +} + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/DataPackModel.h b/launcher/ui/pages/modplatform/DataPackModel.h new file mode 100644 index 000000000..89e83969c --- /dev/null +++ b/launcher/ui/pages/modplatform/DataPackModel.h @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2023 flowln +// SPDX-FileCopyrightText: 2023 TheKodeToad +// +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include + +#include "BaseInstance.h" + +#include "modplatform/ModIndex.h" + +#include "ui/pages/modplatform/ResourceModel.h" + +class Version; + +namespace ResourceDownload { + +class DataPackResourceModel : public ResourceModel { + Q_OBJECT + + public: + DataPackResourceModel(BaseInstance const&, ResourceAPI*); + + /* Ask the API for more information */ + void searchWithTerm(const QString& term, unsigned int sort); + + void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) override = 0; + void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) override = 0; + void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&) override = 0; + + public slots: + ResourceAPI::SearchArgs createSearchArguments() override; + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; + ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override; + + protected: + const BaseInstance& m_base_instance; + + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0; +}; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/DataPackPage.cpp b/launcher/ui/pages/modplatform/DataPackPage.cpp new file mode 100644 index 000000000..82892b318 --- /dev/null +++ b/launcher/ui/pages/modplatform/DataPackPage.cpp @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2023 flowln +// SPDX-FileCopyrightText: 2023 TheKodeToad +// +// SPDX-License-Identifier: GPL-3.0-only + +#include "DataPackPage.h" +#include "ui_ResourcePage.h" + +#include "DataPackModel.h" + +#include "ui/dialogs/ResourceDownloadDialog.h" + +#include + +namespace ResourceDownload { + +DataPackResourcePage::DataPackResourcePage(DataPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) {} + +/******** Callbacks to events in the UI (set up in the derived classes) ********/ + +void DataPackResourcePage::triggerSearch() +{ + m_ui->packView->clearSelection(); + m_ui->packDescription->clear(); + m_ui->versionSelectionBox->clear(); + + updateSelectionButton(); + + static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt()); + m_fetchProgress.watch(m_model->activeSearchJob().get()); +} + +QMap DataPackResourcePage::urlHandlers() const +{ + QMap map; + map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/resourcepack\\/([^\\/]+)\\/?"), "modrinth"); + map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/texture-packs\\/([^\\/]+)\\/?"), + "curseforge"); + map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge"); + return map; +} + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/DataPackPage.h b/launcher/ui/pages/modplatform/DataPackPage.h new file mode 100644 index 000000000..cf78df96c --- /dev/null +++ b/launcher/ui/pages/modplatform/DataPackPage.h @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2023 flowln +// SPDX-FileCopyrightText: 2023 TheKodeToad +// +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include "ui/pages/modplatform/DataPackModel.h" +#include "ui/pages/modplatform/ResourcePage.h" + +namespace Ui { +class ResourcePage; +} + +namespace ResourceDownload { + +class DataPackDownloadDialog; + +class DataPackResourcePage : public ResourcePage { + Q_OBJECT + + public: + template + static T* create(DataPackDownloadDialog* dialog, BaseInstance& instance) + { + auto page = new T(dialog, instance); + auto model = static_cast(page->getModel()); + + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); + connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); + connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); + + return page; + } + + //: The plural version of 'data pack' + [[nodiscard]] inline QString resourcesString() const override { return tr("data packs"); } + //: The singular version of 'data packs' + [[nodiscard]] inline QString resourceString() const override { return tr("data pack"); } + + [[nodiscard]] bool supportsFiltering() const override { return false; }; + + [[nodiscard]] QMap urlHandlers() const override; + + protected: + DataPackResourcePage(DataPackDownloadDialog* dialog, BaseInstance& instance); + + protected slots: + void triggerSearch() override; +}; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp index 31ddefebf..9917c29e6 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -170,4 +170,32 @@ auto FlameShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArr return Json::ensureArray(obj.object(), "data"); } +FlameDataPackModel::FlameDataPackModel(const BaseInstance& base) : DataPackResourceModel(base, new FlameAPI) {} + +void FlameDataPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + FlameMod::loadIndexedPack(m, obj); +} + +// We already deal with the URLs when initializing the pack, due to the API response's structure +void FlameDataPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + FlameMod::loadBody(m, obj); +} + +void FlameDataPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) +{ + FlameMod::loadIndexedPackVersions(m, arr); +} + +bool FlameDataPackModel::optedOut(const ModPlatform::IndexedVersion& ver) const +{ + return isOptedOut(ver); +} + +auto FlameDataPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray +{ + return Json::ensureArray(obj.object(), "data"); +} + } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h index 9b86a0944..5fffe6361 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h @@ -93,4 +93,23 @@ class FlameShaderPackModel : public ShaderPackResourceModel { auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; }; +class FlameDataPackModel : public DataPackResourceModel { + Q_OBJECT + + public: + FlameDataPackModel(const BaseInstance&); + ~FlameDataPackModel() override = default; + + bool optedOut(const ModPlatform::IndexedVersion& ver) const override; + + private: + [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } + [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } + + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; +}; + } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index 4bea52fc0..32175a356 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -152,6 +152,22 @@ void FlameTexturePackPage::openUrl(const QUrl& url) TexturePackResourcePage::openUrl(url); } +void FlameDataPackPage::openUrl(const QUrl& url) +{ + if (url.scheme().isEmpty()) { + QString query = url.query(QUrl::FullyDecoded); + + if (query.startsWith("remoteUrl=")) { + // attempt to resolve url from warning page + query.remove(0, 10); + DataPackResourcePage::openUrl({ QUrl::fromPercentEncoding(query.toUtf8()) }); // double decoding is necessary + return; + } + } + + DataPackResourcePage::openUrl(url); +} + FlameShaderPackPage::FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ShaderPackResourcePage(dialog, instance) { @@ -171,6 +187,25 @@ FlameShaderPackPage::FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseI m_ui->packDescription->setMetaEntry(metaEntryBase()); } +FlameDataPackPage::FlameDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance) + : DataPackResourcePage(dialog, instance) +{ + m_model = new FlameDataPackModel(instance); + m_ui->packView->setModel(m_model); + + addSortings(); + + // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, + // so it's best not to connect them in the parent's constructor... + connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameDataPackPage::onSelectionChanged); + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &FlameDataPackPage::onVersionSelectionChanged); + connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameDataPackPage::onResourceSelected); + + m_ui->packDescription->setMetaEntry(metaEntryBase()); +} + void FlameShaderPackPage::openUrl(const QUrl& url) { if (url.scheme().isEmpty()) { @@ -206,6 +241,10 @@ auto FlameShaderPackPage::shouldDisplay() const -> bool { return true; } +auto FlameDataPackPage::shouldDisplay() const -> bool +{ + return true; +} std::unique_ptr FlameModPage::createFilterWidget() { diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index 3518e7c24..309e1e019 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -39,6 +39,7 @@ #pragma once +#include #include "Application.h" #include "modplatform/ResourceAPI.h" @@ -180,4 +181,31 @@ class FlameShaderPackPage : public ShaderPackResourcePage { void openUrl(const QUrl& url) override; }; + + +class FlameDataPackPage : public DataPackResourcePage { + Q_OBJECT + + public: + static FlameDataPackPage* create(DataPackDownloadDialog* dialog, BaseInstance& instance) + { + return DataPackResourcePage::create(dialog, instance); + } + + FlameDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance); + ~FlameDataPackPage() override = default; + + [[nodiscard]] bool shouldDisplay() const override; + + [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); } + [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); } + [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); } + [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); } + [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } + + [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + + void openUrl(const QUrl& url) override; +}; + } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp index 213d6e39e..91e9ad791 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp @@ -118,4 +118,27 @@ auto ModrinthShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJson return obj.object().value("hits").toArray(); } +ModrinthDataPackModel::ModrinthDataPackModel(const BaseInstance& base) : DataPackResourceModel(base, new ModrinthAPI) {} + +void ModrinthDataPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + ::Modrinth::loadIndexedPack(m, obj); +} + +void ModrinthDataPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + ::Modrinth::loadExtraPackData(m, obj); +} + +void ModrinthDataPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) +{ + ::Modrinth::loadIndexedPackVersions(m, arr); +} + +auto ModrinthDataPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray +{ + return obj.object().value("hits").toArray(); +} + + } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h index 15cd58544..6a5ba0382 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h @@ -20,6 +20,7 @@ #pragma once +#include "ui/pages/modplatform/DataPackModel.h" #include "ui/pages/modplatform/ModModel.h" #include "ui/pages/modplatform/ResourcePackModel.h" #include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h" @@ -99,4 +100,22 @@ class ModrinthShaderPackModel : public ShaderPackResourceModel { auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; }; +class ModrinthDataPackModel : public DataPackResourceModel { + Q_OBJECT + + public: + ModrinthDataPackModel(const BaseInstance&); + ~ModrinthDataPackModel() override = default; + + private: + [[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; } + [[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } + + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; +}; + } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index 398bf0455..f75323a28 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -122,6 +122,25 @@ ModrinthShaderPackPage::ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog, m_ui->packDescription->setMetaEntry(metaEntryBase()); } +ModrinthDataPackPage::ModrinthDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance) + : DataPackResourcePage(dialog, instance) +{ + m_model = new ModrinthDataPackModel(instance); + m_ui->packView->setModel(m_model); + + addSortings(); + + // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, + // so it's best not to connect them in the parent's constructor... + connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthDataPackPage::onSelectionChanged); + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &ModrinthDataPackPage::onVersionSelectionChanged); + connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthDataPackPage::onResourceSelected); + + m_ui->packDescription->setMetaEntry(metaEntryBase()); +} + // I don't know why, but doing this on the parent class makes it so that // other mod providers start loading before being selected, at least with // my Qt, so we need to implement this in every derived class... @@ -141,6 +160,10 @@ auto ModrinthShaderPackPage::shouldDisplay() const -> bool { return true; } +auto ModrinthDataPackPage::shouldDisplay() const -> bool +{ + return true; +} std::unique_ptr ModrinthModPage::createFilterWidget() { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h index 7f8d9d571..a4c7344b5 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -42,6 +42,7 @@ #include "modplatform/ResourceAPI.h" +#include "ui/pages/modplatform/DataPackPage.h" #include "ui/pages/modplatform/ModPage.h" #include "ui/pages/modplatform/ResourcePackPage.h" #include "ui/pages/modplatform/ShaderPackPage.h" @@ -170,4 +171,27 @@ class ModrinthShaderPackPage : public ShaderPackResourcePage { [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } }; +class ModrinthDataPackPage : public DataPackResourcePage { + Q_OBJECT + + public: + static ModrinthDataPackPage* create(DataPackDownloadDialog* dialog, BaseInstance& instance) + { + return DataPackResourcePage::create(dialog, instance); + } + + ModrinthDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance); + ~ModrinthDataPackPage() override = default; + + [[nodiscard]] bool shouldDisplay() const override; + + [[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); } + [[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); } + [[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); } + [[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); } + [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } + + [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } +}; + } // namespace ResourceDownload diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index 93520f611..8d6505655 100644 --- a/launcher/ui/widgets/InfoFrame.cpp +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -227,6 +227,12 @@ void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack) setImage(resource_pack.image({ 64, 64 })); } +void InfoFrame::updateWithDataPack(DataPack& data_pack) { + setName(renderColorCodes(data_pack.name())); + setDescription(renderColorCodes(data_pack.description())); + setImage(data_pack.image({ 64, 64 })); +} + void InfoFrame::updateWithTexturePack(TexturePack& texture_pack) { QString name = renderColorCodes(texture_pack.name()); diff --git a/launcher/ui/widgets/InfoFrame.h b/launcher/ui/widgets/InfoFrame.h index d6764baa2..20c54e2e5 100644 --- a/launcher/ui/widgets/InfoFrame.h +++ b/launcher/ui/widgets/InfoFrame.h @@ -37,6 +37,7 @@ #include +#include "minecraft/mod/DataPack.h" #include "minecraft/mod/Mod.h" #include "minecraft/mod/ResourcePack.h" #include "minecraft/mod/TexturePack.h" @@ -63,6 +64,7 @@ class InfoFrame : public QFrame { void updateWithMod(Mod const& m); void updateWithResource(Resource const& resource); void updateWithResourcePack(ResourcePack& rp); + void updateWithDataPack(DataPack& rp); void updateWithTexturePack(TexturePack& tp); static QString renderColorCodes(QString input); diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp index 610dc143b..5eceb0b76 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -36,7 +36,9 @@ */ #include "MinecraftSettingsWidget.h" +#include "ui_MinecraftSettingsWidget.h" +#include #include "Application.h" #include "BuildConfig.h" #include "Json.h" @@ -44,7 +46,6 @@ #include "minecraft/WorldList.h" #include "minecraft/auth/AccountList.h" #include "settings/Setting.h" -#include "ui_MinecraftSettingsWidget.h" MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstancePtr instance, QWidget* parent) : QWidget(parent), m_instance(std::move(instance)), m_ui(new Ui::MinecraftSettingsWidget) @@ -57,6 +58,7 @@ MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstancePtr instance, m_ui->openGlobalSettingsButton->setVisible(false); m_ui->instanceAccountGroupBox->hide(); m_ui->serverJoinGroupBox->hide(); + m_ui->globalDataPacksGroupBox->hide(); m_ui->loaderGroup->hide(); } else { m_javaSettings = new JavaSettingsWidget(m_instance, this); @@ -97,6 +99,14 @@ MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstancePtr instance, connect(m_ui->serverJoinAddressButton, &QAbstractButton::toggled, m_ui->serverJoinAddress, &QWidget::setEnabled); connect(m_ui->worldJoinButton, &QAbstractButton::toggled, m_ui->worldsCb, &QWidget::setEnabled); + connect(m_ui->globalDataPacksGroupBox, &QGroupBox::toggled, this, [this](bool value) { + m_instance->settings()->set("GlobalDataPacksEnabled", value); + if (!value) + m_instance->settings()->reset("GlobalDataPacksPath"); + }); + connect(m_ui->dataPacksPathEdit, &QLineEdit::editingFinished, this, &MinecraftSettingsWidget::editedDataPacksPath); + connect(m_ui->dataPacksPathBrowse, &QPushButton::clicked, this, &MinecraftSettingsWidget::selectDataPacksFolder); + connect(m_ui->loaderGroup, &QGroupBox::toggled, this, [this](bool value) { m_instance->settings()->set("OverrideModDownloadLoaders", value); if (!value) @@ -267,6 +277,13 @@ void MinecraftSettingsWidget::loadSettings() m_ui->legacySettingsGroupBox->setChecked(settings->get("OverrideLegacySettings").toBool()); m_ui->onlineFixes->setChecked(settings->get("OnlineFixes").toBool()); + + m_ui->globalDataPacksGroupBox->blockSignals(true); + m_ui->dataPacksPathEdit->blockSignals(true); + m_ui->globalDataPacksGroupBox->setChecked(settings->get("GlobalDataPacksEnabled").toBool()); + m_ui->dataPacksPathEdit->setText(settings->get("GlobalDataPacksPath").toString()); + m_ui->globalDataPacksGroupBox->blockSignals(false); + m_ui->dataPacksPathEdit->blockSignals(false); } void MinecraftSettingsWidget::saveSettings() @@ -487,6 +504,34 @@ bool MinecraftSettingsWidget::isQuickPlaySupported() return m_instance->traits().contains("feature:is_quick_play_singleplayer"); } +void MinecraftSettingsWidget::editedDataPacksPath() +{ + if (QDir::separator() != '/') + m_ui->dataPacksPathEdit->setText(m_ui->dataPacksPathEdit->text().replace(QDir::separator(), '/')); + + m_instance->settings()->set("GlobalDataPacksPath", m_ui->dataPacksPathEdit->text()); +} + +void MinecraftSettingsWidget::selectDataPacksFolder() +{ + QString path = QFileDialog::getExistingDirectory(this, tr("Select Global Data Packs Folder"), m_instance->gameRoot()); + + if (path.isEmpty()) + return; + + // if it's inside the instance dir, set path relative to .minecraft + // (so that if it's directly in instance dir it will still lead with .. but more than two levels up are kept absolute) + + const QUrl instanceRootUrl = QUrl::fromLocalFile(m_instance->instanceRoot()); + const QUrl pathUrl = QUrl::fromLocalFile(path); + + if (instanceRootUrl.isParentOf(pathUrl)) + path = QDir(m_instance->gameRoot()).relativeFilePath(path); + + m_ui->dataPacksPathEdit->setText(path); + m_instance->settings()->set("GlobalDataPacksPath", path); +} + void MinecraftSettingsWidget::selectedLoadersChanged() { QStringList loaders; @@ -501,4 +546,4 @@ void MinecraftSettingsWidget::selectedLoadersChanged() if (m_ui->liteLoader->isChecked()) loaders << getModLoaderAsString(ModPlatform::LiteLoader); m_instance->settings()->set("ModDownloadLoaders", Json::fromStringList(loaders)); -} \ No newline at end of file +} diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.h b/launcher/ui/widgets/MinecraftSettingsWidget.h index 6be73375e..1481d0fae 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.h +++ b/launcher/ui/widgets/MinecraftSettingsWidget.h @@ -58,6 +58,8 @@ class MinecraftSettingsWidget : public QWidget { bool isQuickPlaySupported(); private slots: void selectedLoadersChanged(); + void editedDataPacksPath(); + void selectDataPacksFolder(); MinecraftInstancePtr m_instance; Ui::MinecraftSettingsWidget* m_ui; diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.ui b/launcher/ui/widgets/MinecraftSettingsWidget.ui index 15406873a..4a35addc0 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.ui +++ b/launcher/ui/widgets/MinecraftSettingsWidget.ui @@ -58,9 +58,9 @@ 0 - -537 - 623 - 1007 + 0 + 603 + 1042 @@ -252,6 +252,70 @@ + + + + &Global Data Packs + + + true + + + true + + + + + + Allows installing data packs across all worlds if an applicable mod is installed. +It is most likely you will need to change the path - please refer to the mod's website. + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + + + + + Folder Path + + + + + + + + + datapacks + + + + + + + Browse + + + + + + + + @@ -489,8 +553,8 @@ 0 0 - 98 - 28 + 624 + 487 @@ -513,8 +577,8 @@ 0 0 - 299 - 499 + 624 + 487 diff --git a/tests/MetaComponentParse_test.cpp b/tests/MetaComponentParse_test.cpp index 9979a9fa6..c5c41388b 100644 --- a/tests/MetaComponentParse_test.cpp +++ b/tests/MetaComponentParse_test.cpp @@ -41,7 +41,7 @@ #include -#include +#include class MetaComponentParseTest : public QObject { Q_OBJECT @@ -69,7 +69,7 @@ class MetaComponentParseTest : public QObject { QString expected = expected_json.toString(); - QString processed = ResourcePackUtils::processComponent(description_json); + QString processed = DataPackUtils::processComponent(description_json); QCOMPARE(processed, expected); } diff --git a/tests/ResourcePackParse_test.cpp b/tests/ResourcePackParse_test.cpp index 17c0482fc..e9b5244ad 100644 --- a/tests/ResourcePackParse_test.cpp +++ b/tests/ResourcePackParse_test.cpp @@ -23,7 +23,6 @@ #include #include -#include class ResourcePackParseTest : public QObject { Q_OBJECT