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/<appid>,
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 <danielhimebauch@gmail.com>
Co-authored-by: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-25 17:49:58 -04:00
1 parent 02767b7e6c
commit b4b7883648
2 files changed
+74 -5

No files matched your search

@@ -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)
+44
View File
@@ -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"