import hashlib import io import json import os import shutil from pathlib import Path import runpy import shlex import subprocess import sys import tempfile from unittest import mock import unittest from unittest.mock import patch BIN = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin" MODULE = runpy.run_path(str(BIN / "droiddeck-game-env")) COMPAT = runpy.run_path(str(BIN / "steam-compatibility")) STATE = tempfile.TemporaryDirectory() COMPAT["main"].__globals__["COMPAT_DIR"] = STATE.name class SupergiantDotnetReporterTest(unittest.TestCase): def test_allows_the_builtin_mscoree_for_supergiant_reporter(self): game = mock.Mock(files={"basecampbugreporter.native.dll"}) self.assertEqual( ({"WINEDLLOVERRIDES": "mscoree=n,b"}, [], "Supergiant: allow the builtin mscoree"), MODULE["supergiant_dotnet_reporter"](game), ) def test_ignores_games_without_the_supergiant_reporter(self): game = mock.Mock(files={"enginewin64s.dll"}) self.assertIsNone(MODULE["supergiant_dotnet_reporter"](game)) def mapping_of(text): tokens = COMPAT["tokenize"](text) return COMPAT["read_mapping"](tokens, COMPAT["mapping_block"](tokens)) def config_text(entries): mapping = "".join('"%s" { "name" "%s" "config" "" "priority" "%s" }' % (app, name, "75" if app == "0" else "250") for app, name in entries) return '"InstallConfigStore" { "Software" { "Valve" { "Steam" { "CompatToolMapping" { %s } } } } }' % mapping class RenderPathDefaultsTest(unittest.TestCase): DRIVER = "/drivers/DD-Turnip_v0.4.0/icd.json" COMMAND = ["proton", "waitforexitandrun", "/games/Spintires/SpinTires.exe"] def apply(self, env=None, command=None, model="Adreno722", library="libvulkan_freedreno.so"): env = dict({"VK_DRIVER_FILES": self.DRIVER} if env is None else env) manifest = json.dumps({"ICD": {"library_path": library}}) with mock.patch.object(Path, "read_text", return_value=model), \ mock.patch.object(Path, "is_file", return_value=False), \ mock.patch("builtins.open", mock.mock_open(read_data=manifest)): note = MODULE["render_path_defaults"](command or self.COMMAND, env) return env, note def test_tested_render_path_keeps_other_options(self): env, note = self.apply({"VK_DRIVER_FILES": self.DRIVER, "DXVK_CONFIG": "d3d9.maxFrameLatency = 2; ", "CUSTOM": "kept"}) self.assertEqual(env["DXVK_CONFIG"], "d3d9.maxFrameLatency = 2; dxvk.tilerMode = False") self.assertEqual(env["CUSTOM"], "kept") self.assertIsNotNone(note) self.assertEqual(self.apply()[0]["DXVK_CONFIG"], "dxvk.tilerMode = False") self.assertIsNotNone(self.apply({"VK_ICD_FILENAMES": self.DRIVER})[1]) def test_other_games_devices_and_drivers_keep_their_environment(self): for changes in ( {"command": ["proton", "waitforexitandrun", "/games/MudRunner.exe"]}, {"command": ["proton", "run", self.COMMAND[2]]}, {"command": ["proton"]}, {"model": "Adreno740"}, {"model": "Mali-G715"}, {"library": "libvulkan.so"}, {"env": {}}, {"env": {"VK_DRIVER_FILES": "/drivers/new-turnip/icd.json"}}, {"env": {"VK_DRIVER_FILES": "", "VK_ICD_FILENAMES": self.DRIVER}}, ): with self.subTest(changes=changes): env, note = self.apply(**changes) self.assertIsNone(note) self.assertNotIn("DXVK_CONFIG", env) def test_explicit_environment_and_config_files_take_precedence(self): for value in ("dxvk.tilerMode = Auto", "d3d9.maxFrameLatency = 1; dxvk.tilerMode=True", "dxvk.tilerMode = False"): env, note = self.apply({"VK_DRIVER_FILES": self.DRIVER, "DXVK_CONFIG": value}) self.assertIsNone(note) self.assertEqual(env["DXVK_CONFIG"], value) env, note = self.apply({"VK_DRIVER_FILES": self.DRIVER, "DXVK_CONFIG_FILE": "/games/custom.conf"}) self.assertIsNone(note) self.assertNotIn("DXVK_CONFIG", env) with mock.patch.object(Path, "is_file", return_value=True): env = {"VK_DRIVER_FILES": self.DRIVER} self.assertIsNone(MODULE["render_path_defaults"](self.COMMAND, env)) self.assertNotIn("DXVK_CONFIG", env) def test_unreadable_or_invalid_driver_metadata_leaves_launch_unchanged(self): for data in ("not-json", "{}", '{"ICD":{"library_path":null}}'): with mock.patch.object(Path, "read_text", return_value="Adreno722"), \ mock.patch.object(Path, "is_file", return_value=False), \ mock.patch("builtins.open", mock.mock_open(read_data=data)): env = {"VK_DRIVER_FILES": self.DRIVER} self.assertIsNone(MODULE["render_path_defaults"](self.COMMAND, env)) self.assertNotIn("DXVK_CONFIG", env) with mock.patch.object(Path, "read_text", side_effect=PermissionError), \ mock.patch.object(Path, "is_file", return_value=False): self.assertIsNone(MODULE["render_path_defaults"](self.COMMAND, {"VK_DRIVER_FILES": self.DRIVER})) class DirtTelemetryFixTest(unittest.TestCase): SHOWDOWN_ON = '' SHOWDOWN_DEFAULT = '' def setUp(self): self.tmp = tempfile.TemporaryDirectory() root = Path(self.tmp.name) self.prefix = root / "compatdata/2848447563" self.proton = root / "Proton/proton" self.own = root / "Proton/files/lib/wine/i386-windows/ws2_32.dll" self.own.parent.mkdir(parents=True) self.own.write_bytes(b"MZ wine ws2_32") self.proton.write_text("") self.target = self.prefix / "pfx/drive_c/windows/syswow64/ws2_32.dll" self.target.parent.mkdir(parents=True) self.target.symlink_to(self.own) self.stand_in = root / "droiddeck-dirt/ws2_32.dll" self.stand_in.parent.mkdir() self.stand_in.write_bytes(b"MZ droiddeck-dirt-ws2_32 stand-in") self.patch = mock.patch.dict(MODULE["dirt_ws2_wanted"].__globals__, {"DIRT_WS2": str(self.stand_in)}) self.patch.start() def tearDown(self): self.patch.stop() self.tmp.cleanup() def config(self, folder, text): path = self.prefix / "pfx/drive_c/users/steamuser/Documents/My Games" / folder / "hardwaresettings/hardware_settings_config.xml" path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text) def command(self, exe="showdown.exe"): return [str(self.proton), "waitforexitandrun", "/root/Storage/games/DiRT Showdown/" + exe] def env(self): return {"STEAM_COMPAT_DATA_PATH": str(self.prefix)} def test_telemetry_counts_as_on_when_the_game_sends_somewhere(self): self.config("DiRT Showdown", self.SHOWDOWN_DEFAULT) self.assertFalse(MODULE["dirt_telemetry_on"](self.prefix, "dirtshowdown")) self.config("DiRT Showdown", self.SHOWDOWN_ON) self.assertTrue(MODULE["dirt_telemetry_on"](self.prefix, "dirtshowdown")) self.config("DiRT Showdown", self.SHOWDOWN_ON.replace('enabled="true"', 'enabled="false"')) self.assertFalse(MODULE["dirt_telemetry_on"](self.prefix, "dirtshowdown")) self.config("DiRT3", '') self.assertTrue(MODULE["dirt_telemetry_on"](self.prefix, "dirt3")) def test_stand_in_is_wanted_only_for_the_dirt_games_with_telemetry_on_and_a_proton_without_the_fix(self): wanted = MODULE["dirt_ws2_wanted"] self.config("DiRT Showdown", self.SHOWDOWN_DEFAULT) self.assertIsNone(wanted(self.command(), self.env())) self.config("DiRT Showdown", self.SHOWDOWN_ON) self.assertEqual(wanted(self.command(), self.env()), str(self.prefix)) self.assertEqual(wanted(self.command("Showdown_AVX.exe"), self.env()), str(self.prefix)) self.assertIsNone(wanted(self.command("DiRTRally2.exe"), self.env())) self.own.write_bytes(b"MZ wine ws2_32 " + "showdown_avx.exe".encode("utf-16-le")) self.assertIsNone(wanted(self.command(), self.env())) def test_swap_keeps_the_prefix_link_and_restore_puts_it_back(self): MODULE["swap_dirt_ws2"](self.prefix) saved = self.target.with_name("ws2_32.dll.droiddeck-original") self.assertFalse(self.target.is_symlink()) self.assertEqual(self.target.read_bytes(), self.stand_in.read_bytes()) self.assertEqual(os.readlink(saved), str(self.own)) self.assertEqual(self.own.read_bytes(), b"MZ wine ws2_32") MODULE["swap_dirt_ws2"](self.prefix) self.assertEqual(os.readlink(saved), str(self.own)) self.assertTrue(MODULE["restore_dirt_ws2"](self.prefix, self.proton)) self.assertEqual(os.readlink(self.target), str(self.own)) self.assertFalse(os.path.lexists(saved)) self.assertFalse(MODULE["restore_dirt_ws2"](self.prefix, self.proton)) def test_restore_keeps_a_file_proton_put_back_meanwhile(self): MODULE["swap_dirt_ws2"](self.prefix) newer = self.own.with_name("ws2_32-newer.dll") newer.write_bytes(b"MZ newer wine ws2_32") self.target.unlink() self.target.symlink_to(newer) self.assertFalse(MODULE["restore_dirt_ws2"](self.prefix, self.proton)) self.assertEqual(os.readlink(self.target), str(newer)) self.assertFalse(os.path.lexists(self.target.with_name("ws2_32.dll.droiddeck-original"))) def test_after_a_proton_switch_the_running_protons_file_goes_back(self): MODULE["swap_dirt_ws2"](self.prefix) other = Path(self.tmp.name) / "GE-Proton/proton" other_own = other.parent / "files/lib/wine/i386-windows/ws2_32.dll" other_own.parent.mkdir(parents=True) other_own.write_bytes(b"MZ GE wine ws2_32") other.write_text("") self.assertTrue(MODULE["restore_dirt_ws2"](self.prefix, other)) self.assertEqual(os.readlink(self.target), str(other_own)) self.assertFalse(os.path.lexists(self.target.with_name("ws2_32.dll.droiddeck-original"))) def test_restore_follows_a_proton_reached_through_a_linked_folder(self): MODULE["swap_dirt_ws2"](self.prefix) pack = Path(self.tmp.name) / "pack" (pack / "files/lib/wine").mkdir(parents=True) (pack / "files/lib/wine/i386-windows").symlink_to(self.own.parent) (pack / "proton").write_text("") self.assertTrue(MODULE["restore_dirt_ws2"](self.prefix, pack / "proton")) self.assertEqual(os.readlink(self.target), str(self.own)) def test_a_stand_in_left_without_the_saved_link_is_replaced_by_protons_own_file(self): self.target.unlink() self.target.write_bytes(self.stand_in.read_bytes()) self.assertTrue(MODULE["restore_dirt_ws2"](self.prefix, self.proton)) self.assertEqual(os.readlink(self.target), str(self.own)) def test_launch_swaps_the_stand_in_in_for_the_game_and_back_afterwards(self): self.config("DiRT Showdown", self.SHOWDOWN_ON) self.proton.write_text("#!" + sys.executable + "\nimport os, sys\n" "target = os.path.join(os.environ['STEAM_COMPAT_DATA_PATH'], 'pfx/drive_c/windows/syswow64/ws2_32.dll')\n" "print(os.environ.get('WINEDLLOVERRIDES', ''))\n" "print(os.path.islink(target), b'droiddeck-dirt-ws2_32' in open(target, 'rb').read())\n") self.proton.chmod(0o755) runner = Path(self.tmp.name) / "run.py" runner.write_text("import runpy, sys\nmain = runpy.run_path(sys.argv[1])['main']\nmain.__globals__['DIRT_WS2'] = sys.argv[2]\n" "sys.argv = [sys.argv[1]] + sys.argv[3:]\nsys.exit(main())\n") home = Path(self.tmp.name) / "home" home.mkdir() result = subprocess.run([sys.executable, str(runner), str(BIN / "droiddeck-game-env"), str(self.stand_in), *self.command()], env={**os.environ, "HOME": str(home), "STEAM_COMPAT_DATA_PATH": str(self.prefix)}, text=True, capture_output=True) self.assertEqual(result.returncode, 0, result.stderr) overrides, state = result.stdout.splitlines()[-2:] self.assertIn("ws2_32=n,b", overrides.split(";")) self.assertEqual(state, "False True") self.assertIn("DiRT telemetry fix: ws2_32 stand-in", result.stderr) self.assertEqual(os.readlink(self.target), str(self.own)) self.assertFalse(os.path.lexists(self.target.with_name("ws2_32.dll.droiddeck-original"))) def test_a_proton_that_cannot_start_leaves_the_prefix_as_it_was(self): self.config("DiRT Showdown", self.SHOWDOWN_ON) self.proton.write_text("not a program") self.proton.chmod(0o644) runner = Path(self.tmp.name) / "run.py" runner.write_text("import runpy, sys\nmain = runpy.run_path(sys.argv[1])['main']\nmain.__globals__['DIRT_WS2'] = sys.argv[2]\n" "sys.argv = [sys.argv[1]] + sys.argv[3:]\nsys.exit(main())\n") home = Path(self.tmp.name) / "home" home.mkdir() result = subprocess.run([sys.executable, str(runner), str(BIN / "droiddeck-game-env"), str(self.stand_in), *self.command()], env={**os.environ, "HOME": str(home), "STEAM_COMPAT_DATA_PATH": str(self.prefix)}, text=True, capture_output=True) self.assertNotEqual(result.returncode, 0) self.assertIn("DiRT telemetry fix: ws2_32 stand-in", result.stderr) self.assertEqual(os.readlink(self.target), str(self.own)) self.assertFalse(os.path.lexists(self.target.with_name("ws2_32.dll.droiddeck-original"))) def test_launch_with_telemetry_off_leaves_the_prefix_alone(self): self.config("DiRT Showdown", self.SHOWDOWN_DEFAULT) self.proton.write_text("#!" + sys.executable + "\nimport os\nprint(os.environ.get('WINEDLLOVERRIDES', ''))\n") self.proton.chmod(0o755) home = Path(self.tmp.name) / "home" home.mkdir() result = subprocess.run([sys.executable, str(BIN / "droiddeck-game-env"), *self.command()], env={**os.environ, "HOME": str(home), "STEAM_COMPAT_DATA_PATH": str(self.prefix)}, text=True, capture_output=True) self.assertEqual(result.returncode, 0, result.stderr) self.assertNotIn("ws2_32=n,b", result.stdout) self.assertEqual(os.readlink(self.target), str(self.own)) def test_a_failed_put_back_does_not_end_the_launch_with_an_error(self): def fail(): raise OSError(28, "No space left on device") stderr = io.StringIO() with mock.patch("sys.stderr", stderr): code = MODULE["start"]([sys.executable, "-c", "raise SystemExit(0)"], dict(os.environ), None, fail) self.assertEqual(code, 0) self.assertIn("not put back", stderr.getvalue()) def test_a_half_done_swap_is_put_back(self): saved = self.target.with_name("ws2_32.dll.droiddeck-original") os.replace(self.target, saved) self.assertTrue(MODULE["restore_dirt_ws2"](self.prefix, self.proton)) self.assertEqual(os.readlink(self.target), str(self.own)) self.assertFalse(os.path.lexists(saved)) def test_a_native_ws2_32_goes_back_as_it_was_after_a_proton_switch(self): self.target.unlink() self.target.write_bytes(b"MZ a mod's own ws2_32") MODULE["swap_dirt_ws2"](self.prefix) other = Path(self.tmp.name) / "GE-Proton/proton" (other.parent / "files/lib/wine/i386-windows").mkdir(parents=True) (other.parent / "files/lib/wine/i386-windows/ws2_32.dll").write_bytes(b"MZ GE wine ws2_32") other.write_text("") self.assertTrue(MODULE["restore_dirt_ws2"](self.prefix, other)) self.assertFalse(self.target.is_symlink()) self.assertEqual(self.target.read_bytes(), b"MZ a mod's own ws2_32") def test_a_link_to_a_mods_ws2_32_goes_back_as_it_was(self): mod = Path(self.tmp.name) / "mods/ws2_32.dll" mod.parent.mkdir() mod.write_bytes(b"MZ a mod's ws2_32") self.target.unlink() self.target.symlink_to(mod) MODULE["swap_dirt_ws2"](self.prefix) other = Path(self.tmp.name) / "GE-Proton/proton" (other.parent / "files/lib/wine/i386-windows").mkdir(parents=True) (other.parent / "files/lib/wine/i386-windows/ws2_32.dll").write_bytes(b"MZ GE wine ws2_32") other.write_text("") self.assertTrue(MODULE["restore_dirt_ws2"](self.prefix, other)) self.assertEqual(os.readlink(self.target), str(mod)) def test_start_gives_the_signal_handlers_back(self): import signal before = {s: signal.getsignal(s) for s in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP)} MODULE["start"]([sys.executable, "-c", "pass"], dict(os.environ), None, lambda: None) self.assertEqual(before, {s: signal.getsignal(s) for s in before}) def test_start_puts_the_file_back_after_the_game_exits(self): calls = [] code = MODULE["start"]([sys.executable, "-c", "raise SystemExit(3)"], dict(os.environ), None, lambda: calls.append("restored")) self.assertEqual(code, 3) self.assertEqual(calls, ["restored"]) with self.assertRaises(OSError): MODULE["start"]([str(Path(self.tmp.name) / "missing")], dict(os.environ), None, lambda: calls.append("again")) self.assertEqual(calls, ["restored"]) class EgoCorePlanTest(unittest.TestCase): @staticmethod def rule(op, value, worker_map): return (' \r\n \r\n' ' \r\n' ' \r\n \r\n' % (op, value, worker_map)) def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.addCleanup(self.tmp.cleanup) self.system = Path(self.tmp.name) / "DiRT 3/system" self.system.mkdir(parents=True) cpu = "".join(self.rule("int_eq", n, "workerMap%dCore.xml" % n) for n in (2, 3, 4, 6)) + self.rule("int_ge", 8, "workerMap8Core.xml") self.shipped = ('\r\n\r\n \r\n' ' \r\n' + cpu + ' \r\n \r\n' ' \r\n' ' \r\n \r\n \r\n \r\n' ' \r\n' + self.rule("int_eq", 5, "audio.xml") + ' \r\n' '').encode("latin-1") self.path = self.system / "hardware_settings_restrictions.xml" self.path.write_bytes(self.shipped) for name in ("workerMap.xml", "workerMap2Core.xml", "workerMap3Core.xml", "workerMap4Core.xml", "workerMap6Core.xml", "workerMap8Core.xml"): (self.system / name).write_text("") self.command = ["/proton", "waitforexitandrun", str(self.system.parent / "dirt3_game.exe")] self.saved = self.path.with_name(self.path.name + ".droiddeck-original") def plan(self, topology, command=None): env = {} if topology is None else {"WINE_CPU_TOPOLOGY": topology} return MODULE["ego_core_plan"](command or self.command, env) def test_five_cores_get_a_copy_of_the_four_core_plan(self): note = self.plan("5:7,3,4,5,6") self.assertIn("5 cores", note) self.assertIn("4-core plan", note) text = self.path.read_bytes() four = self.rule("int_eq", 4, "workerMap4Core.xml").encode("latin-1") five = self.rule("int_eq", 5, "workerMap4Core.xml").encode("latin-1") self.assertEqual(text, self.shipped.replace(four, four + five)) self.assertNotIn(b"\n", text.replace(b"\r\n", b"")) self.assertEqual(self.saved.read_bytes(), self.shipped) self.assertEqual(sorted(p.name for p in self.system.glob("*droiddeck-tmp*")), []) self.assertIsNone(self.plan("5:7,3,4,5,6")) self.assertEqual(self.path.read_bytes(), text) self.assertEqual(self.saved.read_bytes(), self.shipped) def test_seven_cores_get_the_six_core_plan(self): self.assertIn("6-core plan", self.plan("7:7,1,2,3,4,5,6")) self.assertIn(b'value="7">\r\n \r\n > %s\nexit ${EXIT_CODE:-0}\n" % shlex.quote(str(runs))) proton.chmod(0o755) def evaluate(exit_code="0"): script.write_text(self.SCRIPT if exit_code != "changed" else self.SCRIPT.replace("setup.exe", "setup-update.exe")) env = dict(os.environ, HOME=tmp, STEAM_COMPAT_CLIENT_INSTALL_PATH=str(root), STEAM_COMPAT_DATA_PATH=str(prefix.parent), EXIT_CODE="0" if exit_code == "changed" else exit_code) result = subprocess.run([sys.executable, str(BIN / "droiddeck-game-env"), str(proton), "run", str(evaluator), "legacycompat\\evaluatorscript_1.vdf"], env=env, text=True, capture_output=True) return result.returncode, len(runs.read_text().splitlines()) if runs.exists() else 0 self.assertEqual(evaluate("3"), (3, 1)) self.assertFalse((prefix / ".droiddeck-install-scripts").exists()) self.assertEqual(evaluate(), (0, 2)) self.assertEqual(evaluate(), (0, 2)) self.assertEqual(evaluate("changed"), (0, 3)) def test_the_mali_bridge_caps_the_vkd3d_feature_level(self): bridge = {"BL_VKBRIDGE": "1", "VKD3D_FEATURE_LEVEL": "12_2"} self.assertIn("capped", MODULE["bridge_feature_level"](bridge)) self.assertEqual(bridge["VKD3D_FEATURE_LEVEL"], "12_0") for env in ({"BL_VKBRIDGE": "1", "VKD3D_FEATURE_LEVEL": "11_1"}, {"VKD3D_FEATURE_LEVEL": "12_2"}, {"BL_VKBRIDGE": "1"}): before = dict(env) self.assertIsNone(MODULE["bridge_feature_level"](env)) self.assertEqual(env, before) def test_profile_precedence_and_unset(self): env = {"KEEP": "inherited", "REMOVE": "inherited", "CUSTOM": "launch option"} config = {"version": 1, "shared": {"CUSTOM": "shared"}, "games": {"42": {"REMOVE": None, "CUSTOM": "game", "EMPTY": ""}}} self.assertEqual(MODULE["apply_config"](env, config, "42"), {"KEEP": "inherited", "CUSTOM": "game", "EMPTY": ""}) self.assertEqual(env["REMOVE"], "inherited") self.assertEqual(MODULE["apply_config"](env, config, "43")["CUSTOM"], "shared") def test_games_get_their_own_turnip_debug_and_a_game_profile_still_wins(self): gpu = MODULE["game_gpu"] self.assertEqual(gpu({"TU_DEBUG": "sysmem", "DROIDDECK_GAME_TU_DEBUG": "", "KEEP": "1"}), {"KEEP": "1"}) self.assertEqual(gpu({"TU_DEBUG": "sysmem", "DROIDDECK_GAME_TU_DEBUG": "sysmem"}), {"TU_DEBUG": "sysmem"}) self.assertEqual(gpu({"DROIDDECK_GAME_TU_DEBUG": "nolrz"}), {"TU_DEBUG": "nolrz"}) self.assertEqual(gpu({"TU_DEBUG": "sysmem"}), {"TU_DEBUG": "sysmem"}) config = {"version": 1, "games": {"42": {"TU_DEBUG": "sysmem"}}} env = gpu({"TU_DEBUG": "sysmem", "DROIDDECK_GAME_TU_DEBUG": ""}) self.assertEqual(MODULE["apply_config"](env, config, "42")["TU_DEBUG"], "sysmem") self.assertNotIn("TU_DEBUG", MODULE["apply_config"](env, config, "43")) def test_the_vram_cap_comes_before_the_games_own_dxvk_settings(self): cap = MODULE["vram_cap"] self.assertEqual(cap({"BL_VRAM_MB": "4096"})["DXVK_CONFIG"], "dxgi.maxDeviceMemory = 4096") self.assertEqual(cap({"BL_VRAM_MB": "2048", "DXVK_CONFIG": "dxvk.maxFrameRate = 60;"})["DXVK_CONFIG"], "dxgi.maxDeviceMemory = 2048; dxvk.maxFrameRate = 60") for off in ({}, {"BL_VRAM_MB": "0"}, {"BL_VRAM_MB": "lots"}, {"BL_VRAM_MB": "²"}, {"BL_VRAM_MB": "-1"}): self.assertNotIn("DXVK_CONFIG", cap(off)) self.assertNotIn("DROIDDECK_VRAM_CAP", cap(off)) self.assertNotIn("DROIDDECK_VRAM_CAP_MB", cap(off)) def test_the_vram_cap_also_turns_on_the_vulkan_layer_that_holds_the_game_to_it(self): env = MODULE["vram_cap"]({"BL_VRAM_MB": "3072", "DXVK_CONFIG": "dxgi.maxDeviceMemory = 16384"}) self.assertEqual((env["DROIDDECK_VRAM_CAP"], env["DROIDDECK_VRAM_CAP_MB"]), ("1", "3072")) def test_a_games_wine_sync_choice_also_reaches_its_setup_runs(self): env = {"BL_SYNC": "1", "BL_SYNC_FALLBACK": "1", "BL_FSYNC_FIRST": "0", "OTHER": "kept"} config = {"version": 1, "games": {"42": {"BL_SYNC": "0", "BL_FSYNC_FIRST": "1", "PROTON_NO_ESYNC": "1", "FEX_TSOENABLED": "1"}}} chosen = MODULE["apply_config"](env, MODULE["sync_config"](config, "42"), "42") self.assertEqual(chosen, {"BL_SYNC": "0", "BL_SYNC_FALLBACK": "1", "BL_FSYNC_FIRST": "1", "PROTON_NO_ESYNC": "1", "OTHER": "kept"}) def test_component_profiles_override_general_and_restore_it_for_the_next_game(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) home = root / "home" component_dir = home / ".local/share/droiddeck-components" base = root / "proton" target = base / "files/lib/wine/dxvk/aarch64-windows/d3d11.dll" leftover = target.with_name("d3d8.dll") component_dir.mkdir(parents=True) target.parent.mkdir(parents=True) (base / "version").write_text("1 build-1\n") general = root / "general.dll" game = root / "game.dll" general.write_text("general") game.write_text("game") target.write_text("old") leftover.write_text("old") digest = lambda path: hashlib.sha256(path.read_bytes()).hexdigest() (component_dir / "desired.tsv").write_text( "*\tproton\tbuild-1\tdxvk\tfiles/lib/wine/dxvk/aarch64-windows/d3d11.dll\t%s\t%s\n" "42\tproton\tbuild-1\tdxvk\tfiles/lib/wine/dxvk/aarch64-windows/d3d11.dll\t%s\t%s\n" "42\tproton\tbuild-1\tdxvk\tfiles/lib/wine/dxvk/aarch64-windows/d3d8.dll\t-\t-\n" % (digest(general), general, digest(game), game) ) script = COMPAT["BL_COMPONENTS_SETUP"] + '\nverb=waitforexitandrun; bl_components "$1"\n' env = dict(os.environ, HOME=str(home), STEAM_COMPAT_DATA_PATH="/steam/compatdata/42") subprocess.run(["bash", "-c", script, "test", str(base)], env=env, check=True) self.assertEqual(target.read_text(), "game") self.assertFalse(leftover.exists()) env["STEAM_COMPAT_DATA_PATH"] = "/steam/compatdata/43" subprocess.run(["bash", "-c", script, "test", str(base)], env=env, check=True) self.assertEqual(target.read_text(), "general") def test_system_powervr_preserves_dxvk_profiles_without_applying_them(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) home = root / "home" component_dir = home / ".local/share/droiddeck-components" component_dir.mkdir(parents=True) base = root / "proton" paths = { "dxvk": "files/lib/wine/dxvk/aarch64-windows/d3d11.dll", "fex": "files/lib/wine/aarch64-windows/libarm64ecfex.dll", "vkd3d": "files/lib/wine/vkd3d-proton/aarch64-windows/d3d12.dll", } rows = [] for comp, rel in paths.items(): target = base / rel target.parent.mkdir(parents=True, exist_ok=True) target.write_text("bundled " + comp) for scope in ("*", "42"): source = root / (comp + ("-general" if scope == "*" else "-game")) source.write_text(source.name) rows.append("%s\tproton\tbuild-1\t%s\t%s\t%s\t%s\n" % ( scope, comp, rel, hashlib.sha256(source.read_bytes()).hexdigest(), source, )) leftover = (base / paths["dxvk"]).with_name("d3d8.dll") leftover.write_text("bundled d3d8") rows.append("42\tproton\tbuild-1\tdxvk\t%s\t-\t-\n" % leftover.relative_to(base)) (base / "version").write_text("1 build-1\n") selection = component_dir / "desired.tsv" selection.write_text("".join(rows)) saved = selection.read_bytes() script = COMPAT["BL_COMPONENTS_SETUP"] + '\nverb=waitforexitandrun; bl_components "$1"\n' env = dict(os.environ, HOME=str(home), STEAM_COMPAT_DATA_PATH="/steam/compatdata/42", DROIDDECK_POWERVR_RUNTIME="/system_vulkan/runtime") subprocess.run(["bash", "-c", script, "test", str(base)], env=env, check=True) self.assertEqual((base / paths["dxvk"]).read_text(), "bundled dxvk") self.assertEqual(leftover.read_text(), "bundled d3d8") for comp in ("fex", "vkd3d"): self.assertEqual((base / paths[comp]).read_text(), comp + "-game") self.assertEqual(selection.read_bytes(), saved) # A subsequent manual-driver session still applies the saved per-game DXVK choice. env.pop("DROIDDECK_POWERVR_RUNTIME") subprocess.run(["bash", "-c", script, "test", str(base)], env=env, check=True) self.assertEqual((base / paths["dxvk"]).read_text(), "dxvk-game") self.assertFalse(leftover.exists()) self.assertEqual(selection.read_bytes(), saved) def test_component_scope_matches_environment_scope_for_shortcut_ids(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) component_dir = root / ".local/share/droiddeck-components" base = root / "proton" target = base / "files/lib/wine/dxvk/aarch64-windows/d3d11.dll" component_dir.mkdir(parents=True) target.parent.mkdir(parents=True) (base / "version").write_text("1 build-1\n") rows = [] for scope in ("*", "42", "4294967295", "2147483648"): source = root / ("default" if scope == "*" else scope) source.write_text(scope) rows.append("%s\tproton\tbuild-1\tdxvk\t%s\t%s\t%s\n" % ( scope, target.relative_to(base), hashlib.sha256(source.read_bytes()).hexdigest(), source, )) (component_dir / "desired.tsv").write_text("".join(rows)) script = COMPAT["BL_COMPONENTS_SETUP"] + '\nverb=waitforexitandrun; bl_components "$1"\n' for value in ( "42", "00042", "43", "-1", "-0001", "4294967295", "-2147483648", "2147483648", "-1///", "0", "-0", "000", "-2147483649", "4294967296", "999999999999999999999999999", "not-an-id", "1+1", "", ): with self.subTest(prefix=value): env = dict(os.environ, HOME=str(root), STEAM_COMPAT_DATA_PATH="/steam/compatdata/" + value) target.write_text("untouched") subprocess.run(["bash", "-c", script, "test", str(base)], env=env, check=True, capture_output=True) scope = MODULE["game_id"](env) expected = "untouched" if scope is None else scope if scope in {"42", "4294967295", "2147483648"} else "*" self.assertEqual(target.read_text(), expected) def test_engine_fixes_follow_the_games_files(self): with tempfile.TemporaryDirectory() as tmp: game = Path(tmp) / "Godot Game" (game / "data_game_windows_x86_64").mkdir(parents=True) for name in ("Game.exe", "Game.pck", "libsentry.windows.release.x86_64.dll", "data_game_windows_x86_64/coreclr.dll"): (game / name).touch() env, extra, notes = MODULE["engine_fixes"](str(game / "Game.exe"), []) self.assertEqual(env["FEX_TSOENABLED"], "1") self.assertNotIn("FEX_MULTIBLOCK", env) self.assertEqual(env["WINEDLLOVERRIDES"], "libsentry.windows.release.x86_64=d") self.assertEqual(extra, ["--rendering-driver", "vulkan"]) self.assertEqual(MODULE["engine_fixes"](str(game / "Game.exe"), ["--rendering-driver", "opengl3"])[1], []) script = Path(tmp) / "GDScript Game" script.mkdir() for name in ("Game.exe", "Game.pck", "libsentry.windows.release.x86_64.dll"): (script / name).touch() env, extra, notes = MODULE["engine_fixes"](str(script / "Game.exe"), []) self.assertNotIn("WINEDLLOVERRIDES", env) self.assertEqual(extra, ["--rendering-driver", "vulkan"]) other = Path(tmp) / "Other" other.mkdir() (other / "Other.exe").touch() self.assertEqual(MODULE["engine_fixes"](str(other / "Other.exe"), []), ({}, [], [])) def test_easy_anti_cheat_games_start_offline_only_when_opted_in(self): with tempfile.TemporaryDirectory() as tmp: game = Path(tmp) / "ELDEN RING/Game" (game / "EasyAntiCheat").mkdir(parents=True) for name in ("start_protected_game.exe", "eldenring.exe"): (game / name).touch() (game / "EasyAntiCheat/settings.json").write_text('\ufeff{ "executable" : "eldenring.exe" }', encoding="utf-8") command = ["proton", "waitforexitandrun", str(game / "start_protected_game.exe"), "-arg"] offline = MODULE["offline_without_anticheat"] self.assertEqual(offline(command, {}), command) self.assertEqual(offline(command, {"DROIDDECK_ANTICHEAT_OFFLINE": "0"}), command) self.assertEqual(offline(command, {"DROIDDECK_ANTICHEAT_OFFLINE": "1"}), ["proton", "waitforexitandrun", str(game / "eldenring.exe"), "-arg"]) (game / "EasyAntiCheat/settings.json").write_text('{ "executable" : "../../escape.exe" }') (Path(tmp) / "escape.exe").touch() self.assertEqual(offline(command, {"DROIDDECK_ANTICHEAT_OFFLINE": "1"}), command) (game / "EasyAntiCheat/settings.json").write_text('{ "executable" : "missing.exe" }') self.assertEqual(offline(command, {"DROIDDECK_ANTICHEAT_OFFLINE": "1"}), command) (game / "EasyAntiCheat/settings.json").write_text('{ "executable" : "link/escape.exe" }') outside = Path(tmp) / "outside" outside.mkdir() (outside / "escape.exe").touch() (game / "link").symlink_to(outside, target_is_directory=True) self.assertEqual(offline(command, {"DROIDDECK_ANTICHEAT_OFFLINE": "1"}), command) (game / "EasyAntiCheat/settings.json").write_text('{ "executable" : "eldenring.exe" }') plain = ["proton", "waitforexitandrun", str(game / "eldenring.exe")] self.assertEqual(offline(plain, {"DROIDDECK_ANTICHEAT_OFFLINE": "1"}), plain) self.assertEqual(offline(["proton", "run", command[2]], {"DROIDDECK_ANTICHEAT_OFFLINE": "1"}), ["proton", "run", command[2]]) def test_easy_anti_cheat_forwarding_through_entrypoint(self): with tempfile.TemporaryDirectory() as tmp: home = Path(tmp) game = home / "ELDEN RING/Game" (game / "EasyAntiCheat").mkdir(parents=True) bootstrapper = game / "start_protected_game.exe" target = game / "eldenring.exe" bootstrapper.touch() target.touch() (game / "EasyAntiCheat/settings.json").write_text(json.dumps({"executable": "eldenring.exe"})) proton = home / "fake-proton" proton.write_text("#!" + sys.executable + "\nimport json, sys\nprint(json.dumps(sys.argv[1:]))\n") proton.chmod(0o755) config = home / ".config/droiddeck/game-environment.json" config.parent.mkdir(parents=True) env = dict(os.environ, HOME=tmp, STEAM_COMPAT_DATA_PATH="/compatdata/42") env.pop("DROIDDECK_ANTICHEAT_OFFLINE", None) for setting, expected in (("1", target), ("0", bootstrapper), (None, bootstrapper)): with self.subTest(setting=setting): values = {} if setting is None else {"DROIDDECK_ANTICHEAT_OFFLINE": setting} config.write_text(json.dumps({"version": 1, "games": {"42": values}})) result = subprocess.run([sys.executable, str(BIN / "droiddeck-game-env"), str(proton), "waitforexitandrun", str(bootstrapper), "-arg"], env=env, text=True, capture_output=True, check=True) self.assertEqual(json.loads(result.stdout.strip()), ["waitforexitandrun", str(expected), "-arg"]) def test_deep_rock_gdk_avoids_the_raytracing_pipeline_crash(self): game = "/game/FSD/Binaries/WinGDK/FSD-WinGDK-Shipping.exe" env, extra, notes = MODULE["engine_fixes"](game, []) self.assertEqual(extra, ["-noraytracing", "-ini:Engine:[/Script/Engine.RendererSettings]:r.RayTracing=False"]) self.assertEqual(env, {}) self.assertTrue(notes) for choice in ("-raytracing", "-noraytracing"): self.assertEqual(MODULE["engine_fixes"](game, [choice]), ({}, [], [])) self.assertEqual(MODULE["engine_fixes"]("/game/FSD-Win64-Shipping.exe", []), ({}, [], [])) self.assertEqual(MODULE["engine_fixes"]("/game/Other-WinGDK-Shipping.exe", []), ({}, [], [])) def test_arkham_city_gets_full_x87_precision_over_the_preset(self): with tempfile.TemporaryDirectory() as tmp: city = Path(tmp) / "Batman Arkham City GOTY/Binaries/Win32" city.mkdir(parents=True) for name in ("BatmanAC.exe", "BmLauncher.exe"): (city / name).touch() for exe in ("BatmanAC.exe", "BmLauncher.exe"): env, extra, notes = MODULE["engine_fixes"](str(city / exe), []) self.assertEqual((env, extra), ({"FEX_X87REDUCEDPRECISION": "0"}, [])) config = {"version": 1, "shared": {"FEX_X87REDUCEDPRECISION": "1"}} self.assertEqual(MODULE["apply_config"]({}, config, "200260", env)["FEX_X87REDUCEDPRECISION"], "0") config["games"] = {"200260": {"FEX_X87REDUCEDPRECISION": "1"}} self.assertEqual(MODULE["apply_config"]({}, config, "200260", env)["FEX_X87REDUCEDPRECISION"], "1") asylum = Path(tmp) / "Batman Arkham Asylum GOTY/Binaries" asylum.mkdir(parents=True) for name in ("ShippingPC-BmGame.exe", "BmLauncher.exe"): (asylum / name).touch() self.assertEqual(MODULE["engine_fixes"](str(asylum / "BmLauncher.exe"), []), ({}, [], [])) def test_d3dgear_capture_hook_beside_the_game_is_turned_off(self): with tempfile.TemporaryDirectory() as tmp: game = Path(tmp) / "The Crew 2" game.mkdir() for name in ("TheCrew2.exe", "TheCrew2_BE.exe", "d3dgear64.dll"): (game / name).touch() env, extra, notes = MODULE["engine_fixes"](str(game / "TheCrew2.exe"), []) self.assertEqual((env, extra), ({"WINEDLLOVERRIDES": "d3dgear64=d"}, [])) self.assertTrue(notes) (game / "d3dgear64.dll").rename(game / "D3DGear64.DLL") self.assertEqual(MODULE["engine_fixes"](str(game / "TheCrew2.exe"), [])[0], {"WINEDLLOVERRIDES": "d3dgear64=d"}) (game / "D3DGear64.DLL").unlink() self.assertEqual(MODULE["engine_fixes"](str(game / "TheCrew2.exe"), []), ({}, [], [])) def test_doom_2016_keeps_the_swapchain_image_count_it_asks_for(self): with tempfile.TemporaryDirectory() as tmp: game = Path(tmp) / "DOOM (2016)" game.mkdir() for name in ("DOOMx64.exe", "DOOMx64vk.exe", "Galaxy64.dll"): (game / name).touch() images = { "GAMESCOPE_WSI_MIN_IMAGE_COUNT": None, "GAMESCOPE_WSI_ENSURE_MIN_IMAGE_COUNT": None, "vk_x11_strict_image_count": "true", } for exe in ("DOOMx64.exe", "DOOMx64vk.exe"): env, extra, notes = MODULE["engine_fixes"](str(game / exe), []) self.assertEqual((env, extra), (images, [])) self.assertTrue(notes) launched = {"GAMESCOPE_WSI_MIN_IMAGE_COUNT": "8", "GAMESCOPE_WSI_ENSURE_MIN_IMAGE_COUNT": "1"} result = MODULE["apply_config"](launched, {"version": 1}, "3566086475", env) self.assertNotIn("GAMESCOPE_WSI_MIN_IMAGE_COUNT", result) self.assertNotIn("GAMESCOPE_WSI_ENSURE_MIN_IMAGE_COUNT", result) self.assertEqual(result["vk_x11_strict_image_count"], "true") config = {"version": 1, "games": {"3566086475": {"GAMESCOPE_WSI_MIN_IMAGE_COUNT": "3"}}} self.assertEqual(MODULE["apply_config"](launched, config, "3566086475", env)["GAMESCOPE_WSI_MIN_IMAGE_COUNT"], "3") eternal = Path(tmp) / "DOOMEternal" eternal.mkdir() (eternal / "DOOMEternalx64vk.exe").touch() self.assertEqual(MODULE["engine_fixes"](str(eternal / "DOOMEternalx64vk.exe"), []), ({}, [], [])) other = Path(tmp) / "Other" other.mkdir() for name in ("Other.exe", "DOOMx64vk.exe"): (other / name).touch() self.assertEqual(MODULE["engine_fixes"](str(other / "Other.exe"), []), ({}, [], [])) def test_dll_overrides_from_several_fixes_are_all_kept(self): with tempfile.TemporaryDirectory() as tmp: for name in ("Game.exe", "Game.pck", "GodotSharp.dll", "libsentry.windows.release.x86_64.dll", "d3dgear64.dll"): (Path(tmp) / name).touch() env = MODULE["engine_fixes"](str(Path(tmp) / "Game.exe"), [])[0] self.assertEqual(env["WINEDLLOVERRIDES"], "libsentry.windows.release.x86_64=d;d3dgear64=d") def test_black_ops_iii_and_dark_souls_remastered_skip_protonfixes(self): for exe in ("/game/Call of Duty Black Ops III/BlackOps3.exe", "/game/DARK SOULS REMASTERED/DarkSoulsRemastered.exe"): env, extra, notes = MODULE["engine_fixes"](exe, []) self.assertEqual((env, extra), ({"PROTONFIXES_DISABLE": "1"}, [])) self.assertTrue(notes) self.assertEqual(MODULE["engine_fixes"]("/game/Game/DarkSoulsII.exe", []), ({}, [], [])) def test_little_nightmares_keeps_its_render_thread(self): env, extra, notes = MODULE["engine_fixes"]("/game/Little Nightmares/Atlas/Binaries/Win64/LittleNightmares.exe", []) self.assertEqual((env, extra), ({"PROTONFIXES_DISABLE": "1"}, [])) self.assertTrue(notes) self.assertEqual(MODULE["engine_fixes"]("/game/Little Nightmares II/Moonlight/Binaries/Win64/LittleNightmaresII.exe", []), ({}, [], [])) def test_game_cores_are_parsed_from_lists_and_ranges(self): parse = MODULE["parse_cores"] self.assertEqual(parse("0-3,6"), {0, 1, 2, 3, 6}) self.assertEqual(parse(" 4, 5 "), {4, 5}) for bad in ("", "a", "3-1", "1,,2", "-1", "1-"): self.assertIsNone(parse(bad), bad) def test_a_profiles_game_cores_pin_the_game_within_the_online_cores(self): calls = [] os_mod = MODULE["os"] with mock.patch.dict(MODULE["game_cores"].__globals__, {"online_cores": lambda: {0, 1, 2, 3, 4, 5, 6, 7}}), \ mock.patch.object(os_mod, "sched_setaffinity", side_effect=lambda pid, cores: calls.append(set(cores)), create=True): MODULE["game_cores"]({"DROIDDECK_GAME_CORES": "0-2,9"}) MODULE["game_cores"]({"DROIDDECK_GAME_CORES": "junk"}) MODULE["game_cores"]({"DROIDDECK_GAME_CORES": "12-13"}) MODULE["game_cores"]({}) self.assertEqual(calls, [{0, 1, 2}]) self.assertEqual(MODULE["parse_cores"]("0,2-7"), {0, 2, 3, 4, 5, 6, 7}) def test_windows_games_see_only_the_cores_they_are_pinned_to(self): os_mod = MODULE["os"] online = {"online_cores": lambda: {0, 1, 2, 3, 4, 5, 6, 7}} cases = [ ({"BL_GAME_CPUS": "6,7"}, {6, 7}, "2:6,7", "2:6,7"), ({"DROIDDECK_GAME_CORES": "0-2"}, {0, 1, 2}, "3:0,1,2", "3:0,1,2"), ({"BL_GAME_CPUS": "0-7"}, set(range(8)), None, None), ({}, {6, 7}, None, None), ({"BL_GAME_CPUS": "6,7", "WINE_CPU_TOPOLOGY": "4:0,1,2,3"}, {6, 7}, "4:0,1,2,3", "4:0,1,2,3"), ({"WINE_CPU_TOPOLOGY": "2:0,1"}, set(range(8)), "2:0,1", "2:0,1"), ({"BL_GAME_CPUS": "6,7", "WINE_CPU_TOPOLOGY": ""}, {6, 7}, "", None), ({"BL_GAME_CPUS": "6,7", "PROTON_CPU_TOPOLOGY": "1:7"}, {6, 7}, None, "1:7"), ({"BL_GAME_CPUS": "9"}, {9}, None, None), ] for env, affinity, expected, proton in cases: env["DROIDDECK_FASTEST_CORES"] = "0" with mock.patch.dict(MODULE["cpu_topology"].__globals__, online), \ mock.patch.object(os_mod, "sched_getaffinity", return_value=affinity, create=True): MODULE["cpu_topology"](env) self.assertEqual(env.get("WINE_CPU_TOPOLOGY"), expected, env) self.assertEqual(env.get("PROTON_CPU_TOPOLOGY"), proton, env) def test_only_games_get_the_larger_swapchain(self): game = {"STEAM_COMPAT_DATA_PATH": "/steam/compatdata/945360"} self.assertTrue(MODULE["game_images"](game)) self.assertEqual(game["GAMESCOPE_WSI_MIN_IMAGE_COUNT"], "8") self.assertEqual(game["GAMESCOPE_WSI_ENSURE_MIN_IMAGE_COUNT"], "1") own = {"STEAM_COMPAT_DATA_PATH": "/steam/compatdata/945360", "GAMESCOPE_WSI_MIN_IMAGE_COUNT": "3"} MODULE["game_images"](own) self.assertEqual(own["GAMESCOPE_WSI_MIN_IMAGE_COUNT"], "3") for env in ({}, {"STEAM_COMPAT_DATA_PATH": "/steam/compatdata/0"}): self.assertFalse(MODULE["game_images"](env)) self.assertNotIn("GAMESCOPE_WSI_MIN_IMAGE_COUNT", env) def test_opengl_games_do_not_wait_for_the_screen_refresh(self): cases = [ ({}, "0"), ({"STEAM_COMPAT_DATA_PATH": "/steam/compatdata/-686078602"}, "0"), ({"vblank_mode": "2"}, "2"), ({"vblank_mode": ""}, ""), ({"STEAM_COMPAT_DATA_PATH": "/steam/compatdata/0"}, None), ({"STEAM_COMPAT_DATA_PATH": ""}, None), ] for env, expected in cases: env.setdefault("STEAM_COMPAT_DATA_PATH", "/steam/compatdata/3608888694") added = expected == "0" and "vblank_mode" not in env self.assertEqual(MODULE["gl_vsync"](env), added, env) self.assertEqual(env.get("vblank_mode"), expected, env) said = "OpenGL does not wait for the screen refresh" with tempfile.TemporaryDirectory() as tmp: probe = Path(tmp) / "probe" probe.write_text("#!" + sys.executable + "\nimport os\nprint(os.environ.get('vblank_mode', 'unset'))\n") probe.chmod(0o755) config = Path(tmp) / ".config/droiddeck/game-environment.json" config.parent.mkdir(parents=True) for inherited, edits, expected in (({}, None, "0"), ({"vblank_mode": "3"}, None, "3"), ({}, {"vblank_mode": None}, "unset"), ({}, {"vblank_mode": "2"}, "2"), ({"vblank_mode": "3"}, {"vblank_mode": None}, "unset")): with self.subTest(inherited=inherited, edits=edits): config.write_text(json.dumps({"version": 1, "games": {"42": edits or {}}})) env = {name: value for name, value in os.environ.items() if name != "vblank_mode"} env.update(HOME=tmp, STEAM_COMPAT_DATA_PATH="/compatdata/42", **inherited) result = subprocess.run([sys.executable, str(BIN / "droiddeck-game-env"), str(probe), "waitforexitandrun"], env=env, text=True, capture_output=True, check=True) self.assertEqual(result.stdout.strip(), expected) self.assertEqual(said in result.stderr, expected == "0" and not inherited, result.stderr) def test_teardown_keeps_its_own_vsync_unless_the_player_chose(self): with tempfile.TemporaryDirectory() as tmp: probe = Path(tmp) / "probe" probe.write_text("#!" + sys.executable + "\nimport os\nprint(os.environ.get('vblank_mode', 'unset'))\n") probe.chmod(0o755) game = Path(tmp) / "Teardown/teardown.exe" game.parent.mkdir() game.write_bytes(b"MZ") config = Path(tmp) / ".config/droiddeck/game-environment.json" config.parent.mkdir(parents=True) for inherited, edits, shared, expected in (({}, None, None, "2"), ({"vblank_mode": "0"}, None, None, "0"), ({}, {"vblank_mode": "1"}, None, "1"), ({}, None, {"vblank_mode": "1"}, "1")): with self.subTest(inherited=inherited, edits=edits, shared=shared): config.write_text(json.dumps({"version": 1, "shared": shared or {}, "games": {"1167630": edits or {}}})) env = {name: value for name, value in os.environ.items() if name != "vblank_mode"} env.update(HOME=tmp, STEAM_COMPAT_DATA_PATH="/compatdata/1167630", **inherited) result = subprocess.run([sys.executable, str(BIN / "droiddeck-game-env"), str(probe), "waitforexitandrun", str(game)], env=env, text=True, capture_output=True, check=True) self.assertEqual(result.stdout.strip(), expected, result.stderr) def test_windows_cpu_0_is_the_fastest_core(self): os_mod = MODULE["os"] pocket_fit = {0: 379, 1: 379, 2: 923, 3: 923, 4: 923, 5: 867, 6: 867, 7: 1024} every = set(range(8)) with tempfile.TemporaryDirectory() as tmp: proton = Path(tmp) / "proton" proton.write_text('default_cpu_limit = {\n "19900" : "16", # Far Cry 2\n' ' "2229830" : "1", # Command & Conquer\n "11440" : "4", # DiRT\n' ' "1038250" : "P", # DIRT 5\n "618970" : 4, # Outcast, written as Proton 10 does\n' '}\n\nother = {\n "620" : "2",\n}\n') command = [str(proton), "waitforexitandrun", "/game/game.exe"] cases = [ ({}, every, pocket_fit, "8:7,2,3,4,5,6,0,1", "8:7,2,3,4,5,6,0,1"), ({"BL_GAME_CPUS": "6,7"}, {6, 7}, pocket_fit, "2:7,6", "2:7,6"), ({"DROIDDECK_GAME_CORES": "0-2"}, {0, 1, 2}, pocket_fit, "3:2,0,1", "3:2,0,1"), ({"BL_GAME_CPUS": "0-7"}, every, pocket_fit, "8:7,2,3,4,5,6,0,1", "8:7,2,3,4,5,6,0,1"), ({"WINE_CPU_TOPOLOGY": "2:0,1"}, every, pocket_fit, "2:0,1", "2:0,1"), ({"WINE_CPU_TOPOLOGY": ""}, every, pocket_fit, "", None), ({"PROTON_CPU_TOPOLOGY": "1:0"}, every, pocket_fit, None, "1:0"), ({"DROIDDECK_FASTEST_CORES": "0"}, every, pocket_fit, None, None), ({"SteamGameId": "2229830"}, every, pocket_fit, "1:7", "1:7"), ({"SteamGameId": "11440"}, every, pocket_fit, "4:7,2,3,4", "4:7,2,3,4"), ({"SteamGameId": "19900"}, every, pocket_fit, "8:7,2,3,4,5,6,0,1", "8:7,2,3,4,5,6,0,1"), ({"SteamGameId": "1038250"}, every, pocket_fit, None, None), ({"SteamGameId": "618970"}, every, pocket_fit, "4:7,2,3,4", "4:7,2,3,4"), ({"SteamGameId": "620"}, every, pocket_fit, "8:7,2,3,4,5,6,0,1", "8:7,2,3,4,5,6,0,1"), ({"SteamGameId": "2229830", "BL_GAME_CPUS": "0,1"}, {0, 1}, pocket_fit, "2:0,1", "2:0,1"), ({"SteamGameId": "2229830", "DROIDDECK_GAME_CORES": "12-13"}, every, pocket_fit, "1:7", "1:7"), ({"SteamGameId": "11440", "BL_GAME_CPUS": "0-7"}, every, pocket_fit, "4:7,2,3,4", "4:7,2,3,4"), ({}, every, {core: 100 for core in every}, None, None), ({}, every, None, None, None), ({"SteamGameId": "11440"}, every, None, "4:0,1,2,3", "4:0,1,2,3"), ({"BL_GAME_CPUS": "9"}, {9}, pocket_fit, None, None), ] cases += [ ({"STEAM_COMPAT_DATA_PATH": "/steam/compatdata/0"}, every, pocket_fit, None, None), ({"STEAM_COMPAT_DATA_PATH": ""}, every, pocket_fit, None, None), ({"STEAM_COMPAT_DATA_PATH": "/steam/compatdata/0", "BL_GAME_CPUS": "6,7"}, {6, 7}, pocket_fit, "2:6,7", "2:6,7"), ] for env, affinity, speeds, expected, proton_topology in cases: env.setdefault("STEAM_COMPAT_DATA_PATH", "/steam/compatdata/3608888694") with mock.patch.dict(MODULE["cpu_topology"].__globals__, {"online_cores": lambda: every}), \ mock.patch.dict(MODULE["fastest_first"].__globals__, {"core_speeds": lambda cores, speeds=speeds: speeds}), \ mock.patch.object(os_mod, "sched_getaffinity", return_value=affinity, create=True): MODULE["cpu_topology"](env, command) self.assertEqual(env.get("WINE_CPU_TOPOLOGY"), expected, env) self.assertEqual(env.get("PROTON_CPU_TOPOLOGY"), proton_topology, env) self.assertIsNone(MODULE["proton_cpu_limit"](["/missing/proton"], {"SteamGameId": "11440"})) self.assertIsNone(MODULE["proton_cpu_limit"](command, {})) self.assertEqual(MODULE["proton_cpu_limit"](command, {"SteamGameId": "1038250"}), "P") self.assertEqual(MODULE["proton_cpu_limit"](command, {"SteamGameId": "618970"}), 4) def test_core_speeds_read_capacity_then_top_clock(self): with tempfile.TemporaryDirectory() as tmp: for core, capacity, clock in ((0, "379", "2265600"), (1, None, "3302400")): folder = Path(tmp) / ("cpu%d" % core) / "cpufreq" folder.mkdir(parents=True) (folder / "cpuinfo_max_freq").write_text(clock + "\n") if capacity: (folder.parent / "cpu_capacity").write_text(capacity + "\n") speeds = MODULE["core_speeds"] self.assertEqual(speeds([0], tmp), {0: 379}) self.assertEqual(speeds([0, 1], tmp), {0: 2265600, 1: 3302400}) self.assertIsNone(speeds([0, 2], tmp)) def test_titles_with_former_feature_level_workarounds_keep_detected_capabilities(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) names = ( "StellarBlade/SB.exe", "StellarBlade/.droiddeck-launch.bat", "StellarBlade/SB/Binaries/Win64/SB-Win64-Shipping.exe", "Caravan/CaravanSandWitch.exe", "Caravan/.droiddeck-launch.bat", "Caravan/CaravanSandWitch/Binaries/Win64/CaravanSandWitch-Win64-Shipping.exe", "RoboCop/RoboCop.exe", "RoboCop/RoboCop/Binaries/Win64/RoboCop-Win64-Shipping.exe", "ReSkate/Skate.exe", "ReSkate/ReSkate.dll", "ReSkate/ReSkateLauncher.exe", "Cyberpunk/REDprelauncher.exe", "Cyberpunk/bin/x64/Cyberpunk2077.exe", ) for name in names: p = root / name p.parent.mkdir(parents=True, exist_ok=True) p.touch() for name in names: if name.endswith(".dll"): continue env, extra, notes = MODULE["engine_fixes"](str(root / name), []) self.assertNotIn("VKD3D_FEATURE_LEVEL", env, name) self.assertNotIn("VKD3D_SHADER_MODEL", env, name) for values in ({}, {"VKD3D_FEATURE_LEVEL": "12_2", "VKD3D_SHADER_MODEL": "6_9"}): config = {"version": 1, "shared": values} self.assertEqual(MODULE["apply_config"]({}, config, "42", env), values, name) config = {"version": 1, "shared": {}, "games": {"42": values}} self.assertEqual(MODULE["apply_config"]({}, config, "42", env), values, name) def test_unity_players_keep_tso_over_the_preset(self): with tempfile.TemporaryDirectory() as tmp: def game(*names): for name in names: (Path(tmp) / name).parent.mkdir(parents=True, exist_ok=True) (Path(tmp) / name).touch() return str(Path(tmp) / names[0]) il2cpp = game("Megabonk/Megabonk.x86_64", "Megabonk/UnityPlayer.so", "Megabonk/GameAssembly.so") mono = game("Mono/Game.x86_64", "Mono/UnityPlayer.so") windows = game("Crab Game/Crab Game.exe", "Crab Game/UnityPlayer.dll", "Crab Game/GameAssembly.dll") windows_mono = game("RoR2/Risk of Rain 2.exe", "RoR2/UnityPlayer.dll", "RoR2/MonoBleedingEdge/mono-2.0-bdwgc.dll") for exe in (il2cpp, mono, windows, windows_mono): env, extra, notes = MODULE["engine_fixes"](exe, []) self.assertEqual((env, extra, notes), ({"FEX_TSOENABLED": "1"}, [], ["Unity: FEX with TSO"])) performance = {"version": 1, "shared": {"FEX_TSOENABLED": "0", "FEX_VECTORTSOENABLED": "0"}} result = MODULE["apply_config"]({}, performance, "3405340", env) self.assertEqual((result["FEX_TSOENABLED"], result["FEX_VECTORTSOENABLED"]), ("1", "0")) performance["games"] = {"3405340": {"FEX_TSOENABLED": "0"}} self.assertEqual(MODULE["apply_config"]({}, performance, "3405340", env)["FEX_TSOENABLED"], "0") def test_a_new_fix_only_needs_registering(self): with tempfile.TemporaryDirectory() as tmp: (Path(tmp) / "Game.exe").touch() (Path(tmp) / "engine.dll").touch() check = lambda game: ({"ENGINE": "1"}, ["--flag"], "engine") if "engine.dll" in game.files else None MODULE["FIXES"].append(check) try: self.assertEqual(MODULE["engine_fixes"](str(Path(tmp) / "Game.exe"), []), ({"ENGINE": "1"}, ["--flag"], ["engine"])) finally: MODULE["FIXES"].remove(check) def test_engine_fixes_sit_between_shared_and_game_profiles(self): env = {"WINEDLLOVERRIDES": "dxgi=n"} fixes = {"FEX_MULTIBLOCK": "0", "WINEDLLOVERRIDES": "libsentry=d"} config = {"version": 1, "shared": {"FEX_MULTIBLOCK": "1"}, "games": {"42": {"FEX_TSOENABLED": "0"}}} result = MODULE["apply_config"](env, config, "42", {**fixes, "FEX_TSOENABLED": "1"}) self.assertEqual(result["FEX_MULTIBLOCK"], "0") self.assertEqual(result["FEX_TSOENABLED"], "0") self.assertEqual(result["WINEDLLOVERRIDES"], "dxgi=n;libsentry=d") def test_invalid_configuration_is_atomic(self): env = {"ORIGINAL": "unchanged"} for entries in ({"A": "ok", "BAD=KEY": "x"}, {"A": "bad\0value"}, {"A": 1}): with self.assertRaises(ValueError): MODULE["apply_config"](env, {"version": 1, "shared": entries}, "42") self.assertEqual(env, {"ORIGINAL": "unchanged"}) def test_texture_filtering_is_appended_to_dxvk_config(self): options = "d3d9.samplerAnisotropy = 16; d3d11.samplerAnisotropy = 16" config = {"version": 1, "shared": {}, "games": {}, "dxvkConfig": options} self.assertEqual(MODULE["apply_config"]({}, config, "42")["DXVK_CONFIG"], options) own = {"DXVK_CONFIG": "dxvk.maxFrameRate = 60; "} self.assertEqual(MODULE["apply_config"](own, config, "42")["DXVK_CONFIG"], "dxvk.maxFrameRate = 60; " + options) shared = {"version": 1, "shared": {"DXVK_CONFIG": "dxvk.tearFree = True"}, "games": {}, "dxvkConfig": options} self.assertEqual(MODULE["apply_config"]({}, shared, "42")["DXVK_CONFIG"], "dxvk.tearFree = True; " + options) self.assertNotIn("DXVK_CONFIG", MODULE["apply_config"]({}, {**config, "dxvkConfig": ""}, "42")) for bad in (1, "a\0b", "x" * 8193): with self.assertRaises(ValueError): MODULE["apply_config"]({}, {**config, "dxvkConfig": bad}, "42") def test_game_texture_filtering_overrides_default_without_leaking(self): default = "d3d9.samplerAnisotropy = 16" game = "d3d9.samplerAnisotropy = 4" config = { "version": 1, "shared": {}, "games": {}, "dxvkConfig": default, "dxvkConfigGames": {"42": game, "43": ""}, } self.assertEqual(MODULE["apply_config"]({}, config, "42")["DXVK_CONFIG"], game) self.assertNotIn("DXVK_CONFIG", MODULE["apply_config"]({}, config, "43")) self.assertEqual(MODULE["apply_config"]({}, config, "44")["DXVK_CONFIG"], default) with self.assertRaises(ValueError): MODULE["apply_config"]({}, {**config, "dxvkConfigGames": []}, "42") def test_game_logs_only_when_asked_and_only_for_game_launches(self): game_logs = MODULE["game_logs"] with tempfile.TemporaryDirectory() as tmp: command = ["/proton", "waitforexitandrun", "/games/Hades/Hades.exe"] env = {"BL_DEBUG_DIR": tmp, "VK_DRIVER_FILES": "/icd.json"} self.assertEqual(game_logs(command, env, "1145360", now=0), env) on = dict(env, BL_GAME_LOGS="1") self.assertEqual(game_logs(["/proton", "run", "x.exe"], on, "1145360", now=0), on) result = game_logs(command, on, "1145360", now=0) folder = Path(result["PROTON_LOG_DIR"]) self.assertEqual(folder.parent, Path(tmp) / "games") self.assertTrue(folder.name.endswith("-1145360")) self.assertEqual(result["DXVK_LOG_PATH"], str(folder)) self.assertEqual(result["PROTON_LOG"], "1") launch = (folder / "launch.txt").read_text() self.assertIn("VK_DRIVER_FILES=/icd.json", launch) self.assertNotIn("PROTON_LOG", env) named = game_logs(command, on, None, now=0) self.assertTrue(Path(named["PROTON_LOG_DIR"]).name.endswith("-Hades")) def test_rapid_game_launches_keep_separate_diagnostics(self): with tempfile.TemporaryDirectory() as tmp, mock.patch("subprocess.Popen"): env = {"BL_GAME_LOGS": "1", "BL_DEBUG_DIR": tmp} first = MODULE["game_logs"](["/proton", "waitforexitandrun", "first.exe"], env, "42", now=0) second = MODULE["game_logs"](["/proton", "waitforexitandrun", "second.exe"], env, "42", now=0) self.assertNotEqual(first["PROTON_LOG_DIR"], second["PROTON_LOG_DIR"]) self.assertIn("exe: first.exe", (Path(first["PROTON_LOG_DIR"]) / "launch.txt").read_text()) self.assertIn("exe: second.exe", (Path(second["PROTON_LOG_DIR"]) / "launch.txt").read_text()) def test_open_driver_pairs_report_the_drivers_own_vkd3d_caps(self): apply = MODULE["apply_config"] config = {"version": 1, "shared": {"VKD3D_FEATURE_LEVEL": "12_2", "VKD3D_SHADER_MODEL": "6_9"}, "games": {"42": {"VKD3D_SHADER_MODEL": "6_6"}}} adreno = apply({}, config, "1") self.assertEqual(adreno["VKD3D_FEATURE_LEVEL"], "12_2") pair = apply({"BL_OPEN_DRIVER": "1"}, config, "1") self.assertNotIn("VKD3D_FEATURE_LEVEL", pair) self.assertNotIn("VKD3D_SHADER_MODEL", pair) self.assertEqual(pair["DROIDDECK_VKD3D_CAPS"], "driver") own = apply({"BL_OPEN_DRIVER": "1"}, config, "42") self.assertEqual(own["VKD3D_SHADER_MODEL"], "6_6") self.assertNotIn("VKD3D_FEATURE_LEVEL", own) def test_game_ids_and_probes(self): for prefix in ("", "/compatdata/0", "/compatdata/0-123", "/compatdata/nope", "/compatdata/4294967296"): self.assertIsNone(MODULE["game_id"]({"STEAM_COMPAT_DATA_PATH": prefix})) self.assertEqual(MODULE["game_id"]({"STEAM_COMPAT_DATA_PATH": "/a/compatdata/42/"}), "42") self.assertEqual(MODULE["game_id"]({"STEAM_COMPAT_DATA_PATH": "/a/compatdata/-1"}), "4294967295") def test_launch_reads_updates_preserves_argv_and_does_not_execute_values(self): with tempfile.TemporaryDirectory() as tmp: home = Path(tmp) config = home / ".config/droiddeck/game-environment.json" config.parent.mkdir(parents=True) probe = home / "fake-proton" probe.write_text("#!/usr/bin/python3\nimport json, os, sys\nprint(json.dumps([os.environ.get('CUSTOM'), sys.argv[1:]]))\n") probe.chmod(0o755) env = {**os.environ, "HOME": tmp, "STEAM_COMPAT_DATA_PATH": "/compatdata/42"} for value in ("first value", "$(touch " + str(home / "injected") + "); 'literal'=value"): config.write_text(json.dumps({"version": 1, "shared": {"CUSTOM": value}})) result = subprocess.check_output([sys.executable, str(BIN / "droiddeck-game-env"), str(probe), "waitforexitandrun", "path with spaces", "a=b"], env=env, text=True) self.assertEqual(json.loads(result), [value, ["waitforexitandrun", "path with spaces", "a=b"]]) self.assertFalse((home / "injected").exists()) for verb, prefix in (("run", "/compatdata/42"), ("waitforexitandrun", "/compatdata/0")): output = subprocess.check_output([sys.executable, str(BIN / "droiddeck-game-env"), str(probe), verb], env={**env, "STEAM_COMPAT_DATA_PATH": prefix, "CUSTOM": "original"}, text=True) self.assertEqual(json.loads(output)[0], "original") config.write_text("{broken") result = subprocess.run([sys.executable, str(BIN / "droiddeck-game-env"), str(probe), "waitforexitandrun"], env={**env, "CUSTOM": "original"}, text=True, capture_output=True, check=True) self.assertEqual(json.loads(result.stdout)[0], "original") def test_generated_valve_and_third_party_launchers_apply_configuration(self): with tempfile.TemporaryDirectory() as tmp: home = Path(tmp) steam = home / "Steam" tools = steam / "compatibilitytools.d" depot = steam / "steamapps/common" / COMPAT["SOURCES"][0] (depot / "files/bin-arm64").mkdir(parents=True) extra = tools / "custom-proton" extra.mkdir(parents=True) for base in (depot, extra): proton = base / "proton" proton.write_text("#!/usr/bin/python3\nimport json, os, sys\nprint(json.dumps([os.environ['CUSTOM'], sys.argv[1:]]))\nsys.exit(7)\n") proton.chmod(0o755) (extra / "toolmanifest.vdf").write_text('"manifest" { "commandline" "/proton %verb%" }') COMPAT["build_tool"](str(tools / COMPAT["TOOL"]), str(depot)) COMPAT["adopt_extras"](str(tools)) config = home / ".config/droiddeck/game-environment.json" config.parent.mkdir(parents=True) config.write_text(json.dumps({"version": 1, "shared": {"CUSTOM": "shared"}, "games": {"42": {"CUSTOM": "specific"}}})) wrappers = (tools / COMPAT["TOOL"] / COMPAT["LAUNCHER"], extra / COMPAT["EXTRA_WRAPPER"]) for wrapper in wrappers: wrapper.write_text(wrapper.read_text().replace("/usr/local/bin/droiddeck-game-env", str(BIN / "droiddeck-game-env"))) result = subprocess.run([str(wrapper), "waitforexitandrun", "game with spaces.exe"], env={"PATH": os.defpath, "HOME": tmp, "STEAM_COMPAT_DATA_PATH": "/compatdata/42", "STEAM_COMPAT_CLIENT_INSTALL_PATH": str(steam)}, text=True, capture_output=True) self.assertEqual(result.returncode, 7, result.stderr) self.assertEqual(json.loads(result.stdout), ["specific", ["waitforexitandrun", "game with spaces.exe"]]) def test_default_is_compatible_tool_and_labels_mark_it(self): with tempfile.TemporaryDirectory() as tmp: home = Path(tmp) label = home / "label" label.write_text("Compatible\n") COMPAT["display_name"].__globals__["LABEL_FILE"] = str(label) tools = home / "compatibilitytools.d" extra = tools / "proton-cachyos-11" extra.mkdir(parents=True) (extra / "proton").write_text("") (extra / "toolmanifest.vdf").write_text('"manifest" { "commandline" "/proton %verb%" }') vdf = tools / COMPAT["TOOL"] / "compatibilitytool.vdf" COMPAT["build_tool"](str(tools / COMPAT["TOOL"]), None) self.assertIn('"display_name" "Proton ARM64 (Compatible)"', vdf.read_text()) for source in COMPAT["SOURCES"]: COMPAT["build_tool"](str(tools / COMPAT["TOOL"]), str(home / "steamapps/common" / source)) self.assertIn('"display_name" "%s ARM64 (Compatible)"' % source.replace(" (ARM64)", ""), vdf.read_text()) protect = COMPAT["adopt_extras"](str(tools)) self.assertIn('"display_name" "proton-cachyos-11 (Compatible)"', (extra / "compatibilitytool.vdf").read_text()) config = home / "config.vdf" mapping = "".join('"%s" { "name" "%s" "config" "" "priority" "%s" }' % entry for entry in ( ("0", "GE-Proton10-25", "75"), ("42", "proton_experimental_arm64", "250"), ("43", "proton-cachyos-11", "250"))) config.write_text('"InstallConfigStore" { "Software" { "Valve" { "Steam" { "CompatToolMapping" { %s } } } } }' % mapping) COMPAT["register_default"](str(config), ["44"], protect=protect) tokens = COMPAT["tokenize"](config.read_text()) names = {tokens[i - 1].strip('"'): tokens[i + 2].strip('"') for i in range(1, len(tokens) - 2) if tokens[i] == "{" and tokens[i + 1] == '"name"'} self.assertEqual(names, {"0": COMPAT["TOOL"], "42": COMPAT["TOOL"], "43": "proton-cachyos-11", "44": COMPAT["TOOL"]}) def test_existing_install_moves_to_new_tool_name(self): with tempfile.TemporaryDirectory() as tmp: home = Path(tmp) (home / "label").write_text("Compatible\n") COMPAT["main"].__globals__["LABEL_FILE"] = str(home / "label") steam = home / "Steam" depot = steam / "steamapps/common" / COMPAT["SOURCES"][0] (depot / "files/bin-arm64").mkdir(parents=True) (steam / "steamapps/appmanifest_42.acf").write_text("") legacy = steam / "compatibilitytools.d" / COMPAT["LEGACY_TOOL"] legacy.mkdir(parents=True) for name in COMPAT["OWN_FILES"]: (legacy / name).write_text("old") (steam / "config").mkdir() config = steam / "config/config.vdf" mapping = "".join('"%s" { "name" "%s" "config" "" "priority" "%s" }' % (app, COMPAT["LEGACY_TOOL"], priority) for app, priority in (("0", "75"), ("42", "250"))) config.write_text('"InstallConfigStore" { "Software" { "Valve" { "Steam" { "CompatToolMapping" { %s } } } } }' % mapping) argv = sys.argv try: sys.argv = ["steam-compatibility", str(steam)] COMPAT["main"]() migrated = config.read_text() COMPAT["main"]() finally: sys.argv = argv self.assertFalse(legacy.exists()) self.assertIn('"display_name" "Proton Experimental ARM64 (Compatible)"', (steam / "compatibilitytools.d" / COMPAT["TOOL"] / "compatibilitytool.vdf").read_text()) self.assertNotIn(COMPAT["LEGACY_TOOL"], migrated) self.assertEqual(migrated.count('"%s"' % COMPAT["TOOL"]), 2) self.assertEqual(config.read_text(), migrated) def test_launchers_and_adopted_protons_from_before_the_rename_move_over(self): with tempfile.TemporaryDirectory() as tmp: home = Path(tmp) (home / "label").write_text("Compatible\n") COMPAT["main"].__globals__["LABEL_FILE"] = str(home / "label") tools = home / "compatibilitytools.d" own = tools / COMPAT["TOOL"] own.mkdir(parents=True) for name in ("toolmanifest.vdf", "compatibilitytool.vdf", COMPAT["LEGACY_LAUNCHER"]): (own / name).write_text("old") spare = tools / COMPAT["TOOL_11"] spare.mkdir() for name in ("toolmanifest.vdf", "compatibilitytool.vdf", COMPAT["LEGACY_LAUNCHER"]): (spare / name).write_text("old") extra = tools / "GE-Proton11-7" extra.mkdir() (extra / "proton").write_text("") (extra / "toolmanifest.vdf").write_text('"manifest" { "commandline" "/%s %%verb%%" }' % COMPAT["LEGACY_EXTRA_WRAPPER"]) (extra / ("toolmanifest.vdf" + COMPAT["LEGACY_ORIGINAL_SUFFIX"])).write_text('"manifest" { "commandline" "/proton %verb%" }') (extra / COMPAT["LEGACY_EXTRA_WRAPPER"]).write_text("old") COMPAT["build_tool"](str(own), None) COMPAT["remove_tool"](str(spare)) self.assertEqual(COMPAT["adopt_extras"](str(tools)), {"GE-Proton11-7": "GE-Proton11-7"}) self.assertEqual(sorted(os.listdir(own)), sorted(COMPAT["OWN_FILES"])) self.assertFalse(spare.exists()) self.assertEqual(sorted(os.listdir(extra)), sorted(["proton", "toolmanifest.vdf", "compatibilitytool.vdf", COMPAT["EXTRA_WRAPPER"], "toolmanifest.vdf" + COMPAT["ORIGINAL_SUFFIX"]])) self.assertIn("/%s %%verb%%" % COMPAT["EXTRA_WRAPPER"], (extra / "toolmanifest.vdf").read_text()) self.assertIn('"/proton %verb%"', (extra / ("toolmanifest.vdf" + COMPAT["ORIGINAL_SUFFIX"])).read_text()) self.assertIn('"$here/proton"', (extra / COMPAT["EXTRA_WRAPPER"]).read_text()) self.assertIn("GE-Proton11-7", [entry["name"] for entry in COMPAT["tool_catalog"](str(home), None)]) def test_droiddeck_proton_is_adopted_and_labelled_like_any_other_extra(self): with tempfile.TemporaryDirectory() as tmp: home = Path(tmp) (home / "label").write_text("Compatible\n") COMPAT["display_name"].__globals__["LABEL_FILE"] = str(home / "label") tools = home / "compatibilitytools.d" extra = tools / "DroidDeck-Proton 11-7.5" extra.mkdir(parents=True) (extra / "proton").write_text("") (extra / "toolmanifest.vdf").write_text('"manifest" { "version" "2" "commandline" "/proton %verb%" "require_tool_appid" "4185400" }') (extra / "compatibilitytool.vdf").write_text( '"compatibilitytools" { "compat_tools" { "DroidDeck-Proton-11-7.5" { "install_path" "." ' '"display_name" "DroidDeck-Proton 11-7.5" "from_oslist" "windows" "to_oslist" "linux" } } }') COMPAT["build_tool"](str(tools / COMPAT["TOOL"]), None) protect = COMPAT["adopt_extras"](str(tools)) self.assertEqual(protect, {"DroidDeck-Proton-11-7.5": "DroidDeck-Proton 11-7.5"}) compat = (extra / "compatibilitytool.vdf").read_text() self.assertIn('"DroidDeck-Proton-11-7.5"', compat) self.assertIn('"display_name" "DroidDeck-Proton 11-7.5 (Compatible)"', compat) self.assertNotIn("require_tool_appid", (extra / "toolmanifest.vdf").read_text()) self.assertIn('"$here/proton"', (extra / COMPAT["EXTRA_WRAPPER"]).read_text()) catalog = COMPAT["tool_catalog"](str(home), None) self.assertIn({"name": "DroidDeck-Proton-11-7.5", "display": "DroidDeck-Proton 11-7.5 (Compatible)", "dir": "DroidDeck-Proton 11-7.5", "valve": False}, catalog) config = home / "config.vdf" config.write_text(config_text([("0", COMPAT["TOOL"]), ("42", "DroidDeck-Proton-11-7.5")])) COMPAT["register_default"](str(config), ["42", "43"], protect=protect) self.assertEqual(mapping_of(config.read_text()), {"0": COMPAT["TOOL"], "42": "DroidDeck-Proton-11-7.5", "43": COMPAT["TOOL"]}) changes, _ = COMPAT["plan_mapping"]({"0": COMPAT["TOOL"], "42": "DroidDeck-Proton-11-7.5"}, ["42"], [COMPAT["TOOL"]], COMPAT["TOOL"], {}) self.assertEqual(changes, {"42": COMPAT["TOOL"]}) def test_both_proton_wrappers_call_environment_launcher(self): self.assertIn('bl_directaudio "$depot" "$@"', COMPAT["LAUNCHER_SH"]) self.assertIn('bl_directaudio "$here" "$@"', COMPAT["EXTRA_WRAPPER_SH"] % "proton") for script in (COMPAT["LAUNCHER_SH"], COMPAT["EXTRA_WRAPPER_SH"] % "proton"): subprocess.run(["bash", "-n"], input=script, text=True, check=True) self.assertIn('bl_run ${BL_TASKSET:-} /usr/local/bin/droiddeck-game-env', script) def test_both_proton_wrappers_answer_steams_driver_query_without_wine(self): scripts = (COMPAT["launcher_sh"](), COMPAT["extra_wrapper_sh"]("proton")) query = "/root/.local/share/Steam/steamrtarm64/../bin/d3ddriverquery64.exe" probe = "/root/.local/share/Steam/steamapps/compatdata/0" for script in scripts: subprocess.run(["bash", "-n"], input=script, text=True, check=True) self.assertLess(script.index('case "$verb:${1##*/}:'), script.index("bl_components ")) for args, prefix in (([query, "-d3d12"], probe), ([query], probe + "/"), ([query.replace("64.exe", ".exe")], probe)): result = subprocess.run(["bash", "-s", "run"] + args, input=script, text=True, capture_output=True, env={"PATH": os.defpath, "HOME": "/nonexistent", "STEAM_COMPAT_DATA_PATH": prefix}, timeout=10) self.assertEqual(result.returncode, 1, result.stderr) self.assertEqual(result.stdout, "") self.assertIn("D3D driver query is not run", result.stderr) skip = COMPAT["BL_DRIVER_QUERY_SKIP"] for verb, args, prefix in ( ("getcompatpath", [query], probe), ("run", [query], "/root/.local/share/Steam/steamapps/compatdata/2276548447"), ("run", [query], None), ("waitforexitandrun", ["/games/AIO/AIO-Graphics-Test-64bit.exe", "--cube", "dx11"], probe), ("run", ["/games/Tool/tool.exe", query], probe), ("run", ["/games/d3ddriverquery64.exe.bak"], probe)): env = {"PATH": os.defpath} if prefix is not None: env["STEAM_COMPAT_DATA_PATH"] = prefix out = subprocess.run(["bash", "-c", "verb=" + verb + "\n" + skip + "echo continued", "_"] + args, env=env, capture_output=True, text=True, check=True).stdout self.assertEqual(out, "continued\n", (verb, args, prefix)) def test_wrappers_preload_the_session_library_before_the_input_shim(self): overlay = "/root/.local/share/Steam/ubuntu12_64/gameoverlayrenderer.so" with tempfile.TemporaryDirectory() as tmp: for name in ("libblsession.so", "libfakeinput.so"): (Path(tmp) / name).write_bytes(b"") for script in (COMPAT["LAUNCHER_SH"], COMPAT["EXTRA_WRAPPER_SH"] % "proton"): start = script.index("for fake in ") block = script[start:script.index("\ndone\n", start) + 6].replace("/usr/local/lib/", tmp + "/") for inherited, expected in ( (None, ["libblsession.so", "libfakeinput.so"]), (overlay, ["libblsession.so", "libfakeinput.so", overlay]), (tmp + "/libfakeinput.so:" + overlay, ["libblsession.so", "libfakeinput.so", overlay]), (tmp + "/libblsession.so:" + tmp + "/libfakeinput.so", ["libblsession.so", "libfakeinput.so"])): env = {"PATH": os.defpath} if inherited is not None: env["LD_PRELOAD"] = inherited out = subprocess.run(["bash", "-c", block + 'printf %s "$LD_PRELOAD"'], env=env, capture_output=True, text=True, check=True).stdout self.assertEqual([entry.replace(tmp + "/", "") for entry in out.split(":")], expected, inherited) class ProtonDefaultTest(unittest.TestCase): TOOL = COMPAT["TOOL"] CATALOG = [ {"name": COMPAT["TOOL"], "display": "Proton Experimental ARM64", "dir": "Proton Experimental (ARM64)", "valve": True}, {"name": COMPAT["TOOL_11"], "display": "Proton 11.0 ARM64", "dir": "Proton 11.0 (ARM64)", "valve": True}, {"name": "GE-Proton11-7", "display": "GE-Proton11-7", "dir": "GE-Proton11-7", "valve": False}, ] NAMES = [t["name"] for t in CATALOG] def plan(self, current, installed, default, auto): return COMPAT["plan_mapping"](current, installed, self.NAMES, default, auto) def test_new_installs_and_valve_picks_follow_the_default(self): changes, auto = self.plan({"0": self.TOOL, "10": "proton_experimental", "11": "proton_11_arm64", "12": "GE-Proton11-7", "13": "steamlinuxruntime"}, ["10", "11", "12", "13", "14"], "GE-Proton11-7", {}) self.assertEqual(changes, {"0": "GE-Proton11-7", "10": "GE-Proton11-7", "11": "GE-Proton11-7", "14": "GE-Proton11-7"}) self.assertEqual(auto, {"10": "GE-Proton11-7", "11": "GE-Proton11-7", "14": "GE-Proton11-7"}) def test_windows_only_titles_still_follow_the_default(self): changes, auto = COMPAT["plan_mapping"]( {"0": self.TOOL}, ["42"], self.NAMES, self.TOOL, {}) self.assertEqual(changes, {"42": self.TOOL}) self.assertEqual(auto, {"42": self.TOOL}) def test_native_arm64_default_clears_only_our_auto_mapping(self): with tempfile.TemporaryDirectory() as tmp: config = Path(tmp) / "config.vdf" config.write_text(config_text([("0", self.TOOL), ("42", self.TOOL)])) (Path(tmp) / "state.json").write_text(json.dumps({ "default": self.TOOL, "auto": {"42": self.TOOL}, })) globals_ = COMPAT["register_default"].__globals__ original_native_targets = globals_["native_targets"] globals_["native_targets"] = lambda _root, _apps: {"42": None} try: COMPAT["register_default"]( str(config), ["42"], catalog=self.CATALOG, directory=tmp) first = config.read_bytes() self.assertEqual(mapping_of(first.decode()), {"0": self.TOOL}) self.assertEqual(COMPAT["tokenize"](first.decode()), COMPAT["tokenize"](config_text([("0", self.TOOL)]))) self.assertEqual(json.loads((Path(tmp) / "state.json").read_text())["auto"], {}) COMPAT["register_default"]( str(config), ["42"], catalog=self.CATALOG, directory=tmp) self.assertEqual(config.read_bytes(), first) finally: globals_["native_targets"] = original_native_targets def test_saved_native_choice_survives_valve_pick_without_appinfo(self): app = "452280" with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) steam = root / "Steam" config = steam / "config/config.vdf" config.parent.mkdir(parents=True) (steam / "steamapps").mkdir() (steam / "steamapps" / ("appmanifest_%s.acf" % app)).write_text("") config.write_text(config_text([("0", self.TOOL)])) (root / "state.json").write_text(json.dumps({ "default": self.TOOL, "auto": {}, "native_choices": [app], })) globals_ = COMPAT["register_default"].__globals__ original_native_targets = globals_["native_targets"] globals_["native_targets"] = lambda _root, _apps: {} try: COMPAT["register_default"]( str(config), [], catalog=self.CATALOG, directory=tmp, picks=[app]) self.assertEqual(mapping_of(config.read_text()), {"0": self.TOOL}) state = json.loads((root / "state.json").read_text()) self.assertEqual(state["auto"], {}) self.assertEqual(state["native_choices"], [app]) finally: globals_["native_targets"] = original_native_targets def test_shortcuts_scanner_keeps_native_choice_for_unrelated_app_list(self): app, added_shortcut = "452280", "3537741571" with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) steam = root / "Steam" config = steam / "config/config.vdf" config.parent.mkdir(parents=True) (steam / "steamapps").mkdir() (steam / "steamapps" / ("appmanifest_%s.acf" % app)).write_text("") config.write_text(config_text([("0", self.TOOL)])) (root / "state.json").write_text(json.dumps({ "default": self.TOOL, "auto": {}, "native_choices": [app], })) globals_ = COMPAT["register_default"].__globals__ original_native_targets = globals_["native_targets"] globals_["native_targets"] = lambda _root, wanted: { app: COMPAT["NATIVE_CHOICE"] } if app in wanted else {} try: COMPAT["register_default"]( str(config), [app], catalog=self.CATALOG, directory=tmp) self.assertEqual(json.loads((root / "state.json").read_text())["native_choices"], [app]) COMPAT["register_default"]( str(config), [added_shortcut], catalog=self.CATALOG, directory=tmp) state = json.loads((root / "state.json").read_text()) self.assertEqual(state["native_choices"], [app]) self.assertEqual(state["auto"], {added_shortcut: self.TOOL}) self.assertEqual(mapping_of(config.read_text()), { "0": self.TOOL, added_shortcut: self.TOOL, }) finally: globals_["native_targets"] = original_native_targets def test_a_listed_title_with_a_bundled_arm64_linux_build_defaults_to_fex(self): globals_ = COMPAT["native_targets"].__globals__ original_platforms = globals_["appinfo_platforms"] globals_["appinfo_platforms"] = lambda _path, wanted: { app: ({"linux", "windows"}, "64", "game") for app in ("4385780", "42") if app in wanted } try: native = COMPAT["native_targets"]("/nonexistent", ["4385780", "42"]) finally: globals_["appinfo_platforms"] = original_platforms self.assertEqual(native["4385780"], COMPAT["FEX_TOOL"]) self.assertIs(native["42"], COMPAT["NATIVE_CHOICE"]) changes, auto = COMPAT["plan_mapping"]( {"0": self.TOOL}, ["4385780"], self.NAMES, self.TOOL, {}, native) self.assertEqual(changes, {"4385780": COMPAT["FEX_TOOL"]}) self.assertEqual(auto, {"4385780": COMPAT["FEX_TOOL"]}) for choice in (self.TOOL, "GE-Proton11-7"): changes, auto = COMPAT["plan_mapping"]( {"0": self.TOOL, "4385780": choice}, ["4385780"], self.NAMES, self.TOOL, {}, native) self.assertEqual(changes, {}) self.assertEqual(auto, {}) def test_a_cleared_fex_default_for_a_listed_title_is_kept_as_a_native_choice(self): app = "4385780" fex = COMPAT["FEX_TOOL"] with tempfile.TemporaryDirectory() as tmp: steam = Path(tmp) / "Steam" config = steam / "config/config.vdf" config.parent.mkdir(parents=True) config.write_text(config_text([("0", self.TOOL)])) (Path(tmp) / "state.json").write_text(json.dumps({"default": self.TOOL, "auto": {}})) globals_ = COMPAT["native_targets"].__globals__ original_platforms = globals_["appinfo_platforms"] globals_["appinfo_platforms"] = lambda _path, wanted: { app: ({"linux", "windows"}, "64", "game") } if app in wanted else {} try: def run(): COMPAT["register_default"](str(config), [app], catalog=self.CATALOG, directory=tmp) return json.loads((Path(tmp) / "state.json").read_text()) state = run() self.assertEqual(mapping_of(config.read_text()), {"0": self.TOOL, app: fex}) self.assertEqual(state["auto"], {app: fex}) config.write_text(config_text([("0", self.TOOL), (app, "")])) state = run() cleared = config.read_bytes() self.assertEqual(mapping_of(cleared.decode()), {"0": self.TOOL, app: ""}) self.assertEqual(state["auto"], {}) self.assertEqual(state["native_choices"], [app]) run() self.assertEqual(config.read_bytes(), cleared) config.write_text(config_text([("0", self.TOOL), (app, fex)])) state = run() self.assertEqual(mapping_of(config.read_text()), {"0": self.TOOL, app: fex}) self.assertEqual(state["native_choices"], []) finally: globals_["appinfo_platforms"] = original_platforms def test_factorio_preserves_an_observed_native_choice_despite_ambiguous_metadata(self): with tempfile.TemporaryDirectory() as tmp: steam = Path(tmp) / "Steam" config = steam / "config/config.vdf" config.parent.mkdir(parents=True) config.write_text(config_text([("0", self.TOOL)])) state = {"default": self.TOOL, "auto": {}} (Path(tmp) / "state.json").write_text(json.dumps(state)) globals_ = COMPAT["native_targets"].__globals__ original_platforms = globals_["appinfo_platforms"] globals_["appinfo_platforms"] = lambda _path, wanted: { "427520": ({"linux", "macos", "windows"}, "", "game") } if "427520" in wanted else {} try: native = COMPAT["native_targets"](str(steam), ["427520"]) self.assertIs(native["427520"], COMPAT["NATIVE_CHOICE"]) changes, auto = COMPAT["plan_mapping"]( {"0": self.TOOL}, ["427520"], self.NAMES, self.TOOL, {}, native) self.assertEqual(changes, {"427520": self.TOOL}) self.assertEqual(auto, {"427520": self.TOOL}) COMPAT["register_default"]( str(config), ["427520"], catalog=self.CATALOG, directory=tmp) self.assertEqual(mapping_of(config.read_text()), {"0": self.TOOL, "427520": self.TOOL}) state["auto"] = {"427520": self.TOOL} (Path(tmp) / "state.json").write_text(json.dumps(state)) config.write_text(config_text([("0", self.TOOL)])) COMPAT["register_default"]( str(config), ["427520"], catalog=self.CATALOG, directory=tmp) first = config.read_bytes() self.assertEqual(mapping_of(first.decode()), {"0": self.TOOL}) self.assertEqual(COMPAT["tokenize"](first.decode()), COMPAT["tokenize"](config_text([("0", self.TOOL)]))) self.assertEqual(json.loads((Path(tmp) / "state.json").read_text())["auto"], {}) self.assertEqual(json.loads((Path(tmp) / "state.json").read_text())["native_choices"], ["427520"]) COMPAT["register_default"]( str(config), ["427520"], catalog=self.CATALOG, directory=tmp) self.assertEqual(config.read_bytes(), first) config.write_text(config_text([("0", self.TOOL), ("427520", "")])) COMPAT["register_default"]( str(config), ["427520"], catalog=self.CATALOG, directory=tmp) native_choice = config.read_bytes() self.assertEqual(mapping_of(native_choice.decode()), {"0": self.TOOL, "427520": ""}) self.assertEqual(json.loads((Path(tmp) / "state.json").read_text())["auto"], {}) self.assertEqual(json.loads((Path(tmp) / "state.json").read_text())["native_choices"], ["427520"]) COMPAT["register_default"]( str(config), ["427520"], catalog=self.CATALOG, directory=tmp) self.assertEqual(config.read_bytes(), native_choice) for choice in (COMPAT["FEX_TOOL"], COMPAT["TOOL_11"], "GE-Proton11-7"): config.write_text(config_text([("0", self.TOOL), ("427520", choice)])) COMPAT["register_default"]( str(config), ["427520"], catalog=self.CATALOG, directory=tmp) self.assertEqual(mapping_of(config.read_text()), {"0": self.TOOL, "427520": choice}) self.assertEqual(json.loads((Path(tmp) / "state.json").read_text())["native_choices"], []) state = json.loads((Path(tmp) / "state.json").read_text()) state["native_choices"] = ["427520"] state["auto"] = {} (Path(tmp) / "state.json").write_text(json.dumps(state)) config.write_text(config_text([("0", self.TOOL), ("427520", "proton_experimental")])) COMPAT["register_default"]( str(config), ["427520"], catalog=self.CATALOG, directory=tmp) 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}) finally: globals_["appinfo_platforms"] = original_platforms def test_native_arm64_preserves_a_proton_mapping_without_managed_state(self): changes, auto = COMPAT["plan_mapping"]( {"0": self.TOOL, "42": self.TOOL}, ["42"], self.NAMES, self.TOOL, {"42": self.TOOL}, {"42": None}, managed=()) self.assertEqual(changes, {}) self.assertEqual(auto, {}) def test_native_arm64_keeps_a_chosen_valve_proton_through_its_wrapper(self): changes, auto = COMPAT["plan_mapping"]( {"0": self.TOOL, "42": "proton_experimental"}, ["42"], self.NAMES, self.TOOL, {}, {"42": None}) self.assertEqual(changes, {"42": self.TOOL}) self.assertEqual(auto, {}) changes, auto = COMPAT["plan_mapping"]( {"0": self.TOOL, "42": self.TOOL}, ["42"], self.NAMES, self.TOOL, {}, {"42": None}) self.assertEqual(changes, {}) self.assertEqual(auto, {}) def test_native_arm64_keeps_explicit_runtime_fex_and_proton_profiles(self): fex = COMPAT["FEX_TOOL"] current = { "0": self.TOOL, "42": COMPAT["TOOL_11"], "43": "GE-Proton11-7", "44": "steamlinuxruntime_arm64", "45": fex, "46": "", } changes, auto = COMPAT["plan_mapping"]( current, ["42", "43", "44", "45", "46"], self.NAMES, self.TOOL, {}, {app: None for app in ("42", "43", "44", "45", "46")}, overrides={"46": "GE-Proton11-7"}, ) self.assertEqual(changes, {"46": "GE-Proton11-7"}) self.assertEqual(auto, {}) def test_titles_following_the_default_move_with_it_and_picks_stay(self): current = {"0": self.TOOL, "10": self.TOOL, "11": "GE-Proton11-7", "12": self.TOOL} auto = {"10": self.TOOL, "11": self.TOOL, "12": self.TOOL} changes, auto = self.plan(current, ["10", "11", "12"], COMPAT["TOOL_11"], auto) self.assertEqual(changes, {"0": COMPAT["TOOL_11"], "10": COMPAT["TOOL_11"], "12": COMPAT["TOOL_11"]}) self.assertEqual(auto, {"10": COMPAT["TOOL_11"], "12": COMPAT["TOOL_11"]}) def test_a_removed_tool_falls_back_to_the_default(self): changes, auto = self.plan({"0": self.TOOL, "10": "GE-Proton10-1", "11": "droiddeck-proton-12-arm64"}, ["10", "11"], self.TOOL, {"10": "GE-Proton10-1"}) self.assertEqual(changes, {"10": self.TOOL, "11": self.TOOL}) self.assertEqual(auto, {"10": self.TOOL, "11": self.TOOL}) def test_uninstalled_titles_are_forgotten(self): changes, auto = self.plan({"0": self.TOOL}, [], self.TOOL, {"10": self.TOOL}) self.assertEqual((changes, auto), ({}, {})) def test_per_game_proton_override_and_inherit_follow_steam_mappings(self): current = {"0": self.TOOL, "10": self.TOOL, "11": "GE-Proton11-7", "12": self.TOOL} overrides = {"10": "GE-Proton11-7", "11": None} changes, auto = COMPAT["plan_mapping"]( current, ["10", "11", "12"], self.NAMES, COMPAT["TOOL_11"], {"10": self.TOOL, "12": self.TOOL}, overrides=overrides) self.assertEqual(changes, {"0": COMPAT["TOOL_11"], "10": "GE-Proton11-7", "11": COMPAT["TOOL_11"], "12": COMPAT["TOOL_11"]}) self.assertEqual(auto, {"11": COMPAT["TOOL_11"], "12": COMPAT["TOOL_11"]}) def test_per_game_requests_resolve_identity_and_defer_a_new_live_tool(self): requests = { "10": {"valve": False, "dir": "GE-Proton11-7"}, "11": None, "12": {"valve": True, "dir": "Proton 11.0 (ARM64)"}, "0": None, } self.assertEqual( COMPAT["requested_games"](requests, self.CATALOG), {"10": "GE-Proton11-7", "11": None, "12": COMPAT["TOOL_11"]}, ) self.assertEqual( COMPAT["requested_games"](requests, self.CATALOG, [self.TOOL, COMPAT["TOOL_11"]]), {"10": COMPAT["DEFERRED_GAME_TOOL"], "11": None, "12": COMPAT["TOOL_11"]}, ) def test_missing_per_game_proton_is_preserved_during_default_registration(self): with tempfile.TemporaryDirectory() as tmp: config = Path(tmp) / "config.vdf" config.write_text(config_text([ ("0", self.TOOL), ("10", "GE-Proton9-1"), ("11", self.TOOL), ])) (Path(tmp) / "games.json").write_text(json.dumps({ "version": 1, "games": { "10": {"valve": False, "dir": "GE-Proton9-1"}, }, })) COMPAT["register_default"]( str(config), ["10", "11"], default=COMPAT["TOOL_11"], catalog=self.CATALOG, directory=tmp, ) self.assertEqual(mapping_of(config.read_text()), { "0": COMPAT["TOOL_11"], "10": "GE-Proton9-1", "11": COMPAT["TOOL_11"], }) def test_the_last_side_to_change_wins(self): state = {"default": self.TOOL, "applied": 5} default, state = COMPAT["choose_default"]("GE-Proton11-7", state, {"seq": 5, "dir": "Proton 11.0 (ARM64)", "valve": True}, self.CATALOG, now=100) self.assertEqual((default, state["source"], state["seq"]), ("GE-Proton11-7", "steam", 100)) default, state = COMPAT["choose_default"]("GE-Proton11-7", state, {"seq": 6, "dir": "Proton 11.0 (ARM64)", "valve": True}, self.CATALOG, now=200) self.assertEqual((default, state["source"], state["seq"], state["applied"]), (COMPAT["TOOL_11"], "app", 200, 6)) default, state = COMPAT["choose_default"]("GE-Proton11-7", state, {"seq": 6}, self.CATALOG, adopt_steam=False, now=300) self.assertEqual((default, state["seq"]), (COMPAT["TOOL_11"], 200)) default, state = COMPAT["choose_default"]("proton_experimental", state, {"seq": 6}, self.CATALOG, now=300) self.assertEqual(default, COMPAT["TOOL_11"]) def test_a_request_for_a_missing_tool_is_dropped(self): default, state = COMPAT["choose_default"](self.TOOL, {"default": self.TOOL}, {"seq": 9, "dir": "GE-Proton9-1", "valve": False}, self.CATALOG, now=1) self.assertEqual((default, state["applied"]), (self.TOOL, 9)) default, state = COMPAT["choose_default"]("GE-Proton11-7", state, {"seq": 9, "dir": "GE-Proton9-1", "valve": False}, self.CATALOG, now=2) self.assertEqual(default, "GE-Proton11-7") def test_settle_before_the_client_starts(self): with tempfile.TemporaryDirectory() as tmp: config = Path(tmp) / "config.vdf" config.write_text(config_text([("0", self.TOOL), ("10", "proton_experimental"), ("11", "GE-Proton11-7")])) (Path(tmp) / "request.json").write_text(json.dumps({"seq": 3, "dir": "Proton 11.0 (ARM64)", "valve": True})) default = COMPAT["register_default"](str(config), ["10", "11", "12"], catalog=self.CATALOG, directory=tmp, settle=True) self.assertEqual(default, COMPAT["TOOL_11"]) self.assertEqual(mapping_of(config.read_text()), {"0": COMPAT["TOOL_11"], "10": COMPAT["TOOL_11"], "11": "GE-Proton11-7", "12": COMPAT["TOOL_11"]}) state = json.loads((Path(tmp) / "state.json").read_text()) self.assertEqual((state["applied"], state["source"], state["tools"]), (3, "app", self.CATALOG)) config.write_text(config_text([("0", "GE-Proton11-7"), ("10", COMPAT["TOOL_11"]), ("11", "GE-Proton11-7"), ("12", COMPAT["TOOL_11"])])) default = COMPAT["register_default"](str(config), ["10", "11", "12"], catalog=self.CATALOG, directory=tmp, settle=True) self.assertEqual(default, "GE-Proton11-7") self.assertEqual(mapping_of(config.read_text())["10"], "GE-Proton11-7") self.assertEqual(json.loads((Path(tmp) / "state.json").read_text())["source"], "steam") config.write_text(config_text([("0", "proton_experimental")])) default = COMPAT["register_default"](str(config), ["10"], catalog=self.CATALOG, directory=tmp) self.assertEqual(mapping_of(config.read_text()), {"0": "GE-Proton11-7", "10": "GE-Proton11-7"}) def test_the_shortcut_writer_keeps_the_chosen_default(self): with tempfile.TemporaryDirectory() as tmp: steam = Path(tmp) / "Steam" for name in COMPAT["SOURCES"]: (steam / "steamapps/common" / name / "files/bin-arm64").mkdir(parents=True) (steam / "config").mkdir() config = steam / "config/config.vdf" config.write_text(config_text([("0", COMPAT["TOOL_11"]), ("10", COMPAT["TOOL_11"]), ("3044416433", COMPAT["TOOL_11"])])) (Path(tmp) / "state.json").write_text(json.dumps({"default": COMPAT["TOOL_11"], "auto": {"10": COMPAT["TOOL_11"], "3044416433": COMPAT["TOOL_11"]}})) COMPAT["register_default"](str(config), apps=["3044416433", "3212965118"], protect={}, directory=tmp) self.assertEqual(mapping_of(config.read_text()), {"0": COMPAT["TOOL_11"], "10": COMPAT["TOOL_11"], "3044416433": COMPAT["TOOL_11"], "3212965118": COMPAT["TOOL_11"]}) def test_each_valve_depot_gets_its_own_tool(self): with tempfile.TemporaryDirectory() as tmp: steam = Path(tmp) / "Steam" for name in COMPAT["SOURCES"]: (steam / "steamapps/common" / name / "files/bin-arm64").mkdir(parents=True) self.assertEqual(COMPAT["find_source"](str(steam), COMPAT["OWN_TOOLS"][1][1]), str(steam / "steamapps/common/Proton 11.0 (ARM64)")) self.assertEqual(COMPAT["find_source"](str(steam)), str(steam / "steamapps/common/Proton Experimental (ARM64)")) self.assertEqual([(t["name"], t["dir"]) for t in COMPAT["tool_catalog"](str(steam), {})], [(COMPAT["TOOL"], "Proton Experimental (ARM64)"), (COMPAT["TOOL_11"], "Proton 11.0 (ARM64)")]) for name, sources in COMPAT["OWN_TOOLS"]: COMPAT["build_tool"](str(steam / "compatibilitytools.d" / name), COMPAT["find_source"](str(steam), sources), sources, name) self.assertEqual(COMPAT["adopt_extras"](str(steam / "compatibilitytools.d")), {}) self.assertEqual(sorted(os.listdir(steam / "compatibilitytools.d" / COMPAT["TOOL_11"])), sorted(COMPAT["OWN_FILES"])) launcher = COMPAT["launcher_sh"](COMPAT["OWN_TOOLS"][1][1]) self.assertIn('for name in "Proton 11.0 (ARM64)"; do', launcher) self.assertIn("${major%%.*}", launcher) class DirectAudioPrefixTest(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.addCleanup(self.tmp.cleanup) base = Path(self.tmp.name) self.tool = base / "tool" (self.tool / "files/bin-arm64").mkdir(parents=True) wine = self.tool / "files/bin-arm64/wine" wine.write_text("#!/bin/sh\necho wine-11.0-4c8f2e1 '(Staging)'\n") wine.chmod(0o755) (self.tool / "files/lib/wine/aarch64-unix").mkdir(parents=True) self.winepulse = self.tool / "files/lib/wine/aarch64-unix/winepulse.so" self.winepulse.write_bytes(b"\x7fELF classic wine 11 pulse driver") self.sets = base / "directaudio" for set_name in ("linux-wine11", "linux-wine11-systhread"): for arch in ("aarch64-windows", "i386-windows", "aarch64-unix"): (self.sets / set_name / "lib/wine" / arch).mkdir(parents=True) (self.sets / set_name / "lib/wine/aarch64-windows/winedirectaudio.drv").write_bytes(b"arm64x-" + set_name.encode()) (self.sets / set_name / "lib/wine/i386-windows/winedirectaudio.drv").write_bytes(b"i386") self.audio = self.sets / "linux-wine11" self.compat = base / "compat" self.windows = self.compat / "pfx/drive_c/windows" for folder in ("system32", "syswow64"): (self.windows / folder).mkdir(parents=True) self.reg = self.compat / "pfx/user.reg" def run_setup(self, verb="waitforexitandrun", target="game.exe", cwd=None, extra=None): script = COMPAT["BL_DIRECTAUDIO_SETUP"] + '\nverb=$2\nbl_directaudio "$1" "$3"\necho "WINEDLLPATH=$WINEDLLPATH"\necho "WINE_AUDIO_DRIVER=${WINE_AUDIO_DRIVER:-}"\nenv | grep "^BANNER_AUDIO_DIRECT_" | sort\n' steam = self.compat.parent / "Steam" env = dict(os.environ, BL_DIRECTAUDIO=str(self.sets), STEAM_COMPAT_DATA_PATH=str(self.compat), STEAM_COMPAT_CLIENT_INSTALL_PATH=str(steam), **(extra or {})) env.pop("WINEDLLPATH", None) env.pop("WINE_AUDIO_DRIVER", None) return subprocess.run(["bash", "-c", script, "bash", str(self.tool), verb, target], cwd=cwd, env=env, capture_output=True, text=True, check=True) def audio_values(self): found, current = [], None for line in self.reg.read_text().splitlines(): if line.startswith("["): current = line.rsplit("] ", 1)[0] + "]" elif line.startswith('"Audio"='): found.append((current, line)) return found def link(self, folder): path = self.windows / folder / "winedirectaudio.drv" return os.readlink(path) if path.is_symlink() else None def test_steam_installscript_evaluator_does_not_change_prefix_audio(self): legacycompat = self.compat.parent / "Steam/legacycompat" legacycompat.mkdir(parents=True) (legacycompat / "iscriptevaluator.exe").write_bytes(b"Steam install-script evaluator") self.reg.write_text("WINE REGISTRY Version 2\n\n#arch=win64\n") result = self.run_setup(target="iscriptevaluator.exe", cwd=legacycompat) self.assertIn("DirectAudio skipped for Steam's install-script evaluator", result.stderr) self.assertIn("WINEDLLPATH=\n", result.stdout) self.assertIn("WINE_AUDIO_DRIVER=\n", result.stdout) self.assertEqual(self.reg.read_text(), "WINE REGISTRY Version 2\n\n#arch=win64\n") self.assertIsNone(self.link("system32")) self.assertIsNone(self.link("syswow64")) def test_same_basename_outside_steam_legacycompat_keeps_normal_setup(self): legacycompat = self.compat.parent / "Steam/legacycompat" legacycompat.mkdir(parents=True) (legacycompat / "iscriptevaluator.exe").write_bytes(b"Steam install-script evaluator") other = self.compat.parent / "other" other.mkdir() (other / "iscriptevaluator.exe").write_bytes(b"unrelated executable") self.reg.write_text("WINE REGISTRY Version 2\n\n#arch=win64\n") result = self.run_setup(target="iscriptevaluator.exe", cwd=other) self.assertIn("DirectAudio selected", result.stderr) self.assertEqual(self.link("system32"), str(self.audio / "lib/wine/aarch64-windows/winedirectaudio.drv")) self.assertEqual(self.audio_values(), [("[Software\\\\Wine\\\\Drivers]", '"Audio"="directaudio,pulse"')]) def test_audio_direct_settings_reach_the_driver_under_its_own_names(self): self.reg.write_text("WINE REGISTRY Version 2\n\n#arch=win64\n") result = self.run_setup(extra={"AUDIO_DIRECT_MS": "60", "AUDIO_DIRECT_PERIOD_MS": "20", "AUDIO_DIRECT_RELAY": "/run/relay.sock", "OTHER_AUDIO_DIRECT_MS": "5"}) lines = [line for line in result.stdout.splitlines() if line.startswith("BANNER_")] self.assertEqual(lines, ["BANNER_AUDIO_DIRECT_MS=60", "BANNER_AUDIO_DIRECT_PERIOD_MS=20", "BANNER_AUDIO_DIRECT_RELAY=/run/relay.sock"]) def test_audio_direct_settings_are_not_handed_on_when_directaudio_stays_off(self): legacycompat = self.compat.parent / "Steam/legacycompat" legacycompat.mkdir(parents=True) (legacycompat / "iscriptevaluator.exe").write_bytes(b"Steam install-script evaluator") result = self.run_setup(target="iscriptevaluator.exe", cwd=legacycompat, extra={"AUDIO_DIRECT_MS": "60"}) self.assertNotIn("BANNER_AUDIO_DIRECT_MS", result.stdout) def test_non_launch_verbs_preserve_audio_configuration(self): for verb in ("getcompatpath", "getnativepath", "destroyprefix"): with self.subTest(verb=verb): self.reg.write_text('WINE REGISTRY Version 2\n\n[Software\\\\Wine\\\\Drivers] 1\n"Audio"="pulse"\n') before = self.reg.read_bytes() result = self.run_setup(verb) self.assertEqual(self.reg.read_bytes(), before) self.assertIsNone(self.link("system32")) self.assertIsNone(self.link("syswow64")) self.assertIn("WINEDLLPATH=\n", result.stdout) self.assertIn("WINE_AUDIO_DRIVER=\n", result.stdout) def test_other_windows_launch_verbs_still_set_up_audio(self): self.reg.write_text("WINE REGISTRY Version 2\n") for verb in ("run", "runinprefix"): with self.subTest(verb=verb): result = self.run_setup(verb) self.assertIn("DirectAudio ready", result.stderr) self.assertIn("WINE_AUDIO_DRIVER=directaudio,pulse\n", result.stdout) self.assertEqual(self.link("system32"), str(self.audio / "lib/wine/aarch64-windows/winedirectaudio.drv")) def test_the_driver_is_placed_and_selected_the_way_wine_reads_it(self): self.reg.write_text("WINE REGISTRY Version 2\n;; All keys relative to \\\\User\\\\S-1-5-21-0-0-0-1000\n\n#arch=win64\n") result = self.run_setup() self.assertIn("DirectAudio selected", result.stderr) self.assertIn("WINEDLLPATH=%s/lib/wine" % self.audio, result.stdout) self.assertIn("WINE_AUDIO_DRIVER=directaudio,pulse\n", result.stdout) self.assertEqual(self.link("system32"), str(self.audio / "lib/wine/aarch64-windows/winedirectaudio.drv")) self.assertEqual(self.link("syswow64"), str(self.audio / "lib/wine/i386-windows/winedirectaudio.drv")) self.assertEqual((self.windows / "system32/winedirectaudio.drv").read_bytes(), b"arm64x-linux-wine11") self.assertEqual(self.audio_values(), [("[Software\\\\Wine\\\\Drivers]", '"Audio"="directaudio,pulse"')]) before = self.reg.read_text() self.assertNotIn("DirectAudio selected", self.run_setup().stderr) self.assertEqual(self.reg.read_text(), before) self.assertEqual(sorted(os.listdir(self.windows / "system32")), ["winedirectaudio.drv"]) def test_without_a_prefix_it_waits_for_the_next_launch(self): result = self.run_setup() self.assertIn("takes effect the next time this game starts", result.stderr) self.assertEqual(self.link("system32"), None) def test_the_system_thread_set_is_picked_from_winepulse_not_the_version(self): self.reg.write_text("WINE REGISTRY Version 2\n\n#arch=win64\n") self.winepulse.write_bytes(b"\x7fELF cachyos pulse driver PsCreateSystemThread NtSetInformationThread") result = self.run_setup() systhread = self.sets / "linux-wine11-systhread" self.assertIn("DirectAudio ready (Wine 11, linux-wine11-systhread)", result.stderr) self.assertIn("WINEDLLPATH=%s/lib/wine" % systhread, result.stdout) self.assertEqual(self.link("system32"), str(systhread / "lib/wine/aarch64-windows/winedirectaudio.drv")) self.assertEqual((self.windows / "system32/winedirectaudio.drv").read_bytes(), b"arm64x-linux-wine11-systhread") def test_no_set_for_the_interface_leaves_protons_own_audio(self): self.reg.write_text("WINE REGISTRY Version 2\n\n#arch=win64\n") self.winepulse.write_bytes(b"\x7fELF PsCreateSystemThread") shutil.rmtree(self.sets / "linux-wine11-systhread") result = self.run_setup() self.assertIn("no driver set for the linux-wine11-systhread interface", result.stderr) self.assertIn("WINEDLLPATH=\n", result.stdout) self.assertIn("WINE_AUDIO_DRIVER=\n", result.stdout) self.assertFalse((self.windows / "system32/winedirectaudio.drv").exists()) def test_older_selections_are_upgraded(self): self.reg.write_text('WINE REGISTRY Version 2\n\n[SoftwareWineDrivers] 1790995967\n#time=1dd52e24550e980\n"Audio"="directaudio"\n' '\n[Software\\\\Wine\\\\Drivers] 1790995986\n#time=1dd52e250b31ec4\n"Audio"="directaudio"\n') self.assertIn("DirectAudio selected", self.run_setup().stderr) self.assertEqual(self.audio_values(), [("[SoftwareWineDrivers]", '"Audio"="directaudio"'), ("[Software\\\\Wine\\\\Drivers]", '"Audio"="directaudio"'), ("[Software\\\\Wine\\\\Drivers]", '"Audio"="directaudio,pulse"')]) self.assertNotIn("DirectAudio selected", self.run_setup().stderr) def test_a_copied_driver_is_replaced_by_the_staged_one(self): self.reg.write_text("WINE REGISTRY Version 2\n") (self.windows / "system32/winedirectaudio.drv").write_bytes(b"stale copy") self.run_setup() self.assertEqual(self.link("system32"), str(self.audio / "lib/wine/aarch64-windows/winedirectaudio.drv")) def test_nothing_is_selected_without_the_driver_in_the_prefix(self): self.reg.write_text("WINE REGISTRY Version 2\n") shutil.rmtree(self.windows) self.assertIn("could not be placed", self.run_setup().stderr) self.assertEqual(self.audio_values(), []) (self.windows / "system32").mkdir(parents=True) (self.audio / "lib/wine/aarch64-windows/winedirectaudio.drv").unlink() self.assertIn("could not be placed", self.run_setup().stderr) self.assertEqual(self.audio_values(), []) def test_a_new_prefix_waits_for_the_next_launch(self): shutil.rmtree(self.compat / "pfx") self.assertIn("no prefix yet", self.run_setup().stderr) self.assertFalse(self.reg.exists()) def test_another_wine_major_is_left_alone(self): (self.tool / "files/bin-arm64/wine").write_text("#!/bin/sh\necho wine-10.0\n") self.reg.write_text("WINE REGISTRY Version 2\n") result = self.run_setup() self.assertIn("left off", result.stderr) self.assertIn("WINEDLLPATH=\n", result.stdout) self.assertEqual(self.audio_values(), []) self.assertIsNone(self.link("system32")) class ProjectZomboidJvmTest(unittest.TestCase): ARGS = ["-Djava.awt.headless=true", "-Xmx3072m", "-XX:+UseZGC", "-XX:-CreateCoredumpOnCrash"] def launch(self, tmp, args=None, stem="ProjectZomboid64"): settings = Path(tmp) / (stem + ".json") settings.write_text(json.dumps({"mainClass": "zombie/gameStates/MainScreenState", "vmArgs": args or self.ARGS})) command = ["proton", "waitforexitandrun", str(Path(tmp) / (stem + ".exe"))] return settings, command def test_zgc_becomes_g1_and_the_c1_jit_flag_is_added_once(self): with tempfile.TemporaryDirectory() as tmp: settings, command = self.launch(tmp) MODULE["project_zomboid_jvm"](command) data = json.loads(settings.read_text()) self.assertEqual(data["vmArgs"], ["-Djava.awt.headless=true", "-Xmx3072m", "-XX:-CreateCoredumpOnCrash", "-XX:+UseG1GC", "-XX:TieredStopAtLevel=1"]) self.assertEqual(data["mainClass"], "zombie/gameStates/MainScreenState") before = settings.read_text() MODULE["project_zomboid_jvm"](command) self.assertEqual(settings.read_text(), before) def test_other_collectors_and_windows_specific_lists_are_rewritten(self): with tempfile.TemporaryDirectory() as tmp: settings, command = self.launch(tmp) settings.write_text(json.dumps({"vmArgs": ["-XX:+UseParallelGC", "-Xmx1g"], "windows": {"10.0.17134": {"vmArgs": ["-XX:+UseZGC", "-XX:+UseG1GC"]}}})) MODULE["project_zomboid_jvm"](command) data = json.loads(settings.read_text()) flags = ["-XX:+UseG1GC", "-XX:TieredStopAtLevel=1"] self.assertEqual(data["vmArgs"], ["-Xmx1g"] + flags) self.assertEqual(data["windows"]["10.0.17134"]["vmArgs"], flags) def test_other_games_and_unreadable_files_are_left_alone(self): with tempfile.TemporaryDirectory() as tmp: settings, command = self.launch(tmp, stem="Other") before = settings.read_text() MODULE["project_zomboid_jvm"](command) self.assertEqual(settings.read_text(), before) settings.write_text("{") MODULE["project_zomboid_jvm"](command[:2] + [str(Path(tmp) / "Other.exe")]) (Path(tmp) / "ProjectZomboid64.json").write_text("{") MODULE["project_zomboid_jvm"](command[:2] + [str(Path(tmp) / "ProjectZomboid64.exe")]) self.assertEqual((Path(tmp) / "ProjectZomboid64.json").read_text(), "{") class UbisoftOverlayTest(unittest.TestCase): SETTINGS = ( "language:\n code: en-US\n" "overlay:\n enabled: true\n forceunhookgame: false\n fps_enabled: false\n warning_enabled: true\n" "patchTrigger:\n source: USER_RESTART\n" ) def test_the_overlay_and_its_prompt_are_turned_off_and_the_rest_is_kept(self): got = MODULE["overlay_off"](self.SETTINGS) self.assertEqual(got, self.SETTINGS.replace("enabled: true", "enabled: false")) self.assertEqual(MODULE["overlay_off"](got), got) def test_missing_keys_or_section_are_added(self): self.assertEqual(MODULE["overlay_off"]("overlay:\n fps_enabled: true\nmisc:\n a: 1\n"), "overlay:\n fps_enabled: true\n enabled: false\n warning_enabled: false\nmisc:\n a: 1\n") self.assertEqual(MODULE["overlay_off"](""), "overlay:\n enabled: false\n warning_enabled: false\n") def test_blank_lines_inside_the_overlay_section_do_not_end_it(self): text = "overlay:\n enabled: true\n\n warning_enabled: true\n\nmisc:\n a: 1\n" self.assertEqual(MODULE["overlay_off"](text), text.replace("true", "false")) self.assertEqual(MODULE["overlay_off"]("overlay:\n fps_enabled: true\n\nmisc:\n a: 1\n"), "overlay:\n fps_enabled: true\n enabled: false\n warning_enabled: false\n\nmisc:\n a: 1\n") def test_an_annotated_overlay_header_is_the_same_section(self): self.assertEqual(MODULE["overlay_off"]("overlay: # in-game\n enabled: true\n"), "overlay: # in-game\n enabled: false\n warning_enabled: false\n") def test_only_prefixes_with_ubisoft_connect_are_touched(self): with tempfile.TemporaryDirectory() as tmp: prefix = Path(tmp) / "447040/pfx" env = {"STEAM_COMPAT_DATA_PATH": str(prefix.parent)} settings = prefix / MODULE["UBISOFT_SETTINGS"] prefix.mkdir(parents=True) MODULE["ubisoft_overlay_off"](env) self.assertFalse(settings.exists()) (prefix / MODULE["UBISOFT_LAUNCHER"]).mkdir(parents=True) MODULE["ubisoft_overlay_off"](env) self.assertEqual(settings.read_text(), "overlay:\n enabled: false\n warning_enabled: false\n") settings.write_text(self.SETTINGS) settings.chmod(0o644) MODULE["ubisoft_overlay_off"](env) self.assertEqual(settings.read_text(), self.SETTINGS.replace("enabled: true", "enabled: false")) self.assertEqual(settings.stat().st_mode & 0o777, 0o644) self.assertEqual(sorted(p.name for p in settings.parent.iterdir()), ["settings.yaml"]) class UbisoftCompositionTest(unittest.TestCase): def test_writes_through_wine_without_replacing_a_live_servers_registry(self): with tempfile.TemporaryDirectory() as tmp: prefix = Path(tmp) / "pfx" (prefix / MODULE["UBISOFT_LAUNCHER"]).mkdir(parents=True) registry = prefix / "user.reg" registry.write_text("registry last saved by Wine\n") env = {"STEAM_COMPAT_DATA_PATH": tmp, "WINESERVER": "/selected/wineserver"} keys = [] def write_registry(argv, **kwargs): self.assertEqual(argv[:4], ["/selected/proton", "runinprefix", "reg.exe", "add"]) self.assertEqual(argv[5:], ["/v", "dcomp", "/t", "REG_SZ", "/d", "", "/f"]) self.assertIs(kwargs["env"], env) self.assertTrue(kwargs["check"]) keys.append(argv[4]) registry.write_text("newer state saved by the active Wine server\n") return subprocess.CompletedProcess(argv, 0) with mock.patch("subprocess.run", side_effect=write_registry) as run: MODULE["ubisoft_composition_off"]("/selected/proton", env) self.assertEqual(run.call_count, 2) self.assertEqual(registry.read_text(), "newer state saved by the active Wine server\n") self.assertEqual(keys, [r"HKCU\Software\Wine\AppDefaults\%s\DllOverrides" % program for program in ("upc.exe", "UplayWebCore.exe")]) self.assertEqual(list(prefix.glob(".ubisoft-*.reg")), []) def test_other_prefixes_and_uninitialized_registries_are_untouched(self): with tempfile.TemporaryDirectory() as tmp: prefix = Path(tmp) / "pfx" prefix.mkdir() registry = prefix / "user.reg" registry.write_text("WINE REGISTRY Version 2\n") env = {"STEAM_COMPAT_DATA_PATH": tmp} with mock.patch("subprocess.run") as run: MODULE["ubisoft_composition_off"]("proton", env) self.assertEqual(registry.read_text(), "WINE REGISTRY Version 2\n") registry.unlink() (prefix / MODULE["UBISOFT_LAUNCHER"]).mkdir(parents=True) MODULE["ubisoft_composition_off"]("proton", env) self.assertFalse(registry.exists()) run.assert_not_called() def test_failed_registry_write_keeps_the_game_launch_available(self): with tempfile.TemporaryDirectory() as tmp: prefix = Path(tmp) / "pfx" (prefix / MODULE["UBISOFT_LAUNCHER"]).mkdir(parents=True) (prefix / "user.reg").write_text("WINE REGISTRY Version 2\n") for failed_write in (0, 1): stderr = io.StringIO() results = [subprocess.CompletedProcess([], 0)] * failed_write results.append(subprocess.CalledProcessError(1, "proton")) with mock.patch("subprocess.run", side_effect=results) as run, mock.patch("sys.stderr", stderr): MODULE["ubisoft_composition_off"]("proton", {"STEAM_COMPAT_DATA_PATH": tmp}) self.assertEqual(run.call_count, failed_write + 1) self.assertIn("configuration failed", stderr.getvalue()) self.assertNotIn("uses CEF", stderr.getvalue()) self.assertEqual(list(prefix.glob(".ubisoft-*.reg")), []) if __name__ == "__main__": unittest.main() class ForceSsbsTest(unittest.TestCase): def test_libssbs_is_preloaded_only_when_asked_and_only_into_arm64_proton(self): with tempfile.TemporaryDirectory() as tmp: arm = Path(tmp) / "arm" (arm / "files/lib/wine/aarch64-unix").mkdir(parents=True) x86 = Path(tmp) / "x86" (x86 / "files/lib/wine/x86_64-unix").mkdir(parents=True) lib = Path(tmp) / "libssbs.so" lib.write_bytes(b"\x7fELF") g = MODULE["force_ssbs"].__globals__ old = g["SSBS_LIB"] g["SSBS_LIB"] = str(lib) try: env = {"LD_PRELOAD": "/usr/local/lib/libblsession.so"} self.assertEqual(MODULE["force_ssbs"]([str(arm / "proton"), "waitforexitandrun"], env), env) on = dict(env, DROIDDECK_FORCE_SSBS="1") got = MODULE["force_ssbs"]([str(arm / "proton"), "waitforexitandrun"], on) self.assertEqual(got["LD_PRELOAD"], "/usr/local/lib/libblsession.so:" + str(lib)) self.assertEqual(MODULE["force_ssbs"]([str(arm / "proton")], got)["LD_PRELOAD"], got["LD_PRELOAD"]) self.assertEqual(MODULE["force_ssbs"]([str(x86 / "proton")], on), on) finally: g["SSBS_LIB"] = old