mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-15 03:37:09 -04:00
Games: old Games folders become entries, not Steam libraries (#542)
Games folders saved before the settings section went away were still registered as Steam libraries on Automatic, with no way left to remove them. Once, at app start and before any scan, each game such a folder listed becomes an entry of its own, keeping everything keyed by its folder (chosen exe, shortcut appid, name, Start in, launch options, art, removal); store installs and steamapps are skipped. The folders stay bound at their old guest paths, so no shortcut changes, and the saved roots are cleared. A Games folder stays a library only when Steam installed a game into it; the others leave the library list, and the runtime forgets them.
This commit is contained in:
1 parent
fe60b3c279
commit
abbcb8e3ad
12 files changed
+266
-30
No files matched your search
@@ -905,6 +905,7 @@ class MainActivity : ComponentActivity() {
|
||||
// A session folder left without its ending - the process was killed - gets it now.
|
||||
if (!SessionState.running) Thread({
|
||||
com.droiddeck.launcher.session.LogMigration.run(this)
|
||||
com.droiddeck.launcher.frontend.GamesFolderMigration.run(this)
|
||||
SessionLogShare.clear(this)
|
||||
SessionArtifacts.finishAbandoned(this)
|
||||
SessionArtifacts.scrubOlder(this)
|
||||
|
||||
@@ -43,22 +43,17 @@ object AddedGames {
|
||||
fun folderName(): String = folder.name
|
||||
}
|
||||
|
||||
/** One Games folder and where the session sees it. */
|
||||
/** A Games folder an earlier build had, and where the session sees it. */
|
||||
class Root(val host: File, val guest: String)
|
||||
|
||||
/**
|
||||
* The chosen folders with their guest paths: /root/Games/<folder name>, or with a short hash of
|
||||
* the host path when two chosen folders share a name (so a folder's guest path, and with it
|
||||
* every shortcut's appid, does not change when another folder is added or removed).
|
||||
* The Games folders earlier builds had, bound into the session at the guest paths they had
|
||||
* then (/root/Games/<name>), so the games in them (entries now, [GamesFolderMigration]) keep
|
||||
* their paths and shortcuts. Nothing adds one any more.
|
||||
*/
|
||||
fun roots(context: Context): List<Root> {
|
||||
val hosts = SessionPrefs.addedGamesDirs(context).map { File(it) }
|
||||
val names = hosts.groupingBy { it.name.lowercase() }.eachCount()
|
||||
return hosts.map { host ->
|
||||
val name = host.name.ifEmpty { "games" }
|
||||
val guestName = if ((names[name.lowercase()] ?: 0) > 1) name + "-" + "%08x".format(CRC32().apply { update(host.path.toByteArray()) }.value).take(4) else name
|
||||
Root(host, "$GUEST_DIR/$guestName")
|
||||
}
|
||||
GamesFolderMigration.run(context)
|
||||
return SessionPrefs.gameFolderBinds(context).map { (host, guest) -> Root(File(host), guest) }
|
||||
}
|
||||
|
||||
private val SKIP = Regex(
|
||||
@@ -189,7 +184,8 @@ object AddedGames {
|
||||
|
||||
fun scan(context: Context): List<Game> {
|
||||
val out = ArrayList<Game>()
|
||||
val folders = roots(context).map { it.host } + listOfNotNull(
|
||||
GamesFolderMigration.run(context)
|
||||
val folders = listOfNotNull(
|
||||
GameStorage.effective(context)?.let { File(it.path) },
|
||||
GameStorage.effective(context)?.let { File(it.path, "steamapps/common") },
|
||||
)
|
||||
@@ -263,17 +259,19 @@ object AddedGames {
|
||||
private fun inside(path: String, folder: String) = path.startsWith(folder.trimEnd('/') + "/")
|
||||
|
||||
/**
|
||||
* The game folder [exe] belongs to, without a scan: an entry from the Games tab, a removed
|
||||
* game, a store install, or the first-level folder under a Games folder (or the Steam library)
|
||||
* that holds it. Null when it is in none.
|
||||
* The game folder [exe] belongs to, without a scan: an entry from the Games tab (Games folders
|
||||
* of earlier builds included), a removed game, a store install, or the first-level folder under
|
||||
* the Steam library that holds it. Null when it is in none.
|
||||
*/
|
||||
private fun ownerFolder(context: Context, exe: File): File? {
|
||||
GamesFolderMigration.run(context)
|
||||
val path = canonical(exe)
|
||||
fun holds(f: File) = inside(path, canonical(f))
|
||||
AddedExes.list(context).map { File(it.folder) }.firstOrNull(::holds)?.let { return it }
|
||||
SessionPrefs.removedAddedGames(context).map { File(it) }.firstOrNull(::holds)?.let { return it }
|
||||
StoreInstallRoot.gameFolders(context).firstOrNull(::holds)?.let { return it }
|
||||
val dirs = roots(context).map { it.host } + listOfNotNull(
|
||||
// Games folders of earlier builds are entries now, found above.
|
||||
val dirs = listOfNotNull(
|
||||
GameStorage.effective(context)?.let { File(it.path, "steamapps/common") },
|
||||
GameStorage.effective(context)?.let { File(it.path) },
|
||||
)
|
||||
@@ -347,12 +345,13 @@ object AddedGames {
|
||||
|
||||
/**
|
||||
* The game folders already listed, without building any game: the first-level folders of the
|
||||
* Games folders and the Steam library (as [scan] walks them), the Games tab's entries and the
|
||||
* store installs. Canonical paths; removed games are left out.
|
||||
* Steam library (as [scan] walks them), the Games tab's entries (Games folders of earlier builds
|
||||
* included) and the store installs. Canonical paths; removed games are left out.
|
||||
*/
|
||||
private fun listedFolders(context: Context, removed: Set<String>): List<String> {
|
||||
val out = ArrayList<String>()
|
||||
val dirs = roots(context).map { it.host } + listOfNotNull(
|
||||
GamesFolderMigration.run(context)
|
||||
val dirs = listOfNotNull(
|
||||
GameStorage.effective(context)?.let { File(it.path) },
|
||||
GameStorage.effective(context)?.let { File(it.path, "steamapps/common") },
|
||||
)
|
||||
|
||||
@@ -48,7 +48,7 @@ object GameFileSync {
|
||||
check(File(previousStorage).isDirectory) { context.getString(R.string.game_sync_err_previous_sd) }
|
||||
}
|
||||
storage?.let { check(File(it.path).listFiles() != null) { context.getString(R.string.game_sync_err_game_storage) } }
|
||||
SessionPrefs.addedGamesDirs(context).forEach { check(File(it).listFiles() != null) { context.getString(R.string.game_sync_err_added_games) } }
|
||||
AddedGames.roots(context).forEach { check(it.host.listFiles() != null) { context.getString(R.string.game_sync_err_added_games) } }
|
||||
GameFiles.sync(context, folder, Library.launchableGames(context, strictRead = true))
|
||||
prefs(context).edit().putString("storageSetting", storageSetting).putString("storagePath", storage?.path).apply()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import com.droiddeck.launcher.stores.StoreGameSidecar
|
||||
import com.droiddeck.launcher.stores.StoreInstallRoot
|
||||
import java.io.File
|
||||
import java.util.zip.CRC32
|
||||
|
||||
/**
|
||||
* Games folders (the roots earlier builds let the user add in Steam settings) become what the
|
||||
* Games tab's "Add all games in this folder" makes: one entry per game ([AddedExes]). Everything
|
||||
* a game had is keyed by its own folder and stays as it was - the chosen exe, the shortcut appid,
|
||||
* the name, Start in, launch options, art choices, a removal. The roots stay bound into the
|
||||
* session at the same guest paths ([SessionPrefs.gameFolderBinds]), so every exe keeps its path
|
||||
* inside the session and every Steam shortcut its target; they are no longer Steam libraries.
|
||||
* Runs whenever saved roots remain (so twice is the same as once) and clears them after.
|
||||
*/
|
||||
object GamesFolderMigration {
|
||||
private const val TAG = "GamesFolderMigration"
|
||||
|
||||
@Synchronized
|
||||
fun run(context: Context) {
|
||||
val dirs = SessionPrefs.addedGamesDirs(context)
|
||||
if (dirs.isEmpty()) return
|
||||
val roots = legacyRoots(SessionPrefs.gameFolderBinds(context).map { it.first } + dirs)
|
||||
.filter { root -> root.host.path in dirs }
|
||||
// The binds first: a game's guest path (and with it the shortcut's appid) depends on them.
|
||||
SessionPrefs.setGameFolderBinds(context, SessionPrefs.gameFolderBinds(context) + roots.map { it.host.path to it.guest })
|
||||
val removed = SessionPrefs.removedAddedGames(context).toSet()
|
||||
val entries = AddedExes.list(context).map { it.folder }.toMutableSet()
|
||||
val storeRoots = StoreInstallRoot.roots(context).map { it.absolutePath }
|
||||
for (root in roots) {
|
||||
var n = 0
|
||||
val steamInstalls = AddedGames.steamInstallDirs(root.host)
|
||||
for (folder in root.host.listFiles { f -> f.isDirectory }?.sortedBy { it.name.lowercase() }.orEmpty()) {
|
||||
if (folder.name.equals("steamapps", ignoreCase = true) || folder.name.lowercase() in steamInstalls) continue
|
||||
if (folder.absolutePath in storeRoots || StoreGameSidecar.read(folder) != null) continue
|
||||
if (folder.path in entries) continue
|
||||
val picked = SessionPrefs.addedGameExe(context, folder.path).takeIf { it.isNotEmpty() }?.let { File(it) }?.takeIf { it.isFile }
|
||||
val exe = picked ?: AddedGames.rank(folder).exes.firstOrNull() ?: continue
|
||||
AddedExes.put(context, AddedExes.Entry(folder.path, exe.path, picked = picked != null))
|
||||
entries.add(folder.path)
|
||||
if (folder.path !in removed) n++
|
||||
}
|
||||
Log.i(TAG, "games: migrated ${root.host} → $n games")
|
||||
}
|
||||
SessionPrefs.setAddedGamesDirs(context, emptyList())
|
||||
SessionPrefs.setGamesFoldersMigrated(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* The guest paths earlier builds gave Games folders: /root/Games/<folder name>, with a short
|
||||
* hash of the host path when two share a name. Kept exactly, so nothing moves in the session.
|
||||
*/
|
||||
internal fun legacyRoots(paths: List<String>): List<AddedGames.Root> {
|
||||
val hosts = paths.distinct().map { File(it) }
|
||||
val names = hosts.groupingBy { it.name.lowercase() }.eachCount()
|
||||
return hosts.map { host ->
|
||||
val name = host.name.ifEmpty { "games" }
|
||||
val guestName = if ((names[name.lowercase()] ?: 0) > 1) name + "-" + "%08x".format(CRC32().apply { update(host.path.toByteArray()) }.value).take(4) else name
|
||||
AddedGames.Root(host, "${AddedGames.GUEST_DIR}/$guestName")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,7 +168,7 @@ object Lossless {
|
||||
val libraries = listOfNotNull(
|
||||
File(LinuxRuntime.rootDir(context), "root/.local/share/Steam"),
|
||||
GameStorage.effective(context)?.path?.let(::File),
|
||||
) + SessionPrefs.addedGamesDirs(context).map(::File)
|
||||
) + com.droiddeck.launcher.frontend.AddedGames.roots(context).map { it.host }
|
||||
return libraries.map { File(it, STEAM_DLL) }.filter { it.isFile }.maxByOrNull { it.lastModified() }
|
||||
}
|
||||
|
||||
|
||||
@@ -68,12 +68,19 @@ object GameStorage {
|
||||
|
||||
class Library(val host: File, val guest: String, val label: String)
|
||||
|
||||
/**
|
||||
* Games folders an earlier build registered as Steam libraries that Steam has installed games
|
||||
* into (an appmanifest in their steamapps/): those stay libraries, so the games are not lost.
|
||||
* Any other Games folder is no longer one; the session's library list leaves it out and the
|
||||
* runtime's droiddeck-steam-library forgets it.
|
||||
*/
|
||||
fun gamesFolderLibraries(context: Context): List<Library> {
|
||||
if (SessionPrefs.gameStorage(context).isNotEmpty()) return emptyList()
|
||||
val card = effective(context)?.let { canonical(File(it.path)) }
|
||||
val sm = context.getSystemService(StorageManager::class.java)
|
||||
return AddedGames.roots(context).mapNotNull { root ->
|
||||
val host = canonical(root.host)
|
||||
if (!hasInstalledGames(root.host)) return@mapNotNull null
|
||||
if ('"' in root.guest || '\\' in root.guest || !root.host.isDirectory || !root.host.canWrite()) return@mapNotNull null
|
||||
if (card != null && (host == card || host.startsWith("$card/") || card.startsWith("$host/"))) return@mapNotNull null
|
||||
val volume = try { sm?.getStorageVolume(root.host)?.getDescription(context) } catch (e: Exception) { null }
|
||||
@@ -83,6 +90,10 @@ object GameStorage {
|
||||
}
|
||||
}
|
||||
|
||||
/** Steam has installed a game into [root] as a library: a manifest in its steamapps/. */
|
||||
internal fun hasInstalledGames(root: File): Boolean =
|
||||
File(root, "steamapps").listFiles { f -> f.isFile && f.name.startsWith("appmanifest_") && f.name.endsWith(".acf") }?.isNotEmpty() == true
|
||||
|
||||
private fun canonical(file: File): String = runCatching { file.canonicalPath }.getOrDefault(file.absolutePath)
|
||||
|
||||
/** The label the client shows for a chosen folder: its last name, or the volume's. */
|
||||
|
||||
@@ -629,8 +629,9 @@ object SessionPrefs {
|
||||
}
|
||||
|
||||
/**
|
||||
* The folders of the user's own Windows games (one game per subfolder), any number of them
|
||||
* from anywhere on the device. The single folder an earlier build kept is carried in.
|
||||
* The Games folders earlier builds let the user add (one game per subfolder); read only by
|
||||
* GamesFolderMigration, which turns them into entries and clears them. The single folder an
|
||||
* even earlier build kept is carried in.
|
||||
*/
|
||||
fun addedGamesDirs(context: Context): List<String> {
|
||||
val p = prefs(context)
|
||||
@@ -644,6 +645,26 @@ object SessionPrefs {
|
||||
prefs(context).edit().putString("addedGamesDirs", dirs.distinct().joinToString("\n")).remove("addedGamesDir").apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* The Games folders earlier builds had (host path, guest path), still bound into the session so
|
||||
* their games keep their paths; written once by GamesFolderMigration.
|
||||
*/
|
||||
fun gameFolderBinds(context: Context): List<Pair<String, String>> =
|
||||
(prefs(context).getString("gameFolderBinds", "") ?: "").split('\n').mapNotNull { line ->
|
||||
line.split('\t').takeIf { it.size == 2 && it[0].isNotEmpty() && it[1].isNotEmpty() }?.let { it[0] to it[1] }
|
||||
}
|
||||
|
||||
fun setGameFolderBinds(context: Context, binds: List<Pair<String, String>>) {
|
||||
prefs(context).edit().putString("gameFolderBinds", binds.distinctBy { it.first }.joinToString("\n") { "${it.first}\t${it.second}" }).apply()
|
||||
}
|
||||
|
||||
/** The Games folders were turned into entries (GamesFolderMigration); a record, not a gate. */
|
||||
fun gamesFoldersMigrated(context: Context): Boolean = prefs(context).getBoolean("gamesFoldersMigrated", false)
|
||||
|
||||
fun setGamesFoldersMigrated(context: Context) {
|
||||
prefs(context).edit().putBoolean("gamesFoldersMigrated", true).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Game folders (by path) the user removed from the added games: a scan skips them, so they
|
||||
* stay off the Games tab and out of Steam until restored. Nothing on disk is touched.
|
||||
|
||||
@@ -83,7 +83,8 @@ class AddedExesTest {
|
||||
val result = AddedGames.addExe(app, deep)
|
||||
assertTrue(result is AddedGames.AddResult.Switched)
|
||||
assertEquals(deep.path, AddedGames.scan(app).single().exe.path)
|
||||
assertEquals(0, AddedExes.list(app).size)
|
||||
// The Games folder's game is an entry of its own (GamesFolderMigration); no second one.
|
||||
assertEquals(listOf(File(games, "Example").path), AddedExes.list(app).map { it.folder })
|
||||
}
|
||||
|
||||
@Test fun anExeTheSessionCannotSeeIsNotAdded() {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import com.droiddeck.launcher.session.GameStorage
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import com.droiddeck.launcher.stores.StoreGameSidecar
|
||||
import java.io.File
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
/** Games folders from earlier builds become entries, every game keeping what it had. */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [33])
|
||||
class GamesFolderMigrationTest {
|
||||
private val app get() = RuntimeEnvironment.getApplication()
|
||||
@get:Rule val tmp = TemporaryFolder()
|
||||
|
||||
private fun exe(root: File, rel: String) = File(root, rel).apply { parentFile!!.mkdirs(); writeBytes(ByteArray(16)) }
|
||||
|
||||
@Before fun setUp() {
|
||||
SessionPrefs.setGameStorage(app, "", "")
|
||||
AddedGamesPublisher.now = true
|
||||
}
|
||||
|
||||
@Test fun eachGameBecomesAnEntryKeepingItsExeAppidNameAndRemoval() {
|
||||
val pc = tmp.newFolder("PC")
|
||||
val alpha = File(pc, "Alpha"); exe(pc, "Alpha/Alpha.exe")
|
||||
val beta = File(pc, "Beta"); exe(pc, "Beta/Beta.exe"); val pick = exe(pc, "Beta/bin/Beta64.exe")
|
||||
val gone = File(pc, "Gone"); exe(pc, "Gone/Gone.exe")
|
||||
exe(pc, "steamapps/common/Installed/Installed.exe")
|
||||
val store = File(pc, "Store"); exe(pc, "Store/Store.exe")
|
||||
StoreGameSidecar.file(store).writeText("""{"store":"gog","id":"1","title":"Store Game","exe":"Store.exe"}""")
|
||||
File(pc, "Notes").mkdirs()
|
||||
SessionPrefs.setAddedGamesDirs(app, listOf(pc.path))
|
||||
// What earlier scans had kept, by folder.
|
||||
SessionPrefs.setAddedGameExe(app, beta.path, pick.path)
|
||||
SessionPrefs.addedGameAppId(app, alpha.path, 0x8123_4567L)
|
||||
SessionPrefs.addedGameAppId(app, beta.path, 0x8765_4321L)
|
||||
SessionPrefs.setAddedGameName(app, alpha.path, "Alpha GOTY")
|
||||
SessionPrefs.setAddedGameRemoved(app, gone.path, true)
|
||||
|
||||
GamesFolderMigration.run(app)
|
||||
|
||||
assertEquals(emptyList<String>(), SessionPrefs.addedGamesDirs(app))
|
||||
assertTrue(SessionPrefs.gamesFoldersMigrated(app))
|
||||
val entries = AddedExes.list(app).associateBy { it.folder }
|
||||
assertEquals(setOf(alpha.path, beta.path, gone.path), entries.keys)
|
||||
assertTrue(entries.getValue(beta.path).picked)
|
||||
assertEquals(pick.path, entries.getValue(beta.path).exe)
|
||||
assertFalse(entries.getValue(alpha.path).picked)
|
||||
|
||||
val games = AddedGames.scan(app).associateBy { it.folder.path }
|
||||
assertEquals(setOf(alpha.path, beta.path), games.keys)
|
||||
assertEquals(0x8123_4567L, games.getValue(alpha.path).appId)
|
||||
assertEquals(0x8765_4321L, games.getValue(beta.path).appId)
|
||||
assertEquals("Alpha GOTY", games.getValue(alpha.path).name)
|
||||
assertEquals(pick.path, games.getValue(beta.path).exe.path)
|
||||
// The same path inside the session as before: the shortcut keeps its target.
|
||||
assertEquals(AddedGames.GUEST_DIR + "/PC/Alpha/Alpha.exe", games.getValue(alpha.path).guestExe)
|
||||
assertEquals(AddedGames.GUEST_DIR + "/PC/Beta/bin/Beta64.exe", games.getValue(beta.path).guestExe)
|
||||
assertTrue(SessionPrefs.removedAddedGames(app).contains(gone.path))
|
||||
// Not a library any more.
|
||||
assertTrue(GameStorage.gamesFolderLibraries(app).isEmpty())
|
||||
}
|
||||
|
||||
@Test fun runningAgainChangesNothing() {
|
||||
val pc = tmp.newFolder("PC")
|
||||
exe(pc, "Alpha/Alpha.exe")
|
||||
SessionPrefs.setAddedGamesDirs(app, listOf(pc.path))
|
||||
GamesFolderMigration.run(app)
|
||||
val entries = AddedExes.list(app).map { it.folder to it.exe }
|
||||
val binds = SessionPrefs.gameFolderBinds(app)
|
||||
val before = AddedGames.scan(app).map { it.appId to it.guestExe }
|
||||
GamesFolderMigration.run(app)
|
||||
GamesFolderMigration.run(app)
|
||||
assertEquals(entries, AddedExes.list(app).map { it.folder to it.exe })
|
||||
assertEquals(binds, SessionPrefs.gameFolderBinds(app))
|
||||
assertEquals(before, AddedGames.scan(app).map { it.appId to it.guestExe })
|
||||
assertEquals(listOf(pc.path to AddedGames.GUEST_DIR + "/PC"), binds)
|
||||
}
|
||||
|
||||
@Test fun twoFoldersOfOneNameKeepTheirOwnGuestPaths() {
|
||||
val a = File(tmp.newFolder("a"), "Games").apply { mkdirs() }
|
||||
val b = File(tmp.newFolder("b"), "Games").apply { mkdirs() }
|
||||
exe(a, "One/One.exe")
|
||||
exe(b, "Two/Two.exe")
|
||||
SessionPrefs.setAddedGamesDirs(app, listOf(a.path, b.path))
|
||||
val guests = GamesFolderMigration.legacyRoots(listOf(a.path, b.path)).map { it.guest }
|
||||
val games = AddedGames.scan(app).associateBy { it.name }
|
||||
assertEquals(guests[0] + "/One/One.exe", games.getValue("One").guestExe)
|
||||
assertEquals(guests[1] + "/Two/Two.exe", games.getValue("Two").guestExe)
|
||||
assertTrue(guests[0] != guests[1])
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,14 @@ class GameStorageTest {
|
||||
private val app get() = RuntimeEnvironment.getApplication()
|
||||
@get:Rule val tmp = TemporaryFolder()
|
||||
|
||||
@Test fun gamesFoldersBecomeLibrariesOnAutomatic() {
|
||||
@Test fun onlyAGamesFolderSteamInstalledIntoStaysALibrary() {
|
||||
val pc = tmp.newFolder("PC")
|
||||
File(pc, "steamapps").mkdirs()
|
||||
File(pc, "steamapps/appmanifest_220.acf").writeText("\"AppState\" { \"appid\" \"220\" }")
|
||||
val plain = tmp.newFolder("Games")
|
||||
File(plain, "steamapps/common").mkdirs()
|
||||
val missing = File(tmp.root, "Unplugged")
|
||||
SessionPrefs.setAddedGamesDirs(app, listOf(pc.path, missing.path))
|
||||
SessionPrefs.setAddedGamesDirs(app, listOf(pc.path, plain.path, missing.path))
|
||||
val libraries = GameStorage.gamesFolderLibraries(app)
|
||||
assertEquals(listOf(pc), libraries.map { it.host })
|
||||
assertEquals(listOf(AddedGames.GUEST_DIR + "/PC"), libraries.map { it.guest })
|
||||
@@ -28,7 +32,10 @@ class GameStorageTest {
|
||||
}
|
||||
|
||||
@Test fun aChosenLibraryOrInternalKeepsGamesFoldersPlain() {
|
||||
SessionPrefs.setAddedGamesDirs(app, listOf(tmp.newFolder("PC").path))
|
||||
val pc = tmp.newFolder("PC")
|
||||
File(pc, "steamapps").mkdirs()
|
||||
File(pc, "steamapps/appmanifest_220.acf").writeText("x")
|
||||
SessionPrefs.setAddedGamesDirs(app, listOf(pc.path))
|
||||
SessionPrefs.setGameStorage(app, SessionPrefs.GAME_STORAGE_OFF, "")
|
||||
assertTrue(GameStorage.gamesFolderLibraries(app).isEmpty())
|
||||
SessionPrefs.setGameStorage(app, tmp.newFolder("chosen").path, "Chosen")
|
||||
@@ -36,7 +43,9 @@ class GameStorageTest {
|
||||
}
|
||||
|
||||
@Test fun namesTheClientCannotStoreAreLeftOut() {
|
||||
SessionPrefs.setAddedGamesDirs(app, listOf(tmp.newFolder("Quote\"d").path, tmp.newFolder("Back\\slash").path))
|
||||
val odd = listOf(tmp.newFolder("Quote\"d"), tmp.newFolder("Back\\slash"))
|
||||
odd.forEach { File(it, "steamapps").mkdirs(); File(it, "steamapps/appmanifest_220.acf").writeText("x") }
|
||||
SessionPrefs.setAddedGamesDirs(app, odd.map { it.path })
|
||||
assertTrue(GameStorage.gamesFolderLibraries(app).isEmpty())
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,15 @@ Steam client that runs under `droiddeck-proton-arm64`. They show with the Custom
|
||||
already listed are counted, not added. A summary follows: games added, how many were
|
||||
there already, one row per game (cover, name, exe, "?" when unsure); a row opens that
|
||||
game's editor, straight on Target when unsure, with back to the summary.
|
||||
- Games folders added before this (Steam settings had a Games section) keep listing their
|
||||
games; there is no place to add a new one.
|
||||
- Games folders added before this (Steam settings had a Games section) are migrated once
|
||||
(`GamesFolderMigration`, at app start and before any scan): each game in one becomes an entry
|
||||
as "Add all games in this folder" would make it, keeping everything keyed by its folder (the
|
||||
chosen exe, the shortcut appid, name, Start in, launch options, art, a removal). The folders
|
||||
stay bound into the session at the guest paths they had (`/root/Games/<name>`,
|
||||
`SessionPrefs.gameFolderBinds`), so no exe path or shortcut changes. They are no longer Steam
|
||||
libraries - the session's library list leaves them out and `droiddeck-steam-library` forgets
|
||||
them - unless Steam installed a game into one (a manifest in its `steamapps/`), which then
|
||||
stays a library. Log: `games: migrated <root> → N games`.
|
||||
|
||||
## Exe and name
|
||||
|
||||
|
||||
@@ -7,6 +7,25 @@ the timeline, then lessons and backlog. Companion to the README (what the app *d
|
||||
|
||||
---
|
||||
|
||||
## 2026-10-10 - `fix/games-folder-migration`: Games folders become entries, not Steam libraries
|
||||
|
||||
- **Seen on a device:** Steam's Install dialog listed "Games (Internal shared storage)" and "Games
|
||||
(android)", the user's two Games folders saved before #492, still registered as libraries
|
||||
while Second library is Automatic; with the settings section gone they could not be removed.
|
||||
- **Migration** (`GamesFolderMigration`, once at app start and before any scan, idempotent):
|
||||
every game a Games folder listed becomes an entry (`AddedExes`), keeping its chosen exe, stored
|
||||
appid, name / Start in / launch-option overrides, art choices and removal (all keyed by its
|
||||
folder). Store installs and `steamapps` are skipped. The folders stay bound at their old guest
|
||||
paths (`SessionPrefs.gameFolderBinds`), so the listing and every Steam shortcut are unchanged;
|
||||
`addedGamesDirs` is cleared. Log: `games: migrated <root> → N games`.
|
||||
- **Libraries:** `GameStorage.gamesFolderLibraries` returns only a Games folder Steam installed
|
||||
into (an appmanifest in its `steamapps/`); the others drop out of `BL_STEAM_LIBRARIES` and the
|
||||
runtime's `droiddeck-steam-library` forgets the `/root/Games/...` entries it had registered.
|
||||
The user's two folders have `steamapps/` without manifests (checked on the device), so both go.
|
||||
- **Tests:** `GamesFolderMigrationTest` (exe / appid / name / removal kept, guest paths unchanged,
|
||||
idempotent, store and steamapps skipped, libraries empty, same-name folders), `GameStorageTest`
|
||||
updated. Not yet on a device.
|
||||
|
||||
## 2026-10-09 - `feat/games-add-edit`: add games from the Games tab, edit them like Steam's Properties
|
||||
|
||||
- **Steam ⚙ settings:** the Games section is gone (it was a feature, not a setting). Games
|
||||
|
||||
Reference in new issue
Block a user