aboutsummaryrefslogtreecommitdiff
path: root/launcher/minecraft
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/minecraft')
-rw-r--r--launcher/minecraft/mod/ModFolderModel.cpp13
-rw-r--r--launcher/minecraft/mod/ModFolderModel.h2
-rw-r--r--launcher/minecraft/mod/Resource.h2
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel.cpp36
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel.h20
-rw-r--r--launcher/minecraft/mod/ResourceFolderModel_test.cpp20
-rw-r--r--launcher/minecraft/mod/ResourcePack.cpp116
-rw-r--r--launcher/minecraft/mod/ResourcePack.h58
-rw-r--r--launcher/minecraft/mod/ResourcePackFolderModel.cpp179
-rw-r--r--launcher/minecraft/mod/ResourcePackFolderModel.h17
-rw-r--r--launcher/minecraft/mod/ResourcePackParse_test.cpp73
-rw-r--r--launcher/minecraft/mod/tasks/BasicFolderLoadTask.h37
-rw-r--r--launcher/minecraft/mod/tasks/LocalModParseTask.cpp4
-rw-r--r--launcher/minecraft/mod/tasks/LocalModParseTask.h2
-rw-r--r--launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp164
-rw-r--r--launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h56
-rw-r--r--launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp9
-rw-r--r--launcher/minecraft/mod/tasks/ModFolderLoadTask.h12
-rw-r--r--launcher/minecraft/mod/testdata/another_test_folder/pack.mcmeta6
-rw-r--r--launcher/minecraft/mod/testdata/test_resource_pack_idk.zipbin0 -> 322 bytes
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
new file mode 100644
index 00000000..52b91cdc
--- /dev/null
+++ b/launcher/minecraft/mod/testdata/test_resource_pack_idk.zip
Binary files differ