Data pack management (#1905)

This commit is contained in:
TheKodeToad 2025-06-02 09:18:57 +00:00 committed by GitHub
commit d9b672ee8d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
71 changed files with 1673 additions and 418 deletions

View file

@ -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?
{

View file

@ -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

View file

@ -1,5 +1,6 @@
#pragma once
#include <FileSystem.h>
#include <ui/pages/instance/DataPackPage.h>
#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()));

View file

@ -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<ShaderPackFolderModel> MinecraftInstance::shaderPackList()
return m_shader_pack_list;
}
std::shared_ptr<DataPackFolderModel> 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<std::shared_ptr<ResourceFolderModel>> MinecraftInstance::resourceLists()
{
return { loaderModList(), coreModList(), nilModList(), resourcePackList(), texturePackList(), shaderPackList() };
return { loaderModList(), coreModList(), nilModList(), resourcePackList(), texturePackList(), shaderPackList(), dataPackList() };
}
std::shared_ptr<WorldList> MinecraftInstance::worldList()

View file

@ -36,6 +36,7 @@
#pragma once
#include <java/JavaVersion.h>
#include <minecraft/mod/DataPackFolderModel.h>
#include <QDir>
#include <QProcess>
#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<ResourcePackFolderModel> resourcePackList();
std::shared_ptr<TexturePackFolderModel> texturePackList();
std::shared_ptr<ShaderPackFolderModel> shaderPackList();
std::shared_ptr<DataPackFolderModel> dataPackList();
QList<std::shared_ptr<ResourceFolderModel>> resourceLists();
std::shared_ptr<WorldList> worldList();
std::shared_ptr<GameOptions> gameOptionsModel();
@ -166,6 +169,7 @@ class MinecraftInstance : public BaseInstance {
mutable std::shared_ptr<ResourcePackFolderModel> m_resource_pack_list;
mutable std::shared_ptr<ShaderPackFolderModel> m_shader_pack_list;
mutable std::shared_ptr<TexturePackFolderModel> m_texture_pack_list;
mutable std::shared_ptr<DataPackFolderModel> m_data_pack_list;
mutable std::shared_ptr<WorldList> m_world_list;
mutable std::shared_ptr<GameOptions> m_game_options;
};

View file

@ -25,7 +25,9 @@
#include <QMap>
#include <QRegularExpression>
#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<Version, Version> DataPack::compatibleVersions() const
{
if (!s_pack_format_versions.contains(m_pack_format)) {

View file

@ -24,6 +24,7 @@
#include "Resource.h"
#include <QMutex>
#include <QPixmapCache>
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<Version, Version> compatibleVersions() const;
[[nodiscard]] virtual std::pair<Version, Version> 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;
};

View file

@ -0,0 +1,188 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* 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 <https://www.gnu.org/licenses/>.
*
* 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 <qnamespace.h>
#include <qsize.h>
#include <QIcon>
#include <QStyle>
#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<DataPack*>(&resource));
}

View file

@ -0,0 +1,62 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* 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 <https://www.gnu.org/licenses/>.
*
* 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)
};

View file

@ -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<int, std::pair<Version, Version>> s_pack_format_versions = {
@ -29,62 +27,6 @@ static const QMap<int, std::pair<Version, Version>> 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<Version, Version> ResourcePack::compatibleVersions() const
{
if (!s_pack_format_versions.contains(m_pack_format)) {

View file

@ -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<Version, Version> 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<Version, Version> 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;
};

View file

@ -24,7 +24,6 @@
#include "FileSystem.h"
#include "Json.h"
#include "minecraft/mod/ResourcePack.h"
#include "minecraft/mod/tasks/LocalResourcePackParseTask.h"
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
@ -82,9 +81,8 @@ bool processFolder(DataPack* pack, ProcessingLevel level)
if (level == ProcessingLevel::BasicInfoOnly) {
return true; // only need basic info already checked
}
if (auto rp = dynamic_cast<ResourcePack*>(pack)) {
auto png_invalid = [&pack]() {
qWarning() << "Resource pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png";
qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png";
return true; // the png is optional
};
@ -96,7 +94,7 @@ bool processFolder(DataPack* pack, ProcessingLevel level)
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) {
@ -105,7 +103,6 @@ bool processFolder(DataPack* pack, ProcessingLevel level)
} else {
return png_invalid(); // pack.png does not exists or is not a valid file.
}
}
return true; // all tests passed
}
@ -153,9 +150,9 @@ bool processZIP(DataPack* pack, ProcessingLevel level)
zip.close();
return true; // only need basic info already checked
}
if (auto rp = dynamic_cast<ResourcePack*>(pack)) {
auto png_invalid = [&pack]() {
qWarning() << "Resource pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png";
qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png";
return true; // the png is optional
};
@ -168,7 +165,7 @@ bool processZIP(DataPack* pack, ProcessingLevel level)
auto data = file.readAll();
bool pack_png_result = ResourcePackUtils::processPackPNG(rp, std::move(data));
bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data));
file.close();
zip.close();
@ -179,7 +176,6 @@ bool processZIP(DataPack* pack, ProcessingLevel level)
zip.close();
return png_invalid(); // could not set pack.mcmeta as current file.
}
}
zip.close();
return true;
@ -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("<u>%1</u>").arg(result);
}
if (strikethrough) {
result = QString("<s>%1</s>").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("<span %1>%2</span>").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("<a href=\"%1\">%2</a>").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) {}

View file

@ -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 {

View file

@ -1,192 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "LocalResourcePackParseTask.h"
#include "FileSystem.h"
#include "Json.h"
#include "minecraft/mod/tasks/LocalDataPackParseTask.h"
#include <quazip/quazip.h>
#include <quazip/quazipdir.h>
#include <quazip/quazipfile.h>
#include <QCryptographicHash>
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("<u>%1</u>").arg(result);
}
if (strikethrough) {
result = QString("<s>%1</s>").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("<span %1>%2</span>").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("<a href=\"%1\">%2</a>").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

View file

@ -1,36 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QDebug>
#include <QObject>
#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

View file

@ -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)) {

View file

@ -127,6 +127,8 @@ auto getModLoaderAsString(ModLoaderType type) -> const QString
return "fabric";
case Quilt:
return "quilt";
case DataPack:
return "datapack";
default:
break;
}

View file

@ -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<ModLoaderType> 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 };

View file

@ -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<ModPlatform::ModLoaderType>(static_cast<int>(args.loaders.value())));
url += QString("&modLoaderType=%1").arg(mappedModLoader);
}

View file

@ -89,6 +89,8 @@ void PackInstallTask::copySettings()
break;
case ModPlatform::LiteLoader:
break;
case ModPlatform::DataPack:
break;
}
components->saveNow();

View file

@ -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<QString> getDependencyURL(DependencySearchArgs const& args) const override

View file

@ -9,7 +9,7 @@
<file>scalable/copy.svg</file>
<file>scalable/coremods.svg</file>
<file>scalable/custom-commands.svg</file>
<file>scalable/environment-variables.svg</file>
<file>scalable/datapacks.svg</file>
<file>scalable/discord.svg</file>
<file>scalable/externaltools.svg</file>
<file>scalable/help.svg</file>

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

View file

@ -9,7 +9,7 @@
<file>scalable/copy.svg</file>
<file>scalable/coremods.svg</file>
<file>scalable/custom-commands.svg</file>
<file>scalable/environment-variables.svg</file>
<file>scalable/datapacks.svg</file>
<file>scalable/discord.svg</file>
<file>scalable/externaltools.svg</file>
<file>scalable/help.svg</file>

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

View file

@ -11,7 +11,7 @@
<file>scalable/copy.svg</file>
<file>scalable/coremods.svg</file>
<file>scalable/custom-commands.svg</file>
<file>scalable/environment-variables.svg</file>
<file>scalable/datapacks.svg</file>
<file>scalable/discord.svg</file>
<file>scalable/externaltools.svg</file>
<file>scalable/help.svg</file>

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before After
Before After

View file

@ -11,7 +11,7 @@
<file>scalable/copy.svg</file>
<file>scalable/coremods.svg</file>
<file>scalable/custom-commands.svg</file>
<file>scalable/environment-variables.svg</file>
<file>scalable/datapacks.svg</file>
<file>scalable/discord.svg</file>
<file>scalable/externaltools.svg</file>
<file>scalable/help.svg</file>

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before After
Before After

View file

@ -74,7 +74,7 @@
<file>scalable/screenshots.svg</file>
<file>scalable/custom-commands.svg</file>
<file>scalable/environment-variables.svg</file>
<file>scalable/datapacks.svg</file>
<!-- The cat button. Freeware, http://findicons.com/icon/73096/black_cat -->
<file>16x16/cat.png</file>

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -10,7 +10,7 @@
<file>scalable/copy.svg</file>
<file>scalable/coremods.svg</file>
<file>scalable/custom-commands.svg</file>
<file>scalable/environment-variables.svg</file>
<file>scalable/datapacks.svg</file>
<file>scalable/externaltools.svg</file>
<file>scalable/help.svg</file>
<file>scalable/instance-settings.svg</file>

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View file

