python/steamutil,protontool: Search appinfo cache by name, run game on windows

master
Taeyeon Mori 4 years ago
parent 0df69011e4
commit a9ec47f54a
  1. 55
      bin/protontool
  2. 86
      lib/python/steamutil.py
  3. 20
      lib/python/vdfparser.py

@ -48,7 +48,7 @@ class ProtonTool:
if self.app is None:
die(1, "Could not find App #%s." % args.game)
else:
apps = list(self.steam.find_apps(args.game))
apps = list(self.steam.find_apps(args.game, installed=not args.show_all))
if len(apps) > 1:
print_error("Found more than one installed game matching /%s/, please be more specific" % args.game)
for app in apps:
@ -90,6 +90,7 @@ class ProtonTool:
parser.add_argument("game", help="Can be either the appid or a name pattern to look for")
parser.add_argument("--steam-path", help="Steam install path", default=None)
parser.add_argument("-l", "--list-tools", action=cls.ListAction, help="List all known compatibility tools")
parser.add_argument("-a", "--show-all", action="store_true", default=False, help="Search all games in local appinfo cache")
commands = parser.add_subparsers()
@ -235,10 +236,13 @@ class ProtonTool:
return os.execvp(self.args.cmdline[0], self.args.cmdline)
def cmd_run(self):
""" Run a game executable directly, possibly using a compat tool """
# Obviously has to be installed
if not self.app.installed:
print_error("App is not installed.")
return 50
# Check if we want to use a compat tool
if self.args.tool:
tool = {"name": self.args.tool}
elif self.app.is_proton_app:
@ -246,14 +250,22 @@ class ProtonTool:
else:
tool = None
# Default to native oslist
if sys.platform.startswith("linux"):
target_oslist = "linux"
elif sys.platform.startswith("win"):
target_oslist = "windows"
# Set up compat tool
if tool is not None:
# XXX: what about other tools? Like Steam Linux Runtime
if self.args.config:
tool["config"] = self.args.config
toolinfo = self.compat_tools[tool["name"]]
info_tool, info_path = self._format_tool_info(self.steam, tool, toolinfo)
info_tool, _ = self._format_tool_info(self.steam, tool, toolinfo)
print_info("Using tool", info_tool)
if self.args.waitforexit:
@ -264,25 +276,34 @@ class ProtonTool:
with open(toolinfo["install_path"] / "toolmanifest.vdf") as f:
mfst = steamutil._vdf.parse(f)
cmdline = mfst["manifest"][verb]
path, *args = cmdline.split()
path = str(toolinfo["install_path"]) + path
self.setup_proton_env(tool)
for cfg in self.app.launch_configs:
# TODO: Handle multiple
if cfg["type"] == "default" and toolinfo.get("from_oslist", "windows") in cfg["config"]["oslist"]:
argv = [path, *args, str(self.app.install_path / cfg["executable"])]
print_info("Executing", " ".join(argv))
return os.execv(path, argv)
# Set tool oslist and prepare arguments
target_oslist = toolinfo.get("from_oslist", "windows")
tool_argv = [path, *args]
else:
# TODO windows support?
for cfg in self.app.launch_configs:
if cfg["type"] == "default" and "linux" in cfg["config"]["oslist"]:
return os.execv(self.app.install_path / cfg["executable"], [cfg["executable"]])
tool_argv = []
# Read launch configurations
for cfg in self.app.launch_configs:
# Filter ones that won't work
if "type" in cfg and cfg["type"] != "default": continue
if "oslist" in cfg and target_oslist not in cfg["oslist"]: continue
if "executable" not in cfg: continue
# Execute
game_path = self.app.install_path / cfg["executable"]
argv = [*tool_argv, str(game_path)] # TODO: Use Steam user custom args?
print_info("Launching", " ".join(argv))
os.chdir(game_path.parent)
return os.execv(argv[0], argv)
else:
print_error("No matching launch configuration in appinfo")
return 51
def cmd_proton(self):
# proton and wine subcommands:
# Run proton respective wine with arguments

@ -7,7 +7,7 @@ import re, fnmatch, datetime
from pathlib import Path
from typing import List, Iterable, Dict, Tuple, Callable, Optional, Union
from vdfparser import VdfParser, DeepDict, AppInfoFile
from vdfparser import VdfParser, DeepDict, AppInfoFile, LowerCaseNormalizingDict
class CachedProperty:
@ -94,7 +94,10 @@ class AppInfo:
self.appid = appid
if appinfo_data is not None:
self.__dict__["appinfo"] = appinfo_data
def __repr__(self):
return "<steamutil.AppInfo #%7d '%s' (%s)>" % (self.appid, self.name, self.install_dir)
installed = False
# AppInfo
@ -106,10 +109,10 @@ class AppInfo:
@property
def launch_configs(self):
return self.appinfo["appinfo"]["config"]["launch"].values()
name = DictPathRoProperty("appinfo", ("appinfo", "common", "name"))
name = DictPathRoProperty("appinfo", ("appinfo", "common", "name"), default=None)
oslist = DictPathRoProperty("appinfo", ("appinfo", "common", "oslist"), type=lambda s: s.split(","))
install_dir = DictPathRoProperty("appinfo", ("appinfo", "config", "installdir"))
install_dir = DictPathRoProperty("appinfo", ("appinfo", "config", "installdir"), default=None)
languages = DictPathRoProperty("appinfo", ("appinfo", "common", "supported_languages"))
gameid = DictPathRoProperty("appinfo", ("appinfo", "common", "gameid"), type=int)
@ -156,7 +159,7 @@ class App(AppInfo):
self.manifest_path = manifest_path
if manifest_data is None:
with open(manifest_path) as f:
with open(manifest_path, encoding="utf-8") as f:
self.manifest = _vdf.parse(f)
else:
self.manifest = manifest_data
@ -169,8 +172,8 @@ class App(AppInfo):
installed = True
def __repr__(self):
return "<steamutil.App %d '%s' @ \"%s\">" % (self.appid, self.name, self.install_path)
return "<steamutil.App #%7d '%s' @ \"%s\">" % (self.appid, self.name, self.install_path)
# Basic info
name = DictPathRoProperty("manifest", ("AppState", "name"))
language = DictPathRoProperty("manifest", ("AppState", "UserConfig", "language"), None)
@ -276,7 +279,7 @@ class LibraryFolder:
def find_apps_re(self, regexp: str) -> Iterable[App]:
reg = re.compile(r'"name"\s+".*%s.*"' % regexp, re.IGNORECASE)
for manifest in self.appmanifests: #pylint:disable=not-an-iterable
with open(manifest) as f:
with open(manifest, encoding="utf-8") as f:
content = f.read()
if reg.search(content):
yield App(self, manifest, manifest_data=_vdf.parse_string(content))
@ -351,7 +354,7 @@ class LoginUser:
@CachedProperty
def localconfig(self) -> DeepDict:
with open(self.localconfig_vdf) as f:
with open(self.localconfig_vdf, encoding="utf-8") as f:
return _vdf.parse(f)
# Game config
@ -426,8 +429,10 @@ class Steam:
@CachedProperty
def most_recent_user(self) -> Optional[LoginUser]:
try:
with open(self.loginusers_vdf) as f:
data = _vdf.parse(f)
# Apparently, Steam doesn't care about case in the config/*.vdf keys
vdf_ci = VdfParser(factory=LowerCaseNormalizingDict)
with open(self.loginusers_vdf, encoding="utf-8") as f:
data = vdf_ci.parse(f)
for id, info in data["users"].items():
if info["mostrecent"] == "1":
return LoginUser(self, int(id), info)
@ -443,7 +448,7 @@ class Steam:
# Config
@CachedProperty
def config(self) -> DeepDict:
with open(self.config_vdf) as f:
with open(self.config_vdf, encoding="utf-8") as f:
return _vdf.parse(f)
config_install_store = DictPathProperty("config", ("InstallConfigStore",))
@ -485,7 +490,7 @@ class Steam:
if c.exists():
manifests.append(c)
for mfst_path in manifests:
with open(mfst_path) as f:
with open(mfst_path, encoding="utf-8") as f:
mfst = _vdf.parse(f)
for name, t in mfst["compatibilitytools"]["compat_tools"].items():
# TODO warn duplicate name
@ -496,7 +501,7 @@ class Steam:
# Game/App Library
@CachedProperty
def library_folder_paths(self) -> List[Path]:
with open(self.libraryfolders_vdf) as f:
with open(self.libraryfolders_vdf, encoding="utf-8") as f:
return [Path(v) for k,v in _vdf.parse(f)["LibraryFolders"].items() if k.isdigit()]
@CachedProperty
@ -518,12 +523,49 @@ class Steam:
if appid == id:
return AppInfo(self, appid, appinfo_data=appinfo)
def find_apps(self, pattern: str) -> Iterable[App]:
def find_apps_re(self, regexp: str, installed=True) -> Iterable[App]:
""" Find all apps by regular expression """
if not installed:
# Search whole appinfo cache
reg = re.compile(regexp, re.IGNORECASE)
broken_ids = set()
try:
for appid, appinfo in self.appinfo.items():
# Skip broken entries
try:
name = appinfo["appinfo"]["common"]["name"]
except KeyError:
broken_ids.add(appid)
continue
if reg.search(name):
for lf in self.library_folders: #pylint:disable=not-an-iterable
app = lf.get_app(appid)
if app:
yield app
break
else:
yield AppInfo(self, appid, appinfo_data=appinfo)
except:
import traceback
traceback.print_exc()
print("[SteamUtil] Warning: could not read non-installed apps from Steam appinfo cache. Searching locally")
else:
return
finally:
if broken_ids:
print("[SteamUtil] Warning: found broken entries in appinfo cache:", ",".join(map(str, broken_ids)))
# Search local manifests directly
reg = re.compile(r'"name"\s+".*%s.*"' % regexp, re.IGNORECASE)
for lf in self.library_folders: #pylint:disable=not-an-iterable
yield from lf.find_apps(pattern)
def find_app(self, pattern: str) -> Optional[App]:
for app in self.find_apps(pattern):
return app
for manifest in lf.appmanifests: #pylint:disable=not-an-iterable
with open(manifest, encoding="utf-8") as f:
content = f.read()
if reg.search(content):
yield App(lf, manifest, manifest_data=_vdf.parse_string(content))
def find_apps(self, pattern: str, installed=True) -> Iterable[App]:
return self.find_apps_re(fnmatch.translate(pattern).rstrip("\\Z"), installed=installed)
def find_app(self, pattern: str, installed=True) -> Optional[App]:
for app in self.find_apps(pattern, installed=installed):
return app

@ -15,6 +15,26 @@ from typing import Dict, Union, Mapping, Tuple
DeepDict = Mapping[str, Union[str, "DeepDict"]]
class LowerCaseNormalizingDict(dict):
def __init__(self, *args, **kwds):
super().__init__()
# XXX: is there a better way to do this?
for k,v in dict(*args,**kwds).items():
k_ = k.lower()
if k_ in self:
raise KeyError("Duplicate key in LowerCaseNormalizingDict arguments: %s" % k_)
self[k_] = v
def __setitem__(self, key, value):
return super().__setitem__(key.lower(), value)
def __getitem__(self, key):
return super().__getitem__(key.lower())
def get(self, key, default=None):
return super().get(key.lower(), default=default)
class VdfParser:
"""
Simple Steam/Source VDF parser

Loading…
Cancel
Save