mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 19:41:10 +00:00
Launch the executable a Store package declares (#400)
Store and GDK folders name their game in MicrosoftGame.config or appxmanifest.xml. Picking the top-level Unreal bootstrap instead failed with "CreateProcess() returned 2" when the Store had renamed the shipping exe. Co-authored-by: xXJsonDeruloXx <danielhimebauch@gmail.com>
This commit is contained in:
1 parent
0043cefab5
commit
c1c22dfd37
2 files changed
+73
-6
No files matched your search
@@ -98,18 +98,26 @@ object AddedGames {
|
||||
return null
|
||||
}
|
||||
|
||||
/** The .exe files a game folder offers, best first ([GameExePicker]); also what its exe box lists, in that order. */
|
||||
/** The .exe files a game folder offers, best first ([GameExePicker]); declared Store exes lead. */
|
||||
fun candidates(folder: File): List<File> = rank(folder).exes
|
||||
|
||||
/** [GameExePicker]'s ranking of [folder], without the names this list never offers. */
|
||||
/** [GameExePicker]'s ranking, with a Store-declared executable taking precedence when present. */
|
||||
fun rank(folder: File): GameExePicker.Ranking {
|
||||
val key = "${folder.path}|${folder.lastModified()}"
|
||||
val now = System.currentTimeMillis()
|
||||
ranked[key]?.takeIf { now - it.first < RANK_FRESH_MS }?.let { return it.second }
|
||||
return GameExePicker.rank(folder) { !SKIP.matches(it.name) }.also {
|
||||
if (ranked.size > 256) ranked.clear()
|
||||
ranked[key] = now to it
|
||||
}
|
||||
val guessed = GameExePicker.rank(folder) { !SKIP.matches(it.name) }
|
||||
val declared = declaredExes(folder)
|
||||
val preferred = declared.map { it.path }.toSet()
|
||||
val result = if (declared.isEmpty()) guessed else GameExePicker.Ranking(
|
||||
declared + guessed.exes.filter { it.path !in preferred },
|
||||
List(declared.size) { Int.MAX_VALUE } + guessed.exes.zip(guessed.scores)
|
||||
.filter { it.first.path !in preferred }.map { it.second },
|
||||
uncertain = false,
|
||||
)
|
||||
if (ranked.size > 256) ranked.clear()
|
||||
ranked[key] = now to result
|
||||
return result
|
||||
}
|
||||
|
||||
/** A folder's ranking for a minute, so one add (rank, then the game built from it) walks it once. */
|
||||
@@ -160,6 +168,24 @@ object AddedGames {
|
||||
else -> peName
|
||||
}
|
||||
}
|
||||
/** The executables MicrosoftGame.config or appxmanifest.xml declares that exist inside [folder]. */
|
||||
internal fun declaredExes(folder: File): List<File> {
|
||||
val names = DECLARED.flatMap { (file, pattern) ->
|
||||
runCatching { pattern.findAll(File(folder, file).readText()).map { it.groupValues[1] }.toList() }.getOrDefault(emptyList())
|
||||
}
|
||||
val root = runCatching { folder.canonicalPath + File.separator }.getOrNull() ?: return emptyList()
|
||||
return names.asSequence()
|
||||
.map { File(folder, it.trim().replace('\\', '/').trimStart('/')) }
|
||||
.filter { it.name.endsWith(".exe", ignoreCase = true) && !SKIP.matches(it.name) && it.isFile }
|
||||
.filter { runCatching { it.canonicalPath.startsWith(root) }.getOrDefault(false) }
|
||||
.distinctBy { it.path }
|
||||
.toList()
|
||||
}
|
||||
|
||||
private val DECLARED = listOf(
|
||||
"MicrosoftGame.config" to Regex("<Executable\\b[^>]*?\\bName\\s*=\\s*\"([^\"]+)\"", RegexOption.IGNORE_CASE),
|
||||
"appxmanifest.xml" to Regex("<Application\\b[^>]*?\\bExecutable\\s*=\\s*\"([^\"]+)\"", RegexOption.IGNORE_CASE),
|
||||
)
|
||||
|
||||
fun scan(context: Context): List<Game> {
|
||||
val out = ArrayList<Game>()
|
||||
|
||||
@@ -27,4 +27,45 @@ class AddedGamesScanTest {
|
||||
SessionPrefs.setAddedGamesDirs(app, listOf(games.path))
|
||||
assertEquals(listOf("Example"), AddedGames.scan(app).map { it.name })
|
||||
}
|
||||
|
||||
@Test fun aStorePackageLaunchesTheExecutableItDeclares() {
|
||||
val game = tmp.newFolder("Minecraft Dungeons")
|
||||
File(game, "Dungeons.exe").writeText("unreal bootstrap")
|
||||
File(game, "Dungeons/Binaries/Win64").mkdirs()
|
||||
File(game, "Dungeons/Binaries/Win64/Dungeons.exe").writeText("the game, renamed from Dungeons-Win64-Shipping.exe")
|
||||
File(game, "MicrosoftGame.config").writeText(
|
||||
"<Game><ExecutableList>\n <Executable Name=\"Dungeons\\Binaries\\Win64\\Dungeons.exe\"\n\n Id=\"Game\" />\n</ExecutableList></Game>",
|
||||
)
|
||||
assertEquals(File(game, "Dungeons/Binaries/Win64/Dungeons.exe").path, AddedGames.candidates(game).first().path)
|
||||
assertEquals(2, AddedGames.candidates(game).size)
|
||||
}
|
||||
|
||||
@Test fun anAppxManifestNamesTheGameWhenThereIsNoConfig() {
|
||||
val game = tmp.newFolder("Store Game")
|
||||
File(game, "Launcher.exe").writeText("bootstrap")
|
||||
File(game, "Game/Binaries/Win64").mkdirs()
|
||||
File(game, "Game/Binaries/Win64/Game.exe").writeText("game")
|
||||
File(game, "appxmanifest.xml").writeText("<Package><Applications><Application Id=\"Game\" Executable=\"Game\\Binaries\\Win64\\Game.exe\" /></Applications></Package>")
|
||||
assertEquals(File(game, "Game/Binaries/Win64/Game.exe").path, AddedGames.candidates(game).first().path)
|
||||
}
|
||||
|
||||
@Test fun aDeclaredTopLevelExecutableKeepsItsPlace() {
|
||||
val game = tmp.newFolder("Minecraft Dungeons II")
|
||||
File(game, "Dungeons.exe").writeText("bootstrap")
|
||||
File(game, "Other.exe").writeText("a much longer other executable")
|
||||
File(game, "MicrosoftGame.config").writeText("<Executable Name=\"Dungeons.exe\" Id=\"App\" />")
|
||||
assertEquals(listOf("Dungeons.exe", "Other.exe"), AddedGames.candidates(game).map { it.name })
|
||||
}
|
||||
|
||||
@Test fun declaredHelpersMissingFilesAndPathsOutsideTheFolderAreIgnored() {
|
||||
val game = tmp.newFolder("Pass Game")
|
||||
File(game, "PassGame.exe").writeText("game")
|
||||
File(game, "gamelaunchhelper.exe").writeText("needs gaming services")
|
||||
tmp.newFile("Outside.exe").writeText("not this game")
|
||||
File(game, "MicrosoftGame.config").writeText(
|
||||
"<Executable Name=\"gamelaunchhelper.exe\" /><Executable Name=\"Missing.exe\" /><Executable Name=\"..\\Outside.exe\" />",
|
||||
)
|
||||
assertEquals(emptyList<File>(), AddedGames.declaredExes(game))
|
||||
assertEquals(listOf("PassGame.exe"), AddedGames.candidates(game).map { it.name })
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user