@ -10,7 +10,7 @@
<file>scalable/copy.svg</file>
<file>scalable/coremods.svg</file>
<file>scalable/custom-commands.svg</file>
<file>scalable/environment-variables.svg</file>
<file>scalable/datapacks.svg</file>
<file>scalable/externaltools.svg</file>
<file>scalable/help.svg</file>
<file>scalable/instance-settings.svg</file>

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View file

@ -10,7 +10,7 @@
<file>scalable/copy.svg</file>
<file>scalable/coremods.svg</file>
<file>scalable/custom-commands.svg</file>
<file>scalable/environment-variables.svg</file>
<file>scalable/datapacks.svg</file>
<file>scalable/externaltools.svg</file>
<file>scalable/help.svg</file>
<file>scalable/instance-settings.svg</file>

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View file

@ -10,7 +10,7 @@
<file>scalable/copy.svg</file>
<file>scalable/coremods.svg</file>
<file>scalable/custom-commands.svg</file>
<file>scalable/environment-variables.svg</file>
<file>scalable/datapacks.svg</file>
<file>scalable/externaltools.svg</file>
<file>scalable/help.svg</file>
<file>scalable/instance-settings.svg</file>

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View file

@ -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<int>(std::max(0.5 * parent->width(), 400.0)), static_cast<int>(std::max(0.75 * parent->height(), 400.0)));
setWindowIcon(APPLICATION->getThemedIcon("new"));
@ -396,4 +396,28 @@ void ResourceDownloadDialog::setResourceMetadata(const std::shared_ptr<Metadata:
auto page = selectedPage();
page->openProject(meta->project_id);
}
DataPackDownloadDialog::DataPackDownloadDialog(QWidget* parent,
const std::shared_ptr<DataPackFolderModel>& 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<BasePage*> DataPackDownloadDialog::getPages()
{
QList<BasePage*> pages;
pages.append(ModrinthDataPackPage::create(this, *m_instance));
if (APPLICATION->capabilities() & Application::SupportsFlame)
pages.append(FlameDataPackPage::create(this, *m_instance));
return pages;
}
} // namespace ResourceDownload

View file

@ -25,6 +25,7 @@
#include <QLayout>
#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<DataPackFolderModel>& 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<BasePage*> getPages() override;
private:
BaseInstance* m_instance;
};
} // namespace ResourceDownload

View file

@ -32,17 +32,12 @@ static std::list<Version> mcVersions(BaseInstance* inst)
return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() };
}
static QList<ModPlatform::ModLoaderType> mcLoadersList(BaseInstance* inst)
{
return static_cast<MinecraftInstance*>(inst)->getPackProfile()->getModLoadersList();
}
ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent,
BaseInstance* instance,
const std::shared_ptr<ResourceFolderModel> resource_model,
QList<Resource*>& search_for,
bool include_deps,
bool filter_loaders)
QList<ModPlatform::ModLoaderType> 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<ModPlatform::ModLoaderType>();
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 });

View file

@ -21,7 +21,7 @@ class ResourceUpdateDialog final : public ReviewMessageBox {
std::shared_ptr<ResourceFolderModel> resource_model,
QList<Resource*>& search_for,
bool include_deps,
bool filter_loaders);
QList<ModPlatform::ModLoaderType> 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<ModPlatform::ModLoaderType> m_loadersList;
};

View file

@ -0,0 +1,360 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#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<DataPackFolderModel> 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<MinecraftInstance*>(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<MinecraftInstance*>(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();
}

View file

@ -0,0 +1,76 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QVBoxLayout>
#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<DataPackFolderModel> 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<DataPackFolderModel> m_model;
QPointer<ResourceDownload::DataPackDownloadDialog> 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<QVBoxLayout*>(QWidget::layout()); }
MinecraftInstance* m_instance;
DataPackPage* m_underlyingPage = nullptr;
};

View file

@ -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()) {

View file

@ -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()) {

View file

@ -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()) {

View file

@ -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()) {

View file

@ -40,7 +40,9 @@
#include "ui/dialogs/CustomMessageBox.h"
#include "ui_WorldListPage.h"
#include <ui/widgets/PageContainer.h>
#include <QClipboard>
#include <QDialogButtonBox>
#include <QEvent>
#include <QInputDialog>
#include <QKeyEvent>
@ -49,6 +51,7 @@
#include <QSortFilterProxyModel>
#include <QTreeView>
#include <Qt>
#include <QPushButton>
#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<WorldList> worlds, QWidget* parent)
WorldListPage::WorldListPage(MinecraftInstancePtr inst, std::shared_ptr<WorldList> 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<int>(std::max(0.5 * window()->width(), 400.0)),
static_cast<int>(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<DataPackFolderModel>(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);

View file

@ -53,7 +53,7 @@ class WorldListPage : public QMainWindow, public BasePage {
Q_OBJECT
public:
explicit WorldListPage(InstancePtr inst, std::shared_ptr<WorldList> worlds, QWidget* parent = 0);
explicit WorldListPage(MinecraftInstancePtr inst, std::shared_ptr<WorldList> 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);

View file

@ -86,7 +86,7 @@
<addaction name="actionCopy"/>
<addaction name="actionRemove"/>
<addaction name="actionMCEdit"/>
<addaction name="actionDatapacks"/>
<addaction name="actionData_Packs"/>
<addaction name="actionReset_Icon"/>
<addaction name="separator"/>
<addaction name="actionCopy_Seed"/>
@ -146,9 +146,9 @@
<string>Remove world icon to make the game re-generate it on next load.</string>
</property>
</action>
<action name="actionDatapacks">
<action name="actionData_Packs">
<property name="text">
<string>Datapacks</string>
<string>Data Packs</string>
</property>
<property name="toolTip">
<string>Manage data packs inside the world.</string>

View file

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
// SPDX-FileCopyrightText: 2023 TheKodeToad <TheKodeToad@proton.me>
//
// SPDX-License-Identifier: GPL-3.0-only
#include "DataPackModel.h"
#include <QMessageBox>
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

View file

@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
// SPDX-FileCopyrightText: 2023 TheKodeToad <TheKodeToad@proton.me>
//
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <QAbstractListModel>
#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

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
// SPDX-FileCopyrightText: 2023 TheKodeToad <TheKodeToad@proton.me>
//
// SPDX-License-Identifier: GPL-3.0-only
#include "DataPackPage.h"
#include "ui_ResourcePage.h"
#include "DataPackModel.h"
#include "ui/dialogs/ResourceDownloadDialog.h"
#include <QRegularExpression>
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<DataPackResourceModel*>(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt());
m_fetchProgress.watch(m_model->activeSearchJob().get());
}
QMap<QString, QString> DataPackResourcePage::urlHandlers() const
{
QMap<QString, QString> 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

View file

@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: 2023 flowln <flowlnlnln@gmail.com>
// SPDX-FileCopyrightText: 2023 TheKodeToad <TheKodeToad@proton.me>
//
// 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 <typename T>
static T* create(DataPackDownloadDialog* dialog, BaseInstance& instance)
{
auto page = new T(dialog, instance);
auto model = static_cast<DataPackResourceModel*>(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<QString, QString> urlHandlers() const override;
protected:
DataPackResourcePage(DataPackDownloadDialog* dialog, BaseInstance& instance);
protected slots:
void triggerSearch() override;
};
} // namespace ResourceDownload

View file

@ -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

View file

@ -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

View file

@ -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<int>::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<ModFilterWidget> FlameModPage::createFilterWidget()
{

View file

@ -39,6 +39,7 @@
#pragma once
#include <ui/pages/modplatform/DataPackPage.h>
#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<FlameDataPackPage>(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

View file

@ -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

View file

@ -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

View file

@ -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<int>::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<ModFilterWidget> ModrinthModPage::createFilterWidget()
{

View file

@ -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<ModrinthDataPackPage>(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

View file

@ -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());

View file

@ -37,6 +37,7 @@
#include <QFrame>
#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);

View file

@ -36,7 +36,9 @@
*/
#include "MinecraftSettingsWidget.h"
#include "ui_MinecraftSettingsWidget.h"
#include <QFileDialog>
#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;

View file

@ -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;

View file

@ -58,9 +58,9 @@
<property name="geometry">
<rect>
<x>0</x>
<y>-537</y>
<width>623</width>
<height>1007</height>
<y>0</y>
<width>603</width>
<height>1042</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
@ -252,6 +252,70 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="globalDataPacksGroupBox">
<property name="title">
<string>&amp;Global Data Packs</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_18">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>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.</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>6</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Folder Path</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLineEdit" name="dataPacksPathEdit">
<property name="placeholderText">
<string>datapacks</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="dataPacksPathBrowse">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="gameTimeGroupBox">
<property name="enabled">
@ -489,8 +553,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>98</width>
<height>28</height>
<width>624</width>
<height>487</height>
</rect>
</property>
</widget>
@ -513,8 +577,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>299</width>
<height>499</height>
<width>624</width>
<height>487</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">

View file

@ -41,7 +41,7 @@
#include <FileSystem.h>
#include <minecraft/mod/tasks/LocalResourcePackParseTask.h>
#include <minecraft/mod/tasks/LocalDataPackParseTask.h>
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);
}

View file

@ -23,7 +23,6 @@
#include <FileSystem.h>
#include <minecraft/mod/ResourcePack.h>
#include <minecraft/mod/tasks/LocalResourcePackParseTask.h>
class ResourcePackParseTest : public QObject {
Q_OBJECT