diff options
48 files changed, 924 insertions, 501 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 21f67f32a..6e39e542b 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 | |||
| @@ -247,7 +247,12 @@ object NativeLibrary { | |||
| 247 | 247 | ||
| 248 | external fun setAppDirectory(directory: String) | 248 | external fun setAppDirectory(directory: String) |
| 249 | 249 | ||
| 250 | external fun installFileToNand(filename: String): Int | 250 | /** |
| 251 | * Installs a nsp or xci file to nand | ||
| 252 | * @param filename String representation of file uri | ||
| 253 | * @param extension Lowercase string representation of file extension without "." | ||
| 254 | */ | ||
| 255 | external fun installFileToNand(filename: String, extension: String): Int | ||
| 251 | 256 | ||
| 252 | external fun initializeGpuDriver( | 257 | external fun initializeGpuDriver( |
| 253 | hookLibDir: String?, | 258 | hookLibDir: String?, |
| @@ -512,6 +517,11 @@ object NativeLibrary { | |||
| 512 | external fun submitInlineKeyboardInput(key_code: Int) | 517 | external fun submitInlineKeyboardInput(key_code: Int) |
| 513 | 518 | ||
| 514 | /** | 519 | /** |
| 520 | * Creates a generic user directory if it doesn't exist already | ||
| 521 | */ | ||
| 522 | external fun initializeEmptyUserDirectory() | ||
| 523 | |||
| 524 | /** | ||
| 515 | * Button type for use in onTouchEvent | 525 | * Button type for use in onTouchEvent |
| 516 | */ | 526 | */ |
| 517 | object ButtonType { | 527 | object ButtonType { |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt new file mode 100644 index 000000000..e960fbaab --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt | |||
| @@ -0,0 +1,49 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.adapters | ||
| 5 | |||
| 6 | import android.view.LayoutInflater | ||
| 7 | import android.view.View | ||
| 8 | import android.view.ViewGroup | ||
| 9 | import androidx.recyclerview.widget.RecyclerView | ||
| 10 | import org.yuzu.yuzu_emu.databinding.CardInstallableBinding | ||
| 11 | import org.yuzu.yuzu_emu.model.Installable | ||
| 12 | |||
| 13 | class InstallableAdapter(private val installables: List<Installable>) : | ||
| 14 | RecyclerView.Adapter<InstallableAdapter.InstallableViewHolder>() { | ||
| 15 | override fun onCreateViewHolder( | ||
| 16 | parent: ViewGroup, | ||
| 17 | viewType: Int | ||
| 18 | ): InstallableAdapter.InstallableViewHolder { | ||
| 19 | val binding = | ||
| 20 | CardInstallableBinding.inflate(LayoutInflater.from(parent.context), parent, false) | ||
| 21 | return InstallableViewHolder(binding) | ||
| 22 | } | ||
| 23 | |||
| 24 | override fun getItemCount(): Int = installables.size | ||
| 25 | |||
| 26 | override fun onBindViewHolder(holder: InstallableAdapter.InstallableViewHolder, position: Int) = | ||
| 27 | holder.bind(installables[position]) | ||
| 28 | |||
| 29 | inner class InstallableViewHolder(val binding: CardInstallableBinding) : | ||
| 30 | RecyclerView.ViewHolder(binding.root) { | ||
| 31 | lateinit var installable: Installable | ||
| 32 | |||
| 33 | fun bind(installable: Installable) { | ||
| 34 | this.installable = installable | ||
| 35 | |||
| 36 | binding.title.setText(installable.titleId) | ||
| 37 | binding.description.setText(installable.descriptionId) | ||
| 38 | |||
| 39 | if (installable.install != null) { | ||
| 40 | binding.buttonInstall.visibility = View.VISIBLE | ||
| 41 | binding.buttonInstall.setOnClickListener { installable.install.invoke() } | ||
| 42 | } | ||
| 43 | if (installable.export != null) { | ||
| 44 | binding.buttonExport.visibility = View.VISIBLE | ||
| 45 | binding.buttonExport.setOnClickListener { installable.export.invoke() } | ||
| 46 | } | ||
| 47 | } | ||
| 48 | } | ||
| 49 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt index 4d2f2f604..c73edd50e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt | |||
| @@ -21,6 +21,7 @@ import androidx.navigation.navArgs | |||
| 21 | import com.google.android.material.color.MaterialColors | 21 | import com.google.android.material.color.MaterialColors |
| 22 | import kotlinx.coroutines.flow.collectLatest | 22 | import kotlinx.coroutines.flow.collectLatest |
| 23 | import kotlinx.coroutines.launch | 23 | import kotlinx.coroutines.launch |
| 24 | import org.yuzu.yuzu_emu.NativeLibrary | ||
| 24 | import java.io.IOException | 25 | import java.io.IOException |
| 25 | import org.yuzu.yuzu_emu.R | 26 | import org.yuzu.yuzu_emu.R |
| 26 | import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding | 27 | import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding |
| @@ -168,7 +169,7 @@ class SettingsActivity : AppCompatActivity() { | |||
| 168 | if (!settingsFile.delete()) { | 169 | if (!settingsFile.delete()) { |
| 169 | throw IOException("Failed to delete $settingsFile") | 170 | throw IOException("Failed to delete $settingsFile") |
| 170 | } | 171 | } |
| 171 | Settings.settingsList.forEach { it.reset() } | 172 | NativeLibrary.reloadSettings() |
| 172 | 173 | ||
| 173 | Toast.makeText( | 174 | Toast.makeText( |
| 174 | applicationContext, | 175 | applicationContext, |
| @@ -181,12 +182,14 @@ class SettingsActivity : AppCompatActivity() { | |||
| 181 | private fun setInsets() { | 182 | private fun setInsets() { |
| 182 | ViewCompat.setOnApplyWindowInsetsListener( | 183 | ViewCompat.setOnApplyWindowInsetsListener( |
| 183 | binding.navigationBarShade | 184 | binding.navigationBarShade |
| 184 | ) { view: View, windowInsets: WindowInsetsCompat -> | 185 | ) { _: View, windowInsets: WindowInsetsCompat -> |
| 185 | val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) | 186 | val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) |
| 186 | 187 | ||
| 187 | val mlpShade = view.layoutParams as MarginLayoutParams | 188 | // The only situation where we care to have a nav bar shade is when it's at the bottom |
| 188 | mlpShade.height = barInsets.bottom | 189 | // of the screen where scrolling list elements can go behind it. |
| 189 | view.layoutParams = mlpShade | 190 | val mlpNavShade = binding.navigationBarShade.layoutParams as MarginLayoutParams |
| 191 | mlpNavShade.height = barInsets.bottom | ||
| 192 | binding.navigationBarShade.layoutParams = mlpNavShade | ||
| 190 | 193 | ||
| 191 | windowInsets | 194 | windowInsets |
| 192 | } | 195 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt index 7b8f99872..2ff827c6b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt | |||
| @@ -26,7 +26,6 @@ import org.yuzu.yuzu_emu.BuildConfig | |||
| 26 | import org.yuzu.yuzu_emu.R | 26 | import org.yuzu.yuzu_emu.R |
| 27 | import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding | 27 | import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding |
| 28 | import org.yuzu.yuzu_emu.model.HomeViewModel | 28 | import org.yuzu.yuzu_emu.model.HomeViewModel |
| 29 | import org.yuzu.yuzu_emu.ui.main.MainActivity | ||
| 30 | 29 | ||
| 31 | class AboutFragment : Fragment() { | 30 | class AboutFragment : Fragment() { |
| 32 | private var _binding: FragmentAboutBinding? = null | 31 | private var _binding: FragmentAboutBinding? = null |
| @@ -93,12 +92,6 @@ class AboutFragment : Fragment() { | |||
| 93 | } | 92 | } |
| 94 | } | 93 | } |
| 95 | 94 | ||
| 96 | val mainActivity = requireActivity() as MainActivity | ||
| 97 | binding.buttonExport.setOnClickListener { mainActivity.exportUserData.launch("export.zip") } | ||
| 98 | binding.buttonImport.setOnClickListener { | ||
| 99 | mainActivity.importUserData.launch(arrayOf("application/zip")) | ||
| 100 | } | ||
| 101 | |||
| 102 | binding.buttonDiscord.setOnClickListener { openLink(getString(R.string.support_link)) } | 95 | binding.buttonDiscord.setOnClickListener { openLink(getString(R.string.support_link)) } |
| 103 | binding.buttonWebsite.setOnClickListener { openLink(getString(R.string.website_link)) } | 96 | binding.buttonWebsite.setOnClickListener { openLink(getString(R.string.website_link)) } |
| 104 | binding.buttonGithub.setOnClickListener { openLink(getString(R.string.github_link)) } | 97 | binding.buttonGithub.setOnClickListener { openLink(getString(R.string.github_link)) } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index 750638bc9..e6ad2aa77 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt | |||
| @@ -17,6 +17,7 @@ import android.os.Handler | |||
| 17 | import android.os.Looper | 17 | import android.os.Looper |
| 18 | import android.view.* | 18 | import android.view.* |
| 19 | import android.widget.TextView | 19 | import android.widget.TextView |
| 20 | import android.widget.Toast | ||
| 20 | import androidx.activity.OnBackPressedCallback | 21 | import androidx.activity.OnBackPressedCallback |
| 21 | import androidx.appcompat.widget.PopupMenu | 22 | import androidx.appcompat.widget.PopupMenu |
| 22 | import androidx.core.content.res.ResourcesCompat | 23 | import androidx.core.content.res.ResourcesCompat |
| @@ -53,6 +54,7 @@ import org.yuzu.yuzu_emu.model.Game | |||
| 53 | import org.yuzu.yuzu_emu.model.EmulationViewModel | 54 | import org.yuzu.yuzu_emu.model.EmulationViewModel |
| 54 | import org.yuzu.yuzu_emu.overlay.InputOverlay | 55 | import org.yuzu.yuzu_emu.overlay.InputOverlay |
| 55 | import org.yuzu.yuzu_emu.utils.* | 56 | import org.yuzu.yuzu_emu.utils.* |
| 57 | import java.lang.NullPointerException | ||
| 56 | 58 | ||
| 57 | class EmulationFragment : Fragment(), SurfaceHolder.Callback { | 59 | class EmulationFragment : Fragment(), SurfaceHolder.Callback { |
| 58 | private lateinit var preferences: SharedPreferences | 60 | private lateinit var preferences: SharedPreferences |
| @@ -104,10 +106,21 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 104 | null | 106 | null |
| 105 | } | 107 | } |
| 106 | } | 108 | } |
| 107 | game = if (args.game != null) { | 109 | |
| 108 | args.game!! | 110 | try { |
| 109 | } else { | 111 | game = if (args.game != null) { |
| 110 | intentGame ?: error("[EmulationFragment] No bootable game present!") | 112 | args.game!! |
| 113 | } else { | ||
| 114 | intentGame!! | ||
| 115 | } | ||
| 116 | } catch (e: NullPointerException) { | ||
| 117 | Toast.makeText( | ||
| 118 | requireContext(), | ||
| 119 | R.string.no_game_present, | ||
| 120 | Toast.LENGTH_SHORT | ||
| 121 | ).show() | ||
| 122 | requireActivity().finish() | ||
| 123 | return | ||
| 111 | } | 124 | } |
| 112 | 125 | ||
| 113 | // So this fragment doesn't restart on configuration changes; i.e. rotation. | 126 | // So this fragment doesn't restart on configuration changes; i.e. rotation. |
| @@ -131,6 +144,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 131 | // This is using the correct scope, lint is just acting up | 144 | // This is using the correct scope, lint is just acting up |
| 132 | @SuppressLint("UnsafeRepeatOnLifecycleDetector") | 145 | @SuppressLint("UnsafeRepeatOnLifecycleDetector") |
| 133 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | 146 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| 147 | super.onViewCreated(view, savedInstanceState) | ||
| 148 | if (requireActivity().isFinishing) { | ||
| 149 | return | ||
| 150 | } | ||
| 151 | |||
| 134 | binding.surfaceEmulation.holder.addCallback(this) | 152 | binding.surfaceEmulation.holder.addCallback(this) |
| 135 | binding.showFpsText.setTextColor(Color.YELLOW) | 153 | binding.showFpsText.setTextColor(Color.YELLOW) |
| 136 | binding.doneControlConfig.setOnClickListener { stopConfiguringControls() } | 154 | binding.doneControlConfig.setOnClickListener { stopConfiguringControls() } |
| @@ -286,25 +304,23 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 286 | 304 | ||
| 287 | override fun onConfigurationChanged(newConfig: Configuration) { | 305 | override fun onConfigurationChanged(newConfig: Configuration) { |
| 288 | super.onConfigurationChanged(newConfig) | 306 | super.onConfigurationChanged(newConfig) |
| 307 | if (_binding == null) { | ||
| 308 | return | ||
| 309 | } | ||
| 310 | |||
| 289 | updateScreenLayout() | 311 | updateScreenLayout() |
| 290 | if (emulationActivity?.isInPictureInPictureMode == true) { | 312 | if (emulationActivity?.isInPictureInPictureMode == true) { |
| 291 | if (binding.drawerLayout.isOpen) { | 313 | if (binding.drawerLayout.isOpen) { |
| 292 | binding.drawerLayout.close() | 314 | binding.drawerLayout.close() |
| 293 | } | 315 | } |
| 294 | if (EmulationMenuSettings.showOverlay) { | 316 | if (EmulationMenuSettings.showOverlay) { |
| 295 | binding.surfaceInputOverlay.post { | 317 | binding.surfaceInputOverlay.visibility = View.INVISIBLE |
| 296 | binding.surfaceInputOverlay.visibility = View.INVISIBLE | ||
| 297 | } | ||
| 298 | } | 318 | } |
| 299 | } else { | 319 | } else { |
| 300 | if (EmulationMenuSettings.showOverlay && emulationViewModel.emulationStarted.value) { | 320 | if (EmulationMenuSettings.showOverlay && emulationViewModel.emulationStarted.value) { |
| 301 | binding.surfaceInputOverlay.post { | 321 | binding.surfaceInputOverlay.visibility = View.VISIBLE |
| 302 | binding.surfaceInputOverlay.visibility = View.VISIBLE | ||
| 303 | } | ||
| 304 | } else { | 322 | } else { |
| 305 | binding.surfaceInputOverlay.post { | 323 | binding.surfaceInputOverlay.visibility = View.INVISIBLE |
| 306 | binding.surfaceInputOverlay.visibility = View.INVISIBLE | ||
| 307 | } | ||
| 308 | } | 324 | } |
| 309 | if (!isInFoldableLayout) { | 325 | if (!isInFoldableLayout) { |
| 310 | if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { | 326 | if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index c119e69c9..8923c0ea2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt | |||
| @@ -118,18 +118,13 @@ class HomeSettingsFragment : Fragment() { | |||
| 118 | ) | 118 | ) |
| 119 | add( | 119 | add( |
| 120 | HomeSetting( | 120 | HomeSetting( |
| 121 | R.string.install_amiibo_keys, | 121 | R.string.manage_yuzu_data, |
| 122 | R.string.install_amiibo_keys_description, | 122 | R.string.manage_yuzu_data_description, |
| 123 | R.drawable.ic_nfc, | 123 | R.drawable.ic_install, |
| 124 | { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) } | 124 | { |
| 125 | ) | 125 | binding.root.findNavController() |
| 126 | ) | 126 | .navigate(R.id.action_homeSettingsFragment_to_installableFragment) |
| 127 | add( | 127 | } |
| 128 | HomeSetting( | ||
| 129 | R.string.install_game_content, | ||
| 130 | R.string.install_game_content_description, | ||
| 131 | R.drawable.ic_system_update_alt, | ||
| 132 | { mainActivity.installGameUpdate.launch(arrayOf("*/*")) } | ||
| 133 | ) | 128 | ) |
| 134 | ) | 129 | ) |
| 135 | add( | 130 | add( |
| @@ -150,35 +145,6 @@ class HomeSettingsFragment : Fragment() { | |||
| 150 | ) | 145 | ) |
| 151 | add( | 146 | add( |
| 152 | HomeSetting( | 147 | HomeSetting( |
| 153 | R.string.manage_save_data, | ||
| 154 | R.string.import_export_saves_description, | ||
| 155 | R.drawable.ic_save, | ||
| 156 | { | ||
| 157 | ImportExportSavesFragment().show( | ||
| 158 | parentFragmentManager, | ||
| 159 | ImportExportSavesFragment.TAG | ||
| 160 | ) | ||
| 161 | } | ||
| 162 | ) | ||
| 163 | ) | ||
| 164 | add( | ||
| 165 | HomeSetting( | ||
| 166 | R.string.install_prod_keys, | ||
| 167 | R.string.install_prod_keys_description, | ||
| 168 | R.drawable.ic_unlock, | ||
| 169 | { mainActivity.getProdKey.launch(arrayOf("*/*")) } | ||
| 170 | ) | ||
| 171 | ) | ||
| 172 | add( | ||
| 173 | HomeSetting( | ||
| 174 | R.string.install_firmware, | ||
| 175 | R.string.install_firmware_description, | ||
| 176 | R.drawable.ic_firmware, | ||
| 177 | { mainActivity.getFirmware.launch(arrayOf("application/zip")) } | ||
| 178 | ) | ||
| 179 | ) | ||
| 180 | add( | ||
| 181 | HomeSetting( | ||
| 182 | R.string.share_log, | 148 | R.string.share_log, |
| 183 | R.string.share_log_description, | 149 | R.string.share_log_description, |
| 184 | R.drawable.ic_log, | 150 | R.drawable.ic_log, |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt deleted file mode 100644 index ee2d44718..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt +++ /dev/null | |||
| @@ -1,214 +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.fragments | ||
| 5 | |||
| 6 | import android.app.Dialog | ||
| 7 | import android.content.Intent | ||
| 8 | import android.net.Uri | ||
| 9 | import android.os.Bundle | ||
| 10 | import android.provider.DocumentsContract | ||
| 11 | import android.widget.Toast | ||
| 12 | import androidx.activity.result.ActivityResultLauncher | ||
| 13 | import androidx.activity.result.contract.ActivityResultContracts | ||
| 14 | import androidx.appcompat.app.AppCompatActivity | ||
| 15 | import androidx.documentfile.provider.DocumentFile | ||
| 16 | import androidx.fragment.app.DialogFragment | ||
| 17 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||
| 18 | import java.io.BufferedOutputStream | ||
| 19 | import java.io.File | ||
| 20 | import java.io.FileOutputStream | ||
| 21 | import java.io.FilenameFilter | ||
| 22 | import java.time.LocalDateTime | ||
| 23 | import java.time.format.DateTimeFormatter | ||
| 24 | import java.util.zip.ZipEntry | ||
| 25 | import java.util.zip.ZipOutputStream | ||
| 26 | import kotlinx.coroutines.CoroutineScope | ||
| 27 | import kotlinx.coroutines.Dispatchers | ||
| 28 | import kotlinx.coroutines.launch | ||
| 29 | import kotlinx.coroutines.withContext | ||
| 30 | import org.yuzu.yuzu_emu.R | ||
| 31 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 32 | import org.yuzu.yuzu_emu.features.DocumentProvider | ||
| 33 | import org.yuzu.yuzu_emu.getPublicFilesDir | ||
| 34 | import org.yuzu.yuzu_emu.utils.FileUtil | ||
| 35 | |||
| 36 | class ImportExportSavesFragment : DialogFragment() { | ||
| 37 | private val context = YuzuApplication.appContext | ||
| 38 | private val savesFolder = | ||
| 39 | "${context.getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000" | ||
| 40 | |||
| 41 | // Get first subfolder in saves folder (should be the user folder) | ||
| 42 | private val savesFolderRoot = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: "" | ||
| 43 | private var lastZipCreated: File? = null | ||
| 44 | |||
| 45 | private lateinit var startForResultExportSave: ActivityResultLauncher<Intent> | ||
| 46 | private lateinit var documentPicker: ActivityResultLauncher<Array<String>> | ||
| 47 | |||
| 48 | override fun onCreate(savedInstanceState: Bundle?) { | ||
| 49 | super.onCreate(savedInstanceState) | ||
| 50 | val activity = requireActivity() as AppCompatActivity | ||
| 51 | |||
| 52 | val activityResultRegistry = requireActivity().activityResultRegistry | ||
| 53 | startForResultExportSave = activityResultRegistry.register( | ||
| 54 | "startForResultExportSaveKey", | ||
| 55 | ActivityResultContracts.StartActivityForResult() | ||
| 56 | ) { | ||
| 57 | File(context.getPublicFilesDir().canonicalPath, "temp").deleteRecursively() | ||
| 58 | } | ||
| 59 | documentPicker = activityResultRegistry.register( | ||
| 60 | "documentPickerKey", | ||
| 61 | ActivityResultContracts.OpenDocument() | ||
| 62 | ) { | ||
| 63 | it?.let { uri -> importSave(uri, activity) } | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { | ||
| 68 | return if (savesFolderRoot == "") { | ||
| 69 | MaterialAlertDialogBuilder(requireContext()) | ||
| 70 | .setTitle(R.string.manage_save_data) | ||
| 71 | .setMessage(R.string.import_export_saves_no_profile) | ||
| 72 | .setPositiveButton(android.R.string.ok, null) | ||
| 73 | .show() | ||
| 74 | } else { | ||
| 75 | MaterialAlertDialogBuilder(requireContext()) | ||
| 76 | .setTitle(R.string.manage_save_data) | ||
| 77 | .setMessage(R.string.manage_save_data_description) | ||
| 78 | .setNegativeButton(R.string.export_saves) { _, _ -> | ||
| 79 | exportSave() | ||
| 80 | } | ||
| 81 | .setPositiveButton(R.string.import_saves) { _, _ -> | ||
| 82 | documentPicker.launch(arrayOf("application/zip")) | ||
| 83 | } | ||
| 84 | .setNeutralButton(android.R.string.cancel, null) | ||
| 85 | .show() | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | /** | ||
| 90 | * Zips the save files located in the given folder path and creates a new zip file with the current date and time. | ||
| 91 | * @return true if the zip file is successfully created, false otherwise. | ||
| 92 | */ | ||
| 93 | private fun zipSave(): Boolean { | ||
| 94 | try { | ||
| 95 | val tempFolder = File(requireContext().getPublicFilesDir().canonicalPath, "temp") | ||
| 96 | tempFolder.mkdirs() | ||
| 97 | val saveFolder = File(savesFolderRoot) | ||
| 98 | val outputZipFile = File( | ||
| 99 | tempFolder, | ||
| 100 | "yuzu saves - ${ | ||
| 101 | LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) | ||
| 102 | }.zip" | ||
| 103 | ) | ||
| 104 | outputZipFile.createNewFile() | ||
| 105 | ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos -> | ||
| 106 | saveFolder.walkTopDown().forEach { file -> | ||
| 107 | val zipFileName = | ||
| 108 | file.absolutePath.removePrefix(savesFolderRoot).removePrefix("/") | ||
| 109 | if (zipFileName == "") { | ||
| 110 | return@forEach | ||
| 111 | } | ||
| 112 | val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}") | ||
| 113 | zos.putNextEntry(entry) | ||
| 114 | if (file.isFile) { | ||
| 115 | file.inputStream().use { fis -> fis.copyTo(zos) } | ||
| 116 | } | ||
| 117 | } | ||
| 118 | } | ||
| 119 | lastZipCreated = outputZipFile | ||
| 120 | } catch (e: Exception) { | ||
| 121 | return false | ||
| 122 | } | ||
| 123 | return true | ||
| 124 | } | ||
| 125 | |||
| 126 | /** | ||
| 127 | * Exports the save file located in the given folder path by creating a zip file and sharing it via intent. | ||
| 128 | */ | ||
| 129 | private fun exportSave() { | ||
| 130 | CoroutineScope(Dispatchers.IO).launch { | ||
| 131 | val wasZipCreated = zipSave() | ||
| 132 | val lastZipFile = lastZipCreated | ||
| 133 | if (!wasZipCreated || lastZipFile == null) { | ||
| 134 | withContext(Dispatchers.Main) { | ||
| 135 | Toast.makeText(context, "Failed to export save", Toast.LENGTH_LONG).show() | ||
| 136 | } | ||
| 137 | return@launch | ||
| 138 | } | ||
| 139 | |||
| 140 | withContext(Dispatchers.Main) { | ||
| 141 | val file = DocumentFile.fromSingleUri( | ||
| 142 | context, | ||
| 143 | DocumentsContract.buildDocumentUri( | ||
| 144 | DocumentProvider.AUTHORITY, | ||
| 145 | "${DocumentProvider.ROOT_ID}/temp/${lastZipFile.name}" | ||
| 146 | ) | ||
| 147 | )!! | ||
| 148 | val intent = Intent(Intent.ACTION_SEND) | ||
| 149 | .setDataAndType(file.uri, "application/zip") | ||
| 150 | .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) | ||
| 151 | .putExtra(Intent.EXTRA_STREAM, file.uri) | ||
| 152 | startForResultExportSave.launch(Intent.createChooser(intent, "Share save file")) | ||
| 153 | } | ||
| 154 | } | ||
| 155 | } | ||
| 156 | |||
| 157 | /** | ||
| 158 | * Imports the save files contained in the zip file, and replaces any existing ones with the new save file. | ||
| 159 | * @param zipUri The Uri of the zip file containing the save file(s) to import. | ||
| 160 | */ | ||
| 161 | private fun importSave(zipUri: Uri, activity: AppCompatActivity) { | ||
| 162 | val inputZip = context.contentResolver.openInputStream(zipUri) | ||
| 163 | // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid. | ||
| 164 | var validZip = false | ||
| 165 | val savesFolder = File(savesFolderRoot) | ||
| 166 | val cacheSaveDir = File("${context.cacheDir.path}/saves/") | ||
| 167 | cacheSaveDir.mkdir() | ||
| 168 | |||
| 169 | if (inputZip == null) { | ||
| 170 | Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG) | ||
| 171 | .show() | ||
| 172 | return | ||
| 173 | } | ||
| 174 | |||
| 175 | val filterTitleId = | ||
| 176 | FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) } | ||
| 177 | |||
| 178 | try { | ||
| 179 | CoroutineScope(Dispatchers.IO).launch { | ||
| 180 | FileUtil.unzip(inputZip, cacheSaveDir) | ||
| 181 | cacheSaveDir.list(filterTitleId)?.forEach { savePath -> | ||
| 182 | File(savesFolder, savePath).deleteRecursively() | ||
| 183 | File(cacheSaveDir, savePath).copyRecursively(File(savesFolder, savePath), true) | ||
| 184 | validZip = true | ||
| 185 | } | ||
| 186 | |||
| 187 | withContext(Dispatchers.Main) { | ||
| 188 | if (!validZip) { | ||
| 189 | MessageDialogFragment.newInstance( | ||
| 190 | requireActivity(), | ||
| 191 | titleId = R.string.save_file_invalid_zip_structure, | ||
| 192 | descriptionId = R.string.save_file_invalid_zip_structure_description | ||
| 193 | ).show(activity.supportFragmentManager, MessageDialogFragment.TAG) | ||
| 194 | return@withContext | ||
| 195 | } | ||
| 196 | Toast.makeText( | ||
| 197 | context, | ||
| 198 | context.getString(R.string.save_file_imported_success), | ||
| 199 | Toast.LENGTH_LONG | ||
| 200 | ).show() | ||
| 201 | } | ||
| 202 | |||
| 203 | cacheSaveDir.deleteRecursively() | ||
| 204 | } | ||
| 205 | } catch (e: Exception) { | ||
| 206 | Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG) | ||
| 207 | .show() | ||
| 208 | } | ||
| 209 | } | ||
| 210 | |||
| 211 | companion object { | ||
| 212 | const val TAG = "ImportExportSavesFragment" | ||
| 213 | } | ||
| 214 | } | ||
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/IndeterminateProgressDialogFragment.kt index 0d16a7d37..f128deda8 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/IndeterminateProgressDialogFragment.kt | |||
| @@ -4,12 +4,12 @@ | |||
| 4 | package org.yuzu.yuzu_emu.fragments | 4 | package org.yuzu.yuzu_emu.fragments |
| 5 | 5 | ||
| 6 | import android.app.Dialog | 6 | import android.app.Dialog |
| 7 | import android.content.DialogInterface | ||
| 8 | import android.os.Bundle | 7 | import android.os.Bundle |
| 9 | import android.view.LayoutInflater | 8 | import android.view.LayoutInflater |
| 10 | import android.view.View | 9 | import android.view.View |
| 11 | import android.view.ViewGroup | 10 | import android.view.ViewGroup |
| 12 | import android.widget.Toast | 11 | import android.widget.Toast |
| 12 | import androidx.appcompat.app.AlertDialog | ||
| 13 | import androidx.appcompat.app.AppCompatActivity | 13 | import androidx.appcompat.app.AppCompatActivity |
| 14 | import androidx.fragment.app.DialogFragment | 14 | import androidx.fragment.app.DialogFragment |
| 15 | import androidx.fragment.app.activityViewModels | 15 | import androidx.fragment.app.activityViewModels |
| @@ -39,9 +39,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() { | |||
| 39 | .setView(binding.root) | 39 | .setView(binding.root) |
| 40 | 40 | ||
| 41 | if (cancellable) { | 41 | if (cancellable) { |
| 42 | dialog.setNegativeButton(android.R.string.cancel) { _: DialogInterface, _: Int -> | 42 | dialog.setNegativeButton(android.R.string.cancel, null) |
| 43 | taskViewModel.setCancelled(true) | ||
| 44 | } | ||
| 45 | } | 43 | } |
| 46 | 44 | ||
| 47 | val alertDialog = dialog.create() | 45 | val alertDialog = dialog.create() |
| @@ -98,6 +96,18 @@ class IndeterminateProgressDialogFragment : DialogFragment() { | |||
| 98 | } | 96 | } |
| 99 | } | 97 | } |
| 100 | 98 | ||
| 99 | // By default, the ProgressDialog will immediately dismiss itself upon a button being pressed. | ||
| 100 | // Setting the OnClickListener again after the dialog is shown overrides this behavior. | ||
| 101 | override fun onResume() { | ||
| 102 | super.onResume() | ||
| 103 | val alertDialog = dialog as AlertDialog | ||
| 104 | val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE) | ||
| 105 | negativeButton.setOnClickListener { | ||
| 106 | alertDialog.setTitle(getString(R.string.cancelling)) | ||
| 107 | taskViewModel.setCancelled(true) | ||
| 108 | } | ||
| 109 | } | ||
| 110 | |||
| 101 | companion object { | 111 | companion object { |
| 102 | const val TAG = "IndeterminateProgressDialogFragment" | 112 | const val TAG = "IndeterminateProgressDialogFragment" |
| 103 | 113 | ||
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 new file mode 100644 index 000000000..ec116ab62 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt | |||
| @@ -0,0 +1,138 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.fragments | ||
| 5 | |||
| 6 | import android.os.Bundle | ||
| 7 | import android.view.LayoutInflater | ||
| 8 | import android.view.View | ||
| 9 | import android.view.ViewGroup | ||
| 10 | import androidx.core.view.ViewCompat | ||
| 11 | import androidx.core.view.WindowInsetsCompat | ||
| 12 | import androidx.core.view.updatePadding | ||
| 13 | import androidx.fragment.app.Fragment | ||
| 14 | import androidx.fragment.app.activityViewModels | ||
| 15 | import androidx.navigation.findNavController | ||
| 16 | import androidx.recyclerview.widget.GridLayoutManager | ||
| 17 | import com.google.android.material.transition.MaterialSharedAxis | ||
| 18 | import org.yuzu.yuzu_emu.R | ||
| 19 | import org.yuzu.yuzu_emu.adapters.InstallableAdapter | ||
| 20 | import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding | ||
| 21 | import org.yuzu.yuzu_emu.model.HomeViewModel | ||
| 22 | import org.yuzu.yuzu_emu.model.Installable | ||
| 23 | import org.yuzu.yuzu_emu.ui.main.MainActivity | ||
| 24 | |||
| 25 | class InstallableFragment : Fragment() { | ||
| 26 | private var _binding: FragmentInstallablesBinding? = null | ||
| 27 | private val binding get() = _binding!! | ||
| 28 | |||
| 29 | private val homeViewModel: HomeViewModel by activityViewModels() | ||
| 30 | |||
| 31 | override fun onCreate(savedInstanceState: Bundle?) { | ||
| 32 | super.onCreate(savedInstanceState) | ||
| 33 | enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) | ||
| 34 | returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) | ||
| 35 | reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) | ||
| 36 | } | ||
| 37 | |||
| 38 | override fun onCreateView( | ||
| 39 | inflater: LayoutInflater, | ||
| 40 | container: ViewGroup?, | ||
| 41 | savedInstanceState: Bundle? | ||
| 42 | ): View { | ||
| 43 | _binding = FragmentInstallablesBinding.inflate(layoutInflater) | ||
| 44 | return binding.root | ||
| 45 | } | ||
| 46 | |||
| 47 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| 48 | super.onViewCreated(view, savedInstanceState) | ||
| 49 | |||
| 50 | val mainActivity = requireActivity() as MainActivity | ||
| 51 | |||
| 52 | homeViewModel.setNavigationVisibility(visible = false, animated = true) | ||
| 53 | homeViewModel.setStatusBarShadeVisibility(visible = false) | ||
| 54 | |||
| 55 | binding.toolbarInstallables.setNavigationOnClickListener { | ||
| 56 | binding.root.findNavController().popBackStack() | ||
| 57 | } | ||
| 58 | |||
| 59 | val installables = listOf( | ||
| 60 | Installable( | ||
| 61 | R.string.user_data, | ||
| 62 | R.string.user_data_description, | ||
| 63 | install = { mainActivity.importUserData.launch(arrayOf("application/zip")) }, | ||
| 64 | export = { mainActivity.exportUserData.launch("export.zip") } | ||
| 65 | ), | ||
| 66 | Installable( | ||
| 67 | R.string.install_game_content, | ||
| 68 | R.string.install_game_content_description, | ||
| 69 | install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) } | ||
| 70 | ), | ||
| 71 | Installable( | ||
| 72 | R.string.install_firmware, | ||
| 73 | R.string.install_firmware_description, | ||
| 74 | install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) } | ||
| 75 | ), | ||
| 76 | if (mainActivity.savesFolderRoot != "") { | ||
| 77 | Installable( | ||
| 78 | R.string.manage_save_data, | ||
| 79 | R.string.import_export_saves_description, | ||
| 80 | install = { mainActivity.importSaves.launch(arrayOf("application/zip")) }, | ||
| 81 | export = { mainActivity.exportSave() } | ||
| 82 | ) | ||
| 83 | } else { | ||
| 84 | Installable( | ||
| 85 | R.string.manage_save_data, | ||
| 86 | R.string.import_export_saves_description, | ||
| 87 | install = { mainActivity.importSaves.launch(arrayOf("application/zip")) } | ||
| 88 | ) | ||
| 89 | }, | ||
| 90 | Installable( | ||
| 91 | R.string.install_prod_keys, | ||
| 92 | R.string.install_prod_keys_description, | ||
| 93 | install = { mainActivity.getProdKey.launch(arrayOf("*/*")) } | ||
| 94 | ), | ||
| 95 | Installable( | ||
| 96 | R.string.install_amiibo_keys, | ||
| 97 | R.string.install_amiibo_keys_description, | ||
| 98 | install = { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) } | ||
| 99 | ) | ||
| 100 | ) | ||
| 101 | |||
| 102 | binding.listInstallables.apply { | ||
| 103 | layoutManager = GridLayoutManager( | ||
| 104 | requireContext(), | ||
| 105 | resources.getInteger(R.integer.grid_columns) | ||
| 106 | ) | ||
| 107 | adapter = InstallableAdapter(installables) | ||
| 108 | } | ||
| 109 | |||
| 110 | setInsets() | ||
| 111 | } | ||
| 112 | |||
| 113 | private fun setInsets() = | ||
| 114 | ViewCompat.setOnApplyWindowInsetsListener( | ||
| 115 | binding.root | ||
| 116 | ) { _: View, windowInsets: WindowInsetsCompat -> | ||
| 117 | val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) | ||
| 118 | val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) | ||
| 119 | |||
| 120 | val leftInsets = barInsets.left + cutoutInsets.left | ||
| 121 | val rightInsets = barInsets.right + cutoutInsets.right | ||
| 122 | |||
| 123 | val mlpAppBar = binding.toolbarInstallables.layoutParams as ViewGroup.MarginLayoutParams | ||
| 124 | mlpAppBar.leftMargin = leftInsets | ||
| 125 | mlpAppBar.rightMargin = rightInsets | ||
| 126 | binding.toolbarInstallables.layoutParams = mlpAppBar | ||
| 127 | |||
| 128 | val mlpScrollAbout = | ||
| 129 | binding.listInstallables.layoutParams as ViewGroup.MarginLayoutParams | ||
| 130 | mlpScrollAbout.leftMargin = leftInsets | ||
| 131 | mlpScrollAbout.rightMargin = rightInsets | ||
| 132 | binding.listInstallables.layoutParams = mlpScrollAbout | ||
| 133 | |||
| 134 | binding.listInstallables.updatePadding(bottom = barInsets.bottom) | ||
| 135 | |||
| 136 | windowInsets | ||
| 137 | } | ||
| 138 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt index fbb2f6e18..c66bb635a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt | |||
| @@ -295,8 +295,10 @@ class SetupFragment : Fragment() { | |||
| 295 | 295 | ||
| 296 | override fun onSaveInstanceState(outState: Bundle) { | 296 | override fun onSaveInstanceState(outState: Bundle) { |
| 297 | super.onSaveInstanceState(outState) | 297 | super.onSaveInstanceState(outState) |
| 298 | outState.putBoolean(KEY_NEXT_VISIBILITY, binding.buttonNext.isVisible) | 298 | if (_binding != null) { |
| 299 | outState.putBoolean(KEY_BACK_VISIBILITY, binding.buttonBack.isVisible) | 299 | outState.putBoolean(KEY_NEXT_VISIBILITY, binding.buttonNext.isVisible) |
| 300 | outState.putBoolean(KEY_BACK_VISIBILITY, binding.buttonBack.isVisible) | ||
| 301 | } | ||
| 300 | outState.putBooleanArray(KEY_HAS_BEEN_WARNED, hasBeenWarned) | 302 | outState.putBooleanArray(KEY_HAS_BEEN_WARNED, hasBeenWarned) |
| 301 | } | 303 | } |
| 302 | 304 | ||
| @@ -353,11 +355,15 @@ class SetupFragment : Fragment() { | |||
| 353 | } | 355 | } |
| 354 | 356 | ||
| 355 | fun pageForward() { | 357 | fun pageForward() { |
| 356 | binding.viewPager2.currentItem = binding.viewPager2.currentItem + 1 | 358 | if (_binding != null) { |
| 359 | binding.viewPager2.currentItem += 1 | ||
| 360 | } | ||
| 357 | } | 361 | } |
| 358 | 362 | ||
| 359 | fun pageBackward() { | 363 | fun pageBackward() { |
| 360 | binding.viewPager2.currentItem = binding.viewPager2.currentItem - 1 | 364 | if (_binding != null) { |
| 365 | binding.viewPager2.currentItem -= 1 | ||
| 366 | } | ||
| 361 | } | 367 | } |
| 362 | 368 | ||
| 363 | fun setPageWarned(page: Int) { | 369 | fun setPageWarned(page: Int) { |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Installable.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Installable.kt new file mode 100644 index 000000000..36a7c97b8 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Installable.kt | |||
| @@ -0,0 +1,13 @@ | |||
| 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.StringRes | ||
| 7 | |||
| 8 | data class Installable( | ||
| 9 | @StringRes val titleId: Int, | ||
| 10 | @StringRes val descriptionId: Int, | ||
| 11 | val install: (() -> Unit)? = null, | ||
| 12 | val export: (() -> Unit)? = null | ||
| 13 | ) | ||
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 d6418a666..16a794dee 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 | |||
| @@ -50,3 +50,9 @@ class TaskViewModel : ViewModel() { | |||
| 50 | } | 50 | } |
| 51 | } | 51 | } |
| 52 | } | 52 | } |
| 53 | |||
| 54 | enum class TaskState { | ||
| 55 | Completed, | ||
| 56 | Failed, | ||
| 57 | Cancelled | ||
| 58 | } | ||
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 6fa847631..0fa5df5e5 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 | |||
| @@ -6,6 +6,7 @@ package org.yuzu.yuzu_emu.ui.main | |||
| 6 | import android.content.Intent | 6 | import android.content.Intent |
| 7 | import android.net.Uri | 7 | import android.net.Uri |
| 8 | import android.os.Bundle | 8 | import android.os.Bundle |
| 9 | import android.provider.DocumentsContract | ||
| 9 | import android.view.View | 10 | import android.view.View |
| 10 | import android.view.ViewGroup.MarginLayoutParams | 11 | import android.view.ViewGroup.MarginLayoutParams |
| 11 | import android.view.WindowManager | 12 | import android.view.WindowManager |
| @@ -19,6 +20,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen | |||
| 19 | import androidx.core.view.ViewCompat | 20 | import androidx.core.view.ViewCompat |
| 20 | import androidx.core.view.WindowCompat | 21 | import androidx.core.view.WindowCompat |
| 21 | import androidx.core.view.WindowInsetsCompat | 22 | import androidx.core.view.WindowInsetsCompat |
| 23 | import androidx.documentfile.provider.DocumentFile | ||
| 22 | import androidx.lifecycle.Lifecycle | 24 | import androidx.lifecycle.Lifecycle |
| 23 | import androidx.lifecycle.lifecycleScope | 25 | import androidx.lifecycle.lifecycleScope |
| 24 | import androidx.lifecycle.repeatOnLifecycle | 26 | import androidx.lifecycle.repeatOnLifecycle |
| @@ -29,6 +31,7 @@ import androidx.preference.PreferenceManager | |||
| 29 | import com.google.android.material.color.MaterialColors | 31 | import com.google.android.material.color.MaterialColors |
| 30 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | 32 | import com.google.android.material.dialog.MaterialAlertDialogBuilder |
| 31 | import com.google.android.material.navigation.NavigationBarView | 33 | import com.google.android.material.navigation.NavigationBarView |
| 34 | import kotlinx.coroutines.CoroutineScope | ||
| 32 | import java.io.File | 35 | import java.io.File |
| 33 | import java.io.FilenameFilter | 36 | import java.io.FilenameFilter |
| 34 | import java.io.IOException | 37 | import java.io.IOException |
| @@ -41,20 +44,23 @@ import org.yuzu.yuzu_emu.R | |||
| 41 | import org.yuzu.yuzu_emu.activities.EmulationActivity | 44 | import org.yuzu.yuzu_emu.activities.EmulationActivity |
| 42 | import org.yuzu.yuzu_emu.databinding.ActivityMainBinding | 45 | import org.yuzu.yuzu_emu.databinding.ActivityMainBinding |
| 43 | import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding | 46 | import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding |
| 47 | import org.yuzu.yuzu_emu.features.DocumentProvider | ||
| 44 | import org.yuzu.yuzu_emu.features.settings.model.Settings | 48 | import org.yuzu.yuzu_emu.features.settings.model.Settings |
| 45 | import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment | 49 | import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment |
| 46 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment | 50 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment |
| 51 | import org.yuzu.yuzu_emu.getPublicFilesDir | ||
| 47 | import org.yuzu.yuzu_emu.model.GamesViewModel | 52 | import org.yuzu.yuzu_emu.model.GamesViewModel |
| 48 | import org.yuzu.yuzu_emu.model.HomeViewModel | 53 | import org.yuzu.yuzu_emu.model.HomeViewModel |
| 54 | import org.yuzu.yuzu_emu.model.TaskState | ||
| 49 | import org.yuzu.yuzu_emu.model.TaskViewModel | 55 | import org.yuzu.yuzu_emu.model.TaskViewModel |
| 50 | import org.yuzu.yuzu_emu.utils.* | 56 | import org.yuzu.yuzu_emu.utils.* |
| 51 | import java.io.BufferedInputStream | 57 | import java.io.BufferedInputStream |
| 52 | import java.io.BufferedOutputStream | 58 | import java.io.BufferedOutputStream |
| 53 | import java.io.FileInputStream | ||
| 54 | import java.io.FileOutputStream | 59 | import java.io.FileOutputStream |
| 60 | import java.time.LocalDateTime | ||
| 61 | import java.time.format.DateTimeFormatter | ||
| 55 | import java.util.zip.ZipEntry | 62 | import java.util.zip.ZipEntry |
| 56 | import java.util.zip.ZipInputStream | 63 | import java.util.zip.ZipInputStream |
| 57 | import java.util.zip.ZipOutputStream | ||
| 58 | 64 | ||
| 59 | class MainActivity : AppCompatActivity(), ThemeProvider { | 65 | class MainActivity : AppCompatActivity(), ThemeProvider { |
| 60 | private lateinit var binding: ActivityMainBinding | 66 | private lateinit var binding: ActivityMainBinding |
| @@ -65,6 +71,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 65 | 71 | ||
| 66 | override var themeId: Int = 0 | 72 | override var themeId: Int = 0 |
| 67 | 73 | ||
| 74 | private val savesFolder | ||
| 75 | get() = "${getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000" | ||
| 76 | |||
| 77 | // Get first subfolder in saves folder (should be the user folder) | ||
| 78 | val savesFolderRoot get() = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: "" | ||
| 79 | private var lastZipCreated: File? = null | ||
| 80 | |||
| 68 | override fun onCreate(savedInstanceState: Bundle?) { | 81 | override fun onCreate(savedInstanceState: Bundle?) { |
| 69 | val splashScreen = installSplashScreen() | 82 | val splashScreen = installSplashScreen() |
| 70 | splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } | 83 | splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } |
| @@ -382,7 +395,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 382 | val task: () -> Any = { | 395 | val task: () -> Any = { |
| 383 | var messageToShow: Any | 396 | var messageToShow: Any |
| 384 | try { | 397 | try { |
| 385 | FileUtil.unzip(inputZip, cacheFirmwareDir) | 398 | FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheFirmwareDir) |
| 386 | val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1 | 399 | val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1 |
| 387 | val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 | 400 | val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 |
| 388 | messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { | 401 | messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { |
| @@ -515,7 +528,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 515 | if (documents.isNotEmpty()) { | 528 | if (documents.isNotEmpty()) { |
| 516 | IndeterminateProgressDialogFragment.newInstance( | 529 | IndeterminateProgressDialogFragment.newInstance( |
| 517 | this@MainActivity, | 530 | this@MainActivity, |
| 518 | R.string.install_game_content | 531 | R.string.installing_game_content |
| 519 | ) { | 532 | ) { |
| 520 | var installSuccess = 0 | 533 | var installSuccess = 0 |
| 521 | var installOverwrite = 0 | 534 | var installOverwrite = 0 |
| @@ -523,7 +536,12 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 523 | var errorExtension = 0 | 536 | var errorExtension = 0 |
| 524 | var errorOther = 0 | 537 | var errorOther = 0 |
| 525 | documents.forEach { | 538 | documents.forEach { |
| 526 | when (NativeLibrary.installFileToNand(it.toString())) { | 539 | when ( |
| 540 | NativeLibrary.installFileToNand( | ||
| 541 | it.toString(), | ||
| 542 | FileUtil.getExtension(it) | ||
| 543 | ) | ||
| 544 | ) { | ||
| 527 | NativeLibrary.InstallFileToNandResult.Success -> { | 545 | NativeLibrary.InstallFileToNandResult.Success -> { |
| 528 | installSuccess += 1 | 546 | installSuccess += 1 |
| 529 | } | 547 | } |
| @@ -625,35 +643,17 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 625 | R.string.exporting_user_data, | 643 | R.string.exporting_user_data, |
| 626 | true | 644 | true |
| 627 | ) { | 645 | ) { |
| 628 | val zos = ZipOutputStream( | 646 | val zipResult = FileUtil.zipFromInternalStorage( |
| 629 | BufferedOutputStream(contentResolver.openOutputStream(result)) | 647 | File(DirectoryInitialization.userDirectory!!), |
| 648 | DirectoryInitialization.userDirectory!!, | ||
| 649 | BufferedOutputStream(contentResolver.openOutputStream(result)), | ||
| 650 | taskViewModel.cancelled | ||
| 630 | ) | 651 | ) |
| 631 | zos.use { stream -> | 652 | return@newInstance when (zipResult) { |
| 632 | File(DirectoryInitialization.userDirectory!!).walkTopDown().forEach { file -> | 653 | TaskState.Completed -> getString(R.string.user_data_export_success) |
| 633 | if (taskViewModel.cancelled.value) { | 654 | TaskState.Failed -> R.string.export_failed |
| 634 | return@newInstance R.string.user_data_export_cancelled | 655 | TaskState.Cancelled -> R.string.user_data_export_cancelled |
| 635 | } | ||
| 636 | |||
| 637 | if (!file.isDirectory) { | ||
| 638 | val newPath = file.path.substring( | ||
| 639 | DirectoryInitialization.userDirectory!!.length, | ||
| 640 | file.path.length | ||
| 641 | ) | ||
| 642 | stream.putNextEntry(ZipEntry(newPath)) | ||
| 643 | |||
| 644 | val buffer = ByteArray(8096) | ||
| 645 | var read: Int | ||
| 646 | FileInputStream(file).use { fis -> | ||
| 647 | while (fis.read(buffer).also { read = it } != -1) { | ||
| 648 | stream.write(buffer, 0, read) | ||
| 649 | } | ||
| 650 | } | ||
| 651 | |||
| 652 | stream.closeEntry() | ||
| 653 | } | ||
| 654 | } | ||
| 655 | } | 656 | } |
| 656 | return@newInstance getString(R.string.user_data_export_success) | ||
| 657 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) | 657 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) |
| 658 | } | 658 | } |
| 659 | 659 | ||
| @@ -681,43 +681,28 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 681 | } | 681 | } |
| 682 | } | 682 | } |
| 683 | if (!isYuzuBackup) { | 683 | if (!isYuzuBackup) { |
| 684 | return@newInstance getString(R.string.invalid_yuzu_backup) | 684 | return@newInstance MessageDialogFragment.newInstance( |
| 685 | this, | ||
| 686 | titleId = R.string.invalid_yuzu_backup, | ||
| 687 | descriptionId = R.string.user_data_import_failed_description | ||
| 688 | ) | ||
| 685 | } | 689 | } |
| 686 | 690 | ||
| 691 | // Clear existing user data | ||
| 687 | File(DirectoryInitialization.userDirectory!!).deleteRecursively() | 692 | File(DirectoryInitialization.userDirectory!!).deleteRecursively() |
| 688 | 693 | ||
| 689 | val zis = | 694 | // Copy archive to internal storage |
| 690 | ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result))) | 695 | try { |
| 691 | val userDirectory = File(DirectoryInitialization.userDirectory!!) | 696 | FileUtil.unzipToInternalStorage( |
| 692 | val canonicalPath = userDirectory.canonicalPath + '/' | 697 | BufferedInputStream(contentResolver.openInputStream(result)), |
| 693 | zis.use { stream -> | 698 | File(DirectoryInitialization.userDirectory!!) |
| 694 | var ze: ZipEntry? = stream.nextEntry | 699 | ) |
| 695 | while (ze != null) { | 700 | } catch (e: Exception) { |
| 696 | val newFile = File(userDirectory, ze!!.name) | 701 | return@newInstance MessageDialogFragment.newInstance( |
| 697 | val destinationDirectory = | 702 | this, |
| 698 | if (ze!!.isDirectory) newFile else newFile.parentFile | 703 | titleId = R.string.import_failed, |
| 699 | 704 | descriptionId = R.string.user_data_import_failed_description | |
| 700 | if (!newFile.canonicalPath.startsWith(canonicalPath)) { | 705 | ) |
| 701 | throw SecurityException( | ||
| 702 | "Zip file attempted path traversal! ${ze!!.name}" | ||
| 703 | ) | ||
| 704 | } | ||
| 705 | |||
| 706 | if (!destinationDirectory.isDirectory && !destinationDirectory.mkdirs()) { | ||
| 707 | throw IOException("Failed to create directory $destinationDirectory") | ||
| 708 | } | ||
| 709 | |||
| 710 | if (!ze!!.isDirectory) { | ||
| 711 | val buffer = ByteArray(8096) | ||
| 712 | var read: Int | ||
| 713 | BufferedOutputStream(FileOutputStream(newFile)).use { bos -> | ||
| 714 | while (zis.read(buffer).also { read = it } != -1) { | ||
| 715 | bos.write(buffer, 0, read) | ||
| 716 | } | ||
| 717 | } | ||
| 718 | } | ||
| 719 | ze = stream.nextEntry | ||
| 720 | } | ||
| 721 | } | 706 | } |
| 722 | 707 | ||
| 723 | // Reinitialize relevant data | 708 | // Reinitialize relevant data |
| @@ -727,4 +712,146 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 727 | return@newInstance getString(R.string.user_data_import_success) | 712 | return@newInstance getString(R.string.user_data_import_success) |
| 728 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) | 713 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) |
| 729 | } | 714 | } |
| 715 | |||
| 716 | /** | ||
| 717 | * Zips the save files located in the given folder path and creates a new zip file with the current date and time. | ||
| 718 | * @return true if the zip file is successfully created, false otherwise. | ||
| 719 | */ | ||
| 720 | private fun zipSave(): Boolean { | ||
| 721 | try { | ||
| 722 | val tempFolder = File(getPublicFilesDir().canonicalPath, "temp") | ||
| 723 | tempFolder.mkdirs() | ||
| 724 | val saveFolder = File(savesFolderRoot) | ||
| 725 | val outputZipFile = File( | ||
| 726 | tempFolder, | ||
| 727 | "yuzu saves - ${ | ||
| 728 | LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) | ||
| 729 | }.zip" | ||
| 730 | ) | ||
| 731 | outputZipFile.createNewFile() | ||
| 732 | val result = FileUtil.zipFromInternalStorage( | ||
| 733 | saveFolder, | ||
| 734 | savesFolderRoot, | ||
| 735 | BufferedOutputStream(FileOutputStream(outputZipFile)) | ||
| 736 | ) | ||
| 737 | if (result == TaskState.Failed) { | ||
| 738 | return false | ||
| 739 | } | ||
| 740 | lastZipCreated = outputZipFile | ||
| 741 | } catch (e: Exception) { | ||
| 742 | return false | ||
| 743 | } | ||
| 744 | return true | ||
| 745 | } | ||
| 746 | |||
| 747 | /** | ||
| 748 | * Exports the save file located in the given folder path by creating a zip file and sharing it via intent. | ||
| 749 | */ | ||
| 750 | fun exportSave() { | ||
| 751 | CoroutineScope(Dispatchers.IO).launch { | ||
| 752 | val wasZipCreated = zipSave() | ||
| 753 | val lastZipFile = lastZipCreated | ||
| 754 | if (!wasZipCreated || lastZipFile == null) { | ||
| 755 | withContext(Dispatchers.Main) { | ||
| 756 | Toast.makeText( | ||
| 757 | this@MainActivity, | ||
| 758 | getString(R.string.export_save_failed), | ||
| 759 | Toast.LENGTH_LONG | ||
| 760 | ).show() | ||
| 761 | } | ||
| 762 | return@launch | ||
| 763 | } | ||
| 764 | |||
| 765 | withContext(Dispatchers.Main) { | ||
| 766 | val file = DocumentFile.fromSingleUri( | ||
| 767 | this@MainActivity, | ||
| 768 | DocumentsContract.buildDocumentUri( | ||
| 769 | DocumentProvider.AUTHORITY, | ||
| 770 | "${DocumentProvider.ROOT_ID}/temp/${lastZipFile.name}" | ||
| 771 | ) | ||
| 772 | )!! | ||
| 773 | val intent = Intent(Intent.ACTION_SEND) | ||
| 774 | .setDataAndType(file.uri, "application/zip") | ||
| 775 | .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) | ||
| 776 | .putExtra(Intent.EXTRA_STREAM, file.uri) | ||
| 777 | startForResultExportSave.launch( | ||
| 778 | Intent.createChooser( | ||
| 779 | intent, | ||
| 780 | getString(R.string.share_save_file) | ||
| 781 | ) | ||
| 782 | ) | ||
| 783 | } | ||
| 784 | } | ||
| 785 | } | ||
| 786 | |||
| 787 | private val startForResultExportSave = | ||
| 788 | registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ -> | ||
| 789 | File(getPublicFilesDir().canonicalPath, "temp").deleteRecursively() | ||
| 790 | } | ||
| 791 | |||
| 792 | val importSaves = | ||
| 793 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> | ||
| 794 | if (result == null) { | ||
| 795 | return@registerForActivityResult | ||
| 796 | } | ||
| 797 | |||
| 798 | NativeLibrary.initializeEmptyUserDirectory() | ||
| 799 | |||
| 800 | val inputZip = contentResolver.openInputStream(result) | ||
| 801 | // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid. | ||
| 802 | var validZip = false | ||
| 803 | val savesFolder = File(savesFolderRoot) | ||
| 804 | val cacheSaveDir = File("${applicationContext.cacheDir.path}/saves/") | ||
| 805 | cacheSaveDir.mkdir() | ||
| 806 | |||
| 807 | if (inputZip == null) { | ||
| 808 | Toast.makeText( | ||
| 809 | applicationContext, | ||
| 810 | getString(R.string.fatal_error), | ||
| 811 | Toast.LENGTH_LONG | ||
| 812 | ).show() | ||
| 813 | return@registerForActivityResult | ||
| 814 | } | ||
| 815 | |||
| 816 | val filterTitleId = | ||
| 817 | FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) } | ||
| 818 | |||
| 819 | try { | ||
| 820 | CoroutineScope(Dispatchers.IO).launch { | ||
| 821 | FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) | ||
| 822 | cacheSaveDir.list(filterTitleId)?.forEach { savePath -> | ||
| 823 | File(savesFolder, savePath).deleteRecursively() | ||
| 824 | File(cacheSaveDir, savePath).copyRecursively( | ||
| 825 | File(savesFolder, savePath), | ||
| 826 | true | ||
| 827 | ) | ||
| 828 | validZip = true | ||
| 829 | } | ||
| 830 | |||
| 831 | withContext(Dispatchers.Main) { | ||
| 832 | if (!validZip) { | ||
| 833 | MessageDialogFragment.newInstance( | ||
| 834 | this@MainActivity, | ||
| 835 | titleId = R.string.save_file_invalid_zip_structure, | ||
| 836 | descriptionId = R.string.save_file_invalid_zip_structure_description | ||
| 837 | ).show(supportFragmentManager, MessageDialogFragment.TAG) | ||
| 838 | return@withContext | ||
| 839 | } | ||
| 840 | Toast.makeText( | ||
| 841 | applicationContext, | ||
| 842 | getString(R.string.save_file_imported_success), | ||
| 843 | Toast.LENGTH_LONG | ||
| 844 | ).show() | ||
| 845 | } | ||
| 846 | |||
| 847 | cacheSaveDir.deleteRecursively() | ||
| 848 | } | ||
| 849 | } catch (e: Exception) { | ||
| 850 | Toast.makeText( | ||
| 851 | applicationContext, | ||
| 852 | getString(R.string.fatal_error), | ||
| 853 | Toast.LENGTH_LONG | ||
| 854 | ).show() | ||
| 855 | } | ||
| 856 | } | ||
| 730 | } | 857 | } |
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 142af5f26..c3f53f1c5 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 | |||
| @@ -8,6 +8,7 @@ import android.database.Cursor | |||
| 8 | import android.net.Uri | 8 | import android.net.Uri |
| 9 | import android.provider.DocumentsContract | 9 | import android.provider.DocumentsContract |
| 10 | import androidx.documentfile.provider.DocumentFile | 10 | import androidx.documentfile.provider.DocumentFile |
| 11 | import kotlinx.coroutines.flow.StateFlow | ||
| 11 | import java.io.BufferedInputStream | 12 | import java.io.BufferedInputStream |
| 12 | import java.io.File | 13 | import java.io.File |
| 13 | import java.io.FileOutputStream | 14 | import java.io.FileOutputStream |
| @@ -18,6 +19,9 @@ import java.util.zip.ZipEntry | |||
| 18 | import java.util.zip.ZipInputStream | 19 | import java.util.zip.ZipInputStream |
| 19 | import org.yuzu.yuzu_emu.YuzuApplication | 20 | import org.yuzu.yuzu_emu.YuzuApplication |
| 20 | import org.yuzu.yuzu_emu.model.MinimalDocumentFile | 21 | import org.yuzu.yuzu_emu.model.MinimalDocumentFile |
| 22 | import org.yuzu.yuzu_emu.model.TaskState | ||
| 23 | import java.io.BufferedOutputStream | ||
| 24 | import java.util.zip.ZipOutputStream | ||
| 21 | 25 | ||
| 22 | object FileUtil { | 26 | object FileUtil { |
| 23 | const val PATH_TREE = "tree" | 27 | const val PATH_TREE = "tree" |
| @@ -282,30 +286,65 @@ object FileUtil { | |||
| 282 | 286 | ||
| 283 | /** | 287 | /** |
| 284 | * Extracts the given zip file into the given directory. | 288 | * Extracts the given zip file into the given directory. |
| 285 | * @exception IOException if the file was being created outside of the target directory | ||
| 286 | */ | 289 | */ |
| 287 | @Throws(SecurityException::class) | 290 | @Throws(SecurityException::class) |
| 288 | fun unzip(zipStream: InputStream, destDir: File): Boolean { | 291 | fun unzipToInternalStorage(zipStream: BufferedInputStream, destDir: File) { |
| 289 | ZipInputStream(BufferedInputStream(zipStream)).use { zis -> | 292 | ZipInputStream(zipStream).use { zis -> |
| 290 | var entry: ZipEntry? = zis.nextEntry | 293 | var entry: ZipEntry? = zis.nextEntry |
| 291 | while (entry != null) { | 294 | while (entry != null) { |
| 292 | val entryName = entry.name | 295 | val newFile = File(destDir, entry.name) |
| 293 | val entryFile = File(destDir, entryName) | 296 | val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile |
| 294 | if (!entryFile.canonicalPath.startsWith(destDir.canonicalPath + File.separator)) { | 297 | |
| 295 | throw SecurityException("Entry is outside of the target dir: " + entryFile.name) | 298 | if (!newFile.canonicalPath.startsWith(destDir.canonicalPath + File.separator)) { |
| 299 | throw SecurityException("Zip file attempted path traversal! ${entry.name}") | ||
| 300 | } | ||
| 301 | |||
| 302 | if (!destinationDirectory.isDirectory && !destinationDirectory.mkdirs()) { | ||
| 303 | throw IOException("Failed to create directory $destinationDirectory") | ||
| 296 | } | 304 | } |
| 297 | if (entry.isDirectory) { | 305 | |
| 298 | entryFile.mkdirs() | 306 | if (!entry.isDirectory) { |
| 299 | } else { | 307 | newFile.outputStream().use { fos -> zis.copyTo(fos) } |
| 300 | entryFile.parentFile?.mkdirs() | ||
| 301 | entryFile.createNewFile() | ||
| 302 | entryFile.outputStream().use { fos -> zis.copyTo(fos) } | ||
| 303 | } | 308 | } |
| 304 | entry = zis.nextEntry | 309 | entry = zis.nextEntry |
| 305 | } | 310 | } |
| 306 | } | 311 | } |
| 312 | } | ||
| 307 | 313 | ||
| 308 | return true | 314 | /** |
| 315 | * Creates a zip file from a directory within internal storage | ||
| 316 | * @param inputFile File representation of the item that will be zipped | ||
| 317 | * @param rootDir Directory containing the inputFile | ||
| 318 | * @param outputStream Stream where the zip file will be output | ||
| 319 | */ | ||
| 320 | fun zipFromInternalStorage( | ||
| 321 | inputFile: File, | ||
| 322 | rootDir: String, | ||
| 323 | outputStream: BufferedOutputStream, | ||
| 324 | cancelled: StateFlow<Boolean>? = null | ||
| 325 | ): TaskState { | ||
| 326 | try { | ||
| 327 | ZipOutputStream(outputStream).use { zos -> | ||
| 328 | inputFile.walkTopDown().forEach { file -> | ||
| 329 | if (cancelled?.value == true) { | ||
| 330 | return TaskState.Cancelled | ||
| 331 | } | ||
| 332 | |||
| 333 | if (!file.isDirectory) { | ||
| 334 | val entryName = | ||
| 335 | file.absolutePath.removePrefix(rootDir).removePrefix("/") | ||
| 336 | val entry = ZipEntry(entryName) | ||
| 337 | zos.putNextEntry(entry) | ||
| 338 | if (file.isFile) { | ||
| 339 | file.inputStream().use { fis -> fis.copyTo(zos) } | ||
| 340 | } | ||
| 341 | } | ||
| 342 | } | ||
| 343 | } | ||
| 344 | } catch (e: Exception) { | ||
| 345 | return TaskState.Failed | ||
| 346 | } | ||
| 347 | return TaskState.Completed | ||
| 309 | } | 348 | } |
| 310 | 349 | ||
| 311 | fun isRootTreeUri(uri: Uri): Boolean { | 350 | fun isRootTreeUri(uri: Uri): Boolean { |
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index f31fe054b..9cf71680c 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp | |||
| @@ -13,6 +13,8 @@ | |||
| 13 | 13 | ||
| 14 | #include <android/api-level.h> | 14 | #include <android/api-level.h> |
| 15 | #include <android/native_window_jni.h> | 15 | #include <android/native_window_jni.h> |
| 16 | #include <common/fs/fs.h> | ||
| 17 | #include <core/file_sys/savedata_factory.h> | ||
| 16 | #include <core/loader/nro.h> | 18 | #include <core/loader/nro.h> |
| 17 | #include <jni.h> | 19 | #include <jni.h> |
| 18 | 20 | ||
| @@ -102,7 +104,7 @@ public: | |||
| 102 | m_native_window = native_window; | 104 | m_native_window = native_window; |
| 103 | } | 105 | } |
| 104 | 106 | ||
| 105 | int InstallFileToNand(std::string filename) { | 107 | int InstallFileToNand(std::string filename, std::string file_extension) { |
| 106 | jconst copy_func = [](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest, | 108 | jconst copy_func = [](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest, |
| 107 | std::size_t block_size) { | 109 | std::size_t block_size) { |
| 108 | if (src == nullptr || dest == nullptr) { | 110 | if (src == nullptr || dest == nullptr) { |
| @@ -134,15 +136,11 @@ public: | |||
| 134 | m_system.GetFileSystemController().CreateFactories(*m_vfs); | 136 | m_system.GetFileSystemController().CreateFactories(*m_vfs); |
| 135 | 137 | ||
| 136 | [[maybe_unused]] std::shared_ptr<FileSys::NSP> nsp; | 138 | [[maybe_unused]] std::shared_ptr<FileSys::NSP> nsp; |
| 137 | if (filename.ends_with("nsp")) { | 139 | if (file_extension == "nsp") { |
| 138 | nsp = std::make_shared<FileSys::NSP>(m_vfs->OpenFile(filename, FileSys::Mode::Read)); | 140 | nsp = std::make_shared<FileSys::NSP>(m_vfs->OpenFile(filename, FileSys::Mode::Read)); |
| 139 | if (nsp->IsExtractedType()) { | 141 | if (nsp->IsExtractedType()) { |
| 140 | return InstallError; | 142 | return InstallError; |
| 141 | } | 143 | } |
| 142 | } else if (filename.ends_with("xci")) { | ||
| 143 | jconst xci = | ||
| 144 | std::make_shared<FileSys::XCI>(m_vfs->OpenFile(filename, FileSys::Mode::Read)); | ||
| 145 | nsp = xci->GetSecurePartitionNSP(); | ||
| 146 | } else { | 144 | } else { |
| 147 | return ErrorFilenameExtension; | 145 | return ErrorFilenameExtension; |
| 148 | } | 146 | } |
| @@ -607,8 +605,10 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, jobject | |||
| 607 | } | 605 | } |
| 608 | 606 | ||
| 609 | int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject instance, | 607 | int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject instance, |
| 610 | [[maybe_unused]] jstring j_file) { | 608 | jstring j_file, |
| 611 | return EmulationSession::GetInstance().InstallFileToNand(GetJString(env, j_file)); | 609 | jstring j_file_extension) { |
| 610 | return EmulationSession::GetInstance().InstallFileToNand(GetJString(env, j_file), | ||
| 611 | GetJString(env, j_file_extension)); | ||
| 612 | } | 612 | } |
| 613 | 613 | ||
| 614 | void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env, jclass clazz, | 614 | void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env, jclass clazz, |
| @@ -879,4 +879,24 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_submitInlineKeyboardInput(JNIEnv* env | |||
| 879 | EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardInput(j_key_code); | 879 | EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardInput(j_key_code); |
| 880 | } | 880 | } |
| 881 | 881 | ||
| 882 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmptyUserDirectory(JNIEnv* env, | ||
| 883 | jobject instance) { | ||
| 884 | const auto nand_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir); | ||
| 885 | auto vfs_nand_dir = EmulationSession::GetInstance().System().GetFilesystem()->OpenDirectory( | ||
| 886 | Common::FS::PathToUTF8String(nand_dir), FileSys::Mode::Read); | ||
| 887 | |||
| 888 | Service::Account::ProfileManager manager; | ||
| 889 | const auto user_id = manager.GetUser(static_cast<std::size_t>(0)); | ||
| 890 | ASSERT(user_id); | ||
| 891 | |||
| 892 | const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath( | ||
| 893 | EmulationSession::GetInstance().System(), vfs_nand_dir, FileSys::SaveDataSpaceId::NandUser, | ||
| 894 | FileSys::SaveDataType::SaveData, 1, user_id->AsU128(), 0); | ||
| 895 | |||
| 896 | const auto full_path = Common::FS::ConcatPathSafe(nand_dir, user_save_data_path); | ||
| 897 | if (!Common::FS::CreateParentDirs(full_path)) { | ||
| 898 | LOG_WARNING(Frontend, "Failed to create full path of the default user's save directory"); | ||
| 899 | } | ||
| 900 | } | ||
| 901 | |||
| 882 | } // extern "C" | 902 | } // extern "C" |
diff --git a/src/android/app/src/main/res/layout/activity_settings.xml b/src/android/app/src/main/res/layout/activity_settings.xml index 8a026a30a..a187665f2 100644 --- a/src/android/app/src/main/res/layout/activity_settings.xml +++ b/src/android/app/src/main/res/layout/activity_settings.xml | |||
| @@ -22,7 +22,7 @@ | |||
| 22 | 22 | ||
| 23 | <View | 23 | <View |
| 24 | android:id="@+id/navigation_bar_shade" | 24 | android:id="@+id/navigation_bar_shade" |
| 25 | android:layout_width="match_parent" | 25 | android:layout_width="0dp" |
| 26 | android:layout_height="1px" | 26 | android:layout_height="1px" |
| 27 | android:background="@android:color/transparent" | 27 | android:background="@android:color/transparent" |
| 28 | android:clickable="false" | 28 | android:clickable="false" |
diff --git a/src/android/app/src/main/res/layout/card_installable.xml b/src/android/app/src/main/res/layout/card_installable.xml new file mode 100644 index 000000000..f5b0e3741 --- /dev/null +++ b/src/android/app/src/main/res/layout/card_installable.xml | |||
| @@ -0,0 +1,71 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| 4 | xmlns:tools="http://schemas.android.com/tools" | ||
| 5 | style="?attr/materialCardViewOutlinedStyle" | ||
| 6 | android:layout_width="match_parent" | ||
| 7 | android:layout_height="wrap_content" | ||
| 8 | android:layout_marginHorizontal="16dp" | ||
| 9 | android:layout_marginVertical="12dp"> | ||
| 10 | |||
| 11 | <LinearLayout | ||
| 12 | android:layout_width="match_parent" | ||
| 13 | android:layout_height="wrap_content" | ||
| 14 | android:layout_margin="16dp" | ||
| 15 | android:orientation="horizontal" | ||
| 16 | android:layout_gravity="center"> | ||
| 17 | |||
| 18 | <LinearLayout | ||
| 19 | android:layout_width="0dp" | ||
| 20 | android:layout_height="wrap_content" | ||
| 21 | android:layout_marginEnd="16dp" | ||
| 22 | android:layout_weight="1" | ||
| 23 | android:orientation="vertical"> | ||
| 24 | |||
| 25 | <com.google.android.material.textview.MaterialTextView | ||
| 26 | android:id="@+id/title" | ||
| 27 | style="@style/TextAppearance.Material3.TitleMedium" | ||
| 28 | android:layout_width="match_parent" | ||
| 29 | android:layout_height="wrap_content" | ||
| 30 | android:text="@string/user_data" | ||
| 31 | android:textAlignment="viewStart" /> | ||
| 32 | |||
| 33 | <com.google.android.material.textview.MaterialTextView | ||
| 34 | android:id="@+id/description" | ||
| 35 | style="@style/TextAppearance.Material3.BodyMedium" | ||
| 36 | android:layout_width="match_parent" | ||
| 37 | android:layout_height="wrap_content" | ||
| 38 | android:layout_marginTop="6dp" | ||
| 39 | android:text="@string/user_data_description" | ||
| 40 | android:textAlignment="viewStart" /> | ||
| 41 | |||
| 42 | </LinearLayout> | ||
| 43 | |||
| 44 | <Button | ||
| 45 | android:id="@+id/button_export" | ||
| 46 | style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" | ||
| 47 | android:layout_width="wrap_content" | ||
| 48 | android:layout_height="wrap_content" | ||
| 49 | android:layout_gravity="center_vertical" | ||
| 50 | android:contentDescription="@string/export" | ||
| 51 | android:tooltipText="@string/export" | ||
| 52 | android:visibility="gone" | ||
| 53 | app:icon="@drawable/ic_export" | ||
| 54 | tools:visibility="visible" /> | ||
| 55 | |||
| 56 | <Button | ||
| 57 | android:id="@+id/button_install" | ||
| 58 | style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" | ||
| 59 | android:layout_width="wrap_content" | ||
| 60 | android:layout_height="wrap_content" | ||
| 61 | android:layout_gravity="center_vertical" | ||
| 62 | android:layout_marginStart="12dp" | ||
| 63 | android:contentDescription="@string/string_import" | ||
| 64 | android:tooltipText="@string/string_import" | ||
| 65 | android:visibility="gone" | ||
| 66 | app:icon="@drawable/ic_import" | ||
| 67 | tools:visibility="visible" /> | ||
| 68 | |||
| 69 | </LinearLayout> | ||
| 70 | |||
| 71 | </com.google.android.material.card.MaterialCardView> | ||
diff --git a/src/android/app/src/main/res/layout/fragment_about.xml b/src/android/app/src/main/res/layout/fragment_about.xml index 36b350338..3e1d98451 100644 --- a/src/android/app/src/main/res/layout/fragment_about.xml +++ b/src/android/app/src/main/res/layout/fragment_about.xml | |||
| @@ -184,67 +184,6 @@ | |||
| 184 | <LinearLayout | 184 | <LinearLayout |
| 185 | android:layout_width="match_parent" | 185 | android:layout_width="match_parent" |
| 186 | android:layout_height="wrap_content" | 186 | android:layout_height="wrap_content" |
| 187 | android:orientation="horizontal"> | ||
| 188 | |||
| 189 | <LinearLayout | ||
| 190 | android:layout_width="match_parent" | ||
| 191 | android:layout_height="wrap_content" | ||
| 192 | android:paddingVertical="16dp" | ||
| 193 | android:paddingHorizontal="16dp" | ||
| 194 | android:orientation="vertical" | ||
| 195 | android:layout_weight="1"> | ||
| 196 | |||
| 197 | <com.google.android.material.textview.MaterialTextView | ||
| 198 | style="@style/TextAppearance.Material3.TitleMedium" | ||
| 199 | android:layout_width="match_parent" | ||
| 200 | android:layout_height="wrap_content" | ||
| 201 | android:layout_marginHorizontal="24dp" | ||
| 202 | android:textAlignment="viewStart" | ||
| 203 | android:text="@string/user_data" /> | ||
| 204 | |||
| 205 | <com.google.android.material.textview.MaterialTextView | ||
| 206 | style="@style/TextAppearance.Material3.BodyMedium" | ||
| 207 | android:layout_width="match_parent" | ||
| 208 | android:layout_height="wrap_content" | ||
| 209 | android:layout_marginHorizontal="24dp" | ||
| 210 | android:layout_marginTop="6dp" | ||
| 211 | android:textAlignment="viewStart" | ||
| 212 | android:text="@string/user_data_description" /> | ||
| 213 | |||
| 214 | </LinearLayout> | ||
| 215 | |||
| 216 | <Button | ||
| 217 | android:id="@+id/button_import" | ||
| 218 | style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" | ||
| 219 | android:layout_width="wrap_content" | ||
| 220 | android:layout_height="wrap_content" | ||
| 221 | android:layout_gravity="center_vertical" | ||
| 222 | android:contentDescription="@string/string_import" | ||
| 223 | android:tooltipText="@string/string_import" | ||
| 224 | app:icon="@drawable/ic_import" /> | ||
| 225 | |||
| 226 | <Button | ||
| 227 | android:id="@+id/button_export" | ||
| 228 | style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" | ||
| 229 | android:layout_width="wrap_content" | ||
| 230 | android:layout_height="wrap_content" | ||
| 231 | android:layout_marginStart="12dp" | ||
| 232 | android:layout_marginEnd="24dp" | ||
| 233 | android:layout_gravity="center_vertical" | ||
| 234 | android:contentDescription="@string/export" | ||
| 235 | android:tooltipText="@string/export" | ||
| 236 | app:icon="@drawable/ic_export" /> | ||
| 237 | |||
| 238 | </LinearLayout> | ||
| 239 | |||
| 240 | <com.google.android.material.divider.MaterialDivider | ||
| 241 | android:layout_width="match_parent" | ||
| 242 | android:layout_height="wrap_content" | ||
| 243 | android:layout_marginHorizontal="20dp" /> | ||
| 244 | |||
| 245 | <LinearLayout | ||
| 246 | android:layout_width="match_parent" | ||
| 247 | android:layout_height="wrap_content" | ||
| 248 | android:orientation="horizontal" | 187 | android:orientation="horizontal" |
| 249 | android:gravity="center_horizontal" | 188 | android:gravity="center_horizontal" |
| 250 | android:layout_marginTop="12dp" | 189 | android:layout_marginTop="12dp" |
diff --git a/src/android/app/src/main/res/layout/fragment_emulation.xml b/src/android/app/src/main/res/layout/fragment_emulation.xml index da97d85c1..750ce094a 100644 --- a/src/android/app/src/main/res/layout/fragment_emulation.xml +++ b/src/android/app/src/main/res/layout/fragment_emulation.xml | |||
| @@ -32,7 +32,8 @@ | |||
| 32 | android:layout_width="wrap_content" | 32 | android:layout_width="wrap_content" |
| 33 | android:layout_height="wrap_content" | 33 | android:layout_height="wrap_content" |
| 34 | android:layout_gravity="center" | 34 | android:layout_gravity="center" |
| 35 | android:focusable="false"> | 35 | android:focusable="false" |
| 36 | android:clickable="false"> | ||
| 36 | 37 | ||
| 37 | <androidx.constraintlayout.widget.ConstraintLayout | 38 | <androidx.constraintlayout.widget.ConstraintLayout |
| 38 | android:id="@+id/loading_layout" | 39 | android:id="@+id/loading_layout" |
| @@ -155,7 +156,7 @@ | |||
| 155 | android:id="@+id/in_game_menu" | 156 | android:id="@+id/in_game_menu" |
| 156 | android:layout_width="wrap_content" | 157 | android:layout_width="wrap_content" |
| 157 | android:layout_height="match_parent" | 158 | android:layout_height="match_parent" |
| 158 | android:layout_gravity="start|bottom" | 159 | android:layout_gravity="start" |
| 159 | app:headerLayout="@layout/header_in_game" | 160 | app:headerLayout="@layout/header_in_game" |
| 160 | app:menu="@menu/menu_in_game" | 161 | app:menu="@menu/menu_in_game" |
| 161 | tools:visibility="gone" /> | 162 | tools:visibility="gone" /> |
diff --git a/src/android/app/src/main/res/layout/fragment_installables.xml b/src/android/app/src/main/res/layout/fragment_installables.xml new file mode 100644 index 000000000..3a4df81a6 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_installables.xml | |||
| @@ -0,0 +1,31 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| 4 | android:id="@+id/coordinator_licenses" | ||
| 5 | android:layout_width="match_parent" | ||
| 6 | android:layout_height="match_parent" | ||
| 7 | android:background="?attr/colorSurface"> | ||
| 8 | |||
| 9 | <com.google.android.material.appbar.AppBarLayout | ||
| 10 | android:id="@+id/appbar_installables" | ||
| 11 | android:layout_width="match_parent" | ||
| 12 | android:layout_height="wrap_content" | ||
| 13 | android:fitsSystemWindows="true"> | ||
| 14 | |||
| 15 | <com.google.android.material.appbar.MaterialToolbar | ||
| 16 | android:id="@+id/toolbar_installables" | ||
| 17 | android:layout_width="match_parent" | ||
| 18 | android:layout_height="?attr/actionBarSize" | ||
| 19 | app:title="@string/manage_yuzu_data" | ||
| 20 | app:navigationIcon="@drawable/ic_back" /> | ||
| 21 | |||
| 22 | </com.google.android.material.appbar.AppBarLayout> | ||
| 23 | |||
| 24 | <androidx.recyclerview.widget.RecyclerView | ||
| 25 | android:id="@+id/list_installables" | ||
| 26 | android:layout_width="match_parent" | ||
| 27 | android:layout_height="match_parent" | ||
| 28 | android:clipToPadding="false" | ||
| 29 | app:layout_behavior="@string/appbar_scrolling_view_behavior" /> | ||
| 30 | |||
| 31 | </androidx.coordinatorlayout.widget.CoordinatorLayout> | ||
diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml index 2e0ce7a3d..2356b802b 100644 --- a/src/android/app/src/main/res/navigation/home_navigation.xml +++ b/src/android/app/src/main/res/navigation/home_navigation.xml | |||
| @@ -19,6 +19,9 @@ | |||
| 19 | <action | 19 | <action |
| 20 | android:id="@+id/action_homeSettingsFragment_to_earlyAccessFragment" | 20 | android:id="@+id/action_homeSettingsFragment_to_earlyAccessFragment" |
| 21 | app:destination="@id/earlyAccessFragment" /> | 21 | app:destination="@id/earlyAccessFragment" /> |
| 22 | <action | ||
| 23 | android:id="@+id/action_homeSettingsFragment_to_installableFragment" | ||
| 24 | app:destination="@id/installableFragment" /> | ||
| 22 | </fragment> | 25 | </fragment> |
| 23 | 26 | ||
| 24 | <fragment | 27 | <fragment |
| @@ -88,5 +91,9 @@ | |||
| 88 | <action | 91 | <action |
| 89 | android:id="@+id/action_global_settingsActivity" | 92 | android:id="@+id/action_global_settingsActivity" |
| 90 | app:destination="@id/settingsActivity" /> | 93 | app:destination="@id/settingsActivity" /> |
| 94 | <fragment | ||
| 95 | android:id="@+id/installableFragment" | ||
| 96 | android:name="org.yuzu.yuzu_emu.fragments.InstallableFragment" | ||
| 97 | android:label="InstallableFragment" /> | ||
| 91 | 98 | ||
| 92 | </navigation> | 99 | </navigation> |
diff --git a/src/android/app/src/main/res/values-de/strings.xml b/src/android/app/src/main/res/values-de/strings.xml index daaa7ffde..dd0f36392 100644 --- a/src/android/app/src/main/res/values-de/strings.xml +++ b/src/android/app/src/main/res/values-de/strings.xml | |||
| @@ -79,7 +79,6 @@ | |||
| 79 | <string name="manage_save_data">Speicherdaten verwalten</string> | 79 | <string name="manage_save_data">Speicherdaten verwalten</string> |
| 80 | <string name="manage_save_data_description">Speicherdaten gefunden. Bitte wähle unten eine Option aus.</string> | 80 | <string name="manage_save_data_description">Speicherdaten gefunden. Bitte wähle unten eine Option aus.</string> |
| 81 | <string name="import_export_saves_description">Speicherdaten importieren oder exportieren</string> | 81 | <string name="import_export_saves_description">Speicherdaten importieren oder exportieren</string> |
| 82 | <string name="import_export_saves_no_profile">Keine Speicherdaten gefunden. Bitte starte ein Spiel und versuche es erneut.</string> | ||
| 83 | <string name="save_file_imported_success">Erfolgreich importiert</string> | 82 | <string name="save_file_imported_success">Erfolgreich importiert</string> |
| 84 | <string name="save_file_invalid_zip_structure">Ungültige Speicherverzeichnisstruktur</string> | 83 | <string name="save_file_invalid_zip_structure">Ungültige Speicherverzeichnisstruktur</string> |
| 85 | <string name="save_file_invalid_zip_structure_description">Der erste Unterordnername muss die Titel-ID des Spiels sein.</string> | 84 | <string name="save_file_invalid_zip_structure_description">Der erste Unterordnername muss die Titel-ID des Spiels sein.</string> |
diff --git a/src/android/app/src/main/res/values-es/strings.xml b/src/android/app/src/main/res/values-es/strings.xml index e9129cb00..d398f862f 100644 --- a/src/android/app/src/main/res/values-es/strings.xml +++ b/src/android/app/src/main/res/values-es/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">Administrar datos de guardado</string> | 81 | <string name="manage_save_data">Administrar datos de guardado</string> |
| 82 | <string name="manage_save_data_description">Guardar los datos encontrados. Por favor, seleccione una opción de abajo.</string> | 82 | <string name="manage_save_data_description">Guardar los datos encontrados. Por favor, seleccione una opción de abajo.</string> |
| 83 | <string name="import_export_saves_description">Importar o exportar archivos de guardado</string> | 83 | <string name="import_export_saves_description">Importar o exportar archivos de guardado</string> |
| 84 | <string name="import_export_saves_no_profile">No se han encontrado datos de guardado. Por favor, ejecute un juego y vuelva a intentarlo.</string> | ||
| 85 | <string name="save_file_imported_success">Importado correctamente</string> | 84 | <string name="save_file_imported_success">Importado correctamente</string> |
| 86 | <string name="save_file_invalid_zip_structure">Estructura del directorio de guardado no válido</string> | 85 | <string name="save_file_invalid_zip_structure">Estructura del directorio de guardado no válido</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">El nombre de la primera subcarpeta debe ser el Title ID del juego.</string> | 86 | <string name="save_file_invalid_zip_structure_description">El nombre de la primera subcarpeta debe ser el Title ID del juego.</string> |
diff --git a/src/android/app/src/main/res/values-fr/strings.xml b/src/android/app/src/main/res/values-fr/strings.xml index 2d99d618e..a7abd9077 100644 --- a/src/android/app/src/main/res/values-fr/strings.xml +++ b/src/android/app/src/main/res/values-fr/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">Gérer les données de sauvegarde</string> | 81 | <string name="manage_save_data">Gérer les données de sauvegarde</string> |
| 82 | <string name="manage_save_data_description">Données de sauvegarde trouvées. Veuillez sélectionner une option ci-dessous.</string> | 82 | <string name="manage_save_data_description">Données de sauvegarde trouvées. Veuillez sélectionner une option ci-dessous.</string> |
| 83 | <string name="import_export_saves_description">Importer ou exporter des fichiers de sauvegarde</string> | 83 | <string name="import_export_saves_description">Importer ou exporter des fichiers de sauvegarde</string> |
| 84 | <string name="import_export_saves_no_profile">Aucune données de sauvegarde trouvées. Veuillez lancer un jeu et réessayer.</string> | ||
| 85 | <string name="save_file_imported_success">Importé avec succès</string> | 84 | <string name="save_file_imported_success">Importé avec succès</string> |
| 86 | <string name="save_file_invalid_zip_structure">Structure de répertoire de sauvegarde non valide</string> | 85 | <string name="save_file_invalid_zip_structure">Structure de répertoire de sauvegarde non valide</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">Le nom du premier sous-dossier doit être l\'identifiant du titre du jeu.</string> | 86 | <string name="save_file_invalid_zip_structure_description">Le nom du premier sous-dossier doit être l\'identifiant du titre du jeu.</string> |
diff --git a/src/android/app/src/main/res/values-it/strings.xml b/src/android/app/src/main/res/values-it/strings.xml index d9c3de385..b18161801 100644 --- a/src/android/app/src/main/res/values-it/strings.xml +++ b/src/android/app/src/main/res/values-it/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">Gestisci i salvataggi</string> | 81 | <string name="manage_save_data">Gestisci i salvataggi</string> |
| 82 | <string name="manage_save_data_description">Salvataggio non trovato. Seleziona un\'opzione di seguito.</string> | 82 | <string name="manage_save_data_description">Salvataggio non trovato. Seleziona un\'opzione di seguito.</string> |
| 83 | <string name="import_export_saves_description">Importa o esporta i salvataggi</string> | 83 | <string name="import_export_saves_description">Importa o esporta i salvataggi</string> |
| 84 | <string name="import_export_saves_no_profile">Nessun salvataggio trovato. Avvia un gioco e riprova.</string> | ||
| 85 | <string name="save_file_imported_success">Importato con successo</string> | 84 | <string name="save_file_imported_success">Importato con successo</string> |
| 86 | <string name="save_file_invalid_zip_structure">La struttura della cartella dei salvataggi è invalida</string> | 85 | <string name="save_file_invalid_zip_structure">La struttura della cartella dei salvataggi è invalida</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">La prima sotto cartella <b>deve</b> chiamarsi come l\'ID del titolo del gioco.</string> | 86 | <string name="save_file_invalid_zip_structure_description">La prima sotto cartella <b>deve</b> chiamarsi come l\'ID del titolo del gioco.</string> |
diff --git a/src/android/app/src/main/res/values-ja/strings.xml b/src/android/app/src/main/res/values-ja/strings.xml index 7a226cd5c..88fa5a0bb 100644 --- a/src/android/app/src/main/res/values-ja/strings.xml +++ b/src/android/app/src/main/res/values-ja/strings.xml | |||
| @@ -80,7 +80,6 @@ | |||
| 80 | <string name="manage_save_data">セーブデータを管理</string> | 80 | <string name="manage_save_data">セーブデータを管理</string> |
| 81 | <string name="manage_save_data_description">セーブデータが見つかりました。以下のオプションから選択してください。</string> | 81 | <string name="manage_save_data_description">セーブデータが見つかりました。以下のオプションから選択してください。</string> |
| 82 | <string name="import_export_saves_description">セーブファイルをインポート/エクスポート</string> | 82 | <string name="import_export_saves_description">セーブファイルをインポート/エクスポート</string> |
| 83 | <string name="import_export_saves_no_profile">セーブデータがありません。ゲームを起動してから再度お試しください。</string> | ||
| 84 | <string name="save_file_imported_success">インポートが完了しました</string> | 83 | <string name="save_file_imported_success">インポートが完了しました</string> |
| 85 | <string name="save_file_invalid_zip_structure">セーブデータのディレクトリ構造が無効です</string> | 84 | <string name="save_file_invalid_zip_structure">セーブデータのディレクトリ構造が無効です</string> |
| 86 | <string name="save_file_invalid_zip_structure_description">最初のサブフォルダ名は、ゲームのタイトルIDである必要があります。</string> | 85 | <string name="save_file_invalid_zip_structure_description">最初のサブフォルダ名は、ゲームのタイトルIDである必要があります。</string> |
diff --git a/src/android/app/src/main/res/values-ko/strings.xml b/src/android/app/src/main/res/values-ko/strings.xml index 427b6e5a0..4b658255c 100644 --- a/src/android/app/src/main/res/values-ko/strings.xml +++ b/src/android/app/src/main/res/values-ko/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">저장 데이터 관리</string> | 81 | <string name="manage_save_data">저장 데이터 관리</string> |
| 82 | <string name="manage_save_data_description">데이터를 저장했습니다. 아래에서 옵션을 선택하세요.</string> | 82 | <string name="manage_save_data_description">데이터를 저장했습니다. 아래에서 옵션을 선택하세요.</string> |
| 83 | <string name="import_export_saves_description">저장 파일 가져오기 또는 내보내기</string> | 83 | <string name="import_export_saves_description">저장 파일 가져오기 또는 내보내기</string> |
| 84 | <string name="import_export_saves_no_profile">저장 데이터를 찾을 수 없습니다. 게임을 실행한 후 다시 시도하세요.</string> | ||
| 85 | <string name="save_file_imported_success">가져오기 성공</string> | 84 | <string name="save_file_imported_success">가져오기 성공</string> |
| 86 | <string name="save_file_invalid_zip_structure">저장 디렉터리 구조가 잘못됨</string> | 85 | <string name="save_file_invalid_zip_structure">저장 디렉터리 구조가 잘못됨</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">첫 번째 하위 폴더 이름은 게임의 타이틀 ID여야 합니다.</string> | 86 | <string name="save_file_invalid_zip_structure_description">첫 번째 하위 폴더 이름은 게임의 타이틀 ID여야 합니다.</string> |
diff --git a/src/android/app/src/main/res/values-nb/strings.xml b/src/android/app/src/main/res/values-nb/strings.xml index ce8d7a9e4..dd602a389 100644 --- a/src/android/app/src/main/res/values-nb/strings.xml +++ b/src/android/app/src/main/res/values-nb/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">Administrere lagringsdata</string> | 81 | <string name="manage_save_data">Administrere lagringsdata</string> |
| 82 | <string name="manage_save_data_description">Lagringsdata funnet. Velg et alternativ nedenfor.</string> | 82 | <string name="manage_save_data_description">Lagringsdata funnet. Velg et alternativ nedenfor.</string> |
| 83 | <string name="import_export_saves_description">Importer eller eksporter lagringsfiler</string> | 83 | <string name="import_export_saves_description">Importer eller eksporter lagringsfiler</string> |
| 84 | <string name="import_export_saves_no_profile">Ingen lagringsdata funnet. Start et nytt spill og prøv på nytt.</string> | ||
| 85 | <string name="save_file_imported_success">Vellykket import</string> | 84 | <string name="save_file_imported_success">Vellykket import</string> |
| 86 | <string name="save_file_invalid_zip_structure">Ugyldig struktur for lagringskatalog</string> | 85 | <string name="save_file_invalid_zip_structure">Ugyldig struktur for lagringskatalog</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">Det første undermappenavnet må være spillets tittel-ID.</string> | 86 | <string name="save_file_invalid_zip_structure_description">Det første undermappenavnet må være spillets tittel-ID.</string> |
diff --git a/src/android/app/src/main/res/values-pl/strings.xml b/src/android/app/src/main/res/values-pl/strings.xml index c2c24b48f..2fdd1f952 100644 --- a/src/android/app/src/main/res/values-pl/strings.xml +++ b/src/android/app/src/main/res/values-pl/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">Zarządzaj plikami zapisów gier</string> | 81 | <string name="manage_save_data">Zarządzaj plikami zapisów gier</string> |
| 82 | <string name="manage_save_data_description">Znaleziono pliki zapisów gier. Wybierz opcję poniżej.</string> | 82 | <string name="manage_save_data_description">Znaleziono pliki zapisów gier. Wybierz opcję poniżej.</string> |
| 83 | <string name="import_export_saves_description">Importuj lub wyeksportuj pliki zapisów</string> | 83 | <string name="import_export_saves_description">Importuj lub wyeksportuj pliki zapisów</string> |
| 84 | <string name="import_export_saves_no_profile">Nie znaleziono plików zapisów. Uruchom grę i spróbuj ponownie.</string> | ||
| 85 | <string name="save_file_imported_success">Zaimportowano pomyślnie</string> | 84 | <string name="save_file_imported_success">Zaimportowano pomyślnie</string> |
| 86 | <string name="save_file_invalid_zip_structure">Niepoprawna struktura folderów</string> | 85 | <string name="save_file_invalid_zip_structure">Niepoprawna struktura folderów</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">Pierwszy podkatalog musi zawierać w nazwie numer ID tytułu gry.</string> | 86 | <string name="save_file_invalid_zip_structure_description">Pierwszy podkatalog musi zawierać w nazwie numer ID tytułu gry.</string> |
diff --git a/src/android/app/src/main/res/values-pt-rBR/strings.xml b/src/android/app/src/main/res/values-pt-rBR/strings.xml index 04f276108..2f26367fe 100644 --- a/src/android/app/src/main/res/values-pt-rBR/strings.xml +++ b/src/android/app/src/main/res/values-pt-rBR/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">Gerir dados guardados</string> | 81 | <string name="manage_save_data">Gerir dados guardados</string> |
| 82 | <string name="manage_save_data_description">Dados não encontrados. Por favor seleciona uma opção abaixo.</string> | 82 | <string name="manage_save_data_description">Dados não encontrados. Por favor seleciona uma opção abaixo.</string> |
| 83 | <string name="import_export_saves_description">Importa ou exporta dados guardados</string> | 83 | <string name="import_export_saves_description">Importa ou exporta dados guardados</string> |
| 84 | <string name="import_export_saves_no_profile">Dados não encontrados. Por favor lança o jogo e tenta novamente.</string> | ||
| 85 | <string name="save_file_imported_success">Importado com sucesso</string> | 84 | <string name="save_file_imported_success">Importado com sucesso</string> |
| 86 | <string name="save_file_invalid_zip_structure">Estrutura de diretório de dados invalida</string> | 85 | <string name="save_file_invalid_zip_structure">Estrutura de diretório de dados invalida</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">O nome da primeira sub pasta tem de ser a ID do jogo.</string> | 86 | <string name="save_file_invalid_zip_structure_description">O nome da primeira sub pasta tem de ser a ID do jogo.</string> |
diff --git a/src/android/app/src/main/res/values-pt-rPT/strings.xml b/src/android/app/src/main/res/values-pt-rPT/strings.xml index 66a3a1a2e..4e1eb4cd7 100644 --- a/src/android/app/src/main/res/values-pt-rPT/strings.xml +++ b/src/android/app/src/main/res/values-pt-rPT/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">Gerir dados guardados</string> | 81 | <string name="manage_save_data">Gerir dados guardados</string> |
| 82 | <string name="manage_save_data_description">Dados não encontrados. Por favor seleciona uma opção abaixo.</string> | 82 | <string name="manage_save_data_description">Dados não encontrados. Por favor seleciona uma opção abaixo.</string> |
| 83 | <string name="import_export_saves_description">Importa ou exporta dados guardados</string> | 83 | <string name="import_export_saves_description">Importa ou exporta dados guardados</string> |
| 84 | <string name="import_export_saves_no_profile">Dados não encontrados. Por favor lança o jogo e tenta novamente.</string> | ||
| 85 | <string name="save_file_imported_success">Importado com sucesso</string> | 84 | <string name="save_file_imported_success">Importado com sucesso</string> |
| 86 | <string name="save_file_invalid_zip_structure">Estrutura de diretório de dados invalida</string> | 85 | <string name="save_file_invalid_zip_structure">Estrutura de diretório de dados invalida</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">O nome da primeira sub pasta tem de ser a ID do jogo.</string> | 86 | <string name="save_file_invalid_zip_structure_description">O nome da primeira sub pasta tem de ser a ID do jogo.</string> |
diff --git a/src/android/app/src/main/res/values-ru/strings.xml b/src/android/app/src/main/res/values-ru/strings.xml index f770e954f..f5695dc93 100644 --- a/src/android/app/src/main/res/values-ru/strings.xml +++ b/src/android/app/src/main/res/values-ru/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">Управление данными сохранений</string> | 81 | <string name="manage_save_data">Управление данными сохранений</string> |
| 82 | <string name="manage_save_data_description">Найдено данные сохранений. Пожалуйста, выберите вариант ниже.</string> | 82 | <string name="manage_save_data_description">Найдено данные сохранений. Пожалуйста, выберите вариант ниже.</string> |
| 83 | <string name="import_export_saves_description">Импорт или экспорт файлов сохранения</string> | 83 | <string name="import_export_saves_description">Импорт или экспорт файлов сохранения</string> |
| 84 | <string name="import_export_saves_no_profile">Данные сохранений не найдены. Пожалуйста, запустите игру и повторите попытку.</string> | ||
| 85 | <string name="save_file_imported_success">Успешно импортировано</string> | 84 | <string name="save_file_imported_success">Успешно импортировано</string> |
| 86 | <string name="save_file_invalid_zip_structure">Недопустимая структура папки сохранения</string> | 85 | <string name="save_file_invalid_zip_structure">Недопустимая структура папки сохранения</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">Название первой вложенной папки должно быть идентификатором игры.</string> | 86 | <string name="save_file_invalid_zip_structure_description">Название первой вложенной папки должно быть идентификатором игры.</string> |
diff --git a/src/android/app/src/main/res/values-uk/strings.xml b/src/android/app/src/main/res/values-uk/strings.xml index ea3ab1b15..061bc6f04 100644 --- a/src/android/app/src/main/res/values-uk/strings.xml +++ b/src/android/app/src/main/res/values-uk/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">Керування даними збережень</string> | 81 | <string name="manage_save_data">Керування даними збережень</string> |
| 82 | <string name="manage_save_data_description">Знайдено дані збережень. Будь ласка, виберіть варіант нижче.</string> | 82 | <string name="manage_save_data_description">Знайдено дані збережень. Будь ласка, виберіть варіант нижче.</string> |
| 83 | <string name="import_export_saves_description">Імпорт або експорт файлів збереження</string> | 83 | <string name="import_export_saves_description">Імпорт або експорт файлів збереження</string> |
| 84 | <string name="import_export_saves_no_profile">Дані збережень не знайдено. Будь ласка, запустіть гру та повторіть спробу.</string> | ||
| 85 | <string name="save_file_imported_success">Успішно імпортовано</string> | 84 | <string name="save_file_imported_success">Успішно імпортовано</string> |
| 86 | <string name="save_file_invalid_zip_structure">Неприпустима структура папки збереження</string> | 85 | <string name="save_file_invalid_zip_structure">Неприпустима структура папки збереження</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">Назва першої вкладеної папки має бути ідентифікатором гри.</string> | 86 | <string name="save_file_invalid_zip_structure_description">Назва першої вкладеної папки має бути ідентифікатором гри.</string> |
diff --git a/src/android/app/src/main/res/values-w600dp/integers.xml b/src/android/app/src/main/res/values-w600dp/integers.xml new file mode 100644 index 000000000..9975db801 --- /dev/null +++ b/src/android/app/src/main/res/values-w600dp/integers.xml | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <resources> | ||
| 3 | |||
| 4 | <integer name="grid_columns">2</integer> | ||
| 5 | |||
| 6 | </resources> | ||
diff --git a/src/android/app/src/main/res/values-zh-rCN/strings.xml b/src/android/app/src/main/res/values-zh-rCN/strings.xml index b45a5a528..fe6dd5eaa 100644 --- a/src/android/app/src/main/res/values-zh-rCN/strings.xml +++ b/src/android/app/src/main/res/values-zh-rCN/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">管理存档数据</string> | 81 | <string name="manage_save_data">管理存档数据</string> |
| 82 | <string name="manage_save_data_description">已找到存档数据,请选择下方的选项。</string> | 82 | <string name="manage_save_data_description">已找到存档数据,请选择下方的选项。</string> |
| 83 | <string name="import_export_saves_description">导入或导出存档</string> | 83 | <string name="import_export_saves_description">导入或导出存档</string> |
| 84 | <string name="import_export_saves_no_profile">找不到存档数据,请启动游戏并重试。</string> | ||
| 85 | <string name="save_file_imported_success">已成功导入存档</string> | 84 | <string name="save_file_imported_success">已成功导入存档</string> |
| 86 | <string name="save_file_invalid_zip_structure">无效的存档目录</string> | 85 | <string name="save_file_invalid_zip_structure">无效的存档目录</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">第一个子文件夹名称必须为当前游戏的 ID。</string> | 86 | <string name="save_file_invalid_zip_structure_description">第一个子文件夹名称必须为当前游戏的 ID。</string> |
diff --git a/src/android/app/src/main/res/values-zh-rTW/strings.xml b/src/android/app/src/main/res/values-zh-rTW/strings.xml index 3aab889e4..9b3e54224 100644 --- a/src/android/app/src/main/res/values-zh-rTW/strings.xml +++ b/src/android/app/src/main/res/values-zh-rTW/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">管理儲存資料</string> | 81 | <string name="manage_save_data">管理儲存資料</string> |
| 82 | <string name="manage_save_data_description">已找到儲存資料,請選取下方的選項。</string> | 82 | <string name="manage_save_data_description">已找到儲存資料,請選取下方的選項。</string> |
| 83 | <string name="import_export_saves_description">匯入或匯出儲存檔案</string> | 83 | <string name="import_export_saves_description">匯入或匯出儲存檔案</string> |
| 84 | <string name="import_export_saves_no_profile">找不到儲存資料,請啟動遊戲並重試。</string> | ||
| 85 | <string name="save_file_imported_success">已成功匯入</string> | 84 | <string name="save_file_imported_success">已成功匯入</string> |
| 86 | <string name="save_file_invalid_zip_structure">無效的儲存目錄結構</string> | 85 | <string name="save_file_invalid_zip_structure">無效的儲存目錄結構</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">首個子資料夾名稱必須為遊戲標題 ID。</string> | 86 | <string name="save_file_invalid_zip_structure_description">首個子資料夾名稱必須為遊戲標題 ID。</string> |
diff --git a/src/android/app/src/main/res/values/integers.xml b/src/android/app/src/main/res/values/integers.xml index 5e39bc7d9..dc527965c 100644 --- a/src/android/app/src/main/res/values/integers.xml +++ b/src/android/app/src/main/res/values/integers.xml | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | 1 | <?xml version="1.0" encoding="utf-8"?> |
| 2 | <resources> | 2 | <resources> |
| 3 | <integer name="game_title_lines">2</integer> | 3 | <integer name="grid_columns">1</integer> |
| 4 | 4 | ||
| 5 | <!-- Default SWITCH landscape layout --> | 5 | <!-- Default SWITCH landscape layout --> |
| 6 | <integer name="SWITCH_BUTTON_A_X">760</integer> | 6 | <integer name="SWITCH_BUTTON_A_X">760</integer> |
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 0730143bd..e51edf872 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml | |||
| @@ -90,7 +90,6 @@ | |||
| 90 | <string name="manage_save_data">Manage save data</string> | 90 | <string name="manage_save_data">Manage save data</string> |
| 91 | <string name="manage_save_data_description">Save data found. Please select an option below.</string> | 91 | <string name="manage_save_data_description">Save data found. Please select an option below.</string> |
| 92 | <string name="import_export_saves_description">Import or export save files</string> | 92 | <string name="import_export_saves_description">Import or export save files</string> |
| 93 | <string name="import_export_saves_no_profile">No save data found. Please launch a game and retry.</string> | ||
| 94 | <string name="save_file_imported_success">Imported successfully</string> | 93 | <string name="save_file_imported_success">Imported successfully</string> |
| 95 | <string name="save_file_invalid_zip_structure">Invalid save directory structure</string> | 94 | <string name="save_file_invalid_zip_structure">Invalid save directory structure</string> |
| 96 | <string name="save_file_invalid_zip_structure_description">The first subfolder name must be the title ID of the game.</string> | 95 | <string name="save_file_invalid_zip_structure_description">The first subfolder name must be the title ID of the game.</string> |
| @@ -101,12 +100,13 @@ | |||
| 101 | <string name="firmware_installing">Installing firmware</string> | 100 | <string name="firmware_installing">Installing firmware</string> |
| 102 | <string name="firmware_installed_success">Firmware installed successfully</string> | 101 | <string name="firmware_installed_success">Firmware installed successfully</string> |
| 103 | <string name="firmware_installed_failure">Firmware installation failed</string> | 102 | <string name="firmware_installed_failure">Firmware installation failed</string> |
| 104 | <string name="firmware_installed_failure_description">Verify that the ZIP contains valid firmware and try again.</string> | 103 | <string name="firmware_installed_failure_description">Make sure the firmware nca files are at the root of the zip and try again.</string> |
| 105 | <string name="share_log">Share debug logs</string> | 104 | <string name="share_log">Share debug logs</string> |
| 106 | <string name="share_log_description">Share yuzu\'s log file to debug issues</string> | 105 | <string name="share_log_description">Share yuzu\'s log file to debug issues</string> |
| 107 | <string name="share_log_missing">No log file found</string> | 106 | <string name="share_log_missing">No log file found</string> |
| 108 | <string name="install_game_content">Install game content</string> | 107 | <string name="install_game_content">Install game content</string> |
| 109 | <string name="install_game_content_description">Install game updates or DLC</string> | 108 | <string name="install_game_content_description">Install game updates or DLC</string> |
| 109 | <string name="installing_game_content">Installing content…</string> | ||
| 110 | <string name="install_game_content_failure">Error installing file(s) to NAND</string> | 110 | <string name="install_game_content_failure">Error installing file(s) to NAND</string> |
| 111 | <string name="install_game_content_failure_description">Please ensure content(s) are valid and that the prod.keys file is installed.</string> | 111 | <string name="install_game_content_failure_description">Please ensure content(s) are valid and that the prod.keys file is installed.</string> |
| 112 | <string name="install_game_content_failure_base">Installation of base games isn\'t permitted in order to avoid possible conflicts.</string> | 112 | <string name="install_game_content_failure_base">Installation of base games isn\'t permitted in order to avoid possible conflicts.</string> |
| @@ -118,6 +118,10 @@ | |||
| 118 | <string name="install_game_content_help_link">https://yuzu-emu.org/help/quickstart/#dumping-installed-updates</string> | 118 | <string name="install_game_content_help_link">https://yuzu-emu.org/help/quickstart/#dumping-installed-updates</string> |
| 119 | <string name="custom_driver_not_supported">Custom drivers not supported</string> | 119 | <string name="custom_driver_not_supported">Custom drivers not supported</string> |
| 120 | <string name="custom_driver_not_supported_description">Custom driver loading isn\'t currently supported for this device.\nCheck this option again in the future to see if support was added!</string> | 120 | <string name="custom_driver_not_supported_description">Custom driver loading isn\'t currently supported for this device.\nCheck this option again in the future to see if support was added!</string> |
| 121 | <string name="manage_yuzu_data">Manage yuzu data</string> | ||
| 122 | <string name="manage_yuzu_data_description">Import/export firmware, keys, user data, and more!</string> | ||
| 123 | <string name="share_save_file">Share save file</string> | ||
| 124 | <string name="export_save_failed">Failed to export save</string> | ||
| 121 | 125 | ||
| 122 | <!-- About screen strings --> | 126 | <!-- About screen strings --> |
| 123 | <string name="gaia_is_not_real">Gaia isn\'t real</string> | 127 | <string name="gaia_is_not_real">Gaia isn\'t real</string> |
| @@ -137,6 +141,7 @@ | |||
| 137 | <string name="user_data_export_success">User data exported successfully</string> | 141 | <string name="user_data_export_success">User data exported successfully</string> |
| 138 | <string name="user_data_import_success">User data imported successfully</string> | 142 | <string name="user_data_import_success">User data imported successfully</string> |
| 139 | <string name="user_data_export_cancelled">Export cancelled</string> | 143 | <string name="user_data_export_cancelled">Export cancelled</string> |
| 144 | <string name="user_data_import_failed_description">Make sure the user data folders are at the root of the zip folder and contain a config file at config/config.ini and try again.</string> | ||
| 140 | <string name="support_link">https://discord.gg/u77vRWY</string> | 145 | <string name="support_link">https://discord.gg/u77vRWY</string> |
| 141 | <string name="website_link">https://yuzu-emu.org/</string> | 146 | <string name="website_link">https://yuzu-emu.org/</string> |
| 142 | <string name="github_link">https://github.com/yuzu-emu</string> | 147 | <string name="github_link">https://github.com/yuzu-emu</string> |
| @@ -226,6 +231,8 @@ | |||
| 226 | <string name="string_null">Null</string> | 231 | <string name="string_null">Null</string> |
| 227 | <string name="string_import">Import</string> | 232 | <string name="string_import">Import</string> |
| 228 | <string name="export">Export</string> | 233 | <string name="export">Export</string> |
| 234 | <string name="export_failed">Export failed</string> | ||
| 235 | <string name="import_failed">Import failed</string> | ||
| 229 | <string name="cancelling">Cancelling</string> | 236 | <string name="cancelling">Cancelling</string> |
| 230 | 237 | ||
| 231 | <!-- GPU driver installation --> | 238 | <!-- GPU driver installation --> |
| @@ -293,6 +300,7 @@ | |||
| 293 | <string name="performance_warning">Turning off this setting will significantly reduce emulation performance! For the best experience, it is recommended that you leave this setting enabled.</string> | 300 | <string name="performance_warning">Turning off this setting will significantly reduce emulation performance! For the best experience, it is recommended that you leave this setting enabled.</string> |
| 294 | <string name="device_memory_inadequate">Device RAM: %1$s\nRecommended: %2$s</string> | 301 | <string name="device_memory_inadequate">Device RAM: %1$s\nRecommended: %2$s</string> |
| 295 | <string name="memory_formatted">%1$s %2$s</string> | 302 | <string name="memory_formatted">%1$s %2$s</string> |
| 303 | <string name="no_game_present">No bootable game present!</string> | ||
| 296 | 304 | ||
| 297 | <!-- Region Names --> | 305 | <!-- Region Names --> |
| 298 | <string name="region_japan">Japan</string> | 306 | <string name="region_japan">Japan</string> |
diff --git a/src/common/settings_setting.h b/src/common/settings_setting.h index 7be6f26f7..3175ab07d 100644 --- a/src/common/settings_setting.h +++ b/src/common/settings_setting.h | |||
| @@ -187,6 +187,8 @@ public: | |||
| 187 | this->SetValue(input == "true"); | 187 | this->SetValue(input == "true"); |
| 188 | } else if constexpr (std::is_same_v<Type, float>) { | 188 | } else if constexpr (std::is_same_v<Type, float>) { |
| 189 | this->SetValue(std::stof(input)); | 189 | this->SetValue(std::stof(input)); |
| 190 | } else if constexpr (std::is_same_v<Type, AudioEngine>) { | ||
| 191 | this->SetValue(ToEnum<AudioEngine>(input)); | ||
| 190 | } else { | 192 | } else { |
| 191 | this->SetValue(static_cast<Type>(std::stoll(input))); | 193 | this->SetValue(static_cast<Type>(std::stoll(input))); |
| 192 | } | 194 | } |
diff --git a/src/video_core/host_shaders/convert_msaa_to_non_msaa.comp b/src/video_core/host_shaders/convert_msaa_to_non_msaa.comp index fc3854d18..66f2ad483 100644 --- a/src/video_core/host_shaders/convert_msaa_to_non_msaa.comp +++ b/src/video_core/host_shaders/convert_msaa_to_non_msaa.comp | |||
| @@ -15,11 +15,14 @@ void main() { | |||
| 15 | 15 | ||
| 16 | // TODO: Specialization constants for num_samples? | 16 | // TODO: Specialization constants for num_samples? |
| 17 | const int num_samples = imageSamples(msaa_in); | 17 | const int num_samples = imageSamples(msaa_in); |
| 18 | const ivec3 msaa_size = imageSize(msaa_in); | ||
| 19 | const ivec3 out_size = imageSize(output_img); | ||
| 20 | const ivec3 scale = out_size / msaa_size; | ||
| 18 | for (int curr_sample = 0; curr_sample < num_samples; ++curr_sample) { | 21 | for (int curr_sample = 0; curr_sample < num_samples; ++curr_sample) { |
| 19 | const vec4 pixel = imageLoad(msaa_in, coords, curr_sample); | 22 | const vec4 pixel = imageLoad(msaa_in, coords, curr_sample); |
| 20 | 23 | ||
| 21 | const int single_sample_x = 2 * coords.x + (curr_sample & 1); | 24 | const int single_sample_x = scale.x * coords.x + (curr_sample & 1); |
| 22 | const int single_sample_y = 2 * coords.y + ((curr_sample / 2) & 1); | 25 | const int single_sample_y = scale.y * coords.y + ((curr_sample / 2) & 1); |
| 23 | const ivec3 dest_coords = ivec3(single_sample_x, single_sample_y, coords.z); | 26 | const ivec3 dest_coords = ivec3(single_sample_x, single_sample_y, coords.z); |
| 24 | 27 | ||
| 25 | if (any(greaterThanEqual(dest_coords, imageSize(output_img)))) { | 28 | if (any(greaterThanEqual(dest_coords, imageSize(output_img)))) { |
diff --git a/src/video_core/host_shaders/convert_non_msaa_to_msaa.comp b/src/video_core/host_shaders/convert_non_msaa_to_msaa.comp index dedd962f1..c7ce38efa 100644 --- a/src/video_core/host_shaders/convert_non_msaa_to_msaa.comp +++ b/src/video_core/host_shaders/convert_non_msaa_to_msaa.comp | |||
| @@ -15,9 +15,12 @@ void main() { | |||
| 15 | 15 | ||
| 16 | // TODO: Specialization constants for num_samples? | 16 | // TODO: Specialization constants for num_samples? |
| 17 | const int num_samples = imageSamples(output_msaa); | 17 | const int num_samples = imageSamples(output_msaa); |
| 18 | const ivec3 msaa_size = imageSize(output_msaa); | ||
| 19 | const ivec3 out_size = imageSize(img_in); | ||
| 20 | const ivec3 scale = out_size / msaa_size; | ||
| 18 | for (int curr_sample = 0; curr_sample < num_samples; ++curr_sample) { | 21 | for (int curr_sample = 0; curr_sample < num_samples; ++curr_sample) { |
| 19 | const int single_sample_x = 2 * coords.x + (curr_sample & 1); | 22 | const int single_sample_x = scale.x * coords.x + (curr_sample & 1); |
| 20 | const int single_sample_y = 2 * coords.y + ((curr_sample / 2) & 1); | 23 | const int single_sample_y = scale.y * coords.y + ((curr_sample / 2) & 1); |
| 21 | const ivec3 single_coords = ivec3(single_sample_x, single_sample_y, coords.z); | 24 | const ivec3 single_coords = ivec3(single_sample_x, single_sample_y, coords.z); |
| 22 | 25 | ||
| 23 | if (any(greaterThanEqual(single_coords, imageSize(img_in)))) { | 26 | if (any(greaterThanEqual(single_coords, imageSize(img_in)))) { |
diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.cpp b/src/video_core/renderer_vulkan/vk_compute_pass.cpp index 289d5b25c..617f92910 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pass.cpp +++ b/src/video_core/renderer_vulkan/vk_compute_pass.cpp | |||
| @@ -3,6 +3,7 @@ | |||
| 3 | 3 | ||
| 4 | #include <array> | 4 | #include <array> |
| 5 | #include <memory> | 5 | #include <memory> |
| 6 | #include <numeric> | ||
| 6 | #include <optional> | 7 | #include <optional> |
| 7 | #include <utility> | 8 | #include <utility> |
| 8 | 9 | ||
| @@ -11,7 +12,10 @@ | |||
| 11 | #include "common/assert.h" | 12 | #include "common/assert.h" |
| 12 | #include "common/common_types.h" | 13 | #include "common/common_types.h" |
| 13 | #include "common/div_ceil.h" | 14 | #include "common/div_ceil.h" |
| 15 | #include "common/vector_math.h" | ||
| 14 | #include "video_core/host_shaders/astc_decoder_comp_spv.h" | 16 | #include "video_core/host_shaders/astc_decoder_comp_spv.h" |
| 17 | #include "video_core/host_shaders/convert_msaa_to_non_msaa_comp_spv.h" | ||
| 18 | #include "video_core/host_shaders/convert_non_msaa_to_msaa_comp_spv.h" | ||
| 15 | #include "video_core/host_shaders/queries_prefix_scan_sum_comp_spv.h" | 19 | #include "video_core/host_shaders/queries_prefix_scan_sum_comp_spv.h" |
| 16 | #include "video_core/host_shaders/queries_prefix_scan_sum_nosubgroups_comp_spv.h" | 20 | #include "video_core/host_shaders/queries_prefix_scan_sum_nosubgroups_comp_spv.h" |
| 17 | #include "video_core/host_shaders/resolve_conditional_render_comp_spv.h" | 21 | #include "video_core/host_shaders/resolve_conditional_render_comp_spv.h" |
| @@ -131,6 +135,33 @@ constexpr DescriptorBankInfo ASTC_BANK_INFO{ | |||
| 131 | .score = 2, | 135 | .score = 2, |
| 132 | }; | 136 | }; |
| 133 | 137 | ||
| 138 | constexpr std::array<VkDescriptorSetLayoutBinding, ASTC_NUM_BINDINGS> MSAA_DESCRIPTOR_SET_BINDINGS{{ | ||
| 139 | { | ||
| 140 | .binding = 0, | ||
| 141 | .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, | ||
| 142 | .descriptorCount = 1, | ||
| 143 | .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, | ||
| 144 | .pImmutableSamplers = nullptr, | ||
| 145 | }, | ||
| 146 | { | ||
| 147 | .binding = 1, | ||
| 148 | .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, | ||
| 149 | .descriptorCount = 1, | ||
| 150 | .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, | ||
| 151 | .pImmutableSamplers = nullptr, | ||
| 152 | }, | ||
| 153 | }}; | ||
| 154 | |||
| 155 | constexpr DescriptorBankInfo MSAA_BANK_INFO{ | ||
| 156 | .uniform_buffers = 0, | ||
| 157 | .storage_buffers = 0, | ||
| 158 | .texture_buffers = 0, | ||
| 159 | .image_buffers = 0, | ||
| 160 | .textures = 0, | ||
| 161 | .images = 2, | ||
| 162 | .score = 2, | ||
| 163 | }; | ||
| 164 | |||
| 134 | constexpr VkDescriptorUpdateTemplateEntry INPUT_OUTPUT_DESCRIPTOR_UPDATE_TEMPLATE{ | 165 | constexpr VkDescriptorUpdateTemplateEntry INPUT_OUTPUT_DESCRIPTOR_UPDATE_TEMPLATE{ |
| 135 | .dstBinding = 0, | 166 | .dstBinding = 0, |
| 136 | .dstArrayElement = 0, | 167 | .dstArrayElement = 0, |
| @@ -149,6 +180,15 @@ constexpr VkDescriptorUpdateTemplateEntry QUERIES_SCAN_DESCRIPTOR_UPDATE_TEMPLAT | |||
| 149 | .stride = sizeof(DescriptorUpdateEntry), | 180 | .stride = sizeof(DescriptorUpdateEntry), |
| 150 | }; | 181 | }; |
| 151 | 182 | ||
| 183 | constexpr VkDescriptorUpdateTemplateEntry MSAA_DESCRIPTOR_UPDATE_TEMPLATE{ | ||
| 184 | .dstBinding = 0, | ||
| 185 | .dstArrayElement = 0, | ||
| 186 | .descriptorCount = 2, | ||
| 187 | .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, | ||
| 188 | .offset = 0, | ||
| 189 | .stride = sizeof(DescriptorUpdateEntry), | ||
| 190 | }; | ||
| 191 | |||
| 152 | constexpr std::array<VkDescriptorUpdateTemplateEntry, ASTC_NUM_BINDINGS> | 192 | constexpr std::array<VkDescriptorUpdateTemplateEntry, ASTC_NUM_BINDINGS> |
| 153 | ASTC_PASS_DESCRIPTOR_UPDATE_TEMPLATE_ENTRY{{ | 193 | ASTC_PASS_DESCRIPTOR_UPDATE_TEMPLATE_ENTRY{{ |
| 154 | { | 194 | { |
| @@ -224,6 +264,9 @@ ComputePass::ComputePass(const Device& device_, DescriptorPool& descriptor_pool, | |||
| 224 | }); | 264 | }); |
| 225 | descriptor_allocator = descriptor_pool.Allocator(*descriptor_set_layout, bank_info); | 265 | descriptor_allocator = descriptor_pool.Allocator(*descriptor_set_layout, bank_info); |
| 226 | } | 266 | } |
| 267 | if (code.empty()) { | ||
| 268 | return; | ||
| 269 | } | ||
| 227 | module = device.GetLogical().CreateShaderModule({ | 270 | module = device.GetLogical().CreateShaderModule({ |
| 228 | .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, | 271 | .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, |
| 229 | .pNext = nullptr, | 272 | .pNext = nullptr, |
| @@ -590,4 +633,100 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map, | |||
| 590 | scheduler.Finish(); | 633 | scheduler.Finish(); |
| 591 | } | 634 | } |
| 592 | 635 | ||
| 636 | MSAACopyPass::MSAACopyPass(const Device& device_, Scheduler& scheduler_, | ||
| 637 | DescriptorPool& descriptor_pool_, | ||
| 638 | StagingBufferPool& staging_buffer_pool_, | ||
| 639 | ComputePassDescriptorQueue& compute_pass_descriptor_queue_) | ||
| 640 | : ComputePass(device_, descriptor_pool_, MSAA_DESCRIPTOR_SET_BINDINGS, | ||
| 641 | MSAA_DESCRIPTOR_UPDATE_TEMPLATE, MSAA_BANK_INFO, {}, | ||
| 642 | CONVERT_NON_MSAA_TO_MSAA_COMP_SPV), | ||
| 643 | scheduler{scheduler_}, staging_buffer_pool{staging_buffer_pool_}, | ||
| 644 | compute_pass_descriptor_queue{compute_pass_descriptor_queue_} { | ||
| 645 | const auto make_msaa_pipeline = [this](size_t i, std::span<const u32> code) { | ||
| 646 | modules[i] = device.GetLogical().CreateShaderModule({ | ||
| 647 | .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, | ||
| 648 | .pNext = nullptr, | ||
| 649 | .flags = 0, | ||
| 650 | .codeSize = static_cast<u32>(code.size_bytes()), | ||
| 651 | .pCode = code.data(), | ||
| 652 | }); | ||
| 653 | pipelines[i] = device.GetLogical().CreateComputePipeline({ | ||
| 654 | .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, | ||
| 655 | .pNext = nullptr, | ||
| 656 | .flags = 0, | ||
| 657 | .stage{ | ||
| 658 | .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, | ||
| 659 | .pNext = nullptr, | ||
| 660 | .flags = 0, | ||
| 661 | .stage = VK_SHADER_STAGE_COMPUTE_BIT, | ||
| 662 | .module = *modules[i], | ||
| 663 | .pName = "main", | ||
| 664 | .pSpecializationInfo = nullptr, | ||
| 665 | }, | ||
| 666 | .layout = *layout, | ||
| 667 | .basePipelineHandle = nullptr, | ||
| 668 | .basePipelineIndex = 0, | ||
| 669 | }); | ||
| 670 | }; | ||
| 671 | make_msaa_pipeline(0, CONVERT_NON_MSAA_TO_MSAA_COMP_SPV); | ||
| 672 | make_msaa_pipeline(1, CONVERT_MSAA_TO_NON_MSAA_COMP_SPV); | ||
| 673 | } | ||
| 674 | |||
| 675 | MSAACopyPass::~MSAACopyPass() = default; | ||
| 676 | |||
| 677 | void MSAACopyPass::CopyImage(Image& dst_image, Image& src_image, | ||
| 678 | std::span<const VideoCommon::ImageCopy> copies, | ||
| 679 | bool msaa_to_non_msaa) { | ||
| 680 | const VkPipeline msaa_pipeline = *pipelines[msaa_to_non_msaa ? 1 : 0]; | ||
| 681 | scheduler.RequestOutsideRenderPassOperationContext(); | ||
| 682 | for (const VideoCommon::ImageCopy& copy : copies) { | ||
| 683 | ASSERT(copy.src_subresource.base_layer == 0); | ||
| 684 | ASSERT(copy.src_subresource.num_layers == 1); | ||
| 685 | ASSERT(copy.dst_subresource.base_layer == 0); | ||
| 686 | ASSERT(copy.dst_subresource.num_layers == 1); | ||
| 687 | |||
| 688 | compute_pass_descriptor_queue.Acquire(); | ||
| 689 | compute_pass_descriptor_queue.AddImage( | ||
| 690 | src_image.StorageImageView(copy.src_subresource.base_level)); | ||
| 691 | compute_pass_descriptor_queue.AddImage( | ||
| 692 | dst_image.StorageImageView(copy.dst_subresource.base_level)); | ||
| 693 | const void* const descriptor_data{compute_pass_descriptor_queue.UpdateData()}; | ||
| 694 | |||
| 695 | const Common::Vec3<u32> num_dispatches = { | ||
| 696 | Common::DivCeil(copy.extent.width, 8U), | ||
| 697 | Common::DivCeil(copy.extent.height, 8U), | ||
| 698 | copy.extent.depth, | ||
| 699 | }; | ||
| 700 | |||
| 701 | scheduler.Record([this, dst = dst_image.Handle(), msaa_pipeline, num_dispatches, | ||
| 702 | descriptor_data](vk::CommandBuffer cmdbuf) { | ||
| 703 | const VkDescriptorSet set = descriptor_allocator.Commit(); | ||
| 704 | device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, descriptor_data); | ||
| 705 | cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, msaa_pipeline); | ||
| 706 | cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {}); | ||
| 707 | cmdbuf.Dispatch(num_dispatches.x, num_dispatches.y, num_dispatches.z); | ||
| 708 | const VkImageMemoryBarrier write_barrier{ | ||
| 709 | .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, | ||
| 710 | .pNext = nullptr, | ||
| 711 | .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT, | ||
| 712 | .dstAccessMask = VK_ACCESS_SHADER_READ_BIT, | ||
| 713 | .oldLayout = VK_IMAGE_LAYOUT_GENERAL, | ||
| 714 | .newLayout = VK_IMAGE_LAYOUT_GENERAL, | ||
| 715 | .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, | ||
| 716 | .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, | ||
| 717 | .image = dst, | ||
| 718 | .subresourceRange{ | ||
| 719 | .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, | ||
| 720 | .baseMipLevel = 0, | ||
| 721 | .levelCount = VK_REMAINING_MIP_LEVELS, | ||
| 722 | .baseArrayLayer = 0, | ||
| 723 | .layerCount = VK_REMAINING_ARRAY_LAYERS, | ||
| 724 | }, | ||
| 725 | }; | ||
| 726 | cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, | ||
| 727 | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, write_barrier); | ||
| 728 | }); | ||
| 729 | } | ||
| 730 | } | ||
| 731 | |||
| 593 | } // namespace Vulkan | 732 | } // namespace Vulkan |
diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.h b/src/video_core/renderer_vulkan/vk_compute_pass.h index 3ff935639..7b8f938c1 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pass.h +++ b/src/video_core/renderer_vulkan/vk_compute_pass.h | |||
| @@ -11,6 +11,7 @@ | |||
| 11 | #include "video_core/engines/maxwell_3d.h" | 11 | #include "video_core/engines/maxwell_3d.h" |
| 12 | #include "video_core/renderer_vulkan/vk_descriptor_pool.h" | 12 | #include "video_core/renderer_vulkan/vk_descriptor_pool.h" |
| 13 | #include "video_core/renderer_vulkan/vk_update_descriptor.h" | 13 | #include "video_core/renderer_vulkan/vk_update_descriptor.h" |
| 14 | #include "video_core/texture_cache/types.h" | ||
| 14 | #include "video_core/vulkan_common/vulkan_memory_allocator.h" | 15 | #include "video_core/vulkan_common/vulkan_memory_allocator.h" |
| 15 | #include "video_core/vulkan_common/vulkan_wrapper.h" | 16 | #include "video_core/vulkan_common/vulkan_wrapper.h" |
| 16 | 17 | ||
| @@ -130,4 +131,22 @@ private: | |||
| 130 | MemoryAllocator& memory_allocator; | 131 | MemoryAllocator& memory_allocator; |
| 131 | }; | 132 | }; |
| 132 | 133 | ||
| 134 | class MSAACopyPass final : public ComputePass { | ||
| 135 | public: | ||
| 136 | explicit MSAACopyPass(const Device& device_, Scheduler& scheduler_, | ||
| 137 | DescriptorPool& descriptor_pool_, StagingBufferPool& staging_buffer_pool_, | ||
| 138 | ComputePassDescriptorQueue& compute_pass_descriptor_queue_); | ||
| 139 | ~MSAACopyPass(); | ||
| 140 | |||
| 141 | void CopyImage(Image& dst_image, Image& src_image, | ||
| 142 | std::span<const VideoCommon::ImageCopy> copies, bool msaa_to_non_msaa); | ||
| 143 | |||
| 144 | private: | ||
| 145 | Scheduler& scheduler; | ||
| 146 | StagingBufferPool& staging_buffer_pool; | ||
| 147 | ComputePassDescriptorQueue& compute_pass_descriptor_queue; | ||
| 148 | std::array<vk::ShaderModule, 2> modules; | ||
| 149 | std::array<vk::Pipeline, 2> pipelines; | ||
| 150 | }; | ||
| 151 | |||
| 133 | } // namespace Vulkan | 152 | } // namespace Vulkan |
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index 1f9e7acaa..71fdec809 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp | |||
| @@ -176,6 +176,36 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) { | |||
| 176 | return allocator.CreateImage(image_ci); | 176 | return allocator.CreateImage(image_ci); |
| 177 | } | 177 | } |
| 178 | 178 | ||
| 179 | [[nodiscard]] vk::ImageView MakeStorageView(const vk::Device& device, u32 level, VkImage image, | ||
| 180 | VkFormat format) { | ||
| 181 | static constexpr VkImageViewUsageCreateInfo storage_image_view_usage_create_info{ | ||
| 182 | .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO, | ||
| 183 | .pNext = nullptr, | ||
| 184 | .usage = VK_IMAGE_USAGE_STORAGE_BIT, | ||
| 185 | }; | ||
| 186 | return device.CreateImageView(VkImageViewCreateInfo{ | ||
| 187 | .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, | ||
| 188 | .pNext = &storage_image_view_usage_create_info, | ||
| 189 | .flags = 0, | ||
| 190 | .image = image, | ||
| 191 | .viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY, | ||
| 192 | .format = format, | ||
| 193 | .components{ | ||
| 194 | .r = VK_COMPONENT_SWIZZLE_IDENTITY, | ||
| 195 | .g = VK_COMPONENT_SWIZZLE_IDENTITY, | ||
| 196 | .b = VK_COMPONENT_SWIZZLE_IDENTITY, | ||
| 197 | .a = VK_COMPONENT_SWIZZLE_IDENTITY, | ||
| 198 | }, | ||
| 199 | .subresourceRange{ | ||
| 200 | .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, | ||
| 201 | .baseMipLevel = level, | ||
| 202 | .levelCount = 1, | ||
| 203 | .baseArrayLayer = 0, | ||
| 204 | .layerCount = VK_REMAINING_ARRAY_LAYERS, | ||
| 205 | }, | ||
| 206 | }); | ||
| 207 | } | ||
| 208 | |||
| 179 | [[nodiscard]] VkImageAspectFlags ImageAspectMask(PixelFormat format) { | 209 | [[nodiscard]] VkImageAspectFlags ImageAspectMask(PixelFormat format) { |
| 180 | switch (VideoCore::Surface::GetFormatType(format)) { | 210 | switch (VideoCore::Surface::GetFormatType(format)) { |
| 181 | case VideoCore::Surface::SurfaceType::ColorTexture: | 211 | case VideoCore::Surface::SurfaceType::ColorTexture: |
| @@ -817,6 +847,10 @@ TextureCacheRuntime::TextureCacheRuntime(const Device& device_, Scheduler& sched | |||
| 817 | astc_decoder_pass.emplace(device, scheduler, descriptor_pool, staging_buffer_pool, | 847 | astc_decoder_pass.emplace(device, scheduler, descriptor_pool, staging_buffer_pool, |
| 818 | compute_pass_descriptor_queue, memory_allocator); | 848 | compute_pass_descriptor_queue, memory_allocator); |
| 819 | } | 849 | } |
| 850 | if (device.IsStorageImageMultisampleSupported()) { | ||
| 851 | msaa_copy_pass = std::make_unique<MSAACopyPass>( | ||
| 852 | device, scheduler, descriptor_pool, staging_buffer_pool, compute_pass_descriptor_queue); | ||
| 853 | } | ||
| 820 | if (!device.IsKhrImageFormatListSupported()) { | 854 | if (!device.IsKhrImageFormatListSupported()) { |
| 821 | return; | 855 | return; |
| 822 | } | 856 | } |
| @@ -1285,7 +1319,11 @@ void TextureCacheRuntime::CopyImage(Image& dst, Image& src, | |||
| 1285 | 1319 | ||
| 1286 | void TextureCacheRuntime::CopyImageMSAA(Image& dst, Image& src, | 1320 | void TextureCacheRuntime::CopyImageMSAA(Image& dst, Image& src, |
| 1287 | std::span<const VideoCommon::ImageCopy> copies) { | 1321 | std::span<const VideoCommon::ImageCopy> copies) { |
| 1288 | UNIMPLEMENTED_MSG("Copying images with different samples is not implemented in Vulkan."); | 1322 | const bool msaa_to_non_msaa = src.info.num_samples > 1 && dst.info.num_samples == 1; |
| 1323 | if (msaa_copy_pass) { | ||
| 1324 | return msaa_copy_pass->CopyImage(dst, src, copies, msaa_to_non_msaa); | ||
| 1325 | } | ||
| 1326 | UNIMPLEMENTED_MSG("Copying images with different samples is not supported."); | ||
| 1289 | } | 1327 | } |
| 1290 | 1328 | ||
| 1291 | u64 TextureCacheRuntime::GetDeviceLocalMemory() const { | 1329 | u64 TextureCacheRuntime::GetDeviceLocalMemory() const { |
| @@ -1333,39 +1371,15 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu | |||
| 1333 | if (runtime->device.HasDebuggingToolAttached()) { | 1371 | if (runtime->device.HasDebuggingToolAttached()) { |
| 1334 | original_image.SetObjectNameEXT(VideoCommon::Name(*this).c_str()); | 1372 | original_image.SetObjectNameEXT(VideoCommon::Name(*this).c_str()); |
| 1335 | } | 1373 | } |
| 1336 | static constexpr VkImageViewUsageCreateInfo storage_image_view_usage_create_info{ | ||
| 1337 | .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO, | ||
| 1338 | .pNext = nullptr, | ||
| 1339 | .usage = VK_IMAGE_USAGE_STORAGE_BIT, | ||
| 1340 | }; | ||
| 1341 | current_image = *original_image; | 1374 | current_image = *original_image; |
| 1375 | storage_image_views.resize(info.resources.levels); | ||
| 1342 | if (IsPixelFormatASTC(info.format) && !runtime->device.IsOptimalAstcSupported() && | 1376 | if (IsPixelFormatASTC(info.format) && !runtime->device.IsOptimalAstcSupported() && |
| 1343 | Settings::values.astc_recompression.GetValue() == | 1377 | Settings::values.astc_recompression.GetValue() == |
| 1344 | Settings::AstcRecompression::Uncompressed) { | 1378 | Settings::AstcRecompression::Uncompressed) { |
| 1345 | const auto& device = runtime->device.GetLogical(); | 1379 | const auto& device = runtime->device.GetLogical(); |
| 1346 | storage_image_views.reserve(info.resources.levels); | ||
| 1347 | for (s32 level = 0; level < info.resources.levels; ++level) { | 1380 | for (s32 level = 0; level < info.resources.levels; ++level) { |
| 1348 | storage_image_views.push_back(device.CreateImageView(VkImageViewCreateInfo{ | 1381 | storage_image_views[level] = |
| 1349 | .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, | 1382 | MakeStorageView(device, level, *original_image, VK_FORMAT_A8B8G8R8_UNORM_PACK32); |
| 1350 | .pNext = &storage_image_view_usage_create_info, | ||
| 1351 | .flags = 0, | ||
| 1352 | .image = *original_image, | ||
| 1353 | .viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY, | ||
| 1354 | .format = VK_FORMAT_A8B8G8R8_UNORM_PACK32, | ||
| 1355 | .components{ | ||
| 1356 | .r = VK_COMPONENT_SWIZZLE_IDENTITY, | ||
| 1357 | .g = VK_COMPONENT_SWIZZLE_IDENTITY, | ||
| 1358 | .b = VK_COMPONENT_SWIZZLE_IDENTITY, | ||
| 1359 | .a = VK_COMPONENT_SWIZZLE_IDENTITY, | ||
| 1360 | }, | ||
| 1361 | .subresourceRange{ | ||
| 1362 | .aspectMask = aspect_mask, | ||
| 1363 | .baseMipLevel = static_cast<u32>(level), | ||
| 1364 | .levelCount = 1, | ||
| 1365 | .baseArrayLayer = 0, | ||
| 1366 | .layerCount = VK_REMAINING_ARRAY_LAYERS, | ||
| 1367 | }, | ||
| 1368 | })); | ||
| 1369 | } | 1383 | } |
| 1370 | } | 1384 | } |
| 1371 | } | 1385 | } |
| @@ -1496,6 +1510,17 @@ void Image::DownloadMemory(const StagingBufferRef& map, std::span<const BufferIm | |||
| 1496 | DownloadMemory(buffers, offsets, copies); | 1510 | DownloadMemory(buffers, offsets, copies); |
| 1497 | } | 1511 | } |
| 1498 | 1512 | ||
| 1513 | VkImageView Image::StorageImageView(s32 level) noexcept { | ||
| 1514 | auto& view = storage_image_views[level]; | ||
| 1515 | if (!view) { | ||
| 1516 | const auto format_info = | ||
| 1517 | MaxwellToVK::SurfaceFormat(runtime->device, FormatType::Optimal, true, info.format); | ||
| 1518 | view = | ||
| 1519 | MakeStorageView(runtime->device.GetLogical(), level, current_image, format_info.format); | ||
| 1520 | } | ||
| 1521 | return *view; | ||
| 1522 | } | ||
| 1523 | |||
| 1499 | bool Image::IsRescaled() const noexcept { | 1524 | bool Image::IsRescaled() const noexcept { |
| 1500 | return True(flags & ImageFlagBits::Rescaled); | 1525 | return True(flags & ImageFlagBits::Rescaled); |
| 1501 | } | 1526 | } |
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h index 565ce19a9..d6c5a15cc 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.h +++ b/src/video_core/renderer_vulkan/vk_texture_cache.h | |||
| @@ -117,6 +117,7 @@ public: | |||
| 117 | BlitImageHelper& blit_image_helper; | 117 | BlitImageHelper& blit_image_helper; |
| 118 | RenderPassCache& render_pass_cache; | 118 | RenderPassCache& render_pass_cache; |
| 119 | std::optional<ASTCDecoderPass> astc_decoder_pass; | 119 | std::optional<ASTCDecoderPass> astc_decoder_pass; |
| 120 | std::unique_ptr<MSAACopyPass> msaa_copy_pass; | ||
| 120 | const Settings::ResolutionScalingInfo& resolution; | 121 | const Settings::ResolutionScalingInfo& resolution; |
| 121 | std::array<std::vector<VkFormat>, VideoCore::Surface::MaxPixelFormat> view_formats; | 122 | std::array<std::vector<VkFormat>, VideoCore::Surface::MaxPixelFormat> view_formats; |
| 122 | 123 | ||
| @@ -161,15 +162,13 @@ public: | |||
| 161 | return aspect_mask; | 162 | return aspect_mask; |
| 162 | } | 163 | } |
| 163 | 164 | ||
| 164 | [[nodiscard]] VkImageView StorageImageView(s32 level) const noexcept { | ||
| 165 | return *storage_image_views[level]; | ||
| 166 | } | ||
| 167 | |||
| 168 | /// Returns true when the image is already initialized and mark it as initialized | 165 | /// Returns true when the image is already initialized and mark it as initialized |
| 169 | [[nodiscard]] bool ExchangeInitialization() noexcept { | 166 | [[nodiscard]] bool ExchangeInitialization() noexcept { |
| 170 | return std::exchange(initialized, true); | 167 | return std::exchange(initialized, true); |
| 171 | } | 168 | } |
| 172 | 169 | ||
| 170 | VkImageView StorageImageView(s32 level) noexcept; | ||
| 171 | |||
| 173 | bool IsRescaled() const noexcept; | 172 | bool IsRescaled() const noexcept; |
| 174 | 173 | ||
| 175 | bool ScaleUp(bool ignore = false); | 174 | bool ScaleUp(bool ignore = false); |
diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index 94f41266d..dd1e7ea8c 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h | |||
| @@ -324,6 +324,11 @@ public: | |||
| 324 | return features.shader_float16_int8.shaderInt8; | 324 | return features.shader_float16_int8.shaderInt8; |
| 325 | } | 325 | } |
| 326 | 326 | ||
| 327 | /// Returns true if the device supports binding multisample images as storage images. | ||
| 328 | bool IsStorageImageMultisampleSupported() const { | ||
| 329 | return features.features.shaderStorageImageMultisample; | ||
| 330 | } | ||
| 331 | |||
| 327 | /// Returns true if the device warp size can potentially be bigger than guest's warp size. | 332 | /// Returns true if the device warp size can potentially be bigger than guest's warp size. |
| 328 | bool IsWarpSizePotentiallyBiggerThanGuest() const { | 333 | bool IsWarpSizePotentiallyBiggerThanGuest() const { |
| 329 | return is_warp_potentially_bigger; | 334 | return is_warp_potentially_bigger; |
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index b1b6b9354..adb7b332f 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp | |||
| @@ -3113,10 +3113,9 @@ void GMainWindow::OnMenuInstallToNAND() { | |||
| 3113 | QFuture<InstallResult> future; | 3113 | QFuture<InstallResult> future; |
| 3114 | InstallResult result; | 3114 | InstallResult result; |
| 3115 | 3115 | ||
| 3116 | if (file.endsWith(QStringLiteral("xci"), Qt::CaseInsensitive) || | 3116 | if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) { |
| 3117 | file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) { | ||
| 3118 | 3117 | ||
| 3119 | future = QtConcurrent::run([this, &file] { return InstallNSPXCI(file); }); | 3118 | future = QtConcurrent::run([this, &file] { return InstallNSP(file); }); |
| 3120 | 3119 | ||
| 3121 | while (!future.isFinished()) { | 3120 | while (!future.isFinished()) { |
| 3122 | QCoreApplication::processEvents(); | 3121 | QCoreApplication::processEvents(); |
| @@ -3175,7 +3174,7 @@ void GMainWindow::OnMenuInstallToNAND() { | |||
| 3175 | ui->action_Install_File_NAND->setEnabled(true); | 3174 | ui->action_Install_File_NAND->setEnabled(true); |
| 3176 | } | 3175 | } |
| 3177 | 3176 | ||
| 3178 | InstallResult GMainWindow::InstallNSPXCI(const QString& filename) { | 3177 | InstallResult GMainWindow::InstallNSP(const QString& filename) { |
| 3179 | const auto qt_raw_copy = [this](const FileSys::VirtualFile& src, | 3178 | const auto qt_raw_copy = [this](const FileSys::VirtualFile& src, |
| 3180 | const FileSys::VirtualFile& dest, std::size_t block_size) { | 3179 | const FileSys::VirtualFile& dest, std::size_t block_size) { |
| 3181 | if (src == nullptr || dest == nullptr) { | 3180 | if (src == nullptr || dest == nullptr) { |
| @@ -3209,9 +3208,7 @@ InstallResult GMainWindow::InstallNSPXCI(const QString& filename) { | |||
| 3209 | return InstallResult::Failure; | 3208 | return InstallResult::Failure; |
| 3210 | } | 3209 | } |
| 3211 | } else { | 3210 | } else { |
| 3212 | const auto xci = std::make_shared<FileSys::XCI>( | 3211 | return InstallResult::Failure; |
| 3213 | vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read)); | ||
| 3214 | nsp = xci->GetSecurePartitionNSP(); | ||
| 3215 | } | 3212 | } |
| 3216 | 3213 | ||
| 3217 | if (nsp->GetStatus() != Loader::ResultStatus::Success) { | 3214 | if (nsp->GetStatus() != Loader::ResultStatus::Success) { |
diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 53bedfab3..ba318eb11 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h | |||
| @@ -387,7 +387,7 @@ private: | |||
| 387 | void RemoveCacheStorage(u64 program_id); | 387 | void RemoveCacheStorage(u64 program_id); |
| 388 | bool SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id, | 388 | bool SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id, |
| 389 | u64* selected_title_id, u8* selected_content_record_type); | 389 | u64* selected_title_id, u8* selected_content_record_type); |
| 390 | InstallResult InstallNSPXCI(const QString& filename); | 390 | InstallResult InstallNSP(const QString& filename); |
| 391 | InstallResult InstallNCA(const QString& filename); | 391 | InstallResult InstallNCA(const QString& filename); |
| 392 | void MigrateConfigFiles(); | 392 | void MigrateConfigFiles(); |
| 393 | void UpdateWindowTitle(std::string_view title_name = {}, std::string_view title_version = {}, | 393 | void UpdateWindowTitle(std::string_view title_name = {}, std::string_view title_version = {}, |