From a2e596a2002a1f90f01d5aed5fe1cb4e68fb87cd Mon Sep 17 00:00:00 2001 From: The412Banner <205237651+The412Banner@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:15:56 -0400 Subject: [PATCH] Windows components: picked automatically by each game's source (#554) Each game's Windows components now come from the list its own source trusts, and the ones that list names turn on by themselves. The user's own on/off switches always win; each automatic row says why it is on. - Steam games: the Steamworks Shared redistributables Steam installs with the game (appmanifest SharedDepots, else the client's appinfo.vdf). Steam keeps its install scripts marked as already run. - GOG: the build manifest's dependencies. Epic: the manifest's prerequisite (Unreal's maps to VC++ and DirectX). Amazon: fuel.json PostInstall. Older GOG/Epic installs get their list fetched once with the saved sign-in; game files are never downloaded again. - Custom games (the + button, Add all, migrated folders): one shared Steam match per game, also used for artwork. An appid from the game's files turns components on; a name-only match stays Recommended; no match falls back to the folder scan. - Steam app info for apps the client never saw comes from an anonymous Steam PICS lookup (CM websocket, anonymous logon, product info), cached per appid; the user's Steam sign-in is never used. - Missing components download in the background on one low-priority worker that pauses during sessions and waits on battery saver, heat or a metered network unless the user asked. A game Steam installs during a session is readied right away so it can be played without ending the session; the launch waits up to 10 s only for a component still on its way. - Page: "On for this game" with reasons, a quiet "Checking what this game needs..." only for games never worked out, rows that ease in, pieces that need a download greyed out while offline. The intro names no other app. - The folder scan skips Unity's *_Data/Managed (Mono, not .NET Framework). --- .../com/droiddeck/launcher/MainActivity.kt | 46 +- .../launcher/frontend/AddedGameArt.kt | 25 +- .../launcher/frontend/AutoComponents.kt | 419 +++++++++ .../launcher/frontend/DependencyDetector.kt | 16 + .../launcher/frontend/SessionInstalls.kt | 95 ++ .../launcher/frontend/SteamAppInfo.kt | 202 ++++ .../droiddeck/launcher/frontend/SteamMatch.kt | 77 ++ .../droiddeck/launcher/frontend/SteamPics.kt | 334 +++++++ .../launcher/frontend/StoreListBackfill.kt | 98 ++ .../launcher/frontend/WinCompSources.kt | 160 ++++ .../launcher/session/SessionService.kt | 2 + .../launcher/session/WinComponents.kt | 144 ++- .../launcher/stores/StoreInstalls.kt | 3 + .../launcher/stores/epic/EpicBackend.kt | 6 +- .../stores/epic/EpicDownloadManager.java | 42 +- .../launcher/stores/gog/GogBackend.kt | 3 + .../stores/gog/GogDownloadManager.java | 56 +- .../droiddeck/launcher/ui/FrontEndGames.kt | 20 +- .../droiddeck/launcher/ui/FrontEndSetup.kt | 1 + .../launcher/ui/WinComponentsEditor.kt | 311 ++++-- app/src/main/res/values-es/strings.xml | 22 +- app/src/main/res/values-ja/strings.xml | 20 +- app/src/main/res/values-ko/strings.xml | 20 +- app/src/main/res/values-zh-rCN/strings.xml | 20 +- app/src/main/res/values-zh-rHK/strings.xml | 20 +- app/src/main/res/values-zh-rTW/strings.xml | 20 +- app/src/main/res/values/strings.xml | 21 +- .../frontend/AddedGameArtLookupTest.kt | 39 +- .../launcher/frontend/SteamPicsTest.kt | 95 ++ .../launcher/frontend/WinCompSourcesTest.kt | 465 +++++++++ .../test/resources/steam-pics/app_2054970.vdf | 882 ++++++++++++++++++ .../development/windows-components-sources.md | 116 +++ docs/progress-log.md | 61 ++ .../usr/local/bin/droiddeck-wincomponents | 40 +- tools/tests/test_wincomponents.py | 55 +- 35 files changed, 3810 insertions(+), 146 deletions(-) create mode 100644 app/src/main/java/com/droiddeck/launcher/frontend/AutoComponents.kt create mode 100644 app/src/main/java/com/droiddeck/launcher/frontend/SessionInstalls.kt create mode 100644 app/src/main/java/com/droiddeck/launcher/frontend/SteamAppInfo.kt create mode 100644 app/src/main/java/com/droiddeck/launcher/frontend/SteamMatch.kt create mode 100644 app/src/main/java/com/droiddeck/launcher/frontend/SteamPics.kt create mode 100644 app/src/main/java/com/droiddeck/launcher/frontend/StoreListBackfill.kt create mode 100644 app/src/main/java/com/droiddeck/launcher/frontend/WinCompSources.kt create mode 100644 app/src/test/java/com/droiddeck/launcher/frontend/SteamPicsTest.kt create mode 100644 app/src/test/java/com/droiddeck/launcher/frontend/WinCompSourcesTest.kt create mode 100644 app/src/test/resources/steam-pics/app_2054970.vdf create mode 100644 docs/development/windows-components-sources.md diff --git a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt index 6179f9f5a..d1f17bf1f 100644 --- a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt @@ -366,6 +366,8 @@ class MainActivity : ComponentActivity() { } private var romsDir by mutableStateOf(null) private var steamGames by mutableStateOf>(emptyList()) + @Volatile private var componentSweepDone = false + @Volatile private var componentSweepSession: String? = null private var emulatorList by mutableStateOf>(emptyList()) private var runningLabel by mutableStateOf(null) private var logsEnabled by mutableStateOf(true) @@ -616,7 +618,15 @@ class MainActivity : ComponentActivity() { .putExtra(SessionService.EXTRA_STEAM_UI, "desktop"), steamSession = true) }, // A store game may need something done first (an Epic game its sign-in code). - onSteamGame = { g -> if (shortcutPicker) chooseGameShortcut(g) else com.droiddeck.launcher.stores.StoreLaunch.prepare(this, g) { launchGame(g) } }, + onSteamGame = { g -> + if (shortcutPicker) chooseGameShortcut(g) + else { + // The launch takes the Windows components already on; the game's list is + // looked at again in the background once the session is over. + com.droiddeck.launcher.frontend.AutoComponents.beforeLaunch(this, g) + com.droiddeck.launcher.stores.StoreLaunch.prepare(this, g) { launchGame(g) } + } + }, onGameShortcut = { g -> com.droiddeck.launcher.frontend.GameShortcuts.pin(this, g) }, onExportGameFile = { g -> pickGameExport(g) }, onSyncGameFiles = { pickGameExport(null) }, @@ -1447,6 +1457,8 @@ class MainActivity : ComponentActivity() { val fresh = folders.mapNotNull { f -> com.droiddeck.launcher.frontend.AddedGames.single(this, f)?.let { com.droiddeck.launcher.frontend.Library.addedGame(this, it) } } + // An added or edited game's Windows components, from its source's list. + fresh.forEach { com.droiddeck.launcher.frontend.AutoComponents.refreshLater(this, it) } ui.post { val paths = folders.toSet() steamGames = (steamGames.filterNot { it.library == Library.ADDED && it.gameFiles?.path in paths } + fresh).distinctBy { it.gameId } @@ -1608,13 +1620,39 @@ class MainActivity : ComponentActivity() { val scanGeneration = ++libraryScanGeneration if (shortcutPicker) shortcutLibraryScanning = true Thread({ - // One update with the whole list: the wall places games by their position in it, so a - // partial list first would shuffle every capsule when the rest arrived. LibraryCache - // covers the wait. + // Steam's own games first, from their manifests alone (quick), beside the added games + // already listed: a game Steam installed during the session shows as the session ends, + // without waiting for the added games' walk below. Its Windows components' list is + // queued then too, on the low-priority worker, so its page is usually ready. + if (ready) { + val quick = runCatching { Library.steamGames(this) }.getOrNull() + if (quick != null) ui.post { + if (scanGeneration == libraryScanGeneration && steamGames.isNotEmpty()) { + val before = steamGames.map { it.gameId }.toSet() + val fresh = quick.filter { it.gameId !in before } + if (fresh.isNotEmpty()) { + steamGames = (quick + steamGames.filter { it.library == Library.ADDED }).distinctBy { it.gameId } + fresh.forEach { com.droiddeck.launcher.frontend.AutoComponents.queueGame(this, it, asked = false) } + } + } + } + } + // Then one update with the whole list: the wall places games by their position in it, + // so a partial list first would shuffle every capsule when the rest arrived. + // LibraryCache covers the wait. val games = if (ready) Library.launchableGames(this) else emptyList() val emus = Library.emulators(this) { id -> DesktopCatalog.installed(this, id) != null } val all = games.distinctBy { it.gameId } if (ready) com.droiddeck.launcher.frontend.LibraryCache.save(this, all) + // Once per app start: the Steam games' Windows components from Steam's lists. + // Queued only, after the launcher has been up a little; the worker takes them one at a time. + // And again after each session (a game installed in it, files changed). + val endedSession = SessionState.sessionId.takeIf { !SessionState.running && it != null && it != componentSweepSession } + if (ready && (!componentSweepDone || endedSession != null)) { + componentSweepDone = true + endedSession?.let { componentSweepSession = it } + ui.postDelayed({ Thread({ com.droiddeck.launcher.frontend.AutoComponents.sweep(this, all) }, "components-sweep").start() }, 10_000) + } ui.post { if (scanGeneration == libraryScanGeneration) { steamGames = all diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameArt.kt b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameArt.kt index a29e43679..1fb5bb272 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameArt.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/AddedGameArt.kt @@ -137,8 +137,7 @@ object AddedGameArt { val out = ArrayList