fix: show when non-Steam games were last played, from Steam's shortcuts (#1551)

This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-10-25 11:33:24 -04:00
1 parent c9565bc0b9
commit e62698e395
2 files changed
+75 -3

No files matched your search

@@ -204,9 +204,24 @@ object Library {
(runCatching { g.folder.canonicalPath }.getOrDefault(g.folder.absolutePath) !in claimed &&
AddedGames.guestPath(context, g.folder).let { it == null || it !in claimed })
}
return (steam + own.map { g -> addedGame(context, g, roots) }).distinctBy { it.gameId }
val played = shortcutsLastPlayed(SessionPrefs.steamRoot(context, arch))
return (steam + own.map { g -> addedGame(context, g, roots, played[g.appId] ?: 0L) }).distinctBy { it.gameId }
}
/** Steam's LastPlayTime for each non-Steam shortcut, by its unsigned shortcut app id. */
internal fun shortcutsLastPlayed(root: File): Map<Long, Long> = runCatching {
val config = AddedGames.accountConfig(root) ?: return emptyMap()
val bytes = File(config, "shortcuts.vdf").takeIf { it.isFile }?.readBytes() ?: return emptyMap()
val vdf = SteamAppInfo.readMap(java.nio.ByteBuffer.wrap(bytes).order(java.nio.ByteOrder.LITTLE_ENDIAN), null)
(vdf["shortcuts"] as? Map<*, *>)?.values?.filterIsInstance<Map<*, *>>()?.mapNotNull { s ->
fun int(key: String) = s.entries.firstOrNull { (it.key as? String).equals(key, ignoreCase = true) }
?.let { (it.value as? String)?.toLongOrNull() }?.and(0xFFFFFFFFL)
val id = int("appid") ?: return@mapNotNull null
val time = int("LastPlayTime")?.takeIf { it > 0L } ?: return@mapNotNull null
id to time
}?.toMap()
}.getOrNull().orEmpty()
fun claimedFolders(steam: List<SteamGame>): Set<String> = steam.filter(::fromSteamClient).flatMap { g ->
val dir = g.gameFiles ?: return@flatMap emptyList()
listOfNotNull(
@@ -215,11 +230,14 @@ object Library {
)
}.toSet()
fun addedGame(context: Context, g: AddedGames.Game, roots: List<File> = steamLibraries(context).map { it.first }): SteamGame =
fun addedGame(
context: Context, g: AddedGames.Game, roots: List<File> = steamLibraries(context).map { it.first },
lastPlayed: Long = 0L,
): SteamGame =
AddedGameArt.resolve(context, g).let { art ->
SteamGame(
g.steamAppId ?: g.appId.toInt(), g.name, art.portrait ?: art.header, ADDED, g.gameId,
hero = art.hero ?: art.header, gameFiles = g.folder,
hero = art.hero ?: art.header, lastPlayed = lastPlayed, gameFiles = g.folder,
protonPrefix = protonPrefix(context, g.steamAppId?.toLong() ?: g.appId, libraries = roots),
icon = art.icon?.takeIf { it.extension.lowercase() != "ico" },
source = g.source, storeId = g.storeId,
@@ -0,0 +1,54 @@
package com.droiddeck.launcher.frontend
import java.io.ByteArrayOutputStream
import java.io.File
import java.nio.ByteBuffer
import java.nio.ByteOrder
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [33])
class LibraryShortcutsLastPlayedTest {
@get:Rule val tmp = TemporaryFolder()
private fun steamRoot(): File = tmp.newFolder("Steam").also { root ->
File(root, "config").mkdirs()
File(root, "config/loginusers.vdf").writeText(
"\"users\"\n{\n\t\"76561198352563669\"\n\t{\n\t\t\"AccountName\"\t\t\"a\"\n\t\t\"MostRecent\"\t\t\"1\"\n\t}\n}\n")
}
private class Vdf {
val out = ByteArrayOutputStream()
fun key(type: Int, k: String) { out.write(type); out.write(k.toByteArray()); out.write(0) }
fun map(k: String, body: Vdf.() -> Unit) { key(0, k); body(); out.write(8) }
fun str(k: String, v: String) { key(1, k); out.write(v.toByteArray()); out.write(0) }
fun int(k: String, v: Long) { key(2, k); out.write(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(v.toInt()).array()) }
}
@Test fun readsEachShortcutsLastPlayTimeByUnsignedAppId() {
val root = steamRoot()
val vdf = Vdf().apply {
map("shortcuts") {
map("0") { int("appid", 3388195926L); str("AppName", "Rayman 2"); int("LastPlayTime", 1792931476L) }
map("1") { int("appid", 2228102537L); str("AppName", "Do Not Disturb"); int("LastPlayTime", 0L) }
map("2") { int("appid", 1234L); str("appname", "Old"); int("lastplaytime", 1700000000L) }
}
out.write(8)
}
File(root, "userdata/392297941/config").apply { mkdirs() }.resolve("shortcuts.vdf").writeBytes(vdf.out.toByteArray())
assertEquals(mapOf(3388195926L to 1792931476L, 1234L to 1700000000L), Library.shortcutsLastPlayed(root))
}
@Test fun missingOrBrokenShortcutsGiveNothing() {
val root = steamRoot()
assertEquals(emptyMap<Long, Long>(), Library.shortcutsLastPlayed(root))
File(root, "userdata/392297941/config").apply { mkdirs() }.resolve("shortcuts.vdf").writeBytes(byteArrayOf(0, 's'.code.toByte()))
assertEquals(emptyMap<Long, Long>(), Library.shortcutsLastPlayed(root))
}
}