aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorxbzk <xbzk@eden-emu.dev>2026-03-31 21:32:24 +0200
committercrueter <crueter@eden-emu.dev>2026-03-31 21:32:24 +0200
commit81a344f3dbd3cb549535c6486fd5269c5eacc528 (patch)
tree607f4063ac36dfce71a0db3c6f8389975369b089 /src
parentc0fbb2526d1ea9a0a270e1c88b80c5a13c03f7d6 (diff)
[android,addons] after a crash, launch button will wait for reloadGames to complete, and system will initialize after global config for proper firmware mounting (#3803)
This fixes two problems: 1. After a crash, it was possible to launch a game before external content gets mounted. Now the button will wait for it to complete. 2. Directory initialization was init system before init globalconfig, so after a crash firmware was not being remounted (have you ever noticed fw version = N/A in device overlay, after fiddling with applets?) (this had been fixed in 3755, which was not thoroughly tested by cocoon dev) Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3803 Reviewed-by: Lizzie <lizzie@eden-emu.dev> Reviewed-by: CamilleLaVey <camillelavey99@gmail.com> Co-authored-by: xbzk <xbzk@eden-emu.dev> Co-committed-by: xbzk <xbzk@eden-emu.dev>
Diffstat (limited to 'src')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt17
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt65
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt99
4 files changed, 129 insertions, 54 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
index faa35bc3eb..c3dea79bae 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
@@ -36,6 +36,7 @@ import org.yuzu.yuzu_emu.databinding.FragmentGamePropertiesBinding
import org.yuzu.yuzu_emu.features.DocumentProvider
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.ui.SettingsSubscreen
+import org.yuzu.yuzu_emu.model.AddonViewModel
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.GameProperty
import org.yuzu.yuzu_emu.model.GamesViewModel
@@ -46,6 +47,7 @@ import org.yuzu.yuzu_emu.model.SubmenuProperty
import org.yuzu.yuzu_emu.model.TaskState
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.FileUtil
+import org.yuzu.yuzu_emu.utils.GameHelper
import org.yuzu.yuzu_emu.utils.GameIconUtils
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import org.yuzu.yuzu_emu.utils.MemoryUtil
@@ -61,6 +63,7 @@ class GamePropertiesFragment : Fragment() {
private val homeViewModel: HomeViewModel by activityViewModels()
private val gamesViewModel: GamesViewModel by activityViewModels()
+ private val addonViewModel: AddonViewModel by activityViewModels()
private val driverViewModel: DriverViewModel by activityViewModels()
private val args by navArgs<GamePropertiesFragmentArgs>()
@@ -118,6 +121,20 @@ class GamePropertiesFragment : Fragment() {
.show(childFragmentManager, LaunchGameDialogFragment.TAG)
}
+ if (GameHelper.cachedGameList.isEmpty()) {
+ binding.buttonStart.isEnabled = false
+ viewLifecycleOwner.lifecycleScope.launch {
+ withContext(Dispatchers.IO) {
+ GameHelper.restoreContentForGame(args.game)
+ }
+ if (_binding == null) {
+ return@launch
+ }
+ addonViewModel.onAddonsViewStarted(args.game)
+ binding.buttonStart.isEnabled = true
+ }
+ }
+
reloadList()
homeViewModel.openImportSaves.collect(
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
index 39ff038034..1a63a3ad82 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
@@ -100,42 +100,45 @@ class GamesViewModel : ViewModel() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
- if (firstStartup) {
- // Retrieve list of cached games
- val storedGames =
- PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
- .getStringSet(GameHelper.KEY_GAMES, emptySet())
- if (storedGames!!.isNotEmpty()) {
- val deserializedGames = mutableSetOf<Game>()
- storedGames.forEach {
- val game: Game
- try {
- game = Json.decodeFromString(it)
- } catch (e: Exception) {
- // We don't care about any errors related to parsing the game cache
- return@forEach
- }
-
- val gameExists =
- DocumentFile.fromSingleUri(
- YuzuApplication.appContext,
- Uri.parse(game.path)
- )?.exists()
- if (gameExists == true) {
- deserializedGames.add(game)
+ try {
+ if (firstStartup) {
+ // Retrieve list of cached games
+ val storedGames =
+ PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+ .getStringSet(GameHelper.KEY_GAMES, emptySet())
+ if (storedGames!!.isNotEmpty()) {
+ val deserializedGames = mutableSetOf<Game>()
+ storedGames.forEach {
+ val game: Game
+ try {
+ game = Json.decodeFromString(it)
+ } catch (e: Exception) {
+ // We don't care about any errors related to parsing the game cache
+ return@forEach
+ }
+
+ val gameExists =
+ DocumentFile.fromSingleUri(
+ YuzuApplication.appContext,
+ Uri.parse(game.path)
+ )?.exists()
+ if (gameExists == true) {
+ deserializedGames.add(game)
+ }
}
+ setGames(deserializedGames.toList())
}
- setGames(deserializedGames.toList())
}
- }
- setGames(GameHelper.getGames())
- reloading.set(false)
- _isReloading.value = false
- _shouldScrollAfterReload.value = true
+ setGames(GameHelper.getGames())
+ _shouldScrollAfterReload.value = true
- if (directoriesChanged) {
- setShouldSwapData(true)
+ if (directoriesChanged) {
+ setShouldSwapData(true)
+ }
+ } finally {
+ reloading.set(false)
+ _isReloading.value = false
}
}
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
index f47c60491b..f961c5e984 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
@@ -23,8 +23,8 @@ object DirectoryInitialization {
fun start() {
if (!areDirectoriesReady) {
initializeInternalStorage()
- NativeLibrary.initializeSystem(false)
NativeConfig.initializeGlobalConfig()
+ NativeLibrary.initializeSystem(false)
NativeLibrary.reloadProfiles()
migrateSettings()
areDirectoriesReady = true
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
index 4a3cf61daa..64e035afbe 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
@@ -8,9 +8,11 @@ package org.yuzu.yuzu_emu.utils
import android.content.SharedPreferences
import android.net.Uri
+import android.provider.DocumentsContract
import androidx.preference.PreferenceManager
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
+import java.io.File
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.model.Game
@@ -49,29 +51,8 @@ object GameHelper {
// Remove previous filesystem provider information so we can get up to date version info
NativeLibrary.clearFilesystemProvider()
- // Scan External Content directories and register all NSP/XCI files
- val externalContentDirs = NativeConfig.getExternalContentDirs()
- val uniqueExternalContentDirs = linkedSetOf<String>()
- externalContentDirs.forEach { externalDir ->
- if (externalDir.isNotEmpty()) {
- uniqueExternalContentDirs.add(externalDir)
- }
- }
-
val mountedContainerUris = mutableSetOf<String>()
- for (externalDir in uniqueExternalContentDirs) {
- if (externalDir.isNotEmpty()) {
- val externalDirUri = externalDir.toUri()
- if (FileUtil.isTreeUriValid(externalDirUri)) {
- scanContentContainersRecursive(FileUtil.listFiles(externalDirUri), 3) {
- val containerUri = it.uri.toString()
- if (mountedContainerUris.add(containerUri)) {
- NativeLibrary.addFileToFilesystemProvider(containerUri)
- }
- }
- }
- }
- }
+ mountExternalContentDirectories(mountedContainerUris)
val badDirs = mutableListOf<Int>()
gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
@@ -115,6 +96,15 @@ object GameHelper {
return games.toList()
}
+ fun restoreContentForGame(game: Game) {
+ NativeLibrary.reloadKeys()
+
+ val mountedContainerUris = mutableSetOf<String>()
+ mountExternalContentDirectories(mountedContainerUris)
+ mountGameFolderContent(Uri.parse(game.path), mountedContainerUris)
+ NativeLibrary.addFileToFilesystemProvider(game.path)
+ }
+
// File extensions considered as external content, buuut should
// be done better imo.
private val externalContentExtensions = setOf("nsp", "xci")
@@ -181,6 +171,71 @@ object GameHelper {
}
}
+ private fun mountExternalContentDirectories(mountedContainerUris: MutableSet<String>) {
+ val uniqueExternalContentDirs = linkedSetOf<String>()
+ NativeConfig.getExternalContentDirs().forEach { externalDir ->
+ if (externalDir.isNotEmpty()) {
+ uniqueExternalContentDirs.add(externalDir)
+ }
+ }
+
+ for (externalDir in uniqueExternalContentDirs) {
+ val externalDirUri = externalDir.toUri()
+ if (FileUtil.isTreeUriValid(externalDirUri)) {
+ scanContentContainersRecursive(FileUtil.listFiles(externalDirUri), 3) {
+ val containerUri = it.uri.toString()
+ if (mountedContainerUris.add(containerUri)) {
+ NativeLibrary.addFileToFilesystemProvider(containerUri)
+ }
+ }
+ }
+ }
+ }
+
+ private fun mountGameFolderContent(gameUri: Uri, mountedContainerUris: MutableSet<String>) {
+ if (gameUri.scheme == "content") {
+ val parentUri = getParentDocumentUri(gameUri) ?: return
+ scanContentContainersRecursive(FileUtil.listFiles(parentUri), 1) {
+ val containerUri = it.uri.toString()
+ if (mountedContainerUris.add(containerUri)) {
+ NativeLibrary.addGameFolderFileToFilesystemProvider(containerUri)
+ }
+ }
+ return
+ }
+
+ val gameFile = File(gameUri.path ?: gameUri.toString())
+ val parentDir = gameFile.parentFile ?: return
+ parentDir.listFiles()?.forEach { sibling ->
+ if (!sibling.isFile) {
+ return@forEach
+ }
+
+ val extension = sibling.extension.lowercase()
+ if (externalContentExtensions.contains(extension)) {
+ val containerUri = Uri.fromFile(sibling).toString()
+ if (mountedContainerUris.add(containerUri)) {
+ NativeLibrary.addGameFolderFileToFilesystemProvider(containerUri)
+ }
+ }
+ }
+ }
+
+ private fun getParentDocumentUri(uri: Uri): Uri? {
+ return try {
+ val documentId = DocumentsContract.getDocumentId(uri)
+ val separatorIndex = documentId.lastIndexOf('/')
+ if (separatorIndex == -1) {
+ null
+ } else {
+ val parentDocumentId = documentId.substring(0, separatorIndex)
+ DocumentsContract.buildDocumentUriUsingTree(uri, parentDocumentId)
+ }
+ } catch (_: Exception) {
+ null
+ }
+ }
+
fun getGame(
uri: Uri,
addedToLibrary: Boolean,