|
|
@ -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") |
|
|
|