From 6cf31f1d68d1e5b5e63e86c4b9f18e523565acb9 Mon Sep 17 00:00:00 2001 From: collecting Date: Tue, 21 Oct 2025 20:35:00 +0000 Subject: [PATCH 1/9] feat(fs): Implement Autoloader (W.I.P.) --- src/citron/main.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/citron/main.h b/src/citron/main.h index 76b7b4e0c..8e1b9cbef 100644 --- a/src/citron/main.h +++ b/src/citron/main.h @@ -268,6 +268,7 @@ private: const bool tas_allowed = false); void RegisterMetaTypes(); + void RegisterAutoloaderContents(); void InitializeWidgets(); void InitializeDebugWidgets(); @@ -344,6 +345,9 @@ private: Service::AM::FrontendAppletParameters LibraryAppletParameters(u64 program_id, Service::AM::AppletId applet_id); + // This will hold and provide all discovered Autoloader content. + std::unique_ptr autoloader_provider; + private slots: void OnStartGame(); void OnRestartGame(); @@ -377,7 +381,7 @@ private slots: void OnMenuLoadFolder(); void IncrementInstallProgress(); void OnMenuInstallToNAND(); - void OnMenuTrimXCI(); + void OnMenuInstallWithAutoloader(); void OnMenuRecentFile(); void OnConfigure(); void OnConfigureTas(); From 4db51f9353c744cafe5d8b454f592656acb92075 Mon Sep 17 00:00:00 2001 From: collecting Date: Tue, 21 Oct 2025 20:52:26 +0000 Subject: [PATCH 2/9] feat(fs): Implement Autoloader (W.I.P.) --- src/core/file_sys/patch_manager.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/file_sys/patch_manager.h b/src/core/file_sys/patch_manager.h index 552c0fbe2..826e4baaf 100644 --- a/src/core/file_sys/patch_manager.h +++ b/src/core/file_sys/patch_manager.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -92,6 +93,7 @@ public: [[nodiscard]] Metadata ParseControlNCA(const NCA& nca) const; private: + [[nodiscard]] VirtualFile FindAutoloaderNCA(ContentRecordType type) const; [[nodiscard]] std::vector CollectPatches(const std::vector& patch_dirs, const std::string& build_id) const; From ab39e75ac1aebd80b3f66436bf13ec031b1f29f5 Mon Sep 17 00:00:00 2001 From: collecting Date: Tue, 21 Oct 2025 20:54:17 +0000 Subject: [PATCH 3/9] feat(fs): Implement Autoloader (W.I.P.) --- src/core/file_sys/patch_manager.cpp | 575 ++++++++++++++++++---------- 1 file changed, 379 insertions(+), 196 deletions(-) diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp index 184e74350..469cd91f9 100644 --- a/src/core/file_sys/patch_manager.cpp +++ b/src/core/file_sys/patch_manager.cpp @@ -1,10 +1,12 @@ // 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 "common/hex_util.h" #include "common/logging/log.h" @@ -61,8 +63,6 @@ std::string FormatTitleVersion(u32 version, return fmt::format("v{}.{}.{}", bytes[3], bytes[2], bytes[1]); } -// Returns a directory with name matching name case-insensitive. Returns nullptr if directory -// doesn't have a directory with name. VirtualDir FindSubdirectoryCaseless(const VirtualDir dir, std::string_view name) { #ifdef _WIN32 return dir->GetSubdirectory(name); @@ -74,7 +74,6 @@ VirtualDir FindSubdirectoryCaseless(const VirtualDir dir, std::string_view name) return subdir; } } - return nullptr; #endif } @@ -84,20 +83,17 @@ std::optional> ReadCheatFileFromFolder( 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())); } @@ -120,64 +116,89 @@ 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]; - const auto update_disabled = - std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); + bool autoloader_update_applied = false; - // Game Updates - const auto update_tid = GetUpdateTitleID(title_id); - const auto update = content_provider.GetEntry(update_tid, ContentRecordType::Program); + // --- 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; + } + } + } + } - if (!update_disabled && update != nullptr && update->GetExeFS() != nullptr) { - LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully", - FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0))); - exefs = update->GetExeFS(); + // --- NAND UPDATE (FALLBACK) --- + if (!autoloader_update_applied) { + const auto update_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); + const auto update_tid = GetUpdateTitleID(title_id); + const auto update = content_provider.GetEntry(update_tid, ContentRecordType::Program); + if (!update_disabled && update != nullptr && update->GetExeFS() != nullptr) { + LOG_INFO(Loader, " ExeFS: NAND Update ({}) applied successfully", + FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0))); + exefs = update->GetExeFS(); + } } // LayeredExeFS const auto load_dir = fs_controller.GetModificationLoadRoot(title_id); const auto sdmc_load_dir = fs_controller.GetSDMCModificationLoadRoot(title_id); - std::vector patch_dirs = {sdmc_load_dir}; if (load_dir != nullptr) { const auto load_patch_dirs = load_dir->GetSubdirectories(); patch_dirs.insert(patch_dirs.end(), load_patch_dirs.begin(), load_patch_dirs.end()); } - std::sort(patch_dirs.begin(), patch_dirs.end(), - [](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); }); + [](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 (std::find(disabled.begin(), disabled.end(), subdir->GetName()) != disabled.end()) + if (!subdir || std::find(disabled.begin(), disabled.end(), subdir->GetName()) != disabled.end()) continue; - auto exefs_dir = FindSubdirectoryCaseless(subdir, "exefs"); if (exefs_dir != nullptr) layers.push_back(std::move(exefs_dir)); } - layers.push_back(exefs); - + 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); @@ -186,7 +207,6 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const { VfsRawCopyD(exefs, exefs_dir); } } - return exefs; } @@ -194,19 +214,16 @@ std::vector PatchManager::CollectPatches(const std::vector out; out.reserve(patch_dirs.size()); for (const auto& subdir : patch_dirs) { - if (std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) != disabled.cend()) + if (!subdir || std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) != disabled.cend()) 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) @@ -215,7 +232,6 @@ std::vector PatchManager::CollectPatches(const std::vector PatchManager::CollectPatches(const std::vector PatchManager::PatchNSO(const std::vector& nso, const std::st 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); @@ -249,25 +260,20 @@ std::vector PatchManager::PatchNSO(const std::vector& nso, const std::st 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); - const auto load_dir = fs_controller.GetModificationLoadRoot(title_id); if (load_dir == nullptr) { LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id); return nso; } - auto patch_dirs = load_dir->GetSubdirectories(); 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") { @@ -285,11 +291,9 @@ std::vector PatchManager::PatchNSO(const std::vector& nso, const std::st out = patched->ReadAllBytes(); } } - if (out.size() < sizeof(Loader::NSOHeader)) { return nso; } - std::memcpy(out.data(), &header, sizeof(header)); return out; } @@ -297,19 +301,15 @@ std::vector PatchManager::PatchNSO(const std::vector& nso, const std::st 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); - const auto load_dir = fs_controller.GetModificationLoadRoot(title_id); if (load_dir == nullptr) { LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id); return false; } - auto patch_dirs = load_dir->GetSubdirectories(); 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(); } @@ -320,31 +320,26 @@ std::vector PatchManager::CreateCheatList( LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id); return {}; } - const auto& disabled = Settings::values.disabled_addons[title_id]; auto patch_dirs = load_dir->GetSubdirectories(); 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) { - if (std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) != disabled.cend()) { + if (!subdir || std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) != disabled.cend()) { continue; } - 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; } @@ -357,40 +352,39 @@ static void ApplyLayeredFS(VirtualFile& romfs, u64 title_id, ContentRecordType t (load_dir == nullptr && sdmc_load_dir == nullptr)) { return; } - const auto& disabled = Settings::values.disabled_addons[title_id]; std::vector patch_dirs; if (load_dir) { - patch_dirs = load_dir->GetSubdirectories(); -} + patch_dirs = load_dir->GetSubdirectories(); + } if (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) { return l->GetName() < r->GetName(); }); + [](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 (std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) != disabled.cend()) { + if (!subdir || std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) != disabled.cend()) { continue; } - auto romfs_dir = FindSubdirectoryCaseless(subdir, "romfs"); if (romfs_dir != nullptr) layers.emplace_back(std::make_shared(std::move(romfs_dir))); - - // Support for romfslite introduced in Atmosphere 1.9.5 auto romfslite_dir = FindSubdirectoryCaseless(subdir, "romfslite"); if (romfslite_dir != nullptr) layers.emplace_back(std::make_shared(std::move(romfslite_dir))); - auto ext_dir = FindSubdirectoryCaseless(subdir, "romfs_ext"); if (ext_dir != nullptr) layers_ext.emplace_back(std::make_shared(std::move(ext_dir))); - if (type == ContentRecordType::HtmlDocument) { auto manual_dir = FindSubdirectoryCaseless(subdir, "manual_html"); if (manual_dir != nullptr) @@ -398,30 +392,23 @@ static void ApplyLayeredFS(VirtualFile& romfs, u64 title_id, ContentRecordType t } } - // When there are no layers to apply, return early as there is no need to rebuild the RomFS 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); } @@ -436,41 +423,134 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs } else { LOG_DEBUG(Loader, "{}", log_string); } - auto romfs = base_romfs; - - // Game Updates - const auto update_tid = GetUpdateTitleID(title_id); - const auto update_raw = content_provider.GetEntryRaw(update_tid, type); - const auto& disabled = Settings::values.disabled_addons[title_id]; - const auto update_disabled = - std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); + bool autoloader_update_applied = false; - if (!update_disabled && update_raw != nullptr && base_nca != nullptr) { - const auto new_nca = std::make_shared(update_raw, base_nca); - if (new_nca->GetStatus() == Loader::ResultStatus::Success && - new_nca->GetRomFS() != nullptr) { - LOG_INFO(Loader, " RomFS: Update ({}) applied successfully", - FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0))); - romfs = new_nca->GetRomFS(); - const auto version = - FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0)); - } - } else if (!update_disabled && packed_update_raw != nullptr && base_nca != nullptr) { - const auto new_nca = std::make_shared(packed_update_raw, base_nca); - if (new_nca->GetStatus() == Loader::ResultStatus::Success && - new_nca->GetRomFS() != nullptr) { - LOG_INFO(Loader, " RomFS: Update (PACKED) applied successfully"); - romfs = new_nca->GetRomFS(); + // --- AUTOLOADER UPDATE (PRIORITY) --- + 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; + } + } + } + } + + // --- NAND UPDATE (FALLBACK) --- + if (!autoloader_update_applied) { + const auto update_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); + const auto update_tid = GetUpdateTitleID(title_id); + const auto update_raw = content_provider.GetEntryRaw(update_tid, type); + if (!update_disabled && update_raw != nullptr && base_nca != nullptr) { + const auto new_nca = std::make_shared(update_raw, base_nca); + if (new_nca->GetStatus() == Loader::ResultStatus::Success && + new_nca->GetRomFS() != nullptr) { + LOG_INFO(Loader, " RomFS: NAND Update ({}) applied successfully", + FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0))); + romfs = new_nca->GetRomFS(); + } + } else if (!update_disabled && packed_update_raw != nullptr && base_nca != nullptr) { + const auto new_nca = std::make_shared(packed_update_raw, base_nca); + if (new_nca->GetStatus() == Loader::ResultStatus::Success && + new_nca->GetRomFS() != nullptr) { + LOG_INFO(Loader, " RomFS: Update (PACKED) applied successfully"); + romfs = new_nca->GetRomFS(); + } + } + } + + // --- AUTOLOADER DLC (Layering) --- + 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") { + // Tentatively assume this is the data 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."); + } + } + } + } + } } } - // LayeredFS if (apply_layeredfs) { ApplyLayeredFS(romfs, title_id, type, fs_controller); } - return romfs; } @@ -478,16 +558,14 @@ 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]; - // Game Updates + // --- 1. NAND Update (from original code) --- const auto update_tid = GetUpdateTitleID(title_id); PatchManager update{update_tid, fs_controller, content_provider}; const auto metadata = update.GetControlMetadata(); const auto& nacp = metadata.first; - const auto update_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); Patch update_patch = {.enabled = !update_disabled, @@ -496,134 +574,209 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { .type = PatchType::Update, .program_id = title_id, .title_id = title_id}; - if (nacp != nullptr) { update_patch.version = nacp->GetVersionString(); out.push_back(update_patch); - } 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) { - out.push_back(update_patch); - } else { - update_patch.version = FormatTitleVersion(*meta_ver); - out.push_back(update_patch); - } - } else if (update_raw != nullptr) { - update_patch.version = "PACKED"; + } 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) { + update_patch.version = FormatTitleVersion(*meta_ver); out.push_back(update_patch); } + } else if (update_raw != nullptr) { + update_patch.version = "PACKED"; + out.push_back(update_patch); } - // General Mods (LayeredFS and IPS) + // --- 2. Autoloader Content (Updates and DLC) --- + 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"; + } + 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 romfs = nca_check.GetRomFS()) { + if (auto extracted = ExtractRomFS(romfs)) { + if (auto nacp_file = extracted->GetFile("control.nacp")) { + NACP dlc_nacp(nacp_file); + std::string nacp_name = dlc_nacp.GetApplicationName(); + if (!nacp_name.empty()) { + mod_name_str = nacp_name; + break; + } + } + } + } + } + } + } + } else { // Handle Updates + 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 romfs = nca_check.GetRomFS()) { + if (auto extracted = ExtractRomFS(romfs)) { + if (auto nacp_file = extracted->GetFile("control.nacp")) { + NACP autoloader_nacp(nacp_file); + std::string nacp_version = autoloader_nacp.GetVersionString(); + if (!nacp_version.empty()) { + version_str = nacp_version; + break; + } + } + } + } + } + } + } + if (version_str == "Unknown") { + 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]); + version_str = FormatTitleVersion(cnmt.GetTitleVersion(), TitleVersionFormat::FourElements); + 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. General Mods (from original code) --- const auto mod_dir = fs_controller.GetModificationLoadRoot(title_id); if (mod_dir != nullptr) { for (const auto& mod : mod_dir->GetSubdirectories()) { std::string types; - const auto exefs_dir = FindSubdirectoryCaseless(mod, "exefs"); if (IsDirValidAndNonEmpty(exefs_dir)) { - bool ips = false; - bool ipswitch = false; - bool layeredfs = false; - + bool ips = false, ipswitch = false, layeredfs = false; for (const auto& file : exefs_dir->GetFiles()) { - if (file->GetExtension() == "ips") { - ips = true; - } else if (file->GetExtension() == "pchtxt") { - ipswitch = true; - } else if (std::find(EXEFS_FILE_NAMES.begin(), EXEFS_FILE_NAMES.end(), - file->GetName()) != EXEFS_FILE_NAMES.end()) { - layeredfs = true; - } + if (file->GetExtension() == "ips") ips = true; + else if (file->GetExtension() == "pchtxt") ipswitch = true; + else if (std::find(EXEFS_FILE_NAMES.begin(), EXEFS_FILE_NAMES.end(), file->GetName()) != EXEFS_FILE_NAMES.end()) layeredfs = true; } - - if (ips) - AppendCommaIfNotEmpty(types, "IPS"); - if (ipswitch) - AppendCommaIfNotEmpty(types, "IPSwitch"); - if (layeredfs) - AppendCommaIfNotEmpty(types, "LayeredExeFS"); + if (ips) AppendCommaIfNotEmpty(types, "IPS"); + if (ipswitch) AppendCommaIfNotEmpty(types, "IPSwitch"); + if (layeredfs) AppendCommaIfNotEmpty(types, "LayeredExeFS"); } - if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "romfs")) || - IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "romfslite"))) - AppendCommaIfNotEmpty(types, "LayeredFS"); - if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "cheats"))) - AppendCommaIfNotEmpty(types, "Cheats"); - - if (types.empty()) - continue; - - const auto mod_disabled = - std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end(); - out.push_back({.enabled = !mod_disabled, - .name = mod->GetName(), - .version = types, - .type = PatchType::Mod, - .program_id = title_id, - .title_id = title_id}); + if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "romfs")) || IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "romfslite"))) AppendCommaIfNotEmpty(types, "LayeredFS"); + if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "cheats"))) AppendCommaIfNotEmpty(types, "Cheats"); + if (types.empty()) continue; + const auto mod_disabled = std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end(); + out.push_back({.enabled = !mod_disabled, .name = mod->GetName(), .version = types, .type = PatchType::Mod, .program_id = title_id, .title_id = title_id}); } } - - // SDMC mod directory (RomFS LayeredFS) 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 (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}); + 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}); } } - // DLC - const auto dlc_entries = - content_provider.ListEntriesFilter(TitleType::AOC, ContentRecordType::Data); + // --- 4. NAND DLC (from original code) --- + 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; + content_provider.GetEntry(entry)->GetStatus() == Loader::ResultStatus::Success; }); if (!dlc_match.empty()) { - // Ensure sorted so DLC IDs show in order. 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 = dlc_match.back().title_id}); + 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 = dlc_match.back().title_id}); } return out; } std::optional PatchManager::GetGameVersion() const { + const auto& disabled = Settings::values.disabled_addons[title_id]; + + // --- Autoloader Check (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 autoloader_updates_dir = sdmc_root->GetSubdirectory(autoloader_updates_path); + if (autoloader_updates_dir) { + for (const auto& update_mod : autoloader_updates_dir->GetSubdirectories()) { + if (!update_mod) continue; + + if (std::find(disabled.cbegin(), disabled.cend(), update_mod->GetName()) == disabled.cend()) { + for (const auto& file : update_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]; + for (const auto& cnmt_file_entry : section0->GetFiles()) { + if (cnmt_file_entry->GetExtension() == "cnmt") { + CNMT cnmt(cnmt_file_entry); + return cnmt.GetTitleVersion(); + } + } + } + } + } + } + } + } + } + + // --- NAND Check (FALLBACK) --- const auto update_tid = GetUpdateTitleID(title_id); if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) { return content_provider.GetEntryVersion(update_tid); @@ -633,12 +786,49 @@ std::optional PatchManager::GetGameVersion() const { } PatchManager::Metadata PatchManager::GetControlMetadata() const { - const auto base_control_nca = content_provider.GetEntry(title_id, ContentRecordType::Control); - if (base_control_nca == nullptr) { + 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; + + const std::string mod_name = update_mod->GetName(); + if (std::find(disabled_for_game.begin(), disabled_for_game.end(), mod_name) == 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) { + LOG_INFO(Loader, "Found active Autoloader Control NCA in '{}'", mod_name); + control_nca = std::make_unique(file); + // Found the highest priority enabled control, we are done. + return ParseControlNCA(*control_nca); + } + } + } + } + } + } + } + + // Fallback to NAND if no enabled Autoloader update was found + const auto update_tid = GetUpdateTitleID(title_id); + 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(*base_control_nca); + return ParseControlNCA(*control_nca); } PatchManager::Metadata PatchManager::ParseControlNCA(const NCA& nca) const { @@ -664,35 +854,28 @@ PatchManager::Metadata PatchManager::ParseControlNCA(const NCA& nca) const { auto nacp = nacp_file == nullptr ? nullptr : std::make_unique(nacp_file); - // Get language code from settings const auto language_code = Service::Set::GetLanguageCodeFromIndex( static_cast(Settings::values.language_index.GetValue())); - // Convert to application language and get priority list 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); - // Convert to language names - auto priority_language_names = FileSys::LANGUAGE_NAMES; // Copy + auto priority_language_names = FileSys::LANGUAGE_NAMES; if (language_priority_list) { for (size_t i = 0; i < priority_language_names.size(); ++i) { - // Relies on FileSys::LANGUAGE_NAMES being in the same order as - // Service::NS::ApplicationLanguage 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]; } else { - // Not a catastrophe, unlikely to happen LOG_WARNING(Loader, "Invalid language index {}", language_index); } } } - // Get first matching icon VirtualFile icon_file; for (const auto& language : priority_language_names) { icon_file = extracted->GetFile(std::string("icon_").append(language).append(".dat")); From cd742e76330fff48ce96d9f3e1266cfb12a9fd36 Mon Sep 17 00:00:00 2001 From: collecting Date: Tue, 21 Oct 2025 20:55:16 +0000 Subject: [PATCH 4/9] feat(fs): Implement Autoloader (W.I.P.) --- src/core/file_sys/registered_cache.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/file_sys/registered_cache.h b/src/core/file_sys/registered_cache.h index bf111e466..ffcc9ea18 100644 --- a/src/core/file_sys/registered_cache.h +++ b/src/core/file_sys/registered_cache.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -208,6 +209,7 @@ enum class ContentProviderUnionSlot { UserNAND, ///< User NAND SDMC, ///< SD Card FrontendManual, ///< Frontend-defined game list or similar + Autoloader, ///< Separate functionality for multiple Updates/DLCs without being overwritten by NAND. }; // Combines multiple ContentProvider(s) (i.e. SysNAND, UserNAND, SDMC) into one interface. From 9f71d092c808b144637ac92aed85cce32fd8ce2b Mon Sep 17 00:00:00 2001 From: collecting Date: Tue, 21 Oct 2025 20:57:41 +0000 Subject: [PATCH 5/9] feat(fs): Implement Autoloader (W.I.P.) --- src/citron/configuration/configure_per_game_addons.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/citron/configuration/configure_per_game_addons.h b/src/citron/configuration/configure_per_game_addons.h index 32dc5dde6..3e6971c8f 100644 --- a/src/citron/configuration/configure_per_game_addons.h +++ b/src/citron/configuration/configure_per_game_addons.h @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2016 Citra Emulator Project +// SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #pragma once @@ -7,6 +8,7 @@ #include #include +#include #include "core/file_sys/vfs/vfs_types.h" @@ -14,7 +16,6 @@ namespace Core { class System; } -class QGraphicsScene; class QStandardItem; class QStandardItemModel; class QTreeView; From 06cb160c00d0740ac7e5f3798a57ddc8ac07ec57 Mon Sep 17 00:00:00 2001 From: collecting Date: Tue, 21 Oct 2025 20:59:12 +0000 Subject: [PATCH 6/9] feat(fs): Implement Autoloader (W.I.P.) --- src/citron/main.cpp | 266 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 264 insertions(+), 2 deletions(-) diff --git a/src/citron/main.cpp b/src/citron/main.cpp index 66fb8eafa..7a53ea7ac 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -37,6 +37,7 @@ #include "applets/qt_profile_select.h" #include "applets/qt_software_keyboard.h" #include "applets/qt_web_browser.h" +#include "common/hex_util.h" #include "common/nvidia_flags.h" #include "common/settings_enums.h" #include "configuration/configure_input.h" @@ -45,6 +46,7 @@ #include "core/file_sys/romfs_factory.h" #include "core/file_sys/vfs/vfs.h" #include "core/file_sys/vfs/vfs_real.h" +#include "core/file_sys/nca_metadata.h" #include "core/frontend/applets/cabinet.h" #include "core/frontend/applets/controller.h" #include "core/frontend/applets/general.h" @@ -433,10 +435,13 @@ GMainWindow::GMainWindow(std::unique_ptr config_, bool has_broken_vulk show(); system->SetContentProvider(std::make_unique()); - system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual, - provider.get()); + system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual, provider.get()); system->GetFileSystemController().CreateFactories(*vfs); + autoloader_provider = std::make_unique(); + system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::Autoloader, + autoloader_provider.get()); + // Remove cached contents generated during the previous session RemoveCachedContents(); @@ -1582,6 +1587,7 @@ void GMainWindow::ConnectMenuEvents() { connect_menu(ui->action_Load_File, &GMainWindow::OnMenuLoadFile); connect_menu(ui->action_Load_Folder, &GMainWindow::OnMenuLoadFolder); connect_menu(ui->action_Install_File_NAND, &GMainWindow::OnMenuInstallToNAND); + connect_menu(ui->action_Install_With_Autoloader, &GMainWindow::OnMenuInstallWithAutoloader); connect_menu(ui->action_Trim_XCI_File, &GMainWindow::OnMenuTrimXCI); connect_menu(ui->action_Exit, &QMainWindow::close); connect_menu(ui->action_Load_Amiibo, &GMainWindow::OnLoadAmiibo); @@ -1971,6 +1977,8 @@ void GMainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletP StartGameType type) { LOG_INFO(Frontend, "citron starting..."); + RegisterAutoloaderContents(); + if (params.program_id == 0 || params.program_id > static_cast(Service::AM::AppletProgramId::MaxProgramId)) { StoreRecentFile(filename); // Put the filename on top of the list @@ -6129,6 +6137,260 @@ void GMainWindow::CheckForUpdatesAutomatically() { #endif } +void GMainWindow::RegisterAutoloaderContents() { + autoloader_provider->ClearAllEntries(); + const auto& disabled_addons = Settings::values.disabled_addons; + + const auto sdmc_path = Common::FS::GetCitronPath(Common::FS::CitronPath::SDMCDir); + const auto autoloader_root = sdmc_path / "autoloader"; + if (!Common::FS::IsDir(autoloader_root)) { + return; + } + + LOG_INFO(Frontend, "Scanning for Autoloader contents..."); + + for (const auto& title_dir_entry : std::filesystem::directory_iterator(autoloader_root)) { + if (!title_dir_entry.is_directory()) continue; + + u64 title_id_val = 0; + try { + title_id_val = std::stoull(title_dir_entry.path().filename().string(), nullptr, 16); + } catch (const std::invalid_argument&) { + continue; + } + + const auto it = disabled_addons.find(title_id_val); + const auto& disabled_for_game = (it != disabled_addons.end()) ? it->second : std::vector{}; + + const auto process_content_type = [&](const std::filesystem::path& content_path) { + if (!Common::FS::IsDir(content_path)) return; + + for (const auto& mod_dir_entry : std::filesystem::directory_iterator(content_path)) { + if (!mod_dir_entry.is_directory()) continue; + + const std::string mod_name = mod_dir_entry.path().filename().string(); + if (std::find(disabled_for_game.begin(), disabled_for_game.end(), mod_name) != disabled_for_game.end()) { + LOG_INFO(Frontend, "Skipping disabled Autoloader content: {}", mod_name); + continue; + } + + std::optional cnmt; + for (const auto& file_entry : std::filesystem::directory_iterator(mod_dir_entry.path())) { + if (file_entry.path().string().ends_with(".cnmt.nca")) { + auto vfs_file = vfs->OpenFile(file_entry.path().string(), FileSys::OpenMode::Read); + if (vfs_file) { + FileSys::NCA meta_nca(vfs_file); + if (meta_nca.GetStatus() == Loader::ResultStatus::Success && !meta_nca.GetSubdirectories().empty()) { + auto section0 = meta_nca.GetSubdirectories()[0]; + if (!section0->GetFiles().empty()) { + cnmt.emplace(section0->GetFiles()[0]); + break; + } + } + } + } + } + + if (!cnmt) continue; + + for (const auto& record : cnmt->GetContentRecords()) { + std::string nca_filename = Common::HexToString(record.nca_id) + ".nca"; + std::filesystem::path nca_path = mod_dir_entry.path() / nca_filename; + auto nca_vfs_file = vfs->OpenFile(nca_path.string(), FileSys::OpenMode::Read); + if (nca_vfs_file) { + autoloader_provider->AddEntry(cnmt->GetType(), record.type, cnmt->GetTitleID(), nca_vfs_file); + } + } + } + }; + + process_content_type(title_dir_entry.path() / "Updates"); + process_content_type(title_dir_entry.path() / "DLC"); + } +} + +void GMainWindow::OnMenuInstallWithAutoloader() { + LOG_INFO(Loader, "AUTOLOADER: Starting Autoloader installation process."); + + const QString file_filter = tr("Nintendo Submission Package (*.nsp)"); + QStringList filenames = QFileDialog::getOpenFileNames( + this, tr("Select Update/DLC Files for Autoloader"), + QString::fromStdString(UISettings::values.roms_path), file_filter); + + if (filenames.isEmpty()) { + return; + } + + UISettings::values.roms_path = QFileInfo(filenames[0]).path().toStdString(); + + // Calculate the total size of all files to be installed for the progress dialog. + qint64 total_size_bytes = 0; + for (const QString& file : filenames) { + QString sanitized_path = file; + if (sanitized_path.contains(QLatin1String(".nsp/"))) { + sanitized_path = sanitized_path.left(sanitized_path.indexOf(QLatin1String(".nsp/")) + 4); + } + auto vfs_file = vfs->OpenFile(sanitized_path.toStdString(), FileSys::OpenMode::Read); + if (vfs_file) { + FileSys::NSP nsp(vfs_file); + if (nsp.GetStatus() == Loader::ResultStatus::Success) { + for (const auto& title_pair : nsp.GetNCAs()) { + for (const auto& nca_pair : title_pair.second) { + total_size_bytes += nca_pair.second->GetBaseFile()->GetSize(); + } + } + } + } + } + + if (total_size_bytes == 0) { + QMessageBox::warning(this, tr("No files to install"), tr("Could not find any valid files to install in the selected NSPs.")); + return; + } + + QProgressDialog progress(tr("Installing to Autoloader..."), tr("Cancel"), 0, 100, this); + progress.setWindowModality(Qt::WindowModal); + progress.setMinimumDuration(0); + progress.setValue(0); + progress.show(); + + qint64 total_copied_bytes = 0; + int success_count = 0; + QStringList failed_files; + + for (const QString& file : filenames) { + progress.setLabelText(tr("Installing %1...").arg(QFileInfo(file).fileName())); + QCoreApplication::processEvents(); + + if (progress.wasCanceled()) { + break; + } + + QString sanitized_path = file; + if (sanitized_path.contains(QLatin1String(".nsp/"))) { + sanitized_path = sanitized_path.left(sanitized_path.indexOf(QLatin1String(".nsp/")) + 4); + } + const std::string file_path = sanitized_path.toStdString(); + LOG_INFO(Loader, "AUTOLOADER: Processing sanitized file path: {}", file_path); + + auto vfs_file = vfs->OpenFile(file_path, FileSys::OpenMode::Read); + if (!vfs_file) { + LOG_ERROR(Loader, "AUTOLOADER: FAILED at VFS Open. Could not open file: {}", file_path); + failed_files.append(QFileInfo(file).fileName() + tr(" (File Open Error)")); + continue; + } + + FileSys::NSP nsp(vfs_file); + if (nsp.GetStatus() != Loader::ResultStatus::Success) { + LOG_ERROR(Loader, "AUTOLOADER: FAILED at NSP Parse for file: {}", file_path); + failed_files.append(QFileInfo(file).fileName() + tr(" (NSP Parse Error)")); + continue; + } + + const auto title_map = nsp.GetNCAs(); + if (title_map.empty()) { + LOG_ERROR(Loader, "AUTOLOADER: FAILED, NSP contains no titles: {}", file_path); + failed_files.append(QFileInfo(file).fileName() + tr(" (Empty NSP)")); + continue; + } + + const auto& [title_id, nca_map] = *title_map.begin(); + const auto& [type_pair, meta_nca] = *std::find_if(nca_map.begin(), nca_map.end(), [](const auto& pair){ + return pair.first.second == FileSys::ContentRecordType::Meta; + }); + + if (!meta_nca || meta_nca->GetSubdirectories().empty() || meta_nca->GetSubdirectories()[0]->GetFiles().empty()) { + LOG_ERROR(Loader, "AUTOLOADER: FAILED at Metadata search for title {}: malformed.", title_id); + failed_files.append(QFileInfo(file).fileName() + tr(" (Malformed Metadata)")); + continue; + } + + const auto cnmt_file = meta_nca->GetSubdirectories()[0]->GetFiles()[0]; + const FileSys::CNMT cnmt(cnmt_file); + + std::string type_folder = (cnmt.GetType() == FileSys::TitleType::Update) ? "Updates" : "DLC"; + u64 program_id = FileSys::GetBaseTitleID(title_id); + QString nsp_name = QFileInfo(sanitized_path).completeBaseName(); + std::string sdmc_path = Common::FS::GetCitronPathString(Common::FS::CitronPath::SDMCDir); + std::string dest_path_str = fmt::format("{}/autoloader/{:016X}/{}/{}", sdmc_path, program_id, type_folder, nsp_name.toStdString()); + + auto dest_dir = vfs->CreateDirectory(dest_path_str, FileSys::OpenMode::ReadWrite); + if (!dest_dir) { + LOG_ERROR(Loader, "AUTOLOADER: FAILED to create destination directory: {}", dest_path_str); + failed_files.append(QFileInfo(file).fileName() + tr(" (Directory Creation Error)")); + continue; + } + + bool copy_failed = false; + for (const auto& [key, nca] : nca_map) { + auto source_file = nca->GetBaseFile(); + auto dest_file = dest_dir->CreateFileRelative(source_file->GetName()); + + if (!dest_file->Resize(source_file->GetSize())) { + LOG_ERROR(Loader, "AUTOLOADER: FAILED to resize destination file for {}.", source_file->GetName()); + copy_failed = true; + break; + } + + std::vector buffer(CopyBufferSize); + for (std::size_t i = 0; i < source_file->GetSize(); i += buffer.size()) { + if (progress.wasCanceled()) { + dest_file->Resize(0); + copy_failed = true; + break; + } + + const auto bytes_to_read = std::min(buffer.size(), source_file->GetSize() - i); + const auto bytes_read = source_file->Read(buffer.data(), bytes_to_read, i); + + if (bytes_read == 0 && i < source_file->GetSize()) { + LOG_ERROR(Loader, "AUTOLOADER: FAILED to read from source file {}.", source_file->GetName()); + copy_failed = true; + break; + } + + dest_file->Write(buffer.data(), bytes_read, i); + + total_copied_bytes += bytes_read; + progress.setValue((total_copied_bytes * 100) / total_size_bytes); + QCoreApplication::processEvents(); + } + + if (copy_failed) { + break; + } + } + + if (progress.wasCanceled()) { + failed_files.append(QFileInfo(file).fileName() + tr(" (Cancelled)")); + vfs->DeleteDirectory(dest_path_str); + break; + } + + if (copy_failed) { + failed_files.append(QFileInfo(file).fileName()); + vfs->DeleteDirectory(dest_path_str); + } else { + success_count++; + } + } + + progress.close(); + + QString message = tr("Autoloader install finished."); + if (success_count > 0) { + message += tr("\n%n file(s) successfully installed.", "", success_count); + } + if (!failed_files.isEmpty()) { + message += tr("\n%n file(s) failed to install:", "", failed_files.size()); + message += QStringLiteral("\n- ") + failed_files.join(QStringLiteral("\n- ")); + } + QMessageBox::information(this, tr("Install Complete"), message); + + RegisterAutoloaderContents(); + game_list->PopulateAsync(UISettings::values.game_dirs); +} + void GMainWindow::OnToggleGridView() { game_list->ToggleViewMode(); } From e7b6954511c836b5647064f358dd2e3a9919bd78 Mon Sep 17 00:00:00 2001 From: collecting Date: Tue, 21 Oct 2025 21:01:05 +0000 Subject: [PATCH 7/9] feat(fs): Implement Autoloader (W.I.P.) --- src/citron/main.ui | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/citron/main.ui b/src/citron/main.ui index 79aa342b5..e0ce46b5a 100644 --- a/src/citron/main.ui +++ b/src/citron/main.ui @@ -58,7 +58,7 @@ - + @@ -207,13 +207,13 @@ &Install Files to NAND... - - - true - - - &Trim XCI File... - + + + true + + + Install Files with &Autoloader... + From 5712217a09cb33b4097a0bdee411a9c1471e92e3 Mon Sep 17 00:00:00 2001 From: collecting Date: Tue, 21 Oct 2025 21:25:49 +0000 Subject: [PATCH 8/9] rebase: Autoloader & XCI --- src/citron/main.ui | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/citron/main.ui b/src/citron/main.ui index e0ce46b5a..2b2db061a 100644 --- a/src/citron/main.ui +++ b/src/citron/main.ui @@ -58,6 +58,7 @@ + @@ -208,12 +209,20 @@ - - true - - - Install Files with &Autoloader... - + + true + + + Install Files with &Autoloader... + + + + + true + + + &Trim XCI File... + From cb7b3f6625ef977a333e689b4839e8e6b14f7926 Mon Sep 17 00:00:00 2001 From: collecting Date: Tue, 21 Oct 2025 21:26:23 +0000 Subject: [PATCH 9/9] rebase: Autoloader & XCI --- src/citron/main.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/citron/main.h b/src/citron/main.h index 8e1b9cbef..30ab3f2cc 100644 --- a/src/citron/main.h +++ b/src/citron/main.h @@ -381,6 +381,7 @@ private slots: void OnMenuLoadFolder(); void IncrementInstallProgress(); void OnMenuInstallToNAND(); + void OnMenuTrimXCI(); void OnMenuInstallWithAutoloader(); void OnMenuRecentFile(); void OnConfigure();