core(WIP): Add support for adding external update/DLC files without NAND installation

This commit is contained in:
Briar 2025-04-22 22:59:59 +02:00
parent 4596295b51
commit 79ed4f6c2e
10 changed files with 958 additions and 40 deletions

View file

@ -1,6 +1,9 @@
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project # SPDX-FileCopyrightText: 2018 yuzu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later # 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 add_library(core STATIC
arm/arm_interface.cpp arm/arm_interface.cpp
arm/arm_interface.h arm/arm_interface.h
@ -175,6 +178,8 @@ add_library(core STATIC
file_sys/vfs/vfs_vector.h file_sys/vfs/vfs_vector.h
file_sys/xts_archive.cpp file_sys/xts_archive.cpp
file_sys/xts_archive.h file_sys/xts_archive.h
file_sys/external_content_manager.cpp
file_sys/external_content_manager.h
frontend/applets/cabinet.cpp frontend/applets/cabinet.cpp
frontend/applets/cabinet.h frontend/applets/cabinet.h
frontend/applets/controller.cpp frontend/applets/controller.cpp

View file

@ -1,6 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // 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 <algorithm>
#include <cstring> #include <cstring>
#include <optional> #include <optional>
@ -117,9 +120,7 @@ NCA::NCA(VirtualFile file_, const NCA* base_nca)
} }
if (is_update && base_nca == nullptr) { if (is_update && base_nca == nullptr) {
status = Loader::ResultStatus::ErrorMissingBKTRBaseRomFS; // Ignore this for now to allow external addOns; it shouldn't affect anything
} else {
status = Loader::ResultStatus::Success;
} }
} }

View file

@ -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

View file

@ -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

View file

