Merge pull request #12715 from t895/remove-addons
android: Add uninstall addon button
This commit is contained in:
commit
2faa631676
|
@ -21,6 +21,8 @@ import org.yuzu.yuzu_emu.utils.DocumentsTree
|
||||||
import org.yuzu.yuzu_emu.utils.FileUtil
|
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.Patch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class which contains methods that interact
|
* Class which contains methods that interact
|
||||||
|
@ -235,9 +237,12 @@ object NativeLibrary {
|
||||||
/**
|
/**
|
||||||
* Installs a nsp or xci file to nand
|
* Installs a nsp or xci file to nand
|
||||||
* @param filename String representation of file uri
|
* @param filename String representation of file uri
|
||||||
* @param extension Lowercase string representation of file extension without "."
|
* @return int representation of [InstallResult]
|
||||||
*/
|
*/
|
||||||
external fun installFileToNand(filename: String, extension: String): Int
|
external fun installFileToNand(
|
||||||
|
filename: String,
|
||||||
|
callback: (max: Long, progress: Long) -> Boolean
|
||||||
|
): Int
|
||||||
|
|
||||||
external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean
|
external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean
|
||||||
|
|
||||||
|
@ -535,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
|
||||||
|
@ -609,15 +634,4 @@ object NativeLibrary {
|
||||||
const val RELEASED = 0
|
const val RELEASED = 0
|
||||||
const val PRESSED = 1
|
const val PRESSED = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Result from installFileToNand
|
|
||||||
*/
|
|
||||||
object InstallFileToNandResult {
|
|
||||||
const val Success = 0
|
|
||||||
const val SuccessFileOverwritten = 1
|
|
||||||
const val Error = 2
|
|
||||||
const val ErrorBaseGame = 3
|
|
||||||
const val ErrorFilenameExtension = 4
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -156,22 +171,22 @@ class AddonsFragment : Fragment() {
|
||||||
descriptionId = R.string.invalid_directory_description
|
descriptionId = R.string.invalid_directory_description
|
||||||
)
|
)
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
IndeterminateProgressDialogFragment.newInstance(
|
ProgressDialogFragment.newInstance(
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
R.string.installing_game_content,
|
R.string.installing_game_content,
|
||||||
false
|
false
|
||||||
) {
|
) { progressCallback, _ ->
|
||||||
val parentDirectoryName = externalAddonDirectory.name
|
val parentDirectoryName = externalAddonDirectory.name
|
||||||
val internalAddonDirectory =
|
val internalAddonDirectory =
|
||||||
File(args.game.addonDir + parentDirectoryName)
|
File(args.game.addonDir + parentDirectoryName)
|
||||||
try {
|
try {
|
||||||
externalAddonDirectory.copyFilesTo(internalAddonDirectory)
|
externalAddonDirectory.copyFilesTo(internalAddonDirectory, progressCallback)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
return@newInstance errorMessage
|
return@newInstance errorMessage
|
||||||
}
|
}
|
||||||
addonViewModel.refreshAddons()
|
addonViewModel.refreshAddons()
|
||||||
return@newInstance getString(R.string.addon_installed_successfully)
|
return@newInstance getString(R.string.addon_installed_successfully)
|
||||||
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
||||||
} else {
|
} else {
|
||||||
errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG)
|
errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
|
|
|
@ -173,11 +173,11 @@ class DriverManagerFragment : Fragment() {
|
||||||
return@registerForActivityResult
|
return@registerForActivityResult
|
||||||
}
|
}
|
||||||
|
|
||||||
IndeterminateProgressDialogFragment.newInstance(
|
ProgressDialogFragment.newInstance(
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
R.string.installing_driver,
|
R.string.installing_driver,
|
||||||
false
|
false
|
||||||
) {
|
) { _, _ ->
|
||||||
val driverPath =
|
val driverPath =
|
||||||
"${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename(result)}"
|
"${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename(result)}"
|
||||||
val driverFile = File(driverPath)
|
val driverFile = File(driverPath)
|
||||||
|
@ -213,6 +213,6 @@ class DriverManagerFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return@newInstance Any()
|
return@newInstance Any()
|
||||||
}.show(childFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
}.show(childFragmentManager, ProgressDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,6 @@ import org.yuzu.yuzu_emu.utils.FileUtil
|
||||||
import org.yuzu.yuzu_emu.utils.GameIconUtils
|
import org.yuzu.yuzu_emu.utils.GameIconUtils
|
||||||
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
|
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
|
||||||
import org.yuzu.yuzu_emu.utils.MemoryUtil
|
import org.yuzu.yuzu_emu.utils.MemoryUtil
|
||||||
import java.io.BufferedInputStream
|
|
||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
@ -357,27 +356,17 @@ class GamePropertiesFragment : Fragment() {
|
||||||
return@registerForActivityResult
|
return@registerForActivityResult
|
||||||
}
|
}
|
||||||
|
|
||||||
val inputZip = requireContext().contentResolver.openInputStream(result)
|
|
||||||
val savesFolder = File(args.game.saveDir)
|
val savesFolder = File(args.game.saveDir)
|
||||||
val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
|
val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
|
||||||
cacheSaveDir.mkdir()
|
cacheSaveDir.mkdir()
|
||||||
|
|
||||||
if (inputZip == null) {
|
ProgressDialogFragment.newInstance(
|
||||||
Toast.makeText(
|
|
||||||
YuzuApplication.appContext,
|
|
||||||
getString(R.string.fatal_error),
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
IndeterminateProgressDialogFragment.newInstance(
|
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
R.string.save_files_importing,
|
R.string.save_files_importing,
|
||||||
false
|
false
|
||||||
) {
|
) { _, _ ->
|
||||||
try {
|
try {
|
||||||
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
|
FileUtil.unzipToInternalStorage(result.toString(), cacheSaveDir)
|
||||||
val files = cacheSaveDir.listFiles()
|
val files = cacheSaveDir.listFiles()
|
||||||
var savesFolderFile: File? = null
|
var savesFolderFile: File? = null
|
||||||
if (files != null) {
|
if (files != null) {
|
||||||
|
@ -422,7 +411,7 @@ class GamePropertiesFragment : Fragment() {
|
||||||
Toast.LENGTH_LONG
|
Toast.LENGTH_LONG
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -436,11 +425,11 @@ class GamePropertiesFragment : Fragment() {
|
||||||
return@registerForActivityResult
|
return@registerForActivityResult
|
||||||
}
|
}
|
||||||
|
|
||||||
IndeterminateProgressDialogFragment.newInstance(
|
ProgressDialogFragment.newInstance(
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
R.string.save_files_exporting,
|
R.string.save_files_exporting,
|
||||||
false
|
false
|
||||||
) {
|
) { _, _ ->
|
||||||
val saveLocation = args.game.saveDir
|
val saveLocation = args.game.saveDir
|
||||||
val zipResult = FileUtil.zipFromInternalStorage(
|
val zipResult = FileUtil.zipFromInternalStorage(
|
||||||
File(saveLocation),
|
File(saveLocation),
|
||||||
|
@ -452,6 +441,6 @@ class GamePropertiesFragment : Fragment() {
|
||||||
TaskState.Completed -> getString(R.string.export_success)
|
TaskState.Completed -> getString(R.string.export_success)
|
||||||
TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
|
TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
|
||||||
}
|
}
|
||||||
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,6 @@ import org.yuzu.yuzu_emu.model.TaskState
|
||||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||||
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||||
import org.yuzu.yuzu_emu.utils.FileUtil
|
import org.yuzu.yuzu_emu.utils.FileUtil
|
||||||
import java.io.BufferedInputStream
|
|
||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
|
@ -195,26 +194,20 @@ class InstallableFragment : Fragment() {
|
||||||
return@registerForActivityResult
|
return@registerForActivityResult
|
||||||
}
|
}
|
||||||
|
|
||||||
val inputZip = requireContext().contentResolver.openInputStream(result)
|
|
||||||
val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
|
val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
|
||||||
cacheSaveDir.mkdir()
|
cacheSaveDir.mkdir()
|
||||||
|
|
||||||
if (inputZip == null) {
|
ProgressDialogFragment.newInstance(
|
||||||
Toast.makeText(
|
|
||||||
YuzuApplication.appContext,
|
|
||||||
getString(R.string.fatal_error),
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
IndeterminateProgressDialogFragment.newInstance(
|
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
R.string.save_files_importing,
|
R.string.save_files_importing,
|
||||||
false
|
false
|
||||||
) {
|
) { progressCallback, _ ->
|
||||||
try {
|
try {
|
||||||
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
|
FileUtil.unzipToInternalStorage(
|
||||||
|
result.toString(),
|
||||||
|
cacheSaveDir,
|
||||||
|
progressCallback
|
||||||
|
)
|
||||||
val files = cacheSaveDir.listFiles()
|
val files = cacheSaveDir.listFiles()
|
||||||
var successfulImports = 0
|
var successfulImports = 0
|
||||||
var failedImports = 0
|
var failedImports = 0
|
||||||
|
@ -287,7 +280,7 @@ class InstallableFragment : Fragment() {
|
||||||
Toast.LENGTH_LONG
|
Toast.LENGTH_LONG
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val exportSaves = registerForActivityResult(
|
private val exportSaves = registerForActivityResult(
|
||||||
|
@ -297,11 +290,11 @@ class InstallableFragment : Fragment() {
|
||||||
return@registerForActivityResult
|
return@registerForActivityResult
|
||||||
}
|
}
|
||||||
|
|
||||||
IndeterminateProgressDialogFragment.newInstance(
|
ProgressDialogFragment.newInstance(
|
||||||
requireActivity(),
|
requireActivity(),
|
||||||
R.string.save_files_exporting,
|
R.string.save_files_exporting,
|
||||||
false
|
false
|
||||||
) {
|
) { _, _ ->
|
||||||
val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
|
val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
|
||||||
cacheSaveDir.mkdir()
|
cacheSaveDir.mkdir()
|
||||||
|
|
||||||
|
@ -338,6 +331,6 @@ class InstallableFragment : Fragment() {
|
||||||
TaskState.Completed -> getString(R.string.export_success)
|
TaskState.Completed -> getString(R.string.export_success)
|
||||||
TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
|
TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
|
||||||
}
|
}
|
||||||
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,11 +23,13 @@ import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
|
import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
|
||||||
import org.yuzu.yuzu_emu.model.TaskViewModel
|
import org.yuzu.yuzu_emu.model.TaskViewModel
|
||||||
|
|
||||||
class IndeterminateProgressDialogFragment : DialogFragment() {
|
class ProgressDialogFragment : DialogFragment() {
|
||||||
private val taskViewModel: TaskViewModel by activityViewModels()
|
private val taskViewModel: TaskViewModel by activityViewModels()
|
||||||
|
|
||||||
private lateinit var binding: DialogProgressBarBinding
|
private lateinit var binding: DialogProgressBarBinding
|
||||||
|
|
||||||
|
private val PROGRESS_BAR_RESOLUTION = 1000
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val titleId = requireArguments().getInt(TITLE)
|
val titleId = requireArguments().getInt(TITLE)
|
||||||
val cancellable = requireArguments().getBoolean(CANCELLABLE)
|
val cancellable = requireArguments().getBoolean(CANCELLABLE)
|
||||||
|
@ -61,6 +63,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
binding.message.isSelected = true
|
||||||
viewLifecycleOwner.lifecycleScope.apply {
|
viewLifecycleOwner.lifecycleScope.apply {
|
||||||
launch {
|
launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
@ -97,6 +100,35 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
taskViewModel.progress.collect {
|
||||||
|
if (it != 0.0) {
|
||||||
|
binding.progressBar.apply {
|
||||||
|
isIndeterminate = false
|
||||||
|
progress = (
|
||||||
|
(it / taskViewModel.maxProgress.value) *
|
||||||
|
PROGRESS_BAR_RESOLUTION
|
||||||
|
).toInt()
|
||||||
|
min = 0
|
||||||
|
max = PROGRESS_BAR_RESOLUTION
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
taskViewModel.message.collect {
|
||||||
|
if (it.isEmpty()) {
|
||||||
|
binding.message.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
binding.message.visibility = View.VISIBLE
|
||||||
|
binding.message.text = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,6 +140,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
|
||||||
val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE)
|
val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE)
|
||||||
negativeButton.setOnClickListener {
|
negativeButton.setOnClickListener {
|
||||||
alertDialog.setTitle(getString(R.string.cancelling))
|
alertDialog.setTitle(getString(R.string.cancelling))
|
||||||
|
binding.progressBar.isIndeterminate = true
|
||||||
taskViewModel.setCancelled(true)
|
taskViewModel.setCancelled(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,9 +155,12 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
|
||||||
activity: FragmentActivity,
|
activity: FragmentActivity,
|
||||||
titleId: Int,
|
titleId: Int,
|
||||||
cancellable: Boolean = false,
|
cancellable: Boolean = false,
|
||||||
task: suspend () -> Any
|
task: suspend (
|
||||||
): IndeterminateProgressDialogFragment {
|
progressCallback: (max: Long, progress: Long) -> Boolean,
|
||||||
val dialog = IndeterminateProgressDialogFragment()
|
messageCallback: (message: String) -> Unit
|
||||||
|
) -> Any
|
||||||
|
): ProgressDialogFragment {
|
||||||
|
val dialog = ProgressDialogFragment()
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
ViewModelProvider(activity)[TaskViewModel::class.java].task = task
|
ViewModelProvider(activity)[TaskViewModel::class.java].task = task
|
||||||
args.putInt(TITLE, titleId)
|
args.putInt(TITLE, titleId)
|
|
@ -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,15 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.model
|
||||||
|
|
||||||
|
enum class InstallResult(val int: Int) {
|
||||||
|
Success(0),
|
||||||
|
Overwrite(1),
|
||||||
|
Failure(2),
|
||||||
|
BaseInstallAttempted(3);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(int: Int): InstallResult = entries.firstOrNull { it.int == int } ?: Success
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class TaskViewModel : ViewModel() {
|
class TaskViewModel : ViewModel() {
|
||||||
|
@ -23,13 +24,28 @@ class TaskViewModel : ViewModel() {
|
||||||
val cancelled: StateFlow<Boolean> get() = _cancelled
|
val cancelled: StateFlow<Boolean> get() = _cancelled
|
||||||
private val _cancelled = MutableStateFlow(false)
|
private val _cancelled = MutableStateFlow(false)
|
||||||
|
|
||||||
lateinit var task: suspend () -> Any
|
private val _progress = MutableStateFlow(0.0)
|
||||||
|
val progress = _progress.asStateFlow()
|
||||||
|
|
||||||
|
private val _maxProgress = MutableStateFlow(0.0)
|
||||||
|
val maxProgress = _maxProgress.asStateFlow()
|
||||||
|
|
||||||
|
private val _message = MutableStateFlow("")
|
||||||
|
val message = _message.asStateFlow()
|
||||||
|
|
||||||
|
lateinit var task: suspend (
|
||||||
|
progressCallback: (max: Long, progress: Long) -> Boolean,
|
||||||
|
messageCallback: (message: String) -> Unit
|
||||||
|
) -> Any
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
_result.value = Any()
|
_result.value = Any()
|
||||||
_isComplete.value = false
|
_isComplete.value = false
|
||||||
_isRunning.value = false
|
_isRunning.value = false
|
||||||
_cancelled.value = false
|
_cancelled.value = false
|
||||||
|
_progress.value = 0.0
|
||||||
|
_maxProgress.value = 0.0
|
||||||
|
_message.value = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCancelled(value: Boolean) {
|
fun setCancelled(value: Boolean) {
|
||||||
|
@ -43,7 +59,16 @@ class TaskViewModel : ViewModel() {
|
||||||
_isRunning.value = true
|
_isRunning.value = true
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val res = task()
|
val res = task(
|
||||||
|
{ max, progress ->
|
||||||
|
_maxProgress.value = max.toDouble()
|
||||||
|
_progress.value = progress.toDouble()
|
||||||
|
return@task cancelled.value
|
||||||
|
},
|
||||||
|
{ message ->
|
||||||
|
_message.value = message
|
||||||
|
}
|
||||||
|
)
|
||||||
_result.value = res
|
_result.value = res
|
||||||
_isComplete.value = true
|
_isComplete.value = true
|
||||||
_isRunning.value = false
|
_isRunning.value = false
|
||||||
|
|
|
@ -38,12 +38,13 @@ import org.yuzu.yuzu_emu.activities.EmulationActivity
|
||||||
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
|
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
||||||
import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
|
import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
|
||||||
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
|
import org.yuzu.yuzu_emu.fragments.ProgressDialogFragment
|
||||||
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
||||||
import org.yuzu.yuzu_emu.model.AddonViewModel
|
import org.yuzu.yuzu_emu.model.AddonViewModel
|
||||||
import org.yuzu.yuzu_emu.model.DriverViewModel
|
import org.yuzu.yuzu_emu.model.DriverViewModel
|
||||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
|
import org.yuzu.yuzu_emu.model.InstallResult
|
||||||
import org.yuzu.yuzu_emu.model.TaskState
|
import org.yuzu.yuzu_emu.model.TaskState
|
||||||
import org.yuzu.yuzu_emu.model.TaskViewModel
|
import org.yuzu.yuzu_emu.model.TaskViewModel
|
||||||
import org.yuzu.yuzu_emu.utils.*
|
import org.yuzu.yuzu_emu.utils.*
|
||||||
|
@ -369,26 +370,23 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
return@registerForActivityResult
|
return@registerForActivityResult
|
||||||
}
|
}
|
||||||
|
|
||||||
val inputZip = contentResolver.openInputStream(result)
|
|
||||||
if (inputZip == null) {
|
|
||||||
Toast.makeText(
|
|
||||||
applicationContext,
|
|
||||||
getString(R.string.fatal_error),
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
return@registerForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") }
|
val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") }
|
||||||
|
|
||||||
val firmwarePath =
|
val firmwarePath =
|
||||||
File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/")
|
File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/")
|
||||||
val cacheFirmwareDir = File("${cacheDir.path}/registered/")
|
val cacheFirmwareDir = File("${cacheDir.path}/registered/")
|
||||||
|
|
||||||
val task: () -> Any = {
|
ProgressDialogFragment.newInstance(
|
||||||
|
this,
|
||||||
|
R.string.firmware_installing
|
||||||
|
) { progressCallback, _ ->
|
||||||
var messageToShow: Any
|
var messageToShow: Any
|
||||||
try {
|
try {
|
||||||
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheFirmwareDir)
|
FileUtil.unzipToInternalStorage(
|
||||||
|
result.toString(),
|
||||||
|
cacheFirmwareDir,
|
||||||
|
progressCallback
|
||||||
|
)
|
||||||
val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
|
val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
|
||||||
val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
|
val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
|
||||||
messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
|
messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
|
||||||
|
@ -404,18 +402,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
getString(R.string.save_file_imported_success)
|
getString(R.string.save_file_imported_success)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.error("[MainActivity] Firmware install failed - ${e.message}")
|
||||||
messageToShow = getString(R.string.fatal_error)
|
messageToShow = getString(R.string.fatal_error)
|
||||||
} finally {
|
} finally {
|
||||||
cacheFirmwareDir.deleteRecursively()
|
cacheFirmwareDir.deleteRecursively()
|
||||||
}
|
}
|
||||||
messageToShow
|
messageToShow
|
||||||
}
|
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
|
||||||
|
|
||||||
IndeterminateProgressDialogFragment.newInstance(
|
|
||||||
this,
|
|
||||||
R.string.firmware_installing,
|
|
||||||
task = task
|
|
||||||
).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val getAmiiboKey =
|
val getAmiiboKey =
|
||||||
|
@ -474,11 +467,11 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
return@registerForActivityResult
|
return@registerForActivityResult
|
||||||
}
|
}
|
||||||
|
|
||||||
IndeterminateProgressDialogFragment.newInstance(
|
ProgressDialogFragment.newInstance(
|
||||||
this@MainActivity,
|
this@MainActivity,
|
||||||
R.string.verifying_content,
|
R.string.verifying_content,
|
||||||
false
|
false
|
||||||
) {
|
) { _, _ ->
|
||||||
var updatesMatchProgram = true
|
var updatesMatchProgram = true
|
||||||
for (document in documents) {
|
for (document in documents) {
|
||||||
val valid = NativeLibrary.doesUpdateMatchProgram(
|
val valid = NativeLibrary.doesUpdateMatchProgram(
|
||||||
|
@ -501,44 +494,42 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
positiveAction = { homeViewModel.setContentToInstall(documents) }
|
positiveAction = { homeViewModel.setContentToInstall(documents) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun installContent(documents: List<Uri>) {
|
private fun installContent(documents: List<Uri>) {
|
||||||
IndeterminateProgressDialogFragment.newInstance(
|
ProgressDialogFragment.newInstance(
|
||||||
this@MainActivity,
|
this@MainActivity,
|
||||||
R.string.installing_game_content
|
R.string.installing_game_content
|
||||||
) {
|
) { progressCallback, messageCallback ->
|
||||||
var installSuccess = 0
|
var installSuccess = 0
|
||||||
var installOverwrite = 0
|
var installOverwrite = 0
|
||||||
var errorBaseGame = 0
|
var errorBaseGame = 0
|
||||||
var errorExtension = 0
|
var error = 0
|
||||||
var errorOther = 0
|
|
||||||
documents.forEach {
|
documents.forEach {
|
||||||
|
messageCallback.invoke(FileUtil.getFilename(it))
|
||||||
when (
|
when (
|
||||||
|
InstallResult.from(
|
||||||
NativeLibrary.installFileToNand(
|
NativeLibrary.installFileToNand(
|
||||||
it.toString(),
|
it.toString(),
|
||||||
FileUtil.getExtension(it)
|
progressCallback
|
||||||
|
)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
NativeLibrary.InstallFileToNandResult.Success -> {
|
InstallResult.Success -> {
|
||||||
installSuccess += 1
|
installSuccess += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> {
|
InstallResult.Overwrite -> {
|
||||||
installOverwrite += 1
|
installOverwrite += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> {
|
InstallResult.BaseInstallAttempted -> {
|
||||||
errorBaseGame += 1
|
errorBaseGame += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> {
|
InstallResult.Failure -> {
|
||||||
errorExtension += 1
|
error += 1
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
errorOther += 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -565,7 +556,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
)
|
)
|
||||||
installResult.append(separator)
|
installResult.append(separator)
|
||||||
}
|
}
|
||||||
val errorTotal: Int = errorBaseGame + errorExtension + errorOther
|
val errorTotal: Int = errorBaseGame + error
|
||||||
if (errorTotal > 0) {
|
if (errorTotal > 0) {
|
||||||
installResult.append(separator)
|
installResult.append(separator)
|
||||||
installResult.append(
|
installResult.append(
|
||||||
|
@ -582,14 +573,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
)
|
)
|
||||||
installResult.append(separator)
|
installResult.append(separator)
|
||||||
}
|
}
|
||||||
if (errorExtension > 0) {
|
if (error > 0) {
|
||||||
installResult.append(separator)
|
|
||||||
installResult.append(
|
|
||||||
getString(R.string.install_game_content_failure_file_extension)
|
|
||||||
)
|
|
||||||
installResult.append(separator)
|
|
||||||
}
|
|
||||||
if (errorOther > 0) {
|
|
||||||
installResult.append(
|
installResult.append(
|
||||||
getString(R.string.install_game_content_failure_description)
|
getString(R.string.install_game_content_failure_description)
|
||||||
)
|
)
|
||||||
|
@ -608,7 +592,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
descriptionString = installResult.toString().trim()
|
descriptionString = installResult.toString().trim()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
val exportUserData = registerForActivityResult(
|
val exportUserData = registerForActivityResult(
|
||||||
|
@ -618,16 +602,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
return@registerForActivityResult
|
return@registerForActivityResult
|
||||||
}
|
}
|
||||||
|
|
||||||
IndeterminateProgressDialogFragment.newInstance(
|
ProgressDialogFragment.newInstance(
|
||||||
this,
|
this,
|
||||||
R.string.exporting_user_data,
|
R.string.exporting_user_data,
|
||||||
true
|
true
|
||||||
) {
|
) { progressCallback, _ ->
|
||||||
val zipResult = FileUtil.zipFromInternalStorage(
|
val zipResult = FileUtil.zipFromInternalStorage(
|
||||||
File(DirectoryInitialization.userDirectory!!),
|
File(DirectoryInitialization.userDirectory!!),
|
||||||
DirectoryInitialization.userDirectory!!,
|
DirectoryInitialization.userDirectory!!,
|
||||||
BufferedOutputStream(contentResolver.openOutputStream(result)),
|
BufferedOutputStream(contentResolver.openOutputStream(result)),
|
||||||
taskViewModel.cancelled,
|
progressCallback,
|
||||||
compression = false
|
compression = false
|
||||||
)
|
)
|
||||||
return@newInstance when (zipResult) {
|
return@newInstance when (zipResult) {
|
||||||
|
@ -635,7 +619,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
TaskState.Failed -> R.string.export_failed
|
TaskState.Failed -> R.string.export_failed
|
||||||
TaskState.Cancelled -> R.string.user_data_export_cancelled
|
TaskState.Cancelled -> R.string.user_data_export_cancelled
|
||||||
}
|
}
|
||||||
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
val importUserData =
|
val importUserData =
|
||||||
|
@ -644,10 +628,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
return@registerForActivityResult
|
return@registerForActivityResult
|
||||||
}
|
}
|
||||||
|
|
||||||
IndeterminateProgressDialogFragment.newInstance(
|
ProgressDialogFragment.newInstance(
|
||||||
this,
|
this,
|
||||||
R.string.importing_user_data
|
R.string.importing_user_data
|
||||||
) {
|
) { progressCallback, _ ->
|
||||||
val checkStream =
|
val checkStream =
|
||||||
ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result)))
|
ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result)))
|
||||||
var isYuzuBackup = false
|
var isYuzuBackup = false
|
||||||
|
@ -676,8 +660,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
// Copy archive to internal storage
|
// Copy archive to internal storage
|
||||||
try {
|
try {
|
||||||
FileUtil.unzipToInternalStorage(
|
FileUtil.unzipToInternalStorage(
|
||||||
BufferedInputStream(contentResolver.openInputStream(result)),
|
result.toString(),
|
||||||
File(DirectoryInitialization.userDirectory!!)
|
File(DirectoryInitialization.userDirectory!!),
|
||||||
|
progressCallback
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return@newInstance MessageDialogFragment.newInstance(
|
return@newInstance MessageDialogFragment.newInstance(
|
||||||
|
@ -694,6 +679,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
driverViewModel.reloadDriverData()
|
driverViewModel.reloadDriverData()
|
||||||
|
|
||||||
return@newInstance getString(R.string.user_data_import_success)
|
return@newInstance getString(R.string.user_data_import_success)
|
||||||
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
@ -19,6 +18,7 @@ import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
|
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
|
||||||
import org.yuzu.yuzu_emu.model.TaskState
|
import org.yuzu.yuzu_emu.model.TaskState
|
||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.OutputStream
|
||||||
import java.lang.NullPointerException
|
import java.lang.NullPointerException
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.zip.Deflater
|
import java.util.zip.Deflater
|
||||||
|
@ -283,12 +283,34 @@ object FileUtil {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the given zip file into the given directory.
|
* Extracts the given zip file into the given directory.
|
||||||
|
* @param path String representation of a [Uri] or a typical path delimited by '/'
|
||||||
|
* @param destDir Location to unzip the contents of [path] into
|
||||||
|
* @param progressCallback Lambda that is called with the total number of files and the current
|
||||||
|
* progress through the process. Stops execution as soon as possible if this returns true.
|
||||||
*/
|
*/
|
||||||
@Throws(SecurityException::class)
|
@Throws(SecurityException::class)
|
||||||
fun unzipToInternalStorage(zipStream: BufferedInputStream, destDir: File) {
|
fun unzipToInternalStorage(
|
||||||
ZipInputStream(zipStream).use { zis ->
|
path: String,
|
||||||
|
destDir: File,
|
||||||
|
progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false }
|
||||||
|
) {
|
||||||
|
var totalEntries = 0L
|
||||||
|
ZipInputStream(getInputStream(path)).use { zis ->
|
||||||
|
var tempEntry = zis.nextEntry
|
||||||
|
while (tempEntry != null) {
|
||||||
|
tempEntry = zis.nextEntry
|
||||||
|
totalEntries++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var progress = 0L
|
||||||
|
ZipInputStream(getInputStream(path)).use { zis ->
|
||||||
var entry: ZipEntry? = zis.nextEntry
|
var entry: ZipEntry? = zis.nextEntry
|
||||||
while (entry != null) {
|
while (entry != null) {
|
||||||
|
if (progressCallback.invoke(totalEntries, progress)) {
|
||||||
|
return@use
|
||||||
|
}
|
||||||
|
|
||||||
val newFile = File(destDir, entry.name)
|
val newFile = File(destDir, entry.name)
|
||||||
val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile
|
val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile
|
||||||
|
|
||||||
|
@ -304,6 +326,7 @@ object FileUtil {
|
||||||
newFile.outputStream().use { fos -> zis.copyTo(fos) }
|
newFile.outputStream().use { fos -> zis.copyTo(fos) }
|
||||||
}
|
}
|
||||||
entry = zis.nextEntry
|
entry = zis.nextEntry
|
||||||
|
progress++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -313,14 +336,15 @@ object FileUtil {
|
||||||
* @param inputFile File representation of the item that will be zipped
|
* @param inputFile File representation of the item that will be zipped
|
||||||
* @param rootDir Directory containing the inputFile
|
* @param rootDir Directory containing the inputFile
|
||||||
* @param outputStream Stream where the zip file will be output
|
* @param outputStream Stream where the zip file will be output
|
||||||
* @param cancelled [StateFlow] that reports whether this process has been cancelled
|
* @param progressCallback Lambda that is called with the total number of files and the current
|
||||||
|
* progress through the process. Stops execution as soon as possible if this returns true.
|
||||||
* @param compression Disables compression if true
|
* @param compression Disables compression if true
|
||||||
*/
|
*/
|
||||||
fun zipFromInternalStorage(
|
fun zipFromInternalStorage(
|
||||||
inputFile: File,
|
inputFile: File,
|
||||||
rootDir: String,
|
rootDir: String,
|
||||||
outputStream: BufferedOutputStream,
|
outputStream: BufferedOutputStream,
|
||||||
cancelled: StateFlow<Boolean>? = null,
|
progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false },
|
||||||
compression: Boolean = true
|
compression: Boolean = true
|
||||||
): TaskState {
|
): TaskState {
|
||||||
try {
|
try {
|
||||||
|
@ -330,8 +354,10 @@ object FileUtil {
|
||||||
zos.setLevel(Deflater.NO_COMPRESSION)
|
zos.setLevel(Deflater.NO_COMPRESSION)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var count = 0L
|
||||||
|
val totalFiles = inputFile.walkTopDown().count().toLong()
|
||||||
inputFile.walkTopDown().forEach { file ->
|
inputFile.walkTopDown().forEach { file ->
|
||||||
if (cancelled?.value == true) {
|
if (progressCallback.invoke(totalFiles, count)) {
|
||||||
return TaskState.Cancelled
|
return TaskState.Cancelled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -343,6 +369,7 @@ object FileUtil {
|
||||||
if (file.isFile) {
|
if (file.isFile) {
|
||||||
file.inputStream().use { fis -> fis.copyTo(zos) }
|
file.inputStream().use { fis -> fis.copyTo(zos) }
|
||||||
}
|
}
|
||||||
|
count++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -356,9 +383,14 @@ object FileUtil {
|
||||||
/**
|
/**
|
||||||
* Helper function that copies the contents of a DocumentFile folder into a [File]
|
* Helper function that copies the contents of a DocumentFile folder into a [File]
|
||||||
* @param file [File] representation of the folder to copy into
|
* @param file [File] representation of the folder to copy into
|
||||||
|
* @param progressCallback Lambda that is called with the total number of files and the current
|
||||||
|
* progress through the process. Stops execution as soon as possible if this returns true.
|
||||||
* @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa
|
* @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa
|
||||||
*/
|
*/
|
||||||
fun DocumentFile.copyFilesTo(file: File) {
|
fun DocumentFile.copyFilesTo(
|
||||||
|
file: File,
|
||||||
|
progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false }
|
||||||
|
) {
|
||||||
file.mkdirs()
|
file.mkdirs()
|
||||||
if (!this.isDirectory || !file.isDirectory) {
|
if (!this.isDirectory || !file.isDirectory) {
|
||||||
throw IllegalStateException(
|
throw IllegalStateException(
|
||||||
|
@ -366,7 +398,13 @@ object FileUtil {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var count = 0L
|
||||||
|
val totalFiles = this.listFiles().size.toLong()
|
||||||
this.listFiles().forEach {
|
this.listFiles().forEach {
|
||||||
|
if (progressCallback.invoke(totalFiles, count)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val newFile = File(file, it.name!!)
|
val newFile = File(file, it.name!!)
|
||||||
if (it.isDirectory) {
|
if (it.isDirectory) {
|
||||||
newFile.mkdirs()
|
newFile.mkdirs()
|
||||||
|
@ -381,6 +419,7 @@ object FileUtil {
|
||||||
newFile.outputStream().use { os -> bos.copyTo(os) }
|
newFile.outputStream().use { os -> bos.copyTo(os) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
count++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -427,6 +466,18 @@ object FileUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getInputStream(path: String) = if (path.contains("content://")) {
|
||||||
|
Uri.parse(path).inputStream()
|
||||||
|
} else {
|
||||||
|
File(path).inputStream()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOutputStream(path: String) = if (path.contains("content://")) {
|
||||||
|
Uri.parse(path).outputStream()
|
||||||
|
} else {
|
||||||
|
File(path).outputStream()
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun getStringFromFile(file: File): String =
|
fun getStringFromFile(file: File): String =
|
||||||
String(file.readBytes(), StandardCharsets.UTF_8)
|
String(file.readBytes(), StandardCharsets.UTF_8)
|
||||||
|
@ -434,4 +485,19 @@ object FileUtil {
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun getStringFromInputStream(stream: InputStream): String =
|
fun getStringFromInputStream(stream: InputStream): String =
|
||||||
String(stream.readBytes(), StandardCharsets.UTF_8)
|
String(stream.readBytes(), StandardCharsets.UTF_8)
|
||||||
|
|
||||||
|
fun DocumentFile.inputStream(): InputStream =
|
||||||
|
YuzuApplication.appContext.contentResolver.openInputStream(uri)!!
|
||||||
|
|
||||||
|
fun DocumentFile.outputStream(): OutputStream =
|
||||||
|
YuzuApplication.appContext.contentResolver.openOutputStream(uri)!!
|
||||||
|
|
||||||
|
fun Uri.inputStream(): InputStream =
|
||||||
|
YuzuApplication.appContext.contentResolver.openInputStream(this)!!
|
||||||
|
|
||||||
|
fun Uri.outputStream(): OutputStream =
|
||||||
|
YuzuApplication.appContext.contentResolver.openOutputStream(this)!!
|
||||||
|
|
||||||
|
fun Uri.asDocumentFile(): DocumentFile? =
|
||||||
|
DocumentFile.fromSingleUri(YuzuApplication.appContext, this)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.utils
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import java.io.BufferedInputStream
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import org.yuzu.yuzu_emu.NativeLibrary
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
|
@ -123,7 +122,7 @@ object GpuDriverHelper {
|
||||||
// Unzip the driver.
|
// Unzip the driver.
|
||||||
try {
|
try {
|
||||||
FileUtil.unzipToInternalStorage(
|
FileUtil.unzipToInternalStorage(
|
||||||
BufferedInputStream(copiedFile.inputStream()),
|
copiedFile.path,
|
||||||
File(driverInstallationPath!!)
|
File(driverInstallationPath!!)
|
||||||
)
|
)
|
||||||
} catch (e: SecurityException) {
|
} catch (e: SecurityException) {
|
||||||
|
@ -156,7 +155,7 @@ object GpuDriverHelper {
|
||||||
// Unzip the driver to the private installation directory
|
// Unzip the driver to the private installation directory
|
||||||
try {
|
try {
|
||||||
FileUtil.unzipToInternalStorage(
|
FileUtil.unzipToInternalStorage(
|
||||||
BufferedInputStream(driver.inputStream()),
|
driver.path,
|
||||||
File(driverInstallationPath!!)
|
File(driverInstallationPath!!)
|
||||||
)
|
)
|
||||||
} catch (e: SecurityException) {
|
} catch (e: SecurityException) {
|
||||||
|
|
|
@ -42,3 +42,19 @@ double GetJDouble(JNIEnv* env, jobject jdouble) {
|
||||||
jobject ToJDouble(JNIEnv* env, double value) {
|
jobject ToJDouble(JNIEnv* env, double value) {
|
||||||
return env->NewObject(IDCache::GetDoubleClass(), IDCache::GetDoubleConstructor(), value);
|
return env->NewObject(IDCache::GetDoubleClass(), IDCache::GetDoubleConstructor(), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s32 GetJInteger(JNIEnv* env, jobject jinteger) {
|
||||||
|
return env->GetIntField(jinteger, IDCache::GetIntegerValueField());
|
||||||
|
}
|
||||||
|
|
||||||
|
jobject ToJInteger(JNIEnv* env, s32 value) {
|
||||||
|
return env->NewObject(IDCache::GetIntegerClass(), IDCache::GetIntegerConstructor(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GetJBoolean(JNIEnv* env, jobject jboolean) {
|
||||||
|
return env->GetBooleanField(jboolean, IDCache::GetBooleanValueField());
|
||||||
|
}
|
||||||
|
|
||||||
|
jobject ToJBoolean(JNIEnv* env, bool value) {
|
||||||
|
return env->NewObject(IDCache::GetBooleanClass(), IDCache::GetBooleanConstructor(), value);
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
std::string GetJString(JNIEnv* env, jstring jstr);
|
std::string GetJString(JNIEnv* env, jstring jstr);
|
||||||
jstring ToJString(JNIEnv* env, std::string_view str);
|
jstring ToJString(JNIEnv* env, std::string_view str);
|
||||||
|
@ -13,3 +14,9 @@ jstring ToJString(JNIEnv* env, std::u16string_view str);
|
||||||
|
|
||||||
double GetJDouble(JNIEnv* env, jobject jdouble);
|
double GetJDouble(JNIEnv* env, jobject jdouble);
|
||||||
jobject ToJDouble(JNIEnv* env, double value);
|
jobject ToJDouble(JNIEnv* env, double value);
|
||||||
|
|
||||||
|
s32 GetJInteger(JNIEnv* env, jobject jinteger);
|
||||||
|
jobject ToJInteger(JNIEnv* env, s32 value);
|
||||||
|
|
||||||
|
bool GetJBoolean(JNIEnv* env, jobject jboolean);
|
||||||
|
jobject ToJBoolean(JNIEnv* env, bool value);
|
||||||
|
|
|
@ -43,10 +43,27 @@ 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;
|
||||||
|
|
||||||
|
static jclass s_integer_class;
|
||||||
|
static jmethodID s_integer_constructor;
|
||||||
|
static jfieldID s_integer_value_field;
|
||||||
|
|
||||||
|
static jclass s_boolean_class;
|
||||||
|
static jmethodID s_boolean_constructor;
|
||||||
|
static jfieldID s_boolean_value_field;
|
||||||
|
|
||||||
static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
|
static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
|
||||||
|
|
||||||
namespace IDCache {
|
namespace IDCache {
|
||||||
|
@ -186,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;
|
||||||
}
|
}
|
||||||
|
@ -198,6 +247,30 @@ jfieldID GetDoubleValueField() {
|
||||||
return s_double_value_field;
|
return s_double_value_field;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jclass GetIntegerClass() {
|
||||||
|
return s_integer_class;
|
||||||
|
}
|
||||||
|
|
||||||
|
jmethodID GetIntegerConstructor() {
|
||||||
|
return s_integer_constructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
jfieldID GetIntegerValueField() {
|
||||||
|
return s_integer_value_field;
|
||||||
|
}
|
||||||
|
|
||||||
|
jclass GetBooleanClass() {
|
||||||
|
return s_boolean_class;
|
||||||
|
}
|
||||||
|
|
||||||
|
jmethodID GetBooleanConstructor() {
|
||||||
|
return s_boolean_constructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
jfieldID GetBooleanValueField() {
|
||||||
|
return s_boolean_value_field;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace IDCache
|
} // namespace IDCache
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
@ -278,12 +351,37 @@ 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");
|
||||||
s_double_value_field = env->GetFieldID(double_class, "value", "D");
|
s_double_value_field = env->GetFieldID(double_class, "value", "D");
|
||||||
env->DeleteLocalRef(double_class);
|
env->DeleteLocalRef(double_class);
|
||||||
|
|
||||||
|
const jclass int_class = env->FindClass("java/lang/Integer");
|
||||||
|
s_integer_class = reinterpret_cast<jclass>(env->NewGlobalRef(int_class));
|
||||||
|
s_integer_constructor = env->GetMethodID(int_class, "<init>", "(I)V");
|
||||||
|
s_integer_value_field = env->GetFieldID(int_class, "value", "I");
|
||||||
|
env->DeleteLocalRef(int_class);
|
||||||
|
|
||||||
|
const jclass boolean_class = env->FindClass("java/lang/Boolean");
|
||||||
|
s_boolean_class = reinterpret_cast<jclass>(env->NewGlobalRef(boolean_class));
|
||||||
|
s_boolean_constructor = env->GetMethodID(boolean_class, "<init>", "(Z)V");
|
||||||
|
s_boolean_value_field = env->GetFieldID(boolean_class, "value", "Z");
|
||||||
|
env->DeleteLocalRef(boolean_class);
|
||||||
|
|
||||||
// Initialize Android Storage
|
// Initialize Android Storage
|
||||||
Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
|
Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
|
||||||
|
|
||||||
|
@ -309,7 +407,10 @@ 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_boolean_class);
|
||||||
|
|
||||||
// UnInitialize applets
|
// UnInitialize applets
|
||||||
SoftwareKeyboard::CleanupJNI(env);
|
SoftwareKeyboard::CleanupJNI(env);
|
||||||
|
|
|
@ -43,8 +43,25 @@ 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();
|
||||||
|
|
||||||
|
jclass GetIntegerClass();
|
||||||
|
jmethodID GetIntegerConstructor();
|
||||||
|
jfieldID GetIntegerValueField();
|
||||||
|
|
||||||
|
jclass GetBooleanClass();
|
||||||
|
jmethodID GetBooleanConstructor();
|
||||||
|
jfieldID GetBooleanValueField();
|
||||||
|
|
||||||
} // namespace IDCache
|
} // namespace IDCache
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
#include <core/file_sys/patch_manager.h>
|
#include <core/file_sys/patch_manager.h>
|
||||||
#include <core/file_sys/savedata_factory.h>
|
#include <core/file_sys/savedata_factory.h>
|
||||||
#include <core/loader/nro.h>
|
#include <core/loader/nro.h>
|
||||||
|
#include <frontend_common/content_manager.h>
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
|
|
||||||
#include "common/detached_tasks.h"
|
#include "common/detached_tasks.h"
|
||||||
|
@ -100,67 +101,6 @@ void EmulationSession::SetNativeWindow(ANativeWindow* native_window) {
|
||||||
m_native_window = native_window;
|
m_native_window = native_window;
|
||||||
}
|
}
|
||||||
|
|
||||||
int EmulationSession::InstallFileToNand(std::string filename, std::string file_extension) {
|
|
||||||
jconst copy_func = [](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
|
|
||||||
std::size_t block_size) {
|
|
||||||
if (src == nullptr || dest == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!dest->Resize(src->GetSize())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
using namespace Common::Literals;
|
|
||||||
[[maybe_unused]] std::vector<u8> buffer(1_MiB);
|
|
||||||
|
|
||||||
for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
|
|
||||||
jconst read = src->Read(buffer.data(), buffer.size(), i);
|
|
||||||
dest->Write(buffer.data(), read, i);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum InstallResult {
|
|
||||||
Success = 0,
|
|
||||||
SuccessFileOverwritten = 1,
|
|
||||||
InstallError = 2,
|
|
||||||
ErrorBaseGame = 3,
|
|
||||||
ErrorFilenameExtension = 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
[[maybe_unused]] std::shared_ptr<FileSys::NSP> nsp;
|
|
||||||
if (file_extension == "nsp") {
|
|
||||||
nsp = std::make_shared<FileSys::NSP>(m_vfs->OpenFile(filename, FileSys::Mode::Read));
|
|
||||||
if (nsp->IsExtractedType()) {
|
|
||||||
return InstallError;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return ErrorFilenameExtension;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!nsp) {
|
|
||||||
return InstallError;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nsp->GetStatus() != Loader::ResultStatus::Success) {
|
|
||||||
return InstallError;
|
|
||||||
}
|
|
||||||
|
|
||||||
jconst res = m_system.GetFileSystemController().GetUserNANDContents()->InstallEntry(*nsp, true,
|
|
||||||
copy_func);
|
|
||||||
|
|
||||||
switch (res) {
|
|
||||||
case FileSys::InstallResult::Success:
|
|
||||||
return Success;
|
|
||||||
case FileSys::InstallResult::OverwriteExisting:
|
|
||||||
return SuccessFileOverwritten;
|
|
||||||
case FileSys::InstallResult::ErrorBaseInstall:
|
|
||||||
return ErrorBaseGame;
|
|
||||||
default:
|
|
||||||
return InstallError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void EmulationSession::InitializeGpuDriver(const std::string& hook_lib_dir,
|
void EmulationSession::InitializeGpuDriver(const std::string& hook_lib_dir,
|
||||||
const std::string& custom_driver_dir,
|
const std::string& custom_driver_dir,
|
||||||
const std::string& custom_driver_name,
|
const std::string& custom_driver_name,
|
||||||
|
@ -512,10 +452,20 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, jobject
|
||||||
}
|
}
|
||||||
|
|
||||||
int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject instance,
|
int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject instance,
|
||||||
jstring j_file,
|
jstring j_file, jobject jcallback) {
|
||||||
jstring j_file_extension) {
|
auto jlambdaClass = env->GetObjectClass(jcallback);
|
||||||
return EmulationSession::GetInstance().InstallFileToNand(GetJString(env, j_file),
|
auto jlambdaInvokeMethod = env->GetMethodID(
|
||||||
GetJString(env, j_file_extension));
|
jlambdaClass, "invoke", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
|
||||||
|
const auto callback = [env, jcallback, jlambdaInvokeMethod](size_t max, size_t progress) {
|
||||||
|
auto jwasCancelled = env->CallObjectMethod(jcallback, jlambdaInvokeMethod,
|
||||||
|
ToJDouble(env, max), ToJDouble(env, progress));
|
||||||
|
return GetJBoolean(env, jwasCancelled);
|
||||||
|
};
|
||||||
|
|
||||||
|
return static_cast<int>(
|
||||||
|
ContentManager::InstallNSP(&EmulationSession::GetInstance().System(),
|
||||||
|
EmulationSession::GetInstance().System().GetFilesystem().get(),
|
||||||
|
GetJString(env, j_file), callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* env, jobject jobj,
|
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* env, jobject jobj,
|
||||||
|
@ -824,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);
|
||||||
|
@ -843,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,
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include "core/file_sys/registered_cache.h"
|
#include "core/file_sys/registered_cache.h"
|
||||||
#include "core/hle/service/acc/profile_manager.h"
|
#include "core/hle/service/acc/profile_manager.h"
|
||||||
#include "core/perf_stats.h"
|
#include "core/perf_stats.h"
|
||||||
|
#include "frontend_common/content_manager.h"
|
||||||
#include "jni/applets/software_keyboard.h"
|
#include "jni/applets/software_keyboard.h"
|
||||||
#include "jni/emu_window/emu_window.h"
|
#include "jni/emu_window/emu_window.h"
|
||||||
#include "video_core/rasterizer_interface.h"
|
#include "video_core/rasterizer_interface.h"
|
||||||
|
@ -29,7 +30,6 @@ public:
|
||||||
void SetNativeWindow(ANativeWindow* native_window);
|
void SetNativeWindow(ANativeWindow* native_window);
|
||||||
void SurfaceChanged();
|
void SurfaceChanged();
|
||||||
|
|
||||||
int InstallFileToNand(std::string filename, std::string file_extension);
|
|
||||||
void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir,
|
void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir,
|
||||||
const std::string& custom_driver_name,
|
const std::string& custom_driver_name,
|
||||||
const std::string& file_redirect_dir);
|
const std::string& file_redirect_dir);
|
||||||
|
|
|
@ -1,8 +1,30 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<com.google.android.material.progressindicator.LinearProgressIndicator xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/message"
|
||||||
|
style="@style/TextAppearance.Material3.BodyMedium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginBottom="6dp"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:requiresFadingEdge="horizontal"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
android:id="@+id/progress_bar"
|
android:id="@+id/progress_bar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="24dp"
|
android:padding="24dp"
|
||||||
app:trackCornerRadius="4dp" />
|
app:trackCornerRadius="4dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
add_library(frontend_common STATIC
|
add_library(frontend_common STATIC
|
||||||
config.cpp
|
config.cpp
|
||||||
config.h
|
config.h
|
||||||
|
content_manager.h
|
||||||
)
|
)
|
||||||
|
|
||||||
create_target_directory_groups(frontend_common)
|
create_target_directory_groups(frontend_common)
|
||||||
|
|
|
@ -0,0 +1,238 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <boost/algorithm/string.hpp>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
#include "common/literals.h"
|
||||||
|
#include "core/core.h"
|
||||||
|
#include "core/file_sys/common_funcs.h"
|
||||||
|
#include "core/file_sys/content_archive.h"
|
||||||
|
#include "core/file_sys/mode.h"
|
||||||
|
#include "core/file_sys/nca_metadata.h"
|
||||||
|
#include "core/file_sys/registered_cache.h"
|
||||||
|
#include "core/file_sys/submission_package.h"
|
||||||
|
#include "core/hle/service/filesystem/filesystem.h"
|
||||||
|
#include "core/loader/loader.h"
|
||||||
|
|
||||||
|
namespace ContentManager {
|
||||||
|
|
||||||
|
enum class InstallResult {
|
||||||
|
Success,
|
||||||
|
Overwrite,
|
||||||
|
Failure,
|
||||||
|
BaseInstallAttempted,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Removes a single installed DLC
|
||||||
|
* \param fs_controller [FileSystemController] reference from the Core::System instance
|
||||||
|
* \param title_id Unique title ID representing the DLC which will be removed
|
||||||
|
* \return 'true' if successful
|
||||||
|
*/
|
||||||
|
inline bool RemoveDLC(const Service::FileSystem::FileSystemController& fs_controller,
|
||||||
|
const u64 title_id) {
|
||||||
|
return fs_controller.GetUserNANDContents()->RemoveExistingEntry(title_id) ||
|
||||||
|
fs_controller.GetSDMCContents()->RemoveExistingEntry(title_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Removes all DLC for a game
|
||||||
|
* \param system Raw pointer to the system instance
|
||||||
|
* \param program_id Program ID for the game that will have all of its DLC removed
|
||||||
|
* \return Number of DLC removed
|
||||||
|
*/
|
||||||
|
inline size_t RemoveAllDLC(Core::System* system, const u64 program_id) {
|
||||||
|
size_t count{};
|
||||||
|
const auto& fs_controller = system->GetFileSystemController();
|
||||||
|
const auto dlc_entries = system->GetContentProvider().ListEntriesFilter(
|
||||||
|
FileSys::TitleType::AOC, FileSys::ContentRecordType::Data);
|
||||||
|
std::vector<u64> program_dlc_entries;
|
||||||
|
|
||||||
|
for (const auto& entry : dlc_entries) {
|
||||||
|
if (FileSys::GetBaseTitleID(entry.title_id) == program_id) {
|
||||||
|
program_dlc_entries.push_back(entry.title_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& entry : program_dlc_entries) {
|
||||||
|
if (RemoveDLC(fs_controller, entry)) {
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Removes the installed update for a game
|
||||||
|
* \param fs_controller [FileSystemController] reference from the Core::System instance
|
||||||
|
* \param program_id Program ID for the game that will have its installed update removed
|
||||||
|
* \return 'true' if successful
|
||||||
|
*/
|
||||||
|
inline bool RemoveUpdate(const Service::FileSystem::FileSystemController& fs_controller,
|
||||||
|
const u64 program_id) {
|
||||||
|
const auto update_id = program_id | 0x800;
|
||||||
|
return fs_controller.GetUserNANDContents()->RemoveExistingEntry(update_id) ||
|
||||||
|
fs_controller.GetSDMCContents()->RemoveExistingEntry(update_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Removes the base content for a game
|
||||||
|
* \param fs_controller [FileSystemController] reference from the Core::System instance
|
||||||
|
* \param program_id Program ID for the game that will have its base content removed
|
||||||
|
* \return 'true' if successful
|
||||||
|
*/
|
||||||
|
inline bool RemoveBaseContent(const Service::FileSystem::FileSystemController& fs_controller,
|
||||||
|
const u64 program_id) {
|
||||||
|
return fs_controller.GetUserNANDContents()->RemoveExistingEntry(program_id) ||
|
||||||
|
fs_controller.GetSDMCContents()->RemoveExistingEntry(program_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Removes a mod for a game
|
||||||
|
* \param fs_controller [FileSystemController] reference from the Core::System instance
|
||||||
|
* \param program_id Program ID for the game where [mod_name] will be removed
|
||||||
|
* \param mod_name The name of a mod as given by FileSys::PatchManager::GetPatches. This corresponds
|
||||||
|
* with the name of the mod's directory in a game's load folder.
|
||||||
|
* \return 'true' if successful
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Installs an NSP
|
||||||
|
* \param system Raw pointer to the system instance
|
||||||
|
* \param vfs Raw pointer to the VfsFilesystem instance in Core::System
|
||||||
|
* \param filename Path to the NSP file
|
||||||
|
* \param callback Optional callback to report the progress of the installation. The first size_t
|
||||||
|
* parameter is the total size of the virtual file and the second is the current progress. If you
|
||||||
|
* return false to the callback, it will cancel the installation as soon as possible.
|
||||||
|
* \return [InstallResult] representing how the installation finished
|
||||||
|
*/
|
||||||
|
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)>()) {
|
||||||
|
const auto copy = [callback](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
|
||||||
|
std::size_t block_size) {
|
||||||
|
if (src == nullptr || dest == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!dest->Resize(src->GetSize())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
using namespace Common::Literals;
|
||||||
|
std::vector<u8> buffer(1_MiB);
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
|
||||||
|
if (callback(src->GetSize(), i)) {
|
||||||
|
dest->Resize(0);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto read = src->Read(buffer.data(), buffer.size(), i);
|
||||||
|
dest->Write(buffer.data(), read, i);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::shared_ptr<FileSys::NSP> nsp;
|
||||||
|
FileSys::VirtualFile file = vfs->OpenFile(filename, FileSys::Mode::Read);
|
||||||
|
if (boost::to_lower_copy(file->GetName()).ends_with(std::string("nsp"))) {
|
||||||
|
nsp = std::make_shared<FileSys::NSP>(file);
|
||||||
|
if (nsp->IsExtractedType()) {
|
||||||
|
return InstallResult::Failure;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return InstallResult::Failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nsp->GetStatus() != Loader::ResultStatus::Success) {
|
||||||
|
return InstallResult::Failure;
|
||||||
|
}
|
||||||
|
const auto res =
|
||||||
|
system->GetFileSystemController().GetUserNANDContents()->InstallEntry(*nsp, true, copy);
|
||||||
|
switch (res) {
|
||||||
|
case FileSys::InstallResult::Success:
|
||||||
|
return InstallResult::Success;
|
||||||
|
case FileSys::InstallResult::OverwriteExisting:
|
||||||
|
return InstallResult::Overwrite;
|
||||||
|
case FileSys::InstallResult::ErrorBaseInstall:
|
||||||
|
return InstallResult::BaseInstallAttempted;
|
||||||
|
default:
|
||||||
|
return InstallResult::Failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Installs an NCA
|
||||||
|
* \param vfs Raw pointer to the VfsFilesystem instance in Core::System
|
||||||
|
* \param filename Path to the NCA file
|
||||||
|
* \param registered_cache Raw pointer to the registered cache that the NCA will be installed to
|
||||||
|
* \param title_type Type of NCA package to install
|
||||||
|
* \param callback Optional callback to report the progress of the installation. The first size_t
|
||||||
|
* parameter is the total size of the virtual file and the second is the current progress. If you
|
||||||
|
* return false to the callback, it will cancel the installation as soon as possible.
|
||||||
|
* \return [InstallResult] representing how the installation finished
|
||||||
|
*/
|
||||||
|
inline InstallResult InstallNCA(
|
||||||
|
FileSys::VfsFilesystem* vfs, const std::string& filename,
|
||||||
|
FileSys::RegisteredCache* registered_cache, const FileSys::TitleType title_type,
|
||||||
|
const std::function<bool(size_t, size_t)>& callback = std::function<bool(size_t, size_t)>()) {
|
||||||
|
const auto copy = [callback](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
|
||||||
|
std::size_t block_size) {
|
||||||
|
if (src == nullptr || dest == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!dest->Resize(src->GetSize())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
using namespace Common::Literals;
|
||||||
|
std::vector<u8> buffer(1_MiB);
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
|
||||||
|
if (callback(src->GetSize(), i)) {
|
||||||
|
dest->Resize(0);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto read = src->Read(buffer.data(), buffer.size(), i);
|
||||||
|
dest->Write(buffer.data(), read, i);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto nca = std::make_shared<FileSys::NCA>(vfs->OpenFile(filename, FileSys::Mode::Read));
|
||||||
|
const auto id = nca->GetStatus();
|
||||||
|
|
||||||
|
// Game updates necessary are missing base RomFS
|
||||||
|
if (id != Loader::ResultStatus::Success &&
|
||||||
|
id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
|
||||||
|
return InstallResult::Failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto res = registered_cache->InstallEntry(*nca, title_type, true, copy);
|
||||||
|
if (res == FileSys::InstallResult::Success) {
|
||||||
|
return InstallResult::Success;
|
||||||
|
} else if (res == FileSys::InstallResult::OverwriteExisting) {
|
||||||
|
return InstallResult::Overwrite;
|
||||||
|
} else {
|
||||||
|
return InstallResult::Failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ContentManager
|
|
@ -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") {
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
#include "core/hle/service/am/applet_oe.h"
|
#include "core/hle/service/am/applet_oe.h"
|
||||||
#include "core/hle/service/am/applets/applets.h"
|
#include "core/hle/service/am/applets/applets.h"
|
||||||
#include "core/hle/service/set/system_settings_server.h"
|
#include "core/hle/service/set/system_settings_server.h"
|
||||||
|
#include "frontend_common/content_manager.h"
|
||||||
#include "hid_core/frontend/emulated_controller.h"
|
#include "hid_core/frontend/emulated_controller.h"
|
||||||
#include "hid_core/hid_core.h"
|
#include "hid_core/hid_core.h"
|
||||||
#include "yuzu/multiplayer/state.h"
|
#include "yuzu/multiplayer/state.h"
|
||||||
|
@ -2476,10 +2477,8 @@ void GMainWindow::OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryT
|
||||||
}
|
}
|
||||||
|
|
||||||
void GMainWindow::RemoveBaseContent(u64 program_id, InstalledEntryType type) {
|
void GMainWindow::RemoveBaseContent(u64 program_id, InstalledEntryType type) {
|
||||||
const auto& fs_controller = system->GetFileSystemController();
|
const auto res =
|
||||||
const auto res = fs_controller.GetUserNANDContents()->RemoveExistingEntry(program_id) ||
|
ContentManager::RemoveBaseContent(system->GetFileSystemController(), program_id);
|
||||||
fs_controller.GetSDMCContents()->RemoveExistingEntry(program_id);
|
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
QMessageBox::information(this, tr("Successfully Removed"),
|
QMessageBox::information(this, tr("Successfully Removed"),
|
||||||
tr("Successfully removed the installed base game."));
|
tr("Successfully removed the installed base game."));
|
||||||
|
@ -2491,11 +2490,7 @@ void GMainWindow::RemoveBaseContent(u64 program_id, InstalledEntryType type) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void GMainWindow::RemoveUpdateContent(u64 program_id, InstalledEntryType type) {
|
void GMainWindow::RemoveUpdateContent(u64 program_id, InstalledEntryType type) {
|
||||||
const auto update_id = program_id | 0x800;
|
const auto res = ContentManager::RemoveUpdate(system->GetFileSystemController(), program_id);
|
||||||
const auto& fs_controller = system->GetFileSystemController();
|
|
||||||
const auto res = fs_controller.GetUserNANDContents()->RemoveExistingEntry(update_id) ||
|
|
||||||
fs_controller.GetSDMCContents()->RemoveExistingEntry(update_id);
|
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
QMessageBox::information(this, tr("Successfully Removed"),
|
QMessageBox::information(this, tr("Successfully Removed"),
|
||||||
tr("Successfully removed the installed update."));
|
tr("Successfully removed the installed update."));
|
||||||
|
@ -2506,22 +2501,7 @@ void GMainWindow::RemoveUpdateContent(u64 program_id, InstalledEntryType type) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void GMainWindow::RemoveAddOnContent(u64 program_id, InstalledEntryType type) {
|
void GMainWindow::RemoveAddOnContent(u64 program_id, InstalledEntryType type) {
|
||||||
u32 count{};
|
const size_t count = ContentManager::RemoveAllDLC(system.get(), program_id);
|
||||||
const auto& fs_controller = system->GetFileSystemController();
|
|
||||||
const auto dlc_entries = system->GetContentProvider().ListEntriesFilter(
|
|
||||||
FileSys::TitleType::AOC, FileSys::ContentRecordType::Data);
|
|
||||||
|
|
||||||
for (const auto& entry : dlc_entries) {
|
|
||||||
if (FileSys::GetBaseTitleID(entry.title_id) == program_id) {
|
|
||||||
const auto res =
|
|
||||||
fs_controller.GetUserNANDContents()->RemoveExistingEntry(entry.title_id) ||
|
|
||||||
fs_controller.GetSDMCContents()->RemoveExistingEntry(entry.title_id);
|
|
||||||
if (res) {
|
|
||||||
++count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
QMessageBox::warning(this, GetGameListErrorRemoving(type),
|
QMessageBox::warning(this, GetGameListErrorRemoving(type),
|
||||||
tr("There are no DLC installed for this title."));
|
tr("There are no DLC installed for this title."));
|
||||||
|
@ -3290,12 +3270,21 @@ void GMainWindow::OnMenuInstallToNAND() {
|
||||||
install_progress->setLabelText(
|
install_progress->setLabelText(
|
||||||
tr("Installing file \"%1\"...").arg(QFileInfo(file).fileName()));
|
tr("Installing file \"%1\"...").arg(QFileInfo(file).fileName()));
|
||||||
|
|
||||||
QFuture<InstallResult> future;
|
QFuture<ContentManager::InstallResult> future;
|
||||||
InstallResult result;
|
ContentManager::InstallResult result;
|
||||||
|
|
||||||
if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
|
if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
|
||||||
|
const auto progress_callback = [this](size_t size, size_t progress) {
|
||||||
future = QtConcurrent::run([this, &file] { return InstallNSP(file); });
|
emit UpdateInstallProgress();
|
||||||
|
if (install_progress->wasCanceled()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
future = QtConcurrent::run([this, &file, progress_callback] {
|
||||||
|
return ContentManager::InstallNSP(system.get(), vfs.get(), file.toStdString(),
|
||||||
|
progress_callback);
|
||||||
|
});
|
||||||
|
|
||||||
while (!future.isFinished()) {
|
while (!future.isFinished()) {
|
||||||
QCoreApplication::processEvents();
|
QCoreApplication::processEvents();
|
||||||
|
@ -3311,16 +3300,16 @@ void GMainWindow::OnMenuInstallToNAND() {
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case InstallResult::Success:
|
case ContentManager::InstallResult::Success:
|
||||||
new_files.append(QFileInfo(file).fileName());
|
new_files.append(QFileInfo(file).fileName());
|
||||||
break;
|
break;
|
||||||
case InstallResult::Overwrite:
|
case ContentManager::InstallResult::Overwrite:
|
||||||
overwritten_files.append(QFileInfo(file).fileName());
|
overwritten_files.append(QFileInfo(file).fileName());
|
||||||
break;
|
break;
|
||||||
case InstallResult::Failure:
|
case ContentManager::InstallResult::Failure:
|
||||||
failed_files.append(QFileInfo(file).fileName());
|
failed_files.append(QFileInfo(file).fileName());
|
||||||
break;
|
break;
|
||||||
case InstallResult::BaseInstallAttempted:
|
case ContentManager::InstallResult::BaseInstallAttempted:
|
||||||
failed_files.append(QFileInfo(file).fileName());
|
failed_files.append(QFileInfo(file).fileName());
|
||||||
detected_base_install = true;
|
detected_base_install = true;
|
||||||
break;
|
break;
|
||||||
|
@ -3354,96 +3343,7 @@ void GMainWindow::OnMenuInstallToNAND() {
|
||||||
ui->action_Install_File_NAND->setEnabled(true);
|
ui->action_Install_File_NAND->setEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
InstallResult GMainWindow::InstallNSP(const QString& filename) {
|
ContentManager::InstallResult GMainWindow::InstallNCA(const QString& filename) {
|
||||||
const auto qt_raw_copy = [this](const FileSys::VirtualFile& src,
|
|
||||||
const FileSys::VirtualFile& dest, std::size_t block_size) {
|
|
||||||
if (src == nullptr || dest == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!dest->Resize(src->GetSize())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<u8> buffer(CopyBufferSize);
|
|
||||||
|
|
||||||
for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
|
|
||||||
if (install_progress->wasCanceled()) {
|
|
||||||
dest->Resize(0);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit UpdateInstallProgress();
|
|
||||||
|
|
||||||
const auto read = src->Read(buffer.data(), buffer.size(), i);
|
|
||||||
dest->Write(buffer.data(), read, i);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::shared_ptr<FileSys::NSP> nsp;
|
|
||||||
if (filename.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
|
|
||||||
nsp = std::make_shared<FileSys::NSP>(
|
|
||||||
vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
|
|
||||||
if (nsp->IsExtractedType()) {
|
|
||||||
return InstallResult::Failure;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return InstallResult::Failure;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nsp->GetStatus() != Loader::ResultStatus::Success) {
|
|
||||||
return InstallResult::Failure;
|
|
||||||
}
|
|
||||||
const auto res = system->GetFileSystemController().GetUserNANDContents()->InstallEntry(
|
|
||||||
*nsp, true, qt_raw_copy);
|
|
||||||
switch (res) {
|
|
||||||
case FileSys::InstallResult::Success:
|
|
||||||
return InstallResult::Success;
|
|
||||||
case FileSys::InstallResult::OverwriteExisting:
|
|
||||||
return InstallResult::Overwrite;
|
|
||||||
case FileSys::InstallResult::ErrorBaseInstall:
|
|
||||||
return InstallResult::BaseInstallAttempted;
|
|
||||||
default:
|
|
||||||
return InstallResult::Failure;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
InstallResult GMainWindow::InstallNCA(const QString& filename) {
|
|
||||||
const auto qt_raw_copy = [this](const FileSys::VirtualFile& src,
|
|
||||||
const FileSys::VirtualFile& dest, std::size_t block_size) {
|
|
||||||
if (src == nullptr || dest == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!dest->Resize(src->GetSize())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<u8> buffer(CopyBufferSize);
|
|
||||||
|
|
||||||
for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
|
|
||||||
if (install_progress->wasCanceled()) {
|
|
||||||
dest->Resize(0);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit UpdateInstallProgress();
|
|
||||||
|
|
||||||
const auto read = src->Read(buffer.data(), buffer.size(), i);
|
|
||||||
dest->Write(buffer.data(), read, i);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const auto nca =
|
|
||||||
std::make_shared<FileSys::NCA>(vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
|
|
||||||
const auto id = nca->GetStatus();
|
|
||||||
|
|
||||||
// Game updates necessary are missing base RomFS
|
|
||||||
if (id != Loader::ResultStatus::Success &&
|
|
||||||
id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
|
|
||||||
return InstallResult::Failure;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QStringList tt_options{tr("System Application"),
|
const QStringList tt_options{tr("System Application"),
|
||||||
tr("System Archive"),
|
tr("System Archive"),
|
||||||
tr("System Application Update"),
|
tr("System Application Update"),
|
||||||
|
@ -3464,7 +3364,7 @@ InstallResult GMainWindow::InstallNCA(const QString& filename) {
|
||||||
if (!ok || index == -1) {
|
if (!ok || index == -1) {
|
||||||
QMessageBox::warning(this, tr("Failed to Install"),
|
QMessageBox::warning(this, tr("Failed to Install"),
|
||||||
tr("The title type you selected for the NCA is invalid."));
|
tr("The title type you selected for the NCA is invalid."));
|
||||||
return InstallResult::Failure;
|
return ContentManager::InstallResult::Failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If index is equal to or past Game, add the jump in TitleType.
|
// If index is equal to or past Game, add the jump in TitleType.
|
||||||
|
@ -3478,15 +3378,15 @@ InstallResult GMainWindow::InstallNCA(const QString& filename) {
|
||||||
auto* registered_cache = is_application ? fs_controller.GetUserNANDContents()
|
auto* registered_cache = is_application ? fs_controller.GetUserNANDContents()
|
||||||
: fs_controller.GetSystemNANDContents();
|
: fs_controller.GetSystemNANDContents();
|
||||||
|
|
||||||
const auto res = registered_cache->InstallEntry(*nca, static_cast<FileSys::TitleType>(index),
|
const auto progress_callback = [this](size_t size, size_t progress) {
|
||||||
true, qt_raw_copy);
|
emit UpdateInstallProgress();
|
||||||
if (res == FileSys::InstallResult::Success) {
|
if (install_progress->wasCanceled()) {
|
||||||
return InstallResult::Success;
|
return true;
|
||||||
} else if (res == FileSys::InstallResult::OverwriteExisting) {
|
|
||||||
return InstallResult::Overwrite;
|
|
||||||
} else {
|
|
||||||
return InstallResult::Failure;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
return ContentManager::InstallNCA(vfs.get(), filename.toStdString(), registered_cache,
|
||||||
|
static_cast<FileSys::TitleType>(index), progress_callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GMainWindow::OnMenuRecentFile() {
|
void GMainWindow::OnMenuRecentFile() {
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
#include "common/announce_multiplayer_room.h"
|
#include "common/announce_multiplayer_room.h"
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "configuration/qt_config.h"
|
#include "configuration/qt_config.h"
|
||||||
|
#include "frontend_common/content_manager.h"
|
||||||
#include "input_common/drivers/tas_input.h"
|
#include "input_common/drivers/tas_input.h"
|
||||||
#include "yuzu/compatibility_list.h"
|
#include "yuzu/compatibility_list.h"
|
||||||
#include "yuzu/hotkeys.h"
|
#include "yuzu/hotkeys.h"
|
||||||
|
@ -124,13 +125,6 @@ enum class EmulatedDirectoryTarget {
|
||||||
SDMC,
|
SDMC,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class InstallResult {
|
|
||||||
Success,
|
|
||||||
Overwrite,
|
|
||||||
Failure,
|
|
||||||
BaseInstallAttempted,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class ReinitializeKeyBehavior {
|
enum class ReinitializeKeyBehavior {
|
||||||
NoWarning,
|
NoWarning,
|
||||||
Warning,
|
Warning,
|
||||||
|
@ -427,8 +421,7 @@ private:
|
||||||
void RemoveCacheStorage(u64 program_id);
|
void RemoveCacheStorage(u64 program_id);
|
||||||
bool SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id,
|
bool SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id,
|
||||||
u64* selected_title_id, u8* selected_content_record_type);
|
u64* selected_title_id, u8* selected_content_record_type);
|
||||||
InstallResult InstallNSP(const QString& filename);
|
ContentManager::InstallResult InstallNCA(const QString& filename);
|
||||||
InstallResult InstallNCA(const QString& filename);
|
|
||||||
void MigrateConfigFiles();
|
void MigrateConfigFiles();
|
||||||
void UpdateWindowTitle(std::string_view title_name = {}, std::string_view title_version = {},
|
void UpdateWindowTitle(std::string_view title_name = {}, std::string_view title_version = {},
|
||||||
std::string_view gpu_vendor = {});
|
std::string_view gpu_vendor = {});
|
||||||
|
|
Reference in New Issue