diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat index d0190fb02..7ed1eb71e 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat @@ -561,6 +561,8 @@ class Helper: # "native" from earlier builds also lists dual-platform games nobody chose to run natively. prior_native = state.get("native_choices", ()) native_choices = set(prior_native if isinstance(prior_native, (list, tuple, set)) else ()) + picked = COMPAT["picked_apps"](state) + runnable = {t["name"].lower() for t in live} apps = sorted(set(installed) | set(picks) | set(prior_auto) | native_choices, key=COMPAT["app_order"]) overrides = COMPAT["requested_games"]( COMPAT["game_requests"](), catalog, [t["name"] for t in live]) @@ -585,7 +587,7 @@ class Helper: for app in native_choices: current[app] = "" for app in set(native) - native_choices: - if not current.get(app): + if not current.get(app) and app not in picked: current.pop(app, None) else: for app in set(native) | native_choices: @@ -593,7 +595,8 @@ class Helper: # The client shows an explicit native choice as the tool it inherits, and writes config.vdf late. if raw_mapping[app] == "" or current.get(app) not in self.known: current[app] = raw_mapping[app] - elif app in prior_auto or app in native_choices: + elif app in prior_auto or app in native_choices or app in picked and ( + not current.get(app) or COMPAT["unrunnable"](current[app], runnable)): current[app] = "" elif not current.get(app): current.pop(app, None) @@ -608,7 +611,7 @@ class Helper: state.update( auto={app: tool for app, tool in auto.items() if tool is not COMPAT["NATIVE_CHOICE"]}, native_choices=sorted(refused_native | {app for app, tool in auto.items() if tool is COMPAT["NATIVE_CHOICE"]}), - tools=catalog, live=True) + picked=COMPAT["picked_after"](current, changes), tools=catalog, live=True) state.pop("native", None) if waiting: state["pending"] = wanted diff --git a/tools/linuxfs/overlay/usr/local/bin/steam-compatibility b/tools/linuxfs/overlay/usr/local/bin/steam-compatibility index 67a33b424..51bf2c287 100755 --- a/tools/linuxfs/overlay/usr/local/bin/steam-compatibility +++ b/tools/linuxfs/overlay/usr/local/bin/steam-compatibility @@ -1042,6 +1042,16 @@ def plan_mapping(current, installed, runnable, default, auto, native=None, picks return changes, kept +def picked_apps(state): + picked = state.get("picked", ()) + return {app for app in picked if isinstance(app, str)} if isinstance(picked, list) else set() + + +def picked_after(current, changes): + return sorted(({app for app, tool in current.items() if tool} - set(changes) | + {app for app, tool in changes.items() if tool}) - {"0"}, key=app_order) + + def followers(state, current): if "auto" in state: return state["auto"] @@ -1287,6 +1297,8 @@ def register_default(config_path, apps=(), protect=(), default=None, catalog=Non native = native_targets(steam_root, set(apps) | set(picks) | set(managed) | set(native_choices)) if linux_tool != FEX_TOOL: native = {app: linux_tool if tool == FEX_TOOL else tool for app, tool in native.items()} + for app in picked_apps(state) & set(native): + current.setdefault(app, "") changes, auto = plan_mapping( current, apps, names, default, followers(state, current), native, picks, overrides, managed, native_choices) @@ -1297,6 +1309,7 @@ def register_default(config_path, apps=(), protect=(), default=None, catalog=Non write_changed(config_path, text) state["auto"] = {app: tool for app, tool in auto.items() if tool is not NATIVE_CHOICE} state["native_choices"] = sorted(app for app, tool in auto.items() if tool is NATIVE_CHOICE) + state["picked"] = picked_after(current, changes) state.pop("native", None) state["tools"] = catalog state.setdefault("default", default) diff --git a/tools/tests/test_steam_compat_live.py b/tools/tests/test_steam_compat_live.py index 0a24d5596..3cf27824b 100644 --- a/tools/tests/test_steam_compat_live.py +++ b/tools/tests/test_steam_compat_live.py @@ -509,6 +509,70 @@ class ReconcileTest(unittest.TestCase): self.assertEqual(page.sets, []) self.assertEqual(state["native_choices"], [app]) + def test_clearing_a_tool_picked_in_steam_keeps_the_native_build(self): + app, page = self.dual_platform_game({"default": TOOL, "auto": {"10": TOOL, "11": TOOL}}) + page.known.append(COMPAT["FEX_TOOL"]) + page.mapping[app] = COMPAT["FEX_TOOL"] + config = self.steam / "config/config.vdf" + config.write_text( + '"InstallConfigStore" { "Software" { "Valve" { "Steam" { ' + '"CompatToolMapping" { "0" { "name" "%s" } "%s" { "name" "%s" } } } } } }' % (TOOL, app, COMPAT["FEX_TOOL"])) + state = self.reconcile(page) + self.assertEqual(page.sets, []) + self.assertNotIn(app, state["auto"]) + + page.mapping[app] = "" + state = self.reconcile(page) + self.assertEqual(page.sets, []) + config.write_text( + '"InstallConfigStore" { "Software" { "Valve" { "Steam" { ' + '"CompatToolMapping" { "0" { "name" "%s" } } } } } }' % TOOL) + state = self.reconcile(page) + self.assertEqual(page.sets, []) + self.assertEqual(state["native_choices"], [app]) + + restarted = LIVE["Helper"](str(self.steam), 1) + restarted.page = page + restarted.reconcile() + self.assertEqual(page.sets, []) + + def test_a_new_tool_for_a_picked_game_is_kept_before_the_client_writes_its_config(self): + app, page = self.dual_platform_game({"default": TOOL, "auto": {"10": TOOL, "11": TOOL}, "picked": ["892970"]}) + page.mapping[app] = TOOL_11 + with mock.patch.dict(COMPAT, {"native_targets": lambda _root, apps: {app: COMPAT["FEX_TOOL"]} if app in apps else {}}): + state = self.reconcile(page) + self.assertEqual(page.sets, []) + self.assertEqual(page.mapping[app], TOOL_11) + self.assertIn(app, state["picked"]) + + def test_a_cleared_picked_game_showing_a_stale_inherited_tool_stays_native(self): + app, page = self.dual_platform_game({"default": TOOL, "auto": {"10": TOOL, "11": TOOL}, "picked": ["892970"]}) + page.mapping[app] = "proton" + state = self.reconcile(page) + self.assertEqual(page.sets, []) + self.assertEqual(state["native_choices"], [app]) + + def test_a_clear_saved_after_the_helper_stopped_is_kept_at_the_next_start(self): + app, page = self.dual_platform_game({"default": TOOL, "auto": {"10": TOOL, "11": TOOL}}) + page.known.append(COMPAT["FEX_TOOL"]) + page.mapping[app] = COMPAT["FEX_TOOL"] + config = self.steam / "config/config.vdf" + config.write_text( + '"InstallConfigStore" { "Software" { "Valve" { "Steam" { ' + '"CompatToolMapping" { "0" { "name" "%s" } "%s" { "name" "%s" } } } } } }' % (TOOL, app, COMPAT["FEX_TOOL"])) + state = self.reconcile(page) + self.assertIn(app, state["picked"]) + + config.write_text( + '"InstallConfigStore" { "Software" { "Valve" { "Steam" { ' + '"CompatToolMapping" { "0" { "name" "%s" } } } } } }' % TOOL) + with mock.patch.dict(COMPAT["register_default"].__globals__, {"native_targets": COMPAT["native_targets"]}): + COMPAT["register_default"](str(config), ["10", "11", app], settle=True, directory=str(self.state)) + self.assertNotIn(app, COMPAT["read_config_mapping"](str(config))) + state = json.loads((self.state / "state.json").read_text()) + self.assertEqual(state["native_choices"], [app]) + self.assertNotIn(app, state["picked"]) + def test_unmapped_dual_platform_game_gets_the_default_without_a_readable_config(self): app, page = self.dual_platform_game({"default": TOOL, "auto": {"10": TOOL}, "native_choices": ["11"]}) (self.steam / "config/config.vdf").unlink()