From db0637928b723b1f2d1f29af16647ecd60775d49 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 5 Aug 2026 23:33:25 +0200 Subject: [PATCH] fix(decky): the shortcut liveness guard answered "alive" for every appId MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `shortcutStillExists()` extracted the store method before calling it: const get = appStore?.GetAppOverviewByAppID; return get(appId) != null; `GetAppOverviewByAppID` reads the store's own state (`this.m_mapApps`), so the unbound call throws on the lost `this` — and the function's own `catch { return true }` swallowed it. The guard therefore returned "still exists" for EVERY appId. Not a stale-data bug: it never once answered no. Everything downstream of it was consequently inert. A dangling appId — the documented hazard this guard exists to catch, since the id outlives the shortcut in Steam's CEF localStorage across a plugin reinstall — was never dropped, so `ensureGamepadUiShortcut` always took the reuse branch and `SetShortcut*`'d a dead id (silent no-ops). The visible library entry never came back, `recreateShortcuts` reported success having done nothing (its toast only checks for a non-null appId, and the dead one is non-null), and "Open Punktfunk" ran `RunGame` on the dead id — Steam answers that with "Game configuration unavailable". Call it as a method so `this` survives, and guard the global with `typeof` first: `appStore` is Steam-injected, and a bare reference to a missing one is a ReferenceError that optional chaining does not prevent — which would have landed in the same catch. Verified against the live Deck that hit this: evaluated both versions over its actual appIds, and where the old guard says alive/alive, the fixed one says alive for the live stream shortcut and dead for the dangling UI id — so the stale key now drops and the entry is recreated on the next mount. --- clients/decky/src/steam.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/clients/decky/src/steam.ts b/clients/decky/src/steam.ts index 26f1fcac..176808bc 100644 --- a/clients/decky/src/steam.ts +++ b/clients/decky/src/steam.ts @@ -70,9 +70,18 @@ declare const appStore: * entry from a false "missing". A confident null means the shortcut was deleted → recreate. */ function shortcutStillExists(appId: number): boolean { try { - const get = appStore?.GetAppOverviewByAppID; - if (!get) return true; // no way to verify — preserve the reuse path - return get(appId) != null; + // Call it as a METHOD on appStore — NEVER as an extracted function. Its implementation + // reads the store's own state (`this.m_mapApps`), so `const get = appStore.GetAppOverview…; + // get(id)` throws on the lost `this`, and the catch below turns that into a permanent + // "true". That is not a stale-data bug but a total one: the guard then answers "still + // exists" for EVERY appId, so a dangling id is never dropped, the reuse path repoints a + // dead shortcut (silent no-ops), and "recreate" reports success having done nothing. + // `typeof` first: `appStore` is a Steam-injected global, and a bare reference to a missing + // one is a ReferenceError that optional chaining does NOT prevent. + if (typeof appStore === "undefined" || !appStore?.GetAppOverviewByAppID) { + return true; // no way to verify — preserve the reuse path + } + return appStore.GetAppOverviewByAppID(appId) != null; } catch { return true; }