aboutsummaryrefslogtreecommitdiff
path: root/src/core/file_sys/patch_manager.cpp
diff options
context:
space:
mode:
authorMaufeat <sahyno1996@gmail.com>2026-02-06 14:05:44 +0100
committercrueter <crueter@eden-emu.dev>2026-02-06 14:05:44 +0100
commit69aff83ef448b1f76fad1fa6ae46ff9f70b840c0 (patch)
tree7f6be86d92589346fd007eae219eb3aa99bab5a3 /src/core/file_sys/patch_manager.cpp
parente07e269bd766aa1fc29951b958db2063c1a4048e (diff)
[fs/core] Load external content without NAND install (#2862)
Adds the capability to add DLC and Updates without installing them to NAND. This was tested on Windows only and needs Android integration. Co-authored-by: crueter <crueter@eden-emu.dev> Co-authored-by: wildcard <wildcard@eden-emu.dev> Co-authored-by: nekle <nekle@protonmail.com> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2862 Reviewed-by: DraVee <dravee@eden-emu.dev> Reviewed-by: crueter <crueter@eden-emu.dev> Co-authored-by: Maufeat <sahyno1996@gmail.com> Co-committed-by: Maufeat <sahyno1996@gmail.com>
Diffstat (limited to 'src/core/file_sys/patch_manager.cpp')
-rw-r--r--src/core/file_sys/patch_manager.cpp458
1 files changed, 413 insertions, 45 deletions
diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp
index 657172fb4d..86c2076380 100644
--- a/src/core/file_sys/patch_manager.cpp
+++ b/src/core/file_sys/patch_manager.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
@@ -137,12 +137,127 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
return exefs;
const auto& disabled = Settings::values.disabled_addons[title_id];
- const auto update_disabled =
- std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
- // Game Updates
+ bool update_disabled = true;
+ std::optional<u32> enabled_version;
+ bool checked_external = false;
+ bool checked_manual = false;
+
+ const auto* content_union = dynamic_cast<const ContentProviderUnion*>(&content_provider);
const auto update_tid = GetUpdateTitleID(title_id);
- const auto update = content_provider.GetEntry(update_tid, ContentRecordType::Program);
+
+ if (content_union) {
+ // First, check ExternalContentProvider
+ const auto* external_provider = content_union->GetExternalProvider();
+ if (external_provider) {
+ const auto update_versions = external_provider->ListUpdateVersions(update_tid);
+
+ 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()) {
+ update_disabled = false;
+ enabled_version = update_entry.version;
+ break;
+ }
+ }
+ }
+ }
+
+ // Also check ManualContentProvider (for Android)
+ if (!checked_external) {
+ const auto* manual_provider = dynamic_cast<const ManualContentProvider*>(
+ content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
+ if (manual_provider) {
+ const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid);
+
+ 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()) {
+ update_disabled = false;
+ enabled_version = update_entry.version;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // check for original NAND style
+ // Check NAND if: no external updates exist, OR all external updates are disabled
+ if (!checked_external && !checked_manual) {
+ // Only enable NAND update if it exists AND is not disabled
+ // We need to check if an update actually exists in the content provider
+ const bool has_nand_update = content_provider.HasEntry(update_tid, ContentRecordType::Program);
+
+ if (has_nand_update) {
+ const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
+ const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
+ const bool generic_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
+
+ if (!nand_disabled && !sdmc_disabled && !generic_disabled) {
+ update_disabled = false;
+ }
+ }
+ } else if (update_disabled && content_union) {
+ const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
+ const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
+
+ if (!nand_disabled || !sdmc_disabled) {
+ const auto nand_sdmc_entries = content_union->ListEntriesFilterOrigin(
+ std::nullopt, TitleType::Update, ContentRecordType::Program, update_tid);
+
+ for (const auto& [slot, entry] : nand_sdmc_entries) {
+ if (slot == ContentProviderUnionSlot::UserNAND ||
+ slot == ContentProviderUnionSlot::SysNAND) {
+ if (!nand_disabled) {
+ update_disabled = false;
+ break;
+ }
+ } else if (slot == ContentProviderUnionSlot::SDMC) {
+ if (!sdmc_disabled) {
+ update_disabled = false;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // Game Updates
+ std::unique_ptr<NCA> update = nullptr;
+
+ // If we have a specific enabled version from external provider, use it
+ if (enabled_version.has_value() && content_union) {
+ const auto* external_provider = content_union->GetExternalProvider();
+ if (external_provider) {
+ auto file = external_provider->GetEntryForVersion(update_tid, ContentRecordType::Program, *enabled_version);
+ if (file != nullptr) {
+ update = std::make_unique<NCA>(file);
+ }
+ }
+
+ // Also try ManualContentProvider
+ if (update == nullptr) {
+ const auto* manual_provider = dynamic_cast<const ManualContentProvider*>(
+ content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
+ if (manual_provider) {
+ auto file = manual_provider->GetEntryForVersion(update_tid, ContentRecordType::Program, *enabled_version);
+ if (file != nullptr) {
+ update = std::make_unique<NCA>(file);
+ }
+ }
+ }
+ }
+
+ // Fallback to regular content provider if no external update was loaded
+ if (update == nullptr && !update_disabled) {
+ update = content_provider.GetEntry(update_tid, ContentRecordType::Program);
+ }
if (!update_disabled && update != nullptr && update->GetExeFS() != nullptr) {
LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully",
@@ -447,21 +562,103 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
// Game Updates
const auto update_tid = GetUpdateTitleID(title_id);
- const auto update_raw = content_provider.GetEntryRaw(update_tid, type);
-
const auto& disabled = Settings::values.disabled_addons[title_id];
- const auto update_disabled =
- std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
+
+ bool update_disabled = true;
+ std::optional<u32> enabled_version;
+ VirtualFile update_raw = nullptr;
+ bool checked_external = false;
+ bool checked_manual = false;
+
+ const auto* content_union = dynamic_cast<const ContentProviderUnion*>(&content_provider);
+ if (content_union) {
+ // First, check ExternalContentProvider
+ const auto* external_provider = content_union->GetExternalProvider();
+ if (external_provider) {
+ const auto update_versions = external_provider->ListUpdateVersions(update_tid);
+
+ 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()) {
+ update_disabled = false;
+ enabled_version = update_entry.version;
+ update_raw = external_provider->GetEntryForVersion(update_tid, type, update_entry.version);
+ break;
+ }
+ }
+ }
+ }
+
+ if (!checked_external) {
+ const auto* manual_provider = dynamic_cast<const ManualContentProvider*>(
+ content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
+ if (manual_provider) {
+ const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid);
+
+ 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()) {
+ update_disabled = false;
+ enabled_version = update_entry.version;
+ update_raw = manual_provider->GetEntryForVersion(update_tid, type, update_entry.version);
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (!checked_external && !checked_manual) {
+ const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
+ const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
+ const bool generic_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
+
+ if (!nand_disabled && !sdmc_disabled && !generic_disabled) {
+ update_disabled = false;
+ }
+ if (!update_disabled) {
+ update_raw = content_provider.GetEntryRaw(update_tid, type);
+ }
+ } else if (update_disabled && content_union) {
+ const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
+ const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
+
+ if (!nand_disabled || !sdmc_disabled) {
+ const auto nand_sdmc_entries = content_union->ListEntriesFilterOrigin(
+ std::nullopt, TitleType::Update, type, update_tid);
+
+ for (const auto& [slot, entry] : nand_sdmc_entries) {
+ if (slot == ContentProviderUnionSlot::UserNAND ||
+ slot == ContentProviderUnionSlot::SysNAND) {
+ if (!nand_disabled) {
+ update_disabled = false;
+ update_raw = content_provider.GetEntryRaw(update_tid, type);
+ break;
+ }
+ } else if (slot == ContentProviderUnionSlot::SDMC) {
+ if (!sdmc_disabled) {
+ update_disabled = false;
+ update_raw = content_provider.GetEntryRaw(update_tid, type);
+ break;
+ }
+ }
+ }
+ }
+ }
if (!update_disabled && update_raw != nullptr && base_nca != nullptr) {
const auto new_nca = std::make_shared<NCA>(update_raw, base_nca);
if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
new_nca->GetRomFS() != nullptr) {
LOG_INFO(Loader, " RomFS: Update ({}) applied successfully",
+ enabled_version.has_value() ? FormatTitleVersion(*enabled_version) :
FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0)));
romfs = new_nca->GetRomFS();
- const auto version =
- FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0));
}
} else if (!update_disabled && packed_update_raw != nullptr && base_nca != nullptr) {
const auto new_nca = std::make_shared<NCA>(packed_update_raw, base_nca);
@@ -490,35 +687,180 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
// Game Updates
const auto update_tid = GetUpdateTitleID(title_id);
- PatchManager update{update_tid, fs_controller, content_provider};
- const auto metadata = update.GetControlMetadata();
- const auto& nacp = metadata.first;
-
- const auto update_disabled =
- std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
- Patch update_patch = {.enabled = !update_disabled,
- .name = "Update",
- .version = "",
- .type = PatchType::Update,
- .program_id = title_id,
- .title_id = title_id};
-
- if (nacp != nullptr) {
- update_patch.version = nacp->GetVersionString();
- out.push_back(update_patch);
- } else {
- if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) {
+
+ std::vector<Patch> external_update_patches;
+
+ const auto* content_union = dynamic_cast<const ContentProviderUnion*>(&content_provider);
+
+ if (content_union) {
+ // First, check ExternalContentProvider for updates
+ const auto* external_provider = content_union->GetExternalProvider();
+ if (external_provider) {
+ const auto update_versions = external_provider->ListUpdateVersions(update_tid);
+
+ for (const auto& update_entry : update_versions) {
+ std::string version_str = update_entry.version_string;
+ if (version_str.empty()) {
+ 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();
+
+ Patch update_patch = {.enabled = !update_disabled,
+ .name = "Update",
+ .version = version_str,
+ .type = PatchType::Update,
+ .program_id = title_id,
+ .title_id = update_tid,
+ .source = PatchSource::External,
+ .numeric_version = update_entry.version};
+
+ external_update_patches.push_back(update_patch);
+ }
+ }
+
+ const auto* manual_provider = dynamic_cast<const ManualContentProvider*>(
+ content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
+ if (manual_provider && external_update_patches.empty()) {
+ const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid);
+
+ for (const auto& update_entry : manual_update_versions) {
+ std::string version_str = update_entry.version_string;
+ if (version_str.empty()) {
+ 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();
+
+
+ Patch update_patch = {.enabled = !update_disabled,
+ .name = "Update",
+ .version = version_str,
+ .type = PatchType::Update,
+ .program_id = title_id,
+ .title_id = update_tid,
+ .source = PatchSource::External,
+ .numeric_version = update_entry.version};
+
+ external_update_patches.push_back(update_patch);
+ }
+ }
+
+ if (external_update_patches.size() > 1) {
+ bool found_enabled = false;
+ for (auto& patch : external_update_patches) {
+ if (patch.enabled) {
+ if (found_enabled) {
+ patch.enabled = false;
+ } else {
+ found_enabled = true;
+ }
+ }
+ }
+ }
+
+ for (auto& patch : external_update_patches) {
+ out.push_back(std::move(patch));
+ }
+
+ const auto all_updates = content_union->ListEntriesFilterOrigin(
+ std::nullopt, std::nullopt, ContentRecordType::Program, update_tid);
+
+ for (const auto& [slot, entry] : all_updates) {
+ if (slot == ContentProviderUnionSlot::External) {
+ continue;
+ }
+
+ PatchSource source_type = PatchSource::Unknown;
+ std::string source_suffix;
+
+ switch (slot) {
+ case ContentProviderUnionSlot::UserNAND:
+ case ContentProviderUnionSlot::SysNAND:
+ source_type = PatchSource::NAND;
+ source_suffix = " (NAND)";
+ break;
+ case ContentProviderUnionSlot::SDMC:
+ source_type = PatchSource::NAND;
+ source_suffix = " (SDMC)";
+ break;
+ default:
+ break;
+ }
+
+ std::string version_str;
+ u32 numeric_ver = 0;
+ PatchManager update{update_tid, fs_controller, content_provider};
+ const auto metadata = update.GetControlMetadata();
+ const auto& nacp = metadata.first;
+
+ if (nacp != nullptr) {
+ version_str = nacp->GetVersionString();
+ }
+
const auto meta_ver = content_provider.GetEntryVersion(update_tid);
- if (meta_ver.value_or(0) == 0) {
- out.push_back(update_patch);
- } else {
- update_patch.version = FormatTitleVersion(*meta_ver);
- out.push_back(update_patch);
+ if (meta_ver.has_value()) {
+ numeric_ver = *meta_ver;
+ if (version_str.empty() && numeric_ver != 0) {
+ version_str = FormatTitleVersion(numeric_ver);
+ }
}
- } else if (update_raw != nullptr) {
- update_patch.version = "PACKED";
+
+ std::string patch_name = "Update" + source_suffix;
+
+ bool update_disabled =
+ std::find(disabled.cbegin(), disabled.cend(), patch_name) != disabled.cend();
+
+ Patch update_patch = {.enabled = !update_disabled,
+ .name = patch_name,
+ .version = version_str,
+ .type = PatchType::Update,
+ .program_id = title_id,
+ .title_id = update_tid,
+ .source = source_type,
+ .numeric_version = numeric_ver};
+
out.push_back(update_patch);
}
+ } else {
+ PatchManager update{update_tid, fs_controller, content_provider};
+ const auto metadata = update.GetControlMetadata();
+ const auto& nacp = metadata.first;
+
+ bool update_disabled =
+ std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
+ Patch update_patch = {.enabled = !update_disabled,
+ .name = "Update",
+ .version = "",
+ .type = PatchType::Update,
+ .program_id = title_id,
+ .title_id = title_id,
+ .source = PatchSource::Unknown,
+ .numeric_version = 0};
+
+ if (nacp != nullptr) {
+ update_patch.version = nacp->GetVersionString();
+ out.push_back(update_patch);
+ } else {
+ if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) {
+ const auto meta_ver = content_provider.GetEntryVersion(update_tid);
+ if (meta_ver.value_or(0) == 0) {
+ out.push_back(update_patch);
+ } else {
+ update_patch.version = FormatTitleVersion(*meta_ver);
+ update_patch.numeric_version = *meta_ver;
+ out.push_back(update_patch);
+ }
+ } else if (update_raw != nullptr) {
+ update_patch.version = "PACKED";
+ update_patch.source = PatchSource::Packed;
+ out.push_back(update_patch);
+ }
+ }
}
// General Mods (LayeredFS and IPS)
@@ -533,7 +875,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
.version = "Cheats",
.type = PatchType::Mod,
.program_id = title_id,
- .title_id = title_id
+ .title_id = title_id,
+ .source = PatchSource::Unknown
});
}
@@ -554,7 +897,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
} else if (std::find(EXEFS_FILE_NAMES.begin(), EXEFS_FILE_NAMES.end(),
file->GetName()) != EXEFS_FILE_NAMES.end()) {
layeredfs = true;
- }
+ }
}
if (ips)
@@ -579,7 +922,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
.version = types,
.type = PatchType::Mod,
.program_id = title_id,
- .title_id = title_id});
+ .title_id = title_id,
+ .source = PatchSource::Unknown});
}
}
@@ -593,7 +937,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfs")) ||
IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfslite"))) {
AppendCommaIfNotEmpty(types, "LayeredFS");
- }
+ }
if (!types.empty()) {
const auto mod_disabled =
@@ -603,21 +947,44 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
.version = types,
.type = PatchType::Mod,
.program_id = title_id,
- .title_id = title_id});
+ .title_id = title_id,
+ .source = PatchSource::Unknown});
}
}
// DLC
const auto dlc_entries =
content_provider.ListEntriesFilter(TitleType::AOC, ContentRecordType::Data);
+
std::vector<ContentProviderEntry> dlc_match;
dlc_match.reserve(dlc_entries.size());
std::copy_if(dlc_entries.begin(), dlc_entries.end(), std::back_inserter(dlc_match),
[this](const ContentProviderEntry& entry) {
- return GetBaseTitleID(entry.title_id) == title_id &&
- content_provider.GetEntry(entry)->GetStatus() ==
- Loader::ResultStatus::Success;
+ const auto base_tid = GetBaseTitleID(entry.title_id);
+ const bool matches_base = base_tid == title_id;
+
+ if (!matches_base) {
+ LOG_DEBUG(Loader, "DLC {:016X} base {:016X} doesn't match title {:016X}",
+ entry.title_id, base_tid, title_id);
+ return false;
+ }
+
+ auto nca = content_provider.GetEntry(entry);
+ if (!nca) {
+ LOG_DEBUG(Loader, "Failed to get NCA for DLC {:016X}", entry.title_id);
+ return false;
+ }
+
+ const auto status = nca->GetStatus();
+ if (status != Loader::ResultStatus::Success) {
+ LOG_DEBUG(Loader, "DLC {:016X} NCA has status {}",
+ entry.title_id, static_cast<int>(status));
+ return false;
+ }
+
+ return true;
});
+
if (!dlc_match.empty()) {
// Ensure sorted so DLC IDs show in order.
std::sort(dlc_match.begin(), dlc_match.end());
@@ -635,7 +1002,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
.version = std::move(list),
.type = PatchType::DLC,
.program_id = title_id,
- .title_id = dlc_match.back().title_id});
+ .title_id = dlc_match.back().title_id,
+ .source = PatchSource::Unknown});
}
return out;