diff options
33 files changed, 912 insertions, 424 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index b7556e353..1c9fb0675 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt | |||
| @@ -21,6 +21,8 @@ import org.yuzu.yuzu_emu.utils.DocumentsTree | |||
| 21 | import org.yuzu.yuzu_emu.utils.FileUtil | 21 | import org.yuzu.yuzu_emu.utils.FileUtil |
| 22 | import org.yuzu.yuzu_emu.utils.Log | 22 | import org.yuzu.yuzu_emu.utils.Log |
| 23 | import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable | 23 | import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable |
| 24 | import org.yuzu.yuzu_emu.model.InstallResult | ||
| 25 | import org.yuzu.yuzu_emu.model.Patch | ||
| 24 | 26 | ||
| 25 | /** | 27 | /** |
| 26 | * Class which contains methods that interact | 28 | * Class which contains methods that interact |
| @@ -235,9 +237,12 @@ object NativeLibrary { | |||
| 235 | /** | 237 | /** |
| 236 | * Installs a nsp or xci file to nand | 238 | * Installs a nsp or xci file to nand |
| 237 | * @param filename String representation of file uri | 239 | * @param filename String representation of file uri |
| 238 | * @param extension Lowercase string representation of file extension without "." | 240 | * @return int representation of [InstallResult] |
| 239 | */ | 241 | */ |
| 240 | external fun installFileToNand(filename: String, extension: String): Int | 242 | external fun installFileToNand( |
| 243 | filename: String, | ||
| 244 | callback: (max: Long, progress: Long) -> Boolean | ||
| 245 | ): Int | ||
| 241 | 246 | ||
| 242 | external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean | 247 | external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean |
| 243 | 248 | ||
| @@ -535,9 +540,29 @@ object NativeLibrary { | |||
| 535 | * | 540 | * |
| 536 | * @param path Path to game file. Can be a [Uri]. | 541 | * @param path Path to game file. Can be a [Uri]. |
| 537 | * @param programId String representation of a game's program ID | 542 | * @param programId String representation of a game's program ID |
| 538 | * @return Array of pairs where the first value is the name of an addon and the second is the version | 543 | * @return Array of available patches |
| 539 | */ | 544 | */ |
| 540 | external fun getAddonsForFile(path: String, programId: String): Array<Pair<String, String>>? | 545 | external fun getPatchesForFile(path: String, programId: String): Array<Patch>? |
| 546 | |||
| 547 | /** | ||
| 548 | * Removes an update for a given [programId] | ||
| 549 | * @param programId String representation of a game's program ID | ||
| 550 | */ | ||
| 551 | external fun removeUpdate(programId: String) | ||
| 552 | |||
| 553 | /** | ||
| 554 | * Removes all DLC for a [programId] | ||
| 555 | * @param programId String representation of a game's program ID | ||
| 556 | */ | ||
| 557 | external fun removeDLC(programId: String) | ||
| 558 | |||
| 559 | /** | ||
| 560 | * Removes a mod installed for a given [programId] | ||
| 561 | * @param programId String representation of a game's program ID | ||
| 562 | * @param name The name of a mod as given by [getPatchesForFile]. This corresponds with the name | ||
| 563 | * of the mod's directory in a game's load folder. | ||
| 564 | */ | ||
| 565 | external fun removeMod(programId: String, name: String) | ||
| 541 | 566 | ||
| 542 | /** | 567 | /** |
| 543 | * Gets the save location for a specific game | 568 | * Gets the save location for a specific game |
| @@ -609,15 +634,4 @@ object NativeLibrary { | |||
| 609 | const val RELEASED = 0 | 634 | const val RELEASED = 0 |
| 610 | const val PRESSED = 1 | 635 | const val PRESSED = 1 |
| 611 | } | 636 | } |
| 612 | |||
| 613 | /** | ||
| 614 | * Result from installFileToNand | ||
| 615 | */ | ||
| 616 | object InstallFileToNandResult { | ||
| 617 | const val Success = 0 | ||
| 618 | const val SuccessFileOverwritten = 1 | ||
| 619 | const val Error = 2 | ||
| 620 | const val ErrorBaseGame = 3 | ||
| 621 | const val ErrorFilenameExtension = 4 | ||
| 622 | } | ||
| 623 | } | 637 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt index 94c151325..ff254d9b7 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt | |||
| @@ -6,27 +6,32 @@ package org.yuzu.yuzu_emu.adapters | |||
| 6 | import android.view.LayoutInflater | 6 | import android.view.LayoutInflater |
| 7 | import android.view.ViewGroup | 7 | import android.view.ViewGroup |
| 8 | import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding | 8 | import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding |
| 9 | import org.yuzu.yuzu_emu.model.Addon | 9 | import org.yuzu.yuzu_emu.model.Patch |
| 10 | import org.yuzu.yuzu_emu.model.AddonViewModel | ||
| 10 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder | 11 | import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder |
| 11 | 12 | ||
| 12 | class AddonAdapter : AbstractDiffAdapter<Addon, AddonAdapter.AddonViewHolder>() { | 13 | class AddonAdapter(val addonViewModel: AddonViewModel) : |
| 14 | AbstractDiffAdapter<Patch, AddonAdapter.AddonViewHolder>() { | ||
| 13 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder { | 15 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder { |
| 14 | ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false) | 16 | ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false) |
| 15 | .also { return AddonViewHolder(it) } | 17 | .also { return AddonViewHolder(it) } |
| 16 | } | 18 | } |
| 17 | 19 | ||
| 18 | inner class AddonViewHolder(val binding: ListItemAddonBinding) : | 20 | inner class AddonViewHolder(val binding: ListItemAddonBinding) : |
| 19 | AbstractViewHolder<Addon>(binding) { | 21 | AbstractViewHolder<Patch>(binding) { |
| 20 | override fun bind(model: Addon) { | 22 | override fun bind(model: Patch) { |
| 21 | binding.root.setOnClickListener { | 23 | binding.root.setOnClickListener { |
| 22 | binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked | 24 | binding.addonCheckbox.isChecked = !binding.addonCheckbox.isChecked |
| 23 | } | 25 | } |
| 24 | binding.title.text = model.title | 26 | binding.title.text = model.name |
| 25 | binding.version.text = model.version | 27 | binding.version.text = model.version |
| 26 | binding.addonSwitch.setOnCheckedChangeListener { _, checked -> | 28 | binding.addonCheckbox.setOnCheckedChangeListener { _, checked -> |
| 27 | model.enabled = checked | 29 | model.enabled = checked |
| 28 | } | 30 | } |
| 29 | binding.addonSwitch.isChecked = model.enabled | 31 | binding.addonCheckbox.isChecked = model.enabled |
| 32 | binding.buttonDelete.setOnClickListener { | ||
| 33 | addonViewModel.setAddonToDelete(model) | ||
| 34 | } | ||
| 30 | } | 35 | } |
| 31 | } | 36 | } |
| 32 | } | 37 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt index 816336820..adb65812c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt | |||
| @@ -74,7 +74,7 @@ class AddonsFragment : Fragment() { | |||
| 74 | 74 | ||
| 75 | binding.listAddons.apply { | 75 | binding.listAddons.apply { |
| 76 | layoutManager = LinearLayoutManager(requireContext()) | 76 | layoutManager = LinearLayoutManager(requireContext()) |
| 77 | adapter = AddonAdapter() | 77 | adapter = AddonAdapter(addonViewModel) |
| 78 | } | 78 | } |
| 79 | 79 | ||
| 80 | viewLifecycleOwner.lifecycleScope.apply { | 80 | viewLifecycleOwner.lifecycleScope.apply { |
| @@ -110,6 +110,21 @@ class AddonsFragment : Fragment() { | |||
| 110 | } | 110 | } |
| 111 | } | 111 | } |
| 112 | } | 112 | } |
| 113 | launch { | ||
| 114 | repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
| 115 | addonViewModel.addonToDelete.collect { | ||
| 116 | if (it != null) { | ||
| 117 | MessageDialogFragment.newInstance( | ||
| 118 | requireActivity(), | ||
| 119 | titleId = R.string.confirm_uninstall, | ||
| 120 | descriptionId = R.string.confirm_uninstall_description, | ||
| 121 | positiveAction = { addonViewModel.onDeleteAddon(it) } | ||
| 122 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 123 | addonViewModel.setAddonToDelete(null) | ||
| 124 | } | ||
| 125 | } | ||
| 126 | } | ||
| 127 | } | ||
| 113 | } | 128 | } |
| 114 | 129 | ||
| 115 | binding.buttonInstall.setOnClickListener { | 130 | binding.buttonInstall.setOnClickListener { |
| @@ -156,22 +171,22 @@ class AddonsFragment : Fragment() { | |||
| 156 | descriptionId = R.string.invalid_directory_description | 171 | descriptionId = R.string.invalid_directory_description |
| 157 | ) | 172 | ) |
| 158 | if (isValid) { | 173 | if (isValid) { |
| 159 | IndeterminateProgressDialogFragment.newInstance( | 174 | ProgressDialogFragment.newInstance( |
| 160 | requireActivity(), | 175 | requireActivity(), |
| 161 | R.string.installing_game_content, | 176 | R.string.installing_game_content, |
| 162 | false | 177 | false |
| 163 | ) { | 178 | ) { progressCallback, _ -> |
| 164 | val parentDirectoryName = externalAddonDirectory.name | 179 | val parentDirectoryName = externalAddonDirectory.name |
| 165 | val internalAddonDirectory = | 180 | val internalAddonDirectory = |
| 166 | File(args.game.addonDir + parentDirectoryName) | 181 | File(args.game.addonDir + parentDirectoryName) |
| 167 | try { | 182 | try { |
| 168 | externalAddonDirectory.copyFilesTo(internalAddonDirectory) | 183 | externalAddonDirectory.copyFilesTo(internalAddonDirectory, progressCallback) |
| 169 | } catch (_: Exception) { | 184 | } catch (_: Exception) { |
| 170 | return@newInstance errorMessage | 185 | return@newInstance errorMessage |
| 171 | } | 186 | } |
| 172 | addonViewModel.refreshAddons() | 187 | addonViewModel.refreshAddons() |
| 173 | return@newInstance getString(R.string.addon_installed_successfully) | 188 | return@newInstance getString(R.string.addon_installed_successfully) |
| 174 | }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) | 189 | }.show(parentFragmentManager, ProgressDialogFragment.TAG) |
| 175 | } else { | 190 | } else { |
| 176 | errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG) | 191 | errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG) |
| 177 | } | 192 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt index 9dabb9c41..6c758d80b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt | |||
| @@ -173,11 +173,11 @@ class DriverManagerFragment : Fragment() { | |||
| 173 | return@registerForActivityResult | 173 | return@registerForActivityResult |
| 174 | } | 174 | } |
| 175 | 175 | ||
| 176 | IndeterminateProgressDialogFragment.newInstance( | 176 | ProgressDialogFragment.newInstance( |
| 177 | requireActivity(), | 177 | requireActivity(), |
| 178 | R.string.installing_driver, | 178 | R.string.installing_driver, |
| 179 | false | 179 | false |
| 180 | ) { | 180 | ) { _, _ -> |
| 181 | val driverPath = | 181 | val driverPath = |
| 182 | "${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename(result)}" | 182 | "${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename(result)}" |
| 183 | val driverFile = File(driverPath) | 183 | val driverFile = File(driverPath) |
| @@ -213,6 +213,6 @@ class DriverManagerFragment : Fragment() { | |||
| 213 | } | 213 | } |
| 214 | } | 214 | } |
| 215 | return@newInstance Any() | 215 | return@newInstance Any() |
| 216 | }.show(childFragmentManager, IndeterminateProgressDialogFragment.TAG) | 216 | }.show(childFragmentManager, ProgressDialogFragment.TAG) |
| 217 | } | 217 | } |
| 218 | } | 218 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt index b04d1208f..83a845434 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt | |||
| @@ -44,7 +44,6 @@ import org.yuzu.yuzu_emu.utils.FileUtil | |||
| 44 | import org.yuzu.yuzu_emu.utils.GameIconUtils | 44 | import org.yuzu.yuzu_emu.utils.GameIconUtils |
| 45 | import org.yuzu.yuzu_emu.utils.GpuDriverHelper | 45 | import org.yuzu.yuzu_emu.utils.GpuDriverHelper |
| 46 | import org.yuzu.yuzu_emu.utils.MemoryUtil | 46 | import org.yuzu.yuzu_emu.utils.MemoryUtil |
| 47 | import java.io.BufferedInputStream | ||
| 48 | import java.io.BufferedOutputStream | 47 | import java.io.BufferedOutputStream |
| 49 | import java.io.File | 48 | import java.io.File |
| 50 | 49 | ||
| @@ -357,27 +356,17 @@ class GamePropertiesFragment : Fragment() { | |||
| 357 | return@registerForActivityResult | 356 | return@registerForActivityResult |
| 358 | } | 357 | } |
| 359 | 358 | ||
| 360 | val inputZip = requireContext().contentResolver.openInputStream(result) | ||
| 361 | val savesFolder = File(args.game.saveDir) | 359 | val savesFolder = File(args.game.saveDir) |
| 362 | val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") | 360 | val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") |
| 363 | cacheSaveDir.mkdir() | 361 | cacheSaveDir.mkdir() |
| 364 | 362 | ||
| 365 | if (inputZip == null) { | 363 | ProgressDialogFragment.newInstance( |
| 366 | Toast.makeText( | ||
| 367 | YuzuApplication.appContext, | ||
| 368 | getString(R.string.fatal_error), | ||
| 369 | Toast.LENGTH_LONG | ||
| 370 | ).show() | ||
| 371 | return@registerForActivityResult | ||
| 372 | } | ||
| 373 | |||
| 374 | IndeterminateProgressDialogFragment.newInstance( | ||
| 375 | requireActivity(), | 364 | requireActivity(), |
| 376 | R.string.save_files_importing, | 365 | R.string.save_files_importing, |
| 377 | false | 366 | false |
| 378 | ) { | 367 | ) { _, _ -> |
| 379 | try { | 368 | try { |
| 380 | FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) | 369 | FileUtil.unzipToInternalStorage(result.toString(), cacheSaveDir) |
| 381 | val files = cacheSaveDir.listFiles() | 370 | val files = cacheSaveDir.listFiles() |
| 382 | var savesFolderFile: File? = null | 371 | var savesFolderFile: File? = null |
| 383 | if (files != null) { | 372 | if (files != null) { |
| @@ -422,7 +411,7 @@ class GamePropertiesFragment : Fragment() { | |||
| 422 | Toast.LENGTH_LONG | 411 | Toast.LENGTH_LONG |
| 423 | ).show() | 412 | ).show() |
| 424 | } | 413 | } |
| 425 | }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) | 414 | }.show(parentFragmentManager, ProgressDialogFragment.TAG) |
| 426 | } | 415 | } |
| 427 | 416 | ||
| 428 | /** | 417 | /** |
| @@ -436,11 +425,11 @@ class GamePropertiesFragment : Fragment() { | |||
| 436 | return@registerForActivityResult | 425 | return@registerForActivityResult |
| 437 | } | 426 | } |
| 438 | 427 | ||
| 439 | IndeterminateProgressDialogFragment.newInstance( | 428 | ProgressDialogFragment.newInstance( |
| 440 | requireActivity(), | 429 | requireActivity(), |
| 441 | R.string.save_files_exporting, | 430 | R.string.save_files_exporting, |
| 442 | false | 431 | false |
| 443 | ) { | 432 | ) { _, _ -> |
| 444 | val saveLocation = args.game.saveDir | 433 | val saveLocation = args.game.saveDir |
| 445 | val zipResult = FileUtil.zipFromInternalStorage( | 434 | val zipResult = FileUtil.zipFromInternalStorage( |
| 446 | File(saveLocation), | 435 | File(saveLocation), |
| @@ -452,6 +441,6 @@ class GamePropertiesFragment : Fragment() { | |||
| 452 | TaskState.Completed -> getString(R.string.export_success) | 441 | TaskState.Completed -> getString(R.string.export_success) |
| 453 | TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) | 442 | TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) |
| 454 | } | 443 | } |
| 455 | }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) | 444 | }.show(parentFragmentManager, ProgressDialogFragment.TAG) |
| 456 | } | 445 | } |
| 457 | } | 446 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt index 5b4bf2c9f..7df8e6bf4 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt | |||
| @@ -34,7 +34,6 @@ import org.yuzu.yuzu_emu.model.TaskState | |||
| 34 | import org.yuzu.yuzu_emu.ui.main.MainActivity | 34 | import org.yuzu.yuzu_emu.ui.main.MainActivity |
| 35 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization | 35 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization |
| 36 | import org.yuzu.yuzu_emu.utils.FileUtil | 36 | import org.yuzu.yuzu_emu.utils.FileUtil |
| 37 | import java.io.BufferedInputStream | ||
| 38 | import java.io.BufferedOutputStream | 37 | import java.io.BufferedOutputStream |
| 39 | import java.io.File | 38 | import java.io.File |
| 40 | import java.math.BigInteger | 39 | import java.math.BigInteger |
| @@ -195,26 +194,20 @@ class InstallableFragment : Fragment() { | |||
| 195 | return@registerForActivityResult | 194 | return@registerForActivityResult |
| 196 | } | 195 | } |
| 197 | 196 | ||
| 198 | val inputZip = requireContext().contentResolver.openInputStream(result) | ||
| 199 | val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") | 197 | val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") |
| 200 | cacheSaveDir.mkdir() | 198 | cacheSaveDir.mkdir() |
| 201 | 199 | ||
| 202 | if (inputZip == null) { | 200 | ProgressDialogFragment.newInstance( |
| 203 | Toast.makeText( | ||
| 204 | YuzuApplication.appContext, | ||
| 205 | getString(R.string.fatal_error), | ||
| 206 | Toast.LENGTH_LONG | ||
| 207 | ).show() | ||
| 208 | return@registerForActivityResult | ||
| 209 | } | ||
| 210 | |||
| 211 | IndeterminateProgressDialogFragment.newInstance( | ||
| 212 | requireActivity(), | 201 | requireActivity(), |
| 213 | R.string.save_files_importing, | 202 | R.string.save_files_importing, |
| 214 | false | 203 | false |
| 215 | ) { | 204 | ) { progressCallback, _ -> |
| 216 | try { | 205 | try { |
| 217 | FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) | 206 | FileUtil.unzipToInternalStorage( |
| 207 | result.toString(), | ||
| 208 | cacheSaveDir, | ||
| 209 | progressCallback | ||
| 210 | ) | ||
| 218 | val files = cacheSaveDir.listFiles() | 211 | val files = cacheSaveDir.listFiles() |
| 219 | var successfulImports = 0 | 212 | var successfulImports = 0 |
| 220 | var failedImports = 0 | 213 | var failedImports = 0 |
| @@ -287,7 +280,7 @@ class InstallableFragment : Fragment() { | |||
| 287 | Toast.LENGTH_LONG | 280 | Toast.LENGTH_LONG |
| 288 | ).show() | 281 | ).show() |
| 289 | } | 282 | } |
| 290 | }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) | 283 | }.show(parentFragmentManager, ProgressDialogFragment.TAG) |
| 291 | } | 284 | } |
| 292 | 285 | ||
| 293 | private val exportSaves = registerForActivityResult( | 286 | private val exportSaves = registerForActivityResult( |
| @@ -297,11 +290,11 @@ class InstallableFragment : Fragment() { | |||
| 297 | return@registerForActivityResult | 290 | return@registerForActivityResult |
| 298 | } | 291 | } |
| 299 | 292 | ||
| 300 | IndeterminateProgressDialogFragment.newInstance( | 293 | ProgressDialogFragment.newInstance( |
| 301 | requireActivity(), | 294 | requireActivity(), |
| 302 | R.string.save_files_exporting, | 295 | R.string.save_files_exporting, |
| 303 | false | 296 | false |
| 304 | ) { | 297 | ) { _, _ -> |
| 305 | val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") | 298 | val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") |
| 306 | cacheSaveDir.mkdir() | 299 | cacheSaveDir.mkdir() |
| 307 | 300 | ||
| @@ -338,6 +331,6 @@ class InstallableFragment : Fragment() { | |||
| 338 | TaskState.Completed -> getString(R.string.export_success) | 331 | TaskState.Completed -> getString(R.string.export_success) |
| 339 | TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) | 332 | TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) |
| 340 | } | 333 | } |
| 341 | }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) | 334 | }.show(parentFragmentManager, ProgressDialogFragment.TAG) |
| 342 | } | 335 | } |
| 343 | } | 336 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt index 8847e5531..d201cb80c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt | |||
| @@ -23,11 +23,13 @@ import org.yuzu.yuzu_emu.R | |||
| 23 | import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding | 23 | import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding |
| 24 | import org.yuzu.yuzu_emu.model.TaskViewModel | 24 | import org.yuzu.yuzu_emu.model.TaskViewModel |
| 25 | 25 | ||
| 26 | class IndeterminateProgressDialogFragment : DialogFragment() { | 26 | class ProgressDialogFragment : DialogFragment() { |
| 27 | private val taskViewModel: TaskViewModel by activityViewModels() | 27 | private val taskViewModel: TaskViewModel by activityViewModels() |
| 28 | 28 | ||
| 29 | private lateinit var binding: DialogProgressBarBinding | 29 | private lateinit var binding: DialogProgressBarBinding |
| 30 | 30 | ||
| 31 | private val PROGRESS_BAR_RESOLUTION = 1000 | ||
| 32 | |||
| 31 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { | 33 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { |
| 32 | val titleId = requireArguments().getInt(TITLE) | 34 | val titleId = requireArguments().getInt(TITLE) |
| 33 | val cancellable = requireArguments().getBoolean(CANCELLABLE) | 35 | val cancellable = requireArguments().getBoolean(CANCELLABLE) |
| @@ -61,6 +63,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() { | |||
| 61 | 63 | ||
| 62 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | 64 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| 63 | super.onViewCreated(view, savedInstanceState) | 65 | super.onViewCreated(view, savedInstanceState) |
| 66 | binding.message.isSelected = true | ||
| 64 | viewLifecycleOwner.lifecycleScope.apply { | 67 | viewLifecycleOwner.lifecycleScope.apply { |
| 65 | launch { | 68 | launch { |
| 66 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 69 | repeatOnLifecycle(Lifecycle.State.CREATED) { |
| @@ -97,6 +100,35 @@ class IndeterminateProgressDialogFragment : DialogFragment() { | |||
| 97 | } | 100 | } |
| 98 | } | 101 | } |
| 99 | } | 102 | } |
| 103 | launch { | ||
| 104 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 105 | taskViewModel.progress.collect { | ||
| 106 | if (it != 0.0) { | ||
| 107 | binding.progressBar.apply { | ||
| 108 | isIndeterminate = false | ||
| 109 | progress = ( | ||
| 110 | (it / taskViewModel.maxProgress.value) * | ||
| 111 | PROGRESS_BAR_RESOLUTION | ||
| 112 | ).toInt() | ||
| 113 | min = 0 | ||
| 114 | max = PROGRESS_BAR_RESOLUTION | ||
| 115 | } | ||
| 116 | } | ||
| 117 | } | ||
| 118 | } | ||
| 119 | } | ||
| 120 | launch { | ||
| 121 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 122 | taskViewModel.message.collect { | ||
| 123 | if (it.isEmpty()) { | ||
| 124 | binding.message.visibility = View.GONE | ||
| 125 | } else { | ||
| 126 | binding.message.visibility = View.VISIBLE | ||
| 127 | binding.message.text = it | ||
| 128 | } | ||
| 129 | } | ||
| 130 | } | ||
| 131 | } | ||
| 100 | } | 132 | } |
| 101 | } | 133 | } |
| 102 | 134 | ||
| @@ -108,6 +140,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() { | |||
| 108 | val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE) | 140 | val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE) |
| 109 | negativeButton.setOnClickListener { | 141 | negativeButton.setOnClickListener { |
| 110 | alertDialog.setTitle(getString(R.string.cancelling)) | 142 | alertDialog.setTitle(getString(R.string.cancelling)) |
| 143 | binding.progressBar.isIndeterminate = true | ||
| 111 | taskViewModel.setCancelled(true) | 144 | taskViewModel.setCancelled(true) |
| 112 | } | 145 | } |
| 113 | } | 146 | } |
| @@ -122,9 +155,12 @@ class IndeterminateProgressDialogFragment : DialogFragment() { | |||
| 122 | activity: FragmentActivity, | 155 | activity: FragmentActivity, |
| 123 | titleId: Int, | 156 | titleId: Int, |
| 124 | cancellable: Boolean = false, | 157 | cancellable: Boolean = false, |
| 125 | task: suspend () -> Any | 158 | task: suspend ( |
| 126 | ): IndeterminateProgressDialogFragment { | 159 | progressCallback: (max: Long, progress: Long) -> Boolean, |
| 127 | val dialog = IndeterminateProgressDialogFragment() | 160 | messageCallback: (message: String) -> Unit |
| 161 | ) -> Any | ||
| 162 | ): ProgressDialogFragment { | ||
| 163 | val dialog = ProgressDialogFragment() | ||
| 128 | val args = Bundle() | 164 | val args = Bundle() |
| 129 | ViewModelProvider(activity)[TaskViewModel::class.java].task = task | 165 | ViewModelProvider(activity)[TaskViewModel::class.java].task = task |
| 130 | args.putInt(TITLE, titleId) | 166 | args.putInt(TITLE, titleId) |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt deleted file mode 100644 index ed79a8b02..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt +++ /dev/null | |||
| @@ -1,10 +0,0 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.model | ||
| 5 | |||
| 6 | data class Addon( | ||
| 7 | var enabled: Boolean, | ||
| 8 | val title: String, | ||
| 9 | val version: String | ||
| 10 | ) | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt index 075252f5b..b9c8e49ca 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt | |||
| @@ -15,8 +15,8 @@ import org.yuzu.yuzu_emu.utils.NativeConfig | |||
| 15 | import java.util.concurrent.atomic.AtomicBoolean | 15 | import java.util.concurrent.atomic.AtomicBoolean |
| 16 | 16 | ||
| 17 | class AddonViewModel : ViewModel() { | 17 | class AddonViewModel : ViewModel() { |
| 18 | private val _addonList = MutableStateFlow(mutableListOf<Addon>()) | 18 | private val _patchList = MutableStateFlow(mutableListOf<Patch>()) |
| 19 | val addonList get() = _addonList.asStateFlow() | 19 | val addonList get() = _patchList.asStateFlow() |
| 20 | 20 | ||
| 21 | private val _showModInstallPicker = MutableStateFlow(false) | 21 | private val _showModInstallPicker = MutableStateFlow(false) |
| 22 | val showModInstallPicker get() = _showModInstallPicker.asStateFlow() | 22 | val showModInstallPicker get() = _showModInstallPicker.asStateFlow() |
| @@ -24,6 +24,9 @@ class AddonViewModel : ViewModel() { | |||
| 24 | private val _showModNoticeDialog = MutableStateFlow(false) | 24 | private val _showModNoticeDialog = MutableStateFlow(false) |
| 25 | val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow() | 25 | val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow() |
| 26 | 26 | ||
| 27 | private val _addonToDelete = MutableStateFlow<Patch?>(null) | ||
| 28 | val addonToDelete = _addonToDelete.asStateFlow() | ||
| 29 | |||
| 27 | var game: Game? = null | 30 | var game: Game? = null |
| 28 | 31 | ||
| 29 | private val isRefreshing = AtomicBoolean(false) | 32 | private val isRefreshing = AtomicBoolean(false) |
| @@ -40,36 +43,47 @@ class AddonViewModel : ViewModel() { | |||
| 40 | isRefreshing.set(true) | 43 | isRefreshing.set(true) |
| 41 | viewModelScope.launch { | 44 | viewModelScope.launch { |
| 42 | withContext(Dispatchers.IO) { | 45 | withContext(Dispatchers.IO) { |
| 43 | val addonList = mutableListOf<Addon>() | 46 | val patchList = ( |
| 44 | val disabledAddons = NativeConfig.getDisabledAddons(game!!.programId) | 47 | NativeLibrary.getPatchesForFile(game!!.path, game!!.programId) |
| 45 | NativeLibrary.getAddonsForFile(game!!.path, game!!.programId)?.forEach { | 48 | ?: emptyArray() |
| 46 | val name = it.first.replace("[D] ", "") | 49 | ).toMutableList() |
| 47 | addonList.add(Addon(!disabledAddons.contains(name), name, it.second)) | 50 | patchList.sortBy { it.name } |
| 48 | } | 51 | _patchList.value = patchList |
| 49 | addonList.sortBy { it.title } | ||
| 50 | _addonList.value = addonList | ||
| 51 | isRefreshing.set(false) | 52 | isRefreshing.set(false) |
| 52 | } | 53 | } |
| 53 | } | 54 | } |
| 54 | } | 55 | } |
| 55 | 56 | ||
| 57 | fun setAddonToDelete(patch: Patch?) { | ||
| 58 | _addonToDelete.value = patch | ||
| 59 | } | ||
| 60 | |||
| 61 | fun onDeleteAddon(patch: Patch) { | ||
| 62 | when (PatchType.from(patch.type)) { | ||
| 63 | PatchType.Update -> NativeLibrary.removeUpdate(patch.programId) | ||
| 64 | PatchType.DLC -> NativeLibrary.removeDLC(patch.programId) | ||
| 65 | PatchType.Mod -> NativeLibrary.removeMod(patch.programId, patch.name) | ||
| 66 | } | ||
| 67 | refreshAddons() | ||
| 68 | } | ||
| 69 | |||
| 56 | fun onCloseAddons() { | 70 | fun onCloseAddons() { |
| 57 | if (_addonList.value.isEmpty()) { | 71 | if (_patchList.value.isEmpty()) { |
| 58 | return | 72 | return |
| 59 | } | 73 | } |
| 60 | 74 | ||
| 61 | NativeConfig.setDisabledAddons( | 75 | NativeConfig.setDisabledAddons( |
| 62 | game!!.programId, | 76 | game!!.programId, |
| 63 | _addonList.value.mapNotNull { | 77 | _patchList.value.mapNotNull { |
| 64 | if (it.enabled) { | 78 | if (it.enabled) { |
| 65 | null | 79 | null |
| 66 | } else { | 80 | } else { |
| 67 | it.title | 81 | it.name |
| 68 | } | 82 | } |
| 69 | }.toTypedArray() | 83 | }.toTypedArray() |
| 70 | ) | 84 | ) |
| 71 | NativeConfig.saveGlobalConfig() | 85 | NativeConfig.saveGlobalConfig() |
| 72 | _addonList.value.clear() | 86 | _patchList.value.clear() |
| 73 | game = null | 87 | game = null |
| 74 | } | 88 | } |
| 75 | 89 | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/InstallResult.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/InstallResult.kt new file mode 100644 index 000000000..0c3cd0521 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/InstallResult.kt | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.model | ||
| 5 | |||
| 6 | enum class InstallResult(val int: Int) { | ||
| 7 | Success(0), | ||
| 8 | Overwrite(1), | ||
| 9 | Failure(2), | ||
| 10 | BaseInstallAttempted(3); | ||
| 11 | |||
| 12 | companion object { | ||
| 13 | fun from(int: Int): InstallResult = entries.firstOrNull { it.int == int } ?: Success | ||
| 14 | } | ||
| 15 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Patch.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Patch.kt new file mode 100644 index 000000000..25cb9e365 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Patch.kt | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.model | ||
| 5 | |||
| 6 | import androidx.annotation.Keep | ||
| 7 | |||
| 8 | @Keep | ||
| 9 | data class Patch( | ||
| 10 | var enabled: Boolean, | ||
| 11 | val name: String, | ||
| 12 | val version: String, | ||
| 13 | val type: Int, | ||
| 14 | val programId: String, | ||
| 15 | val titleId: String | ||
| 16 | ) | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/PatchType.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/PatchType.kt new file mode 100644 index 000000000..e9a54162b --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/PatchType.kt | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.model | ||
| 5 | |||
| 6 | enum class PatchType(val int: Int) { | ||
| 7 | Update(0), | ||
| 8 | DLC(1), | ||
| 9 | Mod(2); | ||
| 10 | |||
| 11 | companion object { | ||
| 12 | fun from(int: Int): PatchType = entries.firstOrNull { it.int == int } ?: Update | ||
| 13 | } | ||
| 14 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt index e59c95733..4361eb972 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt | |||
| @@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope | |||
| 8 | import kotlinx.coroutines.Dispatchers | 8 | import kotlinx.coroutines.Dispatchers |
| 9 | import kotlinx.coroutines.flow.MutableStateFlow | 9 | import kotlinx.coroutines.flow.MutableStateFlow |
| 10 | import kotlinx.coroutines.flow.StateFlow | 10 | import kotlinx.coroutines.flow.StateFlow |
| 11 | import kotlinx.coroutines.flow.asStateFlow | ||
| 11 | import kotlinx.coroutines.launch | 12 | import kotlinx.coroutines.launch |
| 12 | 13 | ||
| 13 | class TaskViewModel : ViewModel() { | 14 | class TaskViewModel : ViewModel() { |
| @@ -23,13 +24,28 @@ class TaskViewModel : ViewModel() { | |||
| 23 | val cancelled: StateFlow<Boolean> get() = _cancelled | 24 | val cancelled: StateFlow<Boolean> get() = _cancelled |
| 24 | private val _cancelled = MutableStateFlow(false) | 25 | private val _cancelled = MutableStateFlow(false) |
| 25 | 26 | ||
| 26 | lateinit var task: suspend () -> Any | 27 | private val _progress = MutableStateFlow(0.0) |
| 28 | val progress = _progress.asStateFlow() | ||
| 29 | |||
| 30 | private val _maxProgress = MutableStateFlow(0.0) | ||
| 31 | val maxProgress = _maxProgress.asStateFlow() | ||
| 32 | |||
| 33 | private val _message = MutableStateFlow("") | ||
| 34 | val message = _message.asStateFlow() | ||
| 35 | |||
| 36 | lateinit var task: suspend ( | ||
| 37 | progressCallback: (max: Long, progress: Long) -> Boolean, | ||
| 38 | messageCallback: (message: String) -> Unit | ||
| 39 | ) -> Any | ||
| 27 | 40 | ||
| 28 | fun clear() { | 41 | fun clear() { |
| 29 | _result.value = Any() | 42 | _result.value = Any() |
| 30 | _isComplete.value = false | 43 | _isComplete.value = false |
| 31 | _isRunning.value = false | 44 | _isRunning.value = false |
| 32 | _cancelled.value = false | 45 | _cancelled.value = false |
| 46 | _progress.value = 0.0 | ||
| 47 | _maxProgress.value = 0.0 | ||
| 48 | _message.value = "" | ||
| 33 | } | 49 | } |
| 34 | 50 | ||
| 35 | fun setCancelled(value: Boolean) { | 51 | fun setCancelled(value: Boolean) { |
| @@ -43,7 +59,16 @@ class TaskViewModel : ViewModel() { | |||
| 43 | _isRunning.value = true | 59 | _isRunning.value = true |
| 44 | 60 | ||
| 45 | viewModelScope.launch(Dispatchers.IO) { | 61 | viewModelScope.launch(Dispatchers.IO) { |
| 46 | val res = task() | 62 | val res = task( |
| 63 | { max, progress -> | ||
| 64 | _maxProgress.value = max.toDouble() | ||
| 65 | _progress.value = progress.toDouble() | ||
| 66 | return@task cancelled.value | ||
| 67 | }, | ||
| 68 | { message -> | ||
| 69 | _message.value = message | ||
| 70 | } | ||
| 71 | ) | ||
| 47 | _result.value = res | 72 | _result.value = res |
| 48 | _isComplete.value = true | 73 | _isComplete.value = true |
| 49 | _isRunning.value = false | 74 | _isRunning.value = false |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 644289e25..c2cc29961 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt | |||
| @@ -38,12 +38,13 @@ import org.yuzu.yuzu_emu.activities.EmulationActivity | |||
| 38 | import org.yuzu.yuzu_emu.databinding.ActivityMainBinding | 38 | import org.yuzu.yuzu_emu.databinding.ActivityMainBinding |
| 39 | import org.yuzu.yuzu_emu.features.settings.model.Settings | 39 | import org.yuzu.yuzu_emu.features.settings.model.Settings |
| 40 | import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment | 40 | import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment |
| 41 | import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment | 41 | import org.yuzu.yuzu_emu.fragments.ProgressDialogFragment |
| 42 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment | 42 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment |
| 43 | import org.yuzu.yuzu_emu.model.AddonViewModel | 43 | import org.yuzu.yuzu_emu.model.AddonViewModel |
| 44 | import org.yuzu.yuzu_emu.model.DriverViewModel | 44 | import org.yuzu.yuzu_emu.model.DriverViewModel |
| 45 | import org.yuzu.yuzu_emu.model.GamesViewModel | 45 | import org.yuzu.yuzu_emu.model.GamesViewModel |
| 46 | import org.yuzu.yuzu_emu.model.HomeViewModel | 46 | import org.yuzu.yuzu_emu.model.HomeViewModel |
| 47 | import org.yuzu.yuzu_emu.model.InstallResult | ||
| 47 | import org.yuzu.yuzu_emu.model.TaskState | 48 | import org.yuzu.yuzu_emu.model.TaskState |
| 48 | import org.yuzu.yuzu_emu.model.TaskViewModel | 49 | import org.yuzu.yuzu_emu.model.TaskViewModel |
| 49 | import org.yuzu.yuzu_emu.utils.* | 50 | import org.yuzu.yuzu_emu.utils.* |
| @@ -369,26 +370,23 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 369 | return@registerForActivityResult | 370 | return@registerForActivityResult |
| 370 | } | 371 | } |
| 371 | 372 | ||
| 372 | val inputZip = contentResolver.openInputStream(result) | ||
| 373 | if (inputZip == null) { | ||
| 374 | Toast.makeText( | ||
| 375 | applicationContext, | ||
| 376 | getString(R.string.fatal_error), | ||
| 377 | Toast.LENGTH_LONG | ||
| 378 | ).show() | ||
| 379 | return@registerForActivityResult | ||
| 380 | } | ||
| 381 | |||
| 382 | val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") } | 373 | val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") } |
| 383 | 374 | ||
| 384 | val firmwarePath = | 375 | val firmwarePath = |
| 385 | File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/") | 376 | File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/") |
| 386 | val cacheFirmwareDir = File("${cacheDir.path}/registered/") | 377 | val cacheFirmwareDir = File("${cacheDir.path}/registered/") |
| 387 | 378 | ||
| 388 | val task: () -> Any = { | 379 | ProgressDialogFragment.newInstance( |
| 380 | this, | ||
| 381 | R.string.firmware_installing | ||
| 382 | ) { progressCallback, _ -> | ||
| 389 | var messageToShow: Any | 383 | var messageToShow: Any |
| 390 | try { | 384 | try { |
| 391 | FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheFirmwareDir) | 385 | FileUtil.unzipToInternalStorage( |
| 386 | result.toString(), | ||
| 387 | cacheFirmwareDir, | ||
| 388 | progressCallback | ||
| 389 | ) | ||
| 392 | val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1 | 390 | val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1 |
| 393 | val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 | 391 | val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 |
| 394 | messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { | 392 | messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { |
| @@ -404,18 +402,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 404 | getString(R.string.save_file_imported_success) | 402 | getString(R.string.save_file_imported_success) |
| 405 | } | 403 | } |
| 406 | } catch (e: Exception) { | 404 | } catch (e: Exception) { |
| 405 | Log.error("[MainActivity] Firmware install failed - ${e.message}") | ||
| 407 | messageToShow = getString(R.string.fatal_error) | 406 | messageToShow = getString(R.string.fatal_error) |
| 408 | } finally { | 407 | } finally { |
| 409 | cacheFirmwareDir.deleteRecursively() | 408 | cacheFirmwareDir.deleteRecursively() |
| 410 | } | 409 | } |
| 411 | messageToShow | 410 | messageToShow |
| 412 | } | 411 | }.show(supportFragmentManager, ProgressDialogFragment.TAG) |
| 413 | |||
| 414 | IndeterminateProgressDialogFragment.newInstance( | ||
| 415 | this, | ||
| 416 | R.string.firmware_installing, | ||
| 417 | task = task | ||
| 418 | ).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) | ||
| 419 | } | 412 | } |
| 420 | 413 | ||
| 421 | val getAmiiboKey = | 414 | val getAmiiboKey = |
| @@ -474,11 +467,11 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 474 | return@registerForActivityResult | 467 | return@registerForActivityResult |
| 475 | } | 468 | } |
| 476 | 469 | ||
| 477 | IndeterminateProgressDialogFragment.newInstance( | 470 | ProgressDialogFragment.newInstance( |
| 478 | this@MainActivity, | 471 | this@MainActivity, |
| 479 | R.string.verifying_content, | 472 | R.string.verifying_content, |
| 480 | false | 473 | false |
| 481 | ) { | 474 | ) { _, _ -> |
| 482 | var updatesMatchProgram = true | 475 | var updatesMatchProgram = true |
| 483 | for (document in documents) { | 476 | for (document in documents) { |
| 484 | val valid = NativeLibrary.doesUpdateMatchProgram( | 477 | val valid = NativeLibrary.doesUpdateMatchProgram( |
| @@ -501,44 +494,42 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 501 | positiveAction = { homeViewModel.setContentToInstall(documents) } | 494 | positiveAction = { homeViewModel.setContentToInstall(documents) } |
| 502 | ) | 495 | ) |
| 503 | } | 496 | } |
| 504 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) | 497 | }.show(supportFragmentManager, ProgressDialogFragment.TAG) |
| 505 | } | 498 | } |
| 506 | 499 | ||
| 507 | private fun installContent(documents: List<Uri>) { | 500 | private fun installContent(documents: List<Uri>) { |
| 508 | IndeterminateProgressDialogFragment.newInstance( | 501 | ProgressDialogFragment.newInstance( |
| 509 | this@MainActivity, | 502 | this@MainActivity, |
| 510 | R.string.installing_game_content | 503 | R.string.installing_game_content |
| 511 | ) { | 504 | ) { progressCallback, messageCallback -> |
| 512 | var installSuccess = 0 | 505 | var installSuccess = 0 |
| 513 | var installOverwrite = 0 | 506 | var installOverwrite = 0 |
| 514 | var errorBaseGame = 0 | 507 | var errorBaseGame = 0 |
| 515 | var errorExtension = 0 | 508 | var error = 0 |
| 516 | var errorOther = 0 | ||
| 517 | documents.forEach { | 509 | documents.forEach { |
| 510 | messageCallback.invoke(FileUtil.getFilename(it)) | ||
| 518 | when ( | 511 | when ( |
| 519 | NativeLibrary.installFileToNand( | 512 | InstallResult.from( |
| 520 | it.toString(), | 513 | NativeLibrary.installFileToNand( |
| 521 | FileUtil.getExtension(it) | 514 | it.toString(), |
| 515 | progressCallback | ||
| 516 | ) | ||
| 522 | ) | 517 | ) |
| 523 | ) { | 518 | ) { |
| 524 | NativeLibrary.InstallFileToNandResult.Success -> { | 519 | InstallResult.Success -> { |
| 525 | installSuccess += 1 | 520 | installSuccess += 1 |
| 526 | } | 521 | } |
| 527 | 522 | ||
| 528 | NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> { | 523 | InstallResult.Overwrite -> { |
| 529 | installOverwrite += 1 | 524 | installOverwrite += 1 |
| 530 | } | 525 | } |
| 531 | 526 | ||
| 532 | NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> { | 527 | InstallResult.BaseInstallAttempted -> { |
| 533 | errorBaseGame += 1 | 528 | errorBaseGame += 1 |
| 534 | } | 529 | } |
| 535 | 530 | ||
| 536 | NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> { | 531 | InstallResult.Failure -> { |
| 537 | errorExtension += 1 | 532 | error += 1 |
| 538 | } | ||
| 539 | |||
| 540 | else -> { | ||
| 541 | errorOther += 1 | ||
| 542 | } | 533 | } |
| 543 | } | 534 | } |
| 544 | } | 535 | } |
| @@ -565,7 +556,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 565 | ) | 556 | ) |
| 566 | installResult.append(separator) | 557 | installResult.append(separator) |
| 567 | } | 558 | } |
| 568 | val errorTotal: Int = errorBaseGame + errorExtension + errorOther | 559 | val errorTotal: Int = errorBaseGame + error |
| 569 | if (errorTotal > 0) { | 560 | if (errorTotal > 0) { |
| 570 | installResult.append(separator) | 561 | installResult.append(separator) |
| 571 | installResult.append( | 562 | installResult.append( |
| @@ -582,14 +573,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 582 | ) | 573 | ) |
| 583 | installResult.append(separator) | 574 | installResult.append(separator) |
| 584 | } | 575 | } |
| 585 | if (errorExtension > 0) { | 576 | if (error > 0) { |
| 586 | installResult.append(separator) | ||
| 587 | installResult.append( | ||
| 588 | getString(R.string.install_game_content_failure_file_extension) | ||
| 589 | ) | ||
| 590 | installResult.append(separator) | ||
| 591 | } | ||
| 592 | if (errorOther > 0) { | ||
| 593 | installResult.append( | 577 | installResult.append( |
| 594 | getString(R.string.install_game_content_failure_description) | 578 | getString(R.string.install_game_content_failure_description) |
| 595 | ) | 579 | ) |
| @@ -608,7 +592,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 608 | descriptionString = installResult.toString().trim() | 592 | descriptionString = installResult.toString().trim() |
| 609 | ) | 593 | ) |
| 610 | } | 594 | } |
| 611 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) | 595 | }.show(supportFragmentManager, ProgressDialogFragment.TAG) |
| 612 | } | 596 | } |
| 613 | 597 | ||
| 614 | val exportUserData = registerForActivityResult( | 598 | val exportUserData = registerForActivityResult( |
| @@ -618,16 +602,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 618 | return@registerForActivityResult | 602 | return@registerForActivityResult |
| 619 | } | 603 | } |
| 620 | 604 | ||
| 621 | IndeterminateProgressDialogFragment.newInstance( | 605 | ProgressDialogFragment.newInstance( |
| 622 | this, | 606 | this, |
| 623 | R.string.exporting_user_data, | 607 | R.string.exporting_user_data, |
| 624 | true | 608 | true |
| 625 | ) { | 609 | ) { progressCallback, _ -> |
| 626 | val zipResult = FileUtil.zipFromInternalStorage( | 610 | val zipResult = FileUtil.zipFromInternalStorage( |
| 627 | File(DirectoryInitialization.userDirectory!!), | 611 | File(DirectoryInitialization.userDirectory!!), |
| 628 | DirectoryInitialization.userDirectory!!, | 612 | DirectoryInitialization.userDirectory!!, |
| 629 | BufferedOutputStream(contentResolver.openOutputStream(result)), | 613 | BufferedOutputStream(contentResolver.openOutputStream(result)), |
| 630 | taskViewModel.cancelled, | 614 | progressCallback, |
| 631 | compression = false | 615 | compression = false |
| 632 | ) | 616 | ) |
| 633 | return@newInstance when (zipResult) { | 617 | return@newInstance when (zipResult) { |
| @@ -635,7 +619,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 635 | TaskState.Failed -> R.string.export_failed | 619 | TaskState.Failed -> R.string.export_failed |
| 636 | TaskState.Cancelled -> R.string.user_data_export_cancelled | 620 | TaskState.Cancelled -> R.string.user_data_export_cancelled |
| 637 | } | 621 | } |
| 638 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) | 622 | }.show(supportFragmentManager, ProgressDialogFragment.TAG) |
| 639 | } | 623 | } |
| 640 | 624 | ||
| 641 | val importUserData = | 625 | val importUserData = |
| @@ -644,10 +628,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 644 | return@registerForActivityResult | 628 | return@registerForActivityResult |
| 645 | } | 629 | } |
| 646 | 630 | ||
| 647 | IndeterminateProgressDialogFragment.newInstance( | 631 | ProgressDialogFragment.newInstance( |
| 648 | this, | 632 | this, |
| 649 | R.string.importing_user_data | 633 | R.string.importing_user_data |
| 650 | ) { | 634 | ) { progressCallback, _ -> |
| 651 | val checkStream = | 635 | val checkStream = |
| 652 | ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result))) | 636 | ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result))) |
| 653 | var isYuzuBackup = false | 637 | var isYuzuBackup = false |
| @@ -676,8 +660,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 676 | // Copy archive to internal storage | 660 | // Copy archive to internal storage |
| 677 | try { | 661 | try { |
| 678 | FileUtil.unzipToInternalStorage( | 662 | FileUtil.unzipToInternalStorage( |
| 679 | BufferedInputStream(contentResolver.openInputStream(result)), | 663 | result.toString(), |
| 680 | File(DirectoryInitialization.userDirectory!!) | 664 | File(DirectoryInitialization.userDirectory!!), |
| 665 | progressCallback | ||
| 681 | ) | 666 | ) |
| 682 | } catch (e: Exception) { | 667 | } catch (e: Exception) { |
| 683 | return@newInstance MessageDialogFragment.newInstance( | 668 | return@newInstance MessageDialogFragment.newInstance( |
| @@ -694,6 +679,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 694 | driverViewModel.reloadDriverData() | 679 | driverViewModel.reloadDriverData() |
| 695 | 680 | ||
| 696 | return@newInstance getString(R.string.user_data_import_success) | 681 | return@newInstance getString(R.string.user_data_import_success) |
| 697 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) | 682 | }.show(supportFragmentManager, ProgressDialogFragment.TAG) |
| 698 | } | 683 | } |
| 699 | } | 684 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt index b54a19c65..fc2339f5a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt | |||
| @@ -7,7 +7,6 @@ import android.database.Cursor | |||
| 7 | import android.net.Uri | 7 | import android.net.Uri |
| 8 | import android.provider.DocumentsContract | 8 | import android.provider.DocumentsContract |
| 9 | import androidx.documentfile.provider.DocumentFile | 9 | import androidx.documentfile.provider.DocumentFile |
| 10 | import kotlinx.coroutines.flow.StateFlow | ||
| 11 | import java.io.BufferedInputStream | 10 | import java.io.BufferedInputStream |
| 12 | import java.io.File | 11 | import java.io.File |
| 13 | import java.io.IOException | 12 | import java.io.IOException |
| @@ -19,6 +18,7 @@ import org.yuzu.yuzu_emu.YuzuApplication | |||
| 19 | import org.yuzu.yuzu_emu.model.MinimalDocumentFile | 18 | import org.yuzu.yuzu_emu.model.MinimalDocumentFile |
| 20 | import org.yuzu.yuzu_emu.model.TaskState | 19 | import org.yuzu.yuzu_emu.model.TaskState |
| 21 | import java.io.BufferedOutputStream | 20 | import java.io.BufferedOutputStream |
| 21 | import java.io.OutputStream | ||
| 22 | import java.lang.NullPointerException | 22 | import java.lang.NullPointerException |
| 23 | import java.nio.charset.StandardCharsets | 23 | import java.nio.charset.StandardCharsets |
| 24 | import java.util.zip.Deflater | 24 | import java.util.zip.Deflater |
| @@ -283,12 +283,34 @@ object FileUtil { | |||
| 283 | 283 | ||
| 284 | /** | 284 | /** |
| 285 | * Extracts the given zip file into the given directory. | 285 | * Extracts the given zip file into the given directory. |
| 286 | * @param path String representation of a [Uri] or a typical path delimited by '/' | ||
| 287 | * @param destDir Location to unzip the contents of [path] into | ||
| 288 | * @param progressCallback Lambda that is called with the total number of files and the current | ||
| 289 | * progress through the process. Stops execution as soon as possible if this returns true. | ||
| 286 | */ | 290 | */ |
| 287 | @Throws(SecurityException::class) | 291 | @Throws(SecurityException::class) |
| 288 | fun unzipToInternalStorage(zipStream: BufferedInputStream, destDir: File) { | 292 | fun unzipToInternalStorage( |
| 289 | ZipInputStream(zipStream).use { zis -> | 293 | path: String, |
| 294 | destDir: File, | ||
| 295 | progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false } | ||
| 296 | ) { | ||
| 297 | var totalEntries = 0L | ||
| 298 | ZipInputStream(getInputStream(path)).use { zis -> | ||
| 299 | var tempEntry = zis.nextEntry | ||
| 300 | while (tempEntry != null) { | ||
| 301 | tempEntry = zis.nextEntry | ||
| 302 | totalEntries++ | ||
| 303 | } | ||
| 304 | } | ||
| 305 | |||
| 306 | var progress = 0L | ||
| 307 | ZipInputStream(getInputStream(path)).use { zis -> | ||
| 290 | var entry: ZipEntry? = zis.nextEntry | 308 | var entry: ZipEntry? = zis.nextEntry |
| 291 | while (entry != null) { | 309 | while (entry != null) { |
| 310 | if (progressCallback.invoke(totalEntries, progress)) { | ||
| 311 | return@use | ||
| 312 | } | ||
| 313 | |||
| 292 | val newFile = File(destDir, entry.name) | 314 | val newFile = File(destDir, entry.name) |
| 293 | val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile | 315 | val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile |
| 294 | 316 | ||
| @@ -304,6 +326,7 @@ object FileUtil { | |||
| 304 | newFile.outputStream().use { fos -> zis.copyTo(fos) } | 326 | newFile.outputStream().use { fos -> zis.copyTo(fos) } |
| 305 | } | 327 | } |
| 306 | entry = zis.nextEntry | 328 | entry = zis.nextEntry |
| 329 | progress++ | ||
| 307 | } | 330 | } |
| 308 | } | 331 | } |
| 309 | } | 332 | } |
| @@ -313,14 +336,15 @@ object FileUtil { | |||
| 313 | * @param inputFile File representation of the item that will be zipped | 336 | * @param inputFile File representation of the item that will be zipped |
| 314 | * @param rootDir Directory containing the inputFile | 337 | * @param rootDir Directory containing the inputFile |
| 315 | * @param outputStream Stream where the zip file will be output | 338 | * @param outputStream Stream where the zip file will be output |
| 316 | * @param cancelled [StateFlow] that reports whether this process has been cancelled | 339 | * @param progressCallback Lambda that is called with the total number of files and the current |
| 340 | * progress through the process. Stops execution as soon as possible if this returns true. | ||
| 317 | * @param compression Disables compression if true | 341 | * @param compression Disables compression if true |
| 318 | */ | 342 | */ |
| 319 | fun zipFromInternalStorage( | 343 | fun zipFromInternalStorage( |
| 320 | inputFile: File, | 344 | inputFile: File, |
| 321 | rootDir: String, | 345 | rootDir: String, |
| 322 | outputStream: BufferedOutputStream, | 346 | outputStream: BufferedOutputStream, |
| 323 | cancelled: StateFlow<Boolean>? = null, | 347 | progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false }, |
| 324 | compression: Boolean = true | 348 | compression: Boolean = true |
| 325 | ): TaskState { | 349 | ): TaskState { |
| 326 | try { | 350 | try { |
| @@ -330,8 +354,10 @@ object FileUtil { | |||
| 330 | zos.setLevel(Deflater.NO_COMPRESSION) | 354 | zos.setLevel(Deflater.NO_COMPRESSION) |
| 331 | } | 355 | } |
| 332 | 356 | ||
| 357 | var count = 0L | ||
| 358 | val totalFiles = inputFile.walkTopDown().count().toLong() | ||
| 333 | inputFile.walkTopDown().forEach { file -> | 359 | inputFile.walkTopDown().forEach { file -> |
| 334 | if (cancelled?.value == true) { | 360 | if (progressCallback.invoke(totalFiles, count)) { |
| 335 | return TaskState.Cancelled | 361 | return TaskState.Cancelled |
| 336 | } | 362 | } |
| 337 | 363 | ||
| @@ -343,6 +369,7 @@ object FileUtil { | |||
| 343 | if (file.isFile) { | 369 | if (file.isFile) { |
| 344 | file.inputStream().use { fis -> fis.copyTo(zos) } | 370 | file.inputStream().use { fis -> fis.copyTo(zos) } |
| 345 | } | 371 | } |
| 372 | count++ | ||
| 346 | } | 373 | } |
| 347 | } | 374 | } |
| 348 | } | 375 | } |
| @@ -356,9 +383,14 @@ object FileUtil { | |||
| 356 | /** | 383 | /** |
| 357 | * Helper function that copies the contents of a DocumentFile folder into a [File] | 384 | * Helper function that copies the contents of a DocumentFile folder into a [File] |
| 358 | * @param file [File] representation of the folder to copy into | 385 | * @param file [File] representation of the folder to copy into |
| 386 | * @param progressCallback Lambda that is called with the total number of files and the current | ||
| 387 | * progress through the process. Stops execution as soon as possible if this returns true. | ||
| 359 | * @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa | 388 | * @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa |
| 360 | */ | 389 | */ |
| 361 | fun DocumentFile.copyFilesTo(file: File) { | 390 | fun DocumentFile.copyFilesTo( |
| 391 | file: File, | ||
| 392 | progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false } | ||
| 393 | ) { | ||
| 362 | file.mkdirs() | 394 | file.mkdirs() |
| 363 | if (!this.isDirectory || !file.isDirectory) { | 395 | if (!this.isDirectory || !file.isDirectory) { |
| 364 | throw IllegalStateException( | 396 | throw IllegalStateException( |
| @@ -366,7 +398,13 @@ object FileUtil { | |||
| 366 | ) | 398 | ) |
| 367 | } | 399 | } |
| 368 | 400 | ||
| 401 | var count = 0L | ||
| 402 | val totalFiles = this.listFiles().size.toLong() | ||
| 369 | this.listFiles().forEach { | 403 | this.listFiles().forEach { |
| 404 | if (progressCallback.invoke(totalFiles, count)) { | ||
| 405 | return | ||
| 406 | } | ||
| 407 | |||
| 370 | val newFile = File(file, it.name!!) | 408 | val newFile = File(file, it.name!!) |
| 371 | if (it.isDirectory) { | 409 | if (it.isDirectory) { |
| 372 | newFile.mkdirs() | 410 | newFile.mkdirs() |
| @@ -381,6 +419,7 @@ object FileUtil { | |||
| 381 | newFile.outputStream().use { os -> bos.copyTo(os) } | 419 | newFile.outputStream().use { os -> bos.copyTo(os) } |
| 382 | } | 420 | } |
| 383 | } | 421 | } |
| 422 | count++ | ||
| 384 | } | 423 | } |
| 385 | } | 424 | } |
| 386 | 425 | ||
| @@ -427,6 +466,18 @@ object FileUtil { | |||
| 427 | } | 466 | } |
| 428 | } | 467 | } |
| 429 | 468 | ||
| 469 | fun getInputStream(path: String) = if (path.contains("content://")) { | ||
| 470 | Uri.parse(path).inputStream() | ||
| 471 | } else { | ||
| 472 | File(path).inputStream() | ||
| 473 | } | ||
| 474 | |||
| 475 | fun getOutputStream(path: String) = if (path.contains("content://")) { | ||
| 476 | Uri.parse(path).outputStream() | ||
| 477 | } else { | ||
| 478 | File(path).outputStream() | ||
| 479 | } | ||
| 480 | |||
| 430 | @Throws(IOException::class) | 481 | @Throws(IOException::class) |
| 431 | fun getStringFromFile(file: File): String = | 482 | fun getStringFromFile(file: File): String = |
| 432 | String(file.readBytes(), StandardCharsets.UTF_8) | 483 | String(file.readBytes(), StandardCharsets.UTF_8) |
| @@ -434,4 +485,19 @@ object FileUtil { | |||
| 434 | @Throws(IOException::class) | 485 | @Throws(IOException::class) |
| 435 | fun getStringFromInputStream(stream: InputStream): String = | 486 | fun getStringFromInputStream(stream: InputStream): String = |
| 436 | String(stream.readBytes(), StandardCharsets.UTF_8) | 487 | String(stream.readBytes(), StandardCharsets.UTF_8) |
| 488 | |||
| 489 | fun DocumentFile.inputStream(): InputStream = | ||
| 490 | YuzuApplication.appContext.contentResolver.openInputStream(uri)!! | ||
| 491 | |||
| 492 | fun DocumentFile.outputStream(): OutputStream = | ||
| 493 | YuzuApplication.appContext.contentResolver.openOutputStream(uri)!! | ||
| 494 | |||
| 495 | fun Uri.inputStream(): InputStream = | ||
| 496 | YuzuApplication.appContext.contentResolver.openInputStream(this)!! | ||
| 497 | |||
| 498 | fun Uri.outputStream(): OutputStream = | ||
| 499 | YuzuApplication.appContext.contentResolver.openOutputStream(this)!! | ||
| 500 | |||
| 501 | fun Uri.asDocumentFile(): DocumentFile? = | ||
| 502 | DocumentFile.fromSingleUri(YuzuApplication.appContext, this) | ||
| 437 | } | 503 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt index a8f9dcc34..81212cbee 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt | |||
| @@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.utils | |||
| 5 | 5 | ||
| 6 | import android.net.Uri | 6 | import android.net.Uri |
| 7 | import android.os.Build | 7 | import android.os.Build |
| 8 | import java.io.BufferedInputStream | ||
| 9 | import java.io.File | 8 | import java.io.File |
| 10 | import java.io.IOException | 9 | import java.io.IOException |
| 11 | import org.yuzu.yuzu_emu.NativeLibrary | 10 | import org.yuzu.yuzu_emu.NativeLibrary |
| @@ -123,7 +122,7 @@ object GpuDriverHelper { | |||
| 123 | // Unzip the driver. | 122 | // Unzip the driver. |
| 124 | try { | 123 | try { |
| 125 | FileUtil.unzipToInternalStorage( | 124 | FileUtil.unzipToInternalStorage( |
| 126 | BufferedInputStream(copiedFile.inputStream()), | 125 | copiedFile.path, |
| 127 | File(driverInstallationPath!!) | 126 | File(driverInstallationPath!!) |
| 128 | ) | 127 | ) |
| 129 | } catch (e: SecurityException) { | 128 | } catch (e: SecurityException) { |
| @@ -156,7 +155,7 @@ object GpuDriverHelper { | |||
| 156 | // Unzip the driver to the private installation directory | 155 | // Unzip the driver to the private installation directory |
| 157 | try { | 156 | try { |
| 158 | FileUtil.unzipToInternalStorage( | 157 | FileUtil.unzipToInternalStorage( |
| 159 | BufferedInputStream(driver.inputStream()), | 158 | driver.path, |
| 160 | File(driverInstallationPath!!) | 159 | File(driverInstallationPath!!) |
| 161 | ) | 160 | ) |
| 162 | } catch (e: SecurityException) { | 161 | } catch (e: SecurityException) { |
diff --git a/src/android/app/src/main/jni/android_common/android_common.cpp b/src/android/app/src/main/jni/android_common/android_common.cpp index 1e884ffdd..7018a52af 100644 --- a/src/android/app/src/main/jni/android_common/android_common.cpp +++ b/src/android/app/src/main/jni/android_common/android_common.cpp | |||
| @@ -42,3 +42,19 @@ double GetJDouble(JNIEnv* env, jobject jdouble) { | |||
| 42 | jobject ToJDouble(JNIEnv* env, double value) { | 42 | jobject ToJDouble(JNIEnv* env, double value) { |
| 43 | return env->NewObject(IDCache::GetDoubleClass(), IDCache::GetDoubleConstructor(), value); | 43 | return env->NewObject(IDCache::GetDoubleClass(), IDCache::GetDoubleConstructor(), value); |
| 44 | } | 44 | } |
| 45 | |||
| 46 | s32 GetJInteger(JNIEnv* env, jobject jinteger) { | ||
| 47 | return env->GetIntField(jinteger, IDCache::GetIntegerValueField()); | ||
| 48 | } | ||
| 49 | |||
| 50 | jobject ToJInteger(JNIEnv* env, s32 value) { | ||
| 51 | return env->NewObject(IDCache::GetIntegerClass(), IDCache::GetIntegerConstructor(), value); | ||
| 52 | } | ||
| 53 | |||
| 54 | bool GetJBoolean(JNIEnv* env, jobject jboolean) { | ||
| 55 | return env->GetBooleanField(jboolean, IDCache::GetBooleanValueField()); | ||
| 56 | } | ||
| 57 | |||
| 58 | jobject ToJBoolean(JNIEnv* env, bool value) { | ||
| 59 | return env->NewObject(IDCache::GetBooleanClass(), IDCache::GetBooleanConstructor(), value); | ||
| 60 | } | ||
diff --git a/src/android/app/src/main/jni/android_common/android_common.h b/src/android/app/src/main/jni/android_common/android_common.h index 8eb803e1b..29a338c0a 100644 --- a/src/android/app/src/main/jni/android_common/android_common.h +++ b/src/android/app/src/main/jni/android_common/android_common.h | |||
| @@ -6,6 +6,7 @@ | |||
| 6 | #include <string> | 6 | #include <string> |
| 7 | 7 | ||
| 8 | #include <jni.h> | 8 | #include <jni.h> |
| 9 | #include "common/common_types.h" | ||
| 9 | 10 | ||
| 10 | std::string GetJString(JNIEnv* env, jstring jstr); | 11 | std::string GetJString(JNIEnv* env, jstring jstr); |
| 11 | jstring ToJString(JNIEnv* env, std::string_view str); | 12 | jstring ToJString(JNIEnv* env, std::string_view str); |
| @@ -13,3 +14,9 @@ jstring ToJString(JNIEnv* env, std::u16string_view str); | |||
| 13 | 14 | ||
| 14 | double GetJDouble(JNIEnv* env, jobject jdouble); | 15 | double GetJDouble(JNIEnv* env, jobject jdouble); |
| 15 | jobject ToJDouble(JNIEnv* env, double value); | 16 | jobject ToJDouble(JNIEnv* env, double value); |
| 17 | |||
| 18 | s32 GetJInteger(JNIEnv* env, jobject jinteger); | ||
| 19 | jobject ToJInteger(JNIEnv* env, s32 value); | ||
| 20 | |||
| 21 | bool GetJBoolean(JNIEnv* env, jobject jboolean); | ||
| 22 | jobject ToJBoolean(JNIEnv* env, bool value); | ||
diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp index c79ad7d76..96f2ad3d4 100644 --- a/src/android/app/src/main/jni/id_cache.cpp +++ b/src/android/app/src/main/jni/id_cache.cpp | |||
| @@ -43,10 +43,27 @@ static jfieldID s_overlay_control_data_landscape_position_field; | |||
| 43 | static jfieldID s_overlay_control_data_portrait_position_field; | 43 | static jfieldID s_overlay_control_data_portrait_position_field; |
| 44 | static jfieldID s_overlay_control_data_foldable_position_field; | 44 | static jfieldID s_overlay_control_data_foldable_position_field; |
| 45 | 45 | ||
| 46 | static jclass s_patch_class; | ||
| 47 | static jmethodID s_patch_constructor; | ||
| 48 | static jfieldID s_patch_enabled_field; | ||
| 49 | static jfieldID s_patch_name_field; | ||
| 50 | static jfieldID s_patch_version_field; | ||
| 51 | static jfieldID s_patch_type_field; | ||
| 52 | static jfieldID s_patch_program_id_field; | ||
| 53 | static jfieldID s_patch_title_id_field; | ||
| 54 | |||
| 46 | static jclass s_double_class; | 55 | static jclass s_double_class; |
| 47 | static jmethodID s_double_constructor; | 56 | static jmethodID s_double_constructor; |
| 48 | static jfieldID s_double_value_field; | 57 | static jfieldID s_double_value_field; |
| 49 | 58 | ||
| 59 | static jclass s_integer_class; | ||
| 60 | static jmethodID s_integer_constructor; | ||
| 61 | static jfieldID s_integer_value_field; | ||
| 62 | |||
| 63 | static jclass s_boolean_class; | ||
| 64 | static jmethodID s_boolean_constructor; | ||
| 65 | static jfieldID s_boolean_value_field; | ||
| 66 | |||
| 50 | static constexpr jint JNI_VERSION = JNI_VERSION_1_6; | 67 | static constexpr jint JNI_VERSION = JNI_VERSION_1_6; |
| 51 | 68 | ||
| 52 | namespace IDCache { | 69 | namespace IDCache { |
| @@ -186,6 +203,38 @@ jfieldID GetOverlayControlDataFoldablePositionField() { | |||
| 186 | return s_overlay_control_data_foldable_position_field; | 203 | return s_overlay_control_data_foldable_position_field; |
| 187 | } | 204 | } |
| 188 | 205 | ||
| 206 | jclass GetPatchClass() { | ||
| 207 | return s_patch_class; | ||
| 208 | } | ||
| 209 | |||
| 210 | jmethodID GetPatchConstructor() { | ||
| 211 | return s_patch_constructor; | ||
| 212 | } | ||
| 213 | |||
| 214 | jfieldID GetPatchEnabledField() { | ||
| 215 | return s_patch_enabled_field; | ||
| 216 | } | ||
| 217 | |||
| 218 | jfieldID GetPatchNameField() { | ||
| 219 | return s_patch_name_field; | ||
| 220 | } | ||
| 221 | |||
| 222 | jfieldID GetPatchVersionField() { | ||
| 223 | return s_patch_version_field; | ||
| 224 | } | ||
| 225 | |||
| 226 | jfieldID GetPatchTypeField() { | ||
| 227 | return s_patch_type_field; | ||
| 228 | } | ||
| 229 | |||
| 230 | jfieldID GetPatchProgramIdField() { | ||
| 231 | return s_patch_program_id_field; | ||
| 232 | } | ||
| 233 | |||
| 234 | jfieldID GetPatchTitleIdField() { | ||
| 235 | return s_patch_title_id_field; | ||
| 236 | } | ||
| 237 | |||
| 189 | jclass GetDoubleClass() { | 238 | jclass GetDoubleClass() { |
| 190 | return s_double_class; | 239 | return s_double_class; |
| 191 | } | 240 | } |
| @@ -198,6 +247,30 @@ jfieldID GetDoubleValueField() { | |||
| 198 | return s_double_value_field; | 247 | return s_double_value_field; |
| 199 | } | 248 | } |
| 200 | 249 | ||
| 250 | jclass GetIntegerClass() { | ||
| 251 | return s_integer_class; | ||
| 252 | } | ||
| 253 | |||
| 254 | jmethodID GetIntegerConstructor() { | ||
| 255 | return s_integer_constructor; | ||
| 256 | } | ||
| 257 | |||
| 258 | jfieldID GetIntegerValueField() { | ||
| 259 | return s_integer_value_field; | ||
| 260 | } | ||
| 261 | |||
| 262 | jclass GetBooleanClass() { | ||
| 263 | return s_boolean_class; | ||
| 264 | } | ||
| 265 | |||
| 266 | jmethodID GetBooleanConstructor() { | ||
| 267 | return s_boolean_constructor; | ||
| 268 | } | ||
| 269 | |||
| 270 | jfieldID GetBooleanValueField() { | ||
| 271 | return s_boolean_value_field; | ||
| 272 | } | ||
| 273 | |||
| 201 | } // namespace IDCache | 274 | } // namespace IDCache |
| 202 | 275 | ||
| 203 | #ifdef __cplusplus | 276 | #ifdef __cplusplus |
| @@ -278,12 +351,37 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { | |||
| 278 | env->GetFieldID(overlay_control_data_class, "foldablePosition", "Lkotlin/Pair;"); | 351 | env->GetFieldID(overlay_control_data_class, "foldablePosition", "Lkotlin/Pair;"); |
| 279 | env->DeleteLocalRef(overlay_control_data_class); | 352 | env->DeleteLocalRef(overlay_control_data_class); |
| 280 | 353 | ||
| 354 | const jclass patch_class = env->FindClass("org/yuzu/yuzu_emu/model/Patch"); | ||
| 355 | s_patch_class = reinterpret_cast<jclass>(env->NewGlobalRef(patch_class)); | ||
| 356 | s_patch_constructor = env->GetMethodID( | ||
| 357 | patch_class, "<init>", | ||
| 358 | "(ZLjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)V"); | ||
| 359 | s_patch_enabled_field = env->GetFieldID(patch_class, "enabled", "Z"); | ||
| 360 | s_patch_name_field = env->GetFieldID(patch_class, "name", "Ljava/lang/String;"); | ||
| 361 | s_patch_version_field = env->GetFieldID(patch_class, "version", "Ljava/lang/String;"); | ||
| 362 | s_patch_type_field = env->GetFieldID(patch_class, "type", "I"); | ||
| 363 | s_patch_program_id_field = env->GetFieldID(patch_class, "programId", "Ljava/lang/String;"); | ||
| 364 | s_patch_title_id_field = env->GetFieldID(patch_class, "titleId", "Ljava/lang/String;"); | ||
| 365 | env->DeleteLocalRef(patch_class); | ||
| 366 | |||
| 281 | const jclass double_class = env->FindClass("java/lang/Double"); | 367 | const jclass double_class = env->FindClass("java/lang/Double"); |
| 282 | s_double_class = reinterpret_cast<jclass>(env->NewGlobalRef(double_class)); | 368 | s_double_class = reinterpret_cast<jclass>(env->NewGlobalRef(double_class)); |
| 283 | s_double_constructor = env->GetMethodID(double_class, "<init>", "(D)V"); | 369 | s_double_constructor = env->GetMethodID(double_class, "<init>", "(D)V"); |
| 284 | s_double_value_field = env->GetFieldID(double_class, "value", "D"); | 370 | s_double_value_field = env->GetFieldID(double_class, "value", "D"); |
| 285 | env->DeleteLocalRef(double_class); | 371 | env->DeleteLocalRef(double_class); |
| 286 | 372 | ||
| 373 | const jclass int_class = env->FindClass("java/lang/Integer"); | ||
| 374 | s_integer_class = reinterpret_cast<jclass>(env->NewGlobalRef(int_class)); | ||
| 375 | s_integer_constructor = env->GetMethodID(int_class, "<init>", "(I)V"); | ||
| 376 | s_integer_value_field = env->GetFieldID(int_class, "value", "I"); | ||
| 377 | env->DeleteLocalRef(int_class); | ||
| 378 | |||
| 379 | const jclass boolean_class = env->FindClass("java/lang/Boolean"); | ||
| 380 | s_boolean_class = reinterpret_cast<jclass>(env->NewGlobalRef(boolean_class)); | ||
| 381 | s_boolean_constructor = env->GetMethodID(boolean_class, "<init>", "(Z)V"); | ||
| 382 | s_boolean_value_field = env->GetFieldID(boolean_class, "value", "Z"); | ||
| 383 | env->DeleteLocalRef(boolean_class); | ||
| 384 | |||
| 287 | // Initialize Android Storage | 385 | // Initialize Android Storage |
| 288 | Common::FS::Android::RegisterCallbacks(env, s_native_library_class); | 386 | Common::FS::Android::RegisterCallbacks(env, s_native_library_class); |
| 289 | 387 | ||
| @@ -309,7 +407,10 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) { | |||
| 309 | env->DeleteGlobalRef(s_string_class); | 407 | env->DeleteGlobalRef(s_string_class); |
| 310 | env->DeleteGlobalRef(s_pair_class); | 408 | env->DeleteGlobalRef(s_pair_class); |
| 311 | env->DeleteGlobalRef(s_overlay_control_data_class); | 409 | env->DeleteGlobalRef(s_overlay_control_data_class); |
| 410 | env->DeleteGlobalRef(s_patch_class); | ||
| 312 | env->DeleteGlobalRef(s_double_class); | 411 | env->DeleteGlobalRef(s_double_class); |
| 412 | env->DeleteGlobalRef(s_integer_class); | ||
| 413 | env->DeleteGlobalRef(s_boolean_class); | ||
| 313 | 414 | ||
| 314 | // UnInitialize applets | 415 | // UnInitialize applets |
| 315 | SoftwareKeyboard::CleanupJNI(env); | 416 | SoftwareKeyboard::CleanupJNI(env); |
diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h index 784d1412f..a002e705d 100644 --- a/src/android/app/src/main/jni/id_cache.h +++ b/src/android/app/src/main/jni/id_cache.h | |||
| @@ -43,8 +43,25 @@ jfieldID GetOverlayControlDataLandscapePositionField(); | |||
| 43 | jfieldID GetOverlayControlDataPortraitPositionField(); | 43 | jfieldID GetOverlayControlDataPortraitPositionField(); |
| 44 | jfieldID GetOverlayControlDataFoldablePositionField(); | 44 | jfieldID GetOverlayControlDataFoldablePositionField(); |
| 45 | 45 | ||
| 46 | jclass GetPatchClass(); | ||
| 47 | jmethodID GetPatchConstructor(); | ||
| 48 | jfieldID GetPatchEnabledField(); | ||
| 49 | jfieldID GetPatchNameField(); | ||
| 50 | jfieldID GetPatchVersionField(); | ||
| 51 | jfieldID GetPatchTypeField(); | ||
| 52 | jfieldID GetPatchProgramIdField(); | ||
| 53 | jfieldID GetPatchTitleIdField(); | ||
| 54 | |||
| 46 | jclass GetDoubleClass(); | 55 | jclass GetDoubleClass(); |
| 47 | jmethodID GetDoubleConstructor(); | 56 | jmethodID GetDoubleConstructor(); |
| 48 | jfieldID GetDoubleValueField(); | 57 | jfieldID GetDoubleValueField(); |
| 49 | 58 | ||
| 59 | jclass GetIntegerClass(); | ||
| 60 | jmethodID GetIntegerConstructor(); | ||
| 61 | jfieldID GetIntegerValueField(); | ||
| 62 | |||
| 63 | jclass GetBooleanClass(); | ||
| 64 | jmethodID GetBooleanConstructor(); | ||
| 65 | jfieldID GetBooleanValueField(); | ||
| 66 | |||
| 50 | } // namespace IDCache | 67 | } // namespace IDCache |
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index ed3b1353a..be0a723b1 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp | |||
| @@ -17,6 +17,7 @@ | |||
| 17 | #include <core/file_sys/patch_manager.h> | 17 | #include <core/file_sys/patch_manager.h> |
| 18 | #include <core/file_sys/savedata_factory.h> | 18 | #include <core/file_sys/savedata_factory.h> |
| 19 | #include <core/loader/nro.h> | 19 | #include <core/loader/nro.h> |
| 20 | #include <frontend_common/content_manager.h> | ||
| 20 | #include <jni.h> | 21 | #include <jni.h> |
| 21 | 22 | ||
| 22 | #include "common/detached_tasks.h" | 23 | #include "common/detached_tasks.h" |
| @@ -100,67 +101,6 @@ void EmulationSession::SetNativeWindow(ANativeWindow* native_window) { | |||
| 100 | m_native_window = native_window; | 101 | m_native_window = native_window; |
| 101 | } | 102 | } |
| 102 | 103 | ||
| 103 | int EmulationSession::InstallFileToNand(std::string filename, std::string file_extension) { | ||
| 104 | jconst copy_func = [](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest, | ||
| 105 | std::size_t block_size) { | ||
| 106 | if (src == nullptr || dest == nullptr) { | ||
| 107 | return false; | ||
| 108 | } | ||
| 109 | if (!dest->Resize(src->GetSize())) { | ||
| 110 | return false; | ||
| 111 | } | ||
| 112 | |||
| 113 | using namespace Common::Literals; | ||
| 114 | [[maybe_unused]] std::vector<u8> buffer(1_MiB); | ||
| 115 | |||
| 116 | for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) { | ||
| 117 | jconst read = src->Read(buffer.data(), buffer.size(), i); | ||
| 118 | dest->Write(buffer.data(), read, i); | ||
| 119 | } | ||
| 120 | return true; | ||
| 121 | }; | ||
| 122 | |||
| 123 | enum InstallResult { | ||
| 124 | Success = 0, | ||
| 125 | SuccessFileOverwritten = 1, | ||
| 126 | InstallError = 2, | ||
| 127 | ErrorBaseGame = 3, | ||
| 128 | ErrorFilenameExtension = 4, | ||
| 129 | }; | ||
| 130 | |||
| 131 | [[maybe_unused]] std::shared_ptr<FileSys::NSP> nsp; | ||
| 132 | if (file_extension == "nsp") { | ||
| 133 | nsp = std::make_shared<FileSys::NSP>(m_vfs->OpenFile(filename, FileSys::Mode::Read)); | ||
| 134 | if (nsp->IsExtractedType()) { | ||
| 135 | return InstallError; | ||
| 136 | } | ||
| 137 | } else { | ||
| 138 | return ErrorFilenameExtension; | ||
| 139 | } | ||
| 140 | |||
| 141 | if (!nsp) { | ||
| 142 | return InstallError; | ||
| 143 | } | ||
| 144 | |||
| 145 | if (nsp->GetStatus() != Loader::ResultStatus::Success) { | ||
| 146 | return InstallError; | ||
| 147 | } | ||
| 148 | |||
| 149 | jconst res = m_system.GetFileSystemController().GetUserNANDContents()->InstallEntry(*nsp, true, | ||
| 150 | copy_func); | ||
| 151 | |||
| 152 | switch (res) { | ||
| 153 | case FileSys::InstallResult::Success: | ||
| 154 | return Success; | ||
| 155 | case FileSys::InstallResult::OverwriteExisting: | ||
| 156 | return SuccessFileOverwritten; | ||
| 157 | case FileSys::InstallResult::ErrorBaseInstall: | ||
| 158 | return ErrorBaseGame; | ||
| 159 | default: | ||
| 160 | return InstallError; | ||
| 161 | } | ||
| 162 | } | ||
| 163 | |||
| 164 | void EmulationSession::InitializeGpuDriver(const std::string& hook_lib_dir, | 104 | void EmulationSession::InitializeGpuDriver(const std::string& hook_lib_dir, |
| 165 | const std::string& custom_driver_dir, | 105 | const std::string& custom_driver_dir, |
| 166 | const std::string& custom_driver_name, | 106 | const std::string& custom_driver_name, |
| @@ -512,10 +452,20 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, jobject | |||
| 512 | } | 452 | } |
| 513 | 453 | ||
| 514 | int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject instance, | 454 | int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject instance, |
| 515 | jstring j_file, | 455 | jstring j_file, jobject jcallback) { |
| 516 | jstring j_file_extension) { | 456 | auto jlambdaClass = env->GetObjectClass(jcallback); |
| 517 | return EmulationSession::GetInstance().InstallFileToNand(GetJString(env, j_file), | 457 | auto jlambdaInvokeMethod = env->GetMethodID( |
| 518 | GetJString(env, j_file_extension)); | 458 | jlambdaClass, "invoke", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); |
| 459 | const auto callback = [env, jcallback, jlambdaInvokeMethod](size_t max, size_t progress) { | ||
| 460 | auto jwasCancelled = env->CallObjectMethod(jcallback, jlambdaInvokeMethod, | ||
| 461 | ToJDouble(env, max), ToJDouble(env, progress)); | ||
| 462 | return GetJBoolean(env, jwasCancelled); | ||
| 463 | }; | ||
| 464 | |||
| 465 | return static_cast<int>( | ||
| 466 | ContentManager::InstallNSP(&EmulationSession::GetInstance().System(), | ||
| 467 | EmulationSession::GetInstance().System().GetFilesystem().get(), | ||
| 468 | GetJString(env, j_file), callback)); | ||
| 519 | } | 469 | } |
| 520 | 470 | ||
| 521 | jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* env, jobject jobj, | 471 | jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* env, jobject jobj, |
| @@ -824,9 +774,9 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env, | |||
| 824 | return true; | 774 | return true; |
| 825 | } | 775 | } |
| 826 | 776 | ||
| 827 | jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, jobject jobj, | 777 | jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env, jobject jobj, |
| 828 | jstring jpath, | 778 | jstring jpath, |
| 829 | jstring jprogramId) { | 779 | jstring jprogramId) { |
| 830 | const auto path = GetJString(env, jpath); | 780 | const auto path = GetJString(env, jpath); |
| 831 | const auto vFile = | 781 | const auto vFile = |
| 832 | Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path); | 782 | Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path); |
| @@ -843,20 +793,40 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, | |||
| 843 | FileSys::VirtualFile update_raw; | 793 | FileSys::VirtualFile update_raw; |
| 844 | loader->ReadUpdateRaw(update_raw); | 794 | loader->ReadUpdateRaw(update_raw); |
| 845 | 795 | ||
| 846 | auto addons = pm.GetPatchVersionNames(update_raw); | 796 | auto patches = pm.GetPatches(update_raw); |
| 847 | auto jemptyString = ToJString(env, ""); | 797 | jobjectArray jpatchArray = |
| 848 | auto jemptyStringPair = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(), | 798 | env->NewObjectArray(patches.size(), IDCache::GetPatchClass(), nullptr); |
| 849 | jemptyString, jemptyString); | ||
| 850 | jobjectArray jaddonsArray = | ||
| 851 | env->NewObjectArray(addons.size(), IDCache::GetPairClass(), jemptyStringPair); | ||
| 852 | int i = 0; | 799 | int i = 0; |
| 853 | for (const auto& addon : addons) { | 800 | for (const auto& patch : patches) { |
| 854 | jobject jaddon = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(), | 801 | jobject jpatch = env->NewObject( |
| 855 | ToJString(env, addon.first), ToJString(env, addon.second)); | 802 | IDCache::GetPatchClass(), IDCache::GetPatchConstructor(), patch.enabled, |
| 856 | env->SetObjectArrayElement(jaddonsArray, i, jaddon); | 803 | ToJString(env, patch.name), ToJString(env, patch.version), |
| 804 | static_cast<jint>(patch.type), ToJString(env, std::to_string(patch.program_id)), | ||
| 805 | ToJString(env, std::to_string(patch.title_id))); | ||
| 806 | env->SetObjectArrayElement(jpatchArray, i, jpatch); | ||
| 857 | ++i; | 807 | ++i; |
| 858 | } | 808 | } |
| 859 | return jaddonsArray; | 809 | return jpatchArray; |
| 810 | } | ||
| 811 | |||
| 812 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeUpdate(JNIEnv* env, jobject jobj, | ||
| 813 | jstring jprogramId) { | ||
| 814 | auto program_id = EmulationSession::GetProgramId(env, jprogramId); | ||
| 815 | ContentManager::RemoveUpdate(EmulationSession::GetInstance().System().GetFileSystemController(), | ||
| 816 | program_id); | ||
| 817 | } | ||
| 818 | |||
| 819 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeDLC(JNIEnv* env, jobject jobj, | ||
| 820 | jstring jprogramId) { | ||
| 821 | auto program_id = EmulationSession::GetProgramId(env, jprogramId); | ||
| 822 | ContentManager::RemoveAllDLC(&EmulationSession::GetInstance().System(), program_id); | ||
| 823 | } | ||
| 824 | |||
| 825 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeMod(JNIEnv* env, jobject jobj, jstring jprogramId, | ||
| 826 | jstring jname) { | ||
| 827 | auto program_id = EmulationSession::GetProgramId(env, jprogramId); | ||
| 828 | ContentManager::RemoveMod(EmulationSession::GetInstance().System().GetFileSystemController(), | ||
| 829 | program_id, GetJString(env, jname)); | ||
| 860 | } | 830 | } |
| 861 | 831 | ||
| 862 | jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj, | 832 | jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj, |
diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h index 4a8049578..dadb138ad 100644 --- a/src/android/app/src/main/jni/native.h +++ b/src/android/app/src/main/jni/native.h | |||
| @@ -7,6 +7,7 @@ | |||
| 7 | #include "core/file_sys/registered_cache.h" | 7 | #include "core/file_sys/registered_cache.h" |
| 8 | #include "core/hle/service/acc/profile_manager.h" | 8 | #include "core/hle/service/acc/profile_manager.h" |
| 9 | #include "core/perf_stats.h" | 9 | #include "core/perf_stats.h" |
| 10 | #include "frontend_common/content_manager.h" | ||
| 10 | #include "jni/applets/software_keyboard.h" | 11 | #include "jni/applets/software_keyboard.h" |
| 11 | #include "jni/emu_window/emu_window.h" | 12 | #include "jni/emu_window/emu_window.h" |
| 12 | #include "video_core/rasterizer_interface.h" | 13 | #include "video_core/rasterizer_interface.h" |
| @@ -29,7 +30,6 @@ public: | |||
| 29 | void SetNativeWindow(ANativeWindow* native_window); | 30 | void SetNativeWindow(ANativeWindow* native_window); |
| 30 | void SurfaceChanged(); | 31 | void SurfaceChanged(); |
| 31 | 32 | ||
| 32 | int InstallFileToNand(std::string filename, std::string file_extension); | ||
| 33 | void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir, | 33 | void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir, |
| 34 | const std::string& custom_driver_name, | 34 | const std::string& custom_driver_name, |
| 35 | const std::string& file_redirect_dir); | 35 | const std::string& file_redirect_dir); |
diff --git a/src/android/app/src/main/res/layout/dialog_progress_bar.xml b/src/android/app/src/main/res/layout/dialog_progress_bar.xml index 0209ea082..e61aa5294 100644 --- a/src/android/app/src/main/res/layout/dialog_progress_bar.xml +++ b/src/android/app/src/main/res/layout/dialog_progress_bar.xml | |||
| @@ -1,8 +1,30 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | 1 | <?xml version="1.0" encoding="utf-8"?> |
| 2 | <com.google.android.material.progressindicator.LinearProgressIndicator xmlns:android="http://schemas.android.com/apk/res/android" | 2 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
| 3 | xmlns:app="http://schemas.android.com/apk/res-auto" | 3 | xmlns:app="http://schemas.android.com/apk/res-auto" |
| 4 | android:id="@+id/progress_bar" | ||
| 5 | android:layout_width="match_parent" | 4 | android:layout_width="match_parent" |
| 6 | android:layout_height="wrap_content" | 5 | android:layout_height="wrap_content" |
| 7 | android:padding="24dp" | 6 | android:orientation="vertical"> |
| 8 | app:trackCornerRadius="4dp" /> | 7 | |
| 8 | <com.google.android.material.textview.MaterialTextView | ||
| 9 | android:id="@+id/message" | ||
| 10 | style="@style/TextAppearance.Material3.BodyMedium" | ||
| 11 | android:layout_width="match_parent" | ||
| 12 | android:layout_height="wrap_content" | ||
| 13 | android:layout_marginHorizontal="24dp" | ||
| 14 | android:layout_marginTop="12dp" | ||
| 15 | android:layout_marginBottom="6dp" | ||
| 16 | android:ellipsize="marquee" | ||
| 17 | android:marqueeRepeatLimit="marquee_forever" | ||
| 18 | android:requiresFadingEdge="horizontal" | ||
| 19 | android:singleLine="true" | ||
| 20 | android:textAlignment="viewStart" | ||
| 21 | android:visibility="gone" /> | ||
| 22 | |||
| 23 | <com.google.android.material.progressindicator.LinearProgressIndicator | ||
| 24 | android:id="@+id/progress_bar" | ||
| 25 | android:layout_width="match_parent" | ||
| 26 | android:layout_height="wrap_content" | ||
| 27 | android:padding="24dp" | ||
| 28 | app:trackCornerRadius="4dp" /> | ||
| 29 | |||
| 30 | </LinearLayout> | ||
diff --git a/src/android/app/src/main/res/layout/list_item_addon.xml b/src/android/app/src/main/res/layout/list_item_addon.xml index 74ca04ef1..3a1382fe2 100644 --- a/src/android/app/src/main/res/layout/list_item_addon.xml +++ b/src/android/app/src/main/res/layout/list_item_addon.xml | |||
| @@ -14,12 +14,11 @@ | |||
| 14 | android:id="@+id/text_container" | 14 | android:id="@+id/text_container" |
| 15 | android:layout_width="0dp" | 15 | android:layout_width="0dp" |
| 16 | android:layout_height="wrap_content" | 16 | android:layout_height="wrap_content" |
| 17 | android:layout_marginEnd="16dp" | ||
| 18 | android:orientation="vertical" | 17 | android:orientation="vertical" |
| 19 | app:layout_constraintBottom_toBottomOf="@+id/addon_switch" | 18 | android:layout_marginEnd="16dp" |
| 20 | app:layout_constraintEnd_toStartOf="@+id/addon_switch" | 19 | app:layout_constraintEnd_toStartOf="@+id/addon_checkbox" |
| 21 | app:layout_constraintStart_toStartOf="parent" | 20 | app:layout_constraintStart_toStartOf="parent" |
| 22 | app:layout_constraintTop_toTopOf="@+id/addon_switch"> | 21 | app:layout_constraintTop_toTopOf="parent"> |
| 23 | 22 | ||
| 24 | <com.google.android.material.textview.MaterialTextView | 23 | <com.google.android.material.textview.MaterialTextView |
| 25 | android:id="@+id/title" | 24 | android:id="@+id/title" |
| @@ -42,16 +41,29 @@ | |||
| 42 | 41 | ||
| 43 | </LinearLayout> | 42 | </LinearLayout> |
| 44 | 43 | ||
| 45 | <com.google.android.material.materialswitch.MaterialSwitch | 44 | <com.google.android.material.checkbox.MaterialCheckBox |
| 46 | android:id="@+id/addon_switch" | 45 | android:id="@+id/addon_checkbox" |
| 47 | android:layout_width="wrap_content" | 46 | android:layout_width="wrap_content" |
| 48 | android:layout_height="wrap_content" | 47 | android:layout_height="wrap_content" |
| 49 | android:focusable="true" | 48 | android:focusable="true" |
| 50 | android:gravity="center" | 49 | android:gravity="center" |
| 51 | android:nextFocusLeft="@id/addon_container" | 50 | android:layout_marginEnd="8dp" |
| 52 | app:layout_constraintBottom_toBottomOf="parent" | 51 | app:layout_constraintTop_toTopOf="@+id/text_container" |
| 52 | app:layout_constraintBottom_toBottomOf="@+id/text_container" | ||
| 53 | app:layout_constraintEnd_toStartOf="@+id/button_delete" /> | ||
| 54 | |||
| 55 | <Button | ||
| 56 | android:id="@+id/button_delete" | ||
| 57 | style="@style/Widget.Material3.Button.IconButton" | ||
| 58 | android:layout_width="wrap_content" | ||
| 59 | android:layout_height="wrap_content" | ||
| 60 | android:layout_gravity="center_vertical" | ||
| 61 | android:contentDescription="@string/delete" | ||
| 62 | android:tooltipText="@string/delete" | ||
| 63 | app:icon="@drawable/ic_delete" | ||
| 64 | app:iconTint="?attr/colorControlNormal" | ||
| 53 | app:layout_constraintEnd_toEndOf="parent" | 65 | app:layout_constraintEnd_toEndOf="parent" |
| 54 | app:layout_constraintStart_toEndOf="@id/text_container" | 66 | app:layout_constraintTop_toTopOf="@+id/addon_checkbox" |
| 55 | app:layout_constraintTop_toTopOf="parent" /> | 67 | app:layout_constraintBottom_toBottomOf="@+id/addon_checkbox" /> |
| 56 | 68 | ||
| 57 | </androidx.constraintlayout.widget.ConstraintLayout> | 69 | </androidx.constraintlayout.widget.ConstraintLayout> |
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 547752bda..db5b27d38 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml | |||
| @@ -286,6 +286,7 @@ | |||
| 286 | <string name="custom">Custom</string> | 286 | <string name="custom">Custom</string> |
| 287 | <string name="notice">Notice</string> | 287 | <string name="notice">Notice</string> |
| 288 | <string name="import_complete">Import complete</string> | 288 | <string name="import_complete">Import complete</string> |
| 289 | <string name="more_options">More options</string> | ||
| 289 | 290 | ||
| 290 | <!-- GPU driver installation --> | 291 | <!-- GPU driver installation --> |
| 291 | <string name="select_gpu_driver">Select GPU driver</string> | 292 | <string name="select_gpu_driver">Select GPU driver</string> |
| @@ -348,6 +349,8 @@ | |||
| 348 | <string name="verifying_content">Verifying content…</string> | 349 | <string name="verifying_content">Verifying content…</string> |
| 349 | <string name="content_install_notice">Content install notice</string> | 350 | <string name="content_install_notice">Content install notice</string> |
| 350 | <string name="content_install_notice_description">The content that you selected does not match this game.\nInstall anyway?</string> | 351 | <string name="content_install_notice_description">The content that you selected does not match this game.\nInstall anyway?</string> |
| 352 | <string name="confirm_uninstall">Confirm uninstall</string> | ||
| 353 | <string name="confirm_uninstall_description">Are you sure you want to uninstall this addon?</string> | ||
| 351 | 354 | ||
| 352 | <!-- ROM loading errors --> | 355 | <!-- ROM loading errors --> |
| 353 | <string name="loader_error_encrypted">Your ROM is encrypted</string> | 356 | <string name="loader_error_encrypted">Your ROM is encrypted</string> |
diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp index 4a3dbc6a3..612122224 100644 --- a/src/core/file_sys/patch_manager.cpp +++ b/src/core/file_sys/patch_manager.cpp | |||
| @@ -466,12 +466,12 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs | |||
| 466 | return romfs; | 466 | return romfs; |
| 467 | } | 467 | } |
| 468 | 468 | ||
| 469 | PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile update_raw) const { | 469 | std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const { |
| 470 | if (title_id == 0) { | 470 | if (title_id == 0) { |
| 471 | return {}; | 471 | return {}; |
| 472 | } | 472 | } |
| 473 | 473 | ||
| 474 | std::map<std::string, std::string, std::less<>> out; | 474 | std::vector<Patch> out; |
| 475 | const auto& disabled = Settings::values.disabled_addons[title_id]; | 475 | const auto& disabled = Settings::values.disabled_addons[title_id]; |
| 476 | 476 | ||
| 477 | // Game Updates | 477 | // Game Updates |
| @@ -482,20 +482,28 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u | |||
| 482 | 482 | ||
| 483 | const auto update_disabled = | 483 | const auto update_disabled = |
| 484 | std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); | 484 | std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); |
| 485 | const auto update_label = update_disabled ? "[D] Update" : "Update"; | 485 | Patch update_patch = {.enabled = !update_disabled, |
| 486 | .name = "Update", | ||
| 487 | .version = "", | ||
| 488 | .type = PatchType::Update, | ||
| 489 | .program_id = title_id, | ||
| 490 | .title_id = title_id}; | ||
| 486 | 491 | ||
| 487 | if (nacp != nullptr) { | 492 | if (nacp != nullptr) { |
| 488 | out.insert_or_assign(update_label, nacp->GetVersionString()); | 493 | update_patch.version = nacp->GetVersionString(); |
| 494 | out.push_back(update_patch); | ||
| 489 | } else { | 495 | } else { |
| 490 | if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) { | 496 | if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) { |
| 491 | const auto meta_ver = content_provider.GetEntryVersion(update_tid); | 497 | const auto meta_ver = content_provider.GetEntryVersion(update_tid); |
| 492 | if (meta_ver.value_or(0) == 0) { | 498 | if (meta_ver.value_or(0) == 0) { |
| 493 | out.insert_or_assign(update_label, ""); | 499 | out.push_back(update_patch); |
| 494 | } else { | 500 | } else { |
| 495 | out.insert_or_assign(update_label, FormatTitleVersion(*meta_ver)); | 501 | update_patch.version = FormatTitleVersion(*meta_ver); |
| 502 | out.push_back(update_patch); | ||
| 496 | } | 503 | } |
| 497 | } else if (update_raw != nullptr) { | 504 | } else if (update_raw != nullptr) { |
| 498 | out.insert_or_assign(update_label, "PACKED"); | 505 | update_patch.version = "PACKED"; |
| 506 | out.push_back(update_patch); | ||
| 499 | } | 507 | } |
| 500 | } | 508 | } |
| 501 | 509 | ||
| @@ -539,7 +547,12 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u | |||
| 539 | 547 | ||
| 540 | const auto mod_disabled = | 548 | const auto mod_disabled = |
| 541 | std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end(); | 549 | std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end(); |
| 542 | out.insert_or_assign(mod_disabled ? "[D] " + mod->GetName() : mod->GetName(), types); | 550 | out.push_back({.enabled = !mod_disabled, |
| 551 | .name = mod->GetName(), | ||
| 552 | .version = types, | ||
| 553 | .type = PatchType::Mod, | ||
| 554 | .program_id = title_id, | ||
| 555 | .title_id = title_id}); | ||
| 543 | } | 556 | } |
| 544 | } | 557 | } |
| 545 | 558 | ||
| @@ -557,7 +570,12 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u | |||
| 557 | if (!types.empty()) { | 570 | if (!types.empty()) { |
| 558 | const auto mod_disabled = | 571 | const auto mod_disabled = |
| 559 | std::find(disabled.begin(), disabled.end(), "SDMC") != disabled.end(); | 572 | std::find(disabled.begin(), disabled.end(), "SDMC") != disabled.end(); |
| 560 | out.insert_or_assign(mod_disabled ? "[D] SDMC" : "SDMC", types); | 573 | out.push_back({.enabled = !mod_disabled, |
| 574 | .name = "SDMC", | ||
| 575 | .version = types, | ||
| 576 | .type = PatchType::Mod, | ||
| 577 | .program_id = title_id, | ||
| 578 | .title_id = title_id}); | ||
| 561 | } | 579 | } |
| 562 | } | 580 | } |
| 563 | 581 | ||
| @@ -584,7 +602,12 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u | |||
| 584 | 602 | ||
| 585 | const auto dlc_disabled = | 603 | const auto dlc_disabled = |
| 586 | std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end(); | 604 | std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end(); |
| 587 | out.insert_or_assign(dlc_disabled ? "[D] DLC" : "DLC", std::move(list)); | 605 | out.push_back({.enabled = !dlc_disabled, |
| 606 | .name = "DLC", | ||
| 607 | .version = std::move(list), | ||
| 608 | .type = PatchType::DLC, | ||
| 609 | .program_id = title_id, | ||
| 610 | .title_id = dlc_match.back().title_id}); | ||
| 588 | } | 611 | } |
| 589 | 612 | ||
| 590 | return out; | 613 | return out; |
diff --git a/src/core/file_sys/patch_manager.h b/src/core/file_sys/patch_manager.h index 03e9c7301..2601b8217 100644 --- a/src/core/file_sys/patch_manager.h +++ b/src/core/file_sys/patch_manager.h | |||
| @@ -26,12 +26,22 @@ class ContentProvider; | |||
| 26 | class NCA; | 26 | class NCA; |
| 27 | class NACP; | 27 | class NACP; |
| 28 | 28 | ||
| 29 | enum class PatchType { Update, DLC, Mod }; | ||
| 30 | |||
| 31 | struct Patch { | ||
| 32 | bool enabled; | ||
| 33 | std::string name; | ||
| 34 | std::string version; | ||
| 35 | PatchType type; | ||
| 36 | u64 program_id; | ||
| 37 | u64 title_id; | ||
| 38 | }; | ||
| 39 | |||
| 29 | // A centralized class to manage patches to games. | 40 | // A centralized class to manage patches to games. |
| 30 | class PatchManager { | 41 | class PatchManager { |
| 31 | public: | 42 | public: |
| 32 | using BuildID = std::array<u8, 0x20>; | 43 | using BuildID = std::array<u8, 0x20>; |
| 33 | using Metadata = std::pair<std::unique_ptr<NACP>, VirtualFile>; | 44 | using Metadata = std::pair<std::unique_ptr<NACP>, VirtualFile>; |
| 34 | using PatchVersionNames = std::map<std::string, std::string, std::less<>>; | ||
| 35 | 45 | ||
| 36 | explicit PatchManager(u64 title_id_, | 46 | explicit PatchManager(u64 title_id_, |
| 37 | const Service::FileSystem::FileSystemController& fs_controller_, | 47 | const Service::FileSystem::FileSystemController& fs_controller_, |
| @@ -66,9 +76,8 @@ public: | |||
| 66 | VirtualFile packed_update_raw = nullptr, | 76 | VirtualFile packed_update_raw = nullptr, |
| 67 | bool apply_layeredfs = true) const; | 77 | bool apply_layeredfs = true) const; |
| 68 | 78 | ||
| 69 | // Returns a vector of pairs between patch names and patch versions. | 79 | // Returns a vector of patches |
| 70 | // i.e. Update 3.2.2 will return {"Update", "3.2.2"} | 80 | [[nodiscard]] std::vector<Patch> GetPatches(VirtualFile update_raw = nullptr) const; |
| 71 | [[nodiscard]] PatchVersionNames GetPatchVersionNames(VirtualFile update_raw = nullptr) const; | ||
| 72 | 81 | ||
| 73 | // If the game update exists, returns the u32 version field in its Meta-type NCA. If that fails, | 82 | // If the game update exists, returns the u32 version field in its Meta-type NCA. If that fails, |
| 74 | // it will fallback to the Meta-type NCA of the base game. If that fails, the result will be | 83 | // it will fallback to the Meta-type NCA of the base game. If that fails, the result will be |
diff --git a/src/frontend_common/CMakeLists.txt b/src/frontend_common/CMakeLists.txt index 22e9337c4..94d8cc4c3 100644 --- a/src/frontend_common/CMakeLists.txt +++ b/src/frontend_common/CMakeLists.txt | |||
| @@ -4,6 +4,7 @@ | |||
| 4 | add_library(frontend_common STATIC | 4 | add_library(frontend_common STATIC |
| 5 | config.cpp | 5 | config.cpp |
| 6 | config.h | 6 | config.h |
| 7 | content_manager.h | ||
| 7 | ) | 8 | ) |
| 8 | 9 | ||
| 9 | create_target_directory_groups(frontend_common) | 10 | create_target_directory_groups(frontend_common) |
diff --git a/src/frontend_common/content_manager.h b/src/frontend_common/content_manager.h new file mode 100644 index 000000000..23f2979db --- /dev/null +++ b/src/frontend_common/content_manager.h | |||
| @@ -0,0 +1,238 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2024 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | #pragma once | ||
| 5 | |||
| 6 | #include <boost/algorithm/string.hpp> | ||
| 7 | #include "common/common_types.h" | ||
| 8 | #include "common/literals.h" | ||
| 9 | #include "core/core.h" | ||
| 10 | #include "core/file_sys/common_funcs.h" | ||
| 11 | #include "core/file_sys/content_archive.h" | ||
| 12 | #include "core/file_sys/mode.h" | ||
| 13 | #include "core/file_sys/nca_metadata.h" | ||
| 14 | #include "core/file_sys/registered_cache.h" | ||
| 15 | #include "core/file_sys/submission_package.h" | ||
| 16 | #include "core/hle/service/filesystem/filesystem.h" | ||
| 17 | #include "core/loader/loader.h" | ||
| 18 | |||
| 19 | namespace ContentManager { | ||
| 20 | |||
| 21 | enum class InstallResult { | ||
| 22 | Success, | ||
| 23 | Overwrite, | ||
| 24 | Failure, | ||
| 25 | BaseInstallAttempted, | ||
| 26 | }; | ||
| 27 | |||
| 28 | /** | ||
| 29 | * \brief Removes a single installed DLC | ||
| 30 | * \param fs_controller [FileSystemController] reference from the Core::System instance | ||
| 31 | * \param title_id Unique title ID representing the DLC which will be removed | ||
| 32 | * \return 'true' if successful | ||
| 33 | */ | ||
| 34 | inline bool RemoveDLC(const Service::FileSystem::FileSystemController& fs_controller, | ||
| 35 | const u64 title_id) { | ||
| 36 | return fs_controller.GetUserNANDContents()->RemoveExistingEntry(title_id) || | ||
| 37 | fs_controller.GetSDMCContents()->RemoveExistingEntry(title_id); | ||
| 38 | } | ||
| 39 | |||
| 40 | /** | ||
| 41 | * \brief Removes all DLC for a game | ||
| 42 | * \param system Raw pointer to the system instance | ||
| 43 | * \param program_id Program ID for the game that will have all of its DLC removed | ||
| 44 | * \return Number of DLC removed | ||
| 45 | */ | ||
| 46 | inline size_t RemoveAllDLC(Core::System* system, const u64 program_id) { | ||
| 47 | size_t count{}; | ||
| 48 | const auto& fs_controller = system->GetFileSystemController(); | ||
| 49 | const auto dlc_entries = system->GetContentProvider().ListEntriesFilter( | ||
| 50 | FileSys::TitleType::AOC, FileSys::ContentRecordType::Data); | ||
| 51 | std::vector<u64> program_dlc_entries; | ||
| 52 | |||
| 53 | for (const auto& entry : dlc_entries) { | ||
| 54 | if (FileSys::GetBaseTitleID(entry.title_id) == program_id) { | ||
| 55 | program_dlc_entries.push_back(entry.title_id); | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | for (const auto& entry : program_dlc_entries) { | ||
| 60 | if (RemoveDLC(fs_controller, entry)) { | ||
| 61 | ++count; | ||
| 62 | } | ||
| 63 | } | ||
| 64 | return count; | ||
| 65 | } | ||
| 66 | |||
| 67 | /** | ||
| 68 | * \brief Removes the installed update for a game | ||
| 69 | * \param fs_controller [FileSystemController] reference from the Core::System instance | ||
| 70 | * \param program_id Program ID for the game that will have its installed update removed | ||
| 71 | * \return 'true' if successful | ||
| 72 | */ | ||
| 73 | inline bool RemoveUpdate(const Service::FileSystem::FileSystemController& fs_controller, | ||
| 74 | const u64 program_id) { | ||
| 75 | const auto update_id = program_id | 0x800; | ||
| 76 | return fs_controller.GetUserNANDContents()->RemoveExistingEntry(update_id) || | ||
| 77 | fs_controller.GetSDMCContents()->RemoveExistingEntry(update_id); | ||
| 78 | } | ||
| 79 | |||
| 80 | /** | ||
| 81 | * \brief Removes the base content for a game | ||
| 82 | * \param fs_controller [FileSystemController] reference from the Core::System instance | ||
| 83 | * \param program_id Program ID for the game that will have its base content removed | ||
| 84 | * \return 'true' if successful | ||
| 85 | */ | ||
| 86 | inline bool RemoveBaseContent(const Service::FileSystem::FileSystemController& fs_controller, | ||
| 87 | const u64 program_id) { | ||
| 88 | return fs_controller.GetUserNANDContents()->RemoveExistingEntry(program_id) || | ||
| 89 | fs_controller.GetSDMCContents()->RemoveExistingEntry(program_id); | ||
| 90 | } | ||
| 91 | |||
| 92 | /** | ||
| 93 | * \brief Removes a mod for a game | ||
| 94 | * \param fs_controller [FileSystemController] reference from the Core::System instance | ||
| 95 | * \param program_id Program ID for the game where [mod_name] will be removed | ||
| 96 | * \param mod_name The name of a mod as given by FileSys::PatchManager::GetPatches. This corresponds | ||
| 97 | * with the name of the mod's directory in a game's load folder. | ||
| 98 | * \return 'true' if successful | ||
| 99 | */ | ||
| 100 | inline bool RemoveMod(const Service::FileSystem::FileSystemController& fs_controller, | ||
| 101 | const u64 program_id, const std::string& mod_name) { | ||
| 102 | // Check general Mods (LayeredFS and IPS) | ||
| 103 | const auto mod_dir = fs_controller.GetModificationLoadRoot(program_id); | ||
| 104 | if (mod_dir != nullptr) { | ||
| 105 | return mod_dir->DeleteSubdirectoryRecursive(mod_name); | ||
| 106 | } | ||
| 107 | |||
| 108 | // Check SDMC mod directory (RomFS LayeredFS) | ||
| 109 | const auto sdmc_mod_dir = fs_controller.GetSDMCModificationLoadRoot(program_id); | ||
| 110 | if (sdmc_mod_dir != nullptr) { | ||
| 111 | return sdmc_mod_dir->DeleteSubdirectoryRecursive(mod_name); | ||
| 112 | } | ||
| 113 | |||
| 114 | return false; | ||
| 115 | } | ||
| 116 | |||
| 117 | /** | ||
| 118 | * \brief Installs an NSP | ||
| 119 | * \param system Raw pointer to the system instance | ||
| 120 | * \param vfs Raw pointer to the VfsFilesystem instance in Core::System | ||
| 121 | * \param filename Path to the NSP file | ||
| 122 | * \param callback Optional callback to report the progress of the installation. The first size_t | ||
| 123 | * parameter is the total size of the virtual file and the second is the current progress. If you | ||
| 124 | * return false to the callback, it will cancel the installation as soon as possible. | ||
| 125 | * \return [InstallResult] representing how the installation finished | ||
| 126 | */ | ||
| 127 | inline InstallResult InstallNSP( | ||
| 128 | Core::System* system, FileSys::VfsFilesystem* vfs, const std::string& filename, | ||
| 129 | const std::function<bool(size_t, size_t)>& callback = std::function<bool(size_t, size_t)>()) { | ||
| 130 | const auto copy = [callback](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest, | ||
| 131 | std::size_t block_size) { | ||
| 132 | if (src == nullptr || dest == nullptr) { | ||
| 133 | return false; | ||
| 134 | } | ||
| 135 | if (!dest->Resize(src->GetSize())) { | ||
| 136 | return false; | ||
| 137 | } | ||
| 138 | |||
| 139 | using namespace Common::Literals; | ||
| 140 | std::vector<u8> buffer(1_MiB); | ||
| 141 | |||
| 142 | for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) { | ||
| 143 | if (callback(src->GetSize(), i)) { | ||
| 144 | dest->Resize(0); | ||
| 145 | return false; | ||
| 146 | } | ||
| 147 | const auto read = src->Read(buffer.data(), buffer.size(), i); | ||
| 148 | dest->Write(buffer.data(), read, i); | ||
| 149 | } | ||
| 150 | return true; | ||
| 151 | }; | ||
| 152 | |||
| 153 | std::shared_ptr<FileSys::NSP> nsp; | ||
| 154 | FileSys::VirtualFile file = vfs->OpenFile(filename, FileSys::Mode::Read); | ||
| 155 | if (boost::to_lower_copy(file->GetName()).ends_with(std::string("nsp"))) { | ||
| 156 | nsp = std::make_shared<FileSys::NSP>(file); | ||
| 157 | if (nsp->IsExtractedType()) { | ||
| 158 | return InstallResult::Failure; | ||
| 159 | } | ||
| 160 | } else { | ||
| 161 | return InstallResult::Failure; | ||
| 162 | } | ||
| 163 | |||
| 164 | if (nsp->GetStatus() != Loader::ResultStatus::Success) { | ||
| 165 | return InstallResult::Failure; | ||
| 166 | } | ||
| 167 | const auto res = | ||
| 168 | system->GetFileSystemController().GetUserNANDContents()->InstallEntry(*nsp, true, copy); | ||
| 169 | switch (res) { | ||
| 170 | case FileSys::InstallResult::Success: | ||
| 171 | return InstallResult::Success; | ||
| 172 | case FileSys::InstallResult::OverwriteExisting: | ||
| 173 | return InstallResult::Overwrite; | ||
| 174 | case FileSys::InstallResult::ErrorBaseInstall: | ||
| 175 | return InstallResult::BaseInstallAttempted; | ||
| 176 | default: | ||
| 177 | return InstallResult::Failure; | ||
| 178 | } | ||
| 179 | } | ||
| 180 | |||
| 181 | /** | ||
| 182 | * \brief Installs an NCA | ||
| 183 | * \param vfs Raw pointer to the VfsFilesystem instance in Core::System | ||
| 184 | * \param filename Path to the NCA file | ||
| 185 | * \param registered_cache Raw pointer to the registered cache that the NCA will be installed to | ||
| 186 | * \param title_type Type of NCA package to install | ||
| 187 | * \param callback Optional callback to report the progress of the installation. The first size_t | ||
| 188 | * parameter is the total size of the virtual file and the second is the current progress. If you | ||
| 189 | * return false to the callback, it will cancel the installation as soon as possible. | ||
| 190 | * \return [InstallResult] representing how the installation finished | ||
| 191 | */ | ||
| 192 | inline InstallResult InstallNCA( | ||
| 193 | FileSys::VfsFilesystem* vfs, const std::string& filename, | ||
| 194 | FileSys::RegisteredCache* registered_cache, const FileSys::TitleType title_type, | ||
| 195 | const std::function<bool(size_t, size_t)>& callback = std::function<bool(size_t, size_t)>()) { | ||
| 196 | const auto copy = [callback](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest, | ||
| 197 | std::size_t block_size) { | ||
| 198 | if (src == nullptr || dest == nullptr) { | ||
| 199 | return false; | ||
| 200 | } | ||
| 201 | if (!dest->Resize(src->GetSize())) { | ||
| 202 | return false; | ||
| 203 | } | ||
| 204 | |||
| 205 | using namespace Common::Literals; | ||
| 206 | std::vector<u8> buffer(1_MiB); | ||
| 207 | |||
| 208 | for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) { | ||
| 209 | if (callback(src->GetSize(), i)) { | ||
| 210 | dest->Resize(0); | ||
| 211 | return false; | ||
| 212 | } | ||
| 213 | const auto read = src->Read(buffer.data(), buffer.size(), i); | ||
| 214 | dest->Write(buffer.data(), read, i); | ||
| 215 | } | ||
| 216 | return true; | ||
| 217 | }; | ||
| 218 | |||
| 219 | const auto nca = std::make_shared<FileSys::NCA>(vfs->OpenFile(filename, FileSys::Mode::Read)); | ||
| 220 | const auto id = nca->GetStatus(); | ||
| 221 | |||
| 222 | // Game updates necessary are missing base RomFS | ||
| 223 | if (id != Loader::ResultStatus::Success && | ||
| 224 | id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) { | ||
| 225 | return InstallResult::Failure; | ||
| 226 | } | ||
| 227 | |||
| 228 | const auto res = registered_cache->InstallEntry(*nca, title_type, true, copy); | ||
| 229 | if (res == FileSys::InstallResult::Success) { | ||
| 230 | return InstallResult::Success; | ||
| 231 | } else if (res == FileSys::InstallResult::OverwriteExisting) { | ||
| 232 | return InstallResult::Overwrite; | ||
| 233 | } else { | ||
| 234 | return InstallResult::Failure; | ||
| 235 | } | ||
| 236 | } | ||
| 237 | |||
| 238 | } // namespace ContentManager | ||
diff --git a/src/yuzu/configuration/configure_per_game_addons.cpp b/src/yuzu/configuration/configure_per_game_addons.cpp index 140a7fe5d..568775027 100644 --- a/src/yuzu/configuration/configure_per_game_addons.cpp +++ b/src/yuzu/configuration/configure_per_game_addons.cpp | |||
| @@ -122,9 +122,8 @@ void ConfigurePerGameAddons::LoadConfiguration() { | |||
| 122 | 122 | ||
| 123 | const auto& disabled = Settings::values.disabled_addons[title_id]; | 123 | const auto& disabled = Settings::values.disabled_addons[title_id]; |
| 124 | 124 | ||
| 125 | for (const auto& patch : pm.GetPatchVersionNames(update_raw)) { | 125 | for (const auto& patch : pm.GetPatches(update_raw)) { |
| 126 | const auto name = | 126 | const auto name = QString::fromStdString(patch.name); |
| 127 | QString::fromStdString(patch.first).replace(QStringLiteral("[D] "), QString{}); | ||
| 128 | 127 | ||
| 129 | auto* const first_item = new QStandardItem; | 128 | auto* const first_item = new QStandardItem; |
| 130 | first_item->setText(name); | 129 | first_item->setText(name); |
| @@ -136,7 +135,7 @@ void ConfigurePerGameAddons::LoadConfiguration() { | |||
| 136 | first_item->setCheckState(patch_disabled ? Qt::Unchecked : Qt::Checked); | 135 | first_item->setCheckState(patch_disabled ? Qt::Unchecked : Qt::Checked); |
| 137 | 136 | ||
| 138 | list_items.push_back(QList<QStandardItem*>{ | 137 | list_items.push_back(QList<QStandardItem*>{ |
| 139 | first_item, new QStandardItem{QString::fromStdString(patch.second)}}); | 138 | first_item, new QStandardItem{QString::fromStdString(patch.version)}}); |
| 140 | item_model->appendRow(list_items.back()); | 139 | item_model->appendRow(list_items.back()); |
| 141 | } | 140 | } |
| 142 | 141 | ||
diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp index dc006832e..9747e3fb3 100644 --- a/src/yuzu/game_list_worker.cpp +++ b/src/yuzu/game_list_worker.cpp | |||
| @@ -164,18 +164,19 @@ QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager, | |||
| 164 | QString out; | 164 | QString out; |
| 165 | FileSys::VirtualFile update_raw; | 165 | FileSys::VirtualFile update_raw; |
| 166 | loader.ReadUpdateRaw(update_raw); | 166 | loader.ReadUpdateRaw(update_raw); |
| 167 | for (const auto& kv : patch_manager.GetPatchVersionNames(update_raw)) { | 167 | for (const auto& patch : patch_manager.GetPatches(update_raw)) { |
| 168 | const bool is_update = kv.first == "Update" || kv.first == "[D] Update"; | 168 | const bool is_update = patch.name == "Update"; |
| 169 | if (!updatable && is_update) { | 169 | if (!updatable && is_update) { |
| 170 | continue; | 170 | continue; |
| 171 | } | 171 | } |
| 172 | 172 | ||
| 173 | const QString type = QString::fromStdString(kv.first); | 173 | const QString type = |
| 174 | QString::fromStdString(patch.enabled ? patch.name : "[D] " + patch.name); | ||
| 174 | 175 | ||
| 175 | if (kv.second.empty()) { | 176 | if (patch.version.empty()) { |
| 176 | out.append(QStringLiteral("%1\n").arg(type)); | 177 | out.append(QStringLiteral("%1\n").arg(type)); |
| 177 | } else { | 178 | } else { |
| 178 | auto ver = kv.second; | 179 | auto ver = patch.version; |
| 179 | 180 | ||
| 180 | // Display container name for packed updates | 181 | // Display container name for packed updates |
| 181 | if (is_update && ver == "PACKED") { | 182 | if (is_update && ver == "PACKED") { |
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 3c562e3b2..05bd4174c 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp | |||
| @@ -47,6 +47,7 @@ | |||
| 47 | #include "core/hle/service/am/applet_oe.h" | 47 | #include "core/hle/service/am/applet_oe.h" |
| 48 | #include "core/hle/service/am/applets/applets.h" | 48 | #include "core/hle/service/am/applets/applets.h" |
| 49 | #include "core/hle/service/set/system_settings_server.h" | 49 | #include "core/hle/service/set/system_settings_server.h" |
| 50 | #include "frontend_common/content_manager.h" | ||
| 50 | #include "hid_core/frontend/emulated_controller.h" | 51 | #include "hid_core/frontend/emulated_controller.h" |
| 51 | #include "hid_core/hid_core.h" | 52 | #include "hid_core/hid_core.h" |
| 52 | #include "yuzu/multiplayer/state.h" | 53 | #include "yuzu/multiplayer/state.h" |
| @@ -2476,10 +2477,8 @@ void GMainWindow::OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryT | |||
| 2476 | } | 2477 | } |
| 2477 | 2478 | ||
| 2478 | void GMainWindow::RemoveBaseContent(u64 program_id, InstalledEntryType type) { | 2479 | void GMainWindow::RemoveBaseContent(u64 program_id, InstalledEntryType type) { |
| 2479 | const auto& fs_controller = system->GetFileSystemController(); | 2480 | const auto res = |
| 2480 | const auto res = fs_controller.GetUserNANDContents()->RemoveExistingEntry(program_id) || | 2481 | ContentManager::RemoveBaseContent(system->GetFileSystemController(), program_id); |
| 2481 | fs_controller.GetSDMCContents()->RemoveExistingEntry(program_id); | ||
| 2482 | |||
| 2483 | if (res) { | 2482 | if (res) { |
| 2484 | QMessageBox::information(this, tr("Successfully Removed"), | 2483 | QMessageBox::information(this, tr("Successfully Removed"), |
| 2485 | tr("Successfully removed the installed base game.")); | 2484 | tr("Successfully removed the installed base game.")); |
| @@ -2491,11 +2490,7 @@ void GMainWindow::RemoveBaseContent(u64 program_id, InstalledEntryType type) { | |||
| 2491 | } | 2490 | } |
| 2492 | 2491 | ||
| 2493 | void GMainWindow::RemoveUpdateContent(u64 program_id, InstalledEntryType type) { | 2492 | void GMainWindow::RemoveUpdateContent(u64 program_id, InstalledEntryType type) { |
| 2494 | const auto update_id = program_id | 0x800; | 2493 | const auto res = ContentManager::RemoveUpdate(system->GetFileSystemController(), program_id); |
| 2495 | const auto& fs_controller = system->GetFileSystemController(); | ||
| 2496 | const auto res = fs_controller.GetUserNANDContents()->RemoveExistingEntry(update_id) || | ||
| 2497 | fs_controller.GetSDMCContents()->RemoveExistingEntry(update_id); | ||
| 2498 | |||
| 2499 | if (res) { | 2494 | if (res) { |
| 2500 | QMessageBox::information(this, tr("Successfully Removed"), | 2495 | QMessageBox::information(this, tr("Successfully Removed"), |
| 2501 | tr("Successfully removed the installed update.")); | 2496 | tr("Successfully removed the installed update.")); |
| @@ -2506,22 +2501,7 @@ void GMainWindow::RemoveUpdateContent(u64 program_id, InstalledEntryType type) { | |||
| 2506 | } | 2501 | } |
| 2507 | 2502 | ||
| 2508 | void GMainWindow::RemoveAddOnContent(u64 program_id, InstalledEntryType type) { | 2503 | void GMainWindow::RemoveAddOnContent(u64 program_id, InstalledEntryType type) { |
| 2509 | u32 count{}; | 2504 | const size_t count = ContentManager::RemoveAllDLC(system.get(), program_id); |
| 2510 | const auto& fs_controller = system->GetFileSystemController(); | ||
| 2511 | const auto dlc_entries = system->GetContentProvider().ListEntriesFilter( | ||
| 2512 | FileSys::TitleType::AOC, FileSys::ContentRecordType::Data); | ||
| 2513 | |||
| 2514 | for (const auto& entry : dlc_entries) { | ||
| 2515 | if (FileSys::GetBaseTitleID(entry.title_id) == program_id) { | ||
| 2516 | const auto res = | ||
| 2517 | fs_controller.GetUserNANDContents()->RemoveExistingEntry(entry.title_id) || | ||
| 2518 | fs_controller.GetSDMCContents()->RemoveExistingEntry(entry.title_id); | ||
| 2519 | if (res) { | ||
| 2520 | ++count; | ||
| 2521 | } | ||
| 2522 | } | ||
| 2523 | } | ||
| 2524 | |||
| 2525 | if (count == 0) { | 2505 | if (count == 0) { |
| 2526 | QMessageBox::warning(this, GetGameListErrorRemoving(type), | 2506 | QMessageBox::warning(this, GetGameListErrorRemoving(type), |
| 2527 | tr("There are no DLC installed for this title.")); | 2507 | tr("There are no DLC installed for this title.")); |
| @@ -3290,12 +3270,21 @@ void GMainWindow::OnMenuInstallToNAND() { | |||
| 3290 | install_progress->setLabelText( | 3270 | install_progress->setLabelText( |
| 3291 | tr("Installing file \"%1\"...").arg(QFileInfo(file).fileName())); | 3271 | tr("Installing file \"%1\"...").arg(QFileInfo(file).fileName())); |
| 3292 | 3272 | ||
| 3293 | QFuture<InstallResult> future; | 3273 | QFuture<ContentManager::InstallResult> future; |
| 3294 | InstallResult result; | 3274 | ContentManager::InstallResult result; |
| 3295 | 3275 | ||
| 3296 | if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) { | 3276 | if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) { |
| 3297 | 3277 | const auto progress_callback = [this](size_t size, size_t progress) { | |
| 3298 | future = QtConcurrent::run([this, &file] { return InstallNSP(file); }); | 3278 | emit UpdateInstallProgress(); |
| 3279 | if (install_progress->wasCanceled()) { | ||
| 3280 | return true; | ||
| 3281 | } | ||
| 3282 | return false; | ||
| 3283 | }; | ||
| 3284 | future = QtConcurrent::run([this, &file, progress_callback] { | ||
| 3285 | return ContentManager::InstallNSP(system.get(), vfs.get(), file.toStdString(), | ||
| 3286 | progress_callback); | ||
| 3287 | }); | ||
| 3299 | 3288 | ||
| 3300 | while (!future.isFinished()) { | 3289 | while (!future.isFinished()) { |
| 3301 | QCoreApplication::processEvents(); | 3290 | QCoreApplication::processEvents(); |
| @@ -3311,16 +3300,16 @@ void GMainWindow::OnMenuInstallToNAND() { | |||
| 3311 | std::this_thread::sleep_for(std::chrono::milliseconds(10)); | 3300 | std::this_thread::sleep_for(std::chrono::milliseconds(10)); |
| 3312 | 3301 | ||
| 3313 | switch (result) { | 3302 | switch (result) { |
| 3314 | case InstallResult::Success: | 3303 | case ContentManager::InstallResult::Success: |
| 3315 | new_files.append(QFileInfo(file).fileName()); | 3304 | new_files.append(QFileInfo(file).fileName()); |
| 3316 | break; | 3305 | break; |
| 3317 | case InstallResult::Overwrite: | 3306 | case ContentManager::InstallResult::Overwrite: |
| 3318 | overwritten_files.append(QFileInfo(file).fileName()); | 3307 | overwritten_files.append(QFileInfo(file).fileName()); |
| 3319 | break; | 3308 | break; |
| 3320 | case InstallResult::Failure: | 3309 | case ContentManager::InstallResult::Failure: |
| 3321 | failed_files.append(QFileInfo(file).fileName()); | 3310 | failed_files.append(QFileInfo(file).fileName()); |
| 3322 | break; | 3311 | break; |
| 3323 | case InstallResult::BaseInstallAttempted: | 3312 | case ContentManager::InstallResult::BaseInstallAttempted: |
| 3324 | failed_files.append(QFileInfo(file).fileName()); | 3313 | failed_files.append(QFileInfo(file).fileName()); |
| 3325 | detected_base_install = true; | 3314 | detected_base_install = true; |
| 3326 | break; | 3315 | break; |
| @@ -3354,96 +3343,7 @@ void GMainWindow::OnMenuInstallToNAND() { | |||
| 3354 | ui->action_Install_File_NAND->setEnabled(true); | 3343 | ui->action_Install_File_NAND->setEnabled(true); |
| 3355 | } | 3344 | } |
| 3356 | 3345 | ||
| 3357 | InstallResult GMainWindow::InstallNSP(const QString& filename) { | 3346 | ContentManager::InstallResult GMainWindow::InstallNCA(const QString& filename) { |
| 3358 | const auto qt_raw_copy = [this](const FileSys::VirtualFile& src, | ||
| 3359 | const FileSys::VirtualFile& dest, std::size_t block_size) { | ||
| 3360 | if (src == nullptr || dest == nullptr) { | ||
| 3361 | return false; | ||
| 3362 | } | ||
| 3363 | if (!dest->Resize(src->GetSize())) { | ||
| 3364 | return false; | ||
| 3365 | } | ||
| 3366 | |||
| 3367 | std::vector<u8> buffer(CopyBufferSize); | ||
| 3368 | |||
| 3369 | for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) { | ||
| 3370 | if (install_progress->wasCanceled()) { | ||
| 3371 | dest->Resize(0); | ||
| 3372 | return false; | ||
| 3373 | } | ||
| 3374 | |||
| 3375 | emit UpdateInstallProgress(); | ||
| 3376 | |||
| 3377 | const auto read = src->Read(buffer.data(), buffer.size(), i); | ||
| 3378 | dest->Write(buffer.data(), read, i); | ||
| 3379 | } | ||
| 3380 | return true; | ||
| 3381 | }; | ||
| 3382 | |||
| 3383 | std::shared_ptr<FileSys::NSP> nsp; | ||
| 3384 | if (filename.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) { | ||
| 3385 | nsp = std::make_shared<FileSys::NSP>( | ||
| 3386 | vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read)); | ||
| 3387 | if (nsp->IsExtractedType()) { | ||
| 3388 | return InstallResult::Failure; | ||
| 3389 | } | ||
| 3390 | } else { | ||
| 3391 | return InstallResult::Failure; | ||
| 3392 | } | ||
| 3393 | |||
| 3394 | if (nsp->GetStatus() != Loader::ResultStatus::Success) { | ||
| 3395 | return InstallResult::Failure; | ||
| 3396 | } | ||
| 3397 | const auto res = system->GetFileSystemController().GetUserNANDContents()->InstallEntry( | ||
| 3398 | *nsp, true, qt_raw_copy); | ||
| 3399 | switch (res) { | ||
| 3400 | case FileSys::InstallResult::Success: | ||
| 3401 | return InstallResult::Success; | ||
| 3402 | case FileSys::InstallResult::OverwriteExisting: | ||
| 3403 | return InstallResult::Overwrite; | ||
| 3404 | case FileSys::InstallResult::ErrorBaseInstall: | ||
| 3405 | return InstallResult::BaseInstallAttempted; | ||
| 3406 | default: | ||
| 3407 | return InstallResult::Failure; | ||
| 3408 | } | ||
| 3409 | } | ||
| 3410 | |||
| 3411 | InstallResult GMainWindow::InstallNCA(const QString& filename) { | ||
| 3412 | const auto qt_raw_copy = [this](const FileSys::VirtualFile& src, | ||
| 3413 | const FileSys::VirtualFile& dest, std::size_t block_size) { | ||
| 3414 | if (src == nullptr || dest == nullptr) { | ||
| 3415 | return false; | ||
| 3416 | } | ||
| 3417 | if (!dest->Resize(src->GetSize())) { | ||
| 3418 | return false; | ||
| 3419 | } | ||
| 3420 | |||
| 3421 | std::vector<u8> buffer(CopyBufferSize); | ||
| 3422 | |||
| 3423 | for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) { | ||
| 3424 | if (install_progress->wasCanceled()) { | ||
| 3425 | dest->Resize(0); | ||
| 3426 | return false; | ||
| 3427 | } | ||
| 3428 | |||
| 3429 | emit UpdateInstallProgress(); | ||
| 3430 | |||
| 3431 | const auto read = src->Read(buffer.data(), buffer.size(), i); | ||
| 3432 | dest->Write(buffer.data(), read, i); | ||
| 3433 | } | ||
| 3434 | return true; | ||
| 3435 | }; | ||
| 3436 | |||
| 3437 | const auto nca = | ||
| 3438 | std::make_shared<FileSys::NCA>(vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read)); | ||
| 3439 | const auto id = nca->GetStatus(); | ||
| 3440 | |||
| 3441 | // Game updates necessary are missing base RomFS | ||
| 3442 | if (id != Loader::ResultStatus::Success && | ||
| 3443 | id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) { | ||
| 3444 | return InstallResult::Failure; | ||
| 3445 | } | ||
| 3446 | |||
| 3447 | const QStringList tt_options{tr("System Application"), | 3347 | const QStringList tt_options{tr("System Application"), |
| 3448 | tr("System Archive"), | 3348 | tr("System Archive"), |
| 3449 | tr("System Application Update"), | 3349 | tr("System Application Update"), |
| @@ -3464,7 +3364,7 @@ InstallResult GMainWindow::InstallNCA(const QString& filename) { | |||
| 3464 | if (!ok || index == -1) { | 3364 | if (!ok || index == -1) { |
| 3465 | QMessageBox::warning(this, tr("Failed to Install"), | 3365 | QMessageBox::warning(this, tr("Failed to Install"), |
| 3466 | tr("The title type you selected for the NCA is invalid.")); | 3366 | tr("The title type you selected for the NCA is invalid.")); |
| 3467 | return InstallResult::Failure; | 3367 | return ContentManager::InstallResult::Failure; |
| 3468 | } | 3368 | } |
| 3469 | 3369 | ||
| 3470 | // If index is equal to or past Game, add the jump in TitleType. | 3370 | // If index is equal to or past Game, add the jump in TitleType. |
| @@ -3478,15 +3378,15 @@ InstallResult GMainWindow::InstallNCA(const QString& filename) { | |||
| 3478 | auto* registered_cache = is_application ? fs_controller.GetUserNANDContents() | 3378 | auto* registered_cache = is_application ? fs_controller.GetUserNANDContents() |
| 3479 | : fs_controller.GetSystemNANDContents(); | 3379 | : fs_controller.GetSystemNANDContents(); |
| 3480 | 3380 | ||
| 3481 | const auto res = registered_cache->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), | 3381 | const auto progress_callback = [this](size_t size, size_t progress) { |
| 3482 | true, qt_raw_copy); | 3382 | emit UpdateInstallProgress(); |
| 3483 | if (res == FileSys::InstallResult::Success) { | 3383 | if (install_progress->wasCanceled()) { |
| 3484 | return InstallResult::Success; | 3384 | return true; |
| 3485 | } else if (res == FileSys::InstallResult::OverwriteExisting) { | 3385 | } |
| 3486 | return InstallResult::Overwrite; | 3386 | return false; |
| 3487 | } else { | 3387 | }; |
| 3488 | return InstallResult::Failure; | 3388 | return ContentManager::InstallNCA(vfs.get(), filename.toStdString(), registered_cache, |
| 3489 | } | 3389 | static_cast<FileSys::TitleType>(index), progress_callback); |
| 3490 | } | 3390 | } |
| 3491 | 3391 | ||
| 3492 | void GMainWindow::OnMenuRecentFile() { | 3392 | void GMainWindow::OnMenuRecentFile() { |
diff --git a/src/yuzu/main.h b/src/yuzu/main.h index f3276da64..280fae5c3 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h | |||
| @@ -16,6 +16,7 @@ | |||
| 16 | #include "common/announce_multiplayer_room.h" | 16 | #include "common/announce_multiplayer_room.h" |
| 17 | #include "common/common_types.h" | 17 | #include "common/common_types.h" |
| 18 | #include "configuration/qt_config.h" | 18 | #include "configuration/qt_config.h" |
| 19 | #include "frontend_common/content_manager.h" | ||
| 19 | #include "input_common/drivers/tas_input.h" | 20 | #include "input_common/drivers/tas_input.h" |
| 20 | #include "yuzu/compatibility_list.h" | 21 | #include "yuzu/compatibility_list.h" |
| 21 | #include "yuzu/hotkeys.h" | 22 | #include "yuzu/hotkeys.h" |
| @@ -124,13 +125,6 @@ enum class EmulatedDirectoryTarget { | |||
| 124 | SDMC, | 125 | SDMC, |
| 125 | }; | 126 | }; |
| 126 | 127 | ||
| 127 | enum class InstallResult { | ||
| 128 | Success, | ||
| 129 | Overwrite, | ||
| 130 | Failure, | ||
| 131 | BaseInstallAttempted, | ||
| 132 | }; | ||
| 133 | |||
| 134 | enum class ReinitializeKeyBehavior { | 128 | enum class ReinitializeKeyBehavior { |
| 135 | NoWarning, | 129 | NoWarning, |
| 136 | Warning, | 130 | Warning, |
| @@ -427,8 +421,7 @@ private: | |||
| 427 | void RemoveCacheStorage(u64 program_id); | 421 | void RemoveCacheStorage(u64 program_id); |
| 428 | bool SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id, | 422 | bool SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id, |
| 429 | u64* selected_title_id, u8* selected_content_record_type); | 423 | u64* selected_title_id, u8* selected_content_record_type); |
| 430 | InstallResult InstallNSP(const QString& filename); | 424 | ContentManager::InstallResult InstallNCA(const QString& filename); |
| 431 | InstallResult InstallNCA(const QString& filename); | ||
| 432 | void MigrateConfigFiles(); | 425 | void MigrateConfigFiles(); |
| 433 | void UpdateWindowTitle(std::string_view title_name = {}, std::string_view title_version = {}, | 426 | void UpdateWindowTitle(std::string_view title_name = {}, std::string_view title_version = {}, |
| 434 | std::string_view gpu_vendor = {}); | 427 | std::string_view gpu_vendor = {}); |