Compare commits

...
11 Commits
Author SHA1 Message Date
aclist 5c07a77b63 Merge pull request #409 from aclist/fix/beta-client-vdf
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: handle alternate client versions when parsing vdf
2026-07-15 11:29:54 +09:00
aclist 91773c933c chore: bump version 2026-07-15 11:26:01 +09:00
aclist 6a30ed44fc fix: handle alternate client versions when parsing vdf 2026-07-15 11:25:47 +09:00
aclist bbaec35a5f Merge pull request #408 from aclist/fix/conditional-warning
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
fix: make warning conditional when mods > 0
2026-07-14 17:40:09 +09:00
aclist c46477f0a7 fix: make warning conditional when mods > 0 2026-07-14 17:28:46 +09:00
aclist f47446a2e1 Merge pull request #407 from aclist/fix/parse-nsg-ids
fix: parse NSG ids
2026-07-14 17:18:22 +09:00
aclist cdb311cb1f chore: clear typehinting errors 2026-07-14 17:14:11 +09:00
aclist 11cdcb62b2 chore: drop unused import 2026-07-14 17:08:11 +09:00
aclist 1913f69e7b chore: drop unused import 2026-07-14 17:07:56 +09:00
aclist 8c2cea0fdc chore: bump version 2026-07-14 17:07:47 +09:00
aclist 8c29b9b35a fix: parse out-of-band appids as NSGs 2026-07-14 17:06:40 +09:00
11 changed files with 139 additions and 27 deletions

No files matched your search

+26 -10
View File
@@ -12,6 +12,7 @@ from typing import Any
from dzgui.api.steam import find_user_id_32 from dzgui.api.steam import find_user_id_32
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, IMAGES_PATH from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, IMAGES_PATH
from dzgui.util.dirs import copy_dzgui_to_xdg_data from dzgui.util.dirs import copy_dzgui_to_xdg_data
from dzgui.util.strings import unknown
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
@@ -46,6 +47,25 @@ class Shortcuts:
wrapped_exe = f'"{exe}"' wrapped_exe = f'"{exe}"'
return appname + wrapped_exe return appname + wrapped_exe
def find_appname_by_unsigned_id(self, appid: int) -> str:
# NOTE: bitmask signed int back to 32-bit CRC
# 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: def get_shortcuts(self) -> Any:
return self.shortcuts return self.shortcuts
@@ -111,22 +131,18 @@ class Shortcuts:
https://developer.valvesoftware.com/wiki/Add_Non-Steam_Game https://developer.valvesoftware.com/wiki/Add_Non-Steam_Game
https://developer.valvesoftware.com/wiki/Steam_Library_Shortcuts https://developer.valvesoftware.com/wiki/Steam_Library_Shortcuts
Wiki variously lists keys with title case and lowercase, but keys actually do Key case is not internally consistent and varies between client versions
not seem to be case-sensitive. Some entries generated by Steam do not match the wiki Most keys use Pascal case, but some do not
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
appid: signed int CRC 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 StartDir: directory the executable starts in, generally the parent
""" """
NEW_ENTRY: dict[str, Any] = {} NEW_ENTRY: dict[str, Any] = {}
NEW_ENTRY["appid"] = meta.appid NEW_ENTRY["appid"] = meta.appid
NEW_ENTRY["appname"] = meta.appname NEW_ENTRY["AppName"] = meta.appname
NEW_ENTRY["exe"] = f'"{meta.exe_path}"' NEW_ENTRY["Exe"] = f'"{meta.exe_path}"'
NEW_ENTRY["StartDir"] = f"{meta.start_dir}" NEW_ENTRY["StartDir"] = f"{meta.start_dir}"
NEW_ENTRY["icon"] = meta.icon NEW_ENTRY["icon"] = meta.icon
NEW_ENTRY["ShortcutPath"] = "" NEW_ENTRY["ShortcutPath"] = ""
@@ -134,7 +150,7 @@ class Shortcuts:
NEW_ENTRY["IsHidden"] = 0 NEW_ENTRY["IsHidden"] = 0
NEW_ENTRY["AllowDesktopConfig"] = 1 NEW_ENTRY["AllowDesktopConfig"] = 1
NEW_ENTRY["AllowOverlay"] = 1 NEW_ENTRY["AllowOverlay"] = 1
NEW_ENTRY["openvr"] = 0 NEW_ENTRY["OpenVR"] = 0
NEW_ENTRY["Devkit"] = 0 NEW_ENTRY["Devkit"] = 0
NEW_ENTRY["DevkitGameID"] = "" NEW_ENTRY["DevkitGameID"] = ""
NEW_ENTRY["DevkitOverrideAppID"] = 0 NEW_ENTRY["DevkitOverrideAppID"] = 0
+21 -9
View File
@@ -32,23 +32,25 @@ from dzgui.const.endpoints import (
) )
from dzgui.strings import wizard from dzgui.strings import wizard
from dzgui.util.bash import concat_bash_args from dzgui.util.bash import concat_bash_args
from dzgui.util.strings import unknown
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
class AppNotInstalledError(Exception): class AppNotInstalledError(Exception):
"""App not present in user's libraryfolders""" """App not present in user's libraryfolders"""
pass pass
class AppMovedError(Exception): class AppMovedError(Exception):
"""VDF points to a nonexistent location on disk""" """VDF points to a nonexistent location on disk"""
pass pass
class VDFLoadError(Exception): class VDFLoadError(Exception):
"""Malformed VDF or JSON conversion""" """Malformed VDF or JSON conversion"""
pass pass
@@ -203,21 +205,28 @@ def launch_offline(
proc = subprocess.run([*client_args, *params]) proc = subprocess.run([*client_args, *params])
return proc.returncode return proc.returncode
def find_loginusers(path: Path) -> Path:
return path / "config" / "loginusers.vdf"
def find_user_id(path: Path) -> str | None: def find_user_id(path: Path) -> str | None:
resolved_path = path / "config" / "loginusers.vdf" resolved_path = find_loginusers(path)
try: try:
with open(resolved_path, "r") as f: with open(resolved_path, "r") as f:
v = vdf.load(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"]: for user in v["users"]:
if v["users"][user]["MostRecent"] == "1": if v["users"][user]["MostRecent"] == "1":
return str(user) return str(user)
return None return None
except Exception as e: except Exception as e:
logger.warn(e) logger.debug(e)
return None return None
def find_user_id_32(path: Path) -> int: def find_user_id_32(path: Path) -> int:
uid = find_user_id(path) uid = find_user_id(path)
if uid is None: if uid is None:
@@ -322,9 +331,12 @@ def get_running_app() -> int | None:
def get_app_allows_downloads(path: Path, appid: int) -> bool: def get_app_allows_downloads(path: Path, appid: int) -> bool:
root_path = get_app_path(path, appid) try:
acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf") root_path = get_app_path(path, appid)
flag = ACF(acf).get_allows_downloads() acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf")
flag = ACF(acf).get_allows_downloads()
except AppNotInstalledError:
flag = 0
match flag: match flag:
# NOTE: adheres to global client setting # NOTE: adheres to global client setting
case 0: case 0:
@@ -403,13 +415,13 @@ def get_app_path(folders_path: Path, appid: int) -> Path:
return Path(app_path) return Path(app_path)
def get_app_name(appid: int) -> str: def get_app_name(appid: int) -> str | None:
payload = {"appids": [appid]} payload = {"appids": [appid]}
res = requests.get(APP_DETAILS, params=payload) res = requests.get(APP_DETAILS, params=payload)
if res.status_code != 200: if res.status_code != 200:
return unknown return None
try: try:
return str(res.json()[str(appid)]["data"]["name"]) return str(res.json()[str(appid)]["data"]["name"])
except Exception as e: except Exception as e:
logger.debug(e) logger.debug(e)
return unknown return None
+7
View File
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING
import dzgui.api.pefile as PeFile import dzgui.api.pefile as PeFile
import dzgui.api.servers as Servers import dzgui.api.servers as Servers
from dzgui.api.shortcuts import Shortcuts
from dzgui.api.steam import ( from dzgui.api.steam import (
connect, connect,
@@ -175,6 +176,12 @@ class ConnectionManager:
if running_app is not None: if running_app is not None:
allows_dl = get_app_allows_downloads(steam_path, running_app) allows_dl = get_app_allows_downloads(steam_path, running_app)
running_app_name = get_app_name(running_app) running_app_name = get_app_name(running_app)
# NOTE: for out-of-range appid, assume NSG.
# dzgui.api.steam.get_app_name() will return None
if running_app_name is None:
s = Shortcuts(steam_path)
# NOTE: returns "Unknown" if unparseable
running_app_name = s.find_appname_by_unsigned_id(running_app)
allows_downloads = (allows_dl, running_app_name) allows_downloads = (allows_dl, running_app_name)
else: else:
allows_downloads = (True, "") allows_downloads = (True, "")
+1 -1
View File
@@ -273,7 +273,7 @@ class PreConnectionAssistant(Gtk.Box):
) )
allows_dl, running_app = prereqs.allows_downloads allows_dl, running_app = prereqs.allows_downloads
if allows_dl is False: if len(prereqs.mods) > 0 and allows_dl is False:
msg = ( msg = (
f"The game '{running_app}' is currently running in Steam, but background downloads are not enabled.\n" f"The game '{running_app}' is currently running in Steam, but background downloads are not enabled.\n"
"Either stop the game first, or update your global Steam settings or the game's local settings.\n" "Either stop the game first, or update your global Steam settings or the game's local settings.\n"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
authors = [ authors = [
{name = "aclist"} {name = "aclist"}
] ]
version = "7.0.0b16" version = "7.0.0b18"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
license-files = ["LICENSE"] license-files = ["LICENSE"]
readme = "README.md" readme = "README.md"
+13
View File
@@ -0,0 +1,13 @@
"users"
{
"0"
{
"AccountName" "STEAMUSER"
"PersonaName" "STEAMUSER"
"RememberPassword" "0"
"WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0"
"AutoLogin" "1"
"Timestamp" "0"
}
}
+14
View File
@@ -0,0 +1,14 @@
"users"
{
"0"
{
"AccountName" "STEAMUSER"
"PersonaName" "STEAMUSER"
"RememberPassword" "0"
"WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0"
"AllowAutoLogin" "1"
"MostRecent" "1"
"Timestamp" "0"
}
}
@@ -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"
}
}
+25
View File
@@ -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
+1 -1
View File
@@ -12,7 +12,7 @@ from dzgui.api.steam import (
from dzgui.config.query import get_config from dzgui.config.query import get_config
from dzgui.config.xdg import get_xdg_paths, parse_filepaths from dzgui.config.xdg import get_xdg_paths, parse_filepaths
from dzgui.const.constants import APPID_DAYZ, LIBRARYFOLDERS_PATH from dzgui.const.constants import APPID_DAYZ
from tests.fixtures import fixture_path from tests.fixtures import fixture_path
+5 -5
View File
@@ -30,9 +30,9 @@ def test_no_shortcuts(monkeypatch) -> None:
@pytest.fixture @pytest.fixture
def dummy_app() -> None: def dummy_app() -> None:
d = { d = {
"appname": "TEST APP", "AppName": "TEST APP",
"StartDir": "TEST_DIR", "StartDir": "TEST_DIR",
"exe": "TEST_DIR/TEST_EXE.EXE", "Exe": "TEST_DIR/TEST_EXE.EXE",
"icon": "IMAGES_DIR/TEST_IMAGE.PNG", "icon": "IMAGES_DIR/TEST_IMAGE.PNG",
} }
return d return d
@@ -41,8 +41,8 @@ def dummy_app() -> None:
def test_wrap_exe(dummy_app) -> None: def test_wrap_exe(dummy_app) -> None:
s = Shortcuts(Path("")) s = Shortcuts(Path(""))
s.add_shortcut(*dummy_app.values()) s.add_shortcut(*dummy_app.values())
assert s.shortcuts["shortcuts"]["0"]["exe"][0] == '"' assert s.shortcuts["shortcuts"]["0"]["Exe"][0] == '"'
assert s.shortcuts["shortcuts"]["0"]["exe"][-1] == '"' assert s.shortcuts["shortcuts"]["0"]["Exe"][-1] == '"'
def test_add_shortcut(dummy_app) -> None: def test_add_shortcut(dummy_app) -> None:
@@ -51,7 +51,7 @@ def test_add_shortcut(dummy_app) -> None:
new = s.shortcuts["shortcuts"] new = s.shortcuts["shortcuts"]
ind = str(len(new) - 1) ind = str(len(new) - 1)
for k, v in dummy_app.items(): for k, v in dummy_app.items():
if k == "exe": if k == "Exe":
v = f'"{v}"' v = f'"{v}"'
assert new[ind][k] == v assert new[ind][k] == v