diff --git a/dzgui/api/shortcuts.py b/dzgui/api/shortcuts.py index 243946f..154645e 100644 --- a/dzgui/api/shortcuts.py +++ b/dzgui/api/shortcuts.py @@ -49,9 +49,21 @@ class Shortcuts: def find_appname_by_unsigned_id(self, appid: int) -> str: # NOTE: bitmask signed int back to 32-bit CRC - for key in self.shortcuts["shortcuts"].keys(): - if self.shortcuts["shortcuts"][key]["appid"] & 0xFFFFFFFF == appid: - return str(self.shortcuts["shortcuts"][key]["appname"]) + # varying client versions treat case sensitivity differently + try: + for key in self.shortcuts["shortcuts"].keys(): + if self.shortcuts["shortcuts"][key]["appid"] & 0xFFFFFFFF == appid: + try: + return str(self.shortcuts["shortcuts"][key]["appname"]) + except Exception as e: + logger.debug(e) + try: + return str(self.shortcuts["shortcuts"][key]["AppName"]) + except Exception as e: + logger.debug(e) + return unknown + except Exception as e: + logger.debug(e) return unknown def get_shortcuts(self) -> Any: @@ -119,22 +131,18 @@ class Shortcuts: https://developer.valvesoftware.com/wiki/Add_Non-Steam_Game https://developer.valvesoftware.com/wiki/Steam_Library_Shortcuts - Wiki variously lists keys with title case and lowercase, but keys actually do - not seem to be case-sensitive. Some entries generated by Steam do not match the wiki - - Keys are entered into the dictionary in a linear insertion order - - # TODO: try to replicate key case sensitivity as it is created by Steam with generic shortcut + Key case is not internally consistent and varies between client versions + Most keys use Pascal case, but some do not appid: signed int CRC - exe: absolute path to the executable, must be wrapped in literal quotes + Exe: absolute path to the executable, must be wrapped in literal quotes StartDir: directory the executable starts in, generally the parent """ NEW_ENTRY: dict[str, Any] = {} NEW_ENTRY["appid"] = meta.appid - NEW_ENTRY["appname"] = meta.appname - NEW_ENTRY["exe"] = f'"{meta.exe_path}"' + NEW_ENTRY["AppName"] = meta.appname + NEW_ENTRY["Exe"] = f'"{meta.exe_path}"' NEW_ENTRY["StartDir"] = f"{meta.start_dir}" NEW_ENTRY["icon"] = meta.icon NEW_ENTRY["ShortcutPath"] = "" @@ -142,7 +150,7 @@ class Shortcuts: NEW_ENTRY["IsHidden"] = 0 NEW_ENTRY["AllowDesktopConfig"] = 1 NEW_ENTRY["AllowOverlay"] = 1 - NEW_ENTRY["openvr"] = 0 + NEW_ENTRY["OpenVR"] = 0 NEW_ENTRY["Devkit"] = 0 NEW_ENTRY["DevkitGameID"] = "" NEW_ENTRY["DevkitOverrideAppID"] = 0 diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index 9a1c67f..e007084 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -205,21 +205,28 @@ def launch_offline( proc = subprocess.run([*client_args, *params]) return proc.returncode +def find_loginusers(path: Path) -> Path: + return path / "config" / "loginusers.vdf" def find_user_id(path: Path) -> str | None: - resolved_path = path / "config" / "loginusers.vdf" + resolved_path = find_loginusers(path) try: with open(resolved_path, "r") as f: v = vdf.load(f) + # NOTE: beta client + # /package/beta + if len(v["users"]) == 1: + return str(list(v["users"].keys())[0]) for user in v["users"]: if v["users"][user]["MostRecent"] == "1": return str(user) return None except Exception as e: - logger.warn(e) + logger.debug(e) return None + def find_user_id_32(path: Path) -> int: uid = find_user_id(path) if uid is None: diff --git a/tests/fixtures/api/loginusers_beta_client.vdf b/tests/fixtures/api/loginusers_beta_client.vdf new file mode 100644 index 0000000..4ba1ef7 --- /dev/null +++ b/tests/fixtures/api/loginusers_beta_client.vdf @@ -0,0 +1,13 @@ +"users" +{ + "0" + { + "AccountName" "STEAMUSER" + "PersonaName" "STEAMUSER" + "RememberPassword" "0" + "WantsOfflineMode" "0" + "SkipOfflineModeWarning" "0" + "AutoLogin" "1" + "Timestamp" "0" + } +} diff --git a/tests/fixtures/api/loginusers_legacy_client.vdf b/tests/fixtures/api/loginusers_legacy_client.vdf new file mode 100644 index 0000000..383febd --- /dev/null +++ b/tests/fixtures/api/loginusers_legacy_client.vdf @@ -0,0 +1,14 @@ +"users" +{ + "0" + { + "AccountName" "STEAMUSER" + "PersonaName" "STEAMUSER" + "RememberPassword" "0" + "WantsOfflineMode" "0" + "SkipOfflineModeWarning" "0" + "AllowAutoLogin" "1" + "MostRecent" "1" + "Timestamp" "0" + } +} diff --git a/tests/fixtures/api/loginusers_legacy_client_multiple.vdf b/tests/fixtures/api/loginusers_legacy_client_multiple.vdf new file mode 100644 index 0000000..e353f75 --- /dev/null +++ b/tests/fixtures/api/loginusers_legacy_client_multiple.vdf @@ -0,0 +1,25 @@ +"users" +{ + "0" + { + "AccountName" "STEAMUSER" + "PersonaName" "STEAMUSER" + "RememberPassword" "0" + "WantsOfflineMode" "0" + "SkipOfflineModeWarning" "0" + "AllowAutoLogin" "1" + "MostRecent" "0" + "Timestamp" "0" + } + "1" + { + "AccountName" "STEAMUSER2" + "PersonaName" "STEAMUSER2" + "RememberPassword" "0" + "WantsOfflineMode" "0" + "SkipOfflineModeWarning" "0" + "AllowAutoLogin" "1" + "MostRecent" "1" + "Timestamp" "0" + } +} diff --git a/tests/test_loginusers.py b/tests/test_loginusers.py new file mode 100644 index 0000000..34864dd --- /dev/null +++ b/tests/test_loginusers.py @@ -0,0 +1,25 @@ +import pytest + +from pathlib import Path + +from dzgui.api.steam import find_user_id +from tests.fixtures import fixture_path + +pytestmark = pytest.mark.apitest + + +@pytest.mark.parametrize( + "fixture, expect", + [ + ("api/loginusers_legacy_client.vdf", 0), + ("api/loginusers_legacy_client_multiple.vdf", 1), + ("api/loginusers_beta_client.vdf", 0), + ], +) +def test_loginusers(monkeypatch, fixture: str, expect: int) -> None: + def mock_loginusers(path: Path) -> Path: + return fixture_path(fixture) + + monkeypatch.setattr("dzgui.api.steam.find_loginusers", mock_loginusers) + uid = find_user_id(Path("")) + assert int(uid) == expect diff --git a/tests/test_shortcuts.py b/tests/test_shortcuts.py index 60ab7b9..470b5c8 100644 --- a/tests/test_shortcuts.py +++ b/tests/test_shortcuts.py @@ -30,9 +30,9 @@ def test_no_shortcuts(monkeypatch) -> None: @pytest.fixture def dummy_app() -> None: d = { - "appname": "TEST APP", + "AppName": "TEST APP", "StartDir": "TEST_DIR", - "exe": "TEST_DIR/TEST_EXE.EXE", + "Exe": "TEST_DIR/TEST_EXE.EXE", "icon": "IMAGES_DIR/TEST_IMAGE.PNG", } return d @@ -41,8 +41,8 @@ def dummy_app() -> None: def test_wrap_exe(dummy_app) -> None: s = Shortcuts(Path("")) s.add_shortcut(*dummy_app.values()) - assert s.shortcuts["shortcuts"]["0"]["exe"][0] == '"' - assert s.shortcuts["shortcuts"]["0"]["exe"][-1] == '"' + assert s.shortcuts["shortcuts"]["0"]["Exe"][0] == '"' + assert s.shortcuts["shortcuts"]["0"]["Exe"][-1] == '"' def test_add_shortcut(dummy_app) -> None: @@ -51,7 +51,7 @@ def test_add_shortcut(dummy_app) -> None: new = s.shortcuts["shortcuts"] ind = str(len(new) - 1) for k, v in dummy_app.items(): - if k == "exe": + if k == "Exe": v = f'"{v}"' assert new[ind][k] == v