[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 <crueter@eden-emu.dev>
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Reviewed-by: DraVee <chimera@dravee.dev>
Co-authored-by: xbzk <xbzk@eden-emu.dev>
Co-committed-by: xbzk <xbzk@eden-emu.dev>
This commit is contained in:
xbzk 2026-03-09 00:30:10 +01:00 committed by crueter
parent f5e2b1fb13
commit a1b50e9339
No known key found for this signature in database
GPG key ID: 425ACD2D4830EBC6
6 changed files with 134 additions and 48 deletions

View file

@ -40,11 +40,21 @@ class AddonAdapter(val addonViewModel: AddonViewModel) :
} }
} }
val deleteAction = { val canDelete = model.isRemovable
addonViewModel.setAddonToDelete(model) 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() }
} }
} }
} }

View file

@ -16,5 +16,17 @@ data class Patch(
val type: Int, val type: Int,
val programId: String, val programId: String,
val titleId: 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
}

View file

@ -1407,7 +1407,7 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env
Common::Android::ToJString(env, patch.version), static_cast<jint>(patch.type), Common::Android::ToJString(env, patch.version), static_cast<jint>(patch.type),
Common::Android::ToJString(env, std::to_string(patch.program_id)), 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<jlong>(patch.numeric_version)); static_cast<jlong>(patch.numeric_version), static_cast<jint>(patch.source));
env->SetObjectArrayElement(jpatchArray, i, jpatch); env->SetObjectArrayElement(jpatchArray, i, jpatch);
++i; ++i;
} }

View file

