From a1b50e9339fc4ad003ea55de6fdaabf92a12e163 Mon Sep 17 00:00:00 2001 From: xbzk Date: Mon, 9 Mar 2026 00:30:10 +0100 Subject: [PATCH] [android] patches bin button + version bug fixes (#3691) This fixed the delete button enabled for external content (which is auto handled and the proper way to get rid of them is either by removing its folder from ext content list, or removing the file itself) by streaming patch source thru jni. Along the way stumbled upon another bug: If you have an external content update installed (say latest version for example) and you NAND install a previous update (like in silksong's hard mode update), the newest update version string would leak to the previous one. Did videos for both. Fixed both. Seems good to go. Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3691 Reviewed-by: crueter Reviewed-by: Lizzie Reviewed-by: DraVee Co-authored-by: xbzk Co-committed-by: xbzk --- .../yuzu/yuzu_emu/adapters/AddonAdapter.kt | 18 ++- .../java/org/yuzu/yuzu_emu/model/Patch.kt | 16 +- src/android/app/src/main/jni/native.cpp | 2 +- src/common/android/id_cache.cpp | 2 +- src/core/file_sys/patch_manager.cpp | 153 ++++++++++++------ src/core/file_sys/patch_manager.h | 1 + 6 files changed, 139 insertions(+), 53 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 371fef0025..cbca66e13a 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 @@ -40,11 +40,21 @@ class AddonAdapter(val addonViewModel: AddonViewModel) : } } - val deleteAction = { - addonViewModel.setAddonToDelete(model) + val canDelete = model.isRemovable + binding.deleteCard.isEnabled = canDelete + binding.buttonDelete.isEnabled = canDelete + binding.deleteCard.alpha = if (canDelete) 1f else 0.38f + + if (canDelete) { + val deleteAction = { + addonViewModel.setAddonToDelete(model) + } + binding.deleteCard.setOnClickListener { deleteAction() } + binding.buttonDelete.setOnClickListener { deleteAction() } + } else { + binding.deleteCard.setOnClickListener(null) + binding.buttonDelete.setOnClickListener(null) } - binding.deleteCard.setOnClickListener { deleteAction() } - binding.buttonDelete.setOnClickListener { deleteAction() } } } } 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 8a367116c1..a3785dd3ac 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 @@ -16,5 +16,17 @@ data class Patch( val type: Int, val programId: String, val titleId: String, - val numericVersion: Long = 0 -) + val numericVersion: Long = 0, + val source: Int = 0 +) { + companion object { + const val SOURCE_UNKNOWN = 0 + const val SOURCE_NAND = 1 + const val SOURCE_SDMC = 2 + const val SOURCE_EXTERNAL = 3 + const val SOURCE_PACKED = 4 + } + + val isRemovable: Boolean + get() = source != SOURCE_EXTERNAL && source != SOURCE_PACKED +} diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 3f0029c78a..2108e05911 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -1407,7 +1407,7 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env 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)), - static_cast(patch.numeric_version)); + static_cast(patch.numeric_version), static_cast(patch.source)); env->SetObjectArrayElement(jpatchArray, i, jpatch); ++i; } diff --git a/src/common/android/id_cache.cpp b/src/common/android/id_cache.cpp index c7f5332a68..76af1e0fb2 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;J)V"); + "(ZLjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;JI)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 e9c3bb75c2..627646ee84 100644 --- a/src/core/file_sys/patch_manager.cpp +++ b/src/core/file_sys/patch_manager.cpp @@ -123,6 +123,39 @@ bool IsVersionedExternalUpdateDisabled(const std::vector& disabled, return std::find(disabled.cbegin(), disabled.cend(), disabled_key) != disabled.cend() || std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); } + +std::string GetUpdateVersionStringFromSlot(const ContentProvider* provider, u64 update_tid) { + if (provider == nullptr) { + return {}; + } + + auto control_nca = provider->GetEntry(update_tid, ContentRecordType::Control); + if (control_nca == nullptr || + control_nca->GetStatus() != Loader::ResultStatus::Success) { + return {}; + } + + const auto romfs = control_nca->GetRomFS(); + if (romfs == nullptr) { + return {}; + } + + const auto extracted = ExtractRomFS(romfs); + if (extracted == nullptr) { + return {}; + } + + auto nacp_file = extracted->GetFile("control.nacp"); + if (nacp_file == nullptr) { + nacp_file = extracted->GetFile("Control.nacp"); + } + if (nacp_file == nullptr) { + return {}; + } + + NACP nacp{nacp_file}; + return nacp.GetVersionString(); +} } // Anonymous namespace PatchManager::PatchManager(u64 title_id_, @@ -771,6 +804,7 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { std::nullopt, std::nullopt, ContentRecordType::Program, update_tid); for (const auto& [slot, entry] : all_updates) { + (void)entry; if (slot == ContentProviderUnionSlot::External || slot == ContentProviderUnionSlot::FrontendManual) { continue; @@ -786,7 +820,7 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { source_suffix = " (NAND)"; break; case ContentProviderUnionSlot::SDMC: - source_type = PatchSource::NAND; + source_type = PatchSource::SDMC; source_suffix = " (SDMC)"; break; default: @@ -795,19 +829,16 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { 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.has_value()) { - numeric_ver = *meta_ver; - if (version_str.empty() && numeric_ver != 0) { - version_str = FormatTitleVersion(numeric_ver); + const auto* slot_provider = content_union->GetSlotProvider(slot); + version_str = GetUpdateVersionStringFromSlot(slot_provider, update_tid); + + if (slot_provider != nullptr) { + const auto slot_ver = slot_provider->GetEntryVersion(update_tid); + if (slot_ver.has_value()) { + numeric_ver = *slot_ver; + if (version_str.empty() && numeric_ver != 0) { + version_str = FormatTitleVersion(numeric_ver); + } } } @@ -956,37 +987,60 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { } // DLC - const auto dlc_entries = - content_provider.ListEntriesFilter(TitleType::AOC, ContentRecordType::Data); - std::vector 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) { - 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(status)); - return false; - } - - return true; - }); + bool has_external_dlc = false; + bool has_nand_dlc = false; + bool has_sdmc_dlc = false; + bool has_other_dlc = false; + const auto dlc_entries_with_origin = + content_union->ListEntriesFilterOrigin(std::nullopt, TitleType::AOC, ContentRecordType::Data); + + dlc_match.reserve(dlc_entries_with_origin.size()); + for (const auto& [slot, entry] : dlc_entries_with_origin) { + 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); + continue; + } + + const auto* slot_provider = content_union->GetSlotProvider(slot); + if (slot_provider == nullptr) { + continue; + } + + auto nca = slot_provider->GetEntry(entry); + if (!nca) { + LOG_DEBUG(Loader, "Failed to get NCA for DLC {:016X}", entry.title_id); + continue; + } + + const auto status = nca->GetStatus(); + if (status != Loader::ResultStatus::Success) { + LOG_DEBUG(Loader, "DLC {:016X} NCA has status {}", entry.title_id, + static_cast(status)); + continue; + } + + switch (slot) { + case ContentProviderUnionSlot::External: + case ContentProviderUnionSlot::FrontendManual: + has_external_dlc = true; + break; + case ContentProviderUnionSlot::UserNAND: + case ContentProviderUnionSlot::SysNAND: + has_nand_dlc = true; + break; + case ContentProviderUnionSlot::SDMC: + has_sdmc_dlc = true; + break; + default: + has_other_dlc = true; + break; + } + dlc_match.push_back(entry); + } if (!dlc_match.empty()) { // Ensure sorted so DLC IDs show in order. @@ -1000,13 +1054,22 @@ std::vector PatchManager::GetPatches(VirtualFile update_raw) const { const auto dlc_disabled = std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end(); + PatchSource dlc_source = PatchSource::Unknown; + if (has_external_dlc && !has_nand_dlc && !has_sdmc_dlc && !has_other_dlc) { + dlc_source = PatchSource::External; + } else if (has_nand_dlc && !has_external_dlc && !has_sdmc_dlc && !has_other_dlc) { + dlc_source = PatchSource::NAND; + } else if (has_sdmc_dlc && !has_external_dlc && !has_nand_dlc && !has_other_dlc) { + dlc_source = PatchSource::SDMC; + } + out.push_back({.enabled = !dlc_disabled, .name = "DLC", .version = std::move(list), .type = PatchType::DLC, .program_id = title_id, .title_id = dlc_match.back().title_id, - .source = PatchSource::Unknown}); + .source = dlc_source}); } return out; diff --git a/src/core/file_sys/patch_manager.h b/src/core/file_sys/patch_manager.h index 2be963078d..755d924fd4 100644 --- a/src/core/file_sys/patch_manager.h +++ b/src/core/file_sys/patch_manager.h @@ -34,6 +34,7 @@ enum class PatchType { Update, DLC, Mod }; enum class PatchSource { Unknown, NAND, + SDMC, External, Packed, };