core(WIP): Add support for adding external update/DLC files without NAND installation
This commit is contained in:
parent
4596295b51
commit
79ed4f6c2e
10 changed files with 958 additions and 40 deletions
|
@ -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
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
387
src/core/file_sys/external_content_manager.cpp
Normal file
387
src/core/file_sys/external_content_manager.cpp
Normal 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
|
191
src/core/file_sys/external_content_manager.h
Normal file
191
src/core/file_sys/external_content_manager.h
Normal 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
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue