mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 19:41:10 +00:00
fix: a folder of games with an extra non-game folder still lists each game (Rance 01) (#1822)
* fix: a folder of games still splits into its games when it also holds a folder with no game, as with Rance 01 in a VNs folder A visual-novel folder on game storage held a steamapps folder with no exe in it. The group check asked every subfolder to hold a game, so the whole folder stayed one game and a pick in Rance 01 switched that game instead of adding its own. Folders without a game and Steam library folders no longer count, and old games folders being moved to entries split such a folder the same way. * Check a folder's own entry, removal and store ownership before splitting it, and keep folders with redist or extras as one game An old Games folder that was removed keeps its games removed after the split, a folder already added stays as it is, and a store game's folder is never split. * fix: a split games folder keeps the game it launched and that game's settings When an old games folder entry is split into one entry per game, the folder holding the selected exe is kept even if ranking left it out (or the parent stays whole when the exe sits directly in it), and the parent's name, app ID, launch options and other settings carry over to that game without overwriting settings it already has. * fix: a split games folder never migrates steamapps and keeps the launched exe A saved exe under a steamapps or hidden folder keeps the old entry whole instead of adding that folder as a game, and the game that inherits the old entry's settings keeps the exe the old entry launched when nothing was picked.
This commit is contained in:
1 parent
18f6305977
commit
2ff682a4f5
6 files changed
+166
-21
No files matched your search
@@ -170,9 +170,9 @@ object AddedGames {
|
||||
val files = folder.listFiles() ?: return null
|
||||
if (files.any { it.isFile && it.name.endsWith(".exe", ignoreCase = true) && !skipExe(it.name) }) return null
|
||||
if (declaredExes(folder).isNotEmpty()) return null
|
||||
val dirs = files.filter { it.isDirectory && !it.name.startsWith(".") }.sortedBy { it.name.lowercase() }
|
||||
if (dirs.size < 2 || dirs.any { it.name.lowercase() in GAME_PARTS }) return null
|
||||
return dirs.takeIf { all -> all.all { rank(it).exes.isNotEmpty() } }
|
||||
val dirs = files.filter { it.isDirectory && !it.name.startsWith(".") && !isSteamLibraryFolder(it.name) }.sortedBy { it.name.lowercase() }
|
||||
if (dirs.any { it.name.lowercase() in GAME_PARTS || GameExePicker.auxiliaryDir(it) }) return null
|
||||
return dirs.filter { rank(it).exes.isNotEmpty() }.takeIf { it.size >= 2 }
|
||||
}
|
||||
|
||||
private fun expand(folder: File, entries: Set<String>): List<File> =
|
||||
|
||||
@@ -89,6 +89,8 @@ object GameExePicker {
|
||||
return score
|
||||
}
|
||||
|
||||
internal fun auxiliaryDir(dir: File): Boolean = skipDir(dir, dir.name)
|
||||
|
||||
private fun skipDir(dir: File, relative: String): Boolean {
|
||||
val name = dir.name.lowercase().trim()
|
||||
if (name in SKIP_DIRS || SKIP_DIR_PREFIX.any { name.startsWith(it) }) return true
|
||||
|
||||
@@ -24,15 +24,33 @@ object GamesFolderMigration {
|
||||
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 (AddedGames.isSteamLibraryFolder(folder.name) || 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++
|
||||
for (top in root.host.listFiles { f -> f.isDirectory }?.sortedBy { it.name.lowercase() }.orEmpty()) {
|
||||
if (AddedGames.isSteamLibraryFolder(top.name) || top.name.lowercase() in steamInstalls) continue
|
||||
if (top.absolutePath in storeRoots || StoreGameSidecar.read(top) != null) continue
|
||||
if (top.path in entries) continue
|
||||
val topPick = SessionPrefs.addedGameExe(context, top.path).takeIf { it.isNotEmpty() }?.let { File(it) }?.takeIf { it.isFile }
|
||||
val groups = AddedGames.groupGames(top)
|
||||
val played = topPick ?: groups?.let { AddedGames.rank(top).exes.firstOrNull() }
|
||||
val owner = played?.let { exe -> top.listFiles { f -> f.isDirectory }?.firstOrNull { exe.path.startsWith(it.path + File.separator) } }
|
||||
val folders = when {
|
||||
groups == null -> listOf(top)
|
||||
topPick == null || owner in groups -> groups
|
||||
owner == null || owner.name.startsWith(".") || AddedGames.isSteamLibraryFolder(owner.name) -> listOf(top)
|
||||
else -> (groups + owner).sortedBy { it.name.lowercase() }
|
||||
}
|
||||
for (folder in folders) {
|
||||
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 }
|
||||
?: topPick?.takeIf { it.path.startsWith(folder.path + File.separator) }
|
||||
val ranked = AddedGames.rank(folder).exes.firstOrNull()
|
||||
val exe = picked ?: played?.takeIf { folder == owner } ?: ranked ?: continue
|
||||
AddedExes.put(context, AddedExes.Entry(folder.path, exe.path, picked = picked != null || exe != ranked))
|
||||
entries.add(folder.path)
|
||||
if (folder != top && folder == owner) SessionPrefs.inheritAddedGame(context, top.path, folder.path)
|
||||
if (folder != top && top.path in removed) SessionPrefs.setAddedGameRemoved(context, folder.path, true)
|
||||
else if (folder.path !in removed) n++
|
||||
}
|
||||
}
|
||||
Log.i(TAG, "games: migrated ${root.host} → $n games")
|
||||
}
|
||||
|
||||
@@ -1023,21 +1023,34 @@ object SessionPrefs {
|
||||
if (previous != null && !p.contains("addedAppId:$folderPath")) {
|
||||
val moved = movedAddedGame(p.all, previous, folderPath)
|
||||
moved.keys.forEach { edit.remove(it) }
|
||||
moved.values.forEach { (name, value) ->
|
||||
when (value) {
|
||||
is String -> edit.putString(name, value)
|
||||
is Long -> edit.putLong(name, value)
|
||||
is Int -> edit.putInt(name, value)
|
||||
is Boolean -> edit.putBoolean(name, value)
|
||||
is Float -> edit.putFloat(name, value)
|
||||
}
|
||||
}
|
||||
moved.values.forEach { (name, value) -> edit.putValue(name, value) }
|
||||
val removed = removedAddedGames(context)
|
||||
if (previous in removed) edit.putString("removedAddedGames", removed.map { if (it == previous) folderPath else it }.joinToString("\n"))
|
||||
}
|
||||
edit.apply()
|
||||
}
|
||||
|
||||
fun inheritAddedGame(context: Context, from: String, to: String) = synchronized(addedGameLock) {
|
||||
val p = prefs(context)
|
||||
val all = p.all
|
||||
val edit = p.edit()
|
||||
all.filterKeys { it.startsWith("added") && it.endsWith(":$from") }.forEach { (name, value) ->
|
||||
val key = name.removeSuffix(from) + to
|
||||
if (key !in all) edit.putValue(key, value)
|
||||
}
|
||||
edit.apply()
|
||||
}
|
||||
|
||||
private fun android.content.SharedPreferences.Editor.putValue(name: String, value: Any?) {
|
||||
when (value) {
|
||||
is String -> putString(name, value)
|
||||
is Long -> putLong(name, value)
|
||||
is Int -> putInt(name, value)
|
||||
is Boolean -> putBoolean(name, value)
|
||||
is Float -> putFloat(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun movedAddedGame(all: Map<String, *>, from: String, to: String): Map<String, Pair<String, Any?>> =
|
||||
all.filterKeys { it.startsWith("added") && it.endsWith(":$from") }.mapValues { (name, value) ->
|
||||
val moved = if (value is String && (value == from || value.startsWith("$from/"))) to + value.removePrefix(from) else value
|
||||
|
||||
@@ -138,6 +138,19 @@ class AddedExesTest {
|
||||
SessionPrefs.setGameStorage(app, SessionPrefs.GAME_STORAGE_OFF, "")
|
||||
}
|
||||
|
||||
@Test fun aGroupFolderWithFoldersHoldingNoGameStillListsEachGame() {
|
||||
val storage = visualNovels()
|
||||
val vns = File(storage, "VNs")
|
||||
File(vns, "steamapps/common").mkdirs()
|
||||
File(vns, "Notes/readme.txt").apply { parentFile!!.mkdirs(); writeText("x") }
|
||||
assertEquals(listOf("Albatross", "Angel Beats", "Rance 01", "Sayonara").map { File(vns, it).path },
|
||||
AddedGames.scan(app).map { it.folder.path })
|
||||
val pick = File(vns, "Rance 01/Rance01.exe")
|
||||
assertEquals(pick.parentFile!!.path, result(AddedGames.addExe(app, pick)).folder.path)
|
||||
assertEquals(4, AddedGames.scan(app).size)
|
||||
SessionPrefs.setGameStorage(app, SessionPrefs.GAME_STORAGE_OFF, "")
|
||||
}
|
||||
|
||||
@Test fun anExePickedInAGroupFolderOnGameStorageIsItsOwnGame() {
|
||||
val storage = visualNovels()
|
||||
val vns = File(storage, "VNs")
|
||||
|
||||
@@ -67,6 +67,105 @@ class GamesFolderMigrationTest {
|
||||
assertTrue(GameStorage.gamesFolderLibraries(app).isEmpty())
|
||||
}
|
||||
|
||||
@Test fun aFolderOfGamesInAnOldGamesFolderBecomesOneEntryPerGame() {
|
||||
val pc = tmp.newFolder("PC")
|
||||
val rance = File(pc, "VNs/Rance 01"); exe(pc, "VNs/Rance 01/Rance01.exe")
|
||||
val albatross = File(pc, "VNs/Albatross"); exe(pc, "VNs/Albatross/Albatross Log.exe")
|
||||
File(pc, "VNs/steamapps").mkdirs()
|
||||
val solo = File(pc, "Solo"); exe(pc, "Solo/Solo.exe")
|
||||
SessionPrefs.setAddedGamesDirs(app, listOf(pc.path))
|
||||
GamesFolderMigration.run(app)
|
||||
assertEquals(setOf(albatross.path, rance.path, solo.path), AddedExes.list(app).map { it.folder }.toSet())
|
||||
assertEquals(setOf(albatross.path, rance.path, solo.path), AddedGames.scan(app).map { it.folder.path }.toSet())
|
||||
}
|
||||
|
||||
@Test fun aRemovedOrAlreadyAddedFolderOfGamesKeepsItsState() {
|
||||
val pc = tmp.newFolder("PC")
|
||||
val gone = File(pc, "Gone"); exe(pc, "Gone/A/a.exe"); exe(pc, "Gone/B/b.exe")
|
||||
val kept = File(pc, "Kept"); val keptExe = exe(pc, "Kept/A/a.exe"); exe(pc, "Kept/B/b.exe")
|
||||
val store = File(pc, "Store"); exe(pc, "Store/Content/game.exe"); exe(pc, "Store/Tools/tool.exe")
|
||||
StoreGameSidecar.file(store).writeText("""{"store":"gog","id":"2","title":"Store Game","exe":"Content/game.exe"}""")
|
||||
val solo = File(pc, "Solo"); exe(pc, "Solo/Extras/viewer.exe"); exe(pc, "Solo/Content/solo.exe")
|
||||
SessionPrefs.setAddedGamesDirs(app, listOf(pc.path))
|
||||
SessionPrefs.setAddedGameRemoved(app, gone.path, true)
|
||||
AddedExes.put(app, AddedExes.Entry(kept.path, keptExe.path))
|
||||
GamesFolderMigration.run(app)
|
||||
val entries = AddedExes.list(app).map { it.folder }.toSet()
|
||||
assertEquals(setOf(File(gone, "A").path, File(gone, "B").path, kept.path, solo.path), entries)
|
||||
assertTrue(SessionPrefs.removedAddedGames(app).containsAll(listOf(File(gone, "A").path, File(gone, "B").path)))
|
||||
assertEquals(setOf(kept.path, solo.path), AddedGames.scan(app).map { it.folder.path }.toSet())
|
||||
}
|
||||
|
||||
@Test fun aGameSelectedOutsideTheRankedFoldersStaysListed() {
|
||||
val pc = tmp.newFolder("PC")
|
||||
val vns = File(pc, "VNs")
|
||||
exe(pc, "VNs/A/a.exe"); exe(pc, "VNs/B/b.exe")
|
||||
val pick = exe(pc, "VNs/C/helper.exe")
|
||||
val loose = File(pc, "Loose"); exe(pc, "Loose/A/a.exe"); exe(pc, "Loose/B/b.exe")
|
||||
val loosePick = exe(pc, "Loose/setup.exe")
|
||||
val lib = File(pc, "Lib"); exe(pc, "Lib/A/a.exe"); exe(pc, "Lib/B/b.exe")
|
||||
val libPick = exe(pc, "Lib/steamapps/common/Tool/helper.exe")
|
||||
SessionPrefs.setAddedGamesDirs(app, listOf(pc.path))
|
||||
SessionPrefs.setAddedGameExe(app, vns.path, pick.path)
|
||||
SessionPrefs.setAddedGameExe(app, loose.path, loosePick.path)
|
||||
SessionPrefs.setAddedGameExe(app, lib.path, libPick.path)
|
||||
GamesFolderMigration.run(app)
|
||||
val entries = AddedExes.list(app).associateBy { it.folder }
|
||||
assertEquals(setOf(File(vns, "A").path, File(vns, "B").path, File(vns, "C").path, loose.path, lib.path), entries.keys)
|
||||
assertEquals(libPick.path, entries.getValue(lib.path).exe)
|
||||
assertEquals(pick.path, entries.getValue(File(vns, "C").path).exe)
|
||||
assertTrue(entries.getValue(File(vns, "C").path).picked)
|
||||
assertEquals(loosePick.path, entries.getValue(loose.path).exe)
|
||||
val games = AddedGames.scan(app).associateBy { it.folder.path }
|
||||
assertEquals(pick.path, games.getValue(File(vns, "C").path).exe.path)
|
||||
assertEquals(loosePick.path, games.getValue(loose.path).exe.path)
|
||||
}
|
||||
|
||||
@Test fun aSplitFolderHandsItsSettingsToTheGameItLaunched() {
|
||||
val pc = tmp.newFolder("PC")
|
||||
val vns = File(pc, "VNs")
|
||||
exe(pc, "VNs/Albatross/Albatross.exe")
|
||||
val pick = exe(pc, "VNs/Rance 01/Rance01.exe")
|
||||
File(pc, "VNs/Notes").mkdirs()
|
||||
val plain = File(pc, "Plain"); exe(pc, "Plain/A/a.exe"); exe(pc, "Plain/B/b.exe")
|
||||
SessionPrefs.setAddedGamesDirs(app, listOf(pc.path))
|
||||
SessionPrefs.setAddedGameExe(app, vns.path, pick.path)
|
||||
SessionPrefs.addedGameAppId(app, vns.path, 0x8111_2222L)
|
||||
SessionPrefs.setAddedGameName(app, vns.path, "Rance")
|
||||
SessionPrefs.setAddedGameLaunch(app, vns.path, "-windowed")
|
||||
SessionPrefs.setAddedGameName(app, File(vns, "Rance 01").path, "Rance 01 Remake")
|
||||
SessionPrefs.addedGameAppId(app, plain.path, 0x8333_4444L)
|
||||
GamesFolderMigration.run(app)
|
||||
val rance = File(vns, "Rance 01").path
|
||||
val games = AddedGames.scan(app).associateBy { it.folder.path }
|
||||
assertEquals(0x8111_2222L, games.getValue(rance).appId)
|
||||
assertEquals("Rance 01 Remake", games.getValue(rance).name)
|
||||
assertEquals("-windowed", games.getValue(rance).launchOptions)
|
||||
assertEquals(pick.path, games.getValue(rance).exe.path)
|
||||
assertTrue(games.getValue(File(vns, "Albatross").path).appId != 0x8111_2222L)
|
||||
assertEquals("", SessionPrefs.addedGameLaunch(app, File(vns, "Albatross").path))
|
||||
assertEquals(0x8333_4444L, games.getValue(File(plain, "A").path).appId)
|
||||
assertTrue(games.getValue(File(plain, "B").path).appId != 0x8333_4444L)
|
||||
}
|
||||
|
||||
@Test fun aSplitFolderKeepsTheExeItLaunchedWithoutAPick() {
|
||||
val pc = tmp.newFolder("PC")
|
||||
val vns = File(pc, "VNs")
|
||||
val launched = exe(pc, "VNs/Rance/VNsPlayer.exe"); exe(pc, "VNs/Rance/Rance.exe")
|
||||
val other = exe(pc, "VNs/Other/Other.exe")
|
||||
SessionPrefs.setAddedGamesDirs(app, listOf(pc.path))
|
||||
SessionPrefs.addedGameAppId(app, vns.path, 0x8555_6666L)
|
||||
GamesFolderMigration.run(app)
|
||||
val entries = AddedExes.list(app).associateBy { it.folder }
|
||||
assertEquals(launched.path, entries.getValue(File(vns, "Rance").path).exe)
|
||||
assertTrue(entries.getValue(File(vns, "Rance").path).picked)
|
||||
assertFalse(entries.getValue(File(vns, "Other").path).picked)
|
||||
val games = AddedGames.scan(app).associateBy { it.folder.path }
|
||||
assertEquals(launched.path, games.getValue(File(vns, "Rance").path).exe.path)
|
||||
assertEquals(0x8555_6666L, games.getValue(File(vns, "Rance").path).appId)
|
||||
assertEquals(other.path, games.getValue(File(vns, "Other").path).exe.path)
|
||||
}
|
||||
|
||||
@Test fun runningAgainChangesNothing() {
|
||||
val pc = tmp.newFolder("PC")
|
||||
exe(pc, "Alpha/Alpha.exe")
|
||||
|
||||
Reference in new issue
Block a user