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 86a73c34ed..c547f73afc 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -107,6 +107,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 ae85412043..0c58102428 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -329,6 +329,7 @@ + @@ -390,6 +391,7 @@ + @@ -477,4 +479,4 @@ - + \ No newline at end of file diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index 6899508c17..be4ee0c0ea 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() @@ -391,6 +411,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); @@ -1553,6 +1575,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 6b3e88e930..43a38452a2 100644 --- a/Source/Core/DolphinQt/MenuBar.cpp +++ b/Source/Core/DolphinQt/MenuBar.cpp @@ -204,6 +204,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 eedee4b6d5..19a4612dc7 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 @@ + + +