diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat index 95be74815..901caf2f4 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat @@ -1078,8 +1078,9 @@ class Helper: wanted = COMPAT["requested_tool"](request, catalog) newer = isinstance(request.get("seq"), int) and request["seq"] > int(state.get("applied", 0) or 0) waiting = newer and wanted is not None and wanted not in self.known - default, state = COMPAT["choose_default"](top["selected"], state, request, catalog, apply_request=not waiting, - listed=[t["name"] for t in live]) + listed = [t["name"] for t in live] + default, state = COMPAT["choose_default"](COMPAT["built_from"](top["selected"], catalog, listed) or top["selected"], + state, request, catalog, apply_request=not waiting, listed=listed) installed = COMPAT["installed_apps"](self.steam_root) self.installed = installed picks = COMPAT["valve_picks"](self.steam_root) @@ -1124,13 +1125,20 @@ class Helper: current[app] = "" elif not current.get(app): current.pop(app, None) + moved = COMPAT["rebuilt_picks"](current, catalog, set(prior_auto) | picked, listed) + if moved: + following = COMPAT["followers"](state, current) + state["auto"] = {app: name for app, name in following.items() if app not in moved} if isinstance(following, dict) else {} + prior_auto = {app: name for app, name in prior_auto.items() if app not in moved} + current.update(moved) requests = COMPAT["settled_game_requests"](state, current, catalog) - overrides = COMPAT["requested_games"](requests or {}, catalog, [t["name"] for t in live]) + overrides = COMPAT["requested_games"](requests or {}, catalog, listed) overrides = {app: tool for app, tool in overrides.items() if app not in refused} changes, auto = COMPAT["plan_mapping"]( - current, installed, [t["name"] for t in live], default, + current, installed, listed, default, COMPAT["followers"](state, current), native, picks, overrides, prior_auto.keys(), native_choices) + changes = dict(moved, **changes) for app, tool in sorted(changes.items(), key=lambda item: COMPAT["app_order"](item[0])): page.compat("set(%d, %s)" % (int(app), json.dumps(tool if tool is not None else ""))) say("%s -> %s" % ("default" if app == "0" else "app %s" % app, diff --git a/tools/linuxfs/overlay/usr/local/bin/steam-compatibility b/tools/linuxfs/overlay/usr/local/bin/steam-compatibility index 2f964d96a..071ff0681 100755 --- a/tools/linuxfs/overlay/usr/local/bin/steam-compatibility +++ b/tools/linuxfs/overlay/usr/local/bin/steam-compatibility @@ -16,6 +16,8 @@ LEGACY_LAUNCHER = "bannerlator-proton" LIBRARY = "/mnt/droiddeck" SOURCES = ("Proton Experimental (ARM64)", "Proton 11.0 (ARM64)") OWN_TOOLS = ((TOOL, SOURCES), (TOOL_11, ("Proton 11.0 (ARM64)",))) +VALVE_DEPOTS = {"proton_experimental": SOURCES[0], "proton-experimental-arm64": SOURCES[0], + "proton_11": SOURCES[1], "proton_11-arm64": SOURCES[1]} FEX_TOOL = "droiddeck-fex" FEX_LAUNCHER = "droiddeck-fex-tool" FEX_FILES = ("toolmanifest.vdf", "compatibilitytool.vdf", FEX_LAUNCHER) @@ -1160,6 +1162,18 @@ def tool_catalog(steam_root, extras=None): return catalog +def built_from(name, catalog, runnable=None): + depot = VALVE_DEPOTS.get(name) + tools = [t["name"] for t in catalog if depot and t["valve"] and t["dir"] == depot and + (runnable is None or t["name"] in runnable)] + return tools[-1] if tools else None + + +def rebuilt_picks(current, catalog, known, runnable=None): + picks = {app: built_from(name, catalog, runnable) for app, name in current.items() if app in known} + return {app: tool for app, tool in picks.items() if tool} + + def requested_tool(request, catalog): name = request.get("name") if isinstance(name, str) and any(t["name"] == name for t in catalog): @@ -1513,7 +1527,8 @@ def register_default(config_path, apps=(), protect=(), default=None, catalog=Non state["owned"] = sorted(set(owned), key=app_order) if default is None: if settle: - default, state = choose_default(current.get("0", ""), state, read_json(state_path("request.json", directory)), catalog) + default, state = choose_default(built_from(current.get("0", ""), catalog) or current.get("0", ""), state, + read_json(state_path("request.json", directory)), catalog) else: default = state.get("default") if state.get("default") in names else TOOL if settle and newest and default == TOOL and state.get("source") not in ("app", "steam"): @@ -1526,6 +1541,13 @@ def register_default(config_path, apps=(), protect=(), default=None, catalog=Non current.update(replacements) if isinstance(state.get("auto"), dict): state["auto"] = {app: newest if name in gone else name for app, name in state["auto"].items()} + following = followers(state, current) + following = following if isinstance(following, dict) else {} + moved = rebuilt_picks(current, catalog, set(following) | picked_apps(state)) + if moved: + state["auto"] = {app: name for app, name in following.items() if app not in moved} + set_mappings(tokens, mapping, moved) + current.update(moved) steam_root = os.path.dirname(os.path.dirname(os.path.abspath(config_path))) requests = settled_game_requests(state, current, catalog, not steam_running(), directory) overrides = requested_games(requests or {}, catalog) diff --git a/tools/tests/test_droiddeck_proton_default.py b/tools/tests/test_droiddeck_proton_default.py index 4da81a63b..2173a223a 100644 --- a/tools/tests/test_droiddeck_proton_default.py +++ b/tools/tests/test_droiddeck_proton_default.py @@ -10,7 +10,8 @@ COMPAT = runpy.run_path(str(BIN / "steam-compatibility")) TOOL = COMPAT["TOOL"] OLD = {"name": "DroidDeck-Proton-11-11.2", "display": "DroidDeck-Proton 11-11.2", "dir": "DroidDeck-Proton 11-11.2", "valve": False} NEW = {"name": "DroidDeck-Proton-11-11.4", "display": "DroidDeck-Proton 11-11.4", "dir": "DroidDeck-Proton 11-11.4", "valve": False} -VALVE = {"name": TOOL, "display": "Proton Experimental ARM64", "dir": "Proton - Experimental (ARM64)", "valve": True} +VALVE = {"name": TOOL, "display": "Proton Experimental ARM64", "dir": COMPAT["SOURCES"][0], "valve": True} +VALVE_11 = {"name": COMPAT["TOOL_11"], "display": "Proton 11.0 ARM64", "dir": COMPAT["SOURCES"][1], "valve": True} GE = {"name": "GE-Proton11-7", "display": "GE-Proton11-7", "dir": "GE-Proton11-7", "valve": False} GE10 = {"name": "GE-Proton10-3", "display": "GE-Proton10-3", "dir": "GE-Proton10-3", "valve": False} NEWER = {"name": "DroidDeck-Proton-11-12.1", "display": "DroidDeck-Proton 11-12.1", "dir": "DroidDeck-Proton 11-12.1", "valve": False} @@ -164,6 +165,40 @@ class DroidDeckProtonDefaultTest(unittest.TestCase): self.assertEqual(mapping, {"0": GE10["name"], "3000000001": TOOL, "3000000002": GE10["name"], "3000000003": GE["name"], "3000000004": GE10["name"], "42": GE["name"], "44": GE["name"]}) + def test_proton_experimental_picked_for_a_game_in_steam_stays(self): + default, mapping, state = self.run_default( + {"default": NEW["name"], "written": NEW["name"], "source": "update", + "auto": {"42": NEW["name"], "43": NEW["name"], "45": NEW["name"]}}, + [("0", NEW["name"]), ("42", "proton_experimental"), ("43", NEW["name"]), ("45", TOOL)], [VALVE, NEW], + ["42", "43", "44", "45"]) + self.assertEqual(default, NEW["name"]) + self.assertEqual(mapping, {"0": NEW["name"], "42": TOOL, "43": NEW["name"], "44": NEW["name"], "45": TOOL}) + self.assertEqual(state["auto"], {"43": NEW["name"], "44": NEW["name"]}) + self.assertTrue({"42", "45"} <= set(state["picked"])) + default, mapping, state = self.run_default(state, sorted(mapping.items()), [VALVE, NEWER], ["42", "43", "44", "45"]) + self.assertEqual(default, NEWER["name"]) + self.assertEqual(mapping, {"0": NEWER["name"], "42": TOOL, "43": NEWER["name"], "44": NEWER["name"], "45": TOOL}) + + def test_a_steam_pick_of_a_valve_proton_moves_to_the_build_made_from_it(self): + catalog = [VALVE, VALVE_11, NEW] + default, mapping, state = self.run_default( + {"default": NEW["name"], "written": NEW["name"], "source": "update", "auto": {"42": TOOL, "43": NEW["name"]}, + "picked": ["44"]}, + [("0", NEW["name"]), ("42", "proton-experimental-arm64"), ("43", "proton_11"), ("44", "proton_11-arm64"), + ("45", "proton_9")], catalog, ["42", "43", "44", "45"]) + self.assertEqual(mapping, {"0": NEW["name"], "42": TOOL, "43": VALVE_11["name"], "44": VALVE_11["name"], "45": NEW["name"]}) + self.assertEqual(state["auto"], {"45": NEW["name"]}) + _, mapping, _ = self.run_default(state, sorted(mapping.items()), catalog, ["42", "43", "44", "45"], + request={"seq": 1, "valve": False, "dir": GE["dir"]}) + self.assertEqual(mapping, {"0": NEW["name"], "42": TOOL, "43": VALVE_11["name"], "44": VALVE_11["name"], "45": NEW["name"]}) + + def test_proton_experimental_picked_as_steams_default_stays(self): + default, mapping, state = self.run_default( + {"default": NEW["name"], "written": NEW["name"], "source": "update"}, [("0", "proton_experimental")], [VALVE, NEW]) + self.assertEqual((default, mapping, state["source"]), (TOOL, {"0": TOOL}, "steam")) + default, mapping, state = self.run_default(state, [("0", TOOL)], [VALVE, NEW, NEWER]) + self.assertEqual((default, mapping), (TOOL, {"0": TOOL})) + def test_the_newest_build_is_found_by_version(self): newest, gone = COMPAT["own_proton_moves"]({"tools": [OLD]}, [dict(NEW, name="a", dir="DroidDeck-Proton 11-9.10"), dict(NEW, name="b", dir="DroidDeck-Proton 11-11.1")]) self.assertEqual(newest, "b") diff --git a/tools/tests/test_game_environment.py b/tools/tests/test_game_environment.py index dfc65de1c..6e21ec2de 100644 --- a/tools/tests/test_game_environment.py +++ b/tools/tests/test_game_environment.py @@ -2024,7 +2024,7 @@ class ProtonDefaultTest(unittest.TestCase): self.assertEqual(mapping_of(config.read_text()), {"0": self.TOOL, "427520": self.TOOL}) saved = json.loads((Path(tmp) / "state.json").read_text()) self.assertEqual(saved["native_choices"], []) - self.assertEqual(saved["auto"], {"427520": self.TOOL}) + self.assertEqual(saved["auto"], {}) finally: globals_["appinfo_platforms"] = original_platforms diff --git a/tools/tests/test_steam_compat_live.py b/tools/tests/test_steam_compat_live.py index 2902bef08..15ae63bb8 100644 --- a/tools/tests/test_steam_compat_live.py +++ b/tools/tests/test_steam_compat_live.py @@ -769,6 +769,34 @@ class ReconcileTest(unittest.TestCase): self.assertEqual(default, build) self.assertEqual(COMPAT["read_config_mapping"](str(config)), {"0": build, "10": build, "11": build}) + def test_proton_experimental_picked_for_a_game_in_steam_is_kept_and_mirrored(self): + build = "DroidDeck-Proton 11-12.3" + (self.steam / "compatibilitytools.d" / build).mkdir(parents=True) + (self.steam / "compatibilitytools.d" / build / COMPAT["EXTRA_WRAPPER"]).write_text("") + (self.state / "state.json").write_text(json.dumps( + {"default": build, "written": build, "source": "update", "auto": {"11": build}, "picked": ["10", "11"], "tools": [], + "games_applied": {"10": {"valve": False, "dir": build}}})) + self.write_games({"10": {"valve": False, "dir": build}}) + page = FakePage({"0": build, "10": "proton_experimental", "11": build}, [TOOL, TOOL_11, build, "proton_experimental"]) + state = self.reconcile(page) + self.assertEqual(page.sets, [("10", TOOL)]) + self.assertEqual(state["auto"], {"11": build}) + self.assertEqual(self.games(), {"10": {"valve": True, "dir": COMPAT["SOURCES"][0]}}) + config = self.steam / "config/config.vdf" + config.parent.mkdir(parents=True) + vdf = '"InstallConfigStore" { "Software" { "Valve" { "Steam" { "CompatToolMapping" { %s } } } } }' + entry = '"%s" { "name" "%s" "config" "" "priority" "250" }' + config.write_text(vdf % "".join(entry % (app, page.mapping[app]) for app in ("0", "10", "11"))) + with mock.patch.dict(COMPAT["register_default"].__globals__, steam_running=lambda: False): + default = COMPAT["register_default"](str(config), ["10", "11"], catalog=COMPAT["tool_catalog"](str(self.steam)), + directory=str(self.state), settle=True) + self.assertEqual(default, build) + self.assertEqual(COMPAT["read_config_mapping"](str(config)), {"0": build, "10": TOOL, "11": build}) + page.sets.clear() + self.write_games({"10": {"valve": True, "dir": COMPAT["SOURCES"][1]}}) + self.reconcile(page) + self.assertEqual(page.sets, [("10", TOOL_11)]) + def test_file_mode_defers_to_the_live_helper(self): (self.state / "live.pid").write_text("%d\n" % os.getpid()) self.assertTrue(COMPAT["live_helper_running"]())