diff options
| -rw-r--r-- | py_modules/lsfg_vk/steam_service.py | 38 | ||||
| -rw-r--r-- | src/api/lsfgApi.ts | 1 | ||||
| -rw-r--r-- | src/components/CollapsibleItemGroup.tsx | 4 | ||||
| -rw-r--r-- | src/components/GameConfigurationSelector.tsx | 6 | ||||
| -rw-r--r-- | src/utils/steamLaunchOptions.ts | 41 | ||||
| -rw-r--r-- | tests/gameTargets.test.ts | 9 | ||||
| -rw-r--r-- | tests/steamLaunchOptions.test.ts | 20 | ||||
| -rw-r--r-- | tests/test_steam_service.py | 20 |
8 files changed, 119 insertions, 20 deletions
diff --git a/py_modules/lsfg_vk/steam_service.py b/py_modules/lsfg_vk/steam_service.py index 2108071..93f2593 100644 --- a/py_modules/lsfg_vk/steam_service.py +++ b/py_modules/lsfg_vk/steam_service.py @@ -12,12 +12,34 @@ from .constants import ( class SteamService(BaseService): DEFAULT_BRANCH = "public" MANIFEST_FILENAME = f"appmanifest_{STEAM_LOSSLESS_SCALING_APP_ID}.acf" + # Valve compatibility tools, runtimes, Steamworks redistributables, and LSFG. GAME_SELECTOR_EXCLUDED_APPIDS = { - "858280", "961940", "1054830", "1113280", "1245040", "1420170", - "1493710", "1580130", "1887720", "2180100", "228980", "2348590", - "2805730", "3029110", "3127680", "3658110", "4183110", "4185400", - "4427310", "4628710", "4628740", "4690330", "993090", "1070560", - "1391110", "1628350", + "858280", # Proton 3.7 + "961940", # Proton 3.16 + "1054830", # Proton 4.2 + "1113280", # Proton 4.11 + "1245040", # Proton 5.0 + "1420170", # Proton 5.13 + "1493710", # Proton Experimental + "1580130", # Proton 6.3 + "1887720", # Proton 7 + "2180100", # Proton Hotfix + "228980", # Steamworks Common Redistributables + "2348590", # Proton 8 + "2805730", # Proton 9 + "3029110", # Lepton + "3127680", # fex + "3658110", # Proton 10 + "4183110", # Steam Linux Runtime 4.0 + "4185400", # Steam Linux Runtime 4.0 for arm64 + "4427310", # Proton Experimental (ARM64) + "4628710", # Proton 11 / Proton Next + "4628740", # Proton 11 (ARM64) + "4690330", # Legacy Steam Runtime + "993090", # Lossless Scaling + "1070560", # Steam Linux Runtime 1.0 + "1391110", # Steam Linux Runtime 2.0 + "1628350", # Steam Linux Runtime 3.0 } def _steam_roots(self): @@ -99,11 +121,15 @@ class SteamService(BaseService): name = shortcut.get("AppName") or shortcut.get("appname") if not isinstance(appid, int) or appid == 0 or not isinstance(name, str) or not name: return None - return { + game = { "appid": str(appid & 0xFFFFFFFF), "name": name, "nonSteam": True, } + executable = shortcut.get("Exe") or shortcut.get("exe") + if isinstance(executable, str) and executable.strip().strip('"') in {"flatpak", "/usr/bin/flatpak"}: + game["isFlatpakShortcut"] = True + return game def _shortcut_games(self): games = {} diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts index 883f56e..44b72b5 100644 --- a/src/api/lsfgApi.ts +++ b/src/api/lsfgApi.ts @@ -41,6 +41,7 @@ export interface InstalledGame { appid: string; name: string; nonSteam: boolean; + isFlatpakShortcut?: boolean; } export interface GlobalConfig { diff --git a/src/components/CollapsibleItemGroup.tsx b/src/components/CollapsibleItemGroup.tsx index 29fe945..a4e4092 100644 --- a/src/components/CollapsibleItemGroup.tsx +++ b/src/components/CollapsibleItemGroup.tsx @@ -6,6 +6,7 @@ export interface CollapsibleItem { id: string; label: string; description: string; + disabled?: boolean; } export const collapsibleItemGroupStyles = ` @@ -91,7 +92,8 @@ export function CollapsibleItemGroup({ <Field label={item.label} description={item.description} - onActivate={() => onSelect(item.id)} + disabled={item.disabled} + onActivate={item.disabled ? undefined : () => onSelect(item.id)} highlightOnFocus /> </PanelSectionRow> diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index ad3537d..91dc3df 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -20,6 +20,7 @@ const ENABLED_COLLAPSED_KEY = "lsfg-enabled-games-collapsed-v4"; const AVAILABLE_COLLAPSED_KEY = "lsfg-available-games-collapsed-v3"; function targetDescription(game: GameTarget): string { + if (game.isFlatpakShortcut) return "Non-Steam | Use Flatpak Tab"; return game.source === "unknown" ? "Unknown source ยท excluded from bulk actions" : sourceLabel(game.source); @@ -43,8 +44,8 @@ export function GameConfigurationSelector({ }); const enabledGames = sortGames(targets.filter((game) => game.configured)); const availableGames = sortGames(targets.filter((game) => !game.configured)); - const enableableGames = availableGames.filter((game) => game.source === source); - const removableGames = enabledGames.filter((game) => game.source === source); + const enableableGames = availableGames.filter((game) => game.source === source && !game.isFlatpakShortcut); + const removableGames = enabledGames.filter((game) => game.source === source && !game.isFlatpakShortcut); const sourceName = source === "nonSteam" ? "non-Steam shortcuts" : "Steam games"; const emptyDescription = source === "nonSteam" ? "Steam has not reported any eligible non-Steam shortcuts" @@ -53,6 +54,7 @@ export function GameConfigurationSelector({ id: game.appid, label: game.name, description: targetDescription(game), + disabled: game.isFlatpakShortcut, }); const [enabledCollapsed, toggleEnabled] = usePersistentCollapsed(`${ENABLED_COLLAPSED_KEY}-${source}`); const [availableCollapsed, toggleAvailable] = usePersistentCollapsed(`${AVAILABLE_COLLAPSED_KEY}-${source}`); diff --git a/src/utils/steamLaunchOptions.ts b/src/utils/steamLaunchOptions.ts index f03eeab..83c46f2 100644 --- a/src/utils/steamLaunchOptions.ts +++ b/src/utils/steamLaunchOptions.ts @@ -153,7 +153,25 @@ function tokenize(options: string): LaunchToken[] { } const serialize = (tokens: readonly LaunchToken[]) => tokens.map(({ raw }) => raw).join(" "); -const commandIndex = (tokens: readonly LaunchToken[]) => tokens.findIndex((token) => token.raw.toLowerCase() === COMMAND_TOKEN); +function isCommandToken(token: LaunchToken): boolean { + return token.value.toLowerCase() === COMMAND_TOKEN; +} + +function isMalformedCommandToken(token: LaunchToken): boolean { + const value = token.value.toLowerCase(); + return value === "%command" || value === "command%"; +} + +function normalizeCommandTokens(tokens: LaunchToken[]): void { + for (const token of tokens) { + if (isCommandToken(token) || isMalformedCommandToken(token)) { + token.raw = COMMAND_TOKEN; + token.value = COMMAND_TOKEN; + } + } +} + +const commandIndex = (tokens: readonly LaunchToken[]) => tokens.findIndex(isCommandToken); const isAssignment = (token: LaunchToken) => /^[A-Za-z_][A-Za-z0-9_]*=/.test(token.value); const isLegacyToken = (value: string) => LEGACY_WRAPPER_TOKENS.has(value) || LEGACY_ABSOLUTE_WRAPPER.test(value); const isWrapperToken = (value: string, wrapperPath: string) => decodeToken(value) === wrapperPath || isLegacyWrapperToken(value); @@ -173,9 +191,9 @@ function removeMatchingWrappers(tokens: LaunchToken[], predicate: (value: string function installLaunchOption( options: string, wrapperPath = DEFAULT_WRAPPER_PATH, - shortcutLaunchOptions = false, ) { const tokens = tokenize(options); + normalizeCommandTokens(tokens); removeMatchingWrappers(tokens, isLegacyToken); let command = commandIndex(tokens); if (command >= 0) { @@ -185,11 +203,15 @@ function installLaunchOption( tokens.splice(command, 0, { raw: wrapperPath, value: wrapperPath }); return { options: serialize(tokens), commandTokenAdded: false }; } + + const existingWrapper = tokens.findIndex((token) => decodeToken(token.value) === wrapperPath); + if (existingWrapper >= 0) { + tokens.splice(existingWrapper + 1, 0, { raw: COMMAND_TOKEN, value: COMMAND_TOKEN }); + return { options: serialize(tokens), commandTokenAdded: true }; + } + let insertion = 0; while (insertion < tokens.length && isAssignment(tokens[insertion])) insertion++; - if (!shortcutLaunchOptions && insertion < tokens.length && !tokens[insertion].value.startsWith("-")) { - throw new Error("Launch options do not contain %command%; refusing to guess a launcher command"); - } tokens.splice(insertion, 0, { raw: wrapperPath, value: wrapperPath }, { raw: COMMAND_TOKEN, value: COMMAND_TOKEN }, @@ -207,6 +229,7 @@ export function removeWrapperLaunchOption( commandTokenAdded = false, ): string { const tokens = tokenize(options); + normalizeCommandTokens(tokens); if (removeMatchingWrappers(tokens, (value) => isWrapperToken(value, wrapperPath)) && commandTokenAdded) { const command = commandIndex(tokens); if (command >= 0) tokens.splice(command, 1); @@ -345,14 +368,18 @@ export function installWrapperIntegration( const current = await readSteamLaunchOptions(appId, nonSteam); const cleaned = cleanupPluginAssignments(cleanupLegacyLaunchOptions(current.options)); const alreadyInstalled = hasWrapperLaunchIntegration(current.options, wrapperPath); - const rewrite = installLaunchOption(cleaned, wrapperPath, nonSteam); + const rewrite = installLaunchOption(cleaned, wrapperPath); if (rewrite.options === current.options) return { snapshot: current, commandTokenAdded, changed: false }; const value = await writeVerified( appId, nonSteam, current.options, rewrite.options, (options) => writeOptions(appId, nonSteam, options), "Steam did not accept the launch options", ); - return { snapshot: value, commandTokenAdded: alreadyInstalled ? commandTokenAdded : rewrite.commandTokenAdded, changed: true }; + return { + snapshot: value, + commandTokenAdded: alreadyInstalled ? commandTokenAdded : commandTokenAdded || rewrite.commandTokenAdded, + changed: true, + }; }); } diff --git a/tests/gameTargets.test.ts b/tests/gameTargets.test.ts index 9896d76..2eb8b7e 100644 --- a/tests/gameTargets.test.ts +++ b/tests/gameTargets.test.ts @@ -39,3 +39,12 @@ test("unknown configured profiles are visible in both source tabs", () => { assert.deepEqual(steamTargets.map((target) => target.appid).sort(), ["123", "456"]); assert.deepEqual(nonSteamTargets.map((target) => target.appid).sort(), ["456", "789"]); }); + +test("direct Flatpak shortcuts remain non-Steam targets", () => { + const targets = mergeGameTargets([], [ + { appid: "123", name: "Flatpak shortcut", nonSteam: true, isFlatpakShortcut: true }, + ], []); + + assert.equal(targets[0].source, "nonSteam"); + assert.equal(targets[0].isFlatpakShortcut, true); +}); diff --git a/tests/steamLaunchOptions.test.ts b/tests/steamLaunchOptions.test.ts index 40aeb3c..1d2f762 100644 --- a/tests/steamLaunchOptions.test.ts +++ b/tests/steamLaunchOptions.test.ts @@ -28,7 +28,7 @@ test("inserts one wrapper immediately before an existing command macro", () => { }); }); -test("normalizes blank and argument-only shortcut fields", () => { +test("normalizes blank, malformed, and argument-only launch fields", () => { assert.deepEqual(installWrapperLaunchOption("", wrapper), { options: `${wrapper} %command%`, commandTokenAdded: true, @@ -37,8 +37,22 @@ test("normalizes blank and argument-only shortcut fields", () => { options: `FOO=bar ${wrapper} %command% --windowed`, commandTokenAdded: true, }); - assert.throws(() => installWrapperLaunchOption("gamemoderun --windowed", wrapper), /refusing to guess/); - assert.throws(() => installWrapperLaunchOption('"%command%"', wrapper), /refusing to guess/); + assert.deepEqual(installWrapperLaunchOption("gamemoderun --windowed", wrapper), { + options: `${wrapper} %command% gamemoderun --windowed`, + commandTokenAdded: true, + }); + assert.deepEqual(installWrapperLaunchOption('"%command%"', wrapper), { + options: `${wrapper} %command%`, + commandTokenAdded: false, + }); + assert.deepEqual(installWrapperLaunchOption(`${wrapper} %command`, wrapper), { + options: `${wrapper} %command%`, + commandTokenAdded: false, + }); + assert.deepEqual(installWrapperLaunchOption(`${wrapper} --windowed`, wrapper), { + options: `${wrapper} %command% --windowed`, + commandTokenAdded: true, + }); }); test("preserves assignments quoting suffixes and released wrapper cleanup", () => { diff --git a/tests/test_steam_service.py b/tests/test_steam_service.py index 004ffb6..75cdabf 100644 --- a/tests/test_steam_service.py +++ b/tests/test_steam_service.py @@ -15,7 +15,7 @@ from lsfg_vk.steam_service import SteamService class SteamShortcutTests(unittest.TestCase): - def test_direct_flatpak_shortcut_is_ordinary_non_steam_metadata(self): + def test_direct_flatpak_shortcut_is_marked(self): game = SteamService._shortcut_game( { "appid": 123456, @@ -30,6 +30,24 @@ class SteamShortcutTests(unittest.TestCase): "appid": "123456", "name": "PCSX2 shortcut", "nonSteam": True, + "isFlatpakShortcut": True, + }) + + def test_bare_flatpak_shortcut_is_marked(self): + game = SteamService._shortcut_game( + { + "appid": 654321, + "AppName": "Faugus shortcut", + "Exe": '"flatpak"', + "LaunchOptions": "run io.github.Faugus.faugus-launcher --game elliot", + } + ) + + self.assertEqual(game, { + "appid": "654321", + "name": "Faugus shortcut", + "nonSteam": True, + "isFlatpakShortcut": True, }) def test_emudeck_launcher_is_ordinary_non_steam_metadata(self): |