@ -516,7 +516,7 @@ namespace Common::Android {
s_patch_class = reinterpret_cast<jclass>(env->NewGlobalRef(patch_class)); s_patch_class = reinterpret_cast<jclass>(env->NewGlobalRef(patch_class));
s_patch_constructor = env->GetMethodID( s_patch_constructor = env->GetMethodID(
patch_class, "<init>", patch_class, "<init>",
"(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_enabled_field = env->GetFieldID(patch_class, "enabled", "Z");
s_patch_name_field = env->GetFieldID(patch_class, "name", "Ljava/lang/String;"); s_patch_name_field = env->GetFieldID(patch_class, "name", "Ljava/lang/String;");
s_patch_version_field = env->GetFieldID(patch_class, "version", "Ljava/lang/String;"); s_patch_version_field = env->GetFieldID(patch_class, "version", "Ljava/lang/String;");

View file

@ -123,6 +123,39 @@ bool IsVersionedExternalUpdateDisabled(const std::vector<std::string>& disabled,
return std::find(disabled.cbegin(), disabled.cend(), disabled_key) != disabled.cend() || return std::find(disabled.cbegin(), disabled.cend(), disabled_key) != disabled.cend() ||
std::find(disabled.cbegin(), disabled.cend(), "Update") != 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 } // Anonymous namespace
PatchManager::PatchManager(u64 title_id_, PatchManager::PatchManager(u64 title_id_,
@ -771,6 +804,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
std::nullopt, std::nullopt, ContentRecordType::Program, update_tid); std::nullopt, std::nullopt, ContentRecordType::Program, update_tid);
for (const auto& [slot, entry] : all_updates) { for (const auto& [slot, entry] : all_updates) {
(void)entry;
if (slot == ContentProviderUnionSlot::External || if (slot == ContentProviderUnionSlot::External ||
slot == ContentProviderUnionSlot::FrontendManual) { slot == ContentProviderUnionSlot::FrontendManual) {
continue; continue;
@ -786,7 +820,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
source_suffix = " (NAND)"; source_suffix = " (NAND)";
break; break;
case ContentProviderUnionSlot::SDMC: case ContentProviderUnionSlot::SDMC:
source_type = PatchSource::NAND; source_type = PatchSource::SDMC;
source_suffix = " (SDMC)"; source_suffix = " (SDMC)";
break; break;
default: default:
@ -795,19 +829,16 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
std::string version_str; std::string version_str;
u32 numeric_ver = 0; u32 numeric_ver = 0;
PatchManager update{update_tid, fs_controller, content_provider}; const auto* slot_provider = content_union->GetSlotProvider(slot);
const auto metadata = update.GetControlMetadata(); version_str = GetUpdateVersionStringFromSlot(slot_provider, update_tid);
const auto& nacp = metadata.first;
if (nacp != nullptr) { if (slot_provider != nullptr) {
version_str = nacp->GetVersionString(); const auto slot_ver = slot_provider->GetEntryVersion(update_tid);
} if (slot_ver.has_value()) {
numeric_ver = *slot_ver;
const auto meta_ver = content_provider.GetEntryVersion(update_tid); if (version_str.empty() && numeric_ver != 0) {
if (meta_ver.has_value()) { version_str = FormatTitleVersion(numeric_ver);
numeric_ver = *meta_ver; }
if (version_str.empty() && numeric_ver != 0) {
version_str = FormatTitleVersion(numeric_ver);
} }
} }
@ -956,37 +987,60 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
} }
// DLC // DLC
const auto dlc_entries =
content_provider.ListEntriesFilter(TitleType::AOC, ContentRecordType::Data);
std::vector<ContentProviderEntry> dlc_match; std::vector<ContentProviderEntry> dlc_match;
dlc_match.reserve(dlc_entries.size()); bool has_external_dlc = false;
std::copy_if(dlc_entries.begin(), dlc_entries.end(), std::back_inserter(dlc_match), bool has_nand_dlc = false;
[this](const ContentProviderEntry& entry) { bool has_sdmc_dlc = false;
const auto base_tid = GetBaseTitleID(entry.title_id); bool has_other_dlc = false;
const bool matches_base = base_tid == title_id; const auto dlc_entries_with_origin =
content_union->ListEntriesFilterOrigin(std::nullopt, TitleType::AOC, ContentRecordType::Data);
if (!matches_base) { dlc_match.reserve(dlc_entries_with_origin.size());
LOG_DEBUG(Loader, "DLC {:016X} base {:016X} doesn't match title {:016X}", for (const auto& [slot, entry] : dlc_entries_with_origin) {
entry.title_id, base_tid, title_id); const auto base_tid = GetBaseTitleID(entry.title_id);
return false; 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;
}
auto nca = content_provider.GetEntry(entry); const auto* slot_provider = content_union->GetSlotProvider(slot);
if (!nca) { if (slot_provider == nullptr) {
LOG_DEBUG(Loader, "Failed to get NCA for DLC {:016X}", entry.title_id); continue;
return false; }
}
const auto status = nca->GetStatus(); auto nca = slot_provider->GetEntry(entry);
if (status != Loader::ResultStatus::Success) { if (!nca) {
LOG_DEBUG(Loader, "DLC {:016X} NCA has status {}", LOG_DEBUG(Loader, "Failed to get NCA for DLC {:016X}", entry.title_id);
entry.title_id, static_cast<int>(status)); continue;
return false; }
}
return true; 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));
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()) { if (!dlc_match.empty()) {
// Ensure sorted so DLC IDs show in order. // Ensure sorted so DLC IDs show in order.
@ -1000,13 +1054,22 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
const auto dlc_disabled = const auto dlc_disabled =
std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end(); 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, out.push_back({.enabled = !dlc_disabled,
.name = "DLC", .name = "DLC",
.version = std::move(list), .version = std::move(list),
.type = PatchType::DLC, .type = PatchType::DLC,
.program_id = title_id, .program_id = title_id,
.title_id = dlc_match.back().title_id, .title_id = dlc_match.back().title_id,
.source = PatchSource::Unknown}); .source = dlc_source});
} }
return out; return out;

View file

@ -34,6 +34,7 @@ enum class PatchType { Update, DLC, Mod };
enum class PatchSource { enum class PatchSource {
Unknown, Unknown,
NAND, NAND,
SDMC,
External, External,
Packed, Packed,
}; };