diff --git a/src/common/settings.h b/src/common/settings.h index 48f127bde8..12def092b8 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later // Modified by palfaiate on <2024/03/07> +// Reverted palfaiate's changes on <2024/03/25> -Nine-Ball #pragma once diff --git a/src/suyu/game_list.cpp b/src/suyu/game_list.cpp index bda0d489bf..28a9d9c970 100644 --- a/src/suyu/game_list.cpp +++ b/src/suyu/game_list.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later // Modified by palfaiate on <2024/03/07> +// Reverted palfaiate's changes on <2024/03/25> -Nine-Ball #include #include @@ -580,6 +581,9 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri remove_menu->addSeparator(); QAction* remove_shader_cache = remove_menu->addAction(tr("Remove All Pipeline Caches")); QAction* remove_all_content = remove_menu->addAction(tr("Remove All Installed Contents")); + QAction* dump_romfs_menu = context_menu.addMenu(tr("Dump RomFS")); + QAction* dump_romfs = dump_romfs_menu->addAction(tr("Dump RomFS")); + QAction* dump_romfs_sdmc = dump_romfs_menu->addAction(tr("Dump RomFS to SDMC")); QAction* verify_integrity = context_menu.addAction(tr("Verify Integrity")); QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard")); QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); @@ -647,6 +651,12 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri connect(remove_cache_storage, &QAction::triggered, [this, program_id, path] { emit RemoveFileRequested(program_id, GameListRemoveTarget::CacheStorage, path); }); + connect(dump_romfs, &QAction::triggered, [this, program_id, path]() { + emit DumpRomFSRequested(program_id, path, DumpRomFSTarget::Normal); + }); + connect(dump_romfs_sdmc, &QAction::triggered, [this, program_id, path]() { + emit DumpRomFSRequested(program_id, path, DumpRomFSTarget::SDMC); + }); connect(verify_integrity, &QAction::triggered, [this, path]() { emit VerifyIntegrityRequested(path); }); connect(copy_tid, &QAction::triggered, diff --git a/src/suyu/game_list.h b/src/suyu/game_list.h index 7568e1b6e0..684a7ba2d9 100644 --- a/src/suyu/game_list.h +++ b/src/suyu/game_list.h @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later // Modified by palfaiate on <2024/03/07> +// Reverted palfaiate's changes on <2024/03/25> -Nine-Ball #pragma once @@ -52,6 +53,11 @@ enum class GameListRemoveTarget { CacheStorage, }; +enum class DumpRomFSTarget { + Normal, + SDMC, +}; + enum class GameListShortcutTarget { Desktop, Applications, @@ -113,6 +119,7 @@ signals: void RemoveFileRequested(u64 program_id, GameListRemoveTarget target, const std::string& game_path); void RemovePlayTimeRequested(u64 program_id); + void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target); void VerifyIntegrityRequested(const std::string& game_path); void CopyTIDRequested(u64 program_id); void CreateShortcut(u64 program_id, const std::string& game_path, diff --git a/src/suyu/main.cpp b/src/suyu/main.cpp index 9a3ee7f662..b6af950097 100644 --- a/src/suyu/main.cpp +++ b/src/suyu/main.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later // Modified by palfaiate on <2024/03/07> +// Reverted palfaiate's changes on <2024/03/25> -Nine-Ball #include #include @@ -55,6 +56,18 @@ #include "suyu/multiplayer/state.h" #include "suyu/util/controller_navigation.h" +// These are wrappers to avoid the calls to CreateDirectory and CreateFile because of the Windows +static FileSys::VirtualDir VfsFilesystemCreateDirectoryWrapper( + const FileSys::VirtualFilesystem& vfs, const std::string& path, FileSys::OpenMode mode) { + return vfs->CreateDirectory(path, mode); +} + +// Overloaded function, also removed by palafiate +static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::VirtualDir& dir, const std::string& path) { + return dir->CreateFile(path); +} + + #include #include @@ -1465,6 +1478,7 @@ void GMainWindow::ConnectWidgetEvents() { connect(game_list, &GameList::RemoveFileRequested, this, &GMainWindow::OnGameListRemoveFile); connect(game_list, &GameList::RemovePlayTimeRequested, this, &GMainWindow::OnGameListRemovePlayTimeData); + connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS); connect(game_list, &GameList::VerifyIntegrityRequested, this, &GMainWindow::OnGameListVerifyIntegrity); connect(game_list, &GameList::CopyTIDRequested, this, &GMainWindow::OnGameListCopyTID); @@ -2369,6 +2383,68 @@ void GMainWindow::OnTransferableShaderCacheOpenFile(u64 program_id) { QDesktopServices::openUrl(QUrl::fromLocalFile(qt_shader_cache_path)); } +static bool RomFSRawCopy(size_t total_size, size_t& read_size, QProgressDialog& dialog, + const FileSys::VirtualDir& src, const FileSys::VirtualDir& dest, + bool full) { + if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable()) + return false; + if (dialog.wasCanceled()) + return false; + + std::vector buffer(CopyBufferSize); + auto last_timestamp = std::chrono::steady_clock::now(); + + const auto QtRawCopy = [&](const FileSys::VirtualFile& src_file, + const FileSys::VirtualFile& dest_file) { + if (src_file == nullptr || dest_file == nullptr) { + return false; + } + if (!dest_file->Resize(src_file->GetSize())) { + return false; + } + + for (std::size_t i = 0; i < src_file->GetSize(); i += buffer.size()) { + if (dialog.wasCanceled()) { + dest_file->Resize(0); + return false; + } + + using namespace std::literals::chrono_literals; + const auto new_timestamp = std::chrono::steady_clock::now(); + + if ((new_timestamp - last_timestamp) > 33ms) { + last_timestamp = new_timestamp; + dialog.setValue( + static_cast(std::min(read_size, total_size) * 100 / total_size)); + QCoreApplication::processEvents(); + } + + const auto read = src_file->Read(buffer.data(), buffer.size(), i); + dest_file->Write(buffer.data(), read, i); + + read_size += read; + } + + return true; + }; + + if (full) { + for (const auto& file : src->GetFiles()) { + const auto out = VfsDirectoryCreateFileWrapper(dest, file->GetName()); + if (!QtRawCopy(file, out)) + return false; + } + } + + for (const auto& dir : src->GetSubdirectories()) { + const auto out = dest->CreateSubdirectory(dir->GetName()); + if (!RomFSRawCopy(total_size, read_size, dialog, dir, out, full)) + return false; + } + + return true; +} + QString GMainWindow::GetGameListErrorRemoving(InstalledEntryType type) const { switch (type) { case InstalledEntryType::Game: @@ -2610,6 +2686,121 @@ void GMainWindow::RemoveCacheStorage(u64 program_id) { Common::FS::RemoveDirRecursively(path); } +void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_path, + DumpRomFSTarget target) { + const auto failed = [this] { + QMessageBox::warning(this, tr("RomFS Extraction Failed!"), + tr("There was an error copying the RomFS files or the user " + "cancelled the operation.")); + }; + + const auto loader = + Loader::GetLoader(*system, vfs->OpenFile(game_path, FileSys::OpenMode::Read)); + if (loader == nullptr) { + failed(); + return; + } + + FileSys::VirtualFile packed_update_raw{}; + loader->ReadUpdateRaw(packed_update_raw); + + const auto& installed = system->GetContentProvider(); + + u64 title_id{}; + u8 raw_type{}; + if (!SelectRomFSDumpTarget(installed, program_id, &title_id, &raw_type)) { + failed(); + return; + } + + const auto type = static_cast(raw_type); + const auto base_nca = installed.GetEntry(title_id, type); + if (!base_nca) { + failed(); + return; + } + + const FileSys::NCA update_nca{packed_update_raw, nullptr}; + if (type != FileSys::ContentRecordType::Program || + update_nca.GetStatus() != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS || + update_nca.GetTitleId() != FileSys::GetUpdateTitleID(title_id)) { + packed_update_raw = {}; + } + + const auto base_romfs = base_nca->GetRomFS(); + const auto dump_dir = + target == DumpRomFSTarget::Normal + ? Common::FS::GetSuyuPath(Common::FS::SuyuPath::DumpDir) + : Common::FS::GetSuyuPath(Common::FS::SuyuPath::SDMCDir) / "atmosphere" / "contents"; + const auto romfs_dir = fmt::format("{:016X}/romfs", title_id); + + const auto path = Common::FS::PathToUTF8String(dump_dir / romfs_dir); + + const FileSys::PatchManager pm{title_id, system->GetFileSystemController(), installed}; + auto romfs = pm.PatchRomFS(base_nca.get(), base_romfs, type, packed_update_raw, false); + + const auto out = VfsFilesystemCreateDirectoryWrapper(vfs, path, FileSys::OpenMode::ReadWrite); + + if (out == nullptr) { + failed(); + vfs->DeleteDirectory(path); + return; + } + + bool ok = false; + const QStringList selections{tr("Full"), tr("Skeleton")}; + const auto res = QInputDialog::getItem( + this, tr("Select RomFS Dump Mode"), + tr("Please select the how you would like the RomFS dumped.
Full will copy all of the " + "files into the new directory while
skeleton will only create the directory " + "structure."), + selections, 0, false, &ok); + if (!ok) { + failed(); + vfs->DeleteDirectory(path); + return; + } + + const auto extracted = FileSys::ExtractRomFS(romfs); + if (extracted == nullptr) { + failed(); + return; + } + + const auto full = res == selections.constFirst(); + + // The expected required space is the size of the RomFS + 1 GiB + const auto minimum_free_space = romfs->GetSize() + 0x40000000; + + if (full && Common::FS::GetFreeSpaceSize(path) < minimum_free_space) { + QMessageBox::warning(this, tr("RomFS Extraction Failed!"), + tr("There is not enough free space at %1 to extract the RomFS. Please " + "free up space or select a different dump directory at " + "Emulation > Configure > System > Filesystem > Dump Root") + .arg(QString::fromStdString(path))); + return; + } + + QProgressDialog progress(tr("Extracting RomFS..."), tr("Cancel"), 0, 100, this); + progress.setWindowModality(Qt::WindowModal); + progress.setMinimumDuration(100); + progress.setAutoClose(false); + progress.setAutoReset(false); + + size_t read_size = 0; + + if (RomFSRawCopy(romfs->GetSize(), read_size, progress, extracted, out, full)) { + progress.close(); + QMessageBox::information(this, tr("RomFS Extraction Succeeded!"), + tr("The operation completed successfully.")); + QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(path))); + } else { + progress.close(); + failed(); + vfs->DeleteDirectory(path); + } +} + void GMainWindow::OnGameListVerifyIntegrity(const std::string& game_path) { const auto NotImplemented = [this] { QMessageBox::warning(this, tr("Integrity verification couldn't be performed!"), @@ -4680,6 +4871,66 @@ void GMainWindow::SetFirmwareVersion() { firmware_label->setToolTip(QString::fromStdString(display_title)); } +bool GMainWindow::SelectRomFSDumpTarget(const FileSys::ContentProvider& installed, u64 program_id, + u64* selected_title_id, u8* selected_content_record_type) { + using ContentInfo = std::tuple; + boost::container::flat_set available_title_ids; + + const auto RetrieveEntries = [&](FileSys::TitleType title_type, + FileSys::ContentRecordType record_type) { + const auto entries = installed.ListEntriesFilter(title_type, record_type); + for (const auto& entry : entries) { + if (FileSys::GetBaseTitleID(entry.title_id) == program_id && + installed.GetEntry(entry)->GetStatus() == Loader::ResultStatus::Success) { + available_title_ids.insert({entry.title_id, title_type, record_type}); + } + } + }; + + RetrieveEntries(FileSys::TitleType::Application, FileSys::ContentRecordType::Program); + RetrieveEntries(FileSys::TitleType::Application, FileSys::ContentRecordType::HtmlDocument); + RetrieveEntries(FileSys::TitleType::Application, FileSys::ContentRecordType::LegalInformation); + RetrieveEntries(FileSys::TitleType::AOC, FileSys::ContentRecordType::Data); + + if (available_title_ids.empty()) { + return false; + } + + size_t title_index = 0; + + if (available_title_ids.size() > 1) { + QStringList list; + for (auto& [title_id, title_type, record_type] : available_title_ids) { + const auto hex_title_id = QString::fromStdString(fmt::format("{:X}", title_id)); + if (record_type == FileSys::ContentRecordType::Program) { + list.push_back(QStringLiteral("Program [%1]").arg(hex_title_id)); + } else if (record_type == FileSys::ContentRecordType::HtmlDocument) { + list.push_back(QStringLiteral("HTML document [%1]").arg(hex_title_id)); + } else if (record_type == FileSys::ContentRecordType::LegalInformation) { + list.push_back(QStringLiteral("Legal information [%1]").arg(hex_title_id)); + } else { + list.push_back( + QStringLiteral("DLC %1 [%2]").arg(title_id & 0x7FF).arg(hex_title_id)); + } + } + + bool ok; + const auto res = QInputDialog::getItem( + this, tr("Select RomFS Dump Target"), + tr("Please select which RomFS you would like to dump."), list, 0, false, &ok); + if (!ok) { + return false; + } + + title_index = list.indexOf(res); + } + + const auto& [title_id, title_type, record_type] = *available_title_ids.nth(title_index); + *selected_title_id = title_id; + *selected_content_record_type = static_cast(record_type); + return true; +} + bool GMainWindow::ConfirmClose() { if (emu_thread == nullptr || UISettings::values.confirm_before_stopping.GetValue() == ConfirmStop::Ask_Never) { diff --git a/src/suyu/main.h b/src/suyu/main.h index e20950e238..b2b0f9afc4 100644 --- a/src/suyu/main.h +++ b/src/suyu/main.h @@ -51,6 +51,7 @@ class WaitTreeWidget; enum class GameListOpenTarget; enum class GameListRemoveTarget; enum class GameListShortcutTarget; +enum class DumpRomFSTarget; enum class InstalledEntryType; class GameListPlaceholder; @@ -347,6 +348,7 @@ private slots: void OnGameListRemoveFile(u64 program_id, GameListRemoveTarget target, const std::string& game_path); void OnGameListRemovePlayTimeData(u64 program_id); + void OnGameListDumpRomFS(u64 program_id, const std::string& game_path, DumpRomFSTarget target); void OnGameListVerifyIntegrity(const std::string& game_path); void OnGameListCopyTID(u64 program_id); void OnGameListNavigateToGamedbEntry(u64 program_id,