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:
MaxsTechReviewandxXJsonDeruloXx authored and GitHub committed 2026-10-10 13:46:03 -04:00
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 })
}
}