From 4d2572da2a8e2d6e9066599939b5ccf8e0464f8f Mon Sep 17 00:00:00 2001 From: Maufeat Date: Wed, 4 Feb 2026 18:04:58 +0100 Subject: [PATCH] multi version support for android (and fix bug of not selecting the right one and saving) --- .../yuzu/yuzu_emu/adapters/AddonAdapter.kt | 8 +- .../org/yuzu/yuzu_emu/model/AddonViewModel.kt | 69 ++++++++- .../org/yuzu/yuzu_emu/model/GamesViewModel.kt | 3 + .../java/org/yuzu/yuzu_emu/model/Patch.kt | 3 +- .../app/src/main/jni/android_config.cpp | 21 +++ src/android/app/src/main/jni/native.cpp | 87 ++++++++++- src/common/android/id_cache.cpp | 2 +- src/core/file_sys/patch_manager.cpp | 139 +++++++++++++++++- src/core/file_sys/registered_cache.cpp | 92 ++++++++++++ src/core/file_sys/registered_cache.h | 8 + 10 files changed, 418 insertions(+), 14 deletions(-) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt index 2be91ba46a..00a6ad5136 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt @@ -10,6 +10,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding import org.yuzu.yuzu_emu.model.Patch +import org.yuzu.yuzu_emu.model.PatchType import org.yuzu.yuzu_emu.model.AddonViewModel import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder @@ -31,7 +32,12 @@ class AddonAdapter(val addonViewModel: AddonViewModel) : binding.addonSwitch.isChecked = model.enabled binding.addonSwitch.setOnCheckedChangeListener { _, checked -> - model.enabled = checked + if (PatchType.from(model.type) == PatchType.Update && checked) { + addonViewModel.enableOnlyThisUpdate(model) + notifyDataSetChanged() + } else { + model.enabled = checked + } } val deleteAction = { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt index b9c8e49ca4..cfc3c62930 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt @@ -48,16 +48,74 @@ class AddonViewModel : ViewModel() { ?: emptyArray() ).toMutableList() patchList.sortBy { it.name } + + // Ensure only one update is enabled + ensureSingleUpdateEnabled(patchList) + + removeDuplicates(patchList) + _patchList.value = patchList isRefreshing.set(false) } } } + private fun ensureSingleUpdateEnabled(patchList: MutableList) { + val updates = patchList.filter { PatchType.from(it.type) == PatchType.Update } + if (updates.size <= 1) { + return + } + + val enabledUpdates = updates.filter { it.enabled } + + if (enabledUpdates.size > 1) { + var foundFirst = false + for (patch in patchList) { + if (PatchType.from(patch.type) == PatchType.Update) { + if (!foundFirst && patch.enabled) { + foundFirst = true + } else if (foundFirst && patch.enabled) { + patch.enabled = false + } + } + } + } else if (enabledUpdates.isEmpty()) { + for (patch in patchList) { + if (PatchType.from(patch.type) == PatchType.Update) { + patch.enabled = true + break + } + } + } + } + + private fun removeDuplicates(patchList: MutableList) { + val seen = mutableSetOf() + val iterator = patchList.iterator() + while (iterator.hasNext()) { + val patch = iterator.next() + val key = "${patch.name}|${patch.version}|${patch.type}" + if (seen.contains(key)) { + iterator.remove() + } else { + seen.add(key) + } + } + } + fun setAddonToDelete(patch: Patch?) { _addonToDelete.value = patch } + fun enableOnlyThisUpdate(selectedPatch: Patch) { + val currentList = _patchList.value + for (patch in currentList) { + if (PatchType.from(patch.type) == PatchType.Update) { + patch.enabled = (patch === selectedPatch) + } + } + } + fun onDeleteAddon(patch: Patch) { when (PatchType.from(patch.type)) { PatchType.Update -> NativeLibrary.removeUpdate(patch.programId) @@ -72,13 +130,22 @@ class AddonViewModel : ViewModel() { return } + // Check if there are multiple update versions + val updates = _patchList.value.filter { PatchType.from(it.type) == PatchType.Update } + val hasMultipleUpdates = updates.size > 1 + NativeConfig.setDisabledAddons( game!!.programId, _patchList.value.mapNotNull { if (it.enabled) { null } else { - it.name + // For multiple updates, use "Update@{numericVersion}" as the key (like desktop) + if (hasMultipleUpdates && PatchType.from(it.type) == PatchType.Update) { + "Update@${it.numericVersion}" + } else { + it.name + } } }.toTypedArray() ) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt index 0564a64afe..0f09dad48f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt @@ -153,6 +153,7 @@ class GamesViewModel : ViewModel() { } DirectoryType.EXTERNAL_CONTENT -> { addExternalContentDir(gameDir.uriString) + NativeConfig.saveGlobalConfig() getGameDirsAndExternalContent() } } @@ -230,6 +231,7 @@ class GamesViewModel : ViewModel() { if (!currentDirs.contains(path)) { currentDirs.add(path) NativeConfig.setExternalContentDirs(currentDirs.toTypedArray()) + NativeConfig.saveGlobalConfig() } } @@ -237,5 +239,6 @@ class GamesViewModel : ViewModel() { val currentDirs = NativeConfig.getExternalContentDirs().toMutableList() currentDirs.remove(path) NativeConfig.setExternalContentDirs(currentDirs.toTypedArray()) + NativeConfig.saveGlobalConfig() } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Patch.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Patch.kt index 25cb9e3654..6cf05d2e98 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Patch.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Patch.kt @@ -12,5 +12,6 @@ data class Patch( val version: String, val type: Int, val programId: String, - val titleId: String + val titleId: String, + val numericVersion: Long = 0 ) diff --git a/src/android/app/src/main/jni/android_config.cpp b/src/android/app/src/main/jni/android_config.cpp index 7345a1893f..73ce57039b 100644 --- a/src/android/app/src/main/jni/android_config.cpp +++ b/src/android/app/src/main/jni/android_config.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "android_config.h" #include "android_settings.h" @@ -69,6 +70,18 @@ void AndroidConfig::ReadPathValues() { } EndArray(); + // Read external content directories + Settings::values.external_content_dirs.clear(); + const int external_dirs_size = BeginArray(std::string("external_content_dirs")); + for (int i = 0; i < external_dirs_size; ++i) { + SetArrayIndex(i); + std::string dir_path = ReadStringSetting(std::string("path")); + if (!dir_path.empty()) { + Settings::values.external_content_dirs.push_back(dir_path); + } + } + EndArray(); + const auto nand_dir_setting = ReadStringSetting(std::string("nand_directory")); if (!nand_dir_setting.empty()) { Common::FS::SetEdenPath(Common::FS::EdenPath::NANDDir, nand_dir_setting); @@ -241,6 +254,14 @@ void AndroidConfig::SavePathValues() { } EndArray(); + // Save external content directories + BeginArray(std::string("external_content_dirs")); + for (size_t i = 0; i < Settings::values.external_content_dirs.size(); ++i) { + SetArrayIndex(i); + WriteStringSetting(std::string("path"), Settings::values.external_content_dirs[i]); + } + EndArray(); + // Save custom NAND directory const auto nand_path = Common::FS::GetEdenPathString(Common::FS::EdenPath::NANDDir); WriteStringSetting(std::string("nand_directory"), nand_path, diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 9f86d9d99f..e9ccbb6018 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -54,7 +54,10 @@ #include "core/crypto/key_manager.h" #include "core/file_sys/card_image.h" #include "core/file_sys/content_archive.h" +#include "core/file_sys/control_metadata.h" #include "core/file_sys/fs_filesystem.h" +#include "core/file_sys/nca_metadata.h" +#include "core/file_sys/romfs.h" #include "core/file_sys/submission_package.h" #include "core/file_sys/vfs/vfs.h" #include "core/file_sys/vfs/vfs_real.h" @@ -215,12 +218,81 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath) if (extension == "nsp") { auto nsp = std::make_shared(file); if (nsp->GetStatus() == Loader::ResultStatus::Success) { - for (const auto& title : nsp->GetNCAs()) { - for (const auto& entry : title.second) { - m_manual_provider->AddEntry(entry.first.first, entry.first.second, title.first, - entry.second->GetBaseFile()); - LOG_DEBUG(Frontend, "Added NSP entry - TitleID: {:016X}, TitleType: {}, ContentType: {}", - title.first, static_cast(entry.first.first), static_cast(entry.first.second)); + std::map nsp_versions; + std::map nsp_version_strings; + + for (const auto& [title_id, nca_map] : nsp->GetNCAs()) { + for (const auto& [type_pair, nca] : nca_map) { + const auto& [title_type, content_type] = type_pair; + + if (content_type == FileSys::ContentRecordType::Meta) { + const auto meta_nca = std::make_shared(nca->GetBaseFile()); + if (meta_nca->GetStatus() == Loader::ResultStatus::Success) { + const auto section0 = meta_nca->GetSubdirectories(); + if (!section0.empty()) { + for (const auto& meta_file : section0[0]->GetFiles()) { + if (meta_file->GetExtension() == "cnmt") { + FileSys::CNMT cnmt(meta_file); + nsp_versions[cnmt.GetTitleID()] = cnmt.GetTitleVersion(); + } + } + } + } + } + + if (content_type == FileSys::ContentRecordType::Control && + title_type == FileSys::TitleType::Update) { + auto romfs = nca->GetRomFS(); + if (romfs) { + auto extracted = FileSys::ExtractRomFS(romfs); + if (extracted) { + auto nacp_file = extracted->GetFile("control.nacp"); + if (!nacp_file) { + nacp_file = extracted->GetFile("Control.nacp"); + } + if (nacp_file) { + FileSys::NACP nacp(nacp_file); + auto ver_str = nacp.GetVersionString(); + if (!ver_str.empty()) { + nsp_version_strings[title_id] = ver_str; + } + } + } + } + } + } + } + + for (const auto& [title_id, nca_map] : nsp->GetNCAs()) { + for (const auto& [type_pair, nca] : nca_map) { + const auto& [title_type, content_type] = type_pair; + + if (title_type == FileSys::TitleType::Update) { + u32 version = 0; + auto ver_it = nsp_versions.find(title_id); + if (ver_it != nsp_versions.end()) { + version = ver_it->second; + } + + std::string version_string; + auto str_it = nsp_version_strings.find(title_id); + if (str_it != nsp_version_strings.end()) { + version_string = str_it->second; + } + + m_manual_provider->AddEntryWithVersion( + title_type, content_type, title_id, version, version_string, + nca->GetBaseFile()); + + LOG_DEBUG(Frontend, "Added NSP update entry - TitleID: {:016X}, Version: {}, VersionStr: {}", + title_id, version, version_string); + } else { + // Use regular AddEntry for non-updates + m_manual_provider->AddEntry(title_type, content_type, title_id, + nca->GetBaseFile()); + LOG_DEBUG(Frontend, "Added NSP entry - TitleID: {:016X}, TitleType: {}, ContentType: {}", + title_id, static_cast(title_type), static_cast(content_type)); + } } } return; @@ -1354,7 +1426,8 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env Common::Android::ToJString(env, patch.name), Common::Android::ToJString(env, patch.version), static_cast(patch.type), Common::Android::ToJString(env, std::to_string(patch.program_id)), - Common::Android::ToJString(env, std::to_string(patch.title_id))); + Common::Android::ToJString(env, std::to_string(patch.title_id)), + static_cast(patch.numeric_version)); env->SetObjectArrayElement(jpatchArray, i, jpatch); ++i; } diff --git a/src/common/android/id_cache.cpp b/src/common/android/id_cache.cpp index eb43f4e213..fcaa306fb3 100644 --- a/src/common/android/id_cache.cpp +++ b/src/common/android/id_cache.cpp @@ -516,7 +516,7 @@ namespace Common::Android { s_patch_class = reinterpret_cast(env->NewGlobalRef(patch_class)); s_patch_constructor = env->GetMethodID( patch_class, "", - "(ZLjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)V"); + "(ZLjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;J)V"); s_patch_enabled_field = env->GetFieldID(patch_class, "enabled", "Z"); s_patch_name_field = env->GetFieldID(patch_class, "name", "Ljava/lang/String;"); s_patch_version_field = env->GetFieldID(patch_class, "version", "Ljava/lang/String;"); diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp index 52d4b6db0b..dea736fd36 100644 --- a/src/core/file_sys/patch_manager.cpp +++ b/src/core/file_sys/patch_manager.cpp @@ -141,11 +141,13 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const { bool update_disabled = true; std::optional enabled_version; bool checked_external = false; + bool checked_manual = false; const auto* content_union = dynamic_cast(&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); @@ -168,11 +170,38 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const { } } } + + // Also check ManualContentProvider (for Android) + if (!checked_external) { + const auto* manual_provider = dynamic_cast( + content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual)); + if (manual_provider) { + const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid); + + if (manual_update_versions.size() > 1) { + 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; + } + } + } else if (manual_update_versions.size() == 1) { + checked_manual = true; + if (std::find(disabled.cbegin(), disabled.cend(), "Update") == disabled.cend()) { + update_disabled = false; + enabled_version = manual_update_versions[0].version; + } + } + } + } } // check for original NAND style // BUT only if we didn't check external provider (to avoid loading wrong update) - if (!checked_external && update_disabled) { + if (!checked_external && !checked_manual && update_disabled) { if (std::find(disabled.cbegin(), disabled.cend(), "Update") == disabled.cend()) { update_disabled = false; } @@ -196,10 +225,22 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const { update = std::make_unique(file); } } + + // Also try ManualContentProvider + if (update == nullptr) { + const auto* manual_provider = dynamic_cast( + 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(file); + } + } + } } // Fallback to regular content provider - but only if we didn't check external - if (update == nullptr && !checked_external) { + if (update == nullptr && !checked_external && !checked_manual) { update = content_provider.GetEntry(update_tid, ContentRecordType::Program); } @@ -512,9 +553,11 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs std::optional enabled_version; VirtualFile update_raw = nullptr; bool checked_external = false; + bool checked_manual = false; const auto* content_union = dynamic_cast(&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); @@ -539,9 +582,37 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs } } } + + if (!checked_external) { + const auto* manual_provider = dynamic_cast( + content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual)); + if (manual_provider) { + const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid); + + if (manual_update_versions.size() > 1) { + 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; + } + } + } else if (manual_update_versions.size() == 1) { + checked_manual = true; + if (std::find(disabled.cbegin(), disabled.cend(), "Update") == disabled.cend()) { + update_disabled = false; + enabled_version = manual_update_versions[0].version; + update_raw = manual_provider->GetEntryForVersion(update_tid, type, manual_update_versions[0].version); + } + } + } + } } - if (!checked_external && update_disabled) { + if (!checked_external && !checked_manual && update_disabled) { if (std::find(disabled.cbegin(), disabled.cend(), "Update") == disabled.cend() || std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") == disabled.cend() || std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") == disabled.cend()) { @@ -592,6 +663,7 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { const auto* content_union = dynamic_cast(&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); @@ -652,6 +724,67 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { } } + const auto* manual_provider = dynamic_cast( + content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual)); + if (manual_provider && out.empty()) { + const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid); + + if (manual_update_versions.size() > 1) { + 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 patch_name = "Update"; + + 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 = patch_name, + .version = version_str, + .type = PatchType::Update, + .program_id = title_id, + .title_id = update_tid, + .source = PatchSource::External, + .numeric_version = update_entry.version}; + + out.push_back(update_patch); + } + } else if (manual_update_versions.size() == 1) { + const auto& update_entry = manual_update_versions[0]; + + std::string version_str = update_entry.version_string; + + if (version_str.empty()) { + const auto metadata = GetControlMetadata(); + if (metadata.first) { + version_str = metadata.first->GetVersionString(); + } + } + + if (version_str.empty()) { + version_str = FormatTitleVersion(update_entry.version); + } + + const auto update_disabled = + std::find(disabled.cbegin(), disabled.cend(), "Update") != 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}; + + out.push_back(update_patch); + } + } + const auto all_updates = content_union->ListEntriesFilterOrigin( std::nullopt, std::nullopt, ContentRecordType::Program, update_tid); diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp index d4481f67db..42ec878436 100644 --- a/src/core/file_sys/registered_cache.cpp +++ b/src/core/file_sys/registered_cache.cpp @@ -985,6 +985,14 @@ const ExternalContentProvider* ContentProviderUnion::GetExternalProvider() const 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, @@ -992,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() {} @@ -1048,6 +1099,47 @@ std::vector ManualContentProvider::ListEntriesFilter( return out; } +std::vector ManualContentProvider::ListUpdateVersions(u64 title_id) const { + std::vector 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 load_directories) : load_dirs(std::move(load_directories)) { ExternalContentProvider::Refresh(); diff --git a/src/core/file_sys/registered_cache.h b/src/core/file_sys/registered_cache.h index b820fad8d9..04e231f453 100644 --- a/src/core/file_sys/registered_cache.h +++ b/src/core/file_sys/registered_cache.h @@ -241,6 +241,7 @@ public: std::optional title_id) const override; const ExternalContentProvider* GetExternalProvider() const; + const ContentProvider* GetSlotProvider(ContentProviderUnionSlot slot) const; std::vector> ListEntriesFilterOrigin( std::optional origin = {}, @@ -260,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; @@ -272,8 +275,13 @@ public: std::optional title_type, std::optional record_type, std::optional title_id) const override; + std::vector 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, VirtualFile> entries; + std::vector multi_version_entries; }; class ExternalContentProvider : public ContentProvider {