python/steamutil: Add platform override support

master
Taeyeon Mori 2 months ago
parent c47c5c78db
commit 7b07696298
  1. 91
      lib/python/steamutil.py

@ -61,10 +61,14 @@ class AppInfo:
@property @property
def is_native(self): def is_native(self):
""" Whether the app has a version native to the current platform """
return sys.platform in self.oslist return sys.platform in self.oslist
@cached_property @cached_property
def compat_tool(self) -> dict: def compat_tool(self) -> dict:
""" The compatibility tool selected for this app.
Note: this will still return a default if no tool is used
"""
mapping = self.steam.compat_tool_mapping mapping = self.steam.compat_tool_mapping
appid = str(self.appid) appid = str(self.appid)
# User override # User override
@ -130,15 +134,24 @@ class App(AppInfo):
# Steam Play info # Steam Play info
@property @property
def is_steam_play(self) -> Optional[str]: def platform_override(self) -> Tuple[Optional[str], Optional[str]]:
return dd_getpath(self.manifest, ("AppState", "UserConfig", "platform_override_source"), None, t=str) uc = dd_getpath(self.manifest, ("AppState", "UserConfig"), None, t=Dict[str, str])
if uc:
return uc.get("platform_override_source", None), uc.get("platform_override_dest", None)
return None, None
@property
def is_steam_play(self) -> Union[str, bool]:
""" Whether app needs a compatibility tool to run """
if (po := self.platform_override[0]) is not None:
return po
return not self.is_native
@property @property
def is_proton_app(self) -> Optional[bool]: def is_proton_app(self) -> Optional[bool]:
uc = dd_getpath(self.manifest, ("AppState", "UserConfig"), None, t=dict) """ Whether app needs (specifically) Proton to run """
if uc and "platform_override_source" in uc: # XXX: Should this try to figure out if selected compat tool is actually proton?
return uc["platform_override_source"] == "windows" and uc["platform_override_dest"] == "linux" return self.platform_override[0] == "windows" or not self.is_native and "windows" in self.oslist
return None
@cached_property @cached_property
def compat_path(self) -> Path: def compat_path(self) -> Path:
@ -244,24 +257,19 @@ class UserAppConfig:
@property @property
def _data(self): def _data(self):
try: return dd_getpath(self.user.localconfig, ("UserLocalConfigStore", "Software", "Valve", "Steam", ("Apps", "apps"), str(self.appid)), {}, t=dict)
return self.user.localconfig["UserLocalConfigStore"]["Software"]["Valve"]["Steam"]["Apps"][str(self.appid)]
except KeyError:
return {} # TODO
@property @property
def last_played(self) -> datetime.datetime: def last_played(self) -> datetime.datetime:
return datetime.datetime.fromtimestamp(int(self._data.get("LastPlayed", "0"))) return datetime.datetime.fromtimestamp(int(self._data.get("LastPlayed", 0)))
@property @property
def playtime(self) -> datetime.time: def playtime(self) -> datetime.time:
t = int(self._data.get("Playtime", "0")) return datetime.time(minute=int(self._data.get("Playtime", 0)))
return datetime.time(t // 60, t % 60)
@property @property
def playtime_two_weeks(self) -> datetime.time: def playtime_two_weeks(self) -> datetime.time:
t = int(self._data.get("Playtime2wks", "0")) return datetime.time(minute=int(self._data.get("Playtime2wks", 0)))
return datetime.time(t // 60, t % 60)
launch_options = DictPathProperty[Optional[DeepDict]]("_data", ("LaunchOptions",), None) launch_options = DictPathProperty[Optional[DeepDict]]("_data", ("LaunchOptions",), None)
@ -374,9 +382,8 @@ class Steam:
def most_recent_user(self) -> Optional[LoginUser]: def most_recent_user(self) -> Optional[LoginUser]:
try: try:
# Apparently, Steam doesn't care about case in the config/*.vdf keys # 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: with open(self.loginusers_vdf, encoding="utf-8") as f:
data = vdf_ci.parse(f) data = _vdf_ci.parse(f)
for id, info in cast(Mapping[str, Dict], data["users"]).items(): for id, info in cast(Mapping[str, Dict], data["users"]).items():
if info["mostrecent"] == "1": if info["mostrecent"] == "1":
return LoginUser(self, int(id), info) return LoginUser(self, int(id), info)
@ -397,7 +404,7 @@ class Steam:
config_install_store = DictPathProperty[Dict]("config", ("InstallConfigStore",)) config_install_store = DictPathProperty[Dict]("config", ("InstallConfigStore",))
config_software_steam = DictPathProperty[Dict]("config", ("InstallConfigStore", "Software", "Valve", "Steam")) config_software_steam = DictPathProperty[Dict]("config", ("InstallConfigStore", "Software", "Valve", "Steam"))
compat_tool_mapping = DictPathProperty[Dict]("config_software_steam", ("CompatToolMapping",)) compat_tool_mapping = DictPathProperty[Dict]("config", ("InstallConfigStore", "Software", "Valve", "Steam", "CompatToolMapping"))
# AppInfo cache # AppInfo cache
@cached_property @cached_property
@ -425,21 +432,23 @@ class Steam:
tool["install_path"] = app.install_path tool["install_path"] = app.install_path
tools[name] = tool tools[name] = tool
# Find custom compat tools # Find custom compat tools
manifests = [] compattools_d = self.root / "compatibilitytools.d"
for p in (self.root / "compatibilitytools.d").iterdir(): if compattools_d.exists():
if p.suffix == ".vdf": manifests = []
manifests.append(p) for p in compattools_d.iterdir():
elif p.is_dir(): if p.suffix == ".vdf":
c = p / "compatibilitytool.vdf" manifests.append(p)
if c.exists(): elif p.is_dir():
manifests.append(c) c = p / "compatibilitytool.vdf"
for mfst_path in manifests: if c.exists():
with open(mfst_path, encoding="utf-8") as f: manifests.append(c)
mfst = _vdf.parse(f) for mfst_path in manifests:
for name, t in dd_getpath(mfst, ("compatibilitytools", "compat_tools"), t=dict).items(): with open(mfst_path, encoding="utf-8") as f:
# TODO warn duplicate name mfst = _vdf.parse(f)
t["install_path"] = mfst_path.parent / t["install_path"] for name, t in dd_getpath(mfst, ("compatibilitytools", "compat_tools"), t=dict).items():
tools[name] = t # TODO warn duplicate name
t["install_path"] = mfst_path.parent / t["install_path"]
tools[name] = t
return tools return tools
# Game/App Library # Game/App Library
@ -478,9 +487,9 @@ class Steam:
if app is not None: if app is not None:
return app return app
if not installed: if not installed:
for appid, appinfo in self.appinfo.items(): for appinfo in self.appinfo:
if appid == id: if appinfo.id == id:
return AppInfo(self, appid, appinfo_data=appinfo) return AppInfo(self, id, appinfo_data=appinfo)
return None return None
@overload @overload
@ -495,22 +504,22 @@ class Steam:
reg = re.compile(regexp, re.IGNORECASE) reg = re.compile(regexp, re.IGNORECASE)
broken_ids = set() broken_ids = set()
try: try:
for appid, appinfo in self.appinfo.items(): for appinfo in self.appinfo:
# Skip broken entries # Skip broken entries
try: try:
name = appinfo["appinfo"]["common"]["name"] name = appinfo["appinfo"]["common"]["name"]
except KeyError: except KeyError:
broken_ids.add(appid) broken_ids.add(appinfo.id)
continue continue
if reg.search(name): if reg.search(name):
for lf in self.library_folders: for lf in self.library_folders:
app = lf.get_app(appid) app = lf.get_app(appinfo.id)
if app: if app:
yield app yield app
break break
else: else:
yield AppInfo(self, appid, appinfo_data=appinfo) yield AppInfo(self, appinfo.id, appinfo_data=appinfo)
except: except Exception:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
print("[SteamUtil] Warning: could not read non-installed apps from Steam appinfo cache. Searching locally") print("[SteamUtil] Warning: could not read non-installed apps from Steam appinfo cache. Searching locally")

Loading…
Cancel
Save