From b4b78836484f3f30fd50847bbcb00727de0bb9cf Mon Sep 17 00:00:00 2001 From: kingokksa <47888451+kingokksa@users.noreply.github.com> Date: Mon, 26 Oct 2026 05:49:58 +0800 Subject: [PATCH] fix: Reuse the prefix the client made for a shortcut id (#1107) * fix: Reuse the prefix the client made for a shortcut id droiddeck-proton-run builds the prefix path as steamapps/compatdata/, but the client names that folder with the signed 32-bit form of the shortcut id. A shortcut whose id has the top bit set therefore got a second, empty prefix, and the first launch after that ran wineboot from scratch: the session sat on a black screen for the length of a full prefix init, and the old prefix was never reused. Match the folder by its unsigned value instead, the way the client does: int(name) % 2 ** 32 == appid % 2 ** 32. prefix_of() also accepts a negative folder name now, and reports its unsigned id so the Steam environment it sets up stays valid. * fix: Reject folder names int() cannot read, and skip files lstrip("-").isdigit() accepts "--1" and superscript digits, which int() then raises on, and one such entry in compatdata would stop shortcut resolution. Match r"-?[0-9]+" instead. A matching name is also only used when it is a directory, so a stray file cannot take STEAM_COMPAT_DATA_PATH. * fix: reuse signed shortcut compatdata prefix * fix: prefer initialized Proton shortcut prefixes --------- Co-authored-by: xXJsonDeruloXx Co-authored-by: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> --- .../usr/local/bin/droiddeck-proton-run | 35 ++++++++++++--- tools/tests/test_desktop_open.py | 44 +++++++++++++++++++ 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-run b/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-run index 3648ddede..624d42074 100644 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-run +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-proton-run @@ -193,9 +193,9 @@ def steam_game(program, libs): def prefix_of(program): for folder, below in ancestors(program): parent = os.path.dirname(folder) - if below == "pfx" and os.path.basename(parent) == "compatdata" and os.path.basename(folder).isdigit(): + if below == "pfx" and os.path.basename(parent) == "compatdata" and re.fullmatch(r"-?[0-9]+", os.path.basename(folder)): library = os.path.dirname(os.path.dirname(parent)) - return Target(os.path.basename(folder), folder, os.path.dirname(program), library, "prefix") + return Target(str(int(os.path.basename(folder)) % 2 ** 32), folder, os.path.dirname(program), library, "prefix") return None @@ -238,6 +238,30 @@ def shortcut_entries(root): return entries +def prefix_folder(root, appid): + """The prefix folder the client made for [appid], under either spelling of the id.""" + folder = os.path.join(root, "steamapps", "compatdata") + try: + names = sorted(os.listdir(folder)) + except OSError: + names = [] + candidates = [] + for name in names: + if not re.fullmatch(r"-?[0-9]+", name): + continue + try: + matches = int(name) % 2 ** 32 == appid % 2 ** 32 + except ValueError: + continue + match = os.path.join(folder, name) + if matches and os.path.isdir(match): + candidates.append((os.path.isdir(os.path.join(match, "pfx")), + name == str(appid % 2 ** 32), name, match)) + if candidates: + return min(candidates, key=lambda item: (not item[0], not item[1], item[2]))[3] + return os.path.join(folder, str(appid % 2 ** 32)) + + def vdf_string(token): return re.sub(r'\\(.)', r'\1', token[1:-1]) if token.startswith('"') else "" @@ -275,7 +299,7 @@ def steam_options(app, root): def shortcut(program, root): for appid, exe, start in shortcut_entries(root): if same_file(program, exe): - return Target(appid, os.path.join(root, "steamapps", "compatdata", str(appid)), + return Target(appid, prefix_folder(root, appid), os.path.dirname(program), root, "shortcut", start if os.path.isdir(start) else None) return None @@ -292,7 +316,7 @@ def locate(program, root): if found: return found appid = own_id(program) - return Target(appid, os.path.join(root, "steamapps", "compatdata", str(appid)), + return Target(appid, prefix_folder(root, appid), os.path.dirname(program), root, "own") @@ -376,7 +400,8 @@ def steam_launch(appid, root): def shortcut_target(appid, program, start, root): - return Target(appid, os.path.join(root, "steamapps", "compatdata", str(appid)), os.path.dirname(program), root, + appid = int(appid) % 2 ** 32 + return Target(str(appid), prefix_folder(root, appid), os.path.dirname(program), root, "shortcut", start if start and os.path.isdir(start) else None) diff --git a/tools/tests/test_desktop_open.py b/tools/tests/test_desktop_open.py index 227fd31ba..ef299963b 100644 --- a/tools/tests/test_desktop_open.py +++ b/tools/tests/test_desktop_open.py @@ -570,6 +570,50 @@ class ProtonRunTest(Temp): self.game("70", "Linux Game", "run.sh") self.assertEqual(self.run_main(["--app", "70"])[0], 2) + def test_without_steam_a_shortcut_reuses_a_signed_prefix_folder(self): + bat = self.write("Games/Epic/Game/.droiddeck-launch.bat", b"@echo off") + start = self.tmp / "Games/Epic/Game/Binaries" + start.mkdir() + signed = self.root / "steamapps/compatdata/-1561598272" + signed.mkdir(parents=True) + code, calls, dirs = self.run_main(["--app", "2733369024", "--dir", str(start), "--", str(bat)]) + self.assertIsNone(code) + env = calls[0][2] + self.assertEqual(env["STEAM_COMPAT_DATA_PATH"], str(signed)) + self.assertFalse((self.root / "steamapps/compatdata/2733369024").exists()) + + def test_prefix_folder_prefers_initialized_unsigned_over_empty_signed(self): + signed = self.root / "steamapps/compatdata/-1561598272" + signed.mkdir(parents=True) + initialized = self.root / "steamapps/compatdata/2733369024/pfx" + initialized.mkdir(parents=True) + + self.assertEqual(RUN["prefix_folder"](str(self.root), 2733369024), str(initialized.parent)) + + def test_prefix_folder_prefers_initialized_signed_over_empty_unsigned(self): + unsigned = self.root / "steamapps/compatdata/2733369024" + unsigned.mkdir(parents=True) + initialized = self.root / "steamapps/compatdata/-1561598272/pfx" + initialized.mkdir(parents=True) + + self.assertEqual(RUN["prefix_folder"](str(self.root), 2733369024), str(initialized.parent)) + + def test_prefix_folder_prefers_unsigned_when_initialization_ties(self): + unsigned = self.root / "steamapps/compatdata/2733369024/pfx" + unsigned.mkdir(parents=True) + signed = self.root / "steamapps/compatdata/-1561598272/pfx" + signed.mkdir(parents=True) + + self.assertEqual(RUN["prefix_folder"](str(self.root), 2733369024), str(unsigned.parent)) + + def test_prefix_folder_skips_numeric_names_too_long_to_convert(self): + from unittest.mock import patch + folder = self.root / "steamapps/compatdata" + folder.mkdir(parents=True) + with patch.object(RUN["os"], "listdir", return_value=["9" * 5000]): + self.assertEqual(RUN["prefix_folder"](str(self.root), 2733369024), + str(folder / "2733369024")) + def test_without_steam_a_shortcut_keeps_its_prefix_folder_and_launch_options(self): bat = self.write("Games/Epic/Game/.droiddeck-launch.bat", b"@echo off") start = self.tmp / "Games/Epic/Game/Binaries"