diff options
17 files changed, 305 insertions, 82 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index 8cb98d6d7..1c9fb0675 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt | |||
| @@ -22,6 +22,7 @@ import org.yuzu.yuzu_emu.utils.FileUtil | |||
| 22 | import org.yuzu.yuzu_emu.utils.Log | 22 | import org.yuzu.yuzu_emu.utils.Log |
| 23 | import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable | 23 | import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable |
| 24 | import org.yuzu.yuzu_emu.model.InstallResult | 24 | import org.yuzu.yuzu_emu.model.InstallResult |
| 25 | import org.yuzu.yuzu_emu.model.Patch | ||
| 25 | 26 | ||
| 26 | /** | 27 | /** |
| 27 | * Class which contains methods that interact | 28 | * Class which contains methods that interact |
| @@ -539,9 +540,29 @@ object NativeLibrary { | |||
| 539 | * | 540 | * |
| 540 | * @param path Path to game file. Can be a [Uri]. | 541 | * @param path Path to game file. Can be a [Uri]. |
| 541 | * @param programId String representation of a game's program ID | 542 | * @param programId String representation of a game's program ID |
| 542 | * @return Array of pairs where the first value is the name of an addon and the second is the version | 543 | * @return Array of available patches |
| 543 | */ | 544 | */ |
| 544 | external fun getAddonsForFile(path: String, programId: String): Array<Pair<String, String>>? | 545 | external fun getPatchesForFile(path: String, programId: String): Array<Patch>? |
| 546 | |||
| 547 | /** | ||
| 548 | * Removes an update for a given [programId] | ||
| 549 | * @param programId String representation of a game's program ID | ||
| 550 | */ | ||
| 551 | external fun removeUpdate(programId: String) | ||
| 552 | |||
| 553 | /** | ||
| 554 | * Removes all DLC for a [programId] | ||
| 555 | * @param programId String representation of a game's program ID | ||
| 556 | */ | ||
| 557 | external fun removeDLC(programId: String) | ||
| 558 | |||
| 559 | /** | ||
| 560 | * Removes a mod installed for a given [programId] | ||
| 561 | * @param programId String representation of a game's program ID | ||
| 562 | * @param name The name of a mod as given by [getPatchesForFile]. This corresponds with the name | ||
| 563 | * of the mod's directory in a game's load folder. | ||
| 564 | */ | ||
| 565 | external fun removeMod(programId: String, name: String) | ||
| 545 | 566 | ||
| 546 | /** | 567 | /** |
| 547 | * Gets the save location for a specific game | 568 | * Gets the save location for a specific game |
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 94c151325..ff254d9b7 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 | |||
| @@ -6,27 +6,32 @@ package org.yuzu.yuzu_emu.adapters | |||
| 6 | import android.view.LayoutInflater | 6 | import android.view.LayoutInflater |
| 7 | import android.view.ViewGroup | 7 | import android.view.ViewGroup |
| 8 | import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding | 8 | import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding |
| 9 | import org.yuzu.yuzu_emu.model.Addon | 9 | import org.yuzu.yuzu_emu.model.Patch |
| 10 | import org.yuzu.yuzu_emu.model.AddonViewModel | ||
| 10 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder | 11 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder |
| 11 | 12 | ||
| 12 | class AddonAdapter : AbstractDiffAdapter<Addon, AddonAdapter.AddonViewHolder>() { | 13 | class AddonAdapter(val addonViewModel: AddonViewModel) : |
| 14 | AbstractDiffAdapter<Patch, AddonAdapter.AddonViewHolder>() { | ||
| 13 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder { | 15 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder { |
| 14 | ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false) | 16 | ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false) |
| 15 | .also { return AddonViewHolder(it) } | 17 | .also { return AddonViewHolder(it) } |
| 16 | } | 18 | } |
| 17 | 19 | ||
| 18 | inner class AddonViewHolder(val binding: ListItemAddonBinding) : | 20 | inner class AddonViewHolder(val binding: ListItemAddonBinding) : |
| 19 | AbstractViewHolder<Addon>(binding) { | 21 | AbstractViewHolder<Patch>(binding) { |
| 20 | override fun bind(model: Addon) { | 22 | override fun bind(model: Patch) { |
| 21 | binding.root.setOnClickListener { | 23 | binding.root.setOnClickListener { |
| 22 | binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked | 24 | binding.addonCheckbox.isChecked = !binding.addonCheckbox.isChecked |
| 23 | } | 25 | } |
| 24 | binding.title.text = model.title | 26 | binding.title.text = model.name |
| 25 | binding.version.text = model.version | 27 | binding.version.text = model.version |
| 26 | binding.addonSwitch.setOnCheckedChangeListener { _, checked -> | 28 | binding.addonCheckbox.setOnCheckedChangeListener { _, checked -> |
| 27 | model.enabled = checked | 29 | model.enabled = checked |
| 28 | } | 30 | } |
| 29 | binding.addonSwitch.isChecked = model.enabled | 31 | binding.addonCheckbox.isChecked = model.enabled |
| 32 | binding.buttonDelete.setOnClickListener { | ||
| 33 | addonViewModel.setAddonToDelete(model) | ||
| 34 | } | ||
| 30 | } | 35 | } |
| 31 | } | 36 | } |
| 32 | } | 37 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt index b63ece9a4..adb65812c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt | |||
| @@ -74,7 +74,7 @@ class AddonsFragment : Fragment() { | |||
| 74 | 74 | ||
| 75 | binding.listAddons.apply { | 75 | binding.listAddons.apply { |
| 76 | layoutManager = LinearLayoutManager(requireContext()) | 76 | layoutManager = LinearLayoutManager(requireContext()) |
| 77 | adapter = AddonAdapter() | 77 | adapter = AddonAdapter(addonViewModel) |
| 78 | } | 78 | } |
| 79 | 79 | ||
| 80 | viewLifecycleOwner.lifecycleScope.apply { | 80 | viewLifecycleOwner.lifecycleScope.apply { |
| @@ -110,6 +110,21 @@ class AddonsFragment : Fragment() { | |||
| 110 | } | 110 | } |
| 111 | } | 111 | } |
| 112 | } | 112 | } |
| 113 | launch { | ||
| 114 | repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
| 115 | addonViewModel.addonToDelete.collect { | ||
| 116 | if (it != null) { | ||
| 117 | MessageDialogFragment.newInstance( | ||
| 118 | requireActivity(), | ||
| 119 | titleId = R.string.confirm_uninstall, | ||
| 120 | descriptionId = R.string.confirm_uninstall_description, | ||
| 121 | positiveAction = { addonViewModel.onDeleteAddon(it) } | ||
| 122 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 123 | addonViewModel.setAddonToDelete(null) | ||
| 124 | } | ||
| 125 | } | ||
| 126 | } | ||
| 127 | } | ||
| 113 | } | 128 | } |
| 114 | 129 | ||
| 115 | binding.buttonInstall.setOnClickListener { | 130 | binding.buttonInstall.setOnClickListener { |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt deleted file mode 100644 index ed79a8b02..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt +++ /dev/null | |||
| @@ -1,10 +0,0 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.model | ||
| 5 | |||
| 6 | data class Addon( | ||
| 7 | var enabled: Boolean, | ||
| 8 | val title: String, | ||
| 9 | val version: String | ||
| 10 | ) | ||
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 075252f5b..b9c8e49ca 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 | |||
| @@ -15,8 +15,8 @@ import org.yuzu.yuzu_emu.utils.NativeConfig | |||
| 15 | import java.util.concurrent.atomic.AtomicBoolean | 15 | import java.util.concurrent.atomic.AtomicBoolean |
| 16 | 16 | ||
| 17 | class AddonViewModel : ViewModel() { | 17 | class AddonViewModel : ViewModel() { |
| 18 | private val _addonList = MutableStateFlow(mutableListOf<Addon>()) | 18 | private val _patchList = MutableStateFlow(mutableListOf<Patch>()) |
| 19 | val addonList get() = _addonList.asStateFlow() | 19 | val addonList get() = _patchList.asStateFlow() |
| 20 | 20 | ||
| 21 | private val _showModInstallPicker = MutableStateFlow(false) | 21 | private val _showModInstallPicker = MutableStateFlow(false) |
| 22 | val showModInstallPicker get() = _showModInstallPicker.asStateFlow() | 22 | val showModInstallPicker get() = _showModInstallPicker.asStateFlow() |
| @@ -24,6 +24,9 @@ class AddonViewModel : ViewModel() { | |||
| 24 | private val _showModNoticeDialog = MutableStateFlow(false) | 24 | private val _showModNoticeDialog = MutableStateFlow(false) |
| 25 | val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow() | 25 | val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow() |
| 26 | 26 | ||
| 27 | private val _addonToDelete = MutableStateFlow<Patch?>(null) | ||
| 28 | val addonToDelete = _addonToDelete.asStateFlow() | ||
| 29 | |||
| 27 | var game: Game? = null | 30 | var game: Game? = null |
| 28 | 31 | ||
| 29 | private val isRefreshing = AtomicBoolean(false) | 32 | private val isRefreshing = AtomicBoolean(false) |
| @@ -40,36 +43,47 @@ class AddonViewModel : ViewModel() { | |||
| 40 | isRefreshing.set(true) | 43 | isRefreshing.set(true) |
| 41 | viewModelScope.launch { | 44 | viewModelScope.launch { |
| 42 | withContext(Dispatchers.IO) { | 45 | withContext(Dispatchers.IO) { |
| 43 | val addonList = mutableListOf<Addon>() | 46 | val patchList = ( |
| 44 | val disabledAddons = NativeConfig.getDisabledAddons(game!!.programId) | 47 | NativeLibrary.getPatchesForFile(game!!.path, game!!.programId) |
| 45 | NativeLibrary.getAddonsForFile(game!!.path, game!!.programId)?.forEach { | 48 | ?: emptyArray() |
| 46 | val name = it.first.replace("[D] ", "") | 49 | ).toMutableList() |
| 47 | addonList.add(Addon(!disabledAddons.contains(name), name, it.second)) | 50 | patchList.sortBy { it.name } |
| 48 | } | 51 | _patchList.value = patchList |
| 49 | addonList.sortBy { it.title } | ||
| 50 | _addonList.value = addonList | ||
| 51 | isRefreshing.set(false) | 52 | isRefreshing.set(false) |
| 52 | } | 53 | } |
| 53 | } | 54 | } |
| 54 | } | 55 | } |
| 55 | 56 | ||
| 57 | fun setAddonToDelete(patch: Patch?) { | ||
| 58 | _addonToDelete.value = patch | ||
| 59 | } | ||
| 60 | |||
| 61 | fun onDeleteAddon(patch: Patch) { | ||
| 62 | when (PatchType.from(patch.type)) { | ||
| 63 | PatchType.Update -> NativeLibrary.removeUpdate(patch.programId) | ||
| 64 | PatchType.DLC -> NativeLibrary.removeDLC(patch.programId) | ||
| 65 | PatchType.Mod -> NativeLibrary.removeMod(patch.programId, patch.name) | ||
| 66 | } | ||
| 67 | refreshAddons() | ||
| 68 | } | ||
| 69 | |||
| 56 | fun onCloseAddons() { | 70 | fun onCloseAddons() { |
| 57 | if (_addonList.value.isEmpty()) { | 71 | if (_patchList.value.isEmpty()) { |
| 58 | return | 72 | return |
| 59 | } | 73 | } |
| 60 | 74 | ||
| 61 | NativeConfig.setDisabledAddons( | 75 | NativeConfig.setDisabledAddons( |
| 62 | game!!.programId, | 76 | game!!.programId, |
| 63 | _addonList.value.mapNotNull { | 77 | _patchList.value.mapNotNull { |
| 64 | if (it.enabled) { | 78 | if (it.enabled) { |
| 65 | null | 79 | null |
| 66 | } else { | 80 | } else { |
| 67 | it.title | 81 | it.name |
| 68 | } | 82 | } |
| 69 | }.toTypedArray() | 83 | }.toTypedArray() |
| 70 | ) | 84 | ) |
| 71 | NativeConfig.saveGlobalConfig() | 85 | NativeConfig.saveGlobalConfig() |
| 72 | _addonList.value.clear() | 86 | _patchList.value.clear() |
| 73 | game = null | 87 | game = null |
| 74 | } | 88 | } |
| 75 | 89 | ||
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 new file mode 100644 index 000000000..25cb9e365 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Patch.kt | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.model | ||
| 5 | |||
| 6 | import androidx.annotation.Keep | ||
| 7 | |||
| 8 | @Keep | ||
| 9 | data class Patch( | ||
| 10 | var enabled: Boolean, | ||
| 11 | val name: String, | ||
| 12 | val version: String, | ||
| 13 | val type: Int, | ||
| 14 | val programId: String, | ||
| 15 | val titleId: String | ||
| 16 | ) | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/PatchType.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/PatchType.kt new file mode 100644 index 000000000..e9a54162b --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/PatchType.kt | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.model | ||
| 5 | |||
| 6 | enum class PatchType(val int: Int) { | ||
| 7 | Update(0), | ||
| 8 | DLC(1), | ||
| 9 | Mod(2); | ||
| 10 | |||
| 11 | companion object { | ||
| 12 | fun from(int: Int): PatchType = entries.firstOrNull { it.int == int } ?: Update | ||
| 13 | } | ||
| 14 | } | ||
diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp index 19ced175f..96f2ad3d4 100644 --- a/src/android/app/src/main/jni/id_cache.cpp +++ b/src/android/app/src/main/jni/id_cache.cpp | |||
| @@ -43,6 +43,15 @@ static jfieldID s_overlay_control_data_landscape_position_field; | |||
| 43 | static jfieldID s_overlay_control_data_portrait_position_field; | 43 | static jfieldID s_overlay_control_data_portrait_position_field; |
| 44 | static jfieldID s_overlay_control_data_foldable_position_field; | 44 | static jfieldID s_overlay_control_data_foldable_position_field; |
| 45 | 45 | ||
| 46 | static jclass s_patch_class; | ||
| 47 | static jmethodID s_patch_constructor; | ||
| 48 | static jfieldID s_patch_enabled_field; | ||
| 49 | static jfieldID s_patch_name_field; | ||
| 50 | static jfieldID s_patch_version_field; | ||
| 51 | static jfieldID s_patch_type_field; | ||
| 52 | static jfieldID s_patch_program_id_field; | ||
| 53 | static jfieldID s_patch_title_id_field; | ||
| 54 | |||
| 46 | static jclass s_double_class; | 55 | static jclass s_double_class; |
| 47 | static jmethodID s_double_constructor; | 56 | static jmethodID s_double_constructor; |
| 48 | static jfieldID s_double_value_field; | 57 | static jfieldID s_double_value_field; |
| @@ -194,6 +203,38 @@ jfieldID GetOverlayControlDataFoldablePositionField() { | |||
| 194 | return s_overlay_control_data_foldable_position_field; | 203 | return s_overlay_control_data_foldable_position_field; |
| 195 | } | 204 | } |
| 196 | 205 | ||
| 206 | jclass GetPatchClass() { | ||
| 207 | return s_patch_class; | ||
| 208 | } | ||
| 209 | |||
| 210 | jmethodID GetPatchConstructor() { | ||
| 211 | return s_patch_constructor; | ||
| 212 | } | ||
| 213 | |||
| 214 | jfieldID GetPatchEnabledField() { | ||
| 215 | return s_patch_enabled_field; | ||
| 216 | } | ||
| 217 | |||
| 218 | jfieldID GetPatchNameField() { | ||
| 219 | return s_patch_name_field; | ||
| 220 | } | ||
| 221 | |||
| 222 | jfieldID GetPatchVersionField() { | ||
| 223 | return s_patch_version_field; | ||
| 224 | } | ||
| 225 | |||
| 226 | jfieldID GetPatchTypeField() { | ||
| 227 | return s_patch_type_field; | ||
| 228 | } | ||
| 229 | |||
| 230 | jfieldID GetPatchProgramIdField() { | ||
| 231 | return s_patch_program_id_field; | ||
| 232 | } | ||
| 233 | |||
| 234 | jfieldID GetPatchTitleIdField() { | ||
| 235 | return s_patch_title_id_field; | ||
| 236 | } | ||
| 237 | |||
| 197 | jclass GetDoubleClass() { | 238 | jclass GetDoubleClass() { |
| 198 | return s_double_class; | 239 | return s_double_class; |
| 199 | } | 240 | } |
| @@ -310,6 +351,19 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { | |||
| 310 | env->GetFieldID(overlay_control_data_class, "foldablePosition", "Lkotlin/Pair;"); | 351 | env->GetFieldID(overlay_control_data_class, "foldablePosition", "Lkotlin/Pair;"); |
| 311 | env->DeleteLocalRef(overlay_control_data_class); | 352 | env->DeleteLocalRef(overlay_control_data_class); |
| 312 | 353 | ||
| 354 | const jclass patch_class = env->FindClass("org/yuzu/yuzu_emu/model/Patch"); | ||
| 355 | s_patch_class = reinterpret_cast<jclass>(env->NewGlobalRef(patch_class)); | ||
| 356 | s_patch_constructor = env->GetMethodID( | ||
| 357 | patch_class, "<init>", | ||
| 358 | "(ZLjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)V"); | ||
| 359 | s_patch_enabled_field = env->GetFieldID(patch_class, "enabled", "Z"); | ||
| 360 | s_patch_name_field = env->GetFieldID(patch_class, "name", "Ljava/lang/String;"); | ||
| 361 | s_patch_version_field = env->GetFieldID(patch_class, "version", "Ljava/lang/String;"); | ||
| 362 | s_patch_type_field = env->GetFieldID(patch_class, "type", "I"); | ||
| 363 | s_patch_program_id_field = env->GetFieldID(patch_class, "programId", "Ljava/lang/String;"); | ||
| 364 | s_patch_title_id_field = env->GetFieldID(patch_class, "titleId", "Ljava/lang/String;"); | ||
| 365 | env->DeleteLocalRef(patch_class); | ||
| 366 | |||
| 313 | const jclass double_class = env->FindClass("java/lang/Double"); | 367 | const jclass double_class = env->FindClass("java/lang/Double"); |
| 314 | s_double_class = reinterpret_cast<jclass>(env->NewGlobalRef(double_class)); | 368 | s_double_class = reinterpret_cast<jclass>(env->NewGlobalRef(double_class)); |
| 315 | s_double_constructor = env->GetMethodID(double_class, "<init>", "(D)V"); | 369 | s_double_constructor = env->GetMethodID(double_class, "<init>", "(D)V"); |
| @@ -353,6 +407,7 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) { | |||
| 353 | env->DeleteGlobalRef(s_string_class); | 407 | env->DeleteGlobalRef(s_string_class); |
| 354 | env->DeleteGlobalRef(s_pair_class); | 408 | env->DeleteGlobalRef(s_pair_class); |
| 355 | env->DeleteGlobalRef(s_overlay_control_data_class); | 409 | env->DeleteGlobalRef(s_overlay_control_data_class); |
| 410 | env->DeleteGlobalRef(s_patch_class); | ||
| 356 | env->DeleteGlobalRef(s_double_class); | 411 | env->DeleteGlobalRef(s_double_class); |
| 357 | env->DeleteGlobalRef(s_integer_class); | 412 | env->DeleteGlobalRef(s_integer_class); |
| 358 | env->DeleteGlobalRef(s_boolean_class); | 413 | env->DeleteGlobalRef(s_boolean_class); |
diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h index 0e5267b73..a002e705d 100644 --- a/src/android/app/src/main/jni/id_cache.h +++ b/src/android/app/src/main/jni/id_cache.h | |||
| @@ -43,6 +43,15 @@ jfieldID GetOverlayControlDataLandscapePositionField(); | |||
| 43 | jfieldID GetOverlayControlDataPortraitPositionField(); | 43 | jfieldID GetOverlayControlDataPortraitPositionField(); |
| 44 | jfieldID GetOverlayControlDataFoldablePositionField(); | 44 | jfieldID GetOverlayControlDataFoldablePositionField(); |
| 45 | 45 | ||
| 46 | jclass GetPatchClass(); | ||
| 47 | jmethodID GetPatchConstructor(); | ||
| 48 | jfieldID GetPatchEnabledField(); | ||
| 49 | jfieldID GetPatchNameField(); | ||
| 50 | jfieldID GetPatchVersionField(); | ||
| 51 | jfieldID GetPatchTypeField(); | ||
| 52 | jfieldID GetPatchProgramIdField(); | ||
| 53 | jfieldID GetPatchTitleIdField(); | ||
| 54 | |||
| 46 | jclass GetDoubleClass(); | 55 | jclass GetDoubleClass(); |
| 47 | jmethodID GetDoubleConstructor(); | 56 | jmethodID GetDoubleConstructor(); |
| 48 | jfieldID GetDoubleValueField(); | 57 | jfieldID GetDoubleValueField(); |
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index b8fef5c6f..be0a723b1 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp | |||
| @@ -774,9 +774,9 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env, | |||
| 774 | return true; | 774 | return true; |
| 775 | } | 775 | } |
| 776 | 776 | ||
| 777 | jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, jobject jobj, | 777 | jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env, jobject jobj, |
| 778 | jstring jpath, | 778 | jstring jpath, |
| 779 | jstring jprogramId) { | 779 | jstring jprogramId) { |
| 780 | const auto path = GetJString(env, jpath); | 780 | const auto path = GetJString(env, jpath); |
| 781 | const auto vFile = | 781 | const auto vFile = |
| 782 | Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path); | 782 | Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path); |
| @@ -793,20 +793,40 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, | |||
| 793 | FileSys::VirtualFile update_raw; | 793 | FileSys::VirtualFile update_raw; |
| 794 | loader->ReadUpdateRaw(update_raw); | 794 | loader->ReadUpdateRaw(update_raw); |
| 795 | 795 | ||
| 796 | auto addons = pm.GetPatchVersionNames(update_raw); | 796 | auto patches = pm.GetPatches(update_raw); |
| 797 | auto jemptyString = ToJString(env, ""); | 797 | jobjectArray jpatchArray = |
| 798 | auto jemptyStringPair = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(), | 798 | env->NewObjectArray(patches.size(), IDCache::GetPatchClass(), nullptr); |
| 799 | jemptyString, jemptyString); | ||
| 800 | jobjectArray jaddonsArray = | ||
| 801 | env->NewObjectArray(addons.size(), IDCache::GetPairClass(), jemptyStringPair); | ||
| 802 | int i = 0; | 799 | int i = 0; |
| 803 | for (const auto& addon : addons) { | 800 | for (const auto& patch : patches) { |
| 804 | jobject jaddon = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(), | 801 | jobject jpatch = env->NewObject( |
| 805 | ToJString(env, addon.first), ToJString(env, addon.second)); | 802 | IDCache::GetPatchClass(), IDCache::GetPatchConstructor(), patch.enabled, |
| 806 | env->SetObjectArrayElement(jaddonsArray, i, jaddon); | 803 | ToJString(env, patch.name), ToJString(env, patch.version), |
| 804 | static_cast<jint>(patch.type), ToJString(env, std::to_string(patch.program_id)), | ||
| 805 | ToJString(env, std::to_string(patch.title_id))); | ||
| 806 | env->SetObjectArrayElement(jpatchArray, i, jpatch); | ||
| 807 | ++i; | 807 | ++i; |
| 808 | } | 808 | } |
| 809 | return jaddonsArray; | 809 | return jpatchArray; |
| 810 | } | ||
| 811 | |||
| 812 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeUpdate(JNIEnv* env, jobject jobj, | ||
| 813 | jstring jprogramId) { | ||
| 814 | auto program_id = EmulationSession::GetProgramId(env, jprogramId); | ||
| 815 | ContentManager::RemoveUpdate(EmulationSession::GetInstance().System().GetFileSystemController(), | ||
| 816 | program_id); | ||
| 817 | } | ||
| 818 | |||
| 819 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeDLC(JNIEnv* env, jobject jobj, | ||
| 820 | jstring jprogramId) { | ||
| 821 | auto program_id = EmulationSession::GetProgramId(env, jprogramId); | ||
| 822 | ContentManager::RemoveAllDLC(&EmulationSession::GetInstance().System(), program_id); | ||
| 823 | } | ||
| 824 | |||
| 825 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeMod(JNIEnv* env, jobject jobj, jstring jprogramId, | ||
| 826 | jstring jname) { | ||
| 827 | auto program_id = EmulationSession::GetProgramId(env, jprogramId); | ||
| 828 | ContentManager::RemoveMod(EmulationSession::GetInstance().System().GetFileSystemController(), | ||
| 829 | program_id, GetJString(env, jname)); | ||
| 810 | } | 830 | } |
| 811 | 831 | ||
| 812 | jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj, | 832 | jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj, |
diff --git a/src/android/app/src/main/res/layout/list_item_addon.xml b/src/android/app/src/main/res/layout/list_item_addon.xml index 74ca04ef1..3a1382fe2 100644 --- a/src/android/app/src/main/res/layout/list_item_addon.xml +++ b/src/android/app/src/main/res/layout/list_item_addon.xml | |||
| @@ -14,12 +14,11 @@ | |||
| 14 | android:id="@+id/text_container" | 14 | android:id="@+id/text_container" |
| 15 | android:layout_width="0dp" | 15 | android:layout_width="0dp" |
| 16 | android:layout_height="wrap_content" | 16 | android:layout_height="wrap_content" |
| 17 | android:layout_marginEnd="16dp" | ||
| 18 | android:orientation="vertical" | 17 | android:orientation="vertical" |
| 19 | app:layout_constraintBottom_toBottomOf="@+id/addon_switch" | 18 | android:layout_marginEnd="16dp" |
| 20 | app:layout_constraintEnd_toStartOf="@+id/addon_switch" | 19 | app:layout_constraintEnd_toStartOf="@+id/addon_checkbox" |
| 21 | app:layout_constraintStart_toStartOf="parent" | 20 | app:layout_constraintStart_toStartOf="parent" |
| 22 | app:layout_constraintTop_toTopOf="@+id/addon_switch"> | 21 | app:layout_constraintTop_toTopOf="parent"> |
| 23 | 22 | ||
| 24 | <com.google.android.material.textview.MaterialTextView | 23 | <com.google.android.material.textview.MaterialTextView |
| 25 | android:id="@+id/title" | 24 | android:id="@+id/title" |
| @@ -42,16 +41,29 @@ | |||
| 42 | 41 | ||
| 43 | </LinearLayout> | 42 | </LinearLayout> |
| 44 | 43 | ||
| 45 | <com.google.android.material.materialswitch.MaterialSwitch | 44 | <com.google.android.material.checkbox.MaterialCheckBox |
| 46 | android:id="@+id/addon_switch" | 45 | android:id="@+id/addon_checkbox" |
| 47 | android:layout_width="wrap_content" | 46 | android:layout_width="wrap_content" |
| 48 | android:layout_height="wrap_content" | 47 | android:layout_height="wrap_content" |
| 49 | android:focusable="true" | 48 | android:focusable="true" |
| 50 | android:gravity="center" | 49 | android:gravity="center" |
| 51 | android:nextFocusLeft="@id/addon_container" | 50 | android:layout_marginEnd="8dp" |
| 52 | app:layout_constraintBottom_toBottomOf="parent" | 51 | app:layout_constraintTop_toTopOf="@+id/text_container" |
| 52 | app:layout_constraintBottom_toBottomOf="@+id/text_container" | ||
| 53 | app:layout_constraintEnd_toStartOf="@+id/button_delete" /> | ||
| 54 | |||
| 55 | <Button | ||
| 56 | android:id="@+id/button_delete" | ||
| 57 | style="@style/Widget.Material3.Button.IconButton" | ||
| 58 | android:layout_width="wrap_content" | ||
| 59 | android:layout_height="wrap_content" | ||
| 60 | android:layout_gravity="center_vertical" | ||
| 61 | android:contentDescription="@string/delete" | ||
| 62 | android:tooltipText="@string/delete" | ||
| 63 | app:icon="@drawable/ic_delete" | ||
| 64 | app:iconTint="?attr/colorControlNormal" | ||
| 53 | app:layout_constraintEnd_toEndOf="parent" | 65 | app:layout_constraintEnd_toEndOf="parent" |
| 54 | app:layout_constraintStart_toEndOf="@id/text_container" | 66 | app:layout_constraintTop_toTopOf="@+id/addon_checkbox" |
| 55 | app:layout_constraintTop_toTopOf="parent" /> | 67 | app:layout_constraintBottom_toBottomOf="@+id/addon_checkbox" /> |
| 56 | 68 | ||
| 57 | </androidx.constraintlayout.widget.ConstraintLayout> | 69 | </androidx.constraintlayout.widget.ConstraintLayout> |
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 547752bda..db5b27d38 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml | |||
| @@ -286,6 +286,7 @@ | |||
| 286 | <string name="custom">Custom</string> | 286 | <string name="custom">Custom</string> |
| 287 | <string name="notice">Notice</string> | 287 | <string name="notice">Notice</string> |
| 288 | <string name="import_complete">Import complete</string> | 288 | <string name="import_complete">Import complete</string> |
| 289 | <string name="more_options">More options</string> | ||
| 289 | 290 | ||
| 290 | <!-- GPU driver installation --> | 291 | <!-- GPU driver installation --> |
| 291 | <string name="select_gpu_driver">Select GPU driver</string> | 292 | <string name="select_gpu_driver">Select GPU driver</string> |
| @@ -348,6 +349,8 @@ | |||
| 348 | <string name="verifying_content">Verifying content…</string> | 349 | <string name="verifying_content">Verifying content…</string> |
| 349 | <string name="content_install_notice">Content install notice</string> | 350 | <string name="content_install_notice">Content install notice</string> |
| 350 | <string name="content_install_notice_description">The content that you selected does not match this game.\nInstall anyway?</string> | 351 | <string name="content_install_notice_description">The content that you selected does not match this game.\nInstall anyway?</string> |
| 352 | <string name="confirm_uninstall">Confirm uninstall</string> | ||
| 353 | <string name="confirm_uninstall_description">Are you sure you want to uninstall this addon?</string> | ||
| 351 | 354 | ||
| 352 | <!-- ROM loading errors --> | 355 | <!-- ROM loading errors --> |
| 353 | <string name="loader_error_encrypted">Your ROM is encrypted</string> | 356 | <string name="loader_error_encrypted">Your ROM is encrypted</string> |
diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp index 4a3dbc6a3..612122224 100644 --- a/src/core/file_sys/patch_manager.cpp +++ b/src/core/file_sys/patch_manager.cpp | |||
| @@ -466,12 +466,12 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs | |||
| 466 | return romfs; | 466 | return romfs; |
| 467 | } | 467 | } |
| 468 | 468 | ||
| 469 | PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile update_raw) const { | 469 | std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const { |
| 470 | if (title_id == 0) { | 470 | if (title_id == 0) { |
| 471 | return {}; | 471 | return {}; |
| 472 | } | 472 | } |
| 473 | 473 | ||
| 474 | std::map<std::string, std::string, std::less<>> out; | 474 | std::vector<Patch> out; |
| 475 | const auto& disabled = Settings::values.disabled_addons[title_id]; | 475 | const auto& disabled = Settings::values.disabled_addons[title_id]; |
| 476 | 476 | ||
| 477 | // Game Updates | 477 | // Game Updates |
| @@ -482,20 +482,28 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u | |||
| 482 | 482 | ||
| 483 | const auto update_disabled = | 483 | const auto update_disabled = |
| 484 | std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); | 484 | std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); |
| 485 | const auto update_label = update_disabled ? "[D] Update" : "Update"; | 485 | Patch update_patch = {.enabled = !update_disabled, |
| 486 | .name = "Update", | ||
| 487 | .version = "", | ||
| 488 | .type = PatchType::Update, | ||
| 489 | .program_id = title_id, | ||
| 490 | .title_id = title_id}; | ||
| 486 | 491 | ||
| 487 | if (nacp != nullptr) { | 492 | if (nacp != nullptr) { |
| 488 | out.insert_or_assign(update_label, nacp->GetVersionString()); | 493 | update_patch.version = nacp->GetVersionString(); |
| 494 | out.push_back(update_patch); | ||
| 489 | } else { | 495 | } else { |
| 490 | if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) { | 496 | if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) { |
| 491 | const auto meta_ver = content_provider.GetEntryVersion(update_tid); | 497 | const auto meta_ver = content_provider.GetEntryVersion(update_tid); |
| 492 | if (meta_ver.value_or(0) == 0) { | 498 | if (meta_ver.value_or(0) == 0) { |
| 493 | out.insert_or_assign(update_label, ""); | 499 | out.push_back(update_patch); |
| 494 | } else { | 500 | } else { |
| 495 | out.insert_or_assign(update_label, FormatTitleVersion(*meta_ver)); | 501 | update_patch.version = FormatTitleVersion(*meta_ver); |
| 502 | out.push_back(update_patch); | ||
| 496 | } | 503 | } |
| 497 | } else if (update_raw != nullptr) { | 504 | } else if (update_raw != nullptr) { |
| 498 | out.insert_or_assign(update_label, "PACKED"); | 505 | update_patch.version = "PACKED"; |
| 506 | out.push_back(update_patch); | ||
| 499 | } | 507 | } |
| 500 | } | 508 | } |
| 501 | 509 | ||
| @@ -539,7 +547,12 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u | |||
| 539 | 547 | ||
| 540 | const auto mod_disabled = | 548 | const auto mod_disabled = |
| 541 | std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end(); | 549 | std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end(); |
| 542 | out.insert_or_assign(mod_disabled ? "[D] " + mod->GetName() : mod->GetName(), types); | 550 | out.push_back({.enabled = !mod_disabled, |
| 551 | .name = mod->GetName(), | ||
| 552 | .version = types, | ||
| 553 | .type = PatchType::Mod, | ||
| 554 | .program_id = title_id, | ||
| 555 | .title_id = title_id}); | ||
| 543 | } | 556 | } |
| 544 | } | 557 | } |
| 545 | 558 | ||
| @@ -557,7 +570,12 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u | |||
| 557 | if (!types.empty()) { | 570 | if (!types.empty()) { |
| 558 | const auto mod_disabled = | 571 | const auto mod_disabled = |
| 559 | std::find(disabled.begin(), disabled.end(), "SDMC") != disabled.end(); | 572 | std::find(disabled.begin(), disabled.end(), "SDMC") != disabled.end(); |
| 560 | out.insert_or_assign(mod_disabled ? "[D] SDMC" : "SDMC", types); | 573 | out.push_back({.enabled = !mod_disabled, |
| 574 | .name = "SDMC", | ||
| 575 | .version = types, | ||
| 576 | .type = PatchType::Mod, | ||
| 577 | .program_id = title_id, | ||
| 578 | .title_id = title_id}); | ||
| 561 | } | 579 | } |
| 562 | } | 580 | } |
| 563 | 581 | ||
| @@ -584,7 +602,12 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u | |||
| 584 | 602 | ||
| 585 | const auto dlc_disabled = | 603 | const auto dlc_disabled = |
| 586 | std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end(); | 604 | std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end(); |
| 587 | out.insert_or_assign(dlc_disabled ? "[D] DLC" : "DLC", std::move(list)); | 605 | out.push_back({.enabled = !dlc_disabled, |
| 606 | .name = "DLC", | ||
| 607 | .version = std::move(list), | ||
| 608 | .type = PatchType::DLC, | ||
| 609 | .program_id = title_id, | ||
| 610 | .title_id = dlc_match.back().title_id}); | ||
| 588 | } | 611 | } |
| 589 | 612 | ||
| 590 | return out; | 613 | return out; |
diff --git a/src/core/file_sys/patch_manager.h b/src/core/file_sys/patch_manager.h index 03e9c7301..2601b8217 100644 --- a/src/core/file_sys/patch_manager.h +++ b/src/core/file_sys/patch_manager.h | |||
| @@ -26,12 +26,22 @@ class ContentProvider; | |||
| 26 | class NCA; | 26 | class NCA; |
| 27 | class NACP; | 27 | class NACP; |
| 28 | 28 | ||
| 29 | enum class PatchType { Update, DLC, Mod }; | ||
| 30 | |||
| 31 | struct Patch { | ||
| 32 | bool enabled; | ||
| 33 | std::string name; | ||
| 34 | std::string version; | ||
| 35 | PatchType type; | ||
| 36 | u64 program_id; | ||
| 37 | u64 title_id; | ||
| 38 | }; | ||
| 39 | |||
| 29 | // A centralized class to manage patches to games. | 40 | // A centralized class to manage patches to games. |
| 30 | class PatchManager { | 41 | class PatchManager { |
| 31 | public: | 42 | public: |
| 32 | using BuildID = std::array<u8, 0x20>; | 43 | using BuildID = std::array<u8, 0x20>; |
| 33 | using Metadata = std::pair<std::unique_ptr<NACP>, VirtualFile>; | 44 | using Metadata = std::pair<std::unique_ptr<NACP>, VirtualFile>; |
| 34 | using PatchVersionNames = std::map<std::string, std::string, std::less<>>; | ||
| 35 | 45 | ||
| 36 | explicit PatchManager(u64 title_id_, | 46 | explicit PatchManager(u64 title_id_, |
| 37 | const Service::FileSystem::FileSystemController& fs_controller_, | 47 | const Service::FileSystem::FileSystemController& fs_controller_, |
| @@ -66,9 +76,8 @@ public: | |||
| 66 | VirtualFile packed_update_raw = nullptr, | 76 | VirtualFile packed_update_raw = nullptr, |
| 67 | bool apply_layeredfs = true) const; | 77 | bool apply_layeredfs = true) const; |
| 68 | 78 | ||
| 69 | // Returns a vector of pairs between patch names and patch versions. | 79 | // Returns a vector of patches |
| 70 | // i.e. Update 3.2.2 will return {"Update", "3.2.2"} | 80 | [[nodiscard]] std::vector<Patch> GetPatches(VirtualFile update_raw = nullptr) const; |
| 71 | [[nodiscard]] PatchVersionNames GetPatchVersionNames(VirtualFile update_raw = nullptr) const; | ||
| 72 | 81 | ||
| 73 | // If the game update exists, returns the u32 version field in its Meta-type NCA. If that fails, | 82 | // If the game update exists, returns the u32 version field in its Meta-type NCA. If that fails, |
| 74 | // it will fallback to the Meta-type NCA of the base game. If that fails, the result will be | 83 | // it will fallback to the Meta-type NCA of the base game. If that fails, the result will be |
diff --git a/src/frontend_common/content_manager.h b/src/frontend_common/content_manager.h index 8e55f4ca0..248ce573e 100644 --- a/src/frontend_common/content_manager.h +++ b/src/frontend_common/content_manager.h | |||
| @@ -65,6 +65,23 @@ inline bool RemoveBaseContent(const Service::FileSystem::FileSystemController& f | |||
| 65 | fs_controller.GetSDMCContents()->RemoveExistingEntry(program_id); | 65 | fs_controller.GetSDMCContents()->RemoveExistingEntry(program_id); |
| 66 | } | 66 | } |
| 67 | 67 | ||
| 68 | inline bool RemoveMod(const Service::FileSystem::FileSystemController& fs_controller, | ||
| 69 | const u64 program_id, const std::string& mod_name) { | ||
| 70 | // Check general Mods (LayeredFS and IPS) | ||
| 71 | const auto mod_dir = fs_controller.GetModificationLoadRoot(program_id); | ||
| 72 | if (mod_dir != nullptr) { | ||
| 73 | return mod_dir->DeleteSubdirectoryRecursive(mod_name); | ||
| 74 | } | ||
| 75 | |||
| 76 | // Check SDMC mod directory (RomFS LayeredFS) | ||
| 77 | const auto sdmc_mod_dir = fs_controller.GetSDMCModificationLoadRoot(program_id); | ||
| 78 | if (sdmc_mod_dir != nullptr) { | ||
| 79 | return sdmc_mod_dir->DeleteSubdirectoryRecursive(mod_name); | ||
| 80 | } | ||
| 81 | |||
| 82 | return false; | ||
| 83 | } | ||
| 84 | |||
| 68 | inline InstallResult InstallNSP( | 85 | inline InstallResult InstallNSP( |
| 69 | Core::System* system, FileSys::VfsFilesystem* vfs, const std::string& filename, | 86 | Core::System* system, FileSys::VfsFilesystem* vfs, const std::string& filename, |
| 70 | const std::function<bool(size_t, size_t)>& callback = std::function<bool(size_t, size_t)>()) { | 87 | const std::function<bool(size_t, size_t)>& callback = std::function<bool(size_t, size_t)>()) { |
diff --git a/src/yuzu/configuration/configure_per_game_addons.cpp b/src/yuzu/configuration/configure_per_game_addons.cpp index 140a7fe5d..568775027 100644 --- a/src/yuzu/configuration/configure_per_game_addons.cpp +++ b/src/yuzu/configuration/configure_per_game_addons.cpp | |||
| @@ -122,9 +122,8 @@ void ConfigurePerGameAddons::LoadConfiguration() { | |||
| 122 | 122 | ||
| 123 | const auto& disabled = Settings::values.disabled_addons[title_id]; | 123 | const auto& disabled = Settings::values.disabled_addons[title_id]; |
| 124 | 124 | ||
| 125 | for (const auto& patch : pm.GetPatchVersionNames(update_raw)) { | 125 | for (const auto& patch : pm.GetPatches(update_raw)) { |
| 126 | const auto name = | 126 | const auto name = QString::fromStdString(patch.name); |
| 127 | QString::fromStdString(patch.first).replace(QStringLiteral("[D] "), QString{}); | ||
| 128 | 127 | ||
| 129 | auto* const first_item = new QStandardItem; | 128 | auto* const first_item = new QStandardItem; |
| 130 | first_item->setText(name); | 129 | first_item->setText(name); |
| @@ -136,7 +135,7 @@ void ConfigurePerGameAddons::LoadConfiguration() { | |||
| 136 | first_item->setCheckState(patch_disabled ? Qt::Unchecked : Qt::Checked); | 135 | first_item->setCheckState(patch_disabled ? Qt::Unchecked : Qt::Checked); |
| 137 | 136 | ||
| 138 | list_items.push_back(QList<QStandardItem*>{ | 137 | list_items.push_back(QList<QStandardItem*>{ |
| 139 | first_item, new QStandardItem{QString::fromStdString(patch.second)}}); | 138 | first_item, new QStandardItem{QString::fromStdString(patch.version)}}); |
| 140 | item_model->appendRow(list_items.back()); | 139 | item_model->appendRow(list_items.back()); |
| 141 | } | 140 | } |
| 142 | 141 | ||
diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp index dc006832e..9747e3fb3 100644 --- a/src/yuzu/game_list_worker.cpp +++ b/src/yuzu/game_list_worker.cpp | |||
| @@ -164,18 +164,19 @@ QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager, | |||
| 164 | QString out; | 164 | QString out; |
| 165 | FileSys::VirtualFile update_raw; | 165 | FileSys::VirtualFile update_raw; |
| 166 | loader.ReadUpdateRaw(update_raw); | 166 | loader.ReadUpdateRaw(update_raw); |
| 167 | for (const auto& kv : patch_manager.GetPatchVersionNames(update_raw)) { | 167 | for (const auto& patch : patch_manager.GetPatches(update_raw)) { |
| 168 | const bool is_update = kv.first == "Update" || kv.first == "[D] Update"; | 168 | const bool is_update = patch.name == "Update"; |
| 169 | if (!updatable && is_update) { | 169 | if (!updatable && is_update) { |
| 170 | continue; | 170 | continue; |
| 171 | } | 171 | } |
| 172 | 172 | ||
| 173 | const QString type = QString::fromStdString(kv.first); | 173 | const QString type = |
| 174 | QString::fromStdString(patch.enabled ? patch.name : "[D] " + patch.name); | ||
| 174 | 175 | ||
| 175 | if (kv.second.empty()) { | 176 | if (patch.version.empty()) { |
| 176 | out.append(QStringLiteral("%1\n").arg(type)); | 177 | out.append(QStringLiteral("%1\n").arg(type)); |
| 177 | } else { | 178 | } else { |
| 178 | auto ver = kv.second; | 179 | auto ver = patch.version; |
| 179 | 180 | ||
| 180 | // Display container name for packed updates | 181 | // Display container name for packed updates |
| 181 | if (is_update && ver == "PACKED") { | 182 | if (is_update && ver == "PACKED") { |