Compare commits

..

9 Commits

Author SHA1 Message Date
872825d4aa Android 197 2024-01-19 01:00:45 +00:00
ed294d35d4 Merge yuzu-emu#12701 2024-01-19 01:00:45 +00:00
5f4efa456d Merge yuzu-emu#12688 2024-01-19 01:00:45 +00:00
e52e4871ff Merge yuzu-emu#12687 2024-01-19 01:00:45 +00:00
5e45e469d3 Merge yuzu-emu#12683 2024-01-19 01:00:45 +00:00
760e601a00 Merge yuzu-emu#12678 2024-01-19 01:00:45 +00:00
dfb63caefc Merge yuzu-emu#12660 2024-01-19 01:00:45 +00:00
0409886ebb Merge yuzu-emu#12644 2024-01-19 01:00:45 +00:00
803ccd7a57 Merge yuzu-emu#12579 2024-01-19 01:00:44 +00:00
40 changed files with 483 additions and 976 deletions

View File

@ -1,10 +1,13 @@
| Pull Request | Commit | Title | Author | Merged? | | Pull Request | Commit | Title | Author | Merged? |
|----|----|----|----|----| |----|----|----|----|----|
| [12579](https://github.com/yuzu-emu/yuzu-android//pull/12579) | [`748465f5a`](https://github.com/yuzu-emu/yuzu-android//pull/12579/files) | Core: Implement Device Mapping & GPU SMMU | [FernandoS27](https://github.com/FernandoS27/) | Yes | | [12579](https://github.com/yuzu-emu/yuzu-android//pull/12579) | [`93ef41f03`](https://github.com/yuzu-emu/yuzu-android//pull/12579/files) | Core: Implement Device Mapping & GPU SMMU | [FernandoS27](https://github.com/FernandoS27/) | Yes |
| [12644](https://github.com/yuzu-emu/yuzu-android//pull/12644) | [`2044a289f`](https://github.com/yuzu-emu/yuzu-android//pull/12644/files) | shader_recompiler: fix Offset operand usage for non-OpImage*Gather | [liamwhite](https://github.com/liamwhite/) | Yes |
| [12660](https://github.com/yuzu-emu/yuzu-android//pull/12660) | [`2cacb9d48`](https://github.com/yuzu-emu/yuzu-android//pull/12660/files) | service: hid: Fully implement abstract vibration | [german77](https://github.com/german77/) | Yes | | [12660](https://github.com/yuzu-emu/yuzu-android//pull/12660) | [`2cacb9d48`](https://github.com/yuzu-emu/yuzu-android//pull/12660/files) | service: hid: Fully implement abstract vibration | [german77](https://github.com/german77/) | Yes |
| [12678](https://github.com/yuzu-emu/yuzu-android//pull/12678) | [`7f5adf898`](https://github.com/yuzu-emu/yuzu-android//pull/12678/files) | service: set: Implement stubbed functions | [german77](https://github.com/german77/) | Yes |
| [12683](https://github.com/yuzu-emu/yuzu-android//pull/12683) | [`c661b9586`](https://github.com/yuzu-emu/yuzu-android//pull/12683/files) | service: nfc: Create backup when none exist | [german77](https://github.com/german77/) | Yes |
| [12687](https://github.com/yuzu-emu/yuzu-android//pull/12687) | [`0b0e9ef18`](https://github.com/yuzu-emu/yuzu-android//pull/12687/files) | core: hid: Disable special features before disconnecting the controllers | [german77](https://github.com/german77/) | Yes |
| [12688](https://github.com/yuzu-emu/yuzu-android//pull/12688) | [`e9eb017aa`](https://github.com/yuzu-emu/yuzu-android//pull/12688/files) | renderer_vulkan: recreate swapchain when frame size changes | [liamwhite](https://github.com/liamwhite/) | Yes | | [12688](https://github.com/yuzu-emu/yuzu-android//pull/12688) | [`e9eb017aa`](https://github.com/yuzu-emu/yuzu-android//pull/12688/files) | renderer_vulkan: recreate swapchain when frame size changes | [liamwhite](https://github.com/liamwhite/) | Yes |
| [12701](https://github.com/yuzu-emu/yuzu-android//pull/12701) | [`e4bbb24dc`](https://github.com/yuzu-emu/yuzu-android//pull/12701/files) | vi: check layer state before opening or closing | [liamwhite](https://github.com/liamwhite/) | Yes | | [12701](https://github.com/yuzu-emu/yuzu-android//pull/12701) | [`e4bbb24dc`](https://github.com/yuzu-emu/yuzu-android//pull/12701/files) | vi: check layer state before opening or closing | [liamwhite](https://github.com/liamwhite/) | Yes |
| [12715](https://github.com/yuzu-emu/yuzu-android//pull/12715) | [`47058d705`](https://github.com/yuzu-emu/yuzu-android//pull/12715/files) | android: Add uninstall addon button | [t895](https://github.com/t895/) | Yes |
End of merge log. You can find the original README.md below the break. End of merge log. You can find the original README.md below the break.

View File

@ -21,8 +21,6 @@ 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
@ -237,12 +235,9 @@ 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
* @return int representation of [InstallResult] * @param extension Lowercase string representation of file extension without "."
*/ */
external fun installFileToNand( external fun installFileToNand(filename: String, extension: String): Int
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
@ -540,29 +535,9 @@ 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 available patches * @return Array of pairs where the first value is the name of an addon and the second is the version
*/ */
external fun getPatchesForFile(path: String, programId: String): Array<Patch>? external fun getAddonsForFile(path: String, programId: String): Array<Pair<String, String>>?
/**
* Removes an update for a given [programId]
* @param programId String representation of a game's program ID
*/
external fun removeUpdate(programId: String)
/**
* Removes DLC by its unique [titleId]
* @param titleId String representation of a DLC's title ID
*/
external fun removeDLC(titleId: 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
@ -634,4 +609,15 @@ 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
}
} }

View File

@ -6,32 +6,27 @@ 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.Patch import org.yuzu.yuzu_emu.model.Addon
import org.yuzu.yuzu_emu.model.AddonViewModel
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class AddonAdapter(val addonViewModel: AddonViewModel) : class AddonAdapter : AbstractDiffAdapter<Addon, AddonAdapter.AddonViewHolder>() {
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<Patch>(binding) { AbstractViewHolder<Addon>(binding) {
override fun bind(model: Patch) { override fun bind(model: Addon) {
binding.root.setOnClickListener { binding.root.setOnClickListener {
binding.addonCheckbox.isChecked = !binding.addonCheckbox.isChecked binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked
} }
binding.title.text = model.name binding.title.text = model.title
binding.version.text = model.version binding.version.text = model.version
binding.addonCheckbox.setOnCheckedChangeListener { _, checked -> binding.addonSwitch.setOnCheckedChangeListener { _, checked ->
model.enabled = checked model.enabled = checked
} }
binding.addonCheckbox.isChecked = model.enabled binding.addonSwitch.isChecked = model.enabled
binding.buttonDelete.setOnClickListener {
addonViewModel.setAddonToDelete(model)
}
} }
} }
} }

View File

@ -74,7 +74,7 @@ class AddonsFragment : Fragment() {
binding.listAddons.apply { binding.listAddons.apply {
layoutManager = LinearLayoutManager(requireContext()) layoutManager = LinearLayoutManager(requireContext())
adapter = AddonAdapter(addonViewModel) adapter = AddonAdapter()
} }
viewLifecycleOwner.lifecycleScope.apply { viewLifecycleOwner.lifecycleScope.apply {
@ -110,21 +110,6 @@ 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 {
@ -171,22 +156,22 @@ class AddonsFragment : Fragment() {
descriptionId = R.string.invalid_directory_description descriptionId = R.string.invalid_directory_description
) )
if (isValid) { if (isValid) {
ProgressDialogFragment.newInstance( IndeterminateProgressDialogFragment.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, progressCallback) externalAddonDirectory.copyFilesTo(internalAddonDirectory)
} 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, ProgressDialogFragment.TAG) }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
} else { } else {
errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG) errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG)
} }

View File

@ -173,11 +173,11 @@ class DriverManagerFragment : Fragment() {
return@registerForActivityResult return@registerForActivityResult
} }
ProgressDialogFragment.newInstance( IndeterminateProgressDialogFragment.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, ProgressDialogFragment.TAG) }.show(childFragmentManager, IndeterminateProgressDialogFragment.TAG)
} }
} }

View File

@ -44,6 +44,7 @@ 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
@ -356,17 +357,27 @@ 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()
ProgressDialogFragment.newInstance( if (inputZip == null) {
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(result.toString(), cacheSaveDir) FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
val files = cacheSaveDir.listFiles() val files = cacheSaveDir.listFiles()
var savesFolderFile: File? = null var savesFolderFile: File? = null
if (files != null) { if (files != null) {
@ -411,7 +422,7 @@ class GamePropertiesFragment : Fragment() {
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
} }
}.show(parentFragmentManager, ProgressDialogFragment.TAG) }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
} }
/** /**
@ -425,11 +436,11 @@ class GamePropertiesFragment : Fragment() {
return@registerForActivityResult return@registerForActivityResult
} }
ProgressDialogFragment.newInstance( IndeterminateProgressDialogFragment.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),
@ -441,6 +452,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, ProgressDialogFragment.TAG) }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
} }
} }

View File

@ -23,13 +23,11 @@ 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 ProgressDialogFragment : DialogFragment() { class IndeterminateProgressDialogFragment : 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)
@ -63,7 +61,6 @@ class ProgressDialogFragment : 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) {
@ -100,35 +97,6 @@ class ProgressDialogFragment : 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
}
}
}
}
} }
} }
@ -140,7 +108,6 @@ class ProgressDialogFragment : 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)
} }
} }
@ -155,12 +122,9 @@ class ProgressDialogFragment : DialogFragment() {
activity: FragmentActivity, activity: FragmentActivity,
titleId: Int, titleId: Int,
cancellable: Boolean = false, cancellable: Boolean = false,
task: suspend ( task: suspend () -> Any
progressCallback: (max: Long, progress: Long) -> Boolean, ): IndeterminateProgressDialogFragment {
messageCallback: (message: String) -> Unit val dialog = IndeterminateProgressDialogFragment()
) -> 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)

View File

@ -34,6 +34,7 @@ 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
@ -194,20 +195,26 @@ 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()
ProgressDialogFragment.newInstance( if (inputZip == null) {
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( FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
result.toString(),
cacheSaveDir,
progressCallback
)
val files = cacheSaveDir.listFiles() val files = cacheSaveDir.listFiles()
var successfulImports = 0 var successfulImports = 0
var failedImports = 0 var failedImports = 0
@ -280,7 +287,7 @@ class InstallableFragment : Fragment() {
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
} }
}.show(parentFragmentManager, ProgressDialogFragment.TAG) }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
} }
private val exportSaves = registerForActivityResult( private val exportSaves = registerForActivityResult(
@ -290,11 +297,11 @@ class InstallableFragment : Fragment() {
return@registerForActivityResult return@registerForActivityResult
} }
ProgressDialogFragment.newInstance( IndeterminateProgressDialogFragment.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()
@ -331,6 +338,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, ProgressDialogFragment.TAG) }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
} }
} }

View File

@ -0,0 +1,10 @@
// 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
)

View File

@ -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 _patchList = MutableStateFlow(mutableListOf<Patch>()) private val _addonList = MutableStateFlow(mutableListOf<Addon>())
val addonList get() = _patchList.asStateFlow() val addonList get() = _addonList.asStateFlow()
private val _showModInstallPicker = MutableStateFlow(false) private val _showModInstallPicker = MutableStateFlow(false)
val showModInstallPicker get() = _showModInstallPicker.asStateFlow() val showModInstallPicker get() = _showModInstallPicker.asStateFlow()
@ -24,9 +24,6 @@ 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)
@ -43,47 +40,36 @@ class AddonViewModel : ViewModel() {
isRefreshing.set(true) isRefreshing.set(true)
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val patchList = ( val addonList = mutableListOf<Addon>()
NativeLibrary.getPatchesForFile(game!!.path, game!!.programId) val disabledAddons = NativeConfig.getDisabledAddons(game!!.programId)
?: emptyArray() NativeLibrary.getAddonsForFile(game!!.path, game!!.programId)?.forEach {
).toMutableList() val name = it.first.replace("[D] ", "")
patchList.sortBy { it.name } addonList.add(Addon(!disabledAddons.contains(name), name, it.second))
_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.titleId)
PatchType.Mod -> NativeLibrary.removeMod(patch.programId, patch.name)
}
refreshAddons()
}
fun onCloseAddons() { fun onCloseAddons() {
if (_patchList.value.isEmpty()) { if (_addonList.value.isEmpty()) {
return return
} }
NativeConfig.setDisabledAddons( NativeConfig.setDisabledAddons(
game!!.programId, game!!.programId,
_patchList.value.mapNotNull { _addonList.value.mapNotNull {
if (it.enabled) { if (it.enabled) {
null null
} else { } else {
it.name it.title
} }
}.toTypedArray() }.toTypedArray()
) )
NativeConfig.saveGlobalConfig() NativeConfig.saveGlobalConfig()
_patchList.value.clear() _addonList.value.clear()
game = null game = null
} }

View File

@ -1,15 +0,0 @@
// 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
}
}

View File

@ -1,16 +0,0 @@
// 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
)

View File

@ -1,14 +0,0 @@
// 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
}
}

View File

@ -8,7 +8,6 @@ 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() {
@ -24,28 +23,13 @@ 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)
private val _progress = MutableStateFlow(0.0) lateinit var task: suspend () -> Any
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) {
@ -59,16 +43,7 @@ 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

View File

@ -38,13 +38,12 @@ 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.ProgressDialogFragment import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
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.*
@ -370,23 +369,26 @@ 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/")
ProgressDialogFragment.newInstance( val task: () -> Any = {
this,
R.string.firmware_installing
) { progressCallback, _ ->
var messageToShow: Any var messageToShow: Any
try { try {
FileUtil.unzipToInternalStorage( FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheFirmwareDir)
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) {
@ -402,13 +404,18 @@ 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 =
@ -467,11 +474,11 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
return@registerForActivityResult return@registerForActivityResult
} }
ProgressDialogFragment.newInstance( IndeterminateProgressDialogFragment.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(
@ -494,42 +501,44 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
positiveAction = { homeViewModel.setContentToInstall(documents) } positiveAction = { homeViewModel.setContentToInstall(documents) }
) )
} }
}.show(supportFragmentManager, ProgressDialogFragment.TAG) }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
} }
private fun installContent(documents: List<Uri>) { private fun installContent(documents: List<Uri>) {
ProgressDialogFragment.newInstance( IndeterminateProgressDialogFragment.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 error = 0 var errorExtension = 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
)
) )
) { ) {
InstallResult.Success -> { NativeLibrary.InstallFileToNandResult.Success -> {
installSuccess += 1 installSuccess += 1
} }
InstallResult.Overwrite -> { NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> {
installOverwrite += 1 installOverwrite += 1
} }
InstallResult.BaseInstallAttempted -> { NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> {
errorBaseGame += 1 errorBaseGame += 1
} }
InstallResult.Failure -> { NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> {
error += 1 errorExtension += 1
}
else -> {
errorOther += 1
} }
} }
} }
@ -556,7 +565,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
) )
installResult.append(separator) installResult.append(separator)
} }
val errorTotal: Int = errorBaseGame + error val errorTotal: Int = errorBaseGame + errorExtension + errorOther
if (errorTotal > 0) { if (errorTotal > 0) {
installResult.append(separator) installResult.append(separator)
installResult.append( installResult.append(
@ -573,7 +582,14 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
) )
installResult.append(separator) installResult.append(separator)
} }
if (error > 0) { if (errorExtension > 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)
) )
@ -592,7 +608,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
descriptionString = installResult.toString().trim() descriptionString = installResult.toString().trim()
) )
} }
}.show(supportFragmentManager, ProgressDialogFragment.TAG) }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
} }
val exportUserData = registerForActivityResult( val exportUserData = registerForActivityResult(
@ -602,16 +618,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
return@registerForActivityResult return@registerForActivityResult
} }
ProgressDialogFragment.newInstance( IndeterminateProgressDialogFragment.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)),
progressCallback, taskViewModel.cancelled,
compression = false compression = false
) )
return@newInstance when (zipResult) { return@newInstance when (zipResult) {
@ -619,7 +635,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, ProgressDialogFragment.TAG) }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
} }
val importUserData = val importUserData =
@ -628,10 +644,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
return@registerForActivityResult return@registerForActivityResult
} }
ProgressDialogFragment.newInstance( IndeterminateProgressDialogFragment.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
@ -660,9 +676,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
// Copy archive to internal storage // Copy archive to internal storage
try { try {
FileUtil.unzipToInternalStorage( FileUtil.unzipToInternalStorage(
result.toString(), BufferedInputStream(contentResolver.openInputStream(result)),
File(DirectoryInitialization.userDirectory!!), File(DirectoryInitialization.userDirectory!!)
progressCallback
) )
} catch (e: Exception) { } catch (e: Exception) {
return@newInstance MessageDialogFragment.newInstance( return@newInstance MessageDialogFragment.newInstance(
@ -679,6 +694,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, ProgressDialogFragment.TAG) }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
} }
} }

View File

@ -7,6 +7,7 @@ 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
@ -18,7 +19,6 @@ 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,34 +283,12 @@ 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( fun unzipToInternalStorage(zipStream: BufferedInputStream, destDir: File) {
path: String, ZipInputStream(zipStream).use { zis ->
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
@ -326,7 +304,6 @@ object FileUtil {
newFile.outputStream().use { fos -> zis.copyTo(fos) } newFile.outputStream().use { fos -> zis.copyTo(fos) }
} }
entry = zis.nextEntry entry = zis.nextEntry
progress++
} }
} }
} }
@ -336,15 +313,14 @@ 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 progressCallback Lambda that is called with the total number of files and the current * @param cancelled [StateFlow] that reports whether this process has been cancelled
* 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,
progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false }, cancelled: StateFlow<Boolean>? = null,
compression: Boolean = true compression: Boolean = true
): TaskState { ): TaskState {
try { try {
@ -354,10 +330,8 @@ 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 (progressCallback.invoke(totalFiles, count)) { if (cancelled?.value == true) {
return TaskState.Cancelled return TaskState.Cancelled
} }
@ -369,7 +343,6 @@ object FileUtil {
if (file.isFile) { if (file.isFile) {
file.inputStream().use { fis -> fis.copyTo(zos) } file.inputStream().use { fis -> fis.copyTo(zos) }
} }
count++
} }
} }
} }
@ -383,14 +356,9 @@ 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( fun DocumentFile.copyFilesTo(file: File) {
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(
@ -398,13 +366,7 @@ 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()
@ -419,7 +381,6 @@ object FileUtil {
newFile.outputStream().use { os -> bos.copyTo(os) } newFile.outputStream().use { os -> bos.copyTo(os) }
} }
} }
count++
} }
} }
@ -466,18 +427,6 @@ 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)
@ -485,19 +434,4 @@ 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)
} }

View File

@ -5,6 +5,7 @@ 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
@ -122,7 +123,7 @@ object GpuDriverHelper {
// Unzip the driver. // Unzip the driver.
try { try {
FileUtil.unzipToInternalStorage( FileUtil.unzipToInternalStorage(
copiedFile.path, BufferedInputStream(copiedFile.inputStream()),
File(driverInstallationPath!!) File(driverInstallationPath!!)
) )
} catch (e: SecurityException) { } catch (e: SecurityException) {
@ -155,7 +156,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(
driver.path, BufferedInputStream(driver.inputStream()),
File(driverInstallationPath!!) File(driverInstallationPath!!)
) )
} catch (e: SecurityException) { } catch (e: SecurityException) {

View File

@ -42,19 +42,3 @@ 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);
}

View File

@ -6,7 +6,6 @@
#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);
@ -14,9 +13,3 @@ 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);

View File

@ -43,27 +43,10 @@ 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 {
@ -203,38 +186,6 @@ 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;
} }
@ -247,30 +198,6 @@ 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
@ -351,37 +278,12 @@ 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);
@ -407,10 +309,7 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
env->DeleteGlobalRef(s_string_class); env->DeleteGlobalRef(s_string_class);
env->DeleteGlobalRef(s_pair_class); env->DeleteGlobalRef(s_pair_class);
env->DeleteGlobalRef(s_overlay_control_data_class); env->DeleteGlobalRef(s_overlay_control_data_class);
env->DeleteGlobalRef(s_patch_class);
env->DeleteGlobalRef(s_double_class); env->DeleteGlobalRef(s_double_class);
env->DeleteGlobalRef(s_integer_class);
env->DeleteGlobalRef(s_boolean_class);
// UnInitialize applets // UnInitialize applets
SoftwareKeyboard::CleanupJNI(env); SoftwareKeyboard::CleanupJNI(env);

View File

@ -43,25 +43,8 @@ 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

View File

@ -17,7 +17,6 @@
#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"
@ -101,6 +100,67 @@ 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,
@ -452,20 +512,10 @@ 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, jobject jcallback) { jstring j_file,
auto jlambdaClass = env->GetObjectClass(jcallback); jstring j_file_extension) {
auto jlambdaInvokeMethod = env->GetMethodID( return EmulationSession::GetInstance().InstallFileToNand(GetJString(env, j_file),
jlambdaClass, "invoke", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); GetJString(env, j_file_extension));
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,
@ -774,9 +824,9 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env,
return true; return true;
} }
jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env, jobject jobj, jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, jobject jobj,
jstring jpath, jstring jpath,
jstring jprogramId) { jstring jprogramId) {
const auto path = GetJString(env, jpath); const auto path = GetJString(env, jpath);
const auto vFile = const auto vFile =
Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path); Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path);
@ -793,40 +843,20 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env
FileSys::VirtualFile update_raw; FileSys::VirtualFile update_raw;
loader->ReadUpdateRaw(update_raw); loader->ReadUpdateRaw(update_raw);
auto patches = pm.GetPatches(update_raw); auto addons = pm.GetPatchVersionNames(update_raw);
jobjectArray jpatchArray = auto jemptyString = ToJString(env, "");
env->NewObjectArray(patches.size(), IDCache::GetPatchClass(), nullptr); auto jemptyStringPair = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(),
jemptyString, jemptyString);
jobjectArray jaddonsArray =
env->NewObjectArray(addons.size(), IDCache::GetPairClass(), jemptyStringPair);
int i = 0; int i = 0;
for (const auto& patch : patches) { for (const auto& addon : addons) {
jobject jpatch = env->NewObject( jobject jaddon = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(),
IDCache::GetPatchClass(), IDCache::GetPatchConstructor(), patch.enabled, ToJString(env, addon.first), ToJString(env, addon.second));
ToJString(env, patch.name), ToJString(env, patch.version), env->SetObjectArrayElement(jaddonsArray, i, jaddon);
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 jpatchArray; return jaddonsArray;
}
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 jtitleId) {
auto title_id = EmulationSession::GetProgramId(env, jtitleId);
ContentManager::RemoveDLC(EmulationSession::GetInstance().System().GetFileSystemController(),
title_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,

View File

@ -7,7 +7,6 @@
#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"
@ -30,6 +29,7 @@ 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);

View File

@ -1,30 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.progressindicator.LinearProgressIndicator 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: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:orientation="vertical"> android:padding="24dp"
app:trackCornerRadius="4dp" />
<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:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="24dp"
app:trackCornerRadius="4dp" />
</LinearLayout>

View File

@ -14,11 +14,12 @@
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:orientation="vertical"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
app:layout_constraintEnd_toStartOf="@+id/addon_checkbox" android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="@+id/addon_switch"
app:layout_constraintEnd_toStartOf="@+id/addon_switch"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="@+id/addon_switch">
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/title" android:id="@+id/title"
@ -41,29 +42,16 @@
</LinearLayout> </LinearLayout>
<com.google.android.material.checkbox.MaterialCheckBox <com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/addon_checkbox" android:id="@+id/addon_switch"
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:layout_marginEnd="8dp" android:nextFocusLeft="@id/addon_container"
app:layout_constraintTop_toTopOf="@+id/text_container" app:layout_constraintBottom_toBottomOf="parent"
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_constraintTop_toTopOf="@+id/addon_checkbox" app:layout_constraintStart_toEndOf="@id/text_container"
app:layout_constraintBottom_toBottomOf="@+id/addon_checkbox" /> app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -286,7 +286,6 @@
<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>
@ -349,8 +348,6 @@
<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>

View File

@ -466,12 +466,12 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
return romfs; return romfs;
} }
std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const { PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile update_raw) const {
if (title_id == 0) { if (title_id == 0) {
return {}; return {};
} }
std::vector<Patch> out; std::map<std::string, std::string, std::less<>> out;
const auto& disabled = Settings::values.disabled_addons[title_id]; const auto& disabled = Settings::values.disabled_addons[title_id];
// Game Updates // Game Updates
@ -482,28 +482,20 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
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();
Patch update_patch = {.enabled = !update_disabled, const auto update_label = update_disabled ? "[D] Update" : "Update";
.name = "Update",
.version = "",
.type = PatchType::Update,
.program_id = title_id,
.title_id = title_id};
if (nacp != nullptr) { if (nacp != nullptr) {
update_patch.version = nacp->GetVersionString(); out.insert_or_assign(update_label, 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.push_back(update_patch); out.insert_or_assign(update_label, "");
} else { } else {
update_patch.version = FormatTitleVersion(*meta_ver); out.insert_or_assign(update_label, FormatTitleVersion(*meta_ver));
out.push_back(update_patch);
} }
} else if (update_raw != nullptr) { } else if (update_raw != nullptr) {
update_patch.version = "PACKED"; out.insert_or_assign(update_label, "PACKED");
out.push_back(update_patch);
} }
} }
@ -547,12 +539,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
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.push_back({.enabled = !mod_disabled, out.insert_or_assign(mod_disabled ? "[D] " + mod->GetName() : mod->GetName(), types);
.name = mod->GetName(),
.version = types,
.type = PatchType::Mod,
.program_id = title_id,
.title_id = title_id});
} }
} }
@ -570,12 +557,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
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.push_back({.enabled = !mod_disabled, out.insert_or_assign(mod_disabled ? "[D] SDMC" : "SDMC", types);
.name = "SDMC",
.version = types,
.type = PatchType::Mod,
.program_id = title_id,
.title_id = title_id});
} }
} }
@ -602,12 +584,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
const auto dlc_disabled = const auto dlc_disabled =
std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end(); std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end();
out.push_back({.enabled = !dlc_disabled, out.insert_or_assign(dlc_disabled ? "[D] DLC" : "DLC", std::move(list));
.name = "DLC",
.version = std::move(list),
.type = PatchType::DLC,
.program_id = title_id,
.title_id = dlc_match.back().title_id});
} }
return out; return out;

View File

@ -26,22 +26,12 @@ 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_,
@ -76,8 +66,9 @@ 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 patches // Returns a vector of pairs between patch names and patch versions.
[[nodiscard]] std::vector<Patch> GetPatches(VirtualFile update_raw = nullptr) const; // i.e. Update 3.2.2 will return {"Update", "3.2.2"}
[[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

View File

@ -11,7 +11,6 @@
#include "common/fs/path_util.h" #include "common/fs/path_util.h"
#include "common/polyfill_ranges.h" #include "common/polyfill_ranges.h"
#include "common/settings.h" #include "common/settings.h"
#include "common/string_util.h"
#include "core/hle/service/acc/profile_manager.h" #include "core/hle/service/acc/profile_manager.h"
namespace Service::Account { namespace Service::Account {
@ -165,22 +164,6 @@ std::optional<std::size_t> ProfileManager::GetUserIndex(const ProfileInfo& user)
return GetUserIndex(user.user_uuid); return GetUserIndex(user.user_uuid);
} }
/// Returns the first user profile seen based on username (which does not enforce uniqueness)
std::optional<std::size_t> ProfileManager::GetUserIndex(const std::string& username) const {
const auto iter =
std::find_if(profiles.begin(), profiles.end(), [&username](const ProfileInfo& p) {
const std::string profile_username = Common::StringFromFixedZeroTerminatedBuffer(
reinterpret_cast<const char*>(p.username.data()), p.username.size());
return username.compare(profile_username) == 0;
});
if (iter == profiles.end()) {
return std::nullopt;
}
return static_cast<std::size_t>(std::distance(profiles.begin(), iter));
}
/// Returns the data structure used by the switch when GetProfileBase is called on acc:* /// Returns the data structure used by the switch when GetProfileBase is called on acc:*
bool ProfileManager::GetProfileBase(std::optional<std::size_t> index, ProfileBase& profile) const { bool ProfileManager::GetProfileBase(std::optional<std::size_t> index, ProfileBase& profile) const {
if (!index || index >= MAX_USERS) { if (!index || index >= MAX_USERS) {

View File

@ -70,7 +70,6 @@ public:
std::optional<Common::UUID> GetUser(std::size_t index) const; std::optional<Common::UUID> GetUser(std::size_t index) const;
std::optional<std::size_t> GetUserIndex(const Common::UUID& uuid) const; std::optional<std::size_t> GetUserIndex(const Common::UUID& uuid) const;
std::optional<std::size_t> GetUserIndex(const ProfileInfo& user) const; std::optional<std::size_t> GetUserIndex(const ProfileInfo& user) const;
std::optional<std::size_t> GetUserIndex(const std::string& username) const;
bool GetProfileBase(std::optional<std::size_t> index, ProfileBase& profile) const; bool GetProfileBase(std::optional<std::size_t> index, ProfileBase& profile) const;
bool GetProfileBase(Common::UUID uuid, ProfileBase& profile) const; bool GetProfileBase(Common::UUID uuid, ProfileBase& profile) const;
bool GetProfileBase(const ProfileInfo& user, ProfileBase& profile) const; bool GetProfileBase(const ProfileInfo& user, ProfileBase& profile) const;

View File

@ -24,6 +24,19 @@
#include "core/hle/service/ipc_helpers.h" #include "core/hle/service/ipc_helpers.h"
#include "core/memory.h" #include "core/memory.h"
namespace {
static thread_local std::array read_buffer_data_a{
Common::ScratchBuffer<u8>(),
Common::ScratchBuffer<u8>(),
Common::ScratchBuffer<u8>(),
};
static thread_local std::array read_buffer_data_x{
Common::ScratchBuffer<u8>(),
Common::ScratchBuffer<u8>(),
Common::ScratchBuffer<u8>(),
};
} // Anonymous namespace
namespace Service { namespace Service {
SessionRequestHandler::SessionRequestHandler(Kernel::KernelCore& kernel_, const char* service_name_) SessionRequestHandler::SessionRequestHandler(Kernel::KernelCore& kernel_, const char* service_name_)
@ -331,27 +344,48 @@ std::vector<u8> HLERequestContext::ReadBufferCopy(std::size_t buffer_index) cons
} }
std::span<const u8> HLERequestContext::ReadBufferA(std::size_t buffer_index) const { std::span<const u8> HLERequestContext::ReadBufferA(std::size_t buffer_index) const {
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::UnsafeRead> gm(memory, 0, 0); static thread_local std::array read_buffer_a{
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::UnsafeRead>(memory, 0, 0),
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::UnsafeRead>(memory, 0, 0),
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::UnsafeRead>(memory, 0, 0),
};
ASSERT_OR_EXECUTE_MSG( ASSERT_OR_EXECUTE_MSG(
BufferDescriptorA().size() > buffer_index, { return {}; }, BufferDescriptorA().size() > buffer_index, { return {}; },
"BufferDescriptorA invalid buffer_index {}", buffer_index); "BufferDescriptorA invalid buffer_index {}", buffer_index);
return gm.Read(BufferDescriptorA()[buffer_index].Address(), auto& read_buffer = read_buffer_a[buffer_index];
BufferDescriptorA()[buffer_index].Size(), &read_buffer_data_a[buffer_index]); return read_buffer.Read(BufferDescriptorA()[buffer_index].Address(),
BufferDescriptorA()[buffer_index].Size(),
&read_buffer_data_a[buffer_index]);
} }
std::span<const u8> HLERequestContext::ReadBufferX(std::size_t buffer_index) const { std::span<const u8> HLERequestContext::ReadBufferX(std::size_t buffer_index) const {
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::UnsafeRead> gm(memory, 0, 0); static thread_local std::array read_buffer_x{
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::UnsafeRead>(memory, 0, 0),
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::UnsafeRead>(memory, 0, 0),
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::UnsafeRead>(memory, 0, 0),
};
ASSERT_OR_EXECUTE_MSG( ASSERT_OR_EXECUTE_MSG(
BufferDescriptorX().size() > buffer_index, { return {}; }, BufferDescriptorX().size() > buffer_index, { return {}; },
"BufferDescriptorX invalid buffer_index {}", buffer_index); "BufferDescriptorX invalid buffer_index {}", buffer_index);
return gm.Read(BufferDescriptorX()[buffer_index].Address(), auto& read_buffer = read_buffer_x[buffer_index];
BufferDescriptorX()[buffer_index].Size(), &read_buffer_data_x[buffer_index]); return read_buffer.Read(BufferDescriptorX()[buffer_index].Address(),
BufferDescriptorX()[buffer_index].Size(),
&read_buffer_data_x[buffer_index]);
} }
std::span<const u8> HLERequestContext::ReadBuffer(std::size_t buffer_index) const { std::span<const u8> HLERequestContext::ReadBuffer(std::size_t buffer_index) const {
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::UnsafeRead> gm(memory, 0, 0); static thread_local std::array read_buffer_a{
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::UnsafeRead>(memory, 0, 0),
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::UnsafeRead>(memory, 0, 0),
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::UnsafeRead>(memory, 0, 0),
};
static thread_local std::array read_buffer_x{
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::UnsafeRead>(memory, 0, 0),
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::UnsafeRead>(memory, 0, 0),
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::UnsafeRead>(memory, 0, 0),
};
const bool is_buffer_a{BufferDescriptorA().size() > buffer_index && const bool is_buffer_a{BufferDescriptorA().size() > buffer_index &&
BufferDescriptorA()[buffer_index].Size()}; BufferDescriptorA()[buffer_index].Size()};
@ -368,14 +402,18 @@ std::span<const u8> HLERequestContext::ReadBuffer(std::size_t buffer_index) cons
ASSERT_OR_EXECUTE_MSG( ASSERT_OR_EXECUTE_MSG(
BufferDescriptorA().size() > buffer_index, { return {}; }, BufferDescriptorA().size() > buffer_index, { return {}; },
"BufferDescriptorA invalid buffer_index {}", buffer_index); "BufferDescriptorA invalid buffer_index {}", buffer_index);
return gm.Read(BufferDescriptorA()[buffer_index].Address(), auto& read_buffer = read_buffer_a[buffer_index];
BufferDescriptorA()[buffer_index].Size(), &read_buffer_data_a[buffer_index]); return read_buffer.Read(BufferDescriptorA()[buffer_index].Address(),
BufferDescriptorA()[buffer_index].Size(),
&read_buffer_data_a[buffer_index]);
} else { } else {
ASSERT_OR_EXECUTE_MSG( ASSERT_OR_EXECUTE_MSG(
BufferDescriptorX().size() > buffer_index, { return {}; }, BufferDescriptorX().size() > buffer_index, { return {}; },
"BufferDescriptorX invalid buffer_index {}", buffer_index); "BufferDescriptorX invalid buffer_index {}", buffer_index);
return gm.Read(BufferDescriptorX()[buffer_index].Address(), auto& read_buffer = read_buffer_x[buffer_index];
BufferDescriptorX()[buffer_index].Size(), &read_buffer_data_x[buffer_index]); return read_buffer.Read(BufferDescriptorX()[buffer_index].Address(),
BufferDescriptorX()[buffer_index].Size(),
&read_buffer_data_x[buffer_index]);
} }
} }

View File

@ -426,9 +426,6 @@ private:
Kernel::KernelCore& kernel; Kernel::KernelCore& kernel;
Core::Memory::Memory& memory; Core::Memory::Memory& memory;
mutable std::array<Common::ScratchBuffer<u8>, 3> read_buffer_data_a{};
mutable std::array<Common::ScratchBuffer<u8>, 3> read_buffer_data_x{};
}; };
} // namespace Service } // namespace Service

View File

@ -4,7 +4,6 @@
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)

View File

@ -1,238 +0,0 @@
// 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

View File

@ -1064,13 +1064,6 @@ Result NPad::RegisterAppletResourceUserId(u64 aruid) {
} }
void NPad::UnregisterAppletResourceUserId(u64 aruid) { void NPad::UnregisterAppletResourceUserId(u64 aruid) {
// TODO: Remove this once abstract pad is emulated properly
const auto aruid_index = npad_resource.GetIndexFromAruid(aruid);
for (auto& controller : controller_data[aruid_index]) {
controller.is_connected = false;
controller.shared_memory = nullptr;
}
npad_resource.UnregisterAppletResourceUserId(aruid); npad_resource.UnregisterAppletResourceUserId(aruid);
} }

View File

@ -351,7 +351,7 @@ if (APPLE)
if (NOT USE_SYSTEM_MOLTENVK) if (NOT USE_SYSTEM_MOLTENVK)
set(MOLTENVK_PLATFORM "macOS") set(MOLTENVK_PLATFORM "macOS")
set(MOLTENVK_VERSION "v1.2.7") set(MOLTENVK_VERSION "v1.2.5")
download_moltenvk_external(${MOLTENVK_PLATFORM} ${MOLTENVK_VERSION}) download_moltenvk_external(${MOLTENVK_PLATFORM} ${MOLTENVK_VERSION})
endif() endif()
find_library(MOLTENVK_LIBRARY MoltenVK REQUIRED) find_library(MOLTENVK_LIBRARY MoltenVK REQUIRED)

View File

@ -122,8 +122,9 @@ 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.GetPatches(update_raw)) { for (const auto& patch : pm.GetPatchVersionNames(update_raw)) {
const auto name = QString::fromStdString(patch.name); const auto 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);
@ -135,7 +136,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.version)}}); first_item, new QStandardItem{QString::fromStdString(patch.second)}});
item_model->appendRow(list_items.back()); item_model->appendRow(list_items.back());
} }

View File

@ -164,19 +164,18 @@ 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& patch : patch_manager.GetPatches(update_raw)) { for (const auto& kv : patch_manager.GetPatchVersionNames(update_raw)) {
const bool is_update = patch.name == "Update"; const bool is_update = kv.first == "Update" || kv.first == "[D] Update";
if (!updatable && is_update) { if (!updatable && is_update) {
continue; continue;
} }
const QString type = const QString type = QString::fromStdString(kv.first);
QString::fromStdString(patch.enabled ? patch.name : "[D] " + patch.name);
if (patch.version.empty()) { if (kv.second.empty()) {
out.append(QStringLiteral("%1\n").arg(type)); out.append(QStringLiteral("%1\n").arg(type));
} else { } else {
auto ver = patch.version; auto ver = kv.second;
// Display container name for packed updates // Display container name for packed updates
if (is_update && ver == "PACKED") { if (is_update && ver == "PACKED") {

View File

@ -47,7 +47,6 @@
#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"
@ -519,21 +518,12 @@ GMainWindow::GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulk
continue; continue;
} }
int user_arg_idx = ++i;
bool argument_ok; bool argument_ok;
std::size_t selected_user = args[user_arg_idx].toUInt(&argument_ok); const std::size_t selected_user = args[++i].toUInt(&argument_ok);
if (!argument_ok) { if (!argument_ok) {
// try to look it up by username, only finds the first username that matches. LOG_ERROR(Frontend, "Invalid user argument");
const std::string user_arg_str = args[user_arg_idx].toStdString(); continue;
const auto user_idx = system->GetProfileManager().GetUserIndex(user_arg_str);
if (user_idx == std::nullopt) {
LOG_ERROR(Frontend, "Invalid user argument");
continue;
}
selected_user = user_idx.value();
} }
if (!system->GetProfileManager().UserExistsIndex(selected_user)) { if (!system->GetProfileManager().UserExistsIndex(selected_user)) {
@ -542,8 +532,6 @@ GMainWindow::GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulk
} }
Settings::values.current_user = static_cast<s32>(selected_user); Settings::values.current_user = static_cast<s32>(selected_user);
user_flag_cmd_line = true;
continue; continue;
} }
@ -1954,7 +1942,7 @@ void GMainWindow::BootGame(const QString& filename, u64 program_id, std::size_t
Settings::LogSettings(); Settings::LogSettings();
if (UISettings::values.select_user_on_boot && !user_flag_cmd_line) { if (UISettings::values.select_user_on_boot) {
const Core::Frontend::ProfileSelectParameters parameters{ const Core::Frontend::ProfileSelectParameters parameters{
.mode = Service::AM::Applets::UiMode::UserSelector, .mode = Service::AM::Applets::UiMode::UserSelector,
.invalid_uid_list = {}, .invalid_uid_list = {},
@ -1966,11 +1954,6 @@ void GMainWindow::BootGame(const QString& filename, u64 program_id, std::size_t
} }
} }
// If the user specifies -u (successfully) on the cmd line, don't prompt for a user on first
// game startup only. If the user stops emulation and starts a new one, go back to the expected
// behavior of asking.
user_flag_cmd_line = false;
if (!LoadROM(filename, program_id, program_index, launch_type)) { if (!LoadROM(filename, program_id, program_index, launch_type)) {
return; return;
} }
@ -2477,8 +2460,10 @@ 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 res = const auto& fs_controller = system->GetFileSystemController();
ContentManager::RemoveBaseContent(system->GetFileSystemController(), program_id); const auto res = fs_controller.GetUserNANDContents()->RemoveExistingEntry(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."));
@ -2490,7 +2475,11 @@ 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 res = ContentManager::RemoveUpdate(system->GetFileSystemController(), program_id); const auto update_id = program_id | 0x800;
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."));
@ -2501,7 +2490,22 @@ void GMainWindow::RemoveUpdateContent(u64 program_id, InstalledEntryType type) {
} }
void GMainWindow::RemoveAddOnContent(u64 program_id, InstalledEntryType type) { void GMainWindow::RemoveAddOnContent(u64 program_id, InstalledEntryType type) {
const size_t count = ContentManager::RemoveAllDLC(system.get(), program_id); u32 count{};
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."));
@ -3270,21 +3274,12 @@ 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<ContentManager::InstallResult> future; QFuture<InstallResult> future;
ContentManager::InstallResult result; 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) {
emit UpdateInstallProgress(); future = QtConcurrent::run([this, &file] { return InstallNSP(file); });
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();
@ -3300,16 +3295,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 ContentManager::InstallResult::Success: case InstallResult::Success:
new_files.append(QFileInfo(file).fileName()); new_files.append(QFileInfo(file).fileName());
break; break;
case ContentManager::InstallResult::Overwrite: case InstallResult::Overwrite:
overwritten_files.append(QFileInfo(file).fileName()); overwritten_files.append(QFileInfo(file).fileName());
break; break;
case ContentManager::InstallResult::Failure: case InstallResult::Failure:
failed_files.append(QFileInfo(file).fileName()); failed_files.append(QFileInfo(file).fileName());
break; break;
case ContentManager::InstallResult::BaseInstallAttempted: case InstallResult::BaseInstallAttempted:
failed_files.append(QFileInfo(file).fileName()); failed_files.append(QFileInfo(file).fileName());
detected_base_install = true; detected_base_install = true;
break; break;
@ -3343,7 +3338,96 @@ void GMainWindow::OnMenuInstallToNAND() {
ui->action_Install_File_NAND->setEnabled(true); ui->action_Install_File_NAND->setEnabled(true);
} }
ContentManager::InstallResult GMainWindow::InstallNCA(const QString& filename) { InstallResult GMainWindow::InstallNSP(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"),
@ -3364,7 +3448,7 @@ ContentManager::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 ContentManager::InstallResult::Failure; return 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.
@ -3378,15 +3462,15 @@ ContentManager::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 progress_callback = [this](size_t size, size_t progress) { const auto res = registered_cache->InstallEntry(*nca, static_cast<FileSys::TitleType>(index),
emit UpdateInstallProgress(); true, qt_raw_copy);
if (install_progress->wasCanceled()) { if (res == FileSys::InstallResult::Success) {
return true; return InstallResult::Success;
} } else if (res == FileSys::InstallResult::OverwriteExisting) {
return false; return InstallResult::Overwrite;
}; } else {
return ContentManager::InstallNCA(vfs.get(), filename.toStdString(), registered_cache, return InstallResult::Failure;
static_cast<FileSys::TitleType>(index), progress_callback); }
} }
void GMainWindow::OnMenuRecentFile() { void GMainWindow::OnMenuRecentFile() {

View File

@ -16,7 +16,6 @@
#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"
@ -125,6 +124,13 @@ enum class EmulatedDirectoryTarget {
SDMC, SDMC,
}; };
enum class InstallResult {
Success,
Overwrite,
Failure,
BaseInstallAttempted,
};
enum class ReinitializeKeyBehavior { enum class ReinitializeKeyBehavior {
NoWarning, NoWarning,
Warning, Warning,
@ -421,7 +427,8 @@ 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);
ContentManager::InstallResult InstallNCA(const QString& filename); InstallResult InstallNSP(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 = {});
@ -516,8 +523,6 @@ private:
std::unique_ptr<EmuThread> emu_thread; std::unique_ptr<EmuThread> emu_thread;
// The path to the game currently running // The path to the game currently running
QString current_game_path; QString current_game_path;
// Whether a user was set on the command line (skips UserSelector if it's forced to show up)
bool user_flag_cmd_line = false;
bool auto_paused = false; bool auto_paused = false;
bool auto_muted = false; bool auto_muted = false;