// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include #include #include #include "common/fs/path_util.h" #include "common/hex_util.h" #include "common/logging/log.h" #include "common/settings.h" #ifndef _WIN32 #include "common/string_util.h" #endif #include "core/core.h" #include "core/file_sys/common_funcs.h" #include "core/file_sys/content_archive.h" #include "core/file_sys/control_metadata.h" #include "core/file_sys/ips_layer.h" #include "core/file_sys/patch_manager.h" #include "core/file_sys/registered_cache.h" #include "core/file_sys/romfs.h" #include "core/file_sys/vfs/vfs_cached.h" #include "core/file_sys/vfs/vfs_layered.h" #include "core/file_sys/vfs/vfs_vector.h" #include "core/hle/service/filesystem/filesystem.h" #include "core/hle/service/ns/language.h" #include "core/hle/service/set/settings_server.h" #include "core/loader/loader.h" #include "core/loader/nso.h" #include "core/memory/cheat_engine.h" namespace FileSys { namespace { constexpr u32 SINGLE_BYTE_MODULUS = 0x100; enum class TitleVersionFormat : u8 { ThreeElements, ///< vX.Y.Z FourElements, ///< vX.Y.Z.W }; std::string FormatTitleVersion(u32 version, TitleVersionFormat format = TitleVersionFormat::ThreeElements) { std::array bytes{}; bytes[0] = static_cast(version % SINGLE_BYTE_MODULUS); for (std::size_t i = 1; i < bytes.size(); ++i) { version /= SINGLE_BYTE_MODULUS; bytes[i] = static_cast(version % SINGLE_BYTE_MODULUS); } if (format == TitleVersionFormat::FourElements) { return fmt::format("v{}.{}.{}.{}", bytes[3], bytes[2], bytes[1], bytes[0]); } return fmt::format("v{}.{}.{}", bytes[3], bytes[2], bytes[1]); } VirtualDir FindSubdirectoryCaseless(const VirtualDir dir, std::string_view name) { #ifdef _WIN32 return dir->GetSubdirectory(name); #else const auto subdirs = dir->GetSubdirectories(); for (const auto& subdir : subdirs) { std::string dir_name = Common::ToLower(subdir->GetName()); if (dir_name == name) { return subdir; } } return nullptr; #endif } std::optional> ReadCheatFileFromFolder( u64 title_id, const PatchManager::BuildID& build_id_, const VirtualDir& base_path, bool upper) { const auto build_id_raw = Common::HexToString(build_id_, upper); const auto build_id = build_id_raw.substr(0, sizeof(u64) * 2); const auto file = base_path->GetFile(fmt::format("{}.txt", build_id)); if (file == nullptr) { LOG_INFO(Common_Filesystem, "No cheats file found for title_id={:016X}, build_id={}", title_id, build_id); return std::nullopt; } std::vector data(file->GetSize()); if (file->Read(data.data(), data.size()) != data.size()) { LOG_INFO(Common_Filesystem, "Failed to read cheats file for title_id={:016X}, build_id={}", title_id, build_id); return std::nullopt; } const Core::Memory::TextCheatParser parser; return parser.Parse(std::string_view(reinterpret_cast(data.data()), data.size())); } void AppendCommaIfNotEmpty(std::string& to, std::string_view with) { if (to.empty()) { to += with; } else { to += ", "; to += with; } } bool IsValidModDir(const VirtualDir& dir) { if (!dir) return false; return FindSubdirectoryCaseless(dir, "exefs") != nullptr || FindSubdirectoryCaseless(dir, "romfs") != nullptr || FindSubdirectoryCaseless(dir, "romfslite") != nullptr || FindSubdirectoryCaseless(dir, "cheats") != nullptr; } std::vector GetEnabledModsList( u64 title_id, const Service::FileSystem::FileSystemController& fs_controller) { std::vector mods; const auto load_dir = fs_controller.GetModificationLoadRoot(title_id); if (!load_dir) return mods; const auto& disabled = Settings::values.disabled_addons[title_id]; for (const auto& top_dir : load_dir->GetSubdirectories()) { if (!top_dir) continue; // If it's a mod directory (has exefs/romfs), check if it's disabled. if (IsValidModDir(top_dir)) { if (std::find(disabled.begin(), disabled.end(), top_dir->GetName()) == disabled.end()) { mods.push_back(top_dir); } } else { // If it's not a mod dir itself, check one level deeper for the actual mods. for (const auto& sub_dir : top_dir->GetSubdirectories()) { if (sub_dir && IsValidModDir(sub_dir)) { std::string internal_name = top_dir->GetName() + "/" + sub_dir->GetName(); // First check if the full nested path is disabled, then check if just the // subfolder name is disabled. if (std::find(disabled.begin(), disabled.end(), internal_name) == disabled.end() && std::find(disabled.begin(), disabled.end(), sub_dir->GetName()) == disabled.end()) { mods.push_back(sub_dir); } } } } } return mods; } bool IsDirValidAndNonEmpty(const VirtualDir& dir) { return dir != nullptr && (!dir->GetFiles().empty() || !dir->GetSubdirectories().empty()); } } // Anonymous namespace PatchManager::PatchManager(u64 title_id_, const Service::FileSystem::FileSystemController& fs_controller_, const ContentProvider& content_provider_) : title_id{title_id_}, fs_controller{fs_controller_}, content_provider{content_provider_} {} PatchManager::~PatchManager() = default; u64 PatchManager::GetTitleID() const { return title_id; } VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const { LOG_INFO(Loader, "Patching ExeFS for title_id={:016X}", title_id); if (exefs == nullptr) return exefs; const auto& disabled = Settings::values.disabled_addons[title_id]; bool autoloader_update_applied = false; // --- AUTOLOADER UPDATE (PRIORITY) --- VirtualDir sdmc_root = nullptr; if (fs_controller.OpenSDMC(&sdmc_root).IsSuccess() && sdmc_root) { const auto autoloader_updates_path = fmt::format("autoloader/{:016X}/Updates", title_id); const auto updates_dir = sdmc_root->GetSubdirectory(autoloader_updates_path); if (updates_dir) { const auto base_program_nca = content_provider.GetEntry(title_id, ContentRecordType::Program); if (base_program_nca) { for (const auto& mod : updates_dir->GetSubdirectories()) { if (mod && std::find(disabled.cbegin(), disabled.cend(), mod->GetName()) == disabled.cend()) { for (const auto& file : mod->GetFiles()) { if (file->GetExtension() == "nca") { NCA nca(file, base_program_nca.get()); if (nca.GetStatus() == Loader::ResultStatus::Success && nca.GetType() == NCAContentType::Program) { LOG_INFO( Loader, " ExeFS: Autoloader Update ({}) applied successfully", mod->GetName()); exefs = nca.GetExeFS(); autoloader_update_applied = true; break; } } } } if (autoloader_update_applied) break; } } } } // --- NAND UPDATE (FALLBACK) --- // --- NAND/External UPDATE (FALLBACK) --- if (!autoloader_update_applied) { // Find the highest version enabled update u32 best_version = 0; VirtualFile best_update_raw = nullptr; bool found_best = false; // 1. External Updates const auto* content_provider_union = dynamic_cast(&content_provider); if (content_provider_union) { const auto* external_provider = content_provider_union->GetExternalProvider(); if (external_provider) { const auto updates = external_provider->ListUpdateVersions(title_id); for (const auto& update : updates) { const auto name = fmt::format("Update {}", update.version_string); const auto patch_disabled = std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend(); if (!patch_disabled) { if (!found_best || update.version > best_version) { best_version = update.version; best_update_raw = external_provider->GetEntryForVersion( title_id, ContentRecordType::Program, update.version); found_best = true; } } } } } // 2. System Updates const auto update_tid = GetUpdateTitleID(title_id); PatchManager update_mgr{update_tid, fs_controller, content_provider}; const auto metadata = update_mgr.GetControlMetadata(); const auto& nacp = metadata.first; if (nacp) { const auto version_str = nacp->GetVersionString(); const auto name = fmt::format("Update v{}", version_str); // Matches GetPatches NACP branch const auto patch_disabled = std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend(); if (!patch_disabled) { const auto sys_ver = content_provider.GetEntryVersion(update_tid).value_or(0); if (!found_best || sys_ver > best_version) { best_version = sys_ver; best_update_raw = content_provider.GetEntryRaw(update_tid, ContentRecordType::Program); found_best = true; } } } else if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) { const auto meta_ver = content_provider.GetEntryVersion(update_tid); if (meta_ver.value_or(0) != 0) { const auto version_str = FormatTitleVersion(*meta_ver); const auto name = fmt::format("Update {}", version_str); // Matches GetPatches fallback const auto patch_disabled = std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend(); if (!patch_disabled) { const auto sys_ver = *meta_ver; if (!found_best || sys_ver > best_version) { best_version = sys_ver; best_update_raw = content_provider.GetEntryRaw(update_tid, ContentRecordType::Program); found_best = true; } } } } // Apply the best update found if (found_best && best_update_raw != nullptr) { // We need base_program_nca for patching const auto base_program_nca = content_provider.GetEntry(title_id, ContentRecordType::Program); if (base_program_nca) { const auto new_nca = std::make_shared(best_update_raw, base_program_nca.get()); if (new_nca->GetStatus() == Loader::ResultStatus::Success && new_nca->GetExeFS() != nullptr) { LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully", FormatTitleVersion(best_version)); exefs = new_nca->GetExeFS(); } } else { // Fallback if no base program (unlikely for patching ExeFS) // Or if it's a type that doesn't strictly need base? const auto new_nca = std::make_shared(best_update_raw, nullptr); if (new_nca->GetStatus() == Loader::ResultStatus::Success && new_nca->GetExeFS() != nullptr) { LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully (No Base)", FormatTitleVersion(best_version)); exefs = new_nca->GetExeFS(); } } } } // LayeredExeFS std::vector patch_dirs = GetEnabledModsList(title_id, fs_controller); // SDMC (Atmosphere) logic const auto sdmc_load_dir = fs_controller.GetSDMCModificationLoadRoot(title_id); if (sdmc_load_dir && std::find(disabled.begin(), disabled.end(), "SDMC") == disabled.end()) { patch_dirs.push_back(sdmc_load_dir); } std::sort(patch_dirs.begin(), patch_dirs.end(), [](const VirtualDir& l, const VirtualDir& r) { if (!l) return true; if (!r) return false; return l->GetName() < r->GetName(); }); std::vector layers; layers.reserve(patch_dirs.size() + 1); for (const auto& subdir : patch_dirs) { if (!subdir) continue; auto exefs_dir = FindSubdirectoryCaseless(subdir, "exefs"); if (exefs_dir != nullptr) layers.push_back(std::move(exefs_dir)); } if (exefs) { layers.push_back(exefs); } auto layered = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers)); if (layered != nullptr) { LOG_INFO(Loader, " ExeFS: LayeredExeFS patches applied successfully"); exefs = std::move(layered); } if (Settings::values.dump_exefs) { LOG_INFO(Loader, "Dumping ExeFS for title_id={:016X}", title_id); const auto dump_dir = fs_controller.GetModificationDumpRoot(title_id); if (dump_dir != nullptr) { const auto exefs_dir = GetOrCreateDirectoryRelative(dump_dir, "/exefs"); VfsRawCopyD(exefs, exefs_dir); } } return exefs; } std::vector PatchManager::CollectPatches(const std::vector& patch_dirs, const std::string& build_id) const { const auto nso_build_id = fmt::format("{:0<64}", build_id); std::vector out; out.reserve(patch_dirs.size()); for (const auto& subdir : patch_dirs) { if (!subdir) continue; auto exefs_dir = FindSubdirectoryCaseless(subdir, "exefs"); if (exefs_dir != nullptr) { for (const auto& file : exefs_dir->GetFiles()) { if (file->GetExtension() == "ips") { auto name = file->GetName(); const auto this_build_id = fmt::format("{:0<64}", name.substr(0, name.find('.'))); if (nso_build_id == this_build_id) out.push_back(file); } else if (file->GetExtension() == "pchtxt") { IPSwitchCompiler compiler{file}; if (!compiler.IsValid()) continue; const auto this_build_id = Common::HexToString(compiler.GetBuildID()); if (nso_build_id == this_build_id) out.push_back(file); } } } } return out; } std::vector PatchManager::PatchNSO(const std::vector& nso, const std::string& name) const { if (nso.size() < sizeof(Loader::NSOHeader)) { return nso; } Loader::NSOHeader header; std::memcpy(&header, nso.data(), sizeof(header)); if (header.magic != Common::MakeMagic('N', 'S', 'O', '0')) { return nso; } const auto build_id_raw = Common::HexToString(header.build_id); const auto build_id = build_id_raw.substr(0, build_id_raw.find_last_not_of('0') + 1); if (Settings::values.dump_nso) { LOG_INFO(Loader, "Dumping NSO for name={}, build_id={}, title_id={:016X}", name, build_id, title_id); const auto dump_dir = fs_controller.GetModificationDumpRoot(title_id); if (dump_dir != nullptr) { const auto nso_dir = GetOrCreateDirectoryRelative(dump_dir, "/nso"); const auto file = nso_dir->CreateFile(fmt::format("{}-{}.nso", name, build_id)); file->Resize(nso.size()); file->WriteBytes(nso); } } LOG_INFO(Loader, "Patching NSO for name={}, build_id={}", name, build_id); std::vector patch_dirs = GetEnabledModsList(title_id, fs_controller); std::sort(patch_dirs.begin(), patch_dirs.end(), [](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); }); const auto patches = CollectPatches(patch_dirs, build_id); auto out = nso; for (const auto& patch_file : patches) { if (patch_file->GetExtension() == "ips") { LOG_INFO(Loader, " - Applying IPS patch from mod \"{}\"", patch_file->GetContainingDirectory()->GetParentDirectory()->GetName()); const auto patched = PatchIPS(std::make_shared(out), patch_file); if (patched != nullptr) out = patched->ReadAllBytes(); } else if (patch_file->GetExtension() == "pchtxt") { LOG_INFO(Loader, " - Applying IPSwitch patch from mod \"{}\"", patch_file->GetContainingDirectory()->GetParentDirectory()->GetName()); const IPSwitchCompiler compiler{patch_file}; const auto patched = compiler.Apply(std::make_shared(out)); if (patched != nullptr) out = patched->ReadAllBytes(); } } if (out.size() < sizeof(Loader::NSOHeader)) { return nso; } std::memcpy(out.data(), &header, sizeof(header)); return out; } bool PatchManager::HasNSOPatch(const BuildID& build_id_, std::string_view name) const { const auto build_id_raw = Common::HexToString(build_id_); const auto build_id = build_id_raw.substr(0, build_id_raw.find_last_not_of('0') + 1); LOG_INFO(Loader, "Querying NSO patch existence for build_id={}, name={}", build_id, name); std::vector patch_dirs = GetEnabledModsList(title_id, fs_controller); std::sort(patch_dirs.begin(), patch_dirs.end(), [](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); }); return !CollectPatches(patch_dirs, build_id).empty(); } std::vector PatchManager::CreateCheatList( const BuildID& build_id_) const { std::vector patch_dirs = GetEnabledModsList(title_id, fs_controller); std::sort(patch_dirs.begin(), patch_dirs.end(), [](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); }); std::vector out; for (const auto& subdir : patch_dirs) { auto cheats_dir = FindSubdirectoryCaseless(subdir, "cheats"); if (cheats_dir != nullptr) { if (const auto res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, true)) { std::copy(res->begin(), res->end(), std::back_inserter(out)); continue; } if (const auto res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, false)) { std::copy(res->begin(), res->end(), std::back_inserter(out)); } } } return out; } static void ApplyLayeredFS(VirtualFile& romfs, u64 title_id, ContentRecordType type, const Service::FileSystem::FileSystemController& fs_controller) { const auto load_dir = fs_controller.GetModificationLoadRoot(title_id); const auto sdmc_load_dir = fs_controller.GetSDMCModificationLoadRoot(title_id); if ((type != ContentRecordType::Program && type != ContentRecordType::Data && type != ContentRecordType::HtmlDocument) || (load_dir == nullptr && sdmc_load_dir == nullptr)) { return; } const auto& disabled = Settings::values.disabled_addons[title_id]; std::vector patch_dirs = GetEnabledModsList(title_id, fs_controller); if (sdmc_load_dir && std::find(disabled.cbegin(), disabled.cend(), "SDMC") == disabled.cend()) { patch_dirs.push_back(sdmc_load_dir); } std::sort(patch_dirs.begin(), patch_dirs.end(), [](const VirtualDir& l, const VirtualDir& r) { if (!l) return true; if (!r) return false; return l->GetName() < r->GetName(); }); std::vector layers; std::vector layers_ext; layers.reserve(patch_dirs.size() + 1); layers_ext.reserve(patch_dirs.size() + 1); for (const auto& subdir : patch_dirs) { if (!subdir) continue; auto romfs_dir = FindSubdirectoryCaseless(subdir, "romfs"); if (romfs_dir != nullptr) layers.emplace_back(std::move(romfs_dir)); // REMOVED CachedVfsDirectory hang auto romfslite_dir = FindSubdirectoryCaseless(subdir, "romfslite"); if (romfslite_dir != nullptr) layers.emplace_back(std::move(romfslite_dir)); auto ext_dir = FindSubdirectoryCaseless(subdir, "romfs_ext"); if (ext_dir != nullptr) layers_ext.emplace_back(std::move(ext_dir)); if (type == ContentRecordType::HtmlDocument) { auto manual_dir = FindSubdirectoryCaseless(subdir, "manual_html"); if (manual_dir != nullptr) layers.emplace_back(std::move(manual_dir)); } } if (layers.empty() && layers_ext.empty()) { return; } auto extracted = ExtractRomFS(romfs); if (extracted == nullptr) { return; } layers.emplace_back(std::move(extracted)); auto layered = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers)); if (layered == nullptr) { return; } auto layered_ext = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers_ext)); auto packed = CreateRomFS(std::move(layered), std::move(layered_ext)); if (packed == nullptr) { return; } LOG_INFO(Loader, " RomFS: LayeredFS patches applied successfully"); romfs = std::move(packed); } VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs, ContentRecordType type, VirtualFile packed_update_raw, bool apply_layeredfs) const { const auto log_string = fmt::format("Patching RomFS for title_id={:016X}, type={:02X}", title_id, static_cast(type)); if (type == ContentRecordType::Program || type == ContentRecordType::Data) { LOG_INFO(Loader, "{}", log_string); } else { LOG_DEBUG(Loader, "{}", log_string); } auto romfs = base_romfs; const auto& disabled = Settings::values.disabled_addons[title_id]; bool autoloader_update_applied = false; if (type == ContentRecordType::Program) { VirtualDir sdmc_root = nullptr; if (fs_controller.OpenSDMC(&sdmc_root).IsSuccess() && sdmc_root) { const auto autoloader_updates_path = fmt::format("autoloader/{:016X}/Updates", title_id); const auto updates_dir = sdmc_root->GetSubdirectory(autoloader_updates_path); if (updates_dir) { for (const auto& mod : updates_dir->GetSubdirectories()) { if (mod && std::find(disabled.cbegin(), disabled.cend(), mod->GetName()) == disabled.cend()) { for (const auto& file : mod->GetFiles()) { if (file->GetExtension() == "nca") { const auto new_nca = std::make_shared(file, base_nca); if (new_nca->GetStatus() == Loader::ResultStatus::Success && new_nca->GetType() == NCAContentType::Program && new_nca->GetRomFS() != nullptr) { LOG_INFO( Loader, " RomFS: Autoloader Update ({}) applied successfully", mod->GetName()); romfs = new_nca->GetRomFS(); autoloader_update_applied = true; break; } } } } if (autoloader_update_applied) break; } } } } if (!autoloader_update_applied) { // Find the highest version enabled update u32 best_version = 0; VirtualFile best_update_raw = nullptr; bool found_best = false; // 1. External Updates const auto* content_provider_union = dynamic_cast(&content_provider); if (content_provider_union) { const auto* external_provider = content_provider_union->GetExternalProvider(); if (external_provider) { const auto update_tid = GetUpdateTitleID(title_id); const auto updates = external_provider->ListUpdateVersions(update_tid); for (const auto& update : updates) { std::string version_str; const auto control_file = external_provider->GetEntryForVersion( update_tid, ContentRecordType::Control, update.version); if (control_file) { NCA control_nca(control_file); if (control_nca.GetStatus() == Loader::ResultStatus::Success) { if (auto control_romfs = control_nca.GetRomFS()) { if (auto extracted = ExtractRomFS(control_romfs)) { auto nacp_file = extracted->GetFile("control.nacp"); if (!nacp_file) { nacp_file = extracted->GetFile("Control.nacp"); } if (nacp_file) { NACP nacp(nacp_file); version_str = nacp.GetVersionString(); } } } } } if (version_str.empty()) { version_str = FormatTitleVersion(update.version); } const auto name = fmt::format("Update v{}", version_str); const auto patch_disabled = std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend(); if (!patch_disabled) { if (!found_best || update.version > best_version) { best_version = update.version; best_update_raw = external_provider->GetEntryForVersion( update_tid, type, update.version); found_best = true; } } } } } // 2. System Updates const auto update_tid = GetUpdateTitleID(title_id); if (update_tid != title_id) { PatchManager update_mgr{update_tid, fs_controller, content_provider}; const auto metadata = update_mgr.GetControlMetadata(); const auto& nacp = metadata.first; if (nacp) { const auto version_str = nacp->GetVersionString(); const auto name = fmt::format("Update v{}", version_str); const auto patch_disabled = std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend(); if (!patch_disabled) { const auto sys_ver = content_provider.GetEntryVersion(update_tid).value_or(0); if (!found_best || sys_ver > best_version) { best_version = sys_ver; best_update_raw = content_provider.GetEntryRaw(update_tid, type); found_best = true; } } } else if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) { const auto meta_ver = content_provider.GetEntryVersion(update_tid); if (meta_ver.value_or(0) != 0) { const auto version_str = FormatTitleVersion(*meta_ver); const auto name = fmt::format("Update {}", version_str); const auto patch_disabled = std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend(); if (!patch_disabled) { const auto sys_ver = *meta_ver; if (!found_best || sys_ver > best_version) { best_version = sys_ver; best_update_raw = content_provider.GetEntryRaw(update_tid, type); found_best = true; } } } } } // 3. Packed Update (Fallback) // If we still haven't found a best update, or if the packed one is enabled (named "Update" // usually? Or "Update (PACKED)"?) The GetPatches logic names it "Update" with version // "PACKED". if (!found_best && packed_update_raw != nullptr) { // Only if no specific update found, or check strict priorities? // Usually packed update is last resort. const auto patch_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); if (!patch_disabled) { best_update_raw = packed_update_raw; found_best = true; // Version? Unknown/Packed. } } if (found_best && best_update_raw != nullptr && base_nca != nullptr) { const auto new_nca = std::make_shared(best_update_raw, base_nca); if (new_nca->GetStatus() == Loader::ResultStatus::Success && new_nca->GetRomFS() != nullptr) { LOG_INFO(Loader, " RomFS: Update ({}) applied successfully", FormatTitleVersion(best_version)); romfs = new_nca->GetRomFS(); } } } if (type == ContentRecordType::Program) { VirtualDir sdmc_root = nullptr; if (fs_controller.OpenSDMC(&sdmc_root).IsSuccess() && sdmc_root) { const auto autoloader_dlc_path = fmt::format("autoloader/{:016X}/DLC", title_id); const auto dlc_dir = sdmc_root->GetSubdirectory(autoloader_dlc_path); if (dlc_dir) { std::map dlc_ncas; for (const auto& mod : dlc_dir->GetSubdirectories()) { if (mod && std::find(disabled.cbegin(), disabled.cend(), mod->GetName()) == disabled.cend()) { u64 dlc_title_id = 0; VirtualFile data_nca_file = nullptr; for (const auto& file : mod->GetFiles()) { if (file->GetName().ends_with(".cnmt.nca")) { NCA meta_nca(file); if (meta_nca.GetStatus() == Loader::ResultStatus::Success && !meta_nca.GetSubdirectories().empty()) { auto section0 = meta_nca.GetSubdirectories()[0]; if (!section0->GetFiles().empty()) { CNMT cnmt(section0->GetFiles()[0]); dlc_title_id = cnmt.GetTitleID(); } } } else if (file->GetExtension() == "nca") { data_nca_file = file; } } if (dlc_title_id != 0 && data_nca_file != nullptr) { dlc_ncas[dlc_title_id] = data_nca_file; } } } if (!dlc_ncas.empty()) { std::vector layers; auto base_layer = ExtractRomFS(romfs); if (base_layer) { layers.push_back(std::move(base_layer)); for (const auto& [tid, nca_file] : dlc_ncas) { const auto dlc_nca = std::make_shared(nca_file, base_nca); if (dlc_nca->GetStatus() == Loader::ResultStatus::Success && dlc_nca->GetType() == NCAContentType::Data && dlc_nca->GetRomFS() != nullptr) { auto extracted_dlc_romfs = ExtractRomFS(dlc_nca->GetRomFS()); if (extracted_dlc_romfs) { layers.push_back(std::move(extracted_dlc_romfs)); LOG_INFO(Loader, " RomFS: Staging Autoloader DLC TID {:016X}", tid); } } } if (layers.size() > 1) { auto layered_dir = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers)); auto packed = CreateRomFS(std::move(layered_dir), nullptr); if (packed) { romfs = std::move(packed); LOG_INFO(Loader, " RomFS: Autoloader DLCs layered successfully."); } } } } } } } if (apply_layeredfs) { ApplyLayeredFS(romfs, title_id, type, fs_controller); } return romfs; } std::vector PatchManager::GetPatches(VirtualFile update_raw) const { if (title_id == 0) return {}; std::vector out; const auto& disabled = Settings::values.disabled_addons[title_id]; // --- 1. Update (NAND/External) --- // --- 1. Update (NAND/External) --- // First, check for system updates (NAND/SDMC) const auto update_tid = GetUpdateTitleID(title_id); PatchManager update_mgr{update_tid, fs_controller, content_provider}; const auto metadata = update_mgr.GetControlMetadata(); const auto& nacp = metadata.first; const auto update_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); if (nacp != nullptr) { // System update found const auto version_str = nacp->GetVersionString(); const auto name = fmt::format("Update v{}", version_str); const auto patch_disabled = std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend(); Patch update_patch = {.enabled = !patch_disabled, .name = name, .version = version_str, .type = PatchType::Update, .program_id = title_id, .title_id = title_id}; out.push_back(update_patch); } else if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) { // Fallback for system update without control NCA (rare) const auto meta_ver = content_provider.GetEntryVersion(update_tid); if (meta_ver.value_or(0) != 0) { const auto version_str = FormatTitleVersion(*meta_ver); const auto name = fmt::format("Update {}", version_str); const auto patch_disabled = std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend(); Patch update_patch = {.enabled = !patch_disabled, .name = name, .version = version_str, .type = PatchType::Update, .program_id = title_id, .title_id = title_id}; out.push_back(update_patch); } } // Next, check for external updates const auto* content_provider_union = dynamic_cast(&content_provider); if (content_provider_union) { const auto* external_provider = content_provider_union->GetExternalProvider(); if (external_provider) { const auto updates = external_provider->ListUpdateVersions(update_tid); for (const auto& update : updates) { std::string version_str; const auto control_file = external_provider->GetEntryForVersion( update_tid, ContentRecordType::Control, update.version); if (control_file) { NCA control_nca(control_file); if (control_nca.GetStatus() == Loader::ResultStatus::Success) { if (auto control_romfs = control_nca.GetRomFS()) { if (auto extracted = ExtractRomFS(control_romfs)) { auto nacp_file = extracted->GetFile("control.nacp"); if (!nacp_file) { nacp_file = extracted->GetFile("Control.nacp"); } if (nacp_file) { NACP control_nacp(nacp_file); version_str = control_nacp.GetVersionString(); } } } } } if (version_str.empty()) { version_str = FormatTitleVersion(update.version); } const auto name = fmt::format("Update v{}", version_str); const auto patch_disabled = std::find(disabled.cbegin(), disabled.cend(), name) != disabled.cend(); // Deduplicate against installed system update if versions match bool exists = false; for (const auto& existing : out) { if (existing.type == PatchType::Update && existing.version == version_str) { exists = true; break; } } if (exists) continue; Patch update_patch = {.enabled = !patch_disabled, .name = name, .version = version_str, .type = PatchType::Update, .program_id = title_id, .title_id = title_id}; out.push_back(update_patch); } } } if (out.empty() && update_raw != nullptr) { Patch update_patch = {.enabled = !update_disabled, .name = "Update", .version = "PACKED", .type = PatchType::Update, .program_id = title_id, .title_id = title_id}; out.push_back(update_patch); } // --- 2. Autoloader Content --- VirtualDir sdmc_root = nullptr; if (fs_controller.OpenSDMC(&sdmc_root).IsSuccess() && sdmc_root) { const auto scan_autoloader_content = [&](const std::string& content_type_folder, PatchType patch_type) { const auto autoloader_path = fmt::format("autoloader/{:016X}/{}", title_id, content_type_folder); const auto content_dir = sdmc_root->GetSubdirectory(autoloader_path); if (!content_dir) return; for (const auto& mod : content_dir->GetSubdirectories()) { if (!mod) continue; std::string mod_name_str = mod->GetName(); std::string version_str = "Unknown"; if (patch_type == PatchType::DLC) { u64 dlc_title_id = 0; for (const auto& file : mod->GetFiles()) { if (file->GetName().ends_with(".cnmt.nca")) { NCA meta_nca(file); if (meta_nca.GetStatus() == Loader::ResultStatus::Success && !meta_nca.GetSubdirectories().empty()) { auto section0 = meta_nca.GetSubdirectories()[0]; if (!section0->GetFiles().empty()) { CNMT cnmt(section0->GetFiles()[0]); dlc_title_id = cnmt.GetTitleID(); break; } } } } if (dlc_title_id != 0) { version_str = fmt::format( "{}", (dlc_title_id - GetBaseTitleID(dlc_title_id)) / 0x1000); } else { version_str = "DLC"; } } else { for (const auto& file : mod->GetFiles()) { if (file->GetExtension() == "nca") { NCA nca_check(file); if (nca_check.GetStatus() == Loader::ResultStatus::Success && nca_check.GetType() == NCAContentType::Control) { if (auto rfs = nca_check.GetRomFS()) { if (auto ext = ExtractRomFS(rfs)) { if (auto nacp_f = ext->GetFile("control.nacp")) { NACP auto_nacp(nacp_f); version_str = auto_nacp.GetVersionString(); break; } } } } } } } const auto mod_disabled = std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end(); out.push_back({.enabled = !mod_disabled, .name = mod_name_str, .version = version_str, .type = patch_type, .program_id = title_id, .title_id = title_id}); } }; scan_autoloader_content("Updates", PatchType::Update); scan_autoloader_content("DLC", PatchType::DLC); } // --- 3. Citron Mods --- const auto mod_dir = fs_controller.GetModificationLoadRoot(title_id); if (mod_dir != nullptr) { auto get_mod_types = [](const VirtualDir& dir) -> std::string { std::string types; if (FindSubdirectoryCaseless(dir, "exefs")) AppendCommaIfNotEmpty(types, "IPSwitch/IPS"); if (FindSubdirectoryCaseless(dir, "romfs") || FindSubdirectoryCaseless(dir, "romfslite")) AppendCommaIfNotEmpty(types, "LayeredFS"); if (FindSubdirectoryCaseless(dir, "cheats")) AppendCommaIfNotEmpty(types, "Cheats"); return types; }; for (const auto& top_dir : mod_dir->GetSubdirectories()) { if (!top_dir) continue; auto process_mod = [&](const VirtualDir& dir, bool nested) { std::string types = get_mod_types(dir); if (types.empty()) return; std::string identifier = nested ? (top_dir->GetName() + "/" + dir->GetName()) : dir->GetName(); const auto mod_disabled = std::find(disabled.begin(), disabled.end(), identifier) != disabled.end(); out.push_back({.enabled = !mod_disabled, .name = identifier, .version = types, .type = PatchType::Mod, .program_id = title_id, .title_id = title_id}); }; if (IsValidModDir(top_dir)) process_mod(top_dir, false); else for (const auto& sd : top_dir->GetSubdirectories()) if (sd) process_mod(sd, true); } } const auto sdmc_mod_dir = fs_controller.GetSDMCModificationLoadRoot(title_id); if (sdmc_mod_dir != nullptr) { std::string types; if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "exefs"))) AppendCommaIfNotEmpty(types, "LayeredExeFS"); if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfs")) || IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfslite"))) AppendCommaIfNotEmpty(types, "LayeredFS"); if (!types.empty()) { const auto mod_disabled = std::find(disabled.begin(), disabled.end(), "SDMC") != disabled.end(); out.push_back({.enabled = !mod_disabled, .name = "SDMC", .version = types, .type = PatchType::Mod, .program_id = title_id, .title_id = title_id}); } } // --- 4. NAND DLC --- const auto dlc_entries = content_provider.ListEntriesFilter(TitleType::AOC, ContentRecordType::Data); std::vector dlc_match; dlc_match.reserve(dlc_entries.size()); std::copy_if(dlc_entries.begin(), dlc_entries.end(), std::back_inserter(dlc_match), [this](const ContentProviderEntry& entry) { return GetBaseTitleID(entry.title_id) == title_id && content_provider.GetEntry(entry)->GetStatus() == Loader::ResultStatus::Success; }); if (!dlc_match.empty()) { std::sort(dlc_match.begin(), dlc_match.end()); std::string list; for (size_t i = 0; i < dlc_match.size() - 1; ++i) list += fmt::format("{}, ", dlc_match[i].title_id & 0x7FF); list += fmt::format("{}", dlc_match.back().title_id & 0x7FF); const auto dlc_disabled = std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end(); out.push_back({.enabled = !dlc_disabled, .name = "DLC", .version = std::move(list), .type = PatchType::DLC, .program_id = title_id, .title_id = title_id}); } // Scan for Game-Specific Tools const auto load_root = fs_controller.GetModificationLoadRoot(title_id); if (load_root) { if (auto tools_dir = load_root->GetSubdirectory("tools")) { for (const auto& file : tools_dir->GetFiles()) { FileSys::Patch tool_patch; tool_patch.enabled = true; tool_patch.name = file->GetName(); tool_patch.version = "Tool"; tool_patch.type = PatchType::Mod; tool_patch.program_id = title_id; tool_patch.title_id = title_id; out.push_back(std::move(tool_patch)); } } } // Scan for Global Tools (NX-Optimizer) const std::vector optimizer_supported_ids = {0x0100F2C0115B6000, 0x01007EF00011E000, 0x0100A3D008C5C000, 0x01008F6008C5E000, 0x01008CF01BAAC000, 0x100453019AA8000}; if (std::find(optimizer_supported_ids.begin(), optimizer_supported_ids.end(), title_id) != optimizer_supported_ids.end()) { auto global_tools_path = Common::FS::GetCitronPath(Common::FS::CitronPath::ConfigDir) / "tools"; if (std::filesystem::exists(global_tools_path)) { for (const auto& entry : std::filesystem::directory_iterator(global_tools_path)) { if (entry.is_regular_file()) { const auto extension = entry.path().extension().string(); // Only allow actual executables to show up in the UI bool is_tool = (extension == ".AppImage" || extension == ".exe"); if (!is_tool) { continue; } FileSys::Patch global_tool; global_tool.enabled = true; global_tool.name = entry.path().filename().string(); global_tool.version = "Tool"; global_tool.type = PatchType::Mod; global_tool.program_id = title_id; global_tool.title_id = title_id; out.push_back(std::move(global_tool)); } } } } return out; } std::optional PatchManager::GetGameVersion() const { const auto& disabled = Settings::values.disabled_addons[title_id]; const auto update_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); if (!update_disabled) { const auto update_tid = GetUpdateTitleID(title_id); if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) { return content_provider.GetEntryVersion(update_tid); } } return content_provider.GetEntryVersion(title_id); } PatchManager::Metadata PatchManager::GetControlMetadata() const { std::unique_ptr control_nca = nullptr; const auto& disabled_map = Settings::values.disabled_addons; const auto it = disabled_map.find(title_id); const auto& disabled_for_game = (it != disabled_map.end()) ? it->second : std::vector{}; VirtualDir sdmc_root = nullptr; if (fs_controller.OpenSDMC(&sdmc_root).IsSuccess() && sdmc_root) { const auto autoloader_updates_path = fmt::format("autoloader/{:016X}/Updates", title_id); if (const auto autoloader_updates_dir = sdmc_root->GetSubdirectory(autoloader_updates_path)) { for (const auto& update_mod : autoloader_updates_dir->GetSubdirectories()) { if (!update_mod) continue; if (std::find(disabled_for_game.begin(), disabled_for_game.end(), update_mod->GetName()) == disabled_for_game.end()) { for (const auto& file : update_mod->GetFiles()) { if (file->GetExtension() == "nca") { NCA nca_check(file); if (nca_check.GetStatus() == Loader::ResultStatus::Success && nca_check.GetType() == NCAContentType::Control) { control_nca = std::make_unique(file); return ParseControlNCA(*control_nca); } } } } } } } // Only fetch the Update metadata if the user hasn't disabled it const auto update_disabled = std::find(disabled_for_game.begin(), disabled_for_game.end(), "Update") != disabled_for_game.end(); const auto update_tid = GetUpdateTitleID(title_id); if (!update_disabled) { control_nca = content_provider.GetEntry(update_tid, ContentRecordType::Control); } if (control_nca == nullptr) { control_nca = content_provider.GetEntry(title_id, ContentRecordType::Control); } if (control_nca == nullptr) return {}; return ParseControlNCA(*control_nca); } PatchManager::Metadata PatchManager::ParseControlNCA(const NCA& nca) const { const auto base_romfs = nca.GetRomFS(); if (base_romfs == nullptr) return {}; const auto romfs = PatchRomFS(&nca, base_romfs, ContentRecordType::Control); if (romfs == nullptr) return {}; const auto extracted = ExtractRomFS(romfs); if (extracted == nullptr) return {}; auto nacp_file = extracted->GetFile("control.nacp"); if (nacp_file == nullptr) nacp_file = extracted->GetFile("Control.nacp"); auto nacp = nacp_file == nullptr ? nullptr : std::make_unique(nacp_file); const auto language_code = Service::Set::GetLanguageCodeFromIndex( static_cast(Settings::values.language_index.GetValue())); const auto application_language = Service::NS::ConvertToApplicationLanguage(language_code) .value_or(Service::NS::ApplicationLanguage::AmericanEnglish); const auto language_priority_list = Service::NS::GetApplicationLanguagePriorityList(application_language); auto priority_language_names = FileSys::LANGUAGE_NAMES; if (language_priority_list) { for (size_t i = 0; i < priority_language_names.size(); ++i) { const auto language_index = static_cast(language_priority_list->at(i)); if (language_index < FileSys::LANGUAGE_NAMES.size()) { priority_language_names[i] = FileSys::LANGUAGE_NAMES[language_index]; } } } VirtualFile icon_file; for (const auto& language : priority_language_names) { icon_file = extracted->GetFile(std::string("icon_").append(language).append(".dat")); if (icon_file != nullptr) break; } return {std::move(nacp), icon_file}; } } // namespace FileSys