diff options
Diffstat (limited to 'launcher/minecraft')
20 files changed, 719 insertions, 107 deletions
diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 4e264a74..13fed1c9 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -151,12 +151,12 @@ int ModFolderModel::columnCount(const QModelIndex &parent) const Task* ModFolderModel::createUpdateTask() { auto index_dir = indexDir(); - auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load, this); + auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load); m_first_folder_load = false; return task; } -Task* ModFolderModel::createParseTask(Resource const& resource) +Task* ModFolderModel::createParseTask(Resource& resource) { return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo()); } @@ -259,15 +259,6 @@ void ModFolderModel::onUpdateSucceeded() #endif applyUpdates(current_set, new_set, new_mods); - - m_current_update_task.reset(); - - if (m_scheduled_update) { - m_scheduled_update = false; - update(); - } else { - emit updateFinished(); - } } void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index c33195ed..93980319 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -82,7 +82,7 @@ public: int columnCount(const QModelIndex &parent) const override; [[nodiscard]] Task* createUpdateTask() override; - [[nodiscard]] Task* createParseTask(Resource const&) override; + [[nodiscard]] Task* createParseTask(Resource&) override; bool installMod(QString file_path) { return ResourceFolderModel::installResource(file_path); } bool uninstallMod(const QString& filename, bool preserve_metadata = false); diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index cee1f172..f9bd811e 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -20,6 +20,7 @@ enum class SortType { DATE, VERSION, ENABLED, + PACK_FORMAT }; enum class EnableAction { @@ -80,6 +81,7 @@ class Resource : public QObject { [[nodiscard]] auto shouldResolve() const -> bool { return !m_is_resolving && !m_is_resolved; } [[nodiscard]] auto isResolving() const -> bool { return m_is_resolving; } + [[nodiscard]] auto isResolved() const -> bool { return m_is_resolved; } [[nodiscard]] auto resolutionTicket() const -> int { return m_resolution_ticket; } void setResolving(bool resolving, int resolutionTicket) diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index bc18ddc2..45d1db59 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -1,5 +1,6 @@ #include "ResourceFolderModel.h" +#include <QCoreApplication> #include <QDebug> #include <QMimeData> #include <QThreadPool> @@ -19,6 +20,12 @@ ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent) : QAbstractL connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged); } +ResourceFolderModel::~ResourceFolderModel() +{ + while (!QThreadPool::globalInstance()->waitForDone(100)) + QCoreApplication::processEvents(); +} + bool ResourceFolderModel::startWatching(const QStringList paths) { if (m_is_watching) @@ -229,9 +236,17 @@ bool ResourceFolderModel::update() connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded, Qt::ConnectionType::QueuedConnection); connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection); + connect(m_current_update_task.get(), &Task::finished, this, [=] { + m_current_update_task.reset(); + if (m_scheduled_update) { + m_scheduled_update = false; + update(); + } else { + emit updateFinished(); + } + }, Qt::ConnectionType::QueuedConnection); - auto* thread_pool = QThreadPool::globalInstance(); - thread_pool->start(m_current_update_task.get()); + QThreadPool::globalInstance()->start(m_current_update_task.get()); return true; } @@ -246,10 +261,7 @@ void ResourceFolderModel::resolveResource(Resource::Ptr res) if (!task) return; - m_ticket_mutex.lock(); - int ticket = m_next_resolution_ticket; - m_next_resolution_ticket += 1; - m_ticket_mutex.unlock(); + int ticket = m_next_resolution_ticket.fetch_add(1); res->setResolving(true, ticket); m_active_parse_tasks.insert(ticket, task); @@ -261,8 +273,7 @@ void ResourceFolderModel::resolveResource(Resource::Ptr res) connect( task, &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection); - auto* thread_pool = QThreadPool::globalInstance(); - thread_pool->start(task); + QThreadPool::globalInstance()->start(task); } void ResourceFolderModel::onUpdateSucceeded() @@ -283,15 +294,6 @@ void ResourceFolderModel::onUpdateSucceeded() #endif applyUpdates(current_set, new_set, new_resources); - - m_current_update_task.reset(); - - if (m_scheduled_update) { - m_scheduled_update = false; - update(); - } else { - emit updateFinished(); - } } void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id) diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index e27b5db6..5652c156 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -24,6 +24,7 @@ class ResourceFolderModel : public QAbstractListModel { Q_OBJECT public: ResourceFolderModel(QDir, QObject* parent = nullptr); + ~ResourceFolderModel() override; /** Starts watching the paths for changes. * @@ -145,7 +146,7 @@ class ResourceFolderModel : public QAbstractListModel { * This task should load and parse all heavy info needed by a resource, such as parsing a manifest. It gets executed * in the background, so it slowly updates the UI as tasks get done. */ - [[nodiscard]] virtual Task* createParseTask(Resource const&) { return nullptr; }; + [[nodiscard]] virtual Task* createParseTask(Resource&) { return nullptr; }; /** Standard implementation of the model update logic. * @@ -197,8 +198,7 @@ class ResourceFolderModel : public QAbstractListModel { QMap<QString, int> m_resources_index; QMap<int, Task::Ptr> m_active_parse_tasks; - int m_next_resolution_ticket = 0; - QMutex m_ticket_mutex; + std::atomic<int> m_next_resolution_ticket = 0; }; /* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */ @@ -257,8 +257,11 @@ void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString> // If the resource is resolving, but something about it changed, we don't want to // continue the resolving. if (current_resource->isResolving()) { - auto task = (*m_active_parse_tasks.find(current_resource->resolutionTicket())).get(); - task->abort(); + auto ticket = current_resource->resolutionTicket(); + if (m_active_parse_tasks.contains(ticket)) { + auto task = (*m_active_parse_tasks.find(ticket)).get(); + task->abort(); + } } m_resources[row].reset(new_resource); @@ -285,8 +288,11 @@ void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString> Q_ASSERT(removed_set.contains(removed_it->get()->internal_id())); if ((*removed_it)->isResolving()) { - auto task = (*m_active_parse_tasks.find((*removed_it)->resolutionTicket())).get(); - task->abort(); + auto ticket = (*removed_it)->resolutionTicket(); + if (m_active_parse_tasks.contains(ticket)) { + auto task = (*m_active_parse_tasks.find(ticket)).get(); + task->abort(); + } } beginRemoveRows(QModelIndex(), removed_index, removed_index); diff --git a/launcher/minecraft/mod/ResourceFolderModel_test.cpp b/launcher/minecraft/mod/ResourceFolderModel_test.cpp index fe98552e..aa78e502 100644 --- a/launcher/minecraft/mod/ResourceFolderModel_test.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel_test.cpp @@ -58,7 +58,7 @@ QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); \ expire_timer.stop(); \ \ - disconnect(&model, nullptr, nullptr, nullptr); + disconnect(&model, nullptr, &loop, nullptr); class ResourceFolderModelTest : public QObject { @@ -146,14 +146,10 @@ slots: for (auto mod : model.allMods()) qDebug() << mod->name(); - QCOMPARE(model.size(), 2); + // FIXME: It considers every file in the directory as a mod, but we should probably filter that out somehow. + QCOMPARE(model.size(), 4); model.stopWatching(); - - while (model.hasPendingParseTasks()) { - QTest::qSleep(20); - QCoreApplication::processEvents(); - } } void test_removeResource() @@ -206,11 +202,6 @@ slots: qDebug() << "Removed second mod."; model.stopWatching(); - - while (model.hasPendingParseTasks()) { - QTest::qSleep(20); - QCoreApplication::processEvents(); - } } void test_enable_disable() @@ -262,11 +253,6 @@ slots: QVERIFY(!res_2.enable(initial_enabled_res_2 ? EnableAction::ENABLE : EnableAction::DISABLE)); QVERIFY(res_2.enabled() == initial_enabled_res_2); QVERIFY(res_2.internal_id() == id_2); - - while (model.hasPendingParseTasks()) { - QTest::qSleep(20); - QCoreApplication::processEvents(); - } } }; diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp new file mode 100644 index 00000000..3fc10a2f --- /dev/null +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -0,0 +1,116 @@ +#include "ResourcePack.h" + +#include <QDebug> +#include <QMap> +#include <QRegularExpression> + +#include "Version.h" + +#include "minecraft/mod/tasks/LocalResourcePackParseTask.h" + +// Values taken from: +// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta +static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = { + { 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } }, + { 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } }, + { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } }, + { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } }, + { 9, { Version("1.19"), Version("1.19.2") } }, +}; + +void ResourcePack::setPackFormat(int new_format_id) +{ + QMutexLocker locker(&m_data_lock); + + if (!s_pack_format_versions.contains(new_format_id)) { + qWarning() << "Pack format '%1' is not a recognized resource pack id!"; + } + + m_pack_format = new_format_id; +} + +void ResourcePack::setDescription(QString new_description) +{ + QMutexLocker locker(&m_data_lock); + + m_description = new_description; +} + +void ResourcePack::setImage(QImage new_image) +{ + QMutexLocker locker(&m_data_lock); + + Q_ASSERT(!new_image.isNull()); + + if (m_pack_image_cache_key.key.isValid()) + QPixmapCache::remove(m_pack_image_cache_key.key); + + m_pack_image_cache_key.key = QPixmapCache::insert(QPixmap::fromImage(new_image)); + m_pack_image_cache_key.was_ever_used = true; +} + +QPixmap ResourcePack::image(QSize size) +{ + QPixmap cached_image; + if (QPixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { + if (size.isNull()) + return cached_image; + return cached_image.scaled(size); + } + + // No valid image we can get + if (!m_pack_image_cache_key.was_ever_used) + return {}; + + // Imaged got evicted from the cache. Re-process it and retry. + ResourcePackUtils::process(*this); + return image(size); +} + +std::pair<Version, Version> ResourcePack::compatibleVersions() const +{ + if (!s_pack_format_versions.contains(m_pack_format)) { + return { {}, {} }; + } + + return s_pack_format_versions.constFind(m_pack_format).value(); +} + +std::pair<int, bool> ResourcePack::compare(const Resource& other, SortType type) const +{ + auto const& cast_other = static_cast<ResourcePack const&>(other); + + switch (type) { + default: { + auto res = Resource::compare(other, type); + if (res.first != 0) + return res; + } + case SortType::PACK_FORMAT: { + auto this_ver = packFormat(); + auto other_ver = cast_other.packFormat(); + + if (this_ver > other_ver) + return { 1, type == SortType::PACK_FORMAT }; + if (this_ver < other_ver) + return { -1, type == SortType::PACK_FORMAT }; + } + } + return { 0, false }; +} + +bool ResourcePack::applyFilter(QRegularExpression filter) const +{ + if (filter.match(description()).hasMatch()) + return true; + + if (filter.match(QString::number(packFormat())).hasMatch()) + return true; + + if (filter.match(compatibleVersions().first.toString()).hasMatch()) + return true; + if (filter.match(compatibleVersions().second.toString()).hasMatch()) + return true; + + return Resource::applyFilter(filter); +} diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h index c2cc8690..03121908 100644 --- a/launcher/minecraft/mod/ResourcePack.h +++ b/launcher/minecraft/mod/ResourcePack.h @@ -2,12 +2,68 @@ #include "Resource.h" +#include <QImage> +#include <QMutex> +#include <QPixmap> +#include <QPixmapCache> + +class Version; + +/* TODO: + * + * Store localized descriptions + * */ + class ResourcePack : public Resource { Q_OBJECT - public: + public: using Ptr = shared_qobject_ptr<Resource>; ResourcePack(QObject* parent = nullptr) : Resource(parent) {} ResourcePack(QFileInfo file_info) : Resource(file_info) {} + /** 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; + + /** Gets the description of the resource pack. */ + [[nodiscard]] QString description() const { return m_description; } + + /** Gets the image of the resource pack, converted to a QPixmap for drawing, and scaled to size. */ + [[nodiscard]] QPixmap image(QSize size); + + /** Thread-safe. */ + void setPackFormat(int new_format_id); + + /** Thread-safe. */ + void setDescription(QString new_description); + + /** Thread-safe. */ + void setImage(QImage new_image); + + [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override; + [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; + + protected: + mutable QMutex m_data_lock; + + /* The 'version' of a resource pack, as defined in the pack.mcmeta file. + * See https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta + */ + int m_pack_format = 0; + + /** The resource pack's description, as defined in the pack.mcmeta file. + */ + QString m_description; + + /** 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; + } m_pack_image_cache_key; }; diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index e92be894..f8a6c1cf 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -1,38 +1,151 @@ // SPDX-License-Identifier: GPL-3.0-only /* -* PolyMC - Minecraft Launcher -* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> -* -* 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. -*/ + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln <flowlnlnln@gmail.com> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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 "ResourcePackFolderModel.h" -ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {} +#include "Version.h" + +#include "minecraft/mod/tasks/BasicFolderLoadTask.h" +#include "minecraft/mod/tasks/LocalResourcePackParseTask.h" + +ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir) : ResourceFolderModel(QDir(dir)) +{ + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE }; +} + +QVariant ResourcePackFolderModel::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::ToolTipRole: { + if (column == PackFormatColumn) { + //: The string being explained by this is in the format: ID (Lower version - Upper version) + return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); + } + return m_resources[row]->internal_id(); + } + case Qt::CheckStateRole: + switch (column) { + case ActiveColumn: + return at(row)->enabled() ? Qt::Checked : Qt::Unchecked; + default: + return {}; + } + default: + return {}; + } +} + +QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) { + case Qt::DisplayRole: + switch (section) { + case ActiveColumn: + return QString(); + case NameColumn: + return tr("Name"); + case PackFormatColumn: + return tr("Pack Format"); + case DateColumn: + return tr("Last changed"); + default: + return {}; + } + + case Qt::ToolTipRole: + switch (section) { + case ActiveColumn: + return tr("Is the resource pack enabled? (Only valid for ZIPs)"); + case NameColumn: + return tr("The name of the resource pack."); + case PackFormatColumn: + //: The string being explained by this is in the format: ID (Lower version - Upper version) + return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); + case DateColumn: + return tr("The date and time this resource pack was last changed (or added)."); + default: + return {}; + } + default: + return {}; + } + return {}; +} + +int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const +{ + return NUM_COLUMNS; +} + +Task* ResourcePackFolderModel::createUpdateTask() +{ + return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return new ResourcePack(entry); }); +} + +Task* ResourcePackFolderModel::createParseTask(Resource& resource) +{ + return new LocalResourcePackParseTask(m_next_resolution_ticket, static_cast<ResourcePack&>(resource)); +} diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h index 1fe82867..cb620ce2 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.h +++ b/launcher/minecraft/mod/ResourcePackFolderModel.h @@ -8,7 +8,24 @@ class ResourcePackFolderModel : public ResourceFolderModel { Q_OBJECT public: + enum Columns + { + ActiveColumn = 0, + NameColumn, + PackFormatColumn, + DateColumn, + NUM_COLUMNS + }; + explicit ResourcePackFolderModel(const QString &dir); + [[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]] Task* createUpdateTask() override; + [[nodiscard]] Task* createParseTask(Resource&) override; + RESOURCE_HELPERS(ResourcePack) }; diff --git a/launcher/minecraft/mod/ResourcePackParse_test.cpp b/launcher/minecraft/mod/ResourcePackParse_test.cpp new file mode 100644 index 00000000..a49582d6 --- /dev/null +++ b/launcher/minecraft/mod/ResourcePackParse_test.cpp @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 <QTest> +#include <QTimer> + +#include "FileSystem.h" + +#include "ResourcePack.h" +#include "tasks/LocalResourcePackParseTask.h" + +class ResourcePackParseTest : public QObject { + Q_OBJECT + + private slots: + void test_parseZIP() + { + QString source = QFINDTESTDATA("testdata"); + + QString zip_rp = FS::PathCombine(source, "test_resource_pack_idk.zip"); + ResourcePack pack { QFileInfo(zip_rp) }; + + ResourcePackUtils::processZIP(pack); + + QVERIFY(pack.packFormat() == 3); + QVERIFY(pack.description() == "um dois, feijão com arroz, três quatro, feijão no prato, cinco seis, café inglês, sete oito, comer biscoito, nove dez comer pastéis!!"); + } + + void test_parseFolder() + { + QString source = QFINDTESTDATA("testdata"); + + QString folder_rp = FS::PathCombine(source, "test_folder"); + ResourcePack pack { QFileInfo(folder_rp) }; + + ResourcePackUtils::processFolder(pack); + + QVERIFY(pack.packFormat() == 1); + QVERIFY(pack.description() == "Some resource pack maybe"); + } + + void test_parseFolder2() + { + QString source = QFINDTESTDATA("testdata"); + + QString folder_rp = FS::PathCombine(source, "another_test_folder"); + ResourcePack pack { QFileInfo(folder_rp) }; + + ResourcePackUtils::process(pack); + + QVERIFY(pack.packFormat() == 6); + QVERIFY(pack.description() == "o quartel pegou fogo, policia deu sinal, acode acode acode a bandeira nacional"); + } +}; + +QTEST_GUILESS_MAIN(ResourcePackParseTest) + +#include "ResourcePackParse_test.moc" diff --git a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h index cc02a9b9..be0e752d 100644 --- a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h @@ -10,37 +10,46 @@ #include "tasks/Task.h" -/** Very simple task that just loads a folder's contents directly. +/** Very simple task that just loads a folder's contents directly. */ -class BasicFolderLoadTask : public Task -{ +class BasicFolderLoadTask : public Task { Q_OBJECT -public: + public: struct Result { QMap<QString, Resource::Ptr> resources; }; using ResultPtr = std::shared_ptr<Result>; - [[nodiscard]] ResultPtr result() const { - return m_result; - } + [[nodiscard]] ResultPtr result() const { return m_result; } -public: - BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result) {} + public: + BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result) + { + m_create_func = [](QFileInfo const& entry) -> Resource* { + return new Resource(entry); + }; + } + BasicFolderLoadTask(QDir dir, std::function<Resource*(QFileInfo const&)> create_function) + : Task(nullptr, false), m_dir(dir), m_result(new Result), m_create_func(std::move(create_function)) + {} [[nodiscard]] bool canAbort() const override { return true; } - bool abort() override { m_aborted = true; return true; } + bool abort() override + { + m_aborted.store(true); + return true; + } void executeTask() override { m_dir.refresh(); for (auto entry : m_dir.entryInfoList()) { - auto resource = new Resource(entry); + auto resource = m_create_func(entry); m_result->resources.insert(resource->internal_id(), resource); } if (m_aborted) - emitAborted(); + emit finished(); else emitSucceeded(); } @@ -49,5 +58,7 @@ private: QDir m_dir; ResultPtr m_result; - bool m_aborted = false; + std::atomic<bool> m_aborted = false; + + std::function<Resource*(QFileInfo const&)> m_create_func; }; diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index c486bd46..8a6e54d8 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -499,7 +499,7 @@ void LocalModParseTask::processAsLitemod() bool LocalModParseTask::abort() { - m_aborted = true; + m_aborted.store(true); return true; } @@ -521,7 +521,7 @@ void LocalModParseTask::executeTask() } if (m_aborted) - emitAborted(); + emit finished(); else emitSucceeded(); } diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h index 4bbf3c85..413eb2d1 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h @@ -39,5 +39,5 @@ private: QFileInfo m_modFile; ResultPtr m_result; - bool m_aborted = false; + std::atomic<bool> m_aborted = false; }; diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp new file mode 100644 index 00000000..4f87bc13 --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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 <quazip/quazip.h> +#include <quazip/quazipfile.h> + +#include <QCryptographicHash> + +namespace ResourcePackUtils { + +bool process(ResourcePack& pack) +{ + switch (pack.type()) { + case ResourceType::FOLDER: + ResourcePackUtils::processFolder(pack); + return true; + case ResourceType::ZIPFILE: + ResourcePackUtils::processZIP(pack); + return true; + default: + qWarning() << "Invalid type for resource pack parse task!"; + return false; + } +} + +void processFolder(ResourcePack& pack) +{ + Q_ASSERT(pack.type() == ResourceType::FOLDER); + + QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta")); + if (mcmeta_file_info.isFile()) { + QFile mcmeta_file(mcmeta_file_info.filePath()); + if (!mcmeta_file.open(QIODevice::ReadOnly)) + return; + + auto data = mcmeta_file.readAll(); + + ResourcePackUtils::processMCMeta(pack, std::move(data)); + + mcmeta_file.close(); + } + + QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); + if (image_file_info.isFile()) { + QFile mcmeta_file(image_file_info.filePath()); + if (!mcmeta_file.open(QIODevice::ReadOnly)) + return; + + auto data = mcmeta_file.readAll(); + + ResourcePackUtils::processPackPNG(pack, std::move(data)); + + mcmeta_file.close(); + } +} + +void processZIP(ResourcePack& pack) +{ + Q_ASSERT(pack.type() == ResourceType::ZIPFILE); + + QuaZip zip(pack.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("pack.mcmeta")) { + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file in zip."; + zip.close(); + return; + } + + auto data = file.readAll(); + + ResourcePackUtils::processMCMeta(pack, std::move(data)); + + file.close(); + } + + if (zip.setCurrentFile("pack.png")) { + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file in zip."; + zip.close(); + return; + } + + auto data = file.readAll(); + + ResourcePackUtils::processPackPNG(pack, std::move(data)); + + file.close(); + } + + zip.close(); +} + +// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta +void processMCMeta(ResourcePack& pack, QByteArray&& raw_data) +{ + try { + auto json_doc = QJsonDocument::fromJson(raw_data); + auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); + + pack.setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0)); + pack.setDescription(Json::ensureString(pack_obj, "description", "")); + } catch (Json::JsonException& e) { + qWarning() << "JsonException: " << e.what() << e.cause(); + } +} + +void processPackPNG(ResourcePack& pack, QByteArray&& raw_data) +{ + auto img = QImage::fromData(raw_data); + if (!img.isNull()) { + pack.setImage(img); + } else { + qWarning() << "Failed to parse pack.png."; + } +} +} // namespace ResourcePackUtils + +LocalResourcePackParseTask::LocalResourcePackParseTask(int token, ResourcePack& rp) + : Task(nullptr, false), m_token(token), m_resource_pack(rp) +{} + +bool LocalResourcePackParseTask::abort() +{ + m_aborted = true; + return true; +} + +void LocalResourcePackParseTask::executeTask() +{ + Q_ASSERT(m_resource_pack.valid()); + + if (!ResourcePackUtils::process(m_resource_pack)) + return; + + if (m_aborted) + emitAborted(); + else + emitSucceeded(); +} diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h new file mode 100644 index 00000000..d3c25464 --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - 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" + +#include "tasks/Task.h" + +namespace ResourcePackUtils { +bool process(ResourcePack& pack); + +void processZIP(ResourcePack& pack); +void processFolder(ResourcePack& pack); + +void processMCMeta(ResourcePack& pack, QByteArray&& raw_data); +void processPackPNG(ResourcePack& pack, QByteArray&& raw_data); +} // namespace ResourcePackUtils + +class LocalResourcePackParseTask : public Task { + Q_OBJECT + public: + LocalResourcePackParseTask(int token, ResourcePack& rp); + + [[nodiscard]] bool canAbort() const override { return true; } + bool abort() override; + + void executeTask() override; + + [[nodiscard]] int token() const { return m_token; } + + private: + int m_token; + + ResourcePack& m_resource_pack; + + bool m_aborted = false; +}; diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index a56ba8ab..3a857740 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -38,8 +38,8 @@ #include "minecraft/mod/MetadataHandler.h" -ModFolderLoadTask::ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan, QObject* parent) - : Task(parent, false), m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_clean_orphan(clean_orphan), m_result(new Result()) +ModFolderLoadTask::ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan) + : Task(nullptr, false), m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_clean_orphan(clean_orphan), m_result(new Result()) {} void ModFolderLoadTask::executeTask() @@ -96,7 +96,10 @@ void ModFolderLoadTask::executeTask() } } - emitSucceeded(); + if (m_aborted) + emit finished(); + else + emitSucceeded(); } void ModFolderLoadTask::getFromMetadata() diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h index 840e95e1..0f18b8b9 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h @@ -57,7 +57,15 @@ public: } public: - ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan = false, QObject* parent = nullptr); + ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan = false); + + [[nodiscard]] bool canAbort() const override { return true; } + bool abort() override + { + m_aborted.store(true); + return true; + } + void executeTask() override; @@ -69,4 +77,6 @@ private: bool m_is_indexed; bool m_clean_orphan; ResultPtr m_result; + + std::atomic<bool> m_aborted = false; }; diff --git a/launcher/minecraft/mod/testdata/another_test_folder/pack.mcmeta b/launcher/minecraft/mod/testdata/another_test_folder/pack.mcmeta new file mode 100644 index 00000000..d33a0e5d --- /dev/null +++ b/launcher/minecraft/mod/testdata/another_test_folder/pack.mcmeta @@ -0,0 +1,6 @@ +{
+ "pack": {
+ "pack_format": 6,
+ "description": "o quartel pegou fogo, policia deu sinal, acode acode acode a bandeira nacional"
+ }
+}
diff --git a/launcher/minecraft/mod/testdata/test_resource_pack_idk.zip b/launcher/minecraft/mod/testdata/test_resource_pack_idk.zip Binary files differnew file mode 100644 index 00000000..52b91cdc --- /dev/null +++ b/launcher/minecraft/mod/testdata/test_resource_pack_idk.zip |