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:
parent
d79d4d5986
commit
03fa91ba3c
17 changed files with 305 additions and 82 deletions
|
@ -22,6 +22,7 @@ import org.yuzu.yuzu_emu.utils.FileUtil
|
||||||
import org.yuzu.yuzu_emu.utils.Log
|
import org.yuzu.yuzu_emu.utils.Log
|
||||||
import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
|
import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
|
||||||
import org.yuzu.yuzu_emu.model.InstallResult
|
import org.yuzu.yuzu_emu.model.InstallResult
|
||||||
|
import org.yuzu.yuzu_emu.model.Patch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class which contains methods that interact
|
* Class which contains methods that interact
|
||||||
|
@ -539,9 +540,29 @@ object NativeLibrary {
|
||||||
*
|
*
|
||||||
* @param path Path to game file. Can be a [Uri].
|
* @param path Path to game file. Can be a [Uri].
|
||||||
* @param programId String representation of a game's program ID
|
* @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
|
* 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.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding
|
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
|
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 {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder {
|
||||||
ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
.also { return AddonViewHolder(it) }
|
.also { return AddonViewHolder(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class AddonViewHolder(val binding: ListItemAddonBinding) :
|
inner class AddonViewHolder(val binding: ListItemAddonBinding) :
|
||||||
AbstractViewHolder<Addon>(binding) {
|
AbstractViewHolder<Patch>(binding) {
|
||||||
override fun bind(model: Addon) {
|
override fun bind(model: Patch) {
|
||||||
binding.root.setOnClickListener {
|
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.version.text = model.version
|
||||||
binding.addonSwitch.setOnCheckedChangeListener { _, checked ->
|
binding.addonCheckbox.setOnCheckedChangeListener { _, checked ->
|
||||||
model.enabled = 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 {
|
binding.listAddons.apply {
|
||||||
layoutManager = LinearLayoutManager(requireContext())
|
layoutManager = LinearLayoutManager(requireContext())
|
||||||
adapter = AddonAdapter()
|
adapter = AddonAdapter(addonViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.apply {
|
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 {
|
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
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
class AddonViewModel : ViewModel() {
|
class AddonViewModel : ViewModel() {
|
||||||
private val _addonList = MutableStateFlow(mutableListOf<Addon>())
|
private val _patchList = MutableStateFlow(mutableListOf<Patch>())
|
||||||
val addonList get() = _addonList.asStateFlow()
|
val addonList get() = _patchList.asStateFlow()
|
||||||
|
|
||||||
private val _showModInstallPicker = MutableStateFlow(false)
|
private val _showModInstallPicker = MutableStateFlow(false)
|
||||||
val showModInstallPicker get() = _showModInstallPicker.asStateFlow()
|
val showModInstallPicker get() = _showModInstallPicker.asStateFlow()
|
||||||
|
@ -24,6 +24,9 @@ class AddonViewModel : ViewModel() {
|
||||||
private val _showModNoticeDialog = MutableStateFlow(false)
|
private val _showModNoticeDialog = MutableStateFlow(false)
|
||||||
val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow()
|
val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow()
|
||||||
|
|
||||||
|
private val _addonToDelete = MutableStateFlow<Patch?>(null)
|
||||||
|
val addonToDelete = _addonToDelete.asStateFlow()
|
||||||
|
|
||||||
var game: Game? = null
|
var game: Game? = null
|
||||||
|
|
||||||
private val isRefreshing = AtomicBoolean(false)
|
private val isRefreshing = AtomicBoolean(false)
|
||||||
|
@ -40,36 +43,47 @@ class AddonViewModel : ViewModel() {
|
||||||
isRefreshing.set(true)
|
isRefreshing.set(true)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val addonList = mutableListOf<Addon>()
|
val patchList = (
|
||||||
val disabledAddons = NativeConfig.getDisabledAddons(game!!.programId)
|
NativeLibrary.getPatchesForFile(game!!.path, game!!.programId)
|
||||||
NativeLibrary.getAddonsForFile(game!!.path, game!!.programId)?.forEach {
|
?: emptyArray()
|
||||||
val name = it.first.replace("[D] ", "")
|
).toMutableList()
|
||||||
addonList.add(Addon(!disabledAddons.contains(name), name, it.second))
|
patchList.sortBy { it.name }
|
||||||
}
|
_patchList.value = patchList
|
||||||
addonList.sortBy { it.title }
|
|
||||||
_addonList.value = addonList
|
|
||||||
isRefreshing.set(false)
|
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() {
|
fun onCloseAddons() {
|
||||||
if (_addonList.value.isEmpty()) {
|
if (_patchList.value.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
NativeConfig.setDisabledAddons(
|
NativeConfig.setDisabledAddons(
|
||||||
game!!.programId,
|
game!!.programId,
|
||||||
_addonList.value.mapNotNull {
|
_patchList.value.mapNotNull {
|
||||||
if (it.enabled) {
|
if (it.enabled) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
it.title
|
it.name
|
||||||
}
|
}
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
)
|
)
|
||||||
NativeConfig.saveGlobalConfig()
|
NativeConfig.saveGlobalConfig()
|
||||||
_addonList.value.clear()
|
_patchList.value.clear()
|
||||||
game = null
|
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_portrait_position_field;
|
||||||
static jfieldID s_overlay_control_data_foldable_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 jclass s_double_class;
|
||||||
static jmethodID s_double_constructor;
|
static jmethodID s_double_constructor;
|
||||||
static jfieldID s_double_value_field;
|
static jfieldID s_double_value_field;
|
||||||
|
@ -194,6 +203,38 @@ jfieldID GetOverlayControlDataFoldablePositionField() {
|
||||||
return s_overlay_control_data_foldable_position_field;
|
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() {
|
jclass GetDoubleClass() {
|
||||||
return s_double_class;
|
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->GetFieldID(overlay_control_data_class, "foldablePosition", "Lkotlin/Pair;");
|
||||||
env->DeleteLocalRef(overlay_control_data_class);
|
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");
|
const jclass double_class = env->FindClass("java/lang/Double");
|
||||||
s_double_class = reinterpret_cast<jclass>(env->NewGlobalRef(double_class));
|
s_double_class = reinterpret_cast<jclass>(env->NewGlobalRef(double_class));
|
||||||
s_double_constructor = env->GetMethodID(double_class, "<init>", "(D)V");
|
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_string_class);
|
||||||
env->DeleteGlobalRef(s_pair_class);
|
env->DeleteGlobalRef(s_pair_class);
|
||||||
env->DeleteGlobalRef(s_overlay_control_data_class);
|
env->DeleteGlobalRef(s_overlay_control_data_class);
|
||||||
|
env->DeleteGlobalRef(s_patch_class);
|
||||||
env->DeleteGlobalRef(s_double_class);
|
env->DeleteGlobalRef(s_double_class);
|
||||||
env->DeleteGlobalRef(s_integer_class);
|
env->DeleteGlobalRef(s_integer_class);
|
||||||
env->DeleteGlobalRef(s_boolean_class);
|
env->DeleteGlobalRef(s_boolean_class);
|
||||||
|
|
|
@ -43,6 +43,15 @@ jfieldID GetOverlayControlDataLandscapePositionField();
|
||||||
jfieldID GetOverlayControlDataPortraitPositionField();
|
jfieldID GetOverlayControlDataPortraitPositionField();
|
||||||
jfieldID GetOverlayControlDataFoldablePositionField();
|
jfieldID GetOverlayControlDataFoldablePositionField();
|
||||||
|
|
||||||
|
jclass GetPatchClass();
|
||||||
|
jmethodID GetPatchConstructor();
|
||||||
|
jfieldID GetPatchEnabledField();
|
||||||
|
jfieldID GetPatchNameField();
|
||||||
|
jfieldID GetPatchVersionField();
|
||||||
|
jfieldID GetPatchTypeField();
|
||||||
|
jfieldID GetPatchProgramIdField();
|
||||||
|
jfieldID GetPatchTitleIdField();
|
||||||
|
|
||||||
jclass GetDoubleClass();
|
jclass GetDoubleClass();
|
||||||
jmethodID GetDoubleConstructor();
|
jmethodID GetDoubleConstructor();
|
||||||
jfieldID GetDoubleValueField();
|
jfieldID GetDoubleValueField();
|
||||||
|
|
|
@ -774,7 +774,7 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env,
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, jobject jobj,
|
jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env, jobject jobj,
|
||||||
jstring jpath,
|
jstring jpath,
|
||||||
jstring jprogramId) {
|
jstring jprogramId) {
|
||||||
const auto path = GetJString(env, jpath);
|
const auto path = GetJString(env, jpath);
|
||||||
|
@ -793,20 +793,40 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env,
|
||||||
FileSys::VirtualFile update_raw;
|
FileSys::VirtualFile update_raw;
|
||||||
loader->ReadUpdateRaw(update_raw);
|
loader->ReadUpdateRaw(update_raw);
|
||||||
|
|
||||||
auto addons = pm.GetPatchVersionNames(update_raw);
|
auto patches = pm.GetPatches(update_raw);
|
||||||
auto jemptyString = ToJString(env, "");
|
jobjectArray jpatchArray =
|
||||||
auto jemptyStringPair = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(),
|
env->NewObjectArray(patches.size(), IDCache::GetPatchClass(), nullptr);
|
||||||
jemptyString, jemptyString);
|
|
||||||
jobjectArray jaddonsArray =
|
|
||||||
env->NewObjectArray(addons.size(), IDCache::GetPairClass(), jemptyStringPair);
|
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (const auto& addon : addons) {
|
for (const auto& patch : patches) {
|
||||||
jobject jaddon = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(),
|
jobject jpatch = env->NewObject(
|
||||||
ToJString(env, addon.first), ToJString(env, addon.second));
|
IDCache::GetPatchClass(), IDCache::GetPatchConstructor(), patch.enabled,
|
||||||
env->SetObjectArrayElement(jaddonsArray, i, jaddon);
|
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;
|
++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,
|
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj,
|
||||||
|
|
|
@ -14,12 +14,11 @@
|
||||||
android:id="@+id/text_container"
|
android:id="@+id/text_container"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/addon_switch"
|
android:layout_marginEnd="16dp"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/addon_switch"
|
app:layout_constraintEnd_toStartOf="@+id/addon_checkbox"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@+id/addon_switch">
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/title"
|
android:id="@+id/title"
|
||||||
|
@ -42,16 +41,29 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<com.google.android.material.materialswitch.MaterialSwitch
|
<com.google.android.material.checkbox.MaterialCheckBox
|
||||||
android:id="@+id/addon_switch"
|
android:id="@+id/addon_checkbox"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:nextFocusLeft="@id/addon_container"
|
android:layout_marginEnd="8dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
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_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/text_container"
|
app:layout_constraintTop_toTopOf="@+id/addon_checkbox"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintBottom_toBottomOf="@+id/addon_checkbox" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
@ -286,6 +286,7 @@
|
||||||
<string name="custom">Custom</string>
|
<string name="custom">Custom</string>
|
||||||
<string name="notice">Notice</string>
|
<string name="notice">Notice</string>
|
||||||
<string name="import_complete">Import complete</string>
|
<string name="import_complete">Import complete</string>
|
||||||
|
<string name="more_options">More options</string>
|
||||||
|
|
||||||
<!-- GPU driver installation -->
|
<!-- GPU driver installation -->
|
||||||
<string name="select_gpu_driver">Select GPU driver</string>
|
<string name="select_gpu_driver">Select GPU driver</string>
|
||||||
|
@ -348,6 +349,8 @@
|
||||||
<string name="verifying_content">Verifying content…</string>
|
<string name="verifying_content">Verifying content…</string>
|
||||||
<string name="content_install_notice">Content install notice</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="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 -->
|
<!-- ROM loading errors -->
|
||||||
<string name="loader_error_encrypted">Your ROM is encrypted</string>
|
<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;
|
return romfs;
|
||||||
}
|
}
|
||||||
|
|
||||||
PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile update_raw) const {
|
std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
|
||||||
if (title_id == 0) {
|
if (title_id == 0) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::map<std::string, std::string, std::less<>> out;
|
std::vector<Patch> out;
|
||||||
const auto& disabled = Settings::values.disabled_addons[title_id];
|
const auto& disabled = Settings::values.disabled_addons[title_id];
|
||||||
|
|
||||||
// Game Updates
|
// Game Updates
|
||||||
|
@ -482,20 +482,28 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u
|
||||||
|
|
||||||
const auto update_disabled =
|
const auto update_disabled =
|
||||||
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
|
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) {
|
if (nacp != nullptr) {
|
||||||
out.insert_or_assign(update_label, nacp->GetVersionString());
|
update_patch.version = nacp->GetVersionString();
|
||||||
|
out.push_back(update_patch);
|
||||||
} else {
|
} else {
|
||||||
if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) {
|
if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) {
|
||||||
const auto meta_ver = content_provider.GetEntryVersion(update_tid);
|
const auto meta_ver = content_provider.GetEntryVersion(update_tid);
|
||||||
if (meta_ver.value_or(0) == 0) {
|
if (meta_ver.value_or(0) == 0) {
|
||||||
out.insert_or_assign(update_label, "");
|
out.push_back(update_patch);
|
||||||
} else {
|
} 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) {
|
} 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 =
|
const auto mod_disabled =
|
||||||
std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end();
|
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()) {
|
if (!types.empty()) {
|
||||||
const auto mod_disabled =
|
const auto mod_disabled =
|
||||||
std::find(disabled.begin(), disabled.end(), "SDMC") != disabled.end();
|
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 =
|
const auto dlc_disabled =
|
||||||
std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end();
|
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;
|
return out;
|
||||||
|
|
|
@ -26,12 +26,22 @@ class ContentProvider;
|
||||||
class NCA;
|
class NCA;
|
||||||
class NACP;
|
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.
|
// A centralized class to manage patches to games.
|
||||||
class PatchManager {
|
class PatchManager {
|
||||||
public:
|
public:
|
||||||
using BuildID = std::array<u8, 0x20>;
|
using BuildID = std::array<u8, 0x20>;
|
||||||
using Metadata = std::pair<std::unique_ptr<NACP>, VirtualFile>;
|
using Metadata = std::pair<std::unique_ptr<NACP>, VirtualFile>;
|
||||||
using PatchVersionNames = std::map<std::string, std::string, std::less<>>;
|
|
||||||
|
|
||||||
explicit PatchManager(u64 title_id_,
|
explicit PatchManager(u64 title_id_,
|
||||||
const Service::FileSystem::FileSystemController& fs_controller_,
|
const Service::FileSystem::FileSystemController& fs_controller_,
|
||||||
|
@ -66,9 +76,8 @@ public:
|
||||||
VirtualFile packed_update_raw = nullptr,
|
VirtualFile packed_update_raw = nullptr,
|
||||||
bool apply_layeredfs = true) const;
|
bool apply_layeredfs = true) const;
|
||||||
|
|
||||||
// Returns a vector of pairs between patch names and patch versions.
|
// Returns a vector of patches
|
||||||
// i.e. Update 3.2.2 will return {"Update", "3.2.2"}
|
[[nodiscard]] std::vector<Patch> GetPatches(VirtualFile update_raw = nullptr) const;
|
||||||
[[nodiscard]] PatchVersionNames GetPatchVersionNames(VirtualFile update_raw = nullptr) const;
|
|
||||||
|
|
||||||
// If the game update exists, returns the u32 version field in its Meta-type NCA. If that fails,
|
// 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
|
// 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);
|
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(
|
inline InstallResult InstallNSP(
|
||||||
Core::System* system, FileSys::VfsFilesystem* vfs, const std::string& filename,
|
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)>()) {
|
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];
|
const auto& disabled = Settings::values.disabled_addons[title_id];
|
||||||
|
|
||||||
for (const auto& patch : pm.GetPatchVersionNames(update_raw)) {
|
for (const auto& patch : pm.GetPatches(update_raw)) {
|
||||||
const auto name =
|
const auto name = QString::fromStdString(patch.name);
|
||||||
QString::fromStdString(patch.first).replace(QStringLiteral("[D] "), QString{});
|
|
||||||
|
|
||||||
auto* const first_item = new QStandardItem;
|
auto* const first_item = new QStandardItem;
|
||||||
first_item->setText(name);
|
first_item->setText(name);
|
||||||
|
@ -136,7 +135,7 @@ void ConfigurePerGameAddons::LoadConfiguration() {
|
||||||
first_item->setCheckState(patch_disabled ? Qt::Unchecked : Qt::Checked);
|
first_item->setCheckState(patch_disabled ? Qt::Unchecked : Qt::Checked);
|
||||||
|
|
||||||
list_items.push_back(QList<QStandardItem*>{
|
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());
|
item_model->appendRow(list_items.back());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -164,18 +164,19 @@ QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager,
|
||||||
QString out;
|
QString out;
|
||||||
FileSys::VirtualFile update_raw;
|
FileSys::VirtualFile update_raw;
|
||||||
loader.ReadUpdateRaw(update_raw);
|
loader.ReadUpdateRaw(update_raw);
|
||||||
for (const auto& kv : patch_manager.GetPatchVersionNames(update_raw)) {
|
for (const auto& patch : patch_manager.GetPatches(update_raw)) {
|
||||||
const bool is_update = kv.first == "Update" || kv.first == "[D] Update";
|
const bool is_update = patch.name == "Update";
|
||||||
if (!updatable && is_update) {
|
if (!updatable && is_update) {
|
||||||
continue;
|
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));
|
out.append(QStringLiteral("%1\n").arg(type));
|
||||||
} else {
|
} else {
|
||||||
auto ver = kv.second;
|
auto ver = patch.version;
|
||||||
|
|
||||||
// Display container name for packed updates
|
// Display container name for packed updates
|
||||||
if (is_update && ver == "PACKED") {
|
if (is_update && ver == "PACKED") {
|
||||||
|
|
Loading…
Reference in a new issue