diff --git a/Source/Core/Common/CommonPaths.h b/Source/Core/Common/CommonPaths.h
index 7daeebe630..fe241c5f61 100644
--- a/Source/Core/Common/CommonPaths.h
+++ b/Source/Core/Common/CommonPaths.h
@@ -66,6 +66,7 @@
#define MEMORYWATCHER_DIR "MemoryWatcher"
#define WFSROOT_DIR "WFS"
#define BACKUP_DIR "Backup"
+#define RESOURCEPACK_DIR "ResourcePacks"
// This one is only used to remove it if it was present
#define SHADERCACHE_LEGACY_DIR "ShaderCache"
diff --git a/Source/Core/Common/FileUtil.cpp b/Source/Core/Common/FileUtil.cpp
index 2784d1ebda..fe622ac21a 100644
--- a/Source/Core/Common/FileUtil.cpp
+++ b/Source/Core/Common/FileUtil.cpp
@@ -780,6 +780,7 @@ static void RebuildUserDirectories(unsigned int dir_index)
s_user_paths[D_PIPES_IDX] = s_user_paths[D_USER_IDX] + PIPES_DIR DIR_SEP;
s_user_paths[D_WFSROOT_IDX] = s_user_paths[D_USER_IDX] + WFSROOT_DIR DIR_SEP;
s_user_paths[D_BACKUP_IDX] = s_user_paths[D_USER_IDX] + BACKUP_DIR DIR_SEP;
+ s_user_paths[D_RESOURCEPACK_IDX] = s_user_paths[D_USER_IDX] + RESOURCEPACK_DIR DIR_SEP;
s_user_paths[F_DOLPHINCONFIG_IDX] = s_user_paths[D_CONFIG_IDX] + DOLPHIN_CONFIG;
s_user_paths[F_GCPADCONFIG_IDX] = s_user_paths[D_CONFIG_IDX] + GCPAD_CONFIG;
s_user_paths[F_WIIPADCONFIG_IDX] = s_user_paths[D_CONFIG_IDX] + WIIPAD_CONFIG;
diff --git a/Source/Core/Common/FileUtil.h b/Source/Core/Common/FileUtil.h
index 8f4c3633eb..6adea032cd 100644
--- a/Source/Core/Common/FileUtil.h
+++ b/Source/Core/Common/FileUtil.h
@@ -51,6 +51,7 @@ enum
D_MEMORYWATCHER_IDX,
D_WFSROOT_IDX,
D_BACKUP_IDX,
+ D_RESOURCEPACK_IDX,
F_DOLPHINCONFIG_IDX,
F_GCPADCONFIG_IDX,
F_WIIPADCONFIG_IDX,
@@ -210,4 +211,4 @@ void OpenFStream(T& fstream, const std::string& filename, std::ios_base::openmod
#endif
}
-} // namespace
+} // namespace File
diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt
index 9b11cf3030..8e6deec6f8 100644
--- a/Source/Core/DolphinQt/CMakeLists.txt
+++ b/Source/Core/DolphinQt/CMakeLists.txt
@@ -108,6 +108,7 @@ add_executable(dolphin-emu
QtUtils/WinIconHelper.cpp
QtUtils/WrapInScrollArea.cpp
QtUtils/AspectRatioWidget.cpp
+ ResourcePackManager.cpp
Settings/AdvancedPane.cpp
Settings/AudioPane.cpp
Settings/GameCubePane.cpp
diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj
index b4125f8232..bd0346edd7 100644
--- a/Source/Core/DolphinQt/DolphinQt.vcxproj
+++ b/Source/Core/DolphinQt/DolphinQt.vcxproj
@@ -331,6 +331,7 @@
+
@@ -393,6 +394,7 @@
+
@@ -480,4 +482,4 @@
-
+
\ No newline at end of file
diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp
index 2d81e75267..e3c06d09eb 100644
--- a/Source/Core/DolphinQt/MainWindow.cpp
+++ b/Source/Core/DolphinQt/MainWindow.cpp
@@ -87,6 +87,7 @@
#include "DolphinQt/QtUtils/RunOnObject.h"
#include "DolphinQt/QtUtils/WindowActivationEventFilter.h"
#include "DolphinQt/RenderWidget.h"
+#include "DolphinQt/ResourcePackManager.h"
#include "DolphinQt/Resources.h"
#include "DolphinQt/SearchBar.h"
#include "DolphinQt/Settings.h"
@@ -99,6 +100,10 @@
#include "UICommon/DiscordPresence.h"
#include "UICommon/GameFile.h"
+#include "UICommon/ResourcePack/Manager.h"
+#include "UICommon/ResourcePack/Manifest.h"
+#include "UICommon/ResourcePack/ResourcePack.h"
+
#include "UICommon/UICommon.h"
#include "VideoCommon/VideoConfig.h"
@@ -208,6 +213,21 @@ MainWindow::MainWindow(std::unique_ptr boot_parameters) : QMainW
// Restoring of window states can sometimes go wrong, resulting in widgets being visible when they
// shouldn't be so we have to reapply all our rules afterwards.
Settings::Instance().RefreshWidgetVisibility();
+
+ if (!ResourcePack::Init())
+ QMessageBox::critical(this, tr("Error"), tr("Error occured while loading some texture packs"));
+
+ for (auto& pack : ResourcePack::GetPacks())
+ {
+ if (!pack.IsValid())
+ {
+ QMessageBox::critical(this, tr("Error"),
+ tr("Invalid Pack %1 provided: %2")
+ .arg(QString::fromStdString(pack.GetPath()))
+ .arg(QString::fromStdString(pack.GetError())));
+ return;
+ }
+ }
}
MainWindow::~MainWindow()
@@ -393,6 +413,8 @@ void MainWindow::ConnectMenuBar()
// Tools
connect(m_menu_bar, &MenuBar::ShowMemcardManager, this, &MainWindow::ShowMemcardManager);
+ connect(m_menu_bar, &MenuBar::ShowResourcePackManager, this,
+ &MainWindow::ShowResourcePackManager);
connect(m_menu_bar, &MenuBar::ShowCheatsManager, this, &MainWindow::ShowCheatsManager);
connect(m_menu_bar, &MenuBar::BootGameCubeIPL, this, &MainWindow::OnBootGameCubeIPL);
connect(m_menu_bar, &MenuBar::ImportNANDBackup, this, &MainWindow::OnImportNANDBackup);
@@ -1556,6 +1578,13 @@ void MainWindow::ShowMemcardManager()
manager.exec();
}
+void MainWindow::ShowResourcePackManager()
+{
+ ResourcePackManager manager(this);
+
+ manager.exec();
+}
+
void MainWindow::ShowCheatsManager()
{
m_cheats_manager->show();
diff --git a/Source/Core/DolphinQt/MainWindow.h b/Source/Core/DolphinQt/MainWindow.h
index 937e3beb7c..30c29030db 100644
--- a/Source/Core/DolphinQt/MainWindow.h
+++ b/Source/Core/DolphinQt/MainWindow.h
@@ -131,6 +131,7 @@ private:
void ShowNetPlaySetupDialog();
void ShowFIFOPlayer();
void ShowMemcardManager();
+ void ShowResourcePackManager();
void ShowCheatsManager();
void NetPlayInit();
diff --git a/Source/Core/DolphinQt/MenuBar.cpp b/Source/Core/DolphinQt/MenuBar.cpp
index 1c67569af6..2096339a5b 100644
--- a/Source/Core/DolphinQt/MenuBar.cpp
+++ b/Source/Core/DolphinQt/MenuBar.cpp
@@ -208,6 +208,9 @@ void MenuBar::AddToolsMenu()
m_show_cheat_manager =
tools_menu->addAction(tr("&Cheats Manager"), this, [this] { emit ShowCheatsManager(); });
+ tools_menu->addAction(tr("&Resource Pack Manager"), this,
+ [this] { emit ShowResourcePackManager(); });
+
connect(&Settings::Instance(), &Settings::EnableCheatsChanged, [this](bool enabled) {
m_show_cheat_manager->setEnabled(Core::GetState() != Core::State::Uninitialized && enabled);
});
diff --git a/Source/Core/DolphinQt/MenuBar.h b/Source/Core/DolphinQt/MenuBar.h
index 57691e17c7..6b4a15e5aa 100644
--- a/Source/Core/DolphinQt/MenuBar.h
+++ b/Source/Core/DolphinQt/MenuBar.h
@@ -78,6 +78,7 @@ signals:
void ShowFIFOPlayer();
void ShowAboutDialog();
void ShowCheatsManager();
+ void ShowResourcePackManager();
void ConnectWiiRemote(int id);
// Options
diff --git a/Source/Core/DolphinQt/ResourcePackManager.cpp b/Source/Core/DolphinQt/ResourcePackManager.cpp
new file mode 100644
index 0000000000..2f7287afd0
--- /dev/null
+++ b/Source/Core/DolphinQt/ResourcePackManager.cpp
@@ -0,0 +1,325 @@
+// Copyright 2018 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#include "DolphinQt/ResourcePackManager.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "Common/FileUtil.h"
+#include "UICommon/ResourcePack/Manager.h"
+
+ResourcePackManager::ResourcePackManager(QWidget* widget) : QDialog(widget)
+{
+ CreateWidgets();
+ ConnectWidgets();
+ RepopulateTable();
+
+ setWindowTitle(tr("Resource Pack Manager"));
+ setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
+
+ resize(QSize(900, 600));
+}
+
+void ResourcePackManager::CreateWidgets()
+{
+ auto* layout = new QGridLayout;
+
+ m_table_widget = new QTableWidget;
+
+ m_open_directory_button = new QPushButton(tr("Open Directory..."));
+ m_change_button = new QPushButton(tr("Install"));
+ m_remove_button = new QPushButton(tr("Remove"));
+ m_refresh_button = new QPushButton(tr("Refresh"));
+ m_priority_up_button = new QPushButton(tr("Up"));
+ m_priority_down_button = new QPushButton(tr("Down"));
+
+ auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok);
+
+ connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
+
+ layout->addWidget(m_table_widget, 0, 0, 7, 1);
+ layout->addWidget(m_open_directory_button, 0, 1);
+ layout->addWidget(m_change_button, 1, 1);
+ layout->addWidget(m_remove_button, 2, 1);
+ layout->addWidget(m_refresh_button, 3, 1);
+ layout->addWidget(m_priority_up_button, 4, 1);
+ layout->addWidget(m_priority_down_button, 5, 1);
+
+ layout->addWidget(buttons, 7, 1, Qt::AlignRight);
+ setLayout(layout);
+ setLayout(layout);
+}
+
+void ResourcePackManager::ConnectWidgets()
+{
+ connect(m_open_directory_button, &QPushButton::pressed, this,
+ &ResourcePackManager::OpenResourcePackDir);
+ connect(m_refresh_button, &QPushButton::pressed, this, &ResourcePackManager::Refresh);
+ connect(m_change_button, &QPushButton::pressed, this, &ResourcePackManager::Change);
+ connect(m_remove_button, &QPushButton::pressed, this, &ResourcePackManager::Remove);
+ connect(m_priority_up_button, &QPushButton::pressed, this, &ResourcePackManager::PriorityUp);
+ connect(m_priority_down_button, &QPushButton::pressed, this, &ResourcePackManager::PriorityDown);
+
+ connect(m_table_widget, &QTableWidget::itemSelectionChanged, this,
+ &ResourcePackManager::SelectionChanged);
+
+ connect(m_table_widget, &QTableWidget::itemDoubleClicked, this,
+ &ResourcePackManager::ItemDoubleClicked);
+}
+
+void ResourcePackManager::OpenResourcePackDir()
+{
+ QDesktopServices::openUrl(
+ QUrl::fromLocalFile(QString::fromStdString(File::GetUserPath(D_RESOURCEPACK_IDX))));
+}
+
+void ResourcePackManager::RepopulateTable()
+{
+ m_table_widget->clear();
+ m_table_widget->setColumnCount(6);
+
+ m_table_widget->setHorizontalHeaderLabels({QStringLiteral(""), tr("Name"), tr("Version"),
+ tr("Description"), tr("Author"), tr("Website")});
+
+ auto* header = m_table_widget->horizontalHeader();
+
+ for (int i = 0; i < 4; i++)
+ header->setSectionResizeMode(i, QHeaderView::ResizeToContents);
+
+ header->setStretchLastSection(true);
+
+ int size = static_cast(ResourcePack::GetPacks().size());
+
+ m_table_widget->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_table_widget->setSelectionMode(QAbstractItemView::SingleSelection);
+
+ m_table_widget->setRowCount(size);
+ m_table_widget->setIconSize(QSize(32, 32));
+
+ for (int i = 0; i < size; i++)
+ {
+ const auto& pack = ResourcePack::GetPacks()[size - 1 - i];
+ auto* manifest = pack.GetManifest();
+
+ auto* logo_item = new QTableWidgetItem;
+ auto* name_item = new QTableWidgetItem(QString::fromStdString(manifest->GetName()));
+ auto* version_item = new QTableWidgetItem(QString::fromStdString(manifest->GetVersion()));
+ auto* author_item = new QTableWidgetItem(
+ QString::fromStdString(manifest->GetAuthors().value_or("Unknown author")));
+ auto* description_item =
+ new QTableWidgetItem(QString::fromStdString(manifest->GetDescription().value_or("")));
+ auto* website_item =
+ new QTableWidgetItem(QString::fromStdString(manifest->GetWebsite().value_or("")));
+
+ QPixmap logo;
+
+ logo.loadFromData(reinterpret_cast(pack.GetLogo().data()),
+ (int)pack.GetLogo().size());
+
+ logo_item->setIcon(QIcon(logo));
+
+ QFont link_font = website_item->font();
+
+ link_font.setUnderline(true);
+
+ website_item->setFont(link_font);
+ website_item->setForeground(QBrush(Qt::blue));
+ website_item->setData(Qt::UserRole, website_item->text());
+
+ for (auto* item :
+ {logo_item, name_item, version_item, author_item, description_item, website_item})
+ {
+ item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
+
+ if (ResourcePack::IsInstalled(pack))
+ {
+ item->setBackgroundColor(QColor(Qt::green));
+
+ auto font = item->font();
+ font.setBold(true);
+ item->setFont(font);
+ }
+ }
+
+ m_table_widget->setItem(i, 0, logo_item);
+ m_table_widget->setItem(i, 1, name_item);
+ m_table_widget->setItem(i, 2, version_item);
+ m_table_widget->setItem(i, 3, description_item);
+ m_table_widget->setItem(i, 4, author_item);
+ m_table_widget->setItem(i, 5, website_item);
+ }
+
+ SelectionChanged();
+}
+
+void ResourcePackManager::Change()
+{
+ auto items = m_table_widget->selectedItems();
+
+ if (items.empty())
+ return;
+
+ if (ResourcePack::IsInstalled(ResourcePack::GetPacks()[items[0]->row()]))
+ Uninstall();
+ else
+ Install();
+}
+
+void ResourcePackManager::Install()
+{
+ auto items = m_table_widget->selectedItems();
+
+ if (items.empty())
+ return;
+
+ auto& item = ResourcePack::GetPacks()[m_table_widget->rowCount() - 1 - items[0]->row()];
+
+ bool success = item.Install(File::GetUserPath(D_USER_IDX));
+
+ if (!success)
+ {
+ QMessageBox::critical(
+ this, tr("Error"),
+ tr("Failed to install pack: %1").arg(QString::fromStdString(item.GetError())));
+ }
+
+ RepopulateTable();
+}
+
+void ResourcePackManager::Uninstall()
+{
+ auto items = m_table_widget->selectedItems();
+
+ if (items.empty())
+ return;
+
+ auto& item = ResourcePack::GetPacks()[m_table_widget->rowCount() - 1 - items[0]->row()];
+
+ bool success = item.Uninstall(File::GetUserPath(D_USER_IDX));
+
+ if (!success)
+ {
+ QMessageBox::critical(
+ this, tr("Error"),
+ tr("Failed to uninstall pack: %1").arg(QString::fromStdString(item.GetError())));
+ }
+
+ RepopulateTable();
+}
+
+void ResourcePackManager::Remove()
+{
+ auto items = m_table_widget->selectedItems();
+
+ if (items.empty())
+ return;
+
+ QMessageBox box(this);
+ box.setWindowTitle(tr("Confirmation"));
+ box.setText(tr("Are you sure you want to delete this pack?"));
+ box.setIcon(QMessageBox::Warning);
+ box.setStandardButtons(QMessageBox::Yes | QMessageBox::Abort);
+
+ if (box.exec() != QMessageBox::Yes)
+ return;
+
+ Uninstall();
+ File::Delete(
+ ResourcePack::GetPacks()[m_table_widget->rowCount() - 1 - items[0]->row()].GetPath());
+ RepopulateTable();
+}
+
+void ResourcePackManager::PriorityDown()
+{
+ auto items = m_table_widget->selectedItems();
+
+ if (items.empty())
+ return;
+
+ int row = m_table_widget->rowCount() - 1 - items[0]->row();
+
+ if (items[0]->row() >= m_table_widget->rowCount())
+ return;
+
+ auto& pack = ResourcePack::GetPacks()[row];
+ std::string path = pack.GetPath();
+
+ row--;
+
+ ResourcePack::Remove(pack);
+ ResourcePack::Add(path, row);
+
+ RepopulateTable();
+
+ m_table_widget->selectRow(row == 0 ? m_table_widget->rowCount() - 1 : row);
+}
+
+void ResourcePackManager::PriorityUp()
+{
+ auto items = m_table_widget->selectedItems();
+
+ if (items.empty())
+ return;
+
+ int row = m_table_widget->rowCount() - 1 - items[0]->row();
+
+ if (items[0]->row() == 0)
+ return;
+
+ auto& pack = ResourcePack::GetPacks()[row];
+ std::string path = pack.GetPath();
+
+ row++;
+
+ ResourcePack::Remove(pack);
+ ResourcePack::Add(path, items[0]->row() == m_table_widget->rowCount() ? -1 : row);
+
+ RepopulateTable();
+
+ m_table_widget->selectRow(row == m_table_widget->rowCount() - 1 ? 0 : row);
+}
+
+void ResourcePackManager::Refresh()
+{
+ ResourcePack::Init();
+ RepopulateTable();
+}
+
+void ResourcePackManager::SelectionChanged()
+{
+ auto items = m_table_widget->selectedItems();
+
+ const bool has_selection = !items.empty();
+
+ if (has_selection)
+ {
+ m_change_button->setText(ResourcePack::IsInstalled(ResourcePack::GetPacks()[items[0]->row()]) ?
+ tr("Uninstall") :
+ tr("Install"));
+ }
+
+ for (auto* item : {m_change_button, m_remove_button})
+ item->setEnabled(has_selection);
+
+ m_priority_down_button->setEnabled(has_selection &&
+ items[0]->row() < m_table_widget->rowCount() - 1);
+ m_priority_up_button->setEnabled(has_selection && items[0]->row() != 0);
+}
+
+void ResourcePackManager::ItemDoubleClicked(QTableWidgetItem* item)
+{
+ auto item_data = item->data(Qt::UserRole);
+
+ if (item_data.isNull())
+ return;
+
+ QDesktopServices::openUrl(QUrl(item_data.toString()));
+}
diff --git a/Source/Core/DolphinQt/ResourcePackManager.h b/Source/Core/DolphinQt/ResourcePackManager.h
new file mode 100644
index 0000000000..8d0d848f2c
--- /dev/null
+++ b/Source/Core/DolphinQt/ResourcePackManager.h
@@ -0,0 +1,42 @@
+// Copyright 2018 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include
+
+class QPushButton;
+class QTableWidget;
+class QTableWidgetItem;
+
+class ResourcePackManager : public QDialog
+{
+public:
+ explicit ResourcePackManager(QWidget* parent = nullptr);
+
+private:
+ void CreateWidgets();
+ void ConnectWidgets();
+ void OpenResourcePackDir();
+ void RepopulateTable();
+ void Change();
+ void Install();
+ void Uninstall();
+ void Remove();
+ void PriorityUp();
+ void PriorityDown();
+ void Refresh();
+
+ void SelectionChanged();
+ void ItemDoubleClicked(QTableWidgetItem* item);
+
+ QPushButton* m_open_directory_button;
+ QPushButton* m_change_button;
+ QPushButton* m_remove_button;
+ QPushButton* m_refresh_button;
+ QPushButton* m_priority_up_button;
+ QPushButton* m_priority_down_button;
+
+ QTableWidget* m_table_widget;
+};
diff --git a/Source/Core/UICommon/CMakeLists.txt b/Source/Core/UICommon/CMakeLists.txt
index f5ef5d6f11..0c665a3f20 100644
--- a/Source/Core/UICommon/CMakeLists.txt
+++ b/Source/Core/UICommon/CMakeLists.txt
@@ -5,6 +5,9 @@ add_library(uicommon
DiscordPresence.cpp
GameFile.cpp
GameFileCache.cpp
+ ResourcePack/Manager.cpp
+ ResourcePack/Manifest.cpp
+ ResourcePack/ResourcePack.cpp
UICommon.cpp
USBUtils.cpp
VideoUtils.cpp
@@ -14,6 +17,7 @@ target_link_libraries(uicommon
PUBLIC
common
cpp-optparse
+ minizip
PRIVATE
$<$:${IOK_LIBRARY}>
diff --git a/Source/Core/UICommon/ResourcePack/Manager.cpp b/Source/Core/UICommon/ResourcePack/Manager.cpp
new file mode 100644
index 0000000000..ce739536d4
--- /dev/null
+++ b/Source/Core/UICommon/ResourcePack/Manager.cpp
@@ -0,0 +1,185 @@
+// Copyright 2018 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#include "UICommon/ResourcePack/Manager.h"
+
+#include "Common/CommonTypes.h"
+#include "Common/FileSearch.h"
+#include "Common/FileUtil.h"
+#include "Common/IniFile.h"
+
+#include
+
+namespace
+{
+std::vector packs;
+
+std::string packs_path;
+} // namespace
+
+namespace ResourcePack
+{
+IniFile GetPackConfig()
+{
+ packs_path = File::GetUserPath(D_RESOURCEPACK_IDX) + "/Packs.ini";
+
+ IniFile file;
+ file.Load(packs_path);
+
+ return file;
+}
+
+bool Init()
+{
+ packs.clear();
+ auto pack_list = Common::DoFileSearch({File::GetUserPath(D_RESOURCEPACK_IDX)}, {".zip"});
+
+ bool error = false;
+
+ IniFile file = GetPackConfig();
+
+ auto* order = file.GetOrCreateSection("Order");
+
+ std::sort(pack_list.begin(), pack_list.end(), [order](std::string& a, std::string& b) {
+ std::string order_a = a, order_b = b;
+
+ order->Get(ResourcePack(a).GetManifest()->GetID(), &order_a);
+ order->Get(ResourcePack(b).GetManifest()->GetID(), &order_b);
+
+ return order_a < order_b;
+ });
+
+ for (size_t i = 0; i < pack_list.size(); i++)
+ {
+ const auto& path = pack_list[i];
+
+ error |= !Add(path);
+
+ order->Set(packs[i].GetManifest()->GetID(), static_cast(i));
+ }
+
+ file.Save(packs_path);
+
+ return !error;
+}
+
+std::vector& GetPacks()
+{
+ return packs;
+}
+
+std::vector GetLowerPriorityPacks(ResourcePack& pack)
+{
+ std::vector list;
+ for (auto it = std::find(packs.begin(), packs.end(), pack) + 1; it != packs.end(); it++)
+ {
+ auto& entry = *it;
+ if (!IsInstalled(pack))
+ continue;
+
+ list.push_back(&entry);
+ }
+
+ return list;
+}
+
+std::vector GetHigherPriorityPacks(ResourcePack& pack)
+{
+ std::vector list;
+ auto end = std::find(packs.begin(), packs.end(), pack);
+
+ for (auto it = packs.begin(); it != end; it++)
+ {
+ auto& entry = *it;
+ if (!IsInstalled(entry))
+ continue;
+ list.push_back(&entry);
+ }
+
+ return list;
+}
+
+bool Add(const std::string& path, int offset)
+{
+ if (offset == -1)
+ offset = static_cast(packs.size());
+
+ ResourcePack pack(path);
+
+ IniFile file = GetPackConfig();
+
+ auto* order = file.GetOrCreateSection("Order");
+
+ order->Set(pack.GetManifest()->GetID(), offset);
+
+ for (int i = offset; i < static_cast(packs.size()); i++)
+ order->Set(packs[i].GetManifest()->GetID(), i + 1);
+
+ file.Save(packs_path);
+
+ packs.insert(packs.begin() + offset, std::move(pack));
+
+ return pack.IsValid();
+}
+
+bool Remove(ResourcePack& pack)
+{
+ const auto result = pack.Uninstall(File::GetUserPath(D_USER_IDX));
+
+ if (!result)
+ return false;
+
+ auto pack_iterator = std::find(packs.begin(), packs.end(), pack);
+
+ if (pack_iterator == packs.end())
+ return false;
+
+ std::string filename;
+
+ IniFile file = GetPackConfig();
+
+ auto* order = file.GetOrCreateSection("Order");
+
+ order->Delete(pack.GetManifest()->GetID());
+
+ int offset = pack_iterator - packs.begin();
+
+ for (int i = offset + 1; i < static_cast(packs.size()); i++)
+ order->Set(packs[i].GetManifest()->GetID(), i - 1);
+
+ file.Save(packs_path);
+
+ packs.erase(pack_iterator);
+
+ return true;
+}
+
+void SetInstalled(const ResourcePack& pack, bool installed)
+{
+ IniFile file = GetPackConfig();
+
+ auto* install = file.GetOrCreateSection("Installed");
+
+ if (installed)
+ install->Set(pack.GetManifest()->GetID(), installed);
+ else
+ install->Delete(pack.GetManifest()->GetID());
+
+ file.Save(packs_path);
+}
+
+bool IsInstalled(const ResourcePack& pack)
+{
+ IniFile file = GetPackConfig();
+
+ auto* install = file.GetOrCreateSection("Installed");
+
+ bool installed;
+
+ install->Get(pack.GetManifest()->GetID(), &installed, false);
+
+ return installed;
+}
+
+} // namespace ResourcePack
diff --git a/Source/Core/UICommon/ResourcePack/Manager.h b/Source/Core/UICommon/ResourcePack/Manager.h
new file mode 100644
index 0000000000..c0d646a88d
--- /dev/null
+++ b/Source/Core/UICommon/ResourcePack/Manager.h
@@ -0,0 +1,25 @@
+// Copyright 2018 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include
+#include
+
+#include "UICommon/ResourcePack/ResourcePack.h"
+
+namespace ResourcePack
+{
+bool Init();
+
+bool Add(const std::string& path, int offset = -1);
+bool Remove(ResourcePack& pack);
+void SetInstalled(const ResourcePack& pack, bool installed);
+bool IsInstalled(const ResourcePack& pack);
+
+std::vector& GetPacks();
+
+std::vector GetHigherPriorityPacks(ResourcePack& pack);
+std::vector GetLowerPriorityPacks(ResourcePack& pack);
+} // namespace ResourcePack
diff --git a/Source/Core/UICommon/ResourcePack/Manifest.cpp b/Source/Core/UICommon/ResourcePack/Manifest.cpp
new file mode 100644
index 0000000000..37101b2639
--- /dev/null
+++ b/Source/Core/UICommon/ResourcePack/Manifest.cpp
@@ -0,0 +1,102 @@
+// Copyright 2018 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#include "UICommon/ResourcePack/Manifest.h"
+
+#include
+
+namespace ResourcePack
+{
+Manifest::Manifest(const std::string& json)
+{
+ picojson::value out;
+ auto error = picojson::parse(out, json);
+
+ if (!error.empty())
+ {
+ m_error = "Failed to parse manifest.";
+ m_valid = false;
+ return;
+ }
+
+ // Required fields
+ picojson::value& name = out.get("name");
+ picojson::value& version = out.get("version");
+ picojson::value& id = out.get("id");
+
+ // Optional fields
+ picojson::value& authors = out.get("authors");
+ picojson::value& description = out.get("description");
+ picojson::value& website = out.get("website");
+
+ if (!name.is() || !id.is() || !version.is())
+ {
+ m_error = "Some objects have a bad type.";
+ m_valid = false;
+ return;
+ }
+
+ m_name = name.to_str();
+ m_version = version.to_str();
+ m_id = id.to_str();
+
+ if (authors.is())
+ {
+ std::string author_list;
+ for (const auto& o : authors.get())
+ {
+ author_list += o.to_str() + ", ";
+ }
+
+ if (!author_list.empty())
+ m_authors = author_list.substr(0, author_list.size() - 2);
+ }
+
+ if (description.is())
+ m_description = description.to_str();
+
+ if (website.is())
+ m_website = website.to_str();
+}
+
+bool Manifest::IsValid() const
+{
+ return m_valid;
+}
+
+const std::string& Manifest::GetName() const
+{
+ return m_name;
+}
+
+const std::string& Manifest::GetVersion() const
+{
+ return m_version;
+}
+
+const std::string& Manifest::GetID() const
+{
+ return m_id;
+}
+
+const std::string& Manifest::GetError() const
+{
+ return m_error;
+}
+
+const std::optional& Manifest::GetAuthors() const
+{
+ return m_authors;
+}
+
+const std::optional& Manifest::GetDescription() const
+{
+ return m_description;
+}
+
+const std::optional& Manifest::GetWebsite() const
+{
+ return m_website;
+}
+} // namespace ResourcePack
diff --git a/Source/Core/UICommon/ResourcePack/Manifest.h b/Source/Core/UICommon/ResourcePack/Manifest.h
new file mode 100644
index 0000000000..0c3622c9eb
--- /dev/null
+++ b/Source/Core/UICommon/ResourcePack/Manifest.h
@@ -0,0 +1,40 @@
+// Copyright 2018 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include
+#include
+
+namespace ResourcePack
+{
+class Manifest
+{
+public:
+ explicit Manifest(const std::string& text);
+
+ bool IsValid() const;
+
+ const std::string& GetName() const;
+ const std::string& GetVersion() const;
+ const std::string& GetID() const;
+ const std::string& GetError() const;
+
+ const std::optional& GetAuthors() const;
+ const std::optional& GetDescription() const;
+ const std::optional& GetWebsite() const;
+
+private:
+ bool m_valid = true;
+
+ std::string m_name;
+ std::string m_version;
+ std::string m_id;
+ std::string m_error;
+
+ std::optional m_authors;
+ std::optional m_description;
+ std::optional m_website;
+};
+} // namespace ResourcePack
diff --git a/Source/Core/UICommon/ResourcePack/ResourcePack.cpp b/Source/Core/UICommon/ResourcePack/ResourcePack.cpp
new file mode 100644
index 0000000000..6f20c59ca9
--- /dev/null
+++ b/Source/Core/UICommon/ResourcePack/ResourcePack.cpp
@@ -0,0 +1,326 @@
+// Copyright 2018 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#include "UICommon/ResourcePack/ResourcePack.h"
+
+#include
+
+#include
+
+#include "Common/FileSearch.h"
+#include "Common/FileUtil.h"
+#include "Common/StringUtil.h"
+
+#include "UICommon/ResourcePack/Manager.h"
+#include "UICommon/ResourcePack/Manifest.h"
+
+static const char* TEXTURE_PATH = "Load/Textures/";
+
+namespace ResourcePack
+{
+// Since minzip doesn't provide a way to unzip a file of a length > 65535, we have to implement
+// this ourselves
+static bool ReadCurrentFileUnlimited(unzFile file, std::vector& destination)
+{
+ const uint32_t MAX_BUFFER_SIZE = 65535;
+
+ if (unzOpenCurrentFile(file) != UNZ_OK)
+ return false;
+
+ uint32_t bytes_to_go = static_cast(destination.size());
+
+ while (bytes_to_go > 0)
+ {
+ int bytes_read = unzReadCurrentFile(file, &destination[destination.size() - bytes_to_go],
+ std::min(bytes_to_go, MAX_BUFFER_SIZE));
+
+ if (bytes_read < 0)
+ {
+ unzCloseCurrentFile(file);
+ return false;
+ }
+
+ bytes_to_go -= bytes_read;
+ }
+
+ unzCloseCurrentFile(file);
+
+ return true;
+}
+
+ResourcePack::ResourcePack(const std::string& path) : m_path(path)
+{
+ auto file = unzOpen(path.c_str());
+
+ if (file == nullptr)
+ {
+ m_valid = false;
+ m_error = "Failed to open resource pack";
+ return;
+ }
+
+ if (unzLocateFile(file, "manifest.json", 0) == UNZ_END_OF_LIST_OF_FILE)
+ {
+ m_valid = false;
+ m_error = "Resource pack is missing a manifest.";
+ return;
+ }
+
+ unz_file_info manifest_info;
+
+ unzGetCurrentFileInfo(file, &manifest_info, nullptr, 0, nullptr, 0, nullptr, 0);
+
+ std::vector manifest_contents;
+
+ manifest_contents.resize(manifest_info.uncompressed_size);
+
+ if (!ReadCurrentFileUnlimited(file, manifest_contents))
+ {
+ m_valid = false;
+ m_error = "Failed to read manifest.json";
+ return;
+ }
+
+ unzCloseCurrentFile(file);
+
+ m_manifest =
+ std::make_shared(std::string(manifest_contents.begin(), manifest_contents.end()));
+
+ if (!m_manifest->IsValid())
+ {
+ m_valid = false;
+ m_error = "Manifest error: " + m_manifest->GetError();
+ return;
+ }
+
+ if (unzLocateFile(file, "logo.png", 0) != UNZ_END_OF_LIST_OF_FILE)
+ {
+ unz_file_info logo_info;
+
+ unzGetCurrentFileInfo(file, &logo_info, nullptr, 0, nullptr, 0, nullptr, 0);
+
+ m_logo_data.resize(logo_info.uncompressed_size);
+
+ if (!ReadCurrentFileUnlimited(file, m_logo_data))
+ {
+ m_valid = false;
+ m_error = "Failed to read logo.png";
+ return;
+ }
+ }
+
+ unzGoToFirstFile(file);
+
+ do
+ {
+ std::string filename;
+
+ filename.resize(256);
+
+ unz_file_info texture_info;
+
+ unzGetCurrentFileInfo(file, &texture_info, &filename[0], static_cast(filename.size()),
+ nullptr, 0, nullptr, 0);
+
+ if (filename.compare(0, 9, "textures/") != 0 || texture_info.uncompressed_size == 0)
+ continue;
+
+ // If a texture is compressed, abort.
+ if (texture_info.compression_method != 0)
+ {
+ m_valid = false;
+ m_error = "Texture " + filename + " is compressed!";
+ return;
+ }
+
+ m_textures.push_back(filename.substr(9));
+ } while (unzGoToNextFile(file) != UNZ_END_OF_LIST_OF_FILE);
+
+ unzClose(file);
+}
+
+bool ResourcePack::IsValid() const
+{
+ return m_valid;
+}
+
+const std::vector& ResourcePack::GetLogo() const
+{
+ return m_logo_data;
+}
+
+const std::string& ResourcePack::GetPath() const
+{
+ return m_path;
+}
+
+const std::string& ResourcePack::GetError() const
+{
+ return m_error;
+}
+
+const Manifest* ResourcePack::GetManifest() const
+{
+ return m_manifest.get();
+}
+
+const std::vector& ResourcePack::GetTextures() const
+{
+ return m_textures;
+}
+
+bool ResourcePack::Install(const std::string& path)
+{
+ if (!IsValid())
+ {
+ m_error = "Invalid pack";
+ return false;
+ }
+
+ auto file = unzOpen(m_path.c_str());
+
+ for (const auto& texture : m_textures)
+ {
+ bool provided_by_other_pack = false;
+
+ // Check if a higher priority pack already provides a given texture, don't overwrite it
+ for (const auto& pack : GetHigherPriorityPacks(*this))
+ {
+ if (std::find(pack->GetTextures().begin(), pack->GetTextures().end(), texture) !=
+ pack->GetTextures().end())
+ {
+ provided_by_other_pack = true;
+ break;
+ }
+ }
+
+ if (provided_by_other_pack)
+ continue;
+
+ if (unzLocateFile(file, ("textures/" + texture).c_str(), 0) != UNZ_OK)
+ {
+ m_error = "Failed to locate texture " + texture;
+ return false;
+ }
+
+ std::string m_full_dir;
+
+ SplitPath(path + TEXTURE_PATH + texture, &m_full_dir, nullptr, nullptr);
+
+ if (!File::CreateFullPath(m_full_dir))
+ {
+ m_error = "Failed to create full path " + m_full_dir;
+ return false;
+ }
+
+ unz_file_info texture_info;
+
+ unzGetCurrentFileInfo(file, &texture_info, nullptr, 0, nullptr, 0, nullptr, 0);
+
+ std::vector data;
+ data.resize(texture_info.uncompressed_size);
+
+ if (!ReadCurrentFileUnlimited(file, data))
+ {
+ m_error = "Failed to read texture " + texture;
+ return false;
+ }
+
+ std::ofstream out(path + TEXTURE_PATH + texture, std::ios::trunc | std::ios::binary);
+
+ if (!out.good())
+ {
+ m_error = "Failed to write " + texture;
+ return false;
+ }
+
+ out.write(data.data(), data.size());
+ out.flush();
+ }
+
+ unzClose(file);
+
+ SetInstalled(*this, true);
+
+ return true;
+}
+
+bool ResourcePack::Uninstall(const std::string& path)
+{
+ if (!IsValid())
+ {
+ m_error = "Invalid pack";
+ return false;
+ }
+
+ auto lower = GetLowerPriorityPacks(*this);
+
+ SetInstalled(*this, false);
+
+ for (const auto& texture : m_textures)
+ {
+ bool provided_by_other_pack = false;
+
+ // Check if a higher priority pack already provides a given texture, don't delete it
+ for (const auto& pack : GetHigherPriorityPacks(*this))
+ {
+ if (std::find(pack->GetTextures().begin(), pack->GetTextures().end(), texture) !=
+ pack->GetTextures().end())
+ {
+ provided_by_other_pack = true;
+ break;
+ }
+ }
+
+ if (provided_by_other_pack)
+ continue;
+
+ // Check if a lower priority pack provides a given texture - if so, install it.
+ for (auto& pack : lower)
+ {
+ if (std::find(pack->GetTextures().rbegin(), pack->GetTextures().rend(), texture) !=
+ pack->GetTextures().rend())
+ {
+ pack->Install(path);
+
+ provided_by_other_pack = true;
+ break;
+ }
+ }
+
+ if (provided_by_other_pack)
+ continue;
+
+ if (File::Exists(path + TEXTURE_PATH + texture) && !File::Delete(path + TEXTURE_PATH + texture))
+ {
+ m_error = "Failed to delete texture " + texture;
+ return false;
+ }
+
+ // Recursively delete empty directories
+
+ std::string dir;
+
+ SplitPath(path + TEXTURE_PATH + texture, &dir, nullptr, nullptr);
+
+ while (dir.length() > (path + TEXTURE_PATH).length())
+ {
+ auto is_empty = Common::DoFileSearch({dir}).empty();
+
+ if (is_empty)
+ File::DeleteDir(dir);
+
+ SplitPath(dir.substr(0, dir.size() - 2), &dir, nullptr, nullptr);
+ }
+ }
+
+ return true;
+}
+
+bool ResourcePack::operator==(const ResourcePack& pack)
+{
+ return pack.GetPath() == m_path;
+}
+
+} // namespace ResourcePack
diff --git a/Source/Core/UICommon/ResourcePack/ResourcePack.h b/Source/Core/UICommon/ResourcePack/ResourcePack.h
new file mode 100644
index 0000000000..516114118f
--- /dev/null
+++ b/Source/Core/UICommon/ResourcePack/ResourcePack.h
@@ -0,0 +1,45 @@
+// Copyright 2018 Dolphin Emulator Project
+// Licensed under GPLv2+
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "Common/CommonTypes.h"
+
+#include "UICommon/ResourcePack/Manifest.h"
+
+namespace ResourcePack
+{
+class ResourcePack
+{
+public:
+ explicit ResourcePack(const std::string& path);
+
+ bool IsValid() const;
+ const std::vector& GetLogo() const;
+
+ const std::string& GetPath() const;
+ const std::string& GetError() const;
+ const Manifest* GetManifest() const;
+ const std::vector& GetTextures() const;
+
+ bool Install(const std::string& path);
+ bool Uninstall(const std::string& path);
+
+ bool operator==(const ResourcePack& pack);
+
+private:
+ bool m_valid = true;
+
+ std::string m_path;
+ std::string m_error;
+
+ std::shared_ptr m_manifest;
+ std::vector m_textures;
+ std::vector m_logo_data;
+};
+} // namespace ResourcePack
diff --git a/Source/Core/UICommon/UICommon.cpp b/Source/Core/UICommon/UICommon.cpp
index 66f8642a9a..3b6eb4ff52 100644
--- a/Source/Core/UICommon/UICommon.cpp
+++ b/Source/Core/UICommon/UICommon.cpp
@@ -156,6 +156,7 @@ void SetLocale(std::string locale_name)
void CreateDirectories()
{
+ File::CreateFullPath(File::GetUserPath(D_RESOURCEPACK_IDX));
File::CreateFullPath(File::GetUserPath(D_USER_IDX));
File::CreateFullPath(File::GetUserPath(D_CACHE_IDX));
File::CreateFullPath(File::GetUserPath(D_COVERCACHE_IDX));
diff --git a/Source/Core/UICommon/UICommon.vcxproj b/Source/Core/UICommon/UICommon.vcxproj
index 3100241eee..b669d9b195 100644
--- a/Source/Core/UICommon/UICommon.vcxproj
+++ b/Source/Core/UICommon/UICommon.vcxproj
@@ -53,6 +53,9 @@
+
+
+
@@ -66,6 +69,9 @@
+
+
+