diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index f66467d410..33b4a9faaa 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,6 +1,9 @@ # SPDX-FileCopyrightText: 2018 yuzu Emulator Project # SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +# SPDX-License-Identifier: GPL-3.0-or-later + add_library(core STATIC arm/arm_interface.cpp arm/arm_interface.h @@ -175,6 +178,8 @@ add_library(core STATIC file_sys/vfs/vfs_vector.h file_sys/xts_archive.cpp file_sys/xts_archive.h + file_sys/external_content_manager.cpp + file_sys/external_content_manager.h frontend/applets/cabinet.cpp frontend/applets/cabinet.h frontend/applets/controller.cpp diff --git a/src/core/file_sys/content_archive.cpp b/src/core/file_sys/content_archive.cpp index 6652523589..7202a6d6c1 100644 --- a/src/core/file_sys/content_archive.cpp +++ b/src/core/file_sys/content_archive.cpp @@ -1,6 +1,9 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + #include <algorithm> #include <cstring> #include <optional> @@ -117,9 +120,7 @@ NCA::NCA(VirtualFile file_, const NCA* base_nca) } if (is_update && base_nca == nullptr) { - status = Loader::ResultStatus::ErrorMissingBKTRBaseRomFS; - } else { - status = Loader::ResultStatus::Success; + // Ignore this for now to allow external addOns; it shouldn't affect anything } } diff --git a/src/core/file_sys/external_content_manager.cpp b/src/core/file_sys/external_content_manager.cpp new file mode 100644 index 0000000000..114fa4c648 --- /dev/null +++ b/src/core/file_sys/external_content_manager.cpp @@ -0,0 +1,387 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <algorithm> +#include <filesystem> +#include <fstream> +#include <memory> +#include <string> +#include <set> +#include <regex> +#include "common/fs/file.h" +#include "common/fs/fs.h" +#include "common/logging/log.h" +#include "common/string_util.h" +#include "core/file_sys/external_content_manager.h" +#include "core/file_sys/content_archive.h" +#include "core/file_sys/nca_metadata.h" +#include "core/file_sys/registered_cache.h" +#include "core/file_sys/romfs.h" +#include "core/file_sys/submission_package.h" +#include "core/file_sys/vfs/vfs.h" +#include "core/file_sys/vfs/vfs_real.h" +#include "core/loader/loader.h" +#include "core/file_sys/common_funcs.h" + +namespace FileSys { + +ExternalContentManager::ExternalContentManager() { + vfs = std::make_shared<RealVfsFilesystem>(); +} + +ExternalContentManager::~ExternalContentManager() = default; + +bool ExternalContentManager::RegisterExternalNSP(const std::string& path, u64 program_id) { + if (registered_paths.count(path)) { + return true; + } + + if (path.empty()) { + LOG_ERROR(Loader, "Empty path provided to RegisterExternalNSPInternal"); + return false; + } + + if (!Common::FS::Exists(path)) { + LOG_ERROR(Loader, "File does not exist: {}", path); + return false; + } + + auto file = vfs->OpenFile(path, FileSys::OpenMode::Read); + if (file == nullptr) { + LOG_ERROR(Loader, "Failed to open NSP file with VFS: {}", path); + return false; + } + + std::shared_ptr<NSP> nsp; + try { + nsp = std::make_shared<NSP>(file); + if (nsp->GetStatus() != Loader::ResultStatus::Success) { + LOG_ERROR(Loader, "Failed to parse NSP file: {} (status: {})", + path, static_cast<int>(nsp->GetStatus())); + return false; + } + } catch (const std::exception& e) { + LOG_CRITICAL(Loader, "Exception when parsing NSP file {}: {}", path, e.what()); + return false; + } + + u64 target_program_id = program_id; + if (target_program_id == 0) { + try { + target_program_id = ExtractProgramIDFromFile(nsp); + } catch (const std::exception& e) { + LOG_ERROR(Loader, "Exception when getting program ID from NSP {}: {}", path, e.what()); + return false; + } + } + + external_files[target_program_id].emplace_back(path, file); + registered_paths.insert(path); + + ParseExternalFile(path, target_program_id); + return true; +} + +bool ExternalContentManager::RequestRegisterExternalNSP(const std::string& path, u64 program_id) { + std::lock_guard lock{mutex}; + return RegisterExternalNSP(path, program_id); +} + +void ExternalContentManager::LoadRegisteredPaths(const std::string& config_path) { + std::lock_guard lock{mutex}; + registered_paths.clear(); + external_files.clear(); + update_to_base_map.clear(); + dlc_to_base_map.clear(); + file_metadata.clear(); + + std::ifstream file(config_path); + if (!file.is_open()) { + LOG_WARNING(Service_FS, "Could not open external files config for loading: {}", config_path); + return; + } + + std::string path; + std::set<std::string> valid_paths_from_config; + + while (std::getline(file, path)) { + if (path.empty()) continue; + if (!Common::FS::Exists(path)) { + LOG_WARNING(Loader, "External file path from config not found, skipping: {}", path); + continue; + } + + valid_paths_from_config.insert(path); + + const auto extension = Common::ToLower(std::filesystem::path(path).extension().string()); + bool success = false; + if (extension == ".nsp") { + success = RegisterExternalNSP(path, 0); + } + + if (!success) { + LOG_ERROR(Loader, "Failed to re-register external NSP from config: {}", path); + valid_paths_from_config.erase(path); + } + } + + file.close(); + registered_paths = std::move(valid_paths_from_config); +} + +void ExternalContentManager::SaveRegisteredPaths(const std::string& config_path) const { + std::lock_guard lock{mutex}; + + std::ofstream file(config_path); + if (!file.is_open()) { + LOG_ERROR(Frontend, "Could not open external files config for saving: {}", config_path); + return; + } + + for (const auto& path : registered_paths) { + if (Common::FS::Exists(path)) { + const auto extension = Common::ToLower(std::filesystem::path(path).extension().string()); + if (extension == ".nsp") { // Only save NSP paths + file << path << std::endl; + } + } + } + + file.close(); +} + +std::set<std::string> ExternalContentManager::GetAllRegisteredPaths() const { + std::lock_guard lock{mutex}; + return registered_paths; +} + +/** + * @brief Checks if a given base program ID exists as a value in the map. + * @tparam MapType The type of the map (e.g., std::unordered_map<u64, u64>) + * @param map The map to search (e.g., update_to_base_map or dlc_to_base_map) + * @param program_id The base program ID to look for as a value in the map + * @return true if the program_id is found as a base ID in the map, false otherwise. + */ +template <typename MapType> +bool ExternalContentManager::CheckMapForBaseID(const MapType& map, u64 program_id) const { + for (const auto& [content_id, base_id] : map) { + if (base_id == program_id) { + return true; + } + } + return false; +} + +bool ExternalContentManager::HasExternalUpdate(u64 program_id) const { + std::lock_guard lock{mutex}; + return CheckMapForBaseID(update_to_base_map, program_id); +} + +bool ExternalContentManager::HasExternalDLC(u64 program_id) const { + std::lock_guard lock{mutex}; + const bool has_dlc = CheckMapForBaseID(dlc_to_base_map, program_id); + return has_dlc; +} + +std::vector<std::shared_ptr<VfsFile>> ExternalContentManager::GetExternalFiles( + u64 program_id, ExternalContentType content_type, bool first_only = false) const { + std::lock_guard lock{mutex}; + + std::vector<std::shared_ptr<VfsFile>> result; + + const auto& map = (content_type == ExternalContentType::Update) ? + update_to_base_map : dlc_to_base_map; + + for (const auto& [content_id, base_id] : map) { + if (base_id == program_id) { + auto it = external_files.find(content_id); + if (it != external_files.end() && !it->second.empty()) { + if (first_only) { + // Update + result.push_back(it->second.front().second); + break; + } + // DLC + for (const auto& [path, file] : it->second) { + result.push_back(file); + } + } + } + } + + return result; +} + +std::shared_ptr<VfsFile> ExternalContentManager::GetExternalUpdateFile(u64 program_id) const { + auto files = GetExternalFiles(program_id, ExternalContentType::Update, true); + return files.empty() ? nullptr : files.front(); +} + +std::vector<std::shared_ptr<VfsFile>> ExternalContentManager::GetExternalDLCFiles(u64 program_id) const { + return GetExternalFiles(program_id, ExternalContentType::DLC, false); +} + +void ExternalContentManager::ParseExternalFile(const std::string& path, u64 program_id) { + ExternalFileMetadata metadata; + std::string filename = std::filesystem::path(path).filename().string(); + + const auto extension = std::filesystem::path(path).extension().string(); + + // Should never happen because non nsp files are blocked by the frontend but just in case + if (Common::ToLower(extension) != ".nsp") { + LOG_CRITICAL(Loader, "Only NSP files are supported: {}", path); + return; + } + + auto file = vfs->OpenFile(path, FileSys::OpenMode::Read); + if (file == nullptr) { + LOG_ERROR(Loader, "Failed to open file for parsing: {}", path); + return; + } + + const auto program_id_low = program_id & 0xFFFFFFFF; + const bool is_update = (program_id_low & 0x800) != 0 && (program_id_low & 0xFFF) < 0x1000; + const bool is_dlc = (program_id_low & 0xF000) != 0 || (GetBaseTitleID(program_id) != program_id); + + metadata.name = is_update ? + fmt::format("Update (File): {}", filename) : + (is_dlc ? fmt::format("DLC (File): {}", filename) : filename); + + try { + auto nsp = std::make_shared<FileSys::NSP>(file); + if (nsp->GetStatus() != Loader::ResultStatus::Success) { + LOG_WARNING(Loader, "Could not parse NSP for metadata: {}", path); + file_metadata[program_id] = metadata; + return; + } + + const TitleType title_type = is_update ? TitleType::Update : + (is_dlc ? TitleType::AOC : TitleType::Application); + + // For updates, we need program_id but for DLC we need the actual program_id (not OR'd with 0x800) + const u64 control_id = is_update ? program_id : + (is_dlc ? program_id : (program_id | 0x800)); + + auto control = nsp->GetNCA(control_id, ContentRecordType::Control, title_type); + + if (control && control->GetStatus() == Loader::ResultStatus::Success) { + // Extract metadata from control NCA (Needed for version string) + if (auto romfs = control->GetRomFS()) { + if (auto extracted = ExtractRomFS(romfs)) { + auto nacp_file = extracted->GetFile("control.nacp"); + if (!nacp_file) { + nacp_file = extracted->GetFile("Control.nacp"); + } + + if (nacp_file) { + NACP nacp(nacp_file); + metadata.version = nacp.GetVersionString(); + } + + } + } + } + + if (is_update) { + const u64 base_program_id = program_id & ~0x800ULL; + update_to_base_map[program_id] = base_program_id; + LOG_INFO(Loader, "Identified external NSP as update: {} ({}) for base game {:016X}", + path, metadata.version, base_program_id); + } else if (is_dlc) { + const u64 possible_base_id = GetBaseTitleID(program_id); + dlc_to_base_map[program_id] = possible_base_id; + LOG_INFO(Loader, "Identified external NSP as DLC: {} ({}) for base game {:016X}", + path, metadata.version, possible_base_id); + } else { + LOG_WARNING(Loader, "External NSP {} does not match update or DLC criteria", path); + } + } catch (const std::exception& e) { + LOG_ERROR(Loader, "Error processing external file: {}", e.what()); + } + + file_metadata[program_id] = metadata; +} + +ExternalFileMetadata ExternalContentManager::GetExternalUpdateMetadata(u64 program_id) const { + std::lock_guard lock{mutex}; + + for (const auto& [update_id, base_id] : update_to_base_map) { + if (base_id == program_id) { + auto it = file_metadata.find(update_id); + if (it != file_metadata.end()) { + return it->second; + } + } + } + + // Placeholder empty metadata + ExternalFileMetadata empty_metadata; + empty_metadata.name = ""; + empty_metadata.version = ""; + return empty_metadata; +} + +ExternalFileMetadata ExternalContentManager::GetExternalDLCMetadata(u64 program_id) const { + std::lock_guard lock{mutex}; + ExternalFileMetadata metadata; + metadata.name = "DLC (File)"; + + std::string version_list; + bool first = true; + + for (const auto& [dlc_id, base_id] : dlc_to_base_map) { + if (base_id == program_id) { + if (!first) { + version_list += ", "; + } + + version_list += fmt::format("{}", dlc_id & 0x7FF); + first = false; + + auto it = file_metadata.find(dlc_id); + if (it != file_metadata.end() && !metadata.name.empty()) { + metadata.name = it->second.name; + } + } + } + + metadata.version = version_list.empty() ? "Unknown" : version_list; + return metadata; +} + +u64 ExternalContentManager::ExtractProgramIDFromFile(const std::shared_ptr<NSP>& nsp) { + u64 id = nsp->GetProgramTitleID(); + if (id != 0) return id; + + LOG_WARNING(Loader, "NSP returned zero program ID, attempting fallback methods"); + + // Check all title IDs + const auto all_ids = nsp->GetProgramTitleIDs(); + for (const auto& title_id : all_ids) { + if (title_id != 0) { + LOG_INFO(Loader, "Found ID from title IDs list: {:016X}", title_id); + return title_id; + } + } + + // If all else fails, check the NCA files + const auto ncas = nsp->GetNCAsCollapsed(); + for (const auto& nca : ncas) { + const auto nca_id = nca->GetTitleId(); + if (nca_id != 0) { + LOG_INFO(Loader, "Found ID from NCA: {:016X}", nca_id); + return nca_id; + } + } + + LOG_CRITICAL(Loader, "All ID detection methods failed, defaulting to 0"); + return 0; +} + +std::shared_ptr<ExternalContentManager> GetExternalContentManager() { + static std::shared_ptr<ExternalContentManager> instance = + std::make_shared<ExternalContentManager>(); + return instance; +} + +} // namespace FileSys diff --git a/src/core/file_sys/external_content_manager.h b/src/core/file_sys/external_content_manager.h new file mode 100644 index 0000000000..b28e16f89b --- /dev/null +++ b/src/core/file_sys/external_content_manager.h @@ -0,0 +1,191 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <map> +#include <memory> +#include <mutex> +#include <string> +#include <unordered_map> +#include <vector> +#include <set> + +#include "common/common_types.h" +#include "core/file_sys/submission_package.h" +#include "core/file_sys/vfs/vfs.h" + +namespace FileSys { +class ContentProvider; +class NSP; +} // namespace FileSys + +namespace FileSys { + +class VfsFile; +class VfsFilesystem; + +/** + * @brief Struct that holds metadata for an external content file). + */ +struct ExternalFileMetadata { + std::string name; + std::string version; +}; + + +/** + * @brief The type of external content. + */ +enum class ExternalContentType { + Update, + DLC +}; + +/** + * @brief Manages external NSP addon files that are not installed to the virtual NAND. + * + */ +class ExternalContentManager { +public: + explicit ExternalContentManager(); + ~ExternalContentManager(); + + /** + * @brief Registers an external NSP file with the manager. + * @param path The literal path to the NSP file. + * @param program_id The program ID to associate with this file. If 0, the ID is read from the NSP. + * @return true if registration was successful, false otherwise (e.g., file not found, parse error). + */ + bool RequestRegisterExternalNSP(const std::string& path, u64 program_id = 0); + + + /** + * @brief Checks if an external update is registered for the given base game ID. + * @param program_id The base program ID of the game. + */ + bool HasExternalUpdate(u64 program_id) const; + + /** + * @brief Checks if any external DLC is registered for the given base game ID. + * @param program_id The base program ID of the game. + */ + bool HasExternalDLC(u64 program_id) const; + + /** + * @brief Gets the VFS file object for a registered external file for a given base game ID. + * @param program_id The base program ID of the game. + * @param content_type The type of external content (update or DLC). + * @param first_only If true, only the first file is returned. If false, all files are returned. + * @return A vector of shared pointers to VfsFile objects. Empty if no files found. + */ + std::vector<std::shared_ptr<VfsFile>> GetExternalFiles(u64 program_id, ExternalContentType content_type, + bool first_only) const; + + /** + * @brief Gets the VFS file object for the registered external update for a given base game ID. + * @param program_id The base program ID of the game. + * @return A shared pointer to the VfsFile if an update is found, nullptr otherwise. + */ + std::shared_ptr<VfsFile> GetExternalUpdateFile(u64 program_id) const; + + /** + * @brief Gets a list of VFS file objects for all registered external DLC for a given base game ID. + * @param program_id The base program ID of the game. + * @return A vector of shared pointers to VfsFile objects for the DLC. Empty if no DLC found. + */ + std::vector<std::shared_ptr<VfsFile>> GetExternalDLCFiles(u64 program_id) const; + + /** + * @brief Gets the extracted metadata for the registered external file for a given base game ID. + * @param program_id The base program ID of the game. + * @return An ExternalFileMetadata struct. Contains default values if no update or metadata is found. + */ + ExternalFileMetadata GetExternalUpdateMetadata(u64 program_id) const; + + /** + * @brief Get metadata for DLC files associated with the given base program ID. + * @param program_id The base program ID of the game. + * @return An ExternalFileMetadata struct. Contains default values if no DLC or metadata is found. + */ + ExternalFileMetadata GetExternalDLCMetadata(u64 program_id) const; + + /** + * @brief Extracts the program ID from an NSP file using multiple fallback methods. + * @param nsp The NSP file to extract the program ID from. + * @return The extracted program ID, or 0 if extraction failed. + */ + static u64 ExtractProgramIDFromFile(const std::shared_ptr<NSP> &nsp); + + /** + * @brief Loads the list of registered external NSP paths from a configuration file. + * Clears existing state before loading. Validates paths and attempts re-registration. + * @param config_path Path to the configuration file. + */ + void LoadRegisteredPaths(const std::string& config_path); + + /** + * @brief Saves the list of currently registered and existing external NSP paths to a configuration file. + * @param config_path Path to the configuration file. + */ + void SaveRegisteredPaths(const std::string& config_path) const; + + /** + * @brief Gets the set of all currently registered external NSP file paths. + * @return A copy of the set containing absolute paths. + */ + std::set<std::string> GetAllRegisteredPaths() const; + + /** + * @brief Gets the mapping of external DLC IDs to their base game IDs. + * @return A const reference to the unordered map of external DLC IDs to base game IDs. + */ + const std::unordered_map<u64, u64>& GetExternalDLCMapping() const { + return dlc_to_base_map; + } + + /** + * @brief Gets the VFS file object for a registered external DLC by its title ID. + * @param title_id The title ID of the DLC. + */ + std::shared_ptr<VfsFile> GetExternalDLCFileByID(u64 title_id) const { + std::lock_guard lock{mutex}; + auto it = external_files.find(title_id); + if (it != external_files.end() && !it->second.empty()) { + return it->second.front().second; + } + return nullptr; + } + +private: + /** + * @brief Internal, non-locking version of NSP registration logic. + * Assumes caller holds the mutex. + */ + bool RegisterExternalNSP(const std::string& path, u64 program_id); + + /** + * @brief Parses a registered file to determine if it's an update or DLC and populates internal maps. + * Also triggers metadata extraction and storage. + */ + void ParseExternalFile(const std::string& path, u64 program_id); + + template <typename MapType> + bool CheckMapForBaseID(const MapType& map, u64 program_id) const; + + std::shared_ptr<VfsFilesystem> vfs; + std::set<std::string> registered_paths; + std::unordered_map<u64, std::vector<std::pair<std::string, std::shared_ptr<VfsFile>>>> external_files; + std::unordered_map<u64, u64> update_to_base_map; + std::unordered_map<u64, u64> dlc_to_base_map; + std::unordered_map<u64, ExternalFileMetadata> file_metadata; + mutable std::mutex mutex; +}; + +/** + * @brief Gets the instance of the ExternalContentManager. + * @return A shared pointer to the ExternalContentManager instance. + */ +std::shared_ptr<ExternalContentManager> GetExternalContentManager(); + +} // namespace FileSys \ No newline at end of file diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp index 21d45235e4..83cb8f688f 100644 --- a/src/core/file_sys/patch_manager.cpp +++ b/src/core/file_sys/patch_manager.cpp @@ -1,6 +1,9 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + #include <algorithm> #include <array> #include <cstddef> @@ -30,6 +33,7 @@ #include "core/loader/loader.h" #include "core/loader/nso.h" #include "core/memory/cheat_engine.h" +#include "core/file_sys/external_content_manager.h" namespace FileSys { namespace { @@ -117,9 +121,10 @@ bool IsDirValidAndNonEmpty(const VirtualDir& dir) { } // 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_} {} + const Service::FileSystem::FileSystemController& fs_controller_, + const ContentProvider& content_provider_) + : title_id{title_id_}, fs_controller{fs_controller_}, content_provider{content_provider_}, + external_manager{GetExternalContentManager()} {} PatchManager::~PatchManager() = default; @@ -128,30 +133,43 @@ u64 PatchManager::GetTitleID() const { } VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const { - LOG_INFO(Loader, "Patching ExeFS for title_id={:016X}", title_id); - - if (exefs == nullptr) + if (!exefs) return exefs; - const auto& disabled = Settings::values.disabled_addons[title_id]; - const auto update_disabled = - std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); + // Retrieve base Program NCA + const auto base_program_nca = content_provider.GetEntry(title_id, ContentRecordType::Program); + const bool update_disabled = IsAddOnDisabled("Update", false); + const bool external_update_disabled = IsAddOnDisabled("Update (File):", true); + bool external_update_applied = false; - // Game Updates + // External Game Updates + // TODO: Add a setting for users to select update priority (i.e if external update should be used over NAND) + if (!external_update_disabled && + external_manager && external_manager->HasExternalUpdate(title_id) && base_program_nca) { + + const auto update_file = external_manager->GetExternalUpdateFile(title_id); + if (update_file) { + auto external_exefs = PatchExeFSWithExternal(exefs, base_program_nca.get(), update_file); + if (external_exefs) { + const auto metadata = external_manager->GetExternalUpdateMetadata(title_id); + exefs = external_exefs; + external_update_applied = true; + } + } + } + + // NAND Game Updates 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: Update ({}) applied successfully", - FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0))); + if (!external_update_applied && !update_disabled && update && update->GetExeFS()) { 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<VirtualDir> patch_dirs = {sdmc_load_dir}; + std::vector<VirtualDir> layers; + 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()); @@ -160,8 +178,7 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const { std::sort(patch_dirs.begin(), patch_dirs.end(), [](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); }); - std::vector<VirtualDir> layers; - layers.reserve(patch_dirs.size() + 1); + const auto& disabled = Settings::values.disabled_addons[title_id]; for (const auto& subdir : patch_dirs) { if (std::find(disabled.begin(), disabled.end(), subdir->GetName()) != disabled.end()) continue; @@ -190,6 +207,23 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const { return exefs; } +VirtualDir PatchManager::PatchExeFSWithExternal(VirtualDir exefs, const NCA* base_nca, + VirtualFile external_update) const { + if (!external_update || !base_nca) { + return nullptr; + } + + auto new_nca = GetNCAfromExternalFile(external_update, ContentRecordType::Program, base_nca); + if (new_nca && new_nca->GetStatus() == Loader::ResultStatus::Success) { + const auto external_exefs = new_nca->GetExeFS(); + if (external_exefs) { + return external_exefs; + } + } + + return nullptr; +} + std::vector<VirtualFile> PatchManager::CollectPatches(const std::vector<VirtualDir>& patch_dirs, const std::string& build_id) const { const auto& disabled = Settings::values.disabled_addons[title_id]; @@ -431,15 +465,32 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs auto romfs = base_romfs; + // External updates + // TODO: Add a setting for users to select update priority (I.E if external update should be used over NAND) + const bool update_disabled = IsAddOnDisabled("Update", false); + const bool external_update_disabled = IsAddOnDisabled("Update (File):", true); + bool external_update_applied = false; + + if (!external_update_disabled && + external_manager && external_manager->HasExternalUpdate(title_id)) { + + const auto update_file = external_manager->GetExternalUpdateFile(title_id); + if (update_file && base_nca) { + auto patched = PatchRomFSWithExternal(base_nca, romfs, update_file, type); + if (patched) { + const auto metadata = external_manager->GetExternalUpdateMetadata(title_id); + LOG_INFO(Loader, "Applied external update RomFS (v{})", metadata.version); + romfs = patched; + external_update_applied = true; + } + } + } + // 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(); - - if (!update_disabled && update_raw != nullptr && base_nca != nullptr) { + if (!external_update_applied && !update_disabled && update_raw && base_nca) { const auto new_nca = std::make_shared<NCA>(update_raw, base_nca); if (new_nca->GetStatus() == Loader::ResultStatus::Success && new_nca->GetRomFS() != nullptr) { @@ -449,7 +500,7 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs const auto version = FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0)); } - } else if (!update_disabled && packed_update_raw != nullptr && base_nca != nullptr) { + } else if (!external_update_applied && !update_disabled && packed_update_raw != nullptr && base_nca != nullptr) { const auto new_nca = std::make_shared<NCA>(packed_update_raw, base_nca); if (new_nca->GetStatus() == Loader::ResultStatus::Success && new_nca->GetRomFS() != nullptr) { @@ -466,6 +517,23 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs return romfs; } +VirtualFile PatchManager::PatchRomFSWithExternal(const NCA* base_nca, VirtualFile base_romfs, + VirtualFile external_update, ContentRecordType type) const { + if (!external_update || !base_nca) { + return nullptr; + } + + auto new_nca = GetNCAfromExternalFile(external_update, type, base_nca); + if (new_nca && new_nca->GetStatus() == Loader::ResultStatus::Success) { + const auto update_romfs = new_nca->GetRomFS(); + if (update_romfs) { + return update_romfs; + } + } + + return nullptr; +} + std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const { if (title_id == 0) { return {}; @@ -480,8 +548,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const { const auto metadata = update.GetControlMetadata(); const auto& nacp = metadata.first; - const auto update_disabled = - std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); + const auto update_disabled = IsAddOnDisabled("Update", false); Patch update_patch = {.enabled = !update_disabled, .name = "Update", .version = "", @@ -507,6 +574,21 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const { } } + // External Updates + const auto external_update_disabled = IsAddOnDisabled("Update (File):", true); + if (external_manager->HasExternalUpdate(title_id)) { + // Get metadata for the external update + const auto target_metadata = external_manager->GetExternalUpdateMetadata(title_id); + + Patch external_patch = {.enabled = !external_update_disabled, + .name = target_metadata.name, + .version = target_metadata.version, + .type = PatchType::Update, + .program_id = title_id, + .title_id = title_id}; + out.push_back(external_patch); + } + // General Mods (LayeredFS and IPS) const auto mod_dir = fs_controller.GetModificationLoadRoot(title_id); if (mod_dir != nullptr) { @@ -610,10 +692,34 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const { .title_id = dlc_match.back().title_id}); } + // External DLC + const auto external_dlc_disabled = IsAddOnDisabled("DLC (File):", true); + if (external_manager->HasExternalDLC(title_id)) { + const auto target_metadata = external_manager->GetExternalDLCMetadata(title_id); + + out.push_back({.enabled = !external_dlc_disabled, + .name = target_metadata.name, + .version = target_metadata.version, + .type = PatchType::DLC, + .program_id = title_id, + .title_id = title_id}); + } + return out; } std::optional<u32> PatchManager::GetGameVersion() const { + // Prioritize external update version + const auto external_update_disabled = IsAddOnDisabled("Update (File):", true); + if (!external_update_disabled && external_manager && + external_manager->HasExternalUpdate(title_id)) { + const auto metadata = external_manager->GetExternalUpdateMetadata(title_id); + if (metadata.version != "") { + return std::stoul(metadata.version); + } + } + + // Then check NAND installed version const auto update_tid = GetUpdateTitleID(title_id); if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) { return content_provider.GetEntryVersion(update_tid); @@ -693,4 +799,79 @@ PatchManager::Metadata PatchManager::ParseControlNCA(const NCA& nca) const { return {std::move(nacp), icon_file}; } -} // namespace FileSys + +bool PatchManager::IsAddOnDisabled(const std::string& feature_name, bool check_prefix) const { + const auto it = Settings::values.disabled_addons.find(title_id); + if (it == Settings::values.disabled_addons.end()) { + return false; + } + + const auto& disabled = it->second; + + if (check_prefix) { + return std::find_if(disabled.cbegin(), disabled.cend(), + [&feature_name](const std::string& name) { + return name.starts_with(feature_name); + }) != disabled.cend(); + } + + return std::find(disabled.cbegin(), disabled.cend(), feature_name) != disabled.cend(); +} + +std::shared_ptr<NCA> PatchManager::GetNCAfromExternalFile(VirtualFile update_file, + ContentRecordType type, + const NCA* base_nca) const { + if (!update_file || !base_nca) { + return nullptr; + } + + try { + auto nsp = std::make_shared<NSP>(update_file); + if (nsp->GetStatus() != Loader::ResultStatus::Success) { + LOG_WARNING(Loader, "Failed to parse NSP file"); + return nullptr; + } + + const auto update_id = title_id | 0x800; + auto nca = nsp->GetNCA(update_id, type, TitleType::Update); + + if (!nca || nca->GetStatus() != Loader::ResultStatus::Success) { + LOG_WARNING(Loader, "Direct NCA lookup failed, searching all NCAs for type match"); + + for (const auto& candidate : nsp->GetNCAsCollapsed()) { + bool type_matches = false; + switch (candidate->GetType()) { + case NCAContentType::Program: + type_matches = (type == ContentRecordType::Program); + break; + case NCAContentType::Control: + type_matches = (type == ContentRecordType::Control); + break; + case NCAContentType::Data: + type_matches = (type == ContentRecordType::Data); + break; + default: + type_matches = false; + break; + } + + if (type_matches) { + nca = candidate; + break; + } + } + } + + if (nca && nca->GetStatus() == Loader::ResultStatus::Success) { + return std::make_shared<NCA>(nca->GetBaseFile(), base_nca); + } + + LOG_WARNING(Loader, "No suitable NCA found in external file"); + } catch (const std::exception& e) { + LOG_ERROR(Loader, "Error processing external file: {}", e.what()); + } + + return nullptr; +} + +} // namespace FileSys \ No newline at end of file diff --git a/src/core/file_sys/patch_manager.h b/src/core/file_sys/patch_manager.h index 552c0fbe23..0b53d835a6 100644 --- a/src/core/file_sys/patch_manager.h +++ b/src/core/file_sys/patch_manager.h @@ -1,6 +1,9 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + #pragma once #include <map> @@ -21,7 +24,7 @@ class FileSystemController; } namespace FileSys { - +class ExternalContentManager; class ContentProvider; class NCA; class NACP; @@ -76,6 +79,37 @@ public: VirtualFile packed_update_raw = nullptr, bool apply_layeredfs = true) const; + /** + * Applies an external update file to patch a game's RomFS. + * @param base_nca The base NCA of the content being patched + * @param base_romfs The original RomFS to be patched + * @param external_update The external update file (NSP/XCI) containing the update + * @param type The type of content being patched (Program, Control, etc.) + * @return Patched RomFS VirtualFile if successful, nullptr otherwise + */ + VirtualFile PatchRomFSWithExternal(const NCA *base_nca, VirtualFile base_romfs, + VirtualFile external_update, ContentRecordType type) const; + + /** + * Applies an external update file to patch a game's ExeFS. + * @param exefs The original ExeFS to be patched + * @param base_nca The base NCA of the content being patched + * @param external_update The external update file (NSP/XCI) containing the update + * @return Patched ExeFS VirtualDir if successful, nullptr otherwise + */ + VirtualDir PatchExeFSWithExternal(VirtualDir exefs, const NCA *base_nca, VirtualFile external_update) const; + + + /** + * Extract an NCA of the specified type from an external update file. + * @param update_file The external update file (NSP/XCI) + * @param type The type of NCA to extract (Program, Control, etc.) + * @param base_nca The base NCA to use as a template for the update NCA + * @return Shared pointer to the extracted and templated NCA if successful, nullptr otherwise + */ + std::shared_ptr<NCA> GetNCAfromExternalFile(VirtualFile update_file, ContentRecordType type, + const NCA *base_nca) const; + // Returns a vector of patches [[nodiscard]] std::vector<Patch> GetPatches(VirtualFile update_raw = nullptr) const; @@ -95,9 +129,18 @@ private: [[nodiscard]] std::vector<VirtualFile> CollectPatches(const std::vector<VirtualDir>& patch_dirs, const std::string& build_id) const; + /** + * Checks if a specific feature is disabled in user settings for this title. + * @param feature_name The string name of the addon to check against + * @param check_prefix If true, checks if the addon name starts with the given string else it's a literal check + */ + bool IsAddOnDisabled(const std::string &feature_name, bool check_prefix) const; + + u64 title_id; const Service::FileSystem::FileSystemController& fs_controller; const ContentProvider& content_provider; + std::shared_ptr<ExternalContentManager> external_manager; }; } // namespace FileSys diff --git a/src/core/hle/service/am/process_creation.cpp b/src/core/hle/service/am/process_creation.cpp index aaa03c4c39..7154c3f116 100644 --- a/src/core/hle/service/am/process_creation.cpp +++ b/src/core/hle/service/am/process_creation.cpp @@ -1,6 +1,9 @@ // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + #include "core/core.h" #include "core/file_sys/content_archive.h" #include "core/file_sys/nca_metadata.h" @@ -8,6 +11,7 @@ #include "core/file_sys/registered_cache.h" #include "core/file_sys/romfs_factory.h" #include "core/hle/service/am/process_creation.h" +#include "core/file_sys/external_content_manager.h" #include "core/hle/service/glue/glue_manager.h" #include "core/hle/service/os/process.h" #include "core/loader/loader.h" @@ -97,18 +101,32 @@ std::unique_ptr<Process> CreateApplicationProcess(std::vector<u8>& out_control, } FileSys::NACP nacp; - if (out_loader->ReadControlData(nacp) == Loader::ResultStatus::Success) { - out_control = nacp.GetRawBytes(); - } else { - out_control.resize(sizeof(FileSys::RawNACP)); - std::fill(out_control.begin(), out_control.end(), (u8) 0); + bool nacp_loaded = false; + auto& storage = system.GetContentProviderUnion(); + FileSys::PatchManager pm{program_id, system.GetFileSystemController(), storage}; + + + const auto metadata = pm.GetControlMetadata(); + if (metadata.first) { + out_control = metadata.first->GetRawBytes(); + nacp_loaded = true; + } + + // Fall back to the loader's NACP if no metadata was found through PatchManager + if (!nacp_loaded && out_loader->ReadControlData(nacp) == Loader::ResultStatus::Success) { + out_control = nacp.GetRawBytes(); + nacp_loaded = true; + } + + if (!nacp_loaded) { + LOG_WARNING(Service_AM, "Failed to load NACP data, filling with zeros."); + out_control.resize(sizeof(FileSys::RawNACP)); + std::fill(out_control.begin(), out_control.end(), u8{0}); } - auto& storage = system.GetContentProviderUnion(); Service::Glue::ApplicationLaunchProperty launch{}; launch.title_id = process->GetProgramId(); - FileSys::PatchManager pm{launch.title_id, system.GetFileSystemController(), storage}; launch.version = pm.GetGameVersion().value_or(0); // TODO(DarkLordZach): When FSController/Game Card Support is added, if diff --git a/src/core/hle/service/aoc/addon_content_manager.cpp b/src/core/hle/service/aoc/addon_content_manager.cpp index d47f57d645..cfd288e600 100644 --- a/src/core/hle/service/aoc/addon_content_manager.cpp +++ b/src/core/hle/service/aoc/addon_content_manager.cpp @@ -1,6 +1,10 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + + #include <algorithm> #include <numeric> #include <vector> @@ -16,6 +20,7 @@ #include "core/file_sys/registered_cache.h" #include "core/hle/kernel/k_event.h" #include "core/hle/service/aoc/addon_content_manager.h" +#include "core/file_sys/external_content_manager.h" #include "core/hle/service/aoc/purchase_event_manager.h" #include "core/hle/service/cmif_serialization.h" #include "core/hle/service/ipc_helpers.h" @@ -30,6 +35,8 @@ static bool CheckAOCTitleIDMatchesBase(u64 title_id, u64 base) { static std::vector<u64> AccumulateAOCTitleIDs(Core::System& system) { std::vector<u64> add_on_content; + + // Nand DLC const auto& rcu = system.GetContentProvider(); const auto list = rcu.ListEntriesFilter(FileSys::TitleType::AOC, FileSys::ContentRecordType::Data); @@ -43,6 +50,25 @@ static std::vector<u64> AccumulateAOCTitleIDs(Core::System& system) { Loader::ResultStatus::Success; }), add_on_content.end()); + + // External DLC + if (auto external_manager = FileSys::GetExternalContentManager()) { + const auto current_app_id = system.GetApplicationProcessProgramID(); + const auto base_id = FileSys::GetBaseTitleID(current_app_id); + + const auto& disabled = Settings::values.disabled_addons[base_id]; + const bool external_dlc_disabled = std::find_if(disabled.cbegin(), disabled.cend(), + [](const std::string& name) { return name.starts_with("DLC (File):"); }) != disabled.cend(); + + if (!external_dlc_disabled) { + for (const auto& [dlc_id, mapped_base_id] : external_manager->GetExternalDLCMapping()) { + if (mapped_base_id == base_id) { + add_on_content.push_back(dlc_id); + } + } + } + } + return add_on_content; } @@ -91,7 +117,8 @@ Result IAddOnContentManager::CountAddOnContent(Out<u32> out_count, ClientProcess const auto current = system.GetApplicationProcessProgramID(); const auto& disabled = Settings::values.disabled_addons[current]; - if (std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end()) { + if (std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end() || + std::find(disabled.begin(), disabled.end(), "DLC (File):") != disabled.end()) { *out_count = 0; R_SUCCEED(); } @@ -113,7 +140,8 @@ Result IAddOnContentManager::ListAddOnContent(Out<u32> out_count, std::vector<u32> out; const auto& disabled = Settings::values.disabled_addons[current]; - if (std::find(disabled.begin(), disabled.end(), "DLC") == disabled.end()) { + if (std::find(disabled.begin(), disabled.end(), "DLC") == disabled.end() || + std::find(disabled.begin(), disabled.end(), "DLC (File):") == disabled.end()) { for (u64 content_id : add_on_content) { if (FileSys::GetBaseTitleID(content_id) != current) { continue; diff --git a/src/core/hle/service/filesystem/fsp/fsp_srv.cpp b/src/core/hle/service/filesystem/fsp/fsp_srv.cpp index 005caf6562..6b4e2ff904 100644 --- a/src/core/hle/service/filesystem/fsp/fsp_srv.cpp +++ b/src/core/hle/service/filesystem/fsp/fsp_srv.cpp @@ -1,6 +1,9 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + #include <cinttypes> #include <cstring> #include <iterator> @@ -34,6 +37,8 @@ #include "core/hle/service/filesystem/fsp/fs_i_save_data_info_reader.h" #include "core/hle/service/filesystem/fsp/fs_i_storage.h" #include "core/hle/service/filesystem/fsp/fsp_srv.h" + +#include "core/file_sys/external_content_manager.h" #include "core/hle/service/filesystem/fsp/save_data_transfer_prohibiter.h" #include "core/hle/service/filesystem/romfs_controller.h" #include "core/hle/service/filesystem/save_data_controller.h" @@ -41,6 +46,8 @@ #include "core/hle/service/ipc_helpers.h" #include "core/loader/loader.h" #include "core/reporter.h" +#include "core/file_sys/common_funcs.h" +#include "core/file_sys/submission_package.h" namespace Service::FileSystem { @@ -426,6 +433,10 @@ Result FSP_SRV::OpenDataStorageByDataId(OutInterface<IStorage> out_interface, LOG_DEBUG(Service_FS, "called with storage_id={:02X}, unknown={:08X}, title_id={:016X}", storage_id, unknown, title_id); + if (TryOpenExternalDLC(out_interface, title_id)) { + R_SUCCEED(); + } + auto data = romfs_controller->OpenRomFS(title_id, storage_id, FileSys::ContentRecordType::Data); if (!data) { @@ -454,6 +465,54 @@ Result FSP_SRV::OpenDataStorageByDataId(OutInterface<IStorage> out_interface, R_SUCCEED(); } +bool FSP_SRV::TryOpenExternalDLC(OutInterface<IStorage>& out_interface, u64 title_id) const { + auto external_manager = FileSys::GetExternalContentManager(); + if (!external_manager) { + return false; + } + + const auto base_title_id = FileSys::GetBaseTitleID(title_id); + if (!external_manager->HasExternalDLC(base_title_id)) { + return false; + } + + auto dlc_file = external_manager->GetExternalDLCFileByID(title_id); + if (!dlc_file) { + return false; + } + + const auto nsp = std::make_shared<FileSys::NSP>(dlc_file); + if (nsp->GetStatus() != Loader::ResultStatus::Success) { + LOG_ERROR(Service_FS, "NSP creation failed for DLC title_id={:016X}", title_id); + return false; + } + + std::shared_ptr<FileSys::NCA> data_nca = nullptr; + + for (const auto& nca : nsp->GetNCAsCollapsed()) { + if (nca->GetType() == FileSys::NCAContentType::Data || + nca->GetType() == FileSys::NCAContentType::PublicData) { + data_nca = nca; + break; + } + } + + if (!data_nca || data_nca->GetStatus() != Loader::ResultStatus::Success) { + LOG_ERROR(Service_FS, "Could not find Data NCA in NSP for title_id={:016X}", title_id); + return false; + } + + auto dlc_romfs = data_nca->GetRomFS(); + if (!dlc_romfs) { + LOG_ERROR(Service_FS, "Data NCA exists but has no RomFS for title_id={:016X}", title_id); + return false; + } + + *out_interface = std::make_shared<IStorage>(system, dlc_romfs); + LOG_INFO(Service_FS, "Successfully opened external DLC for title_id={:016X}", title_id); + return true; +} + Result FSP_SRV::OpenPatchDataStorageByCurrentProcess(OutInterface<IStorage> out_interface, FileSys::StorageId storage_id, u64 title_id) { LOG_WARNING(Service_FS, "(STUBBED) called with storage_id={:02X}, title_id={:016X}", storage_id, diff --git a/src/core/hle/service/filesystem/fsp/fsp_srv.h b/src/core/hle/service/filesystem/fsp/fsp_srv.h index cd9d66b7ba..b1cfb7d867 100644 --- a/src/core/hle/service/filesystem/fsp/fsp_srv.h +++ b/src/core/hle/service/filesystem/fsp/fsp_srv.h @@ -1,6 +1,9 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + #pragma once #include <memory> @@ -112,6 +115,8 @@ private: const FileSys::ContentProvider& content_provider; const Core::Reporter& reporter; + bool TryOpenExternalDLC(OutInterface<IStorage>& out_interface, u64 title_id) const; + FileSys::VirtualFile romfs; u64 current_process_id = 0; u32 access_log_program_index = 0;