mirror of
https://git.ryujinx.app/kenji-nx/ryujinx.git
synced 2025-12-13 22:37:07 +00:00
feat: Add functionality to auto-load DLCs and Title Updates from configured directories
This commit is contained in:
parent
78db4c365f
commit
42fa39672b
5 changed files with 173 additions and 4 deletions
|
|
@ -188,7 +188,7 @@ class DlcViewModel(val titleId: String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveChanges() {
|
fun saveChanges() {
|
||||||
data?.apply {
|
data?.apply {
|
||||||
dlcItemsState?.forEach { item ->
|
dlcItemsState?.forEach { item ->
|
||||||
for (container in this) {
|
for (container in this) {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.kenjinx.android.KenjinxNative
|
||||||
|
import org.kenjinx.android.KeyboardMode
|
||||||
import org.kenjinx.android.MainActivity
|
import org.kenjinx.android.MainActivity
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
|
@ -104,10 +106,14 @@ class HomeViewModel(
|
||||||
return@sortWith strA.length - strB.length
|
return@sortWith strA.length - strB.length
|
||||||
}
|
}
|
||||||
|
|
||||||
for(game in loadedCache)
|
for (game in loadedCache) {
|
||||||
{
|
|
||||||
game.getGameInfo()
|
game.getGameInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-load DLCs and Title Updates from configured directories (if any)
|
||||||
|
try {
|
||||||
|
autoloadContent()
|
||||||
|
} catch (_: Throwable) { }
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
GlobalScope.launch(Dispatchers.Main){
|
GlobalScope.launch(Dispatchers.Main){
|
||||||
|
|
@ -116,4 +122,125 @@ class HomeViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to compare update versions from filenames
|
||||||
|
// Returns true if newPath represents a newer version than currentPath
|
||||||
|
private fun shouldSelectNewerUpdate(currentPath: String, newPath: String, allPaths: List<String>): Boolean {
|
||||||
|
// Extract version numbers from filenames using regex pattern [vXXXXXX]
|
||||||
|
val versionPattern = Regex("\\[v(\\d+)]")
|
||||||
|
|
||||||
|
val currentVersion = versionPattern.find(currentPath.lowercase(Locale.getDefault()))?.groupValues?.get(1)?.toIntOrNull() ?: 0
|
||||||
|
val newVersion = versionPattern.find(newPath.lowercase(Locale.getDefault()))?.groupValues?.get(1)?.toIntOrNull() ?: 0
|
||||||
|
|
||||||
|
return newVersion > currentVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scans configured directories for NSPs containing DLCs/Updates and associates them to known titles.
|
||||||
|
private fun autoloadContent() {
|
||||||
|
val prefs = sharedPref ?: return
|
||||||
|
|
||||||
|
// Prefer a single, explicitly selected updates/DLC folder (updatesFolder). Fallback to legacy autoloadDirs.
|
||||||
|
val updatesFolder = prefs.getString("updatesFolder", "") ?: ""
|
||||||
|
val dirs: List<String> = if (updatesFolder.isNotBlank()) {
|
||||||
|
listOf(updatesFolder.trim())
|
||||||
|
} else {
|
||||||
|
// Legacy: semicolon-separated list under key autoloadDirs
|
||||||
|
val raw = prefs.getString("autoloadDirs", "") ?: ""
|
||||||
|
if (raw.isBlank()) emptyList() else raw.split(';').map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dirs.isEmpty()) return
|
||||||
|
|
||||||
|
// Build a map of titleId -> helpers
|
||||||
|
val gamesByTitle = loadedCache.mapNotNull { g ->
|
||||||
|
val tid = g.titleId
|
||||||
|
if (!tid.isNullOrBlank()) tid.lowercase(Locale.getDefault()) to tid else null
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
|
var updatesAdded = 0
|
||||||
|
var dlcAdded = 0
|
||||||
|
|
||||||
|
for (dir in dirs) {
|
||||||
|
val base = java.io.File(dir)
|
||||||
|
if (!base.exists() || !base.isDirectory) continue
|
||||||
|
|
||||||
|
base.walkTopDown().forEach fileLoop@{ f ->
|
||||||
|
if (!f.isFile) return@fileLoop
|
||||||
|
val name = f.name.lowercase(Locale.getDefault())
|
||||||
|
if (!name.endsWith(".nsp")) return@fileLoop
|
||||||
|
|
||||||
|
// Extract title ID from filename
|
||||||
|
val tidPattern = Regex("\\[([0-9a-fA-F]{16})]")
|
||||||
|
val tidMatch = tidPattern.find(name) ?: return@fileLoop
|
||||||
|
val fileTid = tidMatch.groupValues[1].lowercase(Locale.getDefault())
|
||||||
|
|
||||||
|
// Try to find DLC content for all games
|
||||||
|
var isDlc = false
|
||||||
|
try {
|
||||||
|
for ((_, tidOrig) in gamesByTitle) {
|
||||||
|
val contents = KenjinxNative.deviceGetDlcContentList(f.absolutePath, tidOrig.toLong(16))
|
||||||
|
|
||||||
|
if (contents.isNotEmpty()) {
|
||||||
|
isDlc = true
|
||||||
|
val containerPath = f.absolutePath
|
||||||
|
val vm = DlcViewModel(tidOrig)
|
||||||
|
val already = vm.data?.any { it.path == containerPath } == true
|
||||||
|
|
||||||
|
if (!already) {
|
||||||
|
val container = DlcContainerList(containerPath)
|
||||||
|
for (content in contents) {
|
||||||
|
container.dlc_nca_list.add(
|
||||||
|
DlcContainer(
|
||||||
|
true,
|
||||||
|
KenjinxNative.deviceGetDlcTitleId(containerPath, content).toLong(16),
|
||||||
|
content
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
vm.data?.add(container)
|
||||||
|
vm.saveChanges()
|
||||||
|
dlcAdded++
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Throwable) { }
|
||||||
|
|
||||||
|
if (isDlc) return@fileLoop
|
||||||
|
|
||||||
|
// Treat as Title Update - convert update ID to base ID
|
||||||
|
// Update title IDs end in 800, base game IDs end in 000
|
||||||
|
val baseTid = if (fileTid.endsWith("800")) {
|
||||||
|
fileTid.substring(0, fileTid.length - 3) + "000"
|
||||||
|
} else {
|
||||||
|
fileTid
|
||||||
|
}
|
||||||
|
|
||||||
|
val originalTid = gamesByTitle[baseTid]
|
||||||
|
if (originalTid != null) {
|
||||||
|
val vm = TitleUpdateViewModel(originalTid)
|
||||||
|
val path = f.absolutePath
|
||||||
|
val exists = (vm.data?.paths?.contains(path) == true)
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
// Add the new update path
|
||||||
|
vm.data?.paths?.add(path)
|
||||||
|
|
||||||
|
// Auto-select this update if it's newer than the currently selected one
|
||||||
|
// or if no update is currently selected
|
||||||
|
val currentSelected = vm.data?.selected ?: ""
|
||||||
|
val shouldSelect = currentSelected.isEmpty() ||
|
||||||
|
shouldSelectNewerUpdate(currentSelected, path, vm.data?.paths ?: mutableListOf())
|
||||||
|
|
||||||
|
if (shouldSelect) {
|
||||||
|
vm.data?.selected = path
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.saveChanges()
|
||||||
|
updatesAdded++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,30 @@ class SettingsViewModel(val activity: MainActivity) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openUpdatesFolder() {
|
||||||
|
val path = sharedPref.getString("updatesFolder", "") ?: ""
|
||||||
|
|
||||||
|
activity.storageHelper!!.onFolderSelected = { _, folder ->
|
||||||
|
val p = folder.getAbsolutePath(activity)
|
||||||
|
sharedPref.edit {
|
||||||
|
putString("updatesFolder", p)
|
||||||
|
}
|
||||||
|
activity.storageHelper!!.onFolderSelected = previousFolderCallback
|
||||||
|
|
||||||
|
// Trigger a reload of the game list to apply the new updates/DLC folder
|
||||||
|
MainActivity.mainViewModel?.homeViewModel?.requestReload()
|
||||||
|
MainActivity.mainViewModel?.homeViewModel?.ensureReloadIfNecessary()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.isEmpty())
|
||||||
|
activity.storageHelper?.storage?.openFolderPicker()
|
||||||
|
else
|
||||||
|
activity.storageHelper?.storage?.openFolderPicker(
|
||||||
|
activity.storageHelper!!.storage.requestCodeFolderPicker,
|
||||||
|
FileFullPath(activity, path)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun selectKey(installState: MutableState<KeyInstallState>) {
|
fun selectKey(installState: MutableState<KeyInstallState>) {
|
||||||
if (installState.value != KeyInstallState.File)
|
if (installState.value != KeyInstallState.File)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@ class TitleUpdateViewModel(val titleId: String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveChanges() {
|
fun saveChanges() {
|
||||||
val metadata = data ?: TitleUpdateMetadata()
|
val metadata = data ?: TitleUpdateMetadata()
|
||||||
val gson = Gson()
|
val gson = Gson()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import androidx.compose.material.icons.filled.Home
|
||||||
import androidx.compose.material.icons.filled.MailOutline
|
import androidx.compose.material.icons.filled.MailOutline
|
||||||
import androidx.compose.material.icons.outlined.BarChart
|
import androidx.compose.material.icons.outlined.BarChart
|
||||||
import androidx.compose.material.icons.outlined.FileOpen
|
import androidx.compose.material.icons.outlined.FileOpen
|
||||||
|
import androidx.compose.material.icons.outlined.Folder
|
||||||
import androidx.compose.material.icons.outlined.Memory
|
import androidx.compose.material.icons.outlined.Memory
|
||||||
import androidx.compose.material.icons.outlined.Panorama
|
import androidx.compose.material.icons.outlined.Panorama
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
|
|
@ -421,6 +422,23 @@ class SettingViews {
|
||||||
isFullWidth = false,
|
isFullWidth = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(56.dp)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
){
|
||||||
|
ActionButton(
|
||||||
|
onClick = {
|
||||||
|
settingsViewModel.openUpdatesFolder()
|
||||||
|
},
|
||||||
|
text = "Select Updates/DLC Folder",
|
||||||
|
icon = Icons.Outlined.Folder,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
isFullWidth = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue