diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGames.kt b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGames.kt index 0c72812d1..aa10c0705 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGames.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGames.kt @@ -204,8 +204,10 @@ object AddedGames { 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 } + val entries = AddedExes.list(context) + fun owns(f: File) = holds(f) && sameGame(f, currentExe(context, entries, f), exe) + entries.map { File(it.folder) }.filter(::owns).maxByOrNull { canonical(it).length }?.let { return it } + SessionPrefs.removedAddedGames(context).map { File(it) }.filter(::owns).maxByOrNull { canonical(it).length }?.let { return it } StoreInstallRoot.gameFolders(context).firstOrNull(::holds)?.let { return it } val dirs = listOfNotNull( GameStorage.effective(context)?.let { File(it.path, "steamapps/common") }, @@ -220,6 +222,24 @@ object AddedGames { return null } + private fun currentExe(context: Context, entries: List, folder: File): File? = + SessionPrefs.addedGameExe(context, folder.path).takeIf { it.isNotEmpty() }?.let(::File) + ?: entries.firstOrNull { it.folder == folder.path }?.let { File(it.exe) } + ?: rank(folder).exes.firstOrNull() + + private fun sameGame(folder: File, current: File?, pick: File): Boolean { + val base = canonical(folder) + if (current == null || pick.parentFile?.let(::canonical) == base) return true + val known = canonical(current) + if (!inside(known, base) || known.substringBeforeLast('/') == base) return true + val picked = canonical(pick) + fun top(p: String) = p.removePrefix("$base/").substringBefore('/') + fun stem(p: String) = p.substringAfterLast('/').lowercase().removeSuffix(".exe").replace(EXE_BUILD, "").filter { it.isLetterOrDigit() } + return top(known) == top(picked) || stem(known).let { it.isNotEmpty() && it == stem(picked) } + } + + private val EXE_BUILD = Regex("([-_. ](x64|x86|win64|win32|64|32|shipping))+$") + fun addExe(context: Context, exe: File): AddResult { val app = context.applicationContext if (!exe.isFile) return AddResult.NotFound diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/AddedExesTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/AddedExesTest.kt index 6cc36e909..65949fec6 100644 --- a/app/src/test/java/com/droiddeck/launcher/frontend/AddedExesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/frontend/AddedExesTest.kt @@ -85,6 +85,54 @@ class AddedExesTest { assertEquals(listOf(File(games, "Example").path), AddedExes.list(app).map { it.folder }) } + @Test fun aGameInsideAFolderAddedAsOneGameIsAddedOnItsOwn() { + noGamesFolders() + val held = onStorage("Download/ADM/Death Stranding/ds.exe") + val pick = onStorage("Download/ADM/Pro Evolution Soccer 2017/PES2017.exe") + val container = held.parentFile!!.parentFile!! + AddedExes.put(app, AddedExes.Entry(container.path, held.path, picked = false)) + val added = AddedGames.addExe(app, pick) + assertTrue(added is AddedGames.AddResult.Added) + assertEquals(pick.parentFile!!.path, (added as AddedGames.AddResult.Added).game.folder.path) + assertEquals(setOf(held.path, pick.path), AddedGames.scan(app).map { it.exe.path }.toSet()) + } + + @Test fun aRemovedFolderGameDoesNotComeBackForAnotherGame() { + noGamesFolders() + val held = onStorage("Download/ADM/Death Stranding/ds.exe") + val pick = onStorage("Download/ADM/Blur Game/Blur.exe") + val container = held.parentFile!!.parentFile!! + AddedExes.put(app, AddedExes.Entry(container.path, held.path, picked = false)) + SessionPrefs.setAddedGameName(app, container.path, "Death Stranding Directors Cut") + AddedGames.remove(app, container) + val added = AddedGames.addExe(app, pick) + assertTrue(added is AddedGames.AddResult.Added) + assertEquals("Blur Game", (added as AddedGames.AddResult.Added).game.name) + assertTrue(container.path in SessionPrefs.removedAddedGames(app)) + assertEquals(listOf(pick.path), AddedGames.scan(app).map { it.exe.path }) + } + + @Test fun anotherBuildOfTheSameExeInASiblingFolderSwitchesTheGame() { + noGamesFolders() + val held = onStorage("Games2/Oddworld/x64/Oddworld.exe") + val other = onStorage("Games2/Oddworld/x86/Oddworld.exe") + val folder = held.parentFile!!.parentFile!! + AddedExes.put(app, AddedExes.Entry(folder.path, held.path, picked = false)) + val switched = AddedGames.addExe(app, other) + assertTrue(switched is AddedGames.AddResult.Switched) + assertEquals(folder.path, (switched as AddedGames.AddResult.Switched).game.folder.path) + assertEquals(listOf(other.path), AddedGames.scan(app).map { it.exe.path }) + } + + @Test fun aDifferentGameWithANumberedNameIsNotTakenAsAnotherBuild() { + noGamesFolders() + val held = onStorage("Games2/id/Doom/Doom.exe") + val pick = onStorage("Games2/id/Doom64/Doom64.exe") + AddedExes.put(app, AddedExes.Entry(held.parentFile!!.parentFile!!.path, held.path, picked = false)) + assertTrue(AddedGames.addExe(app, pick) is AddedGames.AddResult.Added) + assertEquals(2, AddedGames.scan(app).size) + } + @Test fun anExeTheSessionCannotSeeIsNotAdded() { noGamesFolders() val outside = exe(File(tmp.newFolder("elsewhere"), "Game/Game.exe"))