diff options
Diffstat (limited to 'src/yuzu')
-rw-r--r-- | src/yuzu/bootmanager.h | 2 | ||||
-rw-r--r-- | src/yuzu/game_list.cpp | 103 | ||||
-rw-r--r-- | src/yuzu/game_list_p.h | 28 | ||||
-rw-r--r-- | src/yuzu/main.cpp | 229 | ||||
-rw-r--r-- | src/yuzu/main.h | 1 | ||||
-rw-r--r-- | src/yuzu/main.ui | 7 |
6 files changed, 270 insertions, 100 deletions
diff --git a/src/yuzu/bootmanager.h b/src/yuzu/bootmanager.h index d0f990c64..f133bfadf 100644 --- a/src/yuzu/bootmanager.h +++ b/src/yuzu/bootmanager.h @@ -101,7 +101,7 @@ signals: void ErrorThrown(Core::System::ResultStatus, std::string); }; -class GRenderWindow : public QWidget, public EmuWindow { +class GRenderWindow : public QWidget, public Core::Frontend::EmuWindow { Q_OBJECT public: diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index 1c738d2a4..bc4b93033 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -2,6 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include <regex> #include <QApplication> #include <QDir> #include <QFileInfo> @@ -402,12 +403,72 @@ void GameList::RefreshGameDirectory() { } } -void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion) { - boost::container::flat_map<u64, std::shared_ptr<FileSys::NCA>> nca_control_map; +static void GetMetadataFromControlNCA(const std::shared_ptr<FileSys::NCA>& nca, + std::vector<u8>& icon, std::string& name) { + const auto control_dir = FileSys::ExtractRomFS(nca->GetRomFS()); + if (control_dir == nullptr) + return; + + const auto nacp_file = control_dir->GetFile("control.nacp"); + if (nacp_file == nullptr) + return; + 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_" + std::string(language) + ".dat"); + if (icon_file != nullptr) { + icon = icon_file->ReadAllBytes(); + break; + } + } +} + +void GameListWorker::AddInstalledTitlesToGameList() { + const auto usernand = Service::FileSystem::GetUserNANDContents(); + const auto installed_games = usernand->ListEntriesFilter(FileSys::TitleType::Application, + FileSys::ContentRecordType::Program); + + for (const auto& game : installed_games) { + const auto& file = usernand->GetEntryRaw(game); + std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(file); + if (!loader) + continue; + + std::vector<u8> icon; + std::string name; + u64 program_id = 0; + loader->ReadProgramId(program_id); + + const auto& control = + usernand->GetEntry(game.title_id, FileSys::ContentRecordType::Control); + if (control != nullptr) + GetMetadataFromControlNCA(control, icon, name); + emit EntryReady({ + new GameListItemPath( + FormatGameName(file->GetFullPath()), icon, QString::fromStdString(name), + QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())), + program_id), + new GameListItem( + QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), + new GameListItemSize(file->GetSize()), + }); + } + + const auto control_data = usernand->ListEntriesFilter(FileSys::TitleType::Application, + FileSys::ContentRecordType::Control); + + for (const auto& entry : control_data) { + const auto nca = usernand->GetEntry(entry); + if (nca != nullptr) + nca_control_map.insert_or_assign(entry.title_id, nca); + } +} - const auto nca_control_callback = - [this, &nca_control_map](u64* num_entries_out, const std::string& directory, - const std::string& virtual_name) -> bool { +void GameListWorker::FillControlMap(const std::string& dir_path) { + const auto nca_control_callback = [this](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) @@ -425,10 +486,11 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign }; 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 { +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 { std::string physical_name = directory + DIR_SEP + virtual_name; if (stop_processing) @@ -447,34 +509,18 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign std::vector<u8> icon; const auto res1 = loader->ReadIcon(icon); - u64 program_id; + u64 program_id = 0; 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) && + if (res1 != Loader::ResultStatus::Success && res3 != Loader::ResultStatus::Success && 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]; - const auto control_dir = FileSys::ExtractRomFS(nca->GetRomFS()); - - const 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_" + std::string(language) + ".dat"); - if (icon_file != nullptr) { - icon = icon_file->ReadAllBytes(); - break; - } - } + GetMetadataFromControlNCA(nca, icon, name); } } @@ -501,7 +547,10 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign void GameListWorker::run() { stop_processing = false; watch_list.append(dir_path); + FillControlMap(dir_path.toStdString()); + AddInstalledTitlesToGameList(); AddFstEntriesToGameList(dir_path.toStdString(), deep_scan ? 256 : 0); + nca_control_map.clear(); emit Finished(watch_list); } diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h index 114a0fc7f..10c2ef075 100644 --- a/src/yuzu/game_list_p.h +++ b/src/yuzu/game_list_p.h @@ -4,6 +4,7 @@ #pragma once +#include <array> #include <atomic> #include <utility> #include <QImage> @@ -39,7 +40,6 @@ public: * If this class receives valid SMDH data, it will also display game icons and titles. */ class GameListItemPath : public GameListItem { - public: static const int FullPathRole = Qt::UserRole + 1; static const int TitleRole = Qt::UserRole + 2; @@ -48,18 +48,18 @@ public: GameListItemPath() = default; GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data, - const QString& game_name, const QString& game_type, u64 program_id) - : GameListItem() { + const QString& game_name, const QString& game_type, u64 program_id) { setData(game_path, FullPathRole); setData(game_name, TitleRole); setData(qulonglong(program_id), ProgramIdRole); setData(game_type, FileTypeRole); + const u32 size = UISettings::values.icon_size; + QPixmap picture; - u32 size = UISettings::values.icon_size; - if (!picture.loadFromData(picture_data.data(), picture_data.size())) + if (!picture.loadFromData(picture_data.data(), static_cast<u32>(picture_data.size()))) { picture = GetDefaultIcon(size); - + } picture = picture.scaled(size, size); setData(picture, Qt::DecorationRole); @@ -70,17 +70,16 @@ public: std::string filename; Common::SplitPath(data(FullPathRole).toString().toStdString(), nullptr, &filename, nullptr); - QString title = data(TitleRole).toString(); - std::vector<QString> row_data{ + const std::array<QString, 4> 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); + const auto& row1 = row_data.at(UISettings::values.row_1_text_id); + const auto& row2 = row_data.at(UISettings::values.row_2_text_id); if (row1.isEmpty() || row1 == row2) return row2; @@ -88,9 +87,9 @@ public: return row1; return row1 + "\n " + row2; - } else { - return GameListItem::data(role); } + + return GameListItem::data(role); } }; @@ -164,10 +163,13 @@ signals: private: FileSys::VirtualFilesystem vfs; + std::map<u64, std::shared_ptr<FileSys::NCA>> nca_control_map; QStringList watch_list; QString dir_path; bool deep_scan; std::atomic_bool stop_processing; + void AddInstalledTitlesToGameList(); + void FillControlMap(const std::string& dir_path); void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0); }; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 67e3c6549..11d2331df 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -6,7 +6,10 @@ #include <clocale> #include <memory> #include <thread> + +#include <fmt/ostream.h> #include <glad/glad.h> + #define QT_NO_OPENGL #include <QDesktopWidget> #include <QFileDialog> @@ -24,6 +27,7 @@ #include "common/string_util.h" #include "core/core.h" #include "core/crypto/key_manager.h" +#include "core/file_sys/card_image.h" #include "core/file_sys/vfs_real.h" #include "core/gdbstub/gdbstub.h" #include "core/loader/loader.h" @@ -114,6 +118,9 @@ GMainWindow::GMainWindow() .arg(Common::g_build_name, Common::g_scm_branch, Common::g_scm_desc)); show(); + // Necessary to load titles from nand in gamelist. + Service::FileSystem::RegisterBIS(std::make_unique<FileSys::BISFactory>(vfs->OpenDirectory( + FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), FileSys::Mode::ReadWrite))); game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); // Show one-time "callout" messages to the user @@ -309,6 +316,8 @@ void GMainWindow::ConnectMenuEvents() { // File connect(ui.action_Load_File, &QAction::triggered, this, &GMainWindow::OnMenuLoadFile); connect(ui.action_Load_Folder, &QAction::triggered, this, &GMainWindow::OnMenuLoadFolder); + connect(ui.action_Install_File_NAND, &QAction::triggered, this, + &GMainWindow::OnMenuInstallToNAND); connect(ui.action_Select_Game_List_Root, &QAction::triggered, this, &GMainWindow::OnMenuSelectGameListRoot); connect(ui.action_Exit, &QAction::triggered, this, &QMainWindow::close); @@ -368,6 +377,8 @@ bool GMainWindow::SupportsRequiredGLExtensions() { unsupported_ext.append("ARB_vertex_attrib_binding"); if (!GLAD_GL_ARB_vertex_type_10f_11f_11f_rev) unsupported_ext.append("ARB_vertex_type_10f_11f_11f_rev"); + if (!GLAD_GL_ARB_texture_mirror_clamp_to_edge) + unsupported_ext.append("ARB_texture_mirror_clamp_to_edge"); // Extensions required to support some texture formats. if (!GLAD_GL_EXT_texture_compression_s3tc) @@ -424,67 +435,11 @@ bool GMainWindow::LoadROM(const QString& filename) { QMessageBox::critical(this, tr("Error while loading ROM!"), tr("The ROM format is not supported.")); break; - case Core::System::ResultStatus::ErrorUnsupportedArch: - LOG_CRITICAL(Frontend, "Unsupported architecture detected!", filename.toStdString()); - QMessageBox::critical(this, tr("Error while loading ROM!"), - tr("The ROM uses currently unusable 32-bit architecture")); - break; case Core::System::ResultStatus::ErrorSystemMode: LOG_CRITICAL(Frontend, "Failed to load ROM!"); QMessageBox::critical(this, tr("Error while loading ROM!"), tr("Could not determine the system mode.")); break; - - case Core::System::ResultStatus::ErrorLoader_ErrorMissingKeys: { - const auto reg_found = Core::Crypto::KeyManager::KeyFileExists(false); - const auto title_found = Core::Crypto::KeyManager::KeyFileExists(true); - - std::string file_text; - - if (!reg_found && !title_found) { - file_text = "A proper key file (prod.keys, dev.keys, or title.keys) could not be " - "found. You will need to dump your keys from your switch to continue."; - } else if (reg_found && title_found) { - file_text = - "Both key files were found in your config directory, but the correct key could" - "not be found. You may be missing a titlekey or general key, depending on " - "the game."; - } else if (reg_found) { - file_text = - "The regular keys file (prod.keys/dev.keys) was found in your config, but the " - "titlekeys file (title.keys) was not. You are either missing the correct " - "titlekey or missing a general key required to decrypt the game."; - } else { - file_text = "The title keys file (title.keys) was found in your config, but " - "the regular keys file (prod.keys/dev.keys) was not. Unfortunately, " - "having the titlekey is not enough, you need additional general keys " - "to properly decrypt the game. You should double-check to make sure " - "your keys are correct."; - } - - QMessageBox::critical( - this, tr("Error while loading ROM!"), - tr(("The game you are trying to load is encrypted and the required keys to load " - "the game could not be found in your configuration. " + - file_text + " Please refer to the yuzu wiki for help.") - .c_str())); - break; - } - case Core::System::ResultStatus::ErrorLoader_ErrorDecrypting: { - QMessageBox::critical( - this, tr("Error while loading ROM!"), - tr("There was a general error while decrypting the game. This means that the keys " - "necessary were found, but were either incorrect, the game itself was not a " - "valid game or the game uses an unhandled cryptographic scheme. Please double " - "check that you have the correct " - "keys.")); - break; - } - case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat: - QMessageBox::critical(this, tr("Error while loading ROM!"), - tr("The ROM format is not supported.")); - break; - case Core::System::ResultStatus::ErrorVideoCore: QMessageBox::critical( this, tr("An error occurred initializing the video core."), @@ -499,9 +454,23 @@ bool GMainWindow::LoadROM(const QString& filename) { break; default: - QMessageBox::critical( - this, tr("Error while loading ROM!"), - tr("An unknown error occurred. Please see the log for more details.")); + if (static_cast<u32>(result) > + static_cast<u32>(Core::System::ResultStatus::ErrorLoader)) { + LOG_CRITICAL(Frontend, "Failed to load ROM!"); + const u16 loader_id = static_cast<u16>(Core::System::ResultStatus::ErrorLoader); + const u16 error_id = static_cast<u16>(result) - loader_id; + QMessageBox::critical( + this, tr("Error while loading ROM!"), + QString::fromStdString(fmt::format( + "While attempting to load the ROM requested, an error occured. Please " + "refer to the yuzu wiki for more information or the yuzu discord for " + "additional help.\n\nError Code: {:04X}-{:04X}\nError Description: {}", + loader_id, error_id, static_cast<Loader::ResultStatus>(error_id)))); + } else { + QMessageBox::critical( + this, tr("Error while loading ROM!"), + tr("An unknown error occurred. Please see the log for more details.")); + } break; } return false; @@ -654,6 +623,148 @@ void GMainWindow::OnMenuLoadFolder() { } } +void GMainWindow::OnMenuInstallToNAND() { + const QString file_filter = + tr("Installable Switch File (*.nca *.xci);;Nintendo Content Archive (*.nca);;NX Cartridge " + "Image (*.xci)"); + QString filename = QFileDialog::getOpenFileName(this, tr("Install File"), + UISettings::values.roms_path, file_filter); + + if (filename.isEmpty()) { + return; + } + + const auto qt_raw_copy = [this](FileSys::VirtualFile src, FileSys::VirtualFile dest) { + if (src == nullptr || dest == nullptr) + return false; + if (!dest->Resize(src->GetSize())) + return false; + + std::array<u8, 0x1000> buffer{}; + const int progress_maximum = static_cast<int>(src->GetSize() / buffer.size()); + + QProgressDialog progress( + tr("Installing file \"%1\"...").arg(QString::fromStdString(src->GetName())), + tr("Cancel"), 0, progress_maximum, this); + progress.setWindowModality(Qt::WindowModal); + + for (size_t i = 0; i < src->GetSize(); i += buffer.size()) { + if (progress.wasCanceled()) { + dest->Resize(0); + return false; + } + + const int progress_value = static_cast<int>(i / buffer.size()); + progress.setValue(progress_value); + + const auto read = src->Read(buffer.data(), buffer.size(), i); + dest->Write(buffer.data(), read, i); + } + + return true; + }; + + const auto success = [this]() { + QMessageBox::information(this, tr("Successfully Installed"), + tr("The file was successfully installed.")); + game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); + }; + + const auto failed = [this]() { + QMessageBox::warning( + this, tr("Failed to Install"), + tr("There was an error while attempting to install the provided file. It " + "could have an incorrect format or be missing metadata. Please " + "double-check your file and try again.")); + }; + + const auto overwrite = [this]() { + return QMessageBox::question(this, tr("Failed to Install"), + tr("The file you are attempting to install already exists " + "in the cache. Would you like to overwrite it?")) == + QMessageBox::Yes; + }; + + if (filename.endsWith("xci", Qt::CaseInsensitive)) { + const auto xci = std::make_shared<FileSys::XCI>( + vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read)); + if (xci->GetStatus() != Loader::ResultStatus::Success) { + failed(); + return; + } + const auto res = + Service::FileSystem::GetUserNANDContents()->InstallEntry(xci, false, qt_raw_copy); + if (res == FileSys::InstallResult::Success) { + success(); + } else { + if (res == FileSys::InstallResult::ErrorAlreadyExists) { + if (overwrite()) { + const auto res2 = Service::FileSystem::GetUserNANDContents()->InstallEntry( + xci, true, qt_raw_copy); + if (res2 == FileSys::InstallResult::Success) { + success(); + } else { + failed(); + } + } + } else { + failed(); + } + } + } else { + const auto nca = std::make_shared<FileSys::NCA>( + vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read)); + if (nca->GetStatus() != Loader::ResultStatus::Success) { + failed(); + return; + } + + const QStringList tt_options{tr("System Application"), + tr("System Archive"), + tr("System Application Update"), + tr("Firmware Package (Type A)"), + tr("Firmware Package (Type B)"), + tr("Game"), + tr("Game Update"), + tr("Game DLC"), + tr("Delta Title")}; + bool ok; + const auto item = QInputDialog::getItem( + this, tr("Select NCA Install Type..."), + tr("Please select the type of title you would like to install this NCA as:\n(In " + "most instances, the default 'Game' is fine.)"), + tt_options, 5, false, &ok); + + auto index = tt_options.indexOf(item); + if (!ok || index == -1) { + QMessageBox::warning(this, tr("Failed to Install"), + tr("The title type you selected for the NCA is invalid.")); + return; + } + + if (index >= 5) + index += 0x7B; + + const auto res = Service::FileSystem::GetUserNANDContents()->InstallEntry( + nca, static_cast<FileSys::TitleType>(index), false, qt_raw_copy); + if (res == FileSys::InstallResult::Success) { + success(); + } else if (res == FileSys::InstallResult::ErrorAlreadyExists) { + if (overwrite()) { + const auto res2 = Service::FileSystem::GetUserNANDContents()->InstallEntry( + nca, static_cast<FileSys::TitleType>(index), true, qt_raw_copy); + if (res2 == FileSys::InstallResult::Success) { + success(); + } else { + failed(); + } + } + } else { + failed(); + } + } +} + void GMainWindow::OnMenuSelectGameListRoot() { QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); if (!dir_path.isEmpty()) { diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 74487c58c..5f4d2ab9a 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -125,6 +125,7 @@ private slots: void OnGameListOpenSaveFolder(u64 program_id); void OnMenuLoadFile(); void OnMenuLoadFolder(); + void OnMenuInstallToNAND(); /// Called whenever a user selects the "File->Select Game List Root" menu item void OnMenuSelectGameListRoot(); void OnMenuRecentFile(); diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index 22c4cad08..a3bfb2af3 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -57,6 +57,8 @@ <string>Recent Files</string> </property> </widget> + <addaction name="action_Install_File_NAND" /> + <addaction name="separator"/> <addaction name="action_Load_File"/> <addaction name="action_Load_Folder"/> <addaction name="separator"/> @@ -102,6 +104,11 @@ <addaction name="menu_View"/> <addaction name="menu_Help"/> </widget> + <action name="action_Install_File_NAND"> + <property name="text"> + <string>Install File to NAND...</string> + </property> + </action> <action name="action_Load_File"> <property name="text"> <string>Load File...</string> |