fix(proton): a Proton picked for a game in Steam is kept (#2284)

fix(proton): a Valve Proton picked for a game in Steam stays on the ARM64 build made from it

Steam lists Valve's own Proton Experimental and Proton 11.0 next to the
DroidDeck tools. They are not runnable here, so a per-game pick of one was
replaced with the default. That used to be Proton Experimental ARM64, so the
pick looked kept; since an installed DroidDeck-Proton became the default, the
game is moved to DroidDeck-Proton instead.

When Steam records one of those tools for a title the sync already knows
(a title it mapped or one with a recorded pick), the title now gets the
DroidDeck tool built from the same Valve depot, stops following the default
and the choice is mirrored into the app's per-game setting. The same applies
to Steam's default. Titles nobody picked anything for still follow the
default, and Valve tools with no ARM64 build still fall back to it.
This commit is contained in:
MaxsTechReview authored and GitHub committed 2026-11-08 12:03:21 -05:00
1 parent a4f26799ab
commit 3fef1f3fa1
5 files changed
+100 -7

No files matched your search

@@ -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,
@@ -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)
+36 -1
View File
@@ -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")
+1 -1
View File
@@ -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
+28
View File
@@ -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"]())