@ -1,6 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // 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 <algorithm>
#include <array> #include <array>
#include <cstddef> #include <cstddef>
@ -30,6 +33,7 @@
#include "core/loader/loader.h" #include "core/loader/loader.h"
#include "core/loader/nso.h" #include "core/loader/nso.h"
#include "core/memory/cheat_engine.h" #include "core/memory/cheat_engine.h"
#include "core/file_sys/external_content_manager.h"
namespace FileSys { namespace FileSys {
namespace { namespace {
@ -117,9 +121,10 @@ bool IsDirValidAndNonEmpty(const VirtualDir& dir) {
} // Anonymous namespace } // Anonymous namespace
PatchManager::PatchManager(u64 title_id_, PatchManager::PatchManager(u64 title_id_,
const Service::FileSystem::FileSystemController& fs_controller_, const Service::FileSystem::FileSystemController& fs_controller_,
const ContentProvider& content_provider_) const ContentProvider& content_provider_)
: title_id{title_id_}, fs_controller{fs_controller_}, content_provider{content_provider_} {} : title_id{title_id_}, fs_controller{fs_controller_}, content_provider{content_provider_},
external_manager{GetExternalContentManager()} {}
PatchManager::~PatchManager() = default; PatchManager::~PatchManager() = default;
@ -128,30 +133,43 @@ u64 PatchManager::GetTitleID() const {
} }
VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const { VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
LOG_INFO(Loader, "Patching ExeFS for title_id={:016X}", title_id); if (!exefs)
if (exefs == nullptr)
return exefs; return exefs;
const auto& disabled = Settings::values.disabled_addons[title_id]; // Retrieve base Program NCA
const auto update_disabled = const auto base_program_nca = content_provider.GetEntry(title_id, ContentRecordType::Program);
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); 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_tid = GetUpdateTitleID(title_id);
const auto update = content_provider.GetEntry(update_tid, ContentRecordType::Program); const auto update = content_provider.GetEntry(update_tid, ContentRecordType::Program);
if (!external_update_applied && !update_disabled && update && update->GetExeFS()) {
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(); exefs = update->GetExeFS();
} }
// LayeredExeFS
const auto load_dir = fs_controller.GetModificationLoadRoot(title_id); const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
const auto sdmc_load_dir = fs_controller.GetSDMCModificationLoadRoot(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) { if (load_dir != nullptr) {
const auto load_patch_dirs = load_dir->GetSubdirectories(); const auto load_patch_dirs = load_dir->GetSubdirectories();
patch_dirs.insert(patch_dirs.end(), load_patch_dirs.begin(), load_patch_dirs.end()); 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(), 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) { return l->GetName() < r->GetName(); });
std::vector<VirtualDir> layers; const auto& disabled = Settings::values.disabled_addons[title_id];
layers.reserve(patch_dirs.size() + 1);
for (const auto& subdir : patch_dirs) { for (const auto& subdir : patch_dirs) {
if (std::find(disabled.begin(), disabled.end(), subdir->GetName()) != disabled.end()) if (std::find(disabled.begin(), disabled.end(), subdir->GetName()) != disabled.end())
continue; continue;
@ -190,6 +207,23 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
return exefs; 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, std::vector<VirtualFile> PatchManager::CollectPatches(const std::vector<VirtualDir>& patch_dirs,
const std::string& build_id) const { const std::string& build_id) const {
const auto& disabled = Settings::values.disabled_addons[title_id]; 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; 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 // Game Updates
const auto update_tid = GetUpdateTitleID(title_id); const auto update_tid = GetUpdateTitleID(title_id);
const auto update_raw = content_provider.GetEntryRaw(update_tid, type); const auto update_raw = content_provider.GetEntryRaw(update_tid, type);
const auto& disabled = Settings::values.disabled_addons[title_id]; if (!external_update_applied && !update_disabled && update_raw && base_nca) {
const auto update_disabled =
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
if (!update_disabled && update_raw != nullptr && base_nca != nullptr) {
const auto new_nca = std::make_shared<NCA>(update_raw, base_nca); const auto new_nca = std::make_shared<NCA>(update_raw, base_nca);
if (new_nca->GetStatus() == Loader::ResultStatus::Success && if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
new_nca->GetRomFS() != nullptr) { new_nca->GetRomFS() != nullptr) {
@ -449,7 +500,7 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
const auto version = const auto version =
FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0)); 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); const auto new_nca = std::make_shared<NCA>(packed_update_raw, base_nca);
if (new_nca->GetStatus() == Loader::ResultStatus::Success && if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
new_nca->GetRomFS() != nullptr) { new_nca->GetRomFS() != nullptr) {
@ -466,6 +517,23 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
return 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 { std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
if (title_id == 0) { if (title_id == 0) {
return {}; return {};
@ -480,8 +548,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
const auto metadata = update.GetControlMetadata(); const auto metadata = update.GetControlMetadata();
const auto& nacp = metadata.first; const auto& nacp = metadata.first;
const auto update_disabled = const auto update_disabled = IsAddOnDisabled("Update", false);
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
Patch update_patch = {.enabled = !update_disabled, Patch update_patch = {.enabled = !update_disabled,
.name = "Update", .name = "Update",
.version = "", .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) // General Mods (LayeredFS and IPS)
const auto mod_dir = fs_controller.GetModificationLoadRoot(title_id); const auto mod_dir = fs_controller.GetModificationLoadRoot(title_id);
if (mod_dir != nullptr) { if (mod_dir != nullptr) {
@ -610,10 +692,34 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
.title_id = dlc_match.back().title_id}); .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; return out;
} }
std::optional<u32> PatchManager::GetGameVersion() const { 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); const auto update_tid = GetUpdateTitleID(title_id);
if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) { if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) {
return content_provider.GetEntryVersion(update_tid); return content_provider.GetEntryVersion(update_tid);
@ -693,4 +799,79 @@ PatchManager::Metadata PatchManager::ParseControlNCA(const NCA& nca) const {
return {std::move(nacp), icon_file}; 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

View file

@ -1,6 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // 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 #pragma once
#include <map> #include <map>
@ -21,7 +24,7 @@ class FileSystemController;
} }
namespace FileSys { namespace FileSys {
class ExternalContentManager;
class ContentProvider; class ContentProvider;
class NCA; class NCA;
class NACP; class NACP;
@ -76,6 +79,37 @@ public:
VirtualFile packed_update_raw = nullptr, VirtualFile packed_update_raw = nullptr,
bool apply_layeredfs = true) const; 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 // Returns a vector of patches
[[nodiscard]] std::vector<Patch> GetPatches(VirtualFile update_raw = nullptr) const; [[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, [[nodiscard]] std::vector<VirtualFile> CollectPatches(const std::vector<VirtualDir>& patch_dirs,
const std::string& build_id) const; 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; u64 title_id;
const Service::FileSystem::FileSystemController& fs_controller; const Service::FileSystem::FileSystemController& fs_controller;
const ContentProvider& content_provider; const ContentProvider& content_provider;
std::shared_ptr<ExternalContentManager> external_manager;
}; };
} // namespace FileSys } // namespace FileSys

View file

@ -1,6 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // 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/core.h"
#include "core/file_sys/content_archive.h" #include "core/file_sys/content_archive.h"
#include "core/file_sys/nca_metadata.h" #include "core/file_sys/nca_metadata.h"
@ -8,6 +11,7 @@
#include "core/file_sys/registered_cache.h" #include "core/file_sys/registered_cache.h"
#include "core/file_sys/romfs_factory.h" #include "core/file_sys/romfs_factory.h"
#include "core/hle/service/am/process_creation.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/glue/glue_manager.h"
#include "core/hle/service/os/process.h" #include "core/hle/service/os/process.h"
#include "core/loader/loader.h" #include "core/loader/loader.h"
@ -97,18 +101,32 @@ std::unique_ptr<Process> CreateApplicationProcess(std::vector<u8>& out_control,
} }
FileSys::NACP nacp; FileSys::NACP nacp;
if (out_loader->ReadControlData(nacp) == Loader::ResultStatus::Success) { bool nacp_loaded = false;
out_control = nacp.GetRawBytes(); auto& storage = system.GetContentProviderUnion();
} else { FileSys::PatchManager pm{program_id, system.GetFileSystemController(), storage};
out_control.resize(sizeof(FileSys::RawNACP));
std::fill(out_control.begin(), out_control.end(), (u8) 0);
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{}; Service::Glue::ApplicationLaunchProperty launch{};
launch.title_id = process->GetProgramId(); launch.title_id = process->GetProgramId();
FileSys::PatchManager pm{launch.title_id, system.GetFileSystemController(), storage};
launch.version = pm.GetGameVersion().value_or(0); launch.version = pm.GetGameVersion().value_or(0);
// TODO(DarkLordZach): When FSController/Game Card Support is added, if // TODO(DarkLordZach): When FSController/Game Card Support is added, if

View file

@ -1,6 +1,10 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // 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 <algorithm>
#include <numeric> #include <numeric>
#include <vector> #include <vector>
@ -16,6 +20,7 @@
#include "core/file_sys/registered_cache.h" #include "core/file_sys/registered_cache.h"
#include "core/hle/kernel/k_event.h" #include "core/hle/kernel/k_event.h"
#include "core/hle/service/aoc/addon_content_manager.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/aoc/purchase_event_manager.h"
#include "core/hle/service/cmif_serialization.h" #include "core/hle/service/cmif_serialization.h"
#include "core/hle/service/ipc_helpers.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) { static std::vector<u64> AccumulateAOCTitleIDs(Core::System& system) {
std::vector<u64> add_on_content; std::vector<u64> add_on_content;
// Nand DLC
const auto& rcu = system.GetContentProvider(); const auto& rcu = system.GetContentProvider();
const auto list = const auto list =
rcu.ListEntriesFilter(FileSys::TitleType::AOC, FileSys::ContentRecordType::Data); rcu.ListEntriesFilter(FileSys::TitleType::AOC, FileSys::ContentRecordType::Data);
@ -43,6 +50,25 @@ static std::vector<u64> AccumulateAOCTitleIDs(Core::System& system) {
Loader::ResultStatus::Success; Loader::ResultStatus::Success;
}), }),
add_on_content.end()); 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; return add_on_content;
} }
@ -91,7 +117,8 @@ Result IAddOnContentManager::CountAddOnContent(Out<u32> out_count, ClientProcess
const auto current = system.GetApplicationProcessProgramID(); const auto current = system.GetApplicationProcessProgramID();
const auto& disabled = Settings::values.disabled_addons[current]; 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; *out_count = 0;
R_SUCCEED(); R_SUCCEED();
} }
@ -113,7 +140,8 @@ Result IAddOnContentManager::ListAddOnContent(Out<u32> out_count,
std::vector<u32> out; std::vector<u32> out;
const auto& disabled = Settings::values.disabled_addons[current]; 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) { for (u64 content_id : add_on_content) {
if (FileSys::GetBaseTitleID(content_id) != current) { if (FileSys::GetBaseTitleID(content_id) != current) {
continue; continue;

View file

@ -1,6 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // 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 <cinttypes>
#include <cstring> #include <cstring>
#include <iterator> #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_save_data_info_reader.h"
#include "core/hle/service/filesystem/fsp/fs_i_storage.h" #include "core/hle/service/filesystem/fsp/fs_i_storage.h"
#include "core/hle/service/filesystem/fsp/fsp_srv.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/fsp/save_data_transfer_prohibiter.h"
#include "core/hle/service/filesystem/romfs_controller.h" #include "core/hle/service/filesystem/romfs_controller.h"
#include "core/hle/service/filesystem/save_data_controller.h" #include "core/hle/service/filesystem/save_data_controller.h"
@ -41,6 +46,8 @@
#include "core/hle/service/ipc_helpers.h" #include "core/hle/service/ipc_helpers.h"
#include "core/loader/loader.h" #include "core/loader/loader.h"
#include "core/reporter.h" #include "core/reporter.h"
#include "core/file_sys/common_funcs.h"
#include "core/file_sys/submission_package.h"
namespace Service::FileSystem { 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}", LOG_DEBUG(Service_FS, "called with storage_id={:02X}, unknown={:08X}, title_id={:016X}",
storage_id, unknown, title_id); 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); auto data = romfs_controller->OpenRomFS(title_id, storage_id, FileSys::ContentRecordType::Data);
if (!data) { if (!data) {
@ -454,6 +465,54 @@ Result FSP_SRV::OpenDataStorageByDataId(OutInterface<IStorage> out_interface,
R_SUCCEED(); 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, Result FSP_SRV::OpenPatchDataStorageByCurrentProcess(OutInterface<IStorage> out_interface,
FileSys::StorageId storage_id, u64 title_id) { FileSys::StorageId storage_id, u64 title_id) {
LOG_WARNING(Service_FS, "(STUBBED) called with storage_id={:02X}, title_id={:016X}", storage_id, LOG_WARNING(Service_FS, "(STUBBED) called with storage_id={:02X}, title_id={:016X}", storage_id,

View file

@ -1,6 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // 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 #pragma once
#include <memory> #include <memory>
@ -112,6 +115,8 @@ private:
const FileSys::ContentProvider& content_provider; const FileSys::ContentProvider& content_provider;
const Core::Reporter& reporter; const Core::Reporter& reporter;
bool TryOpenExternalDLC(OutInterface<IStorage>& out_interface, u64 title_id) const;
FileSys::VirtualFile romfs; FileSys::VirtualFile romfs;
u64 current_process_id = 0; u64 current_process_id = 0;
u32 access_log_program_index = 0; u32 access_log_program_index = 0;