summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--py_modules/lsfg_vk/steam_service.py38
-rw-r--r--src/api/lsfgApi.ts1
-rw-r--r--src/components/CollapsibleItemGroup.tsx4
-rw-r--r--src/components/GameConfigurationSelector.tsx6
-rw-r--r--src/utils/steamLaunchOptions.ts41
-rw-r--r--tests/gameTargets.test.ts9
-rw-r--r--tests/steamLaunchOptions.test.ts20
-rw-r--r--tests/test_steam_service.py20
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):