Game storage is automatic: a card in the phone is a Steam library by itself

The choice belongs inside the client - the drive drop-down on Install, Settings > Storage - so
the app no longer needs a setting for the common case. Default = automatic (the first SD card
present); the cog keeps Internal only and a chosen folder.
This commit is contained in:
The412Banner committed 2026-09-22 09:41:58 -04:00
1 parent fa25451766
commit 1374091d1b
7 files changed
+43 -16

No files matched your search

+5 -3
View File
@@ -174,9 +174,11 @@ under proot; Firefox's are turned off in the session's environment, without whic
### Game storage: internal, an SD card, or a folder
The client's own library is internal storage and always there. **Game storage** in the Steam cog
registers one more: an SD card (through the app's own folder on it, the one place a card lets
this app write) or any folder chosen in the File Manager. The session binds it at
The client's own library is internal storage and always there. An **SD card in the phone is
registered as a second library by itself** (through the app's own folder on it, the one place a
card lets this app write), so the choice is made where it is anywhere else - inside Steam, on
every Install. **Game storage** in the Steam cog is only for turning that off or pointing it at a
folder chosen in the File Manager instead. The session binds it at
`/mnt/bannerlator-sd` and `bannerlator-steam-library` registers it with the client under the name
the app gives it; the client then asks where to install every game, lists both on its Storage
page and moves games between them itself. Set to internal only, the entry is removed again. A
@@ -386,7 +386,7 @@ class MainActivity : ComponentActivity() {
/** A second Steam library, proven writable first; "" = internal only. */
private fun setGameStorage(path: String, label: String) {
if (path.isNotEmpty()) {
if (path.isNotEmpty() && path != SessionPrefs.GAME_STORAGE_OFF) {
val problem = GameStorage.prepare(path)
if (problem != null) {
android.widget.Toast.makeText(this, "Not usable: $problem", android.widget.Toast.LENGTH_LONG).show()
@@ -147,7 +147,11 @@ object DeviceReport {
k("On-screen controls", SessionPrefs.oscMode(context))
k("Touch mode", SessionPrefs.touchMode(context))
k("Performance HUD", SessionPrefs.hudEnabled(context))
k("Game storage", SessionPrefs.gameStorage(context).ifEmpty { "internal only" })
k("Game storage", when (val g = SessionPrefs.gameStorage(context)) {
"" -> "automatic (" + (com.steamdeck.launcher.session.GameStorage.effective(context)?.path ?: "no card present") + ")"
SessionPrefs.GAME_STORAGE_OFF -> "internal only"
else -> g
})
h("Android process limits")
// Android 12 kills "phantom" processes - the children an app forks itself rather than
@@ -53,6 +53,16 @@ object GameStorage {
}
}
/** The root the session binds, from the setting: automatic = the first card in the phone. */
fun effective(context: Context): Option? {
val pref = SessionPrefs.gameStorage(context)
return when {
pref == SessionPrefs.GAME_STORAGE_OFF -> null
pref.isEmpty() -> options(context).firstOrNull()?.let { Option(it.label.substringBefore(" ·"), it.path) }
else -> Option(SessionPrefs.gameStorageLabel(context), pref)
}
}
/** The label the client shows for a chosen folder: its last name, or the volume's. */
fun labelFor(context: Context, path: String): String =
options(context).firstOrNull { it.path == path }?.label?.substringBefore(" ·")
@@ -229,10 +229,14 @@ object SessionPrefs {
/**
* A second Steam library on this device: the folder bound at /mnt/bannerlator-sd and
* registered with the client, which then asks where to install every game and shows both
* on its Storage page. "" = internal only (the client's own library, the default).
* on its Storage page. "" = automatic: the SD card when one is in the phone (the default,
* so the choice is made inside the client like anywhere else); GAME_STORAGE_OFF = internal
* only; otherwise the folder chosen.
*/
fun gameStorage(context: Context): String = prefs(context).getString("gameStorage", "") ?: ""
const val GAME_STORAGE_OFF = "off"
fun gameStorageLabel(context: Context): String = prefs(context).getString("gameStorageLabel", "SD Card") ?: "SD Card"
fun setGameStorage(context: Context, path: String, label: String) {
@@ -324,17 +324,19 @@ class SessionService : Service() {
// A second Steam library: the storage chosen in the Steam cog, at the path the runtime's
// bannerlator-steam-library registers with the client. Nothing bound = the script removes
// the entry, so the client never offers a place that is not there.
val library = SessionPrefs.gameStorage(this).takeIf { it.isNotEmpty() }
val library = GameStorage.effective(this)
if (library != null) {
val problem = GameStorage.prepare(library)
val problem = GameStorage.prepare(library.path)
if (problem == null) {
File(LinuxRuntime.rootDir(this), "mnt/bannerlator-sd").mkdirs()
binds.add("$library:/mnt/bannerlator-sd")
guest.add("BL_LIBRARY_LABEL=" + SessionPrefs.gameStorageLabel(this).replace('"', ' '))
Log.i(TAG, "game storage: $library -> /mnt/bannerlator-sd (\"${SessionPrefs.gameStorageLabel(this)}\")")
binds.add("${library.path}:/mnt/bannerlator-sd")
guest.add("BL_LIBRARY_LABEL=" + library.label.replace('"', ' '))
Log.i(TAG, "game storage: ${library.path} -> /mnt/bannerlator-sd (\"${library.label}\")")
} else {
Log.w(TAG, "game storage: $problem; internal only this session")
}
} else {
Log.i(TAG, "game storage: internal only")
}
val roms = SessionPrefs.romsDir(this).takeIf { it.isNotEmpty() }?.let { File(it) }
if (roms != null && roms.isDirectory && roms.canRead()) {
@@ -248,16 +248,21 @@ fun ModeSettingsDialog(s: ModeSettings, a: ModeSettingsActions) {
"storage" -> if (s.gameStorage != null) {
Section(
"Game storage",
"Where Steam may install games. Internal is the client's own library and always there. A second " +
"place is registered with the client at the next session start: it then asks where to install " +
"every game, lists both on its Storage page, and can move a game between them itself.",
"The choice itself is made inside Steam: with a second place registered, every Install asks which " +
"drive, Settings › Storage lists both, and Steam moves games between them. Internal is the " +
"client's own library and always there. Applies at the next session start.",
)
Spacer(Modifier.height(6.dp))
Choice("Internal only — the default", s.gameStorage.isEmpty()) { a.onGameStorage("", "") }
Choice(
"Automatic — the SD card when one is in the phone" +
(if (s.storageOptions.isEmpty()) " (none right now)" else ""),
s.gameStorage.isEmpty(),
) { a.onGameStorage("", "") }
Choice("Internal only", s.gameStorage == "off") { a.onGameStorage("off", "") }
for ((label, path) in s.storageOptions) {
Choice(label, s.gameStorage == path) { a.onGameStorage(path, label.substringBefore(" ·")) }
}
val custom = s.gameStorage.isNotEmpty() && s.storageOptions.none { it.second == s.gameStorage }
val custom = s.gameStorage.isNotEmpty() && s.gameStorage != "off" && s.storageOptions.none { it.second == s.gameStorage }
if (custom) Choice("Folder: ${s.gameStorage}", true) {}
OutlinedButton(onClick = a.onPickGameStorageFolder, modifier = Modifier.fillMaxWidth().padding(top = 8.dp)) {
Text("Choose a folder…")