diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp index 93170b83a..3b5310109 100644 --- a/src/citra_qt/game_list.cpp +++ b/src/citra_qt/game_list.cpp @@ -27,6 +27,8 @@ #include "citra_qt/ui_settings.h" #include "common/common_paths.h" #include "common/logging/log.h" +#include "core/file_sys/archive_extsavedata.h" +#include "core/file_sys/archive_source_sd_savedata.h" #include "core/hle/service/fs/archive.h" #include "core/loader/loader.h" @@ -409,7 +411,9 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { QMenu context_menu; switch (selected.data(GameListItem::TypeRole).value()) { case GameListItemType::Game: - AddGamePopup(context_menu, selected.data(GameListItemPath::ProgramIdRole).toULongLong()); + AddGamePopup(context_menu, selected.data(GameListItemPath::FullPathRole).toString(), + selected.data(GameListItemPath::ProgramIdRole).toULongLong(), + selected.data(GameListItemPath::ExtdataIdRole).toULongLong()); break; case GameListItemType::CustomDir: AddPermDirPopup(context_menu, selected); @@ -423,23 +427,46 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location)); } -void GameList::AddGamePopup(QMenu& context_menu, u64 program_id) { +void GameList::AddGamePopup(QMenu& context_menu, const QString& path, u64 program_id, + u64 extdata_id) { QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location")); + QAction* open_extdata_location = context_menu.addAction(tr("Open Extra Data Location")); QAction* open_application_location = context_menu.addAction(tr("Open Application Location")); QAction* open_update_location = context_menu.addAction(tr("Open Update Data Location")); QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); - open_save_location->setEnabled(program_id != 0); - open_application_location->setVisible(FileUtil::Exists( - Service::AM::GetTitleContentPath(Service::FS::MediaType::SDMC, program_id))); - open_update_location->setEnabled(0x0004000000000000 <= program_id && - program_id <= 0x00040000FFFFFFFF); + const bool is_application = + 0x0004000000000000 <= program_id && program_id <= 0x00040000FFFFFFFF; + + std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); + open_save_location->setVisible( + is_application && FileUtil::Exists(FileSys::ArchiveSource_SDSaveData::GetSaveDataPathFor( + sdmc_dir, program_id))); + + if (extdata_id) { + open_extdata_location->setVisible( + is_application && + FileUtil::Exists(FileSys::GetExtDataPathFromId(sdmc_dir, extdata_id))); + } else { + open_extdata_location->setVisible(false); + } + + auto media_type = Service::AM::GetTitleMediaType(program_id); + open_application_location->setVisible(path.toStdString() == + Service::AM::GetTitleContentPath(media_type, program_id)); + open_update_location->setVisible( + is_application && FileUtil::Exists(Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, + program_id + 0xe00000000) + + "content/")); auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); navigate_to_gamedb_entry->setVisible(it != compatibility_list.end()); connect(open_save_location, &QAction::triggered, [this, program_id] { emit OpenFolderRequested(program_id, GameListOpenTarget::SAVE_DATA); }); + connect(open_extdata_location, &QAction::triggered, [this, extdata_id] { + emit OpenFolderRequested(extdata_id, GameListOpenTarget::EXT_DATA); + }); connect(open_application_location, &QAction::triggered, [this, program_id] { emit OpenFolderRequested(program_id, GameListOpenTarget::APPLICATION); }); @@ -651,6 +678,9 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign u64 program_id = 0; loader->ReadProgramId(program_id); + u64 extdata_id = 0; + loader->ReadExtdataId(extdata_id); + std::vector smdh = [program_id, &loader]() -> std::vector { std::vector original_smdh; loader->ReadIcon(original_smdh); @@ -683,7 +713,8 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign emit EntryReady( { - new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id), + new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id, + extdata_id), new GameListItemCompat(compatibility), new GameListItemRegion(smdh), new GameListItem( diff --git a/src/citra_qt/game_list.h b/src/citra_qt/game_list.h index f8102e0b9..ffa1d64e7 100644 --- a/src/citra_qt/game_list.h +++ b/src/citra_qt/game_list.h @@ -27,7 +27,7 @@ class QTreeView; class QToolButton; class QVBoxLayout; -enum class GameListOpenTarget { SAVE_DATA = 0, APPLICATION = 1, UPDATE_DATA = 2 }; +enum class GameListOpenTarget { SAVE_DATA = 0, EXT_DATA = 1, APPLICATION = 2, UPDATE_DATA = 3 }; class GameList : public QWidget { Q_OBJECT @@ -89,7 +89,7 @@ private: void RefreshGameDirectory(); void PopupContextMenu(const QPoint& menu_location); - void AddGamePopup(QMenu& context_menu, u64 program_id); + void AddGamePopup(QMenu& context_menu, const QString& path, u64 program_id, u64 extdata_id); void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected); void AddPermDirPopup(QMenu& context_menu, QModelIndex selected); diff --git a/src/citra_qt/game_list_p.h b/src/citra_qt/game_list_p.h index 9fcfe1742..93aaedac2 100644 --- a/src/citra_qt/game_list_p.h +++ b/src/citra_qt/game_list_p.h @@ -135,12 +135,15 @@ public: static const int TitleRole = SortRole; static const int FullPathRole = SortRole + 1; static const int ProgramIdRole = SortRole + 2; + static const int ExtdataIdRole = SortRole + 3; GameListItemPath() = default; - GameListItemPath(const QString& game_path, const std::vector& smdh_data, u64 program_id) { + GameListItemPath(const QString& game_path, const std::vector& smdh_data, u64 program_id, + u64 extdata_id) { setData(type(), TypeRole); setData(game_path, FullPathRole); setData(qulonglong(program_id), ProgramIdRole); + setData(qulonglong(extdata_id), ExtdataIdRole); if (!Loader::IsValidSMDH(smdh_data)) { // SMDH is not valid, set a default icon diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index ba569e501..d475a8539 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -52,6 +52,7 @@ #include "common/scm_rev.h" #include "common/scope_exit.h" #include "core/core.h" +#include "core/file_sys/archive_extsavedata.h" #include "core/file_sys/archive_source_sd_savedata.h" #include "core/frontend/applets/default_applets.h" #include "core/gdbstub/gdbstub.h" @@ -879,7 +880,7 @@ void GMainWindow::OnGameListLoadFile(QString game_path) { BootGame(game_path); } -void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target) { +void GMainWindow::OnGameListOpenFolder(u64 data_id, GameListOpenTarget target) { std::string path; std::string open_target; @@ -887,16 +888,24 @@ void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target case GameListOpenTarget::SAVE_DATA: { open_target = "Save Data"; std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); - path = FileSys::ArchiveSource_SDSaveData::GetSaveDataPathFor(sdmc_dir, program_id); + path = FileSys::ArchiveSource_SDSaveData::GetSaveDataPathFor(sdmc_dir, data_id); break; } - case GameListOpenTarget::APPLICATION: - open_target = "Application"; - path = Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, program_id) + "content/"; + case GameListOpenTarget::EXT_DATA: { + open_target = "Extra Data"; + std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); + path = FileSys::GetExtDataPathFromId(sdmc_dir, data_id); break; + } + case GameListOpenTarget::APPLICATION: { + open_target = "Application"; + auto media_type = Service::AM::GetTitleMediaType(data_id); + path = Service::AM::GetTitlePath(media_type, data_id) + "content/"; + break; + } case GameListOpenTarget::UPDATE_DATA: open_target = "Update Data"; - path = Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, program_id + 0xe00000000) + + path = Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, data_id + 0xe00000000) + "content/"; break; default: @@ -914,7 +923,7 @@ void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target return; } - LOG_INFO(Frontend, "Opening {} path for program_id={:016x}", open_target, program_id); + LOG_INFO(Frontend, "Opening {} path for data_id={:016x}", open_target, data_id); QDesktopServices::openUrl(QUrl::fromLocalFile(qpath)); } diff --git a/src/core/file_sys/archive_extsavedata.cpp b/src/core/file_sys/archive_extsavedata.cpp index e8da21193..d49c7bfb1 100644 --- a/src/core/file_sys/archive_extsavedata.cpp +++ b/src/core/file_sys/archive_extsavedata.cpp @@ -176,6 +176,13 @@ std::string GetExtDataContainerPath(const std::string& mount_point, bool shared) return fmt::format("{}Nintendo 3DS/{}/{}/extdata/", mount_point, SYSTEM_ID, SDCARD_ID); } +std::string GetExtDataPathFromId(const std::string& mount_point, u64 extdata_id) { + u32 high = static_cast(extdata_id >> 32); + u32 low = static_cast(extdata_id & 0xFFFFFFFF); + + return fmt::format("{}{:08x}/{:08x}/", GetExtDataContainerPath(mount_point, false), high, low); +} + Path ConstructExtDataBinaryPath(u32 media_type, u32 high, u32 low) { ExtSaveDataArchivePath path; path.media_type = media_type; diff --git a/src/core/file_sys/archive_extsavedata.h b/src/core/file_sys/archive_extsavedata.h index 79d266df4..6774ab195 100644 --- a/src/core/file_sys/archive_extsavedata.h +++ b/src/core/file_sys/archive_extsavedata.h @@ -70,6 +70,15 @@ private: */ std::string GetExtSaveDataPath(const std::string& mount_point, const Path& path); +/** + * Constructs a path to the concrete ExtData archive in the host filesystem based on the + * extdata ID and base mount point. + * @param mount_point The base mount point of the ExtSaveData archives. + * @param extdata_id The id of the ExtSaveData + * @returns The complete path to the specified extdata archive in the host filesystem + */ +std::string GetExtDataPathFromId(const std::string& mount_point, u64 extdata_id); + /** * Constructs a path to the base folder to hold concrete ExtSaveData archives in the host file * system. diff --git a/src/core/file_sys/ncch_container.cpp b/src/core/file_sys/ncch_container.cpp index 70f1c27f9..e817a004f 100644 --- a/src/core/file_sys/ncch_container.cpp +++ b/src/core/file_sys/ncch_container.cpp @@ -576,6 +576,23 @@ Loader::ResultStatus NCCHContainer::ReadProgramId(u64_le& program_id) { return Loader::ResultStatus::Success; } +Loader::ResultStatus NCCHContainer::ReadExtdataId(u64& extdata_id) { + Loader::ResultStatus result = Load(); + if (result != Loader::ResultStatus::Success) + return result; + + if (!has_exheader) + return Loader::ResultStatus::ErrorNotUsed; + + if (exheader_header.arm11_system_local_caps.storage_info.other_attributes >> 1) { + // Extdata id is not present when using extended savedata access + return Loader::ResultStatus::ErrorNotUsed; + } + + extdata_id = exheader_header.arm11_system_local_caps.storage_info.ext_save_data_id; + return Loader::ResultStatus::Success; +} + bool NCCHContainer::HasExeFS() { Loader::ResultStatus result = Load(); if (result != Loader::ResultStatus::Success) diff --git a/src/core/file_sys/ncch_container.h b/src/core/file_sys/ncch_container.h index f5d0f8db2..bee740e3b 100644 --- a/src/core/file_sys/ncch_container.h +++ b/src/core/file_sys/ncch_container.h @@ -125,7 +125,7 @@ struct ExHeader_SystemInfo { }; struct ExHeader_StorageInfo { - u8 ext_save_data_id[8]; + u64_le ext_save_data_id; u8 system_save_data_id[8]; u8 reserved[8]; u8 access_info[7]; @@ -251,6 +251,12 @@ public: */ Loader::ResultStatus ReadProgramId(u64_le& program_id); + /** + * Get the Extdata ID of the NCCH container + * @return ResultStatus result of function + */ + Loader::ResultStatus ReadExtdataId(u64& extdata_id); + /** * Checks whether the NCCH container contains an ExeFS * @return bool check result diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h index f6b4fcccd..5b86fc28e 100644 --- a/src/core/loader/loader.h +++ b/src/core/loader/loader.h @@ -157,6 +157,15 @@ public: return ResultStatus::ErrorNotImplemented; } + /** + * Get the extdata id for the application + * @param out_extdata_id Reference to store extdata id into + * @return ResultStatus result of function + */ + virtual ResultStatus ReadExtdataId(u64& out_extdata_id) { + return ResultStatus::ErrorNotImplemented; + } + /** * Get the RomFS of the application * Since the RomFS can be huge, we return a file reference instead of copying to a buffer diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index 2b29a0f98..12301cb94 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -216,6 +216,14 @@ ResultStatus AppLoader_NCCH::ReadProgramId(u64& out_program_id) { return ResultStatus::Success; } +ResultStatus AppLoader_NCCH::ReadExtdataId(u64& out_extdata_id) { + ResultStatus result = base_ncch.ReadExtdataId(out_extdata_id); + if (result != ResultStatus::Success) + return result; + + return ResultStatus::Success; +} + ResultStatus AppLoader_NCCH::ReadRomFS(std::shared_ptr& romfs_file) { return base_ncch.ReadRomFS(romfs_file); } diff --git a/src/core/loader/ncch.h b/src/core/loader/ncch.h index dcc5c1e59..472bab80f 100644 --- a/src/core/loader/ncch.h +++ b/src/core/loader/ncch.h @@ -51,6 +51,8 @@ public: ResultStatus ReadProgramId(u64& out_program_id) override; + ResultStatus ReadExtdataId(u64& out_extdata_id) override; + ResultStatus ReadRomFS(std::shared_ptr& romfs_file) override; ResultStatus ReadUpdateRomFS(std::shared_ptr& romfs_file) override;