mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 11:01:07 +00:00
* fix: x86 Linux games that also ship for Windows run under Proton again Since #749 the live compatibility helper treated every installed game with both Windows and x86 Linux builds as a user's native choice, because the client reports an empty tool for unmapped games. Those games were left on the Steam Linux Runtime, which cannot start them here (bwrap fails, or FEX looks for a RootFS that is not shipped), so Valheim, Moonlighter, Half-Life 2, TF2 and others exited within seconds. - An unmapped game the helper never managed now gets the default Proton. Clearing a mapping the helper set is still kept as a native choice. - Native choices are stored under "native_choices". The old "native" list also held games nobody picked, so it is dropped. * feat: Linux builds run under FEX by default, with the runtimes fetched automatically - Steam fetches Proton, FEX, Steam Linux Runtime 3.0 and 4.0 on the first start and keeps them updated instead of leaving them pending. - Games that ship a Linux build go to FEX once it can run them; Windows-only games and installed Windows builds keep the Proton default. - FEX runs games on a combined x86 runtime with the newer glibc and libstdc++ the GL and Vulkan thunks need. - Vulkan display extensions the driver cannot handle are hidden in front of the thunk, without an audit module. - Mono's incremental garbage collector is turned off under FEX, which stops Unity games crashing. - x86 Steamworks finds the x86 steamclient. - 32-bit games read the virtual controller. * fix: controllers work in Linux Unity games such as Valheim Joystick ioctls on an event node now fail with EINVAL like the kernel's evdev, so the SDL2 that Unity embeds reads the pad's real ID instead of ignoring it as 0000:0000. * test: skip the switch script test without node, and the ring tests without gcc * fix: games with a Linux build keep the Proton default The Windows build often runs better, so titles that ship both builds stay on the default Proton again. Users can still pick FEX for one, and that choice is kept. Linux-only titles still go to FEX. * fix: unmapped dual-platform games get Proton even when config.vdf cannot be read
166 lines
7.7 KiB
Python
166 lines
7.7 KiB
Python
import importlib.machinery
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import re
|
|
import runpy
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
BIN = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin"
|
|
|
|
|
|
def load(name):
|
|
loader = importlib.machinery.SourceFileLoader(name.replace("-", "_"), str(BIN / name))
|
|
spec = importlib.util.spec_from_loader(loader.name, loader)
|
|
module = importlib.util.module_from_spec(spec)
|
|
loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
library = load("droiddeck-steam-library")
|
|
desktop = load("droiddeck-desktop-games")
|
|
COMPAT = runpy.run_path(str(BIN / "steam-compatibility"))
|
|
|
|
|
|
def entry(index, path, label=""):
|
|
return '\t"%d"\n\t{\n\t\t"path"\t\t"%s"\n\t\t"label"\t\t"%s"\n\t}\n' % (index, path, label)
|
|
|
|
|
|
def manifest(steamapps, appid, name="Game", installdir="Game", flags="4"):
|
|
steamapps.mkdir(parents=True, exist_ok=True)
|
|
(steamapps / ("appmanifest_%s.acf" % appid)).write_text(
|
|
'"AppState"\n{\n\t"appid"\t\t"%s"\n\t"name"\t\t"%s"\n\t"StateFlags"\t\t"%s"\n\t"installdir"\t\t"%s"\n}\n'
|
|
% (appid, name, flags, installdir))
|
|
|
|
|
|
class LibraryListTest(unittest.TestCase):
|
|
def setUp(self):
|
|
temp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(temp.cleanup)
|
|
self.root = Path(temp.name)
|
|
self.steam = self.root / "Steam"
|
|
self.vdf = self.steam / "steamapps/libraryfolders.vdf"
|
|
self.vdf.parent.mkdir(parents=True)
|
|
self.games = self.root / "Games"
|
|
self.card = self.root / "card"
|
|
self.pc = self.games / "PC"
|
|
self.old = self.games / "Old"
|
|
self.user = self.root / "user-library"
|
|
for folder in (self.pc, self.old, self.user, self.card):
|
|
(folder / "steamapps").mkdir(parents=True)
|
|
manifest(self.pc / "steamapps", 10)
|
|
self.vdf.write_text('"libraryfolders"\n{\n' + entry(0, self.steam) + entry(1, self.old, "Old")
|
|
+ entry(2, self.user, "Mine") + entry(3, self.card, "SD Card") + "}\n")
|
|
self.listing = self.root / "steam-libraries.json"
|
|
for target in (patch.object(library, "GAMES_DIR", str(self.games) + "/"),
|
|
patch.object(library, "LIBRARIES", ((str(self.card), "SD Card"),)),
|
|
patch("builtins.print")):
|
|
target.start()
|
|
self.addCleanup(target.stop)
|
|
|
|
def run_main(self, listing=True):
|
|
with patch.dict(os.environ), patch("sys.argv", ["droiddeck-steam-library", str(self.steam)]):
|
|
os.environ.pop("BL_STEAM_LIBRARIES", None)
|
|
if listing:
|
|
os.environ["BL_STEAM_LIBRARIES"] = str(self.listing)
|
|
library.main()
|
|
return self.vdf.read_text()
|
|
|
|
def paths(self, text):
|
|
return [Path(p) for p in re.findall(r'"path"\s+"([^"]*)"', text)]
|
|
|
|
def test_listed_games_folder_is_registered_and_the_rest_are_forgotten(self):
|
|
self.listing.write_text(json.dumps([{"path": str(self.pc), "label": "PC (PNY USB drive)"}]))
|
|
text = self.run_main()
|
|
self.assertEqual([self.steam, self.user, self.pc], self.paths(text))
|
|
self.assertIn('"label"\t\t"PC (PNY USB drive)"', text)
|
|
self.assertIn('"10"\t\t"0"', text)
|
|
self.assertIn('"label"\t\t"PC (PNY USB drive)"', (self.pc / "libraryfolder.vdf").read_text())
|
|
|
|
def test_absent_listed_folder_is_forgotten_and_comes_back(self):
|
|
self.listing.write_text(json.dumps([{"path": str(self.pc), "label": "PC"}, {"path": str(self.card), "label": "SD Card"}]))
|
|
self.run_main()
|
|
moved = self.root / "unplugged"
|
|
self.pc.rename(moved)
|
|
self.assertNotIn(self.pc, self.paths(self.run_main()))
|
|
moved.rename(self.pc)
|
|
self.assertIn(self.pc, self.paths(self.run_main()))
|
|
self.assertIn(self.card, self.paths(self.vdf.read_text()))
|
|
|
|
def test_without_a_list_only_the_card_is_managed(self):
|
|
text = self.run_main(listing=False)
|
|
self.assertEqual([self.steam, self.old, self.user, self.card], self.paths(text))
|
|
|
|
def test_unreadable_list_keeps_games_folder_entries(self):
|
|
self.listing.write_text("{not json")
|
|
text = self.run_main()
|
|
self.assertEqual([self.steam, self.old, self.user, self.card], self.paths(text))
|
|
|
|
def test_labels_cannot_break_the_vdf(self):
|
|
self.listing.write_text(json.dumps([{"path": str(self.pc), "label": 'P"C\\'}]))
|
|
text = self.run_main()
|
|
self.assertIn('"label"\t\t"P C "', text)
|
|
|
|
|
|
class CompatLibrariesTest(unittest.TestCase):
|
|
def setUp(self):
|
|
temp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(temp.cleanup)
|
|
self.root = Path(temp.name)
|
|
self.steam = self.root / "Steam"
|
|
self.pc = self.root / "Games/My PC"
|
|
(self.steam / "steamapps").mkdir(parents=True)
|
|
(self.steam / "steamapps/libraryfolders.vdf").write_text(
|
|
'"libraryfolders"\n{\n' + entry(0, self.steam) + entry(1, self.pc, "PC") + "}\n")
|
|
|
|
def test_titles_and_tools_in_a_registered_library_are_found(self):
|
|
manifest(self.steam / "steamapps", 20)
|
|
manifest(self.pc / "steamapps", 30)
|
|
manifest(self.pc / "steamapps", 3127680, installdir="FEX")
|
|
self.assertEqual(["20", "30"], COMPAT["installed_apps"](str(self.steam)))
|
|
self.assertEqual(["1628350", "4183110"], COMPAT["fex_missing"](str(self.steam)))
|
|
|
|
def test_newest_proton_across_libraries_wins(self):
|
|
for root, build in ((self.steam, "100"), (self.pc, "200")):
|
|
depot = root / "steamapps/common/Proton Experimental (ARM64)"
|
|
(depot / "files/bin-arm64").mkdir(parents=True)
|
|
(depot / "version").write_text(build + " x\n")
|
|
self.assertEqual(str(self.pc / "steamapps/common/Proton Experimental (ARM64)"), COMPAT["find_source"](str(self.steam)))
|
|
|
|
def test_missing_list_still_reads_the_client_library(self):
|
|
(self.steam / "steamapps/libraryfolders.vdf").unlink()
|
|
self.assertEqual(str(self.steam), COMPAT["libraries"](str(self.steam))[0])
|
|
|
|
|
|
class DesktopGamesTest(unittest.TestCase):
|
|
def test_games_in_registered_libraries_get_entries(self):
|
|
with tempfile.TemporaryDirectory() as temp:
|
|
root = Path(temp)
|
|
steam, pc = root / "Steam", root / "Games/PC"
|
|
manifest(steam / "steamapps", 40, name="Internal")
|
|
manifest(pc / "steamapps", 50, name="Outside")
|
|
(steam / "steamapps/libraryfolders.vdf").write_text('"libraryfolders"\n{\n' + entry(0, steam) + entry(1, pc) + "}\n")
|
|
with patch.object(desktop, "STEAM_ROOT", str(steam)), patch.object(desktop, "APPS_DIR", str(root / "apps")), \
|
|
patch.object(desktop, "LIBRARIES", ((str(steam), "Steam Games"), (str(root / "card"), "SD Games"))):
|
|
self.assertEqual({40: "Internal", 50: "Outside"}, desktop.write_entries())
|
|
|
|
def test_runtimes_and_compatibility_tools_get_none(self):
|
|
with tempfile.TemporaryDirectory() as temp:
|
|
root = Path(temp)
|
|
steam = root / "Steam"
|
|
manifest(steam / "steamapps", 40, name="Game")
|
|
manifest(steam / "steamapps", 1628350, name="Steam Linux Runtime 3.0 (sniper)", installdir="SteamLinuxRuntime_sniper")
|
|
manifest(steam / "steamapps", 5000, name="Some Proton", installdir="Some Proton")
|
|
(steam / "steamapps/common/Some Proton").mkdir(parents=True)
|
|
(steam / "steamapps/common/Some Proton/toolmanifest.vdf").write_text('"manifest"\n{\n}\n')
|
|
with patch.object(desktop, "STEAM_ROOT", str(steam)), patch.object(desktop, "APPS_DIR", str(root / "apps")), \
|
|
patch.object(desktop, "LIBRARIES", ((str(steam), "Steam Games"),)):
|
|
self.assertEqual({40: "Game"}, desktop.write_entries())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|