diff --git a/src/core/file_sys/control_metadata.h b/src/core/file_sys/control_metadata.h index cc3b745f7..9fc02612a 100644 --- a/src/core/file_sys/control_metadata.h +++ b/src/core/file_sys/control_metadata.h @@ -62,6 +62,13 @@ enum class Language : u8 { Chinese = 14, }; +static std::array LANGUAGE_NAMES = { + "AmericanEnglish", "BritishEnglish", "Japanese", + "French", "German", "LatinAmericanSpanish", + "Spanish", "Italian", "Dutch", + "CanadianFrench", "Portugese", "Russian", + "Korean", "Taiwanese", "Chinese"}; + // A class representing the format used by NX metadata files, typically named Control.nacp. // These store application name, dev name, title id, and other miscellaneous data. class NACP { diff --git a/src/core/loader/deconstructed_rom_directory.cpp b/src/core/loader/deconstructed_rom_directory.cpp index 076927dff..cc88a44b6 100644 --- a/src/core/loader/deconstructed_rom_directory.cpp +++ b/src/core/loader/deconstructed_rom_directory.cpp @@ -7,6 +7,7 @@ #include "common/file_util.h" #include "common/logging/log.h" #include "core/file_sys/content_archive.h" +#include "core/file_sys/control_metadata.h" #include "core/gdbstub/gdbstub.h" #include "core/hle/kernel/process.h" #include "core/hle/kernel/resource_limit.h" @@ -17,8 +18,50 @@ namespace Loader { -AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys::VirtualFile file) - : AppLoader(std::move(file)) {} +AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys::VirtualFile file_) + : AppLoader(std::move(file_)) { + const auto dir = file->GetContainingDirectory(); + + // Icon + FileSys::VirtualFile icon_file = nullptr; + for (const auto& language : FileSys::LANGUAGE_NAMES) { + icon_file = dir->GetFile("icon_" + language + ".dat"); + if (icon_file != nullptr) { + icon_data = icon_file->ReadAllBytes(); + break; + } + } + + if (icon_data.empty()) { + // Any png, jpeg, or bmp file + const auto& files = dir->GetFiles(); + const auto icon_iter = + std::find_if(files.begin(), files.end(), [](const FileSys::VirtualFile& file) { + return file->GetExtension() == "png" || file->GetExtension() == "jpg" || + file->GetExtension() == "bmp" || file->GetExtension() == "jpeg"; + }); + if (icon_iter != files.end()) + icon_data = (*icon_iter)->ReadAllBytes(); + } + + // Metadata + FileSys::VirtualFile nacp_file = dir->GetFile("control.nacp"); + if (nacp_file == nullptr) { + const auto& files = dir->GetFiles(); + const auto nacp_iter = + std::find_if(files.begin(), files.end(), [](const FileSys::VirtualFile& file) { + return file->GetExtension() == "nacp"; + }); + if (nacp_iter != files.end()) + nacp_file = *nacp_iter; + } + + if (nacp_file != nullptr) { + FileSys::NACP nacp(nacp_file); + title_id = nacp.GetTitleId(); + name = nacp.GetApplicationName(); + } +} AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory( FileSys::VirtualDir directory) @@ -105,4 +148,25 @@ ResultStatus AppLoader_DeconstructedRomDirectory::ReadRomFS(FileSys::VirtualFile return ResultStatus::Success; } +ResultStatus AppLoader_DeconstructedRomDirectory::ReadIcon(std::vector& buffer) { + if (icon_data.empty()) + return ResultStatus::ErrorNotUsed; + buffer = icon_data; + return ResultStatus::Success; +} + +ResultStatus AppLoader_DeconstructedRomDirectory::ReadProgramId(u64& out_program_id) { + if (name.empty()) + return ResultStatus::ErrorNotUsed; + out_program_id = title_id; + return ResultStatus::Success; +} + +ResultStatus AppLoader_DeconstructedRomDirectory::ReadTitle(std::string& title) { + if (name.empty()) + return ResultStatus::ErrorNotUsed; + title = name; + return ResultStatus::Success; +} + } // namespace Loader diff --git a/src/core/loader/deconstructed_rom_directory.h b/src/core/loader/deconstructed_rom_directory.h index 7d5433563..b20804f75 100644 --- a/src/core/loader/deconstructed_rom_directory.h +++ b/src/core/loader/deconstructed_rom_directory.h @@ -39,11 +39,18 @@ public: ResultStatus Load(Kernel::SharedPtr& process) override; ResultStatus ReadRomFS(FileSys::VirtualFile& dir) override; + ResultStatus ReadIcon(std::vector& buffer) override; + ResultStatus ReadProgramId(u64& out_program_id) override; + ResultStatus ReadTitle(std::string& title) override; private: FileSys::ProgramMetadata metadata; FileSys::VirtualFile romfs; FileSys::VirtualDir dir; + + std::vector icon_data; + std::string name; + u64 title_id{}; }; } // namespace Loader diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp index 57e6c0365..0781fb8c1 100644 --- a/src/core/loader/loader.cpp +++ b/src/core/loader/loader.cpp @@ -68,7 +68,7 @@ FileType GuessFromFilename(const std::string& name) { return FileType::Unknown; } -const char* GetFileTypeString(FileType type) { +std::string GetFileTypeString(FileType type) { switch (type) { case FileType::ELF: return "ELF"; diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h index e69ab85ef..7bd0adedb 100644 --- a/src/core/loader/loader.h +++ b/src/core/loader/loader.h @@ -61,7 +61,7 @@ FileType GuessFromFilename(const std::string& name); /** * Convert a FileType into a string which can be displayed to the user. */ -const char* GetFileTypeString(FileType type); +std::string GetFileTypeString(FileType type); /// Return type for functions in Loader namespace enum class ResultStatus { diff --git a/src/core/loader/nca.cpp b/src/core/loader/nca.cpp index dbc67c0b5..46f5cd393 100644 --- a/src/core/loader/nca.cpp +++ b/src/core/loader/nca.cpp @@ -77,8 +77,8 @@ ResultStatus AppLoader_NCA::ReadRomFS(FileSys::VirtualFile& dir) { } ResultStatus AppLoader_NCA::ReadProgramId(u64& out_program_id) { - if (nca == nullptr) - return ResultStatus::ErrorNotLoaded; + if (nca == nullptr || nca->GetStatus() != ResultStatus::Success) + return ResultStatus::ErrorInvalidFormat; out_program_id = nca->GetTitleId(); return ResultStatus::Success; } diff --git a/src/core/loader/nca.h b/src/core/loader/nca.h index 0fd2d0417..443bc1202 100644 --- a/src/core/loader/nca.h +++ b/src/core/loader/nca.h @@ -33,6 +33,7 @@ public: ResultStatus Load(Kernel::SharedPtr& process) override; ResultStatus ReadRomFS(FileSys::VirtualFile& dir) override; + ResultStatus ReadProgramId(u64& out_program_id) override; ResultStatus ReadProgramId(u64& out_program_id) override; @@ -41,6 +42,7 @@ public: private: FileSys::ProgramMetadata metadata; + FileSys::NCAHeader header; std::unique_ptr nca; std::unique_ptr directory_loader; }; diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 475556806..46ed232d8 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -17,6 +17,8 @@ add_executable(yuzu configuration/configure_debug.h configuration/configure_dialog.cpp configuration/configure_dialog.h + configuration/configure_gamelist.cpp + configuration/configure_gamelist.h configuration/configure_general.cpp configuration/configure_general.h configuration/configure_graphics.cpp @@ -59,6 +61,7 @@ set(UIS configuration/configure.ui configuration/configure_audio.ui configuration/configure_debug.ui + configuration/configure_gamelist.ui configuration/configure_general.ui configuration/configure_graphics.ui configuration/configure_input.ui diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index bf469ee73..0bd46dbac 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -122,6 +122,13 @@ void Config::ReadValues() { qt_config->beginGroup("UI"); UISettings::values.theme = qt_config->value("theme", UISettings::themes[0].second).toString(); + qt_config->beginGroup("UIGameList"); + UISettings::values.show_unknown = qt_config->value("show_unknown", true).toBool(); + UISettings::values.icon_size = qt_config->value("icon_size", 48).toUInt(); + UISettings::values.row_1_text_id = qt_config->value("row_1_text_id", 0).toUInt(); + UISettings::values.row_2_text_id = qt_config->value("row_2_text_id", 3).toUInt(); + qt_config->endGroup(); + qt_config->beginGroup("UILayout"); UISettings::values.geometry = qt_config->value("geometry").toByteArray(); UISettings::values.state = qt_config->value("state").toByteArray(); @@ -234,6 +241,13 @@ void Config::SaveValues() { qt_config->beginGroup("UI"); qt_config->setValue("theme", UISettings::values.theme); + qt_config->beginGroup("UIGameList"); + qt_config->setValue("show_unknown", UISettings::values.show_unknown); + qt_config->setValue("icon_size", UISettings::values.icon_size); + qt_config->setValue("row_1_text_id", UISettings::values.row_1_text_id); + qt_config->setValue("row_2_text_id", UISettings::values.row_2_text_id); + qt_config->endGroup(); + qt_config->beginGroup("UILayout"); qt_config->setValue("geometry", UISettings::values.geometry); qt_config->setValue("state", UISettings::values.state); diff --git a/src/yuzu/configuration/configure.ui b/src/yuzu/configuration/configure.ui index c8e0b88af..20f120134 100644 --- a/src/yuzu/configuration/configure.ui +++ b/src/yuzu/configuration/configure.ui @@ -24,6 +24,11 @@ General + + + Game List + + System @@ -67,6 +72,12 @@
configuration/configure_general.h
1 + + ConfigureGameList + QWidget +
configuration/configure_gamelist.h
+ 1 +
ConfigureSystem QWidget diff --git a/src/yuzu/configuration/configure_dialog.cpp b/src/yuzu/configuration/configure_dialog.cpp index 1ca7e876c..d04aa4e31 100644 --- a/src/yuzu/configuration/configure_dialog.cpp +++ b/src/yuzu/configuration/configure_dialog.cpp @@ -18,6 +18,7 @@ void ConfigureDialog::setConfiguration() {} void ConfigureDialog::applyConfiguration() { ui->generalTab->applyConfiguration(); + ui->gameListTab->applyConfiguration(); ui->systemTab->applyConfiguration(); ui->inputTab->applyConfiguration(); ui->graphicsTab->applyConfiguration(); diff --git a/src/yuzu/configuration/configure_gamelist.cpp b/src/yuzu/configuration/configure_gamelist.cpp new file mode 100644 index 000000000..072b3f96f --- /dev/null +++ b/src/yuzu/configuration/configure_gamelist.cpp @@ -0,0 +1,61 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "core/core.h" +#include "core/settings.h" +#include "ui_configure_gamelist.h" +#include "ui_settings.h" +#include "yuzu/configuration/configure_gamelist.h" + +ConfigureGameList::ConfigureGameList(QWidget* parent) + : QWidget(parent), ui(new Ui::ConfigureGameList) { + ui->setupUi(this); + + static std::vector> default_icon_sizes{ + std::make_pair(0, "None"), std::make_pair(24, "Small"), + std::make_pair(48, "Standard"), std::make_pair(96, "Large"), + std::make_pair(256, "Full Size"), + }; + + for (const auto& size : default_icon_sizes) { + ui->icon_size_combobox->addItem(QString::fromStdString(size.second + " (" + + std::to_string(size.first) + "x" + + std::to_string(size.first) + ")"), + size.first); + } + + static std::vector row_text_names{ + "Filename", + "Filetype", + "Title ID", + "Title Name", + }; + + for (size_t i = 0; i < row_text_names.size(); ++i) { + ui->row_1_text_combobox->addItem(QString::fromStdString(row_text_names[i]), i); + ui->row_2_text_combobox->addItem(QString::fromStdString(row_text_names[i]), i); + } + + this->setConfiguration(); +} + +ConfigureGameList::~ConfigureGameList() {} + +void ConfigureGameList::setConfiguration() { + ui->show_unknown->setChecked(UISettings::values.show_unknown); + ui->icon_size_combobox->setCurrentIndex( + ui->icon_size_combobox->findData(UISettings::values.icon_size)); + ui->row_1_text_combobox->setCurrentIndex( + ui->row_1_text_combobox->findData(UISettings::values.row_1_text_id)); + ui->row_2_text_combobox->setCurrentIndex( + ui->row_2_text_combobox->findData(UISettings::values.row_2_text_id)); +} + +void ConfigureGameList::applyConfiguration() { + UISettings::values.show_unknown = ui->show_unknown->isChecked(); + UISettings::values.icon_size = ui->icon_size_combobox->currentData().toUInt(); + UISettings::values.row_1_text_id = ui->row_1_text_combobox->currentData().toUInt(); + UISettings::values.row_2_text_id = ui->row_2_text_combobox->currentData().toUInt(); + Settings::Apply(); +} diff --git a/src/yuzu/configuration/configure_gamelist.h b/src/yuzu/configuration/configure_gamelist.h new file mode 100644 index 000000000..94fba6373 --- /dev/null +++ b/src/yuzu/configuration/configure_gamelist.h @@ -0,0 +1,28 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +namespace Ui { +class ConfigureGameList; +} + +class ConfigureGameList : public QWidget { + Q_OBJECT + +public: + explicit ConfigureGameList(QWidget* parent = nullptr); + ~ConfigureGameList(); + + void applyConfiguration(); + +private: + void setConfiguration(); + +private: + std::unique_ptr ui; +}; diff --git a/src/yuzu/configuration/configure_gamelist.ui b/src/yuzu/configuration/configure_gamelist.ui new file mode 100644 index 000000000..7471fdb60 --- /dev/null +++ b/src/yuzu/configuration/configure_gamelist.ui @@ -0,0 +1,126 @@ + + + ConfigureGameList + + + + 0 + 0 + 300 + 377 + + + + Form + + + + + + + + General + + + + + + + + Show files with type 'Unknown' + + + + + + + + + + + + Icon Size + + + + + + + + + + Icon Size: + + + + + + + + + + + + + + + + + Row Text + + + + + + + + + + Row 1 Text: + + + + + + + + + + + + + + Row 2 Text: + + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index 24f38a3c7..893c5f693 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -12,6 +12,8 @@ #include "common/common_paths.h" #include "common/logging/log.h" #include "common/string_util.h" +#include "core/file_sys/content_archive.h" +#include "core/file_sys/control_metadata.h" #include "core/file_sys/vfs_real.h" #include "core/loader/loader.h" #include "game_list.h" @@ -398,8 +400,32 @@ void GameList::RefreshGameDirectory() { } void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion) { - const auto callback = [this, recursion](u64* num_entries_out, const std::string& directory, - const std::string& virtual_name) -> bool { + boost::container::flat_map> nca_control_map; + + const auto nca_control_callback = + [this, &nca_control_map](u64* num_entries_out, const std::string& directory, + const std::string& virtual_name) -> bool { + std::string physical_name = directory + DIR_SEP + virtual_name; + + if (stop_processing) + return false; // Breaks the callback loop. + + bool is_dir = FileUtil::IsDirectory(physical_name); + QFileInfo file_info(physical_name.c_str()); + if (!is_dir && file_info.suffix().toStdString() == "nca") { + auto nca = std::make_shared( + std::make_shared(physical_name)); + if (nca->GetType() == FileSys::NCAContentType::Control) + nca_control_map.insert_or_assign(nca->GetTitleId(), nca); + } + return true; + }; + + FileUtil::ForeachDirectoryEntry(nullptr, dir_path, nca_control_callback); + + const auto callback = [this, recursion, + &nca_control_map](u64* num_entries_out, const std::string& directory, + const std::string& virtual_name) -> bool { std::string physical_name = directory + DIR_SEP + virtual_name; if (stop_processing) @@ -410,17 +436,50 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign (HasSupportedFileExtension(physical_name) || IsExtractedNCAMain(physical_name))) { std::unique_ptr loader = Loader::GetLoader(std::make_shared(physical_name)); - if (!loader) + if (!loader || ((loader->GetFileType() == Loader::FileType::Unknown || + loader->GetFileType() == Loader::FileType::Error) && + !UISettings::values.show_unknown)) return true; - std::vector smdh; - loader->ReadIcon(smdh); + std::vector icon; + const auto res1 = loader->ReadIcon(icon); - u64 program_id = 0; - loader->ReadProgramId(program_id); + u64 program_id; + const auto res2 = loader->ReadProgramId(program_id); + + std::string name = " "; + const auto res3 = loader->ReadTitle(name); + + if ((res1 == Loader::ResultStatus::ErrorNotUsed || + res1 == Loader::ResultStatus::ErrorNotImplemented) && + (res3 == Loader::ResultStatus::ErrorNotUsed || + res3 == Loader::ResultStatus::ErrorNotImplemented) && + res2 == Loader::ResultStatus::Success) { + // Use from metadata pool. + if (nca_control_map.find(program_id) != nca_control_map.end()) { + const auto nca = nca_control_map[program_id]; + auto control_dir = nca->GetSection(0); + + auto nacp_file = control_dir->GetFile("control.nacp"); + FileSys::NACP nacp(nacp_file); + name = nacp.GetApplicationName(); + + FileSys::VirtualFile icon_file = nullptr; + for (const auto& language : FileSys::LANGUAGE_NAMES) { + icon_file = control_dir->GetFile("icon_" + language + ".dat"); + if (icon_file != nullptr) { + icon = icon_file->ReadAllBytes(); + break; + } + } + } + } emit EntryReady({ - new GameListItemPath(FormatGameName(physical_name), smdh, program_id), + new GameListItemPath( + FormatGameName(physical_name), icon, QString::fromStdString(name), + QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())), + program_id), new GameListItem( QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), new GameListItemSize(FileUtil::GetSize(physical_name)), diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h index aa69a098f..a22025e67 100644 --- a/src/yuzu/game_list_p.h +++ b/src/yuzu/game_list_p.h @@ -11,6 +11,7 @@ #include #include #include "common/string_util.h" +#include "ui_settings.h" #include "yuzu/util/util.h" /** @@ -18,8 +19,7 @@ * @param large If true, returns large icon (48x48), otherwise returns small icon (24x24) * @return QPixmap default icon */ -static QPixmap GetDefaultIcon(bool large) { - int size = large ? 48 : 24; +static QPixmap GetDefaultIcon(u32 size) { QPixmap icon(size, size); icon.fill(Qt::transparent); return icon; @@ -44,11 +44,25 @@ public: static const int FullPathRole = Qt::UserRole + 1; static const int TitleRole = Qt::UserRole + 2; static const int ProgramIdRole = Qt::UserRole + 3; + static const int FileTypeRole = Qt::UserRole + 4; GameListItemPath() = default; - GameListItemPath(const QString& game_path, const std::vector& smdh_data, u64 program_id) { + GameListItemPath(const QString& game_path, const std::vector& picture_data, + const QString& game_name, const QString& game_type, u64 program_id) + : GameListItem() { setData(game_path, FullPathRole); + setData(game_name, TitleRole); setData(qulonglong(program_id), ProgramIdRole); + setData(game_type, FileTypeRole); + + QPixmap picture; + u32 size = UISettings::values.icon_size; + if (!picture.loadFromData(picture_data.data(), picture_data.size())) + picture = GetDefaultIcon(size); + + picture = picture.scaled(size, size); + + setData(picture, Qt::DecorationRole); } QVariant data(int role) const override { @@ -57,7 +71,23 @@ public: Common::SplitPath(data(FullPathRole).toString().toStdString(), nullptr, &filename, nullptr); QString title = data(TitleRole).toString(); - return QString::fromStdString(filename) + (title.isEmpty() ? "" : "\n " + title); + + std::vector row_data{ + QString::fromStdString(filename), + data(FileTypeRole).toString(), + QString::fromStdString(fmt::format("0x{:016X}", data(ProgramIdRole).toULongLong())), + data(TitleRole).toString(), + }; + + auto row1 = row_data.at(UISettings::values.row_1_text_id); + auto row2 = row_data.at(UISettings::values.row_2_text_id); + + if (row1.isEmpty() || row1 == row2) + return row2; + if (row2.isEmpty()) + return row1; + + return row1 + "\n " + row2; } else { return GameListItem::data(role); } diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index dd71bd763..3cba6f403 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -764,6 +764,7 @@ void GMainWindow::OnConfigure() { configureDialog.applyConfiguration(); if (UISettings::values.theme != old_theme) UpdateUITheme(); + game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); config->Save(); } } diff --git a/src/yuzu/ui_settings.h b/src/yuzu/ui_settings.h index 2286c2559..051494bc5 100644 --- a/src/yuzu/ui_settings.h +++ b/src/yuzu/ui_settings.h @@ -54,6 +54,12 @@ struct Values { // logging bool show_console; + + // Game List + bool show_unknown; + uint32_t icon_size; + uint8_t row_1_text_id; + uint8_t row_2_text_id; }; extern Values values;