android: Add addon delete button
Required some refactoring of retrieving patches in order for the frontend to pass the right information to ContentManager for deletion.
This commit is contained in:
		| @@ -22,6 +22,7 @@ import org.yuzu.yuzu_emu.utils.FileUtil | ||||
| import org.yuzu.yuzu_emu.utils.Log | ||||
| import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable | ||||
| import org.yuzu.yuzu_emu.model.InstallResult | ||||
| import org.yuzu.yuzu_emu.model.Patch | ||||
|  | ||||
| /** | ||||
|  * Class which contains methods that interact | ||||
| @@ -539,9 +540,29 @@ object NativeLibrary { | ||||
|      * | ||||
|      * @param path Path to game file. Can be a [Uri]. | ||||
|      * @param programId String representation of a game's program ID | ||||
|      * @return Array of pairs where the first value is the name of an addon and the second is the version | ||||
|      * @return Array of available patches | ||||
|      */ | ||||
|     external fun getAddonsForFile(path: String, programId: String): Array<Pair<String, String>>? | ||||
|     external fun getPatchesForFile(path: String, programId: String): Array<Patch>? | ||||
|  | ||||
|     /** | ||||
|      * Removes an update for a given [programId] | ||||
|      * @param programId String representation of a game's program ID | ||||
|      */ | ||||
|     external fun removeUpdate(programId: String) | ||||
|  | ||||
|     /** | ||||
|      * Removes all DLC for a  [programId] | ||||
|      * @param programId String representation of a game's program ID | ||||
|      */ | ||||
|     external fun removeDLC(programId: String) | ||||
|  | ||||
|     /** | ||||
|      * Removes a mod installed for a given [programId] | ||||
|      * @param programId String representation of a game's program ID | ||||
|      * @param name The name of a mod as given by [getPatchesForFile]. This corresponds with the name | ||||
|      * of the mod's directory in a game's load folder. | ||||
|      */ | ||||
|     external fun removeMod(programId: String, name: String) | ||||
|  | ||||
|     /** | ||||
|      * Gets the save location for a specific game | ||||
|   | ||||
| @@ -6,27 +6,32 @@ package org.yuzu.yuzu_emu.adapters | ||||
| import android.view.LayoutInflater | ||||
| import android.view.ViewGroup | ||||
| import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding | ||||
| import org.yuzu.yuzu_emu.model.Addon | ||||
| import org.yuzu.yuzu_emu.model.Patch | ||||
| import org.yuzu.yuzu_emu.model.AddonViewModel | ||||
| import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder | ||||
|  | ||||
| class AddonAdapter : AbstractDiffAdapter<Addon, AddonAdapter.AddonViewHolder>() { | ||||
| class AddonAdapter(val addonViewModel: AddonViewModel) : | ||||
|     AbstractDiffAdapter<Patch, AddonAdapter.AddonViewHolder>() { | ||||
|     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder { | ||||
|         ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false) | ||||
|             .also { return AddonViewHolder(it) } | ||||
|     } | ||||
|  | ||||
|     inner class AddonViewHolder(val binding: ListItemAddonBinding) : | ||||
|         AbstractViewHolder<Addon>(binding) { | ||||
|         override fun bind(model: Addon) { | ||||
|         AbstractViewHolder<Patch>(binding) { | ||||
|         override fun bind(model: Patch) { | ||||
|             binding.root.setOnClickListener { | ||||
|                 binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked | ||||
|                 binding.addonCheckbox.isChecked = !binding.addonCheckbox.isChecked | ||||
|             } | ||||
|             binding.title.text = model.title | ||||
|             binding.title.text = model.name | ||||
|             binding.version.text = model.version | ||||
|             binding.addonSwitch.setOnCheckedChangeListener { _, checked -> | ||||
|             binding.addonCheckbox.setOnCheckedChangeListener { _, checked -> | ||||
|                 model.enabled = checked | ||||
|             } | ||||
|             binding.addonSwitch.isChecked = model.enabled | ||||
|             binding.addonCheckbox.isChecked = model.enabled | ||||
|             binding.buttonDelete.setOnClickListener { | ||||
|                 addonViewModel.setAddonToDelete(model) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -74,7 +74,7 @@ class AddonsFragment : Fragment() { | ||||
|  | ||||
|         binding.listAddons.apply { | ||||
|             layoutManager = LinearLayoutManager(requireContext()) | ||||
|             adapter = AddonAdapter() | ||||
|             adapter = AddonAdapter(addonViewModel) | ||||
|         } | ||||
|  | ||||
|         viewLifecycleOwner.lifecycleScope.apply { | ||||
| @@ -110,6 +110,21 @@ class AddonsFragment : Fragment() { | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             launch { | ||||
|                 repeatOnLifecycle(Lifecycle.State.STARTED) { | ||||
|                     addonViewModel.addonToDelete.collect { | ||||
|                         if (it != null) { | ||||
|                             MessageDialogFragment.newInstance( | ||||
|                                 requireActivity(), | ||||
|                                 titleId = R.string.confirm_uninstall, | ||||
|                                 descriptionId = R.string.confirm_uninstall_description, | ||||
|                                 positiveAction = { addonViewModel.onDeleteAddon(it) } | ||||
|                             ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||||
|                             addonViewModel.setAddonToDelete(null) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         binding.buttonInstall.setOnClickListener { | ||||
|   | ||||
| @@ -1,10 +0,0 @@ | ||||
| // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||||
|  | ||||
| package org.yuzu.yuzu_emu.model | ||||
|  | ||||
| data class Addon( | ||||
|     var enabled: Boolean, | ||||
|     val title: String, | ||||
|     val version: String | ||||
| ) | ||||
| @@ -15,8 +15,8 @@ import org.yuzu.yuzu_emu.utils.NativeConfig | ||||
| import java.util.concurrent.atomic.AtomicBoolean | ||||
|  | ||||
| class AddonViewModel : ViewModel() { | ||||
|     private val _addonList = MutableStateFlow(mutableListOf<Addon>()) | ||||
|     val addonList get() = _addonList.asStateFlow() | ||||
|     private val _patchList = MutableStateFlow(mutableListOf<Patch>()) | ||||
|     val addonList get() = _patchList.asStateFlow() | ||||
|  | ||||
|     private val _showModInstallPicker = MutableStateFlow(false) | ||||
|     val showModInstallPicker get() = _showModInstallPicker.asStateFlow() | ||||
| @@ -24,6 +24,9 @@ class AddonViewModel : ViewModel() { | ||||
|     private val _showModNoticeDialog = MutableStateFlow(false) | ||||
|     val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow() | ||||
|  | ||||
|     private val _addonToDelete = MutableStateFlow<Patch?>(null) | ||||
|     val addonToDelete = _addonToDelete.asStateFlow() | ||||
|  | ||||
|     var game: Game? = null | ||||
|  | ||||
|     private val isRefreshing = AtomicBoolean(false) | ||||
| @@ -40,36 +43,47 @@ class AddonViewModel : ViewModel() { | ||||
|         isRefreshing.set(true) | ||||
|         viewModelScope.launch { | ||||
|             withContext(Dispatchers.IO) { | ||||
|                 val addonList = mutableListOf<Addon>() | ||||
|                 val disabledAddons = NativeConfig.getDisabledAddons(game!!.programId) | ||||
|                 NativeLibrary.getAddonsForFile(game!!.path, game!!.programId)?.forEach { | ||||
|                     val name = it.first.replace("[D] ", "") | ||||
|                     addonList.add(Addon(!disabledAddons.contains(name), name, it.second)) | ||||
|                 } | ||||
|                 addonList.sortBy { it.title } | ||||
|                 _addonList.value = addonList | ||||
|                 val patchList = ( | ||||
|                     NativeLibrary.getPatchesForFile(game!!.path, game!!.programId) | ||||
|                         ?: emptyArray() | ||||
|                     ).toMutableList() | ||||
|                 patchList.sortBy { it.name } | ||||
|                 _patchList.value = patchList | ||||
|                 isRefreshing.set(false) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun setAddonToDelete(patch: Patch?) { | ||||
|         _addonToDelete.value = patch | ||||
|     } | ||||
|  | ||||
|     fun onDeleteAddon(patch: Patch) { | ||||
|         when (PatchType.from(patch.type)) { | ||||
|             PatchType.Update -> NativeLibrary.removeUpdate(patch.programId) | ||||
|             PatchType.DLC -> NativeLibrary.removeDLC(patch.programId) | ||||
|             PatchType.Mod -> NativeLibrary.removeMod(patch.programId, patch.name) | ||||
|         } | ||||
|         refreshAddons() | ||||
|     } | ||||
|  | ||||
|     fun onCloseAddons() { | ||||
|         if (_addonList.value.isEmpty()) { | ||||
|         if (_patchList.value.isEmpty()) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         NativeConfig.setDisabledAddons( | ||||
|             game!!.programId, | ||||
|             _addonList.value.mapNotNull { | ||||
|             _patchList.value.mapNotNull { | ||||
|                 if (it.enabled) { | ||||
|                     null | ||||
|                 } else { | ||||
|                     it.title | ||||
|                     it.name | ||||
|                 } | ||||
|             }.toTypedArray() | ||||
|         ) | ||||
|         NativeConfig.saveGlobalConfig() | ||||
|         _addonList.value.clear() | ||||
|         _patchList.value.clear() | ||||
|         game = null | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,16 @@ | ||||
| // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||||
|  | ||||
| package org.yuzu.yuzu_emu.model | ||||
|  | ||||
| import androidx.annotation.Keep | ||||
|  | ||||
| @Keep | ||||
| data class Patch( | ||||
|     var enabled: Boolean, | ||||
|     val name: String, | ||||
|     val version: String, | ||||
|     val type: Int, | ||||
|     val programId: String, | ||||
|     val titleId: String | ||||
| ) | ||||
| @@ -0,0 +1,14 @@ | ||||
| // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||||
|  | ||||
| package org.yuzu.yuzu_emu.model | ||||
|  | ||||
| enum class PatchType(val int: Int) { | ||||
|     Update(0), | ||||
|     DLC(1), | ||||
|     Mod(2); | ||||
|  | ||||
|     companion object { | ||||
|         fun from(int: Int): PatchType = entries.firstOrNull { it.int == int } ?: Update | ||||
|     } | ||||
| } | ||||
| @@ -43,6 +43,15 @@ static jfieldID s_overlay_control_data_landscape_position_field; | ||||
| static jfieldID s_overlay_control_data_portrait_position_field; | ||||
| static jfieldID s_overlay_control_data_foldable_position_field; | ||||
|  | ||||
| static jclass s_patch_class; | ||||
| static jmethodID s_patch_constructor; | ||||
| static jfieldID s_patch_enabled_field; | ||||
| static jfieldID s_patch_name_field; | ||||
| static jfieldID s_patch_version_field; | ||||
| static jfieldID s_patch_type_field; | ||||
| static jfieldID s_patch_program_id_field; | ||||
| static jfieldID s_patch_title_id_field; | ||||
|  | ||||
| static jclass s_double_class; | ||||
| static jmethodID s_double_constructor; | ||||
| static jfieldID s_double_value_field; | ||||
| @@ -194,6 +203,38 @@ jfieldID GetOverlayControlDataFoldablePositionField() { | ||||
|     return s_overlay_control_data_foldable_position_field; | ||||
| } | ||||
|  | ||||
| jclass GetPatchClass() { | ||||
|     return s_patch_class; | ||||
| } | ||||
|  | ||||
| jmethodID GetPatchConstructor() { | ||||
|     return s_patch_constructor; | ||||
| } | ||||
|  | ||||
| jfieldID GetPatchEnabledField() { | ||||
|     return s_patch_enabled_field; | ||||
| } | ||||
|  | ||||
| jfieldID GetPatchNameField() { | ||||
|     return s_patch_name_field; | ||||
| } | ||||
|  | ||||
| jfieldID GetPatchVersionField() { | ||||
|     return s_patch_version_field; | ||||
| } | ||||
|  | ||||
| jfieldID GetPatchTypeField() { | ||||
|     return s_patch_type_field; | ||||
| } | ||||
|  | ||||
| jfieldID GetPatchProgramIdField() { | ||||
|     return s_patch_program_id_field; | ||||
| } | ||||
|  | ||||
| jfieldID GetPatchTitleIdField() { | ||||
|     return s_patch_title_id_field; | ||||
| } | ||||
|  | ||||
| jclass GetDoubleClass() { | ||||
|     return s_double_class; | ||||
| } | ||||
| @@ -310,6 +351,19 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { | ||||
|         env->GetFieldID(overlay_control_data_class, "foldablePosition", "Lkotlin/Pair;"); | ||||
|     env->DeleteLocalRef(overlay_control_data_class); | ||||
|  | ||||
|     const jclass patch_class = env->FindClass("org/yuzu/yuzu_emu/model/Patch"); | ||||
|     s_patch_class = reinterpret_cast<jclass>(env->NewGlobalRef(patch_class)); | ||||
|     s_patch_constructor = env->GetMethodID( | ||||
|         patch_class, "<init>", | ||||
|         "(ZLjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)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;"); | ||||
|     s_patch_type_field = env->GetFieldID(patch_class, "type", "I"); | ||||
|     s_patch_program_id_field = env->GetFieldID(patch_class, "programId", "Ljava/lang/String;"); | ||||
|     s_patch_title_id_field = env->GetFieldID(patch_class, "titleId", "Ljava/lang/String;"); | ||||
|     env->DeleteLocalRef(patch_class); | ||||
|  | ||||
|     const jclass double_class = env->FindClass("java/lang/Double"); | ||||
|     s_double_class = reinterpret_cast<jclass>(env->NewGlobalRef(double_class)); | ||||
|     s_double_constructor = env->GetMethodID(double_class, "<init>", "(D)V"); | ||||
| @@ -353,6 +407,7 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) { | ||||
|     env->DeleteGlobalRef(s_string_class); | ||||
|     env->DeleteGlobalRef(s_pair_class); | ||||
|     env->DeleteGlobalRef(s_overlay_control_data_class); | ||||
|     env->DeleteGlobalRef(s_patch_class); | ||||
|     env->DeleteGlobalRef(s_double_class); | ||||
|     env->DeleteGlobalRef(s_integer_class); | ||||
|     env->DeleteGlobalRef(s_boolean_class); | ||||
|   | ||||
| @@ -43,6 +43,15 @@ jfieldID GetOverlayControlDataLandscapePositionField(); | ||||
| jfieldID GetOverlayControlDataPortraitPositionField(); | ||||
| jfieldID GetOverlayControlDataFoldablePositionField(); | ||||
|  | ||||
| jclass GetPatchClass(); | ||||
| jmethodID GetPatchConstructor(); | ||||
| jfieldID GetPatchEnabledField(); | ||||
| jfieldID GetPatchNameField(); | ||||
| jfieldID GetPatchVersionField(); | ||||
| jfieldID GetPatchTypeField(); | ||||
| jfieldID GetPatchProgramIdField(); | ||||
| jfieldID GetPatchTitleIdField(); | ||||
|  | ||||
| jclass GetDoubleClass(); | ||||
| jmethodID GetDoubleConstructor(); | ||||
| jfieldID GetDoubleValueField(); | ||||
|   | ||||
| @@ -774,9 +774,9 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env, | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, jobject jobj, | ||||
|                                                                     jstring jpath, | ||||
|                                                                     jstring jprogramId) { | ||||
| jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env, jobject jobj, | ||||
|                                                                      jstring jpath, | ||||
|                                                                      jstring jprogramId) { | ||||
|     const auto path = GetJString(env, jpath); | ||||
|     const auto vFile = | ||||
|         Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path); | ||||
| @@ -793,20 +793,40 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, | ||||
|     FileSys::VirtualFile update_raw; | ||||
|     loader->ReadUpdateRaw(update_raw); | ||||
|  | ||||
|     auto addons = pm.GetPatchVersionNames(update_raw); | ||||
|     auto jemptyString = ToJString(env, ""); | ||||
|     auto jemptyStringPair = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(), | ||||
|                                            jemptyString, jemptyString); | ||||
|     jobjectArray jaddonsArray = | ||||
|         env->NewObjectArray(addons.size(), IDCache::GetPairClass(), jemptyStringPair); | ||||
|     auto patches = pm.GetPatches(update_raw); | ||||
|     jobjectArray jpatchArray = | ||||
|         env->NewObjectArray(patches.size(), IDCache::GetPatchClass(), nullptr); | ||||
|     int i = 0; | ||||
|     for (const auto& addon : addons) { | ||||
|         jobject jaddon = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(), | ||||
|                                         ToJString(env, addon.first), ToJString(env, addon.second)); | ||||
|         env->SetObjectArrayElement(jaddonsArray, i, jaddon); | ||||
|     for (const auto& patch : patches) { | ||||
|         jobject jpatch = env->NewObject( | ||||
|             IDCache::GetPatchClass(), IDCache::GetPatchConstructor(), patch.enabled, | ||||
|             ToJString(env, patch.name), ToJString(env, patch.version), | ||||
|             static_cast<jint>(patch.type), ToJString(env, std::to_string(patch.program_id)), | ||||
|             ToJString(env, std::to_string(patch.title_id))); | ||||
|         env->SetObjectArrayElement(jpatchArray, i, jpatch); | ||||
|         ++i; | ||||
|     } | ||||
|     return jaddonsArray; | ||||
|     return jpatchArray; | ||||
| } | ||||
|  | ||||
| void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeUpdate(JNIEnv* env, jobject jobj, | ||||
|                                                         jstring jprogramId) { | ||||
|     auto program_id = EmulationSession::GetProgramId(env, jprogramId); | ||||
|     ContentManager::RemoveUpdate(EmulationSession::GetInstance().System().GetFileSystemController(), | ||||
|                                  program_id); | ||||
| } | ||||
|  | ||||
| void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeDLC(JNIEnv* env, jobject jobj, | ||||
|                                                      jstring jprogramId) { | ||||
|     auto program_id = EmulationSession::GetProgramId(env, jprogramId); | ||||
|     ContentManager::RemoveAllDLC(&EmulationSession::GetInstance().System(), program_id); | ||||
| } | ||||
|  | ||||
| void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeMod(JNIEnv* env, jobject jobj, jstring jprogramId, | ||||
|                                                      jstring jname) { | ||||
|     auto program_id = EmulationSession::GetProgramId(env, jprogramId); | ||||
|     ContentManager::RemoveMod(EmulationSession::GetInstance().System().GetFileSystemController(), | ||||
|                               program_id, GetJString(env, jname)); | ||||
| } | ||||
|  | ||||
| jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj, | ||||
|   | ||||
| @@ -14,12 +14,11 @@ | ||||
|         android:id="@+id/text_container" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginEnd="16dp" | ||||
|         android:orientation="vertical" | ||||
|         app:layout_constraintBottom_toBottomOf="@+id/addon_switch" | ||||
|         app:layout_constraintEnd_toStartOf="@+id/addon_switch" | ||||
|         android:layout_marginEnd="16dp" | ||||
|         app:layout_constraintEnd_toStartOf="@+id/addon_checkbox" | ||||
|         app:layout_constraintStart_toStartOf="parent" | ||||
|         app:layout_constraintTop_toTopOf="@+id/addon_switch"> | ||||
|         app:layout_constraintTop_toTopOf="parent"> | ||||
|  | ||||
|         <com.google.android.material.textview.MaterialTextView | ||||
|             android:id="@+id/title" | ||||
| @@ -42,16 +41,29 @@ | ||||
|  | ||||
|     </LinearLayout> | ||||
|  | ||||
|     <com.google.android.material.materialswitch.MaterialSwitch | ||||
|         android:id="@+id/addon_switch" | ||||
|     <com.google.android.material.checkbox.MaterialCheckBox | ||||
|         android:id="@+id/addon_checkbox" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:focusable="true" | ||||
|         android:gravity="center" | ||||
|         android:nextFocusLeft="@id/addon_container" | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         android:layout_marginEnd="8dp" | ||||
|         app:layout_constraintTop_toTopOf="@+id/text_container" | ||||
|         app:layout_constraintBottom_toBottomOf="@+id/text_container" | ||||
|         app:layout_constraintEnd_toStartOf="@+id/button_delete" /> | ||||
|  | ||||
|     <Button | ||||
|         android:id="@+id/button_delete" | ||||
|         style="@style/Widget.Material3.Button.IconButton" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_gravity="center_vertical" | ||||
|         android:contentDescription="@string/delete" | ||||
|         android:tooltipText="@string/delete" | ||||
|         app:icon="@drawable/ic_delete" | ||||
|         app:iconTint="?attr/colorControlNormal" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintStart_toEndOf="@id/text_container" | ||||
|         app:layout_constraintTop_toTopOf="parent" /> | ||||
|         app:layout_constraintTop_toTopOf="@+id/addon_checkbox" | ||||
|         app:layout_constraintBottom_toBottomOf="@+id/addon_checkbox" /> | ||||
|  | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
|   | ||||
| @@ -286,6 +286,7 @@ | ||||
|     <string name="custom">Custom</string> | ||||
|     <string name="notice">Notice</string> | ||||
|     <string name="import_complete">Import complete</string> | ||||
|     <string name="more_options">More options</string> | ||||
|  | ||||
|     <!-- GPU driver installation --> | ||||
|     <string name="select_gpu_driver">Select GPU driver</string> | ||||
| @@ -348,6 +349,8 @@ | ||||
|     <string name="verifying_content">Verifying content…</string> | ||||
|     <string name="content_install_notice">Content install notice</string> | ||||
|     <string name="content_install_notice_description">The content that you selected does not match this game.\nInstall anyway?</string> | ||||
|     <string name="confirm_uninstall">Confirm uninstall</string> | ||||
|     <string name="confirm_uninstall_description">Are you sure you want to uninstall this addon?</string> | ||||
|  | ||||
|     <!-- ROM loading errors --> | ||||
|     <string name="loader_error_encrypted">Your ROM is encrypted</string> | ||||
|   | ||||
| @@ -466,12 +466,12 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs | ||||
|     return romfs; | ||||
| } | ||||
|  | ||||
| PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile update_raw) const { | ||||
| std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const { | ||||
|     if (title_id == 0) { | ||||
|         return {}; | ||||
|     } | ||||
|  | ||||
|     std::map<std::string, std::string, std::less<>> out; | ||||
|     std::vector<Patch> out; | ||||
|     const auto& disabled = Settings::values.disabled_addons[title_id]; | ||||
|  | ||||
|     // Game Updates | ||||
| @@ -482,20 +482,28 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u | ||||
|  | ||||
|     const auto update_disabled = | ||||
|         std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); | ||||
|     const auto update_label = update_disabled ? "[D] Update" : "Update"; | ||||
|     Patch update_patch = {.enabled = !update_disabled, | ||||
|                           .name = "Update", | ||||
|                           .version = "", | ||||
|                           .type = PatchType::Update, | ||||
|                           .program_id = title_id, | ||||
|                           .title_id = title_id}; | ||||
|  | ||||
|     if (nacp != nullptr) { | ||||
|         out.insert_or_assign(update_label, nacp->GetVersionString()); | ||||
|         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.insert_or_assign(update_label, ""); | ||||
|                 out.push_back(update_patch); | ||||
|             } else { | ||||
|                 out.insert_or_assign(update_label, FormatTitleVersion(*meta_ver)); | ||||
|                 update_patch.version = FormatTitleVersion(*meta_ver); | ||||
|                 out.push_back(update_patch); | ||||
|             } | ||||
|         } else if (update_raw != nullptr) { | ||||
|             out.insert_or_assign(update_label, "PACKED"); | ||||
|             update_patch.version = "PACKED"; | ||||
|             out.push_back(update_patch); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -539,7 +547,12 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u | ||||
|  | ||||
|             const auto mod_disabled = | ||||
|                 std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end(); | ||||
|             out.insert_or_assign(mod_disabled ? "[D] " + mod->GetName() : mod->GetName(), types); | ||||
|             out.push_back({.enabled = !mod_disabled, | ||||
|                            .name = mod->GetName(), | ||||
|                            .version = types, | ||||
|                            .type = PatchType::Mod, | ||||
|                            .program_id = title_id, | ||||
|                            .title_id = title_id}); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -557,7 +570,12 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u | ||||
|         if (!types.empty()) { | ||||
|             const auto mod_disabled = | ||||
|                 std::find(disabled.begin(), disabled.end(), "SDMC") != disabled.end(); | ||||
|             out.insert_or_assign(mod_disabled ? "[D] SDMC" : "SDMC", types); | ||||
|             out.push_back({.enabled = !mod_disabled, | ||||
|                            .name = "SDMC", | ||||
|                            .version = types, | ||||
|                            .type = PatchType::Mod, | ||||
|                            .program_id = title_id, | ||||
|                            .title_id = title_id}); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -584,7 +602,12 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u | ||||
|  | ||||
|         const auto dlc_disabled = | ||||
|             std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end(); | ||||
|         out.insert_or_assign(dlc_disabled ? "[D] DLC" : "DLC", std::move(list)); | ||||
|         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}); | ||||
|     } | ||||
|  | ||||
|     return out; | ||||
|   | ||||
| @@ -26,12 +26,22 @@ class ContentProvider; | ||||
| class NCA; | ||||
| class NACP; | ||||
|  | ||||
| enum class PatchType { Update, DLC, Mod }; | ||||
|  | ||||
| struct Patch { | ||||
|     bool enabled; | ||||
|     std::string name; | ||||
|     std::string version; | ||||
|     PatchType type; | ||||
|     u64 program_id; | ||||
|     u64 title_id; | ||||
| }; | ||||
|  | ||||
| // A centralized class to manage patches to games. | ||||
| class PatchManager { | ||||
| public: | ||||
|     using BuildID = std::array<u8, 0x20>; | ||||
|     using Metadata = std::pair<std::unique_ptr<NACP>, VirtualFile>; | ||||
|     using PatchVersionNames = std::map<std::string, std::string, std::less<>>; | ||||
|  | ||||
|     explicit PatchManager(u64 title_id_, | ||||
|                           const Service::FileSystem::FileSystemController& fs_controller_, | ||||
| @@ -66,9 +76,8 @@ public: | ||||
|                                          VirtualFile packed_update_raw = nullptr, | ||||
|                                          bool apply_layeredfs = true) const; | ||||
|  | ||||
|     // Returns a vector of pairs between patch names and patch versions. | ||||
|     // i.e. Update 3.2.2 will return {"Update", "3.2.2"} | ||||
|     [[nodiscard]] PatchVersionNames GetPatchVersionNames(VirtualFile update_raw = nullptr) const; | ||||
|     // Returns a vector of patches | ||||
|     [[nodiscard]] std::vector<Patch> GetPatches(VirtualFile update_raw = nullptr) const; | ||||
|  | ||||
|     // If the game update exists, returns the u32 version field in its Meta-type NCA. If that fails, | ||||
|     // it will fallback to the Meta-type NCA of the base game. If that fails, the result will be | ||||
|   | ||||
| @@ -65,6 +65,23 @@ inline bool RemoveBaseContent(const Service::FileSystem::FileSystemController& f | ||||
|            fs_controller.GetSDMCContents()->RemoveExistingEntry(program_id); | ||||
| } | ||||
|  | ||||
| inline bool RemoveMod(const Service::FileSystem::FileSystemController& fs_controller, | ||||
|                       const u64 program_id, const std::string& mod_name) { | ||||
|     // Check general Mods (LayeredFS and IPS) | ||||
|     const auto mod_dir = fs_controller.GetModificationLoadRoot(program_id); | ||||
|     if (mod_dir != nullptr) { | ||||
|         return mod_dir->DeleteSubdirectoryRecursive(mod_name); | ||||
|     } | ||||
|  | ||||
|     // Check SDMC mod directory (RomFS LayeredFS) | ||||
|     const auto sdmc_mod_dir = fs_controller.GetSDMCModificationLoadRoot(program_id); | ||||
|     if (sdmc_mod_dir != nullptr) { | ||||
|         return sdmc_mod_dir->DeleteSubdirectoryRecursive(mod_name); | ||||
|     } | ||||
|  | ||||
|     return false; | ||||
| } | ||||
|  | ||||
| inline InstallResult InstallNSP( | ||||
|     Core::System* system, FileSys::VfsFilesystem* vfs, const std::string& filename, | ||||
|     const std::function<bool(size_t, size_t)>& callback = std::function<bool(size_t, size_t)>()) { | ||||
|   | ||||
| @@ -122,9 +122,8 @@ void ConfigurePerGameAddons::LoadConfiguration() { | ||||
|  | ||||
|     const auto& disabled = Settings::values.disabled_addons[title_id]; | ||||
|  | ||||
|     for (const auto& patch : pm.GetPatchVersionNames(update_raw)) { | ||||
|         const auto name = | ||||
|             QString::fromStdString(patch.first).replace(QStringLiteral("[D] "), QString{}); | ||||
|     for (const auto& patch : pm.GetPatches(update_raw)) { | ||||
|         const auto name = QString::fromStdString(patch.name); | ||||
|  | ||||
|         auto* const first_item = new QStandardItem; | ||||
|         first_item->setText(name); | ||||
| @@ -136,7 +135,7 @@ void ConfigurePerGameAddons::LoadConfiguration() { | ||||
|         first_item->setCheckState(patch_disabled ? Qt::Unchecked : Qt::Checked); | ||||
|  | ||||
|         list_items.push_back(QList<QStandardItem*>{ | ||||
|             first_item, new QStandardItem{QString::fromStdString(patch.second)}}); | ||||
|             first_item, new QStandardItem{QString::fromStdString(patch.version)}}); | ||||
|         item_model->appendRow(list_items.back()); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -164,18 +164,19 @@ QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager, | ||||
|     QString out; | ||||
|     FileSys::VirtualFile update_raw; | ||||
|     loader.ReadUpdateRaw(update_raw); | ||||
|     for (const auto& kv : patch_manager.GetPatchVersionNames(update_raw)) { | ||||
|         const bool is_update = kv.first == "Update" || kv.first == "[D] Update"; | ||||
|     for (const auto& patch : patch_manager.GetPatches(update_raw)) { | ||||
|         const bool is_update = patch.name == "Update"; | ||||
|         if (!updatable && is_update) { | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         const QString type = QString::fromStdString(kv.first); | ||||
|         const QString type = | ||||
|             QString::fromStdString(patch.enabled ? patch.name : "[D] " + patch.name); | ||||
|  | ||||
|         if (kv.second.empty()) { | ||||
|         if (patch.version.empty()) { | ||||
|             out.append(QStringLiteral("%1\n").arg(type)); | ||||
|         } else { | ||||
|             auto ver = kv.second; | ||||
|             auto ver = patch.version; | ||||
|  | ||||
|             // Display container name for packed updates | ||||
|             if (is_update && ver == "PACKED") { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user