mirror of
https://git.eden-emu.dev/eden-emu/eden
synced 2026-04-24 16:18:59 +02:00
[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>
This commit is contained in:
parent
e07e269bd7
commit
69aff83ef4
40 changed files with 1790 additions and 126 deletions
|
|
@ -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();
|
||||
|
||||
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);
|
||||
|
||||
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
|
||||
const auto update_tid = GetUpdateTitleID(title_id);
|
||||
const auto update = content_provider.GetEntry(update_tid, ContentRecordType::Program);
|
||||
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,34 +687,179 @@ 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};
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
if (meta_ver.has_value()) {
|
||||
numeric_ver = *meta_ver;
|
||||
if (version_str.empty() && numeric_ver != 0) {
|
||||
version_str = FormatTitleVersion(numeric_ver);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
} else if (update_raw != nullptr) {
|
||||
update_patch.version = "PACKED";
|
||||
out.push_back(update_patch);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -28,6 +31,13 @@ class NACP;
|
|||
|
||||
enum class PatchType { Update, DLC, Mod };
|
||||
|
||||
enum class PatchSource {
|
||||
Unknown,
|
||||
NAND,
|
||||
External,
|
||||
Packed,
|
||||
};
|
||||
|
||||
struct Patch {
|
||||
bool enabled;
|
||||
std::string name;
|
||||
|
|
@ -35,6 +45,8 @@ struct Patch {
|
|||
PatchType type;
|
||||
u64 program_id;
|
||||
u64 title_id;
|
||||
PatchSource source;
|
||||
u32 numeric_version{0};
|
||||
};
|
||||
|
||||
// A centralized class to manage patches to games.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -13,12 +13,15 @@
|
|||
#include "common/hex_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/scope_exit.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/crypto/key_manager.h"
|
||||
#include "core/file_sys/card_image.h"
|
||||
#include "core/file_sys/common_funcs.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/control_metadata.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_concat.h"
|
||||
#include "core/loader/loader.h"
|
||||
|
|
@ -974,6 +977,22 @@ std::optional<ContentProviderUnionSlot> ContentProviderUnion::GetSlotForEntry(
|
|||
return iter->first;
|
||||
}
|
||||
|
||||
const ExternalContentProvider* ContentProviderUnion::GetExternalProvider() const {
|
||||
auto it = providers.find(ContentProviderUnionSlot::External);
|
||||
if (it != providers.end() && it->second != nullptr) {
|
||||
return dynamic_cast<const ExternalContentProvider*>(it->second);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ContentProvider* ContentProviderUnion::GetSlotProvider(ContentProviderUnionSlot slot) const {
|
||||
auto it = providers.find(slot);
|
||||
if (it != providers.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ManualContentProvider::~ManualContentProvider() = default;
|
||||
|
||||
void ManualContentProvider::AddEntry(TitleType title_type, ContentRecordType content_type,
|
||||
|
|
@ -981,8 +1000,51 @@ void ManualContentProvider::AddEntry(TitleType title_type, ContentRecordType con
|
|||
entries.insert_or_assign({title_type, content_type, title_id}, file);
|
||||
}
|
||||
|
||||
void ManualContentProvider::AddEntryWithVersion(TitleType title_type, ContentRecordType content_type,
|
||||
u64 title_id, u32 version,
|
||||
const std::string& version_string, VirtualFile file) {
|
||||
if (title_type == TitleType::Update) {
|
||||
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()) {
|
||||
// Update existing entry
|
||||
it->files[content_type] = file;
|
||||
if (!version_string.empty()) {
|
||||
it->version_string = version_string;
|
||||
}
|
||||
} else {
|
||||
// Add new entry
|
||||
ExternalUpdateEntry new_entry;
|
||||
new_entry.title_id = title_id;
|
||||
new_entry.version = version;
|
||||
new_entry.version_string = version_string;
|
||||
new_entry.files[content_type] = file;
|
||||
multi_version_entries.push_back(new_entry);
|
||||
}
|
||||
|
||||
auto existing = entries.find({title_type, content_type, title_id});
|
||||
if (existing == entries.end()) {
|
||||
entries.insert_or_assign({title_type, content_type, title_id}, file);
|
||||
} else {
|
||||
// Check if this version is higher
|
||||
for (const auto& entry : multi_version_entries) {
|
||||
if (entry.title_id == title_id && entry.version > version) {
|
||||
return; // Don't replace with lower version
|
||||
}
|
||||
}
|
||||
entries.insert_or_assign({title_type, content_type, title_id}, file);
|
||||
}
|
||||
} else {
|
||||
entries.insert_or_assign({title_type, content_type, title_id}, file);
|
||||
}
|
||||
}
|
||||
|
||||
void ManualContentProvider::ClearAllEntries() {
|
||||
entries.clear();
|
||||
multi_version_entries.clear();
|
||||
}
|
||||
|
||||
void ManualContentProvider::Refresh() {}
|
||||
|
|
@ -1036,4 +1098,459 @@ std::vector<ContentProviderEntry> ManualContentProvider::ListEntriesFilter(
|
|||
out.erase(std::unique(out.begin(), out.end()), out.end());
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<ExternalUpdateEntry> ManualContentProvider::ListUpdateVersions(u64 title_id) const {
|
||||
std::vector<ExternalUpdateEntry> out;
|
||||
|
||||
for (const auto& entry : multi_version_entries) {
|
||||
if (entry.title_id == title_id) {
|
||||
out.push_back(entry);
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(out.begin(), out.end(), [](const ExternalUpdateEntry& a, const ExternalUpdateEntry& b) {
|
||||
return a.version > b.version;
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
VirtualFile ManualContentProvider::GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const {
|
||||
for (const auto& entry : multi_version_entries) {
|
||||
if (entry.title_id == title_id && entry.version == version) {
|
||||
auto it = entry.files.find(type);
|
||||
if (it != entry.files.end()) {
|
||||
return it->second;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ManualContentProvider::HasMultipleVersions(u64 title_id, ContentRecordType type) const {
|
||||
int count = 0;
|
||||
for (const auto& entry : multi_version_entries) {
|
||||
if (entry.title_id == title_id && entry.files.count(type) > 0) {
|
||||
count++;
|
||||
if (count > 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ExternalContentProvider::ExternalContentProvider(std::vector<VirtualDir> load_directories)
|
||||
: load_dirs(std::move(load_directories)) {
|
||||
ExternalContentProvider::Refresh();
|
||||
}
|
||||
|
||||
ExternalContentProvider::~ExternalContentProvider() = default;
|
||||
|
||||
void ExternalContentProvider::AddDirectory(VirtualDir directory) {
|
||||
if (directory != nullptr) {
|
||||
load_dirs.push_back(std::move(directory));
|
||||
ScanDirectory(load_dirs.back());
|
||||
}
|
||||
}
|
||||
|
||||
void ExternalContentProvider::ClearDirectories() {
|
||||
load_dirs.clear();
|
||||
entries.clear();
|
||||
versions.clear();
|
||||
multi_version_entries.clear();
|
||||
}
|
||||
|
||||
void ExternalContentProvider::Refresh() {
|
||||
entries.clear();
|
||||
versions.clear();
|
||||
multi_version_entries.clear();
|
||||
for (const auto& dir : load_dirs) {
|
||||
if (dir != nullptr) {
|
||||
ScanDirectory(dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ExternalContentProvider::ScanDirectory(const VirtualDir& dir) {
|
||||
if (dir == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& file : dir->GetFiles()) {
|
||||
const auto filename = file->GetName();
|
||||
const auto dot_pos = filename.find_last_of('.');
|
||||
|
||||
if (dot_pos == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto extension = Common::ToLower(filename.substr(dot_pos + 1));
|
||||
|
||||
if (extension == "nsp") {
|
||||
ProcessNSP(file);
|
||||
} else if (extension == "xci") {
|
||||
ProcessXCI(file);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& subdir : dir->GetSubdirectories()) {
|
||||
ScanDirectory(subdir);
|
||||
}
|
||||
}
|
||||
|
||||
void ExternalContentProvider::ProcessNSP(const VirtualFile& file) {
|
||||
auto nsp = NSP(file);
|
||||
if (nsp.GetStatus() != Loader::ResultStatus::Success) {
|
||||
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::map<ContentRecordType, VirtualFile>> 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}][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) {
|
||||
for (const auto& [content_type, _file] : files_map) {
|
||||
existing.files[content_type] = _file;
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ExternalContentProvider::ProcessXCI(const VirtualFile& file) {
|
||||
auto xci = XCI(file);
|
||||
if (xci.GetStatus() != Loader::ResultStatus::Success) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto nsp = xci.GetSecurePartitionNSP();
|
||||
if (nsp == nullptr) {
|
||||
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::map<ContentRecordType, VirtualFile>> 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}][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) {
|
||||
for (const auto& [content_type, _file] : files_map) {
|
||||
existing.files[content_type] = _file;
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ExternalContentProvider::HasEntry(u64 title_id, ContentRecordType type) const {
|
||||
return GetEntryRaw(title_id, type) != nullptr;
|
||||
}
|
||||
|
||||
std::optional<u32> ExternalContentProvider::GetEntryVersion(u64 title_id) const {
|
||||
const auto it = versions.find(title_id);
|
||||
if (it != versions.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
VirtualFile ExternalContentProvider::GetEntryUnparsed(u64 title_id, ContentRecordType type) const {
|
||||
return GetEntryRaw(title_id, type);
|
||||
}
|
||||
|
||||
VirtualFile ExternalContentProvider::GetEntryRaw(u64 title_id, ContentRecordType type) const {
|
||||
// Try to find in AOC (DLC) entries
|
||||
{
|
||||
const auto it = entries.find({title_id, type, TitleType::AOC});
|
||||
if (it != entries.end()) {
|
||||
return it->second;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find in Update entries
|
||||
{
|
||||
const auto it = entries.find({title_id, type, TitleType::Update});
|
||||
if (it != entries.end()) {
|
||||
return it->second;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::unique_ptr<NCA> ExternalContentProvider::GetEntry(u64 title_id,
|
||||
ContentRecordType type) const {
|
||||
const auto file = GetEntryRaw(title_id, type);
|
||||
if (file == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return std::make_unique<NCA>(file);
|
||||
}
|
||||
|
||||
std::vector<ContentProviderEntry> ExternalContentProvider::ListEntriesFilter(
|
||||
std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
|
||||
std::optional<u64> title_id) const {
|
||||
std::vector<ContentProviderEntry> out;
|
||||
|
||||
for (const auto& [key, file] : entries) {
|
||||
const auto& [e_title_id, e_content_type, e_title_type] = key;
|
||||
|
||||
if ((title_type == std::nullopt || e_title_type == *title_type) &&
|
||||
(record_type == std::nullopt || e_content_type == *record_type) &&
|
||||
(title_id == std::nullopt || e_title_id == *title_id)) {
|
||||
out.emplace_back(ContentProviderEntry{e_title_id, e_content_type});
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(out.begin(), out.end());
|
||||
out.erase(std::unique(out.begin(), out.end()), out.end());
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<ExternalUpdateEntry> ExternalContentProvider::ListUpdateVersions(u64 title_id) const {
|
||||
std::vector<ExternalUpdateEntry> out;
|
||||
|
||||
for (const auto& entry : multi_version_entries) {
|
||||
if (entry.title_id == title_id) {
|
||||
out.push_back(entry);
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(out.begin(), out.end(), [](const ExternalUpdateEntry& a, const ExternalUpdateEntry& b) {
|
||||
return a.version > b.version;
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
VirtualFile ExternalContentProvider::GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const {
|
||||
for (const auto& entry : multi_version_entries) {
|
||||
if (entry.title_id == title_id && entry.version == version) {
|
||||
auto it = entry.files.find(type);
|
||||
if (it != entry.files.end()) {
|
||||
return it->second;
|
||||
}
|
||||
}
|
||||
}
|
||||
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.count(type) > 0) {
|
||||
count++;
|
||||
if (count > 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -14,7 +17,8 @@
|
|||
#include "core/file_sys/vfs/vfs.h"
|
||||
|
||||
namespace FileSys {
|
||||
class CNMT;
|
||||
class ExternalContentProvider;
|
||||
class CNMT;
|
||||
class NCA;
|
||||
class NSP;
|
||||
class XCI;
|
||||
|
|
@ -48,6 +52,13 @@ struct ContentProviderEntry {
|
|||
std::string DebugInfo() const;
|
||||
};
|
||||
|
||||
struct ExternalUpdateEntry {
|
||||
u64 title_id;
|
||||
u32 version;
|
||||
std::string version_string;
|
||||
std::map<ContentRecordType, VirtualFile> files;
|
||||
};
|
||||
|
||||
constexpr u64 GetUpdateTitleID(u64 base_title_id) {
|
||||
return base_title_id | 0x800;
|
||||
}
|
||||
|
|
@ -208,6 +219,7 @@ enum class ContentProviderUnionSlot {
|
|||
UserNAND, ///< User NAND
|
||||
SDMC, ///< SD Card
|
||||
FrontendManual, ///< Frontend-defined game list or similar
|
||||
External, ///< External content from NSP/XCI files in configured directories
|
||||
};
|
||||
|
||||
// Combines multiple ContentProvider(s) (i.e. SysNAND, UserNAND, SDMC) into one interface.
|
||||
|
|
@ -228,6 +240,9 @@ public:
|
|||
std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
|
||||
std::optional<u64> title_id) const override;
|
||||
|
||||
const ExternalContentProvider* GetExternalProvider() const;
|
||||
const ContentProvider* GetSlotProvider(ContentProviderUnionSlot slot) const;
|
||||
|
||||
std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> ListEntriesFilterOrigin(
|
||||
std::optional<ContentProviderUnionSlot> origin = {},
|
||||
std::optional<TitleType> title_type = {}, std::optional<ContentRecordType> record_type = {},
|
||||
|
|
@ -246,6 +261,8 @@ public:
|
|||
|
||||
void AddEntry(TitleType title_type, ContentRecordType content_type, u64 title_id,
|
||||
VirtualFile file);
|
||||
void AddEntryWithVersion(TitleType title_type, ContentRecordType content_type, u64 title_id,
|
||||
u32 version, const std::string& version_string, VirtualFile file);
|
||||
void ClearAllEntries();
|
||||
|
||||
void Refresh() override;
|
||||
|
|
@ -258,8 +275,46 @@ public:
|
|||
std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
|
||||
std::optional<u64> title_id) const override;
|
||||
|
||||
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;
|
||||
std::vector<ExternalUpdateEntry> multi_version_entries;
|
||||
};
|
||||
|
||||
class ExternalContentProvider : public ContentProvider {
|
||||
public:
|
||||
explicit ExternalContentProvider(std::vector<VirtualDir> load_directories = {});
|
||||
~ExternalContentProvider() override;
|
||||
|
||||
void AddDirectory(VirtualDir directory);
|
||||
void ClearDirectories();
|
||||
|
||||
void Refresh() override;
|
||||
bool HasEntry(u64 title_id, ContentRecordType type) const override;
|
||||
std::optional<u32> GetEntryVersion(u64 title_id) const override;
|
||||
VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const override;
|
||||
VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const override;
|
||||
std::unique_ptr<NCA> GetEntry(u64 title_id, ContentRecordType type) const override;
|
||||
std::vector<ContentProviderEntry> ListEntriesFilter(
|
||||
std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
|
||||
std::optional<u64> title_id) const override;
|
||||
|
||||
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);
|
||||
void ProcessNSP(const VirtualFile& file);
|
||||
void ProcessXCI(const VirtualFile& file);
|
||||
|
||||
std::vector<VirtualDir> load_dirs;
|
||||
std::map<std::tuple<u64, ContentRecordType, TitleType>, VirtualFile> entries;
|
||||
std::map<u64, u32> versions;
|
||||
std::vector<ExternalUpdateEntry> multi_version_entries;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -275,6 +278,14 @@ void NSP::ReadNCAs(const std::vector<VirtualFile>& files) {
|
|||
ncas[next_nca->GetTitleId()][{cnmt.GetType(), rec.type}] =
|
||||
std::move(next_nca);
|
||||
} else {
|
||||
// fix for Bayonetta Origins in Bayonetta 3 and external content
|
||||
// where multiple update NCAs exist for the same title and type.
|
||||
auto& target_map = ncas[cnmt.GetTitleID()];
|
||||
auto existing = target_map.find({cnmt.GetType(), rec.type});
|
||||
|
||||
if (existing != target_map.end() && rec.type == ContentRecordType::Program) {
|
||||
continue;
|
||||
}
|
||||
ncas[cnmt.GetTitleID()][{cnmt.GetType(), rec.type}] = std::move(next_nca);
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -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,6 +9,7 @@
|
|||
#include "common/assert.h"
|
||||
#include "common/fs/fs.h"
|
||||
#include "common/fs/path_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/settings.h"
|
||||
#include "core/core.h"
|
||||
#include "core/file_sys/bis_factory.h"
|
||||
|
|
@ -507,6 +508,10 @@ FileSys::RegisteredCache* FileSystemController::GetSDMCContents() const {
|
|||
return sdmc_factory->GetSDMCContents();
|
||||
}
|
||||
|
||||
FileSys::ExternalContentProvider* FileSystemController::GetExternalContentProvider() const {
|
||||
return external_provider.get();
|
||||
}
|
||||
|
||||
FileSys::PlaceholderCache* FileSystemController::GetSystemNANDPlaceholder() const {
|
||||
LOG_TRACE(Service_FS, "Opening System NAND Placeholder");
|
||||
|
||||
|
|
@ -684,6 +689,7 @@ void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool ove
|
|||
if (overwrite) {
|
||||
bis_factory = nullptr;
|
||||
sdmc_factory = nullptr;
|
||||
external_provider = nullptr;
|
||||
}
|
||||
|
||||
using EdenPath = Common::FS::EdenPath;
|
||||
|
|
@ -716,6 +722,36 @@ void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool ove
|
|||
system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::SDMC,
|
||||
sdmc_factory->GetSDMCContents());
|
||||
}
|
||||
|
||||
if (external_provider == nullptr) {
|
||||
std::vector<FileSys::VirtualDir> external_dirs;
|
||||
|
||||
LOG_DEBUG(Service_FS, "Initializing ExternalContentProvider with {} configured directories",
|
||||
Settings::values.external_content_dirs.size());
|
||||
|
||||
for (const auto& dir_path : Settings::values.external_content_dirs) {
|
||||
if (!dir_path.empty()) {
|
||||
LOG_DEBUG(Service_FS, "Attempting to open directory: {}", dir_path);
|
||||
auto dir = vfs.OpenDirectory(dir_path, FileSys::OpenMode::Read);
|
||||
if (dir != nullptr) {
|
||||
external_dirs.push_back(std::move(dir));
|
||||
LOG_DEBUG(Service_FS, "Successfully opened directory: {}", dir_path);
|
||||
} else {
|
||||
LOG_ERROR(Service_FS, "Failed to open directory: {}", dir_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUG(Service_FS, "Creating ExternalContentProvider with {} opened directories",
|
||||
external_dirs.size());
|
||||
|
||||
external_provider = std::make_unique<FileSys::ExternalContentProvider>(
|
||||
std::move(external_dirs));
|
||||
system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::External,
|
||||
external_provider.get());
|
||||
|
||||
LOG_DEBUG(Service_FS, "ExternalContentProvider registered to content provider union");
|
||||
}
|
||||
}
|
||||
|
||||
void FileSystemController::Reset() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -17,6 +20,7 @@ class System;
|
|||
|
||||
namespace FileSys {
|
||||
class BISFactory;
|
||||
class ExternalContentProvider;
|
||||
class NCA;
|
||||
class RegisteredCache;
|
||||
class RegisteredCacheUnion;
|
||||
|
|
@ -117,6 +121,8 @@ public:
|
|||
|
||||
FileSys::VirtualDir GetBCATDirectory(u64 title_id) const;
|
||||
|
||||
FileSys::ExternalContentProvider* GetExternalContentProvider() const;
|
||||
|
||||
// Creates the SaveData, SDMC, and BIS Factories. Should be called once and before any function
|
||||
// above is called.
|
||||
void CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite = true);
|
||||
|
|
@ -138,6 +144,8 @@ private:
|
|||
std::unique_ptr<FileSys::SDMCFactory> sdmc_factory;
|
||||
std::unique_ptr<FileSys::BISFactory> bis_factory;
|
||||
|
||||
std::unique_ptr<FileSys::ExternalContentProvider> external_provider;
|
||||
|
||||
std::unique_ptr<FileSys::XCI> gamecard;
|
||||
std::unique_ptr<FileSys::RegisteredCache> gamecard_registered;
|
||||
std::unique_ptr<FileSys::PlaceholderCache> gamecard_placeholder;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue