fix: store installs stay shortcuts instead of becoming Steam installs (#816)

This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-10-14 10:10:12 -04:00
1 parent 799be037e5
commit 9838c83fc2
4 files changed
+61 -1

No files matched your search

@@ -392,6 +392,7 @@ object AddedGames {
.append(",\"dir\":").append(quote(g.guestDir)).append(",\"appid\":").append(g.appId)
.append(",\"seen\":").append(SessionPrefs.addedGameExeSeen(context, g.folder.path))
if (g.launchOptions.isNotEmpty()) json.append(",\"launch\":").append(quote(g.launchOptions))
if (g.source != Library.ADDED) json.append(",\"store\":").append(quote(g.source))
val art = AddedGameArt.resolve(context, g)
val pieces = listOf("p" to art.portrait, "header" to art.header, "hero" to art.hero, "logo" to art.logo, "icon" to art.icon)
.mapNotNull { (k, f) -> f?.let { file -> artGuestPath(context, file)?.let { k to it } } }
@@ -62,5 +62,6 @@ class AddedGamesStoreScanTest {
val listing = AddedGames.writeListing(app, games).readText()
assertTrue(listing.contains("\"name\":\"Celeste\""))
assertTrue(listing.contains(StoreLaunch.LAUNCHER))
assertTrue(listing.contains("\"store\":\"${Store.EPIC.id}\""))
}
}
@@ -62,6 +62,12 @@ def source_stamp(game):
return None
def store_game(game):
"""Games a store client installed belong to that store: the Steam copy of the same title is
another license with its own files, and an import would let Steam update over the store's."""
return bool(game.get("store"))
def manifest_ids(steamapps, name):
matches = []
for manifest in Path(steamapps).glob("appmanifest_*.acf"):
@@ -178,7 +184,7 @@ def unimported(steam_root, snapshot, candidates):
def watch(steam_root, listing):
games = read_json(listing, [])
games = [game for game in read_json(listing, []) if not store_game(game)]
if os.environ.get("BL_STEAM_OFFLINE") == "1" or not games:
return
candidates, sources = {}, {}
@@ -363,6 +369,22 @@ def seed_install(steam_root, game, appid):
return True
def release(steam_root, game):
"""Undoes an import made before store games were skipped. Only our symlink and manifest go;
the store's folder stays where it is."""
steamapps = Path(steam_root) / "steamapps"
sources = {game.get("folder", ""), legacy_path(game.get("folder", ""))}
for target in sorted((steamapps / "common").glob("DroidDeck-*")):
appid = target.name.removeprefix("DroidDeck-")
if not (appid.isdigit() and target.is_symlink() and os.readlink(target) in sources):
continue
for manifest in (steamapps / f"appmanifest_{appid}.acf", steamapps / PARKED / f"appmanifest_{appid}.acf"):
if manifest.is_file() and ours(manifest, appid):
manifest.unlink()
target.unlink()
print(f"droiddeck-steam-games: {game.get('name', appid)} came from {game['store']}; it is a shortcut again, not Steam app {appid}")
def route(steam_root, acct, games):
snapshot = read_json(Path(acct) / "config" / STATE, {})
if not isinstance(snapshot, dict):
@@ -379,6 +401,13 @@ def route(steam_root, acct, games):
except OSError as error:
print(f"droiddeck-steam-games: set-aside installs not restored ({error})")
for game in games:
if store_game(game):
try:
release(steam_root, game)
except OSError as error:
print(f"droiddeck-steam-games: store import not undone ({error})")
shortcuts.append(game)
continue
folder = game.get("folder", "")
appid = candidates.get(folder, candidates.get(legacy_path(folder)))
stamp = source_stamp(game)
+29
View File
@@ -55,6 +55,35 @@ class ImportsTest(unittest.TestCase):
self.assertEqual(self.folder, (self.steam / 'steamapps/common/DroidDeck-42').resolve())
self.assertEqual(b'game', self.exe.read_bytes())
def test_store_installs_stay_shortcuts_even_when_owned_on_steam(self):
self.snapshot()
game = dict(self.game, store='amazon')
games, routes = imports.route(self.steam, self.acct, [game])
self.assertEqual([game], games)
self.assertEqual({}, routes)
self.assertFalse((self.steam / 'steamapps/appmanifest_42.acf').exists())
self.assertFalse(os.path.lexists(self.steam / 'steamapps/common/DroidDeck-42'))
def test_store_installs_imported_earlier_are_released(self):
self.snapshot()
imports.route(self.steam, self.acct, [self.game])
unrelated = self.steam / 'steamapps/appmanifest_7.acf'
unrelated.write_text('"AppState"\n{\n "appid" "7"\n "installdir" "Other"\n}\n')
games, routes = imports.route(self.steam, self.acct, [dict(self.game, store='epic')])
self.assertEqual({}, routes)
self.assertEqual(1, len(games))
self.assertFalse((self.steam / 'steamapps/appmanifest_42.acf').exists())
self.assertFalse(os.path.lexists(self.steam / 'steamapps/common/DroidDeck-42'))
self.assertTrue(unrelated.is_file())
self.assertEqual(b'game', self.exe.read_bytes())
def test_watch_never_looks_up_store_installs(self):
listing = self.root / 'games.json'
listing.write_text(json.dumps([dict(self.game, store='gog')]))
with patch.object(imports, 'identify') as identify:
imports.watch(str(self.steam), str(listing))
identify.assert_not_called()
def test_migrates_only_unchanged_legacy_import_manifests(self):
self.snapshot()
imports.route(self.steam, self.acct, [self.game])