aboutsummaryrefslogtreecommitdiff
path: root/src/core
diff options
context:
space:
mode:
authorxbzk <xbzk@eden-emu.dev>2026-03-04 22:51:35 +0100
committercrueter <crueter@eden-emu.dev>2026-03-04 22:51:35 +0100
commit7f5de6bcd651f8145e4746c76d0531b6560fd5e4 (patch)
treecaefce3d338f65309db1725ac84257c99c14784e /src/core
parentc682306788055c57953aea38fdded95a0c6dfc09 (diff)
[android/fs] external content loader + nca/xci patches (#3596)
Foreword: WHY DON'T EVERYBODY USE ONE FOLDER FOR EACH GAME+CONTENTS? AIN'T THIS THE FORMAT GAMES COME WHEN YOU BUE THEM? DO YOU LIVE WITH ALL YOUR FRIENDS AND HAVE A 2ND HOUSE FOR ALL THE CHILDREN? Nice, i feel better now. This feat extends Maufeat's work on external content loading. It harmonically additions: "...also, if in each game folder X, you find a folder Y, and in this folder Y you detect ONLY a single game, then mount all external content for that game found in that folder Y and its subfolders." Permanent (not toggleable). External Content folders are supported equally. Also: -Reworked several routines for preserving single source of truth between android and other systems; -Fixed the annoying unknown format error for content files, by providing proper format detection. Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3596 Reviewed-by: MaranBr <maranbr@eden-emu.dev> Reviewed-by: Lizzie <lizzie@eden-emu.dev> Co-authored-by: xbzk <xbzk@eden-emu.dev> Co-committed-by: xbzk <xbzk@eden-emu.dev>
Diffstat (limited to 'src/core')
-rw-r--r--src/core/file_sys/patch_manager.cpp27
-rw-r--r--src/core/file_sys/registered_cache.cpp473
-rw-r--r--src/core/file_sys/registered_cache.h5
-rw-r--r--src/core/loader/loader.cpp70
-rw-r--r--src/core/loader/loader.h22
-rw-r--r--src/core/loader/nsp.cpp36
-rw-r--r--src/core/loader/xci.cpp14
7 files changed, 368 insertions, 279 deletions
diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp
index 82944ceceb..e9c3bb75c2 100644
--- a/src/core/file_sys/patch_manager.cpp
+++ b/src/core/file_sys/patch_manager.cpp
@@ -117,6 +117,12 @@ void AppendCommaIfNotEmpty(std::string& to, std::string_view with) {
bool IsDirValidAndNonEmpty(const VirtualDir& dir) {
return dir != nullptr && (!dir->GetFiles().empty() || !dir->GetSubdirectories().empty());
}
+
+bool IsVersionedExternalUpdateDisabled(const std::vector<std::string>& disabled, u32 version) {
+ const std::string disabled_key = fmt::format("Update@{}", version);
+ return std::find(disabled.cbegin(), disabled.cend(), disabled_key) != disabled.cend() ||
+ std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
+}
} // Anonymous namespace
PatchManager::PatchManager(u64 title_id_,
@@ -155,8 +161,7 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
if (!update_versions.empty()) {
checked_external = true;
for (const auto& update_entry : update_versions) {
- std::string disabled_key = fmt::format("Update@{}", update_entry.version);
- if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
+ if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
update_disabled = false;
enabled_version = update_entry.version;
break;
@@ -175,8 +180,7 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
if (!manual_update_versions.empty()) {
checked_manual = true;
for (const auto& update_entry : manual_update_versions) {
- std::string disabled_key = fmt::format("Update@{}", update_entry.version);
- if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
+ if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
update_disabled = false;
enabled_version = update_entry.version;
break;
@@ -580,8 +584,7 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
if (!update_versions.empty()) {
checked_external = true;
for (const auto& update_entry : update_versions) {
- std::string disabled_key = fmt::format("Update@{}", update_entry.version);
- if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
+ if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
update_disabled = false;
enabled_version = update_entry.version;
update_raw = external_provider->GetEntryForVersion(update_tid, type, update_entry.version);
@@ -600,8 +603,7 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
if (!manual_update_versions.empty()) {
checked_manual = true;
for (const auto& update_entry : manual_update_versions) {
- std::string disabled_key = fmt::format("Update@{}", update_entry.version);
- if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
+ if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
update_disabled = false;
enabled_version = update_entry.version;
update_raw = manual_provider->GetEntryForVersion(update_tid, type, update_entry.version);
@@ -704,9 +706,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
version_str = FormatTitleVersion(update_entry.version);
}
- std::string disabled_key = fmt::format("Update@{}", update_entry.version);
const auto update_disabled =
- std::find(disabled.cbegin(), disabled.cend(), disabled_key) != disabled.cend();
+ IsVersionedExternalUpdateDisabled(disabled, update_entry.version);
Patch update_patch = {.enabled = !update_disabled,
.name = "Update",
@@ -732,9 +733,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
version_str = FormatTitleVersion(update_entry.version);
}
- std::string disabled_key = fmt::format("Update@{}", update_entry.version);
const auto update_disabled =
- std::find(disabled.cbegin(), disabled.cend(), disabled_key) != disabled.cend();
+ IsVersionedExternalUpdateDisabled(disabled, update_entry.version);
Patch update_patch = {.enabled = !update_disabled,
@@ -771,7 +771,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
std::nullopt, std::nullopt, ContentRecordType::Program, update_tid);
for (const auto& [slot, entry] : all_updates) {
- if (slot == ContentProviderUnionSlot::External) {
+ if (slot == ContentProviderUnionSlot::External ||
+ slot == ContentProviderUnionSlot::FrontendManual) {
continue;
}
diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp
index 7bf2ad8fcd..020d403c95 100644
--- a/src/core/file_sys/registered_cache.cpp
+++ b/src/core/file_sys/registered_cache.cpp
@@ -104,6 +104,206 @@ static std::string GetCNMTName(TitleType type, u64 title_id) {
return fmt::format("{}_{:016x}.cnmt", TITLE_TYPE_NAMES[index], title_id);
}
+static std::shared_ptr<NSP> OpenContainerAsNsp(const VirtualFile& file, Loader::FileType type) {
+ if (!file) {
+ return nullptr;
+ }
+
+ if (type == Loader::FileType::Unknown || type == Loader::FileType::Error) {
+ type = Loader::IdentifyFile(file);
+ if (type == Loader::FileType::Unknown) {
+ type = Loader::GuessFromFilename(file->GetName());
+ }
+ }
+
+ if (type == Loader::FileType::NSP) {
+ auto nsp = std::make_shared<NSP>(file);
+ return nsp->GetStatus() == Loader::ResultStatus::Success ? nsp : nullptr;
+ }
+
+ if (type == Loader::FileType::XCI) {
+ XCI xci(file);
+ if (xci.GetStatus() != Loader::ResultStatus::Success) {
+ return nullptr;
+ }
+
+ auto secure_partition = xci.GetSecurePartitionNSP();
+ if (secure_partition == nullptr) {
+ return nullptr;
+ }
+
+ return secure_partition;
+ }
+
+ // SAF-backed files can occasionally fail type-guessing despite being valid NSP/XCI.
+ // As a last resort, probe both container parsers directly.
+ {
+ auto nsp = std::make_shared<NSP>(file);
+ if (nsp->GetStatus() == Loader::ResultStatus::Success) {
+ return nsp;
+ }
+ }
+ {
+ XCI xci(file);
+ if (xci.GetStatus() == Loader::ResultStatus::Success) {
+ auto secure_partition = xci.GetSecurePartitionNSP();
+ if (secure_partition != nullptr) {
+ return secure_partition;
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+template <typename Callback>
+bool ForEachContainerEntry(const std::shared_ptr<NSP>& nsp, bool only_content,
+ std::optional<u64> base_program_id, Callback&& on_entry) {
+ if (!nsp) {
+ return false;
+ }
+
+ const auto& ncas = nsp->GetNCAs();
+ if (ncas.empty()) {
+ return false;
+ }
+
+ std::map<u64, u32> versions;
+ std::map<u64, std::string> version_strings;
+
+ for (const auto& [title_id, nca_map] : ncas) {
+ for (const auto& [type_pair, nca] : nca_map) {
+ if (!nca) {
+ continue;
+ }
+
+ const auto& [title_type, content_type] = type_pair;
+
+ if (content_type == ContentRecordType::Meta) {
+ const auto subdirs = nca->GetSubdirectories();
+ if (!subdirs.empty()) {
+ for (const auto& inner_file : subdirs[0]->GetFiles()) {
+ if (inner_file->GetExtension() == "cnmt") {
+ const CNMT cnmt(inner_file);
+ versions[cnmt.GetTitleID()] = cnmt.GetTitleVersion();
+ break;
+ }
+ }
+ }
+ }
+
+ if (title_type == TitleType::Update && content_type == ContentRecordType::Control) {
+ const auto romfs = nca->GetRomFS();
+ if (!romfs) {
+ continue;
+ }
+
+ const auto extracted = ExtractRomFS(romfs);
+ if (!extracted) {
+ continue;
+ }
+
+ auto nacp_file = extracted->GetFile("control.nacp");
+ if (!nacp_file) {
+ nacp_file = extracted->GetFile("Control.nacp");
+ }
+ if (!nacp_file) {
+ continue;
+ }
+
+ const NACP nacp(nacp_file);
+ auto version_string = nacp.GetVersionString();
+ if (!version_string.empty()) {
+ version_strings[title_id] = std::move(version_string);
+ }
+ }
+ }
+ }
+
+ bool added_entries = false;
+ for (const auto& [title_id, nca_map] : ncas) {
+ if (base_program_id.has_value() && GetBaseTitleID(title_id) != *base_program_id) {
+ continue;
+ }
+
+ for (const auto& [type_pair, nca] : nca_map) {
+ const auto& [title_type, content_type] = type_pair;
+ if (only_content && title_type != TitleType::Update && title_type != TitleType::AOC) {
+ continue;
+ }
+
+ auto entry_file = nca ? nca->GetBaseFile() : nullptr;
+ if (!entry_file) {
+ continue;
+ }
+
+ u32 version = 0;
+ std::string version_string;
+
+ if (title_type == TitleType::Update) {
+ if (const auto version_it = versions.find(title_id); version_it != versions.end()) {
+ version = version_it->second;
+ }
+
+ if (const auto version_str_it = version_strings.find(title_id);
+ version_str_it != version_strings.end()) {
+ version_string = version_str_it->second;
+ }
+ }
+
+ on_entry(title_type, content_type, title_id, entry_file, version, version_string);
+ added_entries = true;
+ }
+ }
+
+ return added_entries;
+}
+
+static void UpsertExternalVersionEntry(std::vector<ExternalUpdateEntry>& multi_version_entries,
+ u64 title_id, u32 version,
+ const std::string& version_string,
+ ContentRecordType content_type, const VirtualFile& file) {
+ auto it = std::find_if(multi_version_entries.begin(), multi_version_entries.end(),
+ [title_id, version](const ExternalUpdateEntry& entry) {
+ return entry.title_id == title_id && entry.version == version;
+ });
+
+ if (it == multi_version_entries.end()) {
+ ExternalUpdateEntry update_entry;
+ update_entry.title_id = title_id;
+ update_entry.version = version;
+ update_entry.version_string = version_string;
+ update_entry.files[static_cast<std::size_t>(content_type)] = file;
+ multi_version_entries.push_back(std::move(update_entry));
+ return;
+ }
+
+ it->files[static_cast<std::size_t>(content_type)] = file;
+ if (it->version_string.empty() && !version_string.empty()) {
+ it->version_string = version_string;
+ }
+}
+
+template <typename EntryMap, typename VersionMap>
+static bool AddExternalEntriesFromContainer(const std::shared_ptr<NSP>& nsp, EntryMap& entries,
+ VersionMap& versions,
+ std::vector<ExternalUpdateEntry>& multi_version_entries) {
+ return ForEachContainerEntry(
+ nsp, true, std::nullopt,
+ [&entries, &versions,
+ &multi_version_entries](TitleType title_type, ContentRecordType content_type, u64 title_id,
+ const VirtualFile& file, u32 version,
+ const std::string& version_string) {
+ entries[{title_id, content_type, title_type}] = file;
+
+ if (title_type == TitleType::Update) {
+ versions[title_id] = version;
+ UpsertExternalVersionEntry(multi_version_entries, title_id, version, version_string,
+ content_type, file);
+ }
+ });
+}
+
ContentRecordType GetCRTypeFromNCAType(NCAContentType type) {
switch (type) {
case NCAContentType::Program:
@@ -1008,6 +1208,26 @@ void ManualContentProvider::AddEntryWithVersion(TitleType title_type, ContentRec
}
}
+bool ManualContentProvider::AddEntriesFromContainer(VirtualFile file, bool only_content,
+ std::optional<u64> base_program_id) {
+ const auto nsp = OpenContainerAsNsp(file, Loader::FileType::Unknown);
+ if (!nsp) {
+ return false;
+ }
+
+ return ForEachContainerEntry(
+ nsp, only_content, base_program_id,
+ [this](TitleType title_type, ContentRecordType content_type, u64 title_id,
+ const VirtualFile& entry_file, u32 version, const std::string& version_string) {
+ if (title_type == TitleType::Update) {
+ AddEntryWithVersion(title_type, content_type, title_id, version, version_string,
+ entry_file);
+ } else {
+ AddEntry(title_type, content_type, title_id, entry_file);
+ }
+ });
+}
+
void ManualContentProvider::ClearAllEntries() {
entries.clear();
multi_version_entries.clear();
@@ -1091,14 +1311,6 @@ VirtualFile ManualContentProvider::GetEntryForVersion(u64 title_id, ContentRecor
return nullptr;
}
-bool ManualContentProvider::HasMultipleVersions(u64 title_id, ContentRecordType type) const {
- size_t count = 0;
- for (const auto& entry : multi_version_entries)
- if (entry.title_id == title_id && entry.files[size_t(type)])
- ++count;
- return count > 0;
-}
-
ExternalContentProvider::ExternalContentProvider(std::vector<VirtualDir> load_directories)
: load_dirs(std::move(load_directories)) {
ExternalContentProvider::Refresh();
@@ -1159,247 +1371,22 @@ void ExternalContentProvider::ScanDirectory(const VirtualDir& dir) {
}
void ExternalContentProvider::ProcessNSP(const VirtualFile& file) {
- auto nsp = NSP(file);
- if (nsp.GetStatus() != Loader::ResultStatus::Success) {
+ const auto nsp = OpenContainerAsNsp(file, Loader::FileType::NSP);
+ if (!nsp) {
return;
}
LOG_DEBUG(Service_FS, "Processing NSP file: {}", file->GetName());
-
- const auto ncas = nsp.GetNCAs();
-
- std::map<u64, u32> nsp_versions;
- std::map<u64, std::string> nsp_version_strings; // title_id -> NACP version string
-
- for (const auto& [title_id, nca_map] : ncas) {
- for (const auto& [type_pair, nca] : nca_map) {
- const auto& [title_type, content_type] = type_pair;
-
- if (content_type == ContentRecordType::Meta) {
- const auto subdirs = nca->GetSubdirectories();
- if (!subdirs.empty()) {
- const auto section0 = subdirs[0];
- const auto files = section0->GetFiles();
- for (const auto& inner_file : files) {
- if (inner_file->GetExtension() == "cnmt") {
- const CNMT cnmt(inner_file);
- const auto cnmt_title_id = cnmt.GetTitleID();
- const auto version = cnmt.GetTitleVersion();
- nsp_versions[cnmt_title_id] = version;
- versions[cnmt_title_id] = version;
- break;
- }
- }
- }
- }
-
- if (content_type == ContentRecordType::Control && title_type == TitleType::Update) {
- auto romfs = nca->GetRomFS();
- if (romfs) {
- auto extracted = ExtractRomFS(romfs);
- if (extracted) {
- auto nacp_file = extracted->GetFile("control.nacp");
- if (!nacp_file) {
- nacp_file = extracted->GetFile("Control.nacp");
- }
- if (nacp_file) {
- NACP nacp(nacp_file);
- auto ver_str = nacp.GetVersionString();
- if (!ver_str.empty()) {
- nsp_version_strings[title_id] = ver_str;
- }
- }
- }
- }
- }
- }
- }
-
- std::map<std::pair<u64, u32>, std::array<VirtualFile, size_t(ContentRecordType::Count)>> version_files;
-
- for (const auto& [title_id, nca_map] : ncas) {
- for (const auto& [type_pair, nca] : nca_map) {
- const auto& [title_type, content_type] = type_pair;
-
- if (title_type != TitleType::AOC && title_type != TitleType::Update) {
- continue;
- }
-
- auto nca_file = nsp.GetNCAFile(title_id, content_type, title_type);
- if (nca_file != nullptr) {
- entries[{title_id, content_type, title_type}] = nca_file;
-
- if (title_type == TitleType::Update) {
- u32 version = 0;
- auto ver_it = nsp_versions.find(title_id);
- if (ver_it != nsp_versions.end()) {
- version = ver_it->second;
- }
-
- version_files[{title_id, version}][size_t(content_type)] = nca_file;
- }
-
- LOG_DEBUG(Service_FS, "Added entry - Title ID: {:016X}, Type: {}, Content: {}",
- title_id, static_cast<int>(title_type), static_cast<int>(content_type));
- }
- }
- }
-
- for (const auto& [key, files_map] : version_files) {
- const auto& [title_id, version] = key;
-
- std::string ver_str;
- auto str_it = nsp_version_strings.find(title_id);
- if (str_it != nsp_version_strings.end()) {
- ver_str = str_it->second;
- }
-
- bool version_exists = false;
- for (auto& existing : multi_version_entries) {
- if (existing.title_id == title_id && existing.version == version) {
- existing.files = files_map;
- if (existing.version_string.empty() && !ver_str.empty()) {
- existing.version_string = ver_str;
- }
- version_exists = true;
- break;
- }
- }
-
- if (!version_exists && !files_map.empty()) {
- ExternalUpdateEntry update_entry{
- .title_id = title_id,
- .version = version,
- .version_string = ver_str,
- .files = files_map
- };
- multi_version_entries.push_back(update_entry);
- LOG_DEBUG(Service_FS, "Added multi-version update - Title ID: {:016X}, Version: {}, VersionStr: {}, Content types: {}",
- title_id, version, ver_str, files_map.size());
- }
- }
+ AddExternalEntriesFromContainer(nsp, entries, versions, multi_version_entries);
}
void ExternalContentProvider::ProcessXCI(const VirtualFile& file) {
- auto xci = XCI(file);
- if (xci.GetStatus() != Loader::ResultStatus::Success) {
- return;
- }
-
- auto nsp = xci.GetSecurePartitionNSP();
- if (nsp == nullptr) {
+ const auto nsp = OpenContainerAsNsp(file, Loader::FileType::XCI);
+ if (!nsp) {
return;
}
- const auto ncas = nsp->GetNCAs();
-
- std::map<u64, u32> xci_versions;
- std::map<u64, std::string> xci_version_strings;
-
- for (const auto& [title_id, nca_map] : ncas) {
- for (const auto& [type_pair, nca] : nca_map) {
- const auto& [title_type, content_type] = type_pair;
-
- if (content_type == ContentRecordType::Meta) {
- const auto subdirs = nca->GetSubdirectories();
- if (!subdirs.empty()) {
- const auto section0 = subdirs[0];
- const auto files = section0->GetFiles();
- for (const auto& inner_file : files) {
- if (inner_file->GetExtension() == "cnmt") {
- const CNMT cnmt(inner_file);
- const auto cnmt_title_id = cnmt.GetTitleID();
- const auto version = cnmt.GetTitleVersion();
- xci_versions[cnmt_title_id] = version;
- versions[cnmt_title_id] = version;
- break;
- }
- }
- }
- }
-
- if (content_type == ContentRecordType::Control && title_type == TitleType::Update) {
- auto romfs = nca->GetRomFS();
- if (romfs) {
- auto extracted = ExtractRomFS(romfs);
- if (extracted) {
- auto nacp_file = extracted->GetFile("control.nacp");
- if (!nacp_file) {
- nacp_file = extracted->GetFile("Control.nacp");
- }
- if (nacp_file) {
- NACP nacp(nacp_file);
- auto ver_str = nacp.GetVersionString();
- if (!ver_str.empty()) {
- xci_version_strings[title_id] = ver_str;
- }
- }
- }
- }
- }
- }
- }
-
- std::map<std::pair<u64, u32>, std::array<VirtualFile, size_t(ContentRecordType::Count)>> version_files;
-
- for (const auto& [title_id, nca_map] : ncas) {
- for (const auto& [type_pair, nca] : nca_map) {
- const auto& [title_type, content_type] = type_pair;
-
- if (title_type != TitleType::AOC && title_type != TitleType::Update) {
- continue;
- }
-
- auto nca_file = nsp->GetNCAFile(title_id, content_type, title_type);
- if (nca_file != nullptr) {
- entries[{title_id, content_type, title_type}] = nca_file;
-
- if (title_type == TitleType::Update) {
- u32 version = 0;
- auto ver_it = xci_versions.find(title_id);
- if (ver_it != xci_versions.end()) {
- version = ver_it->second;
- }
-
- version_files[{title_id, version}][size_t(content_type)] = nca_file;
- }
- }
- }
- }
-
- for (const auto& [key, files_map] : version_files) {
- const auto& [title_id, version] = key;
-
- std::string ver_str;
- auto str_it = xci_version_strings.find(title_id);
- if (str_it != xci_version_strings.end()) {
- ver_str = str_it->second;
- }
-
- bool version_exists = false;
- for (auto& existing : multi_version_entries) {
- if (existing.title_id == title_id && existing.version == version) {
- existing.files = files_map;
- if (existing.version_string.empty() && !ver_str.empty()) {
- existing.version_string = ver_str;
- }
- version_exists = true;
- break;
- }
- }
-
- if (!version_exists && !files_map.empty()) {
- ExternalUpdateEntry update_entry{
- .title_id = title_id,
- .version = version,
- .version_string = ver_str,
- .files = files_map
- };
- multi_version_entries.push_back(update_entry);
- LOG_DEBUG(Service_FS, "Added multi-version update from XCI - Title ID: {:016X}, Version: {}, VersionStr: {}, Content types: {}",
- title_id, version, ver_str, files_map.size());
- }
- }
+ AddExternalEntriesFromContainer(nsp, entries, versions, multi_version_entries);
}
bool ExternalContentProvider::HasEntry(u64 title_id, ContentRecordType type) const {
@@ -1491,12 +1478,4 @@ VirtualFile ExternalContentProvider::GetEntryForVersion(u64 title_id, ContentRec
return nullptr;
}
-bool ExternalContentProvider::HasMultipleVersions(u64 title_id, ContentRecordType type) const {
- size_t count = 0;
- for (const auto& entry : multi_version_entries)
- if (entry.title_id == title_id && entry.files[size_t(type)])
- ++count;
- return count > 1;
-}
-
} // namespace FileSys
diff --git a/src/core/file_sys/registered_cache.h b/src/core/file_sys/registered_cache.h
index 2e39f74db8..32134d1c48 100644
--- a/src/core/file_sys/registered_cache.h
+++ b/src/core/file_sys/registered_cache.h
@@ -9,6 +9,7 @@
#include <array>
#include <functional>
#include <memory>
+#include <optional>
#include <string>
#include <vector>
#include <ankerl/unordered_dense.h>
@@ -262,6 +263,8 @@ public:
VirtualFile file);
void AddEntryWithVersion(TitleType title_type, ContentRecordType content_type, u64 title_id,
u32 version, const std::string& version_string, VirtualFile file);
+ bool AddEntriesFromContainer(VirtualFile file, bool only_content = false,
+ std::optional<u64> base_program_id = std::nullopt);
void ClearAllEntries();
void Refresh() override;
@@ -276,7 +279,6 @@ public:
std::vector<ExternalUpdateEntry> ListUpdateVersions(u64 title_id) const;
VirtualFile GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const;
- bool HasMultipleVersions(u64 title_id, ContentRecordType type) const;
private:
std::map<std::tuple<TitleType, ContentRecordType, u64>, VirtualFile> entries;
@@ -303,7 +305,6 @@ public:
std::vector<ExternalUpdateEntry> ListUpdateVersions(u64 title_id) const;
VirtualFile GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const;
- bool HasMultipleVersions(u64 title_id, ContentRecordType type) const;
private:
void ScanDirectory(const VirtualDir& dir);
diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp
index 4379634d03..b4d50227d3 100644
--- a/src/core/loader/loader.cpp
+++ b/src/core/loader/loader.cpp
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -9,11 +9,15 @@
#include <ostream>
#include <string>
#include <concepts>
+#include <algorithm>
#include "common/concepts.h"
#include "common/fs/path_util.h"
#include "common/logging/log.h"
#include "common/string_util.h"
#include "core/core.h"
+#include "core/file_sys/card_image.h"
+#include "core/file_sys/common_funcs.h"
+#include "core/file_sys/submission_package.h"
#include "core/hle/kernel/k_process.h"
#include "core/loader/deconstructed_rom_directory.h"
#include "core/loader/kip.h"
@@ -37,6 +41,49 @@ std::optional<FileType> IdentifyFileLoader(FileSys::VirtualFile file) {
return std::nullopt;
}
+std::shared_ptr<FileSys::NSP> OpenContainerAsNsp(FileSys::VirtualFile file, FileType type,
+ u64 program_id = 0,
+ std::size_t program_index = 0) {
+ if (!file) {
+ return nullptr;
+ }
+
+ if (type == FileType::NSP) {
+ auto nsp = std::make_shared<FileSys::NSP>(file, program_id, program_index);
+ return nsp->GetStatus() == ResultStatus::Success ? nsp : nullptr;
+ }
+
+ if (type == FileType::XCI) {
+ FileSys::XCI xci{file, program_id, program_index};
+ if (xci.GetStatus() != ResultStatus::Success) {
+ return nullptr;
+ }
+
+ auto secure_nsp = xci.GetSecurePartitionNSP();
+ if (secure_nsp == nullptr || secure_nsp->GetStatus() != ResultStatus::Success) {
+ return nullptr;
+ }
+
+ return secure_nsp;
+ }
+
+ return nullptr;
+}
+
+bool HasApplicationProgramContent(const std::shared_ptr<FileSys::NSP>& nsp) {
+ if (!nsp) {
+ return false;
+ }
+
+ const auto& ncas = nsp->GetNCAs();
+ return std::any_of(ncas.cbegin(), ncas.cend(), [](const auto& title_entry) {
+ const auto& nca_map = title_entry.second;
+ return nca_map.find(
+ {FileSys::TitleType::Application, FileSys::ContentRecordType::Program}) !=
+ nca_map.end();
+ });
+}
+
} // namespace
FileType IdentifyFile(FileSys::VirtualFile file) {
@@ -62,6 +109,27 @@ FileType IdentifyFile(FileSys::VirtualFile file) {
}
}
+bool IsContainerType(FileType type) {
+ return type == FileType::NSP || type == FileType::XCI;
+}
+
+bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type, u64 program_id,
+ std::size_t program_index) {
+ if (!file) {
+ return false;
+ }
+
+ if (type == FileType::Unknown) {
+ type = IdentifyFile(file);
+ }
+
+ if (!IsContainerType(type)) {
+ return false;
+ }
+
+ return HasApplicationProgramContent(OpenContainerAsNsp(file, type, program_id, program_index));
+}
+
FileType GuessFromFilename(const std::string& name) {
if (name == "main")
return FileType::DeconstructedRomDirectory;
diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h
index f4e932cec9..95ce638da0 100644
--- a/src/core/loader/loader.h
+++ b/src/core/loader/loader.h
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -46,13 +49,30 @@ enum class FileType {
};
/**
- * Identifies the type of a bootable file based on the magic value in its header.
+ * Identifies the type of a supported file/container based on its structure.
* @param file open file
* @return FileType of file
*/
FileType IdentifyFile(FileSys::VirtualFile file);
/**
+ * Returns whether the file type represents a container format that can bundle multiple titles
+ * (currently NSP/XCI).
+ */
+bool IsContainerType(FileType type);
+
+/**
+ * Returns whether a container file is bootable as a game (has Application/Program content).
+ *
+ * @param file open file
+ * @param type optional file type; if Unknown it is auto-detected.
+ * @param program_id optional program id hint for multi-program containers.
+ * @param program_index optional program index hint for multi-program containers.
+ */
+bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type = FileType::Unknown,
+ u64 program_id = 0, std::size_t program_index = 0);
+
+/**
* Guess the type of a bootable file from its name
* @param name String name of bootable file
* @return FileType of file. Note: this will return FileType::Unknown if it is unable to determine
diff --git a/src/core/loader/nsp.cpp b/src/core/loader/nsp.cpp
index 3016d5f25f..4333acb70c 100644
--- a/src/core/loader/nsp.cpp
+++ b/src/core/loader/nsp.cpp
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -55,19 +58,30 @@ AppLoader_NSP::~AppLoader_NSP() = default;
FileType AppLoader_NSP::IdentifyType(const FileSys::VirtualFile& nsp_file) {
const FileSys::NSP nsp(nsp_file);
- if (nsp.GetStatus() == ResultStatus::Success) {
- // Extracted Type case
- if (nsp.IsExtractedType() && nsp.GetExeFS() != nullptr &&
- FileSys::IsDirectoryExeFS(nsp.GetExeFS())) {
- return FileType::NSP;
+ if (nsp.GetStatus() != ResultStatus::Success) {
+ return FileType::Error;
+ }
+
+ // Extracted Type case
+ if (nsp.IsExtractedType() && nsp.GetExeFS() != nullptr &&
+ FileSys::IsDirectoryExeFS(nsp.GetExeFS())) {
+ return FileType::NSP;
+ }
+
+ // Non-extracted NSPs can legitimately contain only update/DLC content.
+ // Identify the container format itself; bootability is validated by Load().
+ if (!nsp.GetNCAs().empty()) {
+ return FileType::NSP;
+ }
+
+ // Fallback when NCAs couldn't be parsed (e.g. missing keys) but the PFS still contains NCAs.
+ for (const auto& entry : nsp.GetFiles()) {
+ if (entry == nullptr) {
+ continue;
}
- // Non-Extracted Type case
- const auto program_id = nsp.GetProgramTitleID();
- if (!nsp.IsExtractedType() &&
- nsp.GetNCA(program_id, FileSys::ContentRecordType::Program) != nullptr &&
- AppLoader_NCA::IdentifyType(
- nsp.GetNCAFile(program_id, FileSys::ContentRecordType::Program)) == FileType::NCA) {
+ const auto& name = entry->GetName();
+ if (name.size() >= 4 && name.substr(name.size() - 4) == ".nca") {
return FileType::NSP;
}
}
diff --git a/src/core/loader/xci.cpp b/src/core/loader/xci.cpp
index e9abb199a1..983184a226 100644
--- a/src/core/loader/xci.cpp
+++ b/src/core/loader/xci.cpp
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -44,10 +47,13 @@ AppLoader_XCI::~AppLoader_XCI() = default;
FileType AppLoader_XCI::IdentifyType(const FileSys::VirtualFile& xci_file) {
const FileSys::XCI xci(xci_file);
- if (xci.GetStatus() == ResultStatus::Success &&
- xci.GetNCAByType(FileSys::NCAContentType::Program) != nullptr &&
- AppLoader_NCA::IdentifyType(xci.GetNCAFileByType(FileSys::NCAContentType::Program)) ==
- FileType::NCA) {
+ if (xci.GetStatus() != ResultStatus::Success) {
+ return FileType::Error;
+ }
+
+ // Identify XCI as a valid container even when it does not include a bootable Program NCA.
+ // Bootability is handled by AppLoader_XCI::Load().
+ if (xci.GetSecurePartitionNSP() != nullptr) {
return FileType::XCI;
}