diff --git a/Source/Core/Core/IOS/ES/Formats.cpp b/Source/Core/Core/IOS/ES/Formats.cpp index aa71ad32ee..0f5af9833c 100644 --- a/Source/Core/Core/IOS/ES/Formats.cpp +++ b/Source/Core/Core/IOS/ES/Formats.cpp @@ -67,6 +67,17 @@ bool Content::IsOptional() const return (type & 0x4000) != 0; } +bool operator==(const Content& lhs, const Content& rhs) +{ + auto fields = [](const Content& c) { return std::tie(c.id, c.index, c.type, c.size, c.sha1); }; + return fields(lhs) == fields(rhs); +} + +bool operator!=(const Content& lhs, const Content& rhs) +{ + return !operator==(lhs, rhs); +} + SignedBlobReader::SignedBlobReader(const std::vector& bytes) : m_bytes(bytes) { } diff --git a/Source/Core/Core/IOS/ES/Formats.h b/Source/Core/Core/IOS/ES/Formats.h index cbacb6cfcb..51d67f91d3 100644 --- a/Source/Core/Core/IOS/ES/Formats.h +++ b/Source/Core/Core/IOS/ES/Formats.h @@ -93,6 +93,8 @@ struct Content std::array sha1; }; static_assert(sizeof(Content) == 36, "Content has the wrong size"); +bool operator==(const Content&, const Content&); +bool operator!=(const Content&, const Content&); struct TimeLimit { diff --git a/Source/Core/Core/TitleDatabase.cpp b/Source/Core/Core/TitleDatabase.cpp index ea8b2916f1..29fe07e57b 100644 --- a/Source/Core/Core/TitleDatabase.cpp +++ b/Source/Core/Core/TitleDatabase.cpp @@ -14,6 +14,7 @@ #include "Common/MsgHandler.h" #include "Common/StringUtil.h" #include "Core/ConfigManager.h" +#include "Core/IOS/ES/Formats.h" #include "DiscIO/Enums.h" namespace Core @@ -165,6 +166,14 @@ std::string TitleDatabase::GetTitleName(const std::string& game_id, TitleType ty return iterator != map.end() ? iterator->second : ""; } +std::string TitleDatabase::GetTitleName(u64 title_id) const +{ + const std::string id{ + {static_cast((title_id >> 24) & 0xff), static_cast((title_id >> 16) & 0xff), + static_cast((title_id >> 8) & 0xff), static_cast(title_id & 0xff)}}; + return GetTitleName(id, IOS::ES::IsChannel(title_id) ? TitleType::Channel : TitleType::Other); +} + std::string TitleDatabase::Describe(const std::string& game_id, TitleType type) const { const std::string title_name = GetTitleName(game_id, type); diff --git a/Source/Core/Core/TitleDatabase.h b/Source/Core/Core/TitleDatabase.h index 6b6c53fd9d..27793407a3 100644 --- a/Source/Core/Core/TitleDatabase.h +++ b/Source/Core/Core/TitleDatabase.h @@ -7,6 +7,8 @@ #include #include +#include "Common/CommonTypes.h" + namespace Core { // Reader for title database files. @@ -25,6 +27,7 @@ public: // Get a user friendly title name for a game ID. // This falls back to returning an empty string if none could be found. std::string GetTitleName(const std::string& game_id, TitleType = TitleType::Other) const; + std::string GetTitleName(u64 title_id) const; // Get a description for a game ID (title name if available + game ID). std::string Describe(const std::string& game_id, TitleType = TitleType::Other) const; diff --git a/Source/Core/Core/WiiUtils.cpp b/Source/Core/Core/WiiUtils.cpp index 4ce3f03103..4235c9ba96 100644 --- a/Source/Core/Core/WiiUtils.cpp +++ b/Source/Core/Core/WiiUtils.cpp @@ -685,4 +685,106 @@ UpdateResult DoDiscUpdate(UpdateCallback update_callback, const std::string& ima DiscIO::NANDContentManager::Access().ClearCache(); return result; } + +NANDCheckResult CheckNAND(IOS::HLE::Kernel& ios) +{ + NANDCheckResult result; + const auto es = ios.GetES(); + + // Check for NANDs that were used with old Dolphin versions. + if (File::Exists(Common::RootUserPath(Common::FROM_CONFIGURED_ROOT) + "/sys/replace")) + { + ERROR_LOG(CORE, "CheckNAND: NAND was used with old versions, so it is likely to be damaged"); + result.bad = true; + } + + for (const u64 title_id : es->GetInstalledTitles()) + { + // Check for missing title sub directories. + if (!File::IsDirectory(Common::GetTitleContentPath(title_id, Common::FROM_CONFIGURED_ROOT))) + { + ERROR_LOG(CORE, "CheckNAND: Missing content directory for title %016" PRIx64, title_id); + result.bad = true; + } + if (!File::IsDirectory(Common::GetTitleDataPath(title_id, Common::FROM_CONFIGURED_ROOT))) + { + ERROR_LOG(CORE, "CheckNAND: Missing data directory for title %016" PRIx64, title_id); + result.bad = true; + } + + // Check for incomplete title installs (missing ticket, TMD or contents). + const auto ticket = DiscIO::FindSignedTicket(title_id); + if (!IOS::ES::IsDiscTitle(title_id) && !ticket.IsValid()) + { + ERROR_LOG(CORE, "CheckNAND: Missing ticket for title %016" PRIx64, title_id); + result.titles_to_remove.insert(title_id); + result.bad = true; + } + + const std::string content_dir = + Common::GetTitleContentPath(title_id, Common::FROM_CONFIGURED_ROOT); + + const auto tmd = es->FindInstalledTMD(title_id); + if (!tmd.IsValid()) + { + if (File::ScanDirectoryTree(content_dir, false).children.empty()) + { + WARN_LOG(CORE, "CheckNAND: Missing TMD for title %016" PRIx64, title_id); + } + else + { + ERROR_LOG(CORE, "CheckNAND: Missing TMD for title %016" PRIx64, title_id); + result.titles_to_remove.insert(title_id); + result.bad = true; + } + // Further checks require the TMD to be valid. + continue; + } + + const auto installed_contents = es->GetStoredContentsFromTMD(tmd); + const bool is_installed = std::any_of(installed_contents.begin(), installed_contents.end(), + [](const auto& content) { return !content.IsShared(); }); + + if (is_installed && installed_contents != tmd.GetContents() && + (tmd.GetTitleFlags() & IOS::ES::TitleFlags::TITLE_TYPE_WFS_MAYBE) == 0) + { + ERROR_LOG(CORE, "CheckNAND: Missing contents for title %016" PRIx64, title_id); + result.titles_to_remove.insert(title_id); + result.bad = true; + } + } + + return result; +} + +bool RepairNAND(IOS::HLE::Kernel& ios) +{ + const auto es = ios.GetES(); + + // Delete an old, unneeded file + File::Delete(Common::RootUserPath(Common::FROM_CONFIGURED_ROOT) + "/sys/replace"); + + for (const u64 title_id : es->GetInstalledTitles()) + { + // Create missing title sub directories. + const std::string content_dir = + Common::GetTitleContentPath(title_id, Common::FROM_CONFIGURED_ROOT); + const std::string data_dir = Common::GetTitleDataPath(title_id, Common::FROM_CONFIGURED_ROOT); + File::CreateDir(content_dir); + File::CreateDir(data_dir); + + // If there's nothing in the content directory and no ticket, + // this title shouldn't exist at all on the NAND. + // WARNING: This will delete associated save data! + const auto content_files = File::ScanDirectoryTree(content_dir, false).children; + const bool has_no_tmd_but_contents = + !es->FindInstalledTMD(title_id).IsValid() && !content_files.empty(); + if (has_no_tmd_but_contents || !DiscIO::FindSignedTicket(title_id).IsValid()) + { + const std::string title_dir = Common::GetTitlePath(title_id, Common::FROM_CONFIGURED_ROOT); + File::DeleteDirRecursively(title_dir); + } + } + return !CheckNAND(ios).bad; +} } diff --git a/Source/Core/Core/WiiUtils.h b/Source/Core/Core/WiiUtils.h index 49e8175bc2..0047e2c63d 100644 --- a/Source/Core/Core/WiiUtils.h +++ b/Source/Core/Core/WiiUtils.h @@ -7,11 +7,20 @@ #include #include #include +#include #include "Common/CommonTypes.h" // Small utility functions for common Wii related tasks. +namespace IOS +{ +namespace HLE +{ +class Kernel; +} +} + namespace WiiUtils { bool InstallWAD(const std::string& wad_path); @@ -48,4 +57,13 @@ UpdateResult DoOnlineUpdate(UpdateCallback update_callback, const std::string& r // Perform a disc update with behaviour similar to the System Menu. UpdateResult DoDiscUpdate(UpdateCallback update_callback, const std::string& image_path); + +// Check the emulated NAND for common issues. +struct NANDCheckResult +{ + bool bad = false; + std::unordered_set titles_to_remove; +}; +NANDCheckResult CheckNAND(IOS::HLE::Kernel& ios); +bool RepairNAND(IOS::HLE::Kernel& ios); } diff --git a/Source/Core/DolphinQt2/MenuBar.cpp b/Source/Core/DolphinQt2/MenuBar.cpp index 6fb6cc79a5..401f7f4d7e 100644 --- a/Source/Core/DolphinQt2/MenuBar.cpp +++ b/Source/Core/DolphinQt2/MenuBar.cpp @@ -22,6 +22,8 @@ #include "Core/IOS/IOS.h" #include "Core/Movie.h" #include "Core/State.h" +#include "Core/TitleDatabase.h" +#include "Core/WiiUtils.h" #include "DiscIO/NANDImporter.h" #include "DolphinQt2/AboutDialog.h" #include "DolphinQt2/GameList/GameFile.h" @@ -114,7 +116,7 @@ void MenuBar::AddToolsMenu() AddAction(tools_menu, QStringLiteral(""), this, [this] { emit BootWiiSystemMenu(); }); m_import_backup = AddAction(gc_ipl, tr("Import BootMii NAND Backup..."), this, [this] { emit ImportNANDBackup(); }); - + m_check_nand = AddAction(tools_menu, tr("Check NAND..."), this, &MenuBar::CheckNAND); m_extract_certificates = AddAction(tools_menu, tr("Extract Certificates from NAND"), this, &MenuBar::NANDExtractCertificates); @@ -473,6 +475,7 @@ void MenuBar::UpdateToolsMenu(bool emulation_started) m_pal_ipl->setEnabled(!emulation_started && File::Exists(SConfig::GetInstance().GetBootROMPath(EUR_DIR))); m_import_backup->setEnabled(!emulation_started); + m_check_nand->setEnabled(!emulation_started); if (!emulation_started) { @@ -532,6 +535,51 @@ void MenuBar::ExportWiiSaves() CWiiSaveCrypted::ExportAllSaves(); } +void MenuBar::CheckNAND() +{ + IOS::HLE::Kernel ios; + WiiUtils::NANDCheckResult result = WiiUtils::CheckNAND(ios); + if (!result.bad) + { + QMessageBox::information(this, tr("NAND Check"), tr("No issues have been detected.")); + return; + } + + QString message = tr("The emulated NAND is damaged. System titles such as the Wii Menu and " + "the Wii Shop Channel may not work correctly.\n\n" + "Do you want to try to repair the NAND?"); + if (!result.titles_to_remove.empty()) + { + message += tr("\n\nWARNING: Fixing this NAND requires the deletion of titles that have " + "incomplete data on the NAND, including all associated save data. " + "By continuing, the following title(s) will be removed:\n\n"); + Core::TitleDatabase title_db; + for (const u64 title_id : result.titles_to_remove) + { + const std::string name = title_db.GetTitleName(title_id); + message += !name.empty() ? + QStringLiteral("%1 (%2)") + .arg(QString::fromStdString(name)) + .arg(title_id, 16, 16, QLatin1Char('0')) : + QStringLiteral("%1").arg(title_id, 16, 16, QLatin1Char('0')); + message += QStringLiteral("\n"); + } + } + + if (QMessageBox::question(this, tr("NAND Check"), message) != QMessageBox::Yes) + return; + + if (WiiUtils::RepairNAND(ios)) + { + QMessageBox::information(this, tr("NAND Check"), tr("The NAND has been repaired.")); + return; + } + + QMessageBox::critical(this, tr("NAND Check"), + tr("The NAND could not be repaired. It is recommended to back up " + "your current data and start over with a fresh NAND.")); +} + void MenuBar::NANDExtractCertificates() { if (DiscIO::NANDImporter().ExtractCertificates(File::GetUserPath(D_WIIROOT_IDX))) diff --git a/Source/Core/DolphinQt2/MenuBar.h b/Source/Core/DolphinQt2/MenuBar.h index c4a8c6a41c..d9745e7e67 100644 --- a/Source/Core/DolphinQt2/MenuBar.h +++ b/Source/Core/DolphinQt2/MenuBar.h @@ -113,6 +113,7 @@ private: void InstallWAD(); void ImportWiiSave(); void ExportWiiSaves(); + void CheckNAND(); void NANDExtractCertificates(); void OnSelectionChanged(QSharedPointer game_file); @@ -131,6 +132,7 @@ private: QAction* m_ntscu_ipl; QAction* m_pal_ipl; QAction* m_import_backup; + QAction* m_check_nand; QAction* m_extract_certificates; // Emulation diff --git a/Source/Core/DolphinWX/Frame.h b/Source/Core/DolphinWX/Frame.h index a7005841ef..24cb56a94b 100644 --- a/Source/Core/DolphinWX/Frame.h +++ b/Source/Core/DolphinWX/Frame.h @@ -348,6 +348,7 @@ private: void OnInstallWAD(wxCommandEvent& event); void OnUninstallWAD(wxCommandEvent& event); void OnImportBootMiiBackup(wxCommandEvent& event); + void OnCheckNAND(wxCommandEvent& event); void OnExtractCertificates(wxCommandEvent& event); void OnPerformOnlineWiiUpdate(wxCommandEvent& event); void OnPerformDiscWiiUpdate(wxCommandEvent& event); diff --git a/Source/Core/DolphinWX/FrameTools.cpp b/Source/Core/DolphinWX/FrameTools.cpp index e8c7b4f487..592f338cd2 100644 --- a/Source/Core/DolphinWX/FrameTools.cpp +++ b/Source/Core/DolphinWX/FrameTools.cpp @@ -185,6 +185,7 @@ void CFrame::BindMenuBarEvents() Bind(wxEVT_MENU, &CFrame::OnInstallWAD, this, IDM_MENU_INSTALL_WAD); Bind(wxEVT_MENU, &CFrame::OnLoadWiiMenu, this, IDM_LOAD_WII_MENU); Bind(wxEVT_MENU, &CFrame::OnImportBootMiiBackup, this, IDM_IMPORT_NAND); + Bind(wxEVT_MENU, &CFrame::OnCheckNAND, this, IDM_CHECK_NAND); Bind(wxEVT_MENU, &CFrame::OnExtractCertificates, this, IDM_EXTRACT_CERTIFICATES); for (const int idm : {IDM_PERFORM_ONLINE_UPDATE_CURRENT, IDM_PERFORM_ONLINE_UPDATE_EUR, IDM_PERFORM_ONLINE_UPDATE_JPN, IDM_PERFORM_ONLINE_UPDATE_KOR, @@ -1308,6 +1309,48 @@ void CFrame::OnImportBootMiiBackup(wxCommandEvent& WXUNUSED(event)) UpdateLoadWiiMenuItem(); } +void CFrame::OnCheckNAND(wxCommandEvent&) +{ + IOS::HLE::Kernel ios; + WiiUtils::NANDCheckResult result = WiiUtils::CheckNAND(ios); + if (!result.bad) + { + wxMessageBox(_("No issues have been detected."), _("NAND Check"), wxOK | wxICON_INFORMATION); + return; + } + + wxString message = _("The emulated NAND is damaged. System titles such as the Wii Menu and " + "the Wii Shop Channel may not work correctly.\n\n" + "Do you want to try to repair the NAND?"); + if (!result.titles_to_remove.empty()) + { + message += _("\n\nWARNING: Fixing this NAND requires the deletion of titles that have " + "incomplete data on the NAND, including all associated save data. " + "By continuing, the following title(s) will be removed:\n\n"); + Core::TitleDatabase title_db; + for (const u64 title_id : result.titles_to_remove) + { + const std::string name = title_db.GetTitleName(title_id); + message += !name.empty() ? StringFromFormat("%s (%016" PRIx64 ")", name.c_str(), title_id) : + StringFromFormat("%016" PRIx64, title_id); + message += "\n"; + } + } + + if (wxMessageBox(message, _("NAND Check"), wxYES_NO) != wxYES) + return; + + if (WiiUtils::RepairNAND(ios)) + { + wxMessageBox(_("The NAND has been repaired."), _("NAND Check"), wxOK | wxICON_INFORMATION); + return; + } + + wxMessageBox(_("The NAND could not be repaired. It is recommended to back up " + "your current data and start over with a fresh NAND."), + _("NAND Check"), wxOK | wxICON_ERROR); +} + void CFrame::OnExtractCertificates(wxCommandEvent& WXUNUSED(event)) { DiscIO::NANDImporter().ExtractCertificates(File::GetUserPath(D_WIIROOT_IDX)); diff --git a/Source/Core/DolphinWX/Globals.h b/Source/Core/DolphinWX/Globals.h index dad9550cd7..1975716bce 100644 --- a/Source/Core/DolphinWX/Globals.h +++ b/Source/Core/DolphinWX/Globals.h @@ -105,6 +105,7 @@ enum IDM_LIST_INSTALL_WAD, IDM_LIST_UNINSTALL_WAD, IDM_IMPORT_NAND, + IDM_CHECK_NAND, IDM_EXTRACT_CERTIFICATES, IDM_PERFORM_ONLINE_UPDATE_CURRENT, IDM_PERFORM_ONLINE_UPDATE_EUR, diff --git a/Source/Core/DolphinWX/MainMenuBar.cpp b/Source/Core/DolphinWX/MainMenuBar.cpp index c9b505e891..fdb6b633c6 100644 --- a/Source/Core/DolphinWX/MainMenuBar.cpp +++ b/Source/Core/DolphinWX/MainMenuBar.cpp @@ -235,6 +235,7 @@ wxMenu* MainMenuBar::CreateToolsMenu() const tools_menu->Append(IDM_MENU_INSTALL_WAD, _("Install WAD...")); tools_menu->Append(IDM_LOAD_WII_MENU, dummy_string); tools_menu->Append(IDM_IMPORT_NAND, _("Import BootMii NAND Backup...")); + tools_menu->Append(IDM_CHECK_NAND, _("Check NAND...")); tools_menu->Append(IDM_EXTRACT_CERTIFICATES, _("Extract Certificates from NAND")); auto* const online_update_menu = new wxMenu; online_update_menu->Append(IDM_PERFORM_ONLINE_UPDATE_CURRENT, _("Current Region")); @@ -582,7 +583,7 @@ void MainMenuBar::RefreshWiiToolsLabels() const // inconsistent data. const bool enable_wii_tools = !Core::IsRunning() || !SConfig::GetInstance().bWii; for (const int index : - {IDM_MENU_INSTALL_WAD, IDM_EXPORT_ALL_SAVE, IDM_IMPORT_SAVE, IDM_IMPORT_NAND, + {IDM_MENU_INSTALL_WAD, IDM_EXPORT_ALL_SAVE, IDM_IMPORT_SAVE, IDM_IMPORT_NAND, IDM_CHECK_NAND, IDM_EXTRACT_CERTIFICATES, IDM_LOAD_WII_MENU, IDM_PERFORM_ONLINE_UPDATE_CURRENT, IDM_PERFORM_ONLINE_UPDATE_EUR, IDM_PERFORM_ONLINE_UPDATE_JPN, IDM_PERFORM_ONLINE_UPDATE_KOR, IDM_PERFORM_ONLINE_UPDATE_USA})