From 0ce3ab39159689327c906e62bb5207b7c90011fa Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:10:12 +0900 Subject: [PATCH 01/16] chore: add vdf dependency --- docs/licenses/vdf/LICENSE | 19 +++++++++++++++++++ docs/{ => sphinx}/Makefile | 0 docs/{ => sphinx}/README.md | 0 docs/{ => sphinx}/make.bat | 0 docs/{ => sphinx}/requirements.txt | 0 pyproject.toml | 1 + 6 files changed, 20 insertions(+) create mode 100644 docs/licenses/vdf/LICENSE rename docs/{ => sphinx}/Makefile (100%) rename docs/{ => sphinx}/README.md (100%) rename docs/{ => sphinx}/make.bat (100%) rename docs/{ => sphinx}/requirements.txt (100%) diff --git a/docs/licenses/vdf/LICENSE b/docs/licenses/vdf/LICENSE new file mode 100644 index 0000000..ee59795 --- /dev/null +++ b/docs/licenses/vdf/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 Rossen Georgiev + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/Makefile b/docs/sphinx/Makefile similarity index 100% rename from docs/Makefile rename to docs/sphinx/Makefile diff --git a/docs/README.md b/docs/sphinx/README.md similarity index 100% rename from docs/README.md rename to docs/sphinx/README.md diff --git a/docs/make.bat b/docs/sphinx/make.bat similarity index 100% rename from docs/make.bat rename to docs/sphinx/make.bat diff --git a/docs/requirements.txt b/docs/sphinx/requirements.txt similarity index 100% rename from docs/requirements.txt rename to docs/sphinx/requirements.txt diff --git a/pyproject.toml b/pyproject.toml index 0972486..0016735 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ dependencies = [ "psutil==7.1.3", "python-a2s", "requests==2.32.5", + "vdf==3.4" ] [project.scripts] From 33e367182eab7b390995120c31b8d16dc5ce6349 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:10:52 +0900 Subject: [PATCH 02/16] feat: ACF class --- dzgui/api/acf.py | 54 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/dzgui/api/acf.py b/dzgui/api/acf.py index 2518cd7..e676934 100644 --- a/dzgui/api/acf.py +++ b/dzgui/api/acf.py @@ -1,12 +1,18 @@ +import logging import re + from collections.abc import Iterator +from pathlib import Path from typing import Any from warnings import deprecated +from dzgui.const.constants import APP_NAME -@deprecated("Use dzgui.api.steam.unsubscribe()") -class WorkshopACF: - def __init__(self, file: str) -> None: +logger = logging.getLogger(APP_NAME) + + +class ACF: + def __init__(self, file: Path) -> None: super().__init__() self.dict: dict[str, Any] @@ -15,9 +21,10 @@ class WorkshopACF: def as_dict(self) -> dict[str, Any]: return self.dict - def load(self, file: str) -> None: + def load(self, file: Path) -> None: delimiter = r"\t\t" lines = [] + # TODO: illegal file handling with open(file, "r", encoding="utf-8") as f: for line in f: line = line.strip() @@ -27,6 +34,14 @@ class WorkshopACF: lines.append(els) self.dict = self.parse(iter(lines)) + @classmethod + def enquote(cls, s: str) -> str: + return f'"{s}"' + + @classmethod + def dequote(cls, s: str) -> str: + return s.rstrip('"').lstrip('"') + def parse(self, lines: Iterator[list[str]]) -> dict[str, str]: acf: dict[str, Any] = {} try: @@ -54,6 +69,24 @@ class WorkshopACF: except StopIteration: return acf + def get_allows_downloads(self) -> int | None: + try: + # TODO: consider coercing strs to ints on initial import + flag = int(self.dict["AppState"]["AllowOtherDownloadsWhileRunning"]) + return flag + except Exception as e: + logger.critical(e) + return None + + +@deprecated( + "Serializing workshop files is no longer supported, use dzgui.api.steam.unsubscribe" +) +class WorkshopACF(ACF): + def __init__(self, file: Path) -> None: + super().__init__(file) + + # TODO: break into workshop parser and generic acf parser def unpack(self, d: dict, lines: list[Any] = []) -> str: t1 = "AppWorkshop" t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails") @@ -78,18 +111,9 @@ class WorkshopACF: s += line + "\n" return s - def to_file(self, file: str) -> None: + def to_file(self, file: Path) -> None: s = self.unpack(self.dict) - with open(file, "w") as f: - f.write(s) - - @classmethod - def enquote(cls, s: str) -> str: - return f'"{s}"' - - @classmethod - def dequote(cls, s: str) -> str: - return s.rstrip('"').lstrip('"') + file.write_text(s) def delete(self, modid: int) -> None: del self.dict["AppWorkshop"]["WorkshopItemsInstalled"][modid] From 48f0f3913ceee0c03ccd99da5958a89710e93d40 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:11:07 +0900 Subject: [PATCH 03/16] chore: add reminder --- dzgui/api/pefile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dzgui/api/pefile.py b/dzgui/api/pefile.py index 3688cdb..d98bf3a 100644 --- a/dzgui/api/pefile.py +++ b/dzgui/api/pefile.py @@ -424,6 +424,7 @@ def get_pefile_path(steam_path: Path, appid: int) -> Path: return pe_path +# TODO: move to dzgui.api.steam def get_app_path(folders_path: Path, appid: int) -> Path: app_path = None From a91ce17501c26a732a6e7114ba50174e5ca766af Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:11:30 +0900 Subject: [PATCH 04/16] feat: config parsing POC --- dzgui/api/steam.py | 118 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 115 insertions(+), 3 deletions(-) diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index dc64529..f7214d6 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -1,17 +1,21 @@ import json import logging import os +import psutil import requests import subprocess -from typing import Union -from warnings import deprecated +import vdf from shlex import shlex from pathlib import Path +from typing import Any, Union +from warnings import deprecated +from dzgui.api.acf import ACF from dzgui.init.prereqs import has_steam_client from dzgui.const.constants import ( APPID_DAYZ, + APPID_DAYZ_EXP, APP_NAME, DEBIAN_STEAM_PATH, DEFAULT_STEAM_PATH, @@ -24,7 +28,6 @@ from dzgui.const.endpoints import SUB_ENDPOINT, STEAM_PUBLISHED_FILES, UNSUB_END from dzgui.strings import wizard from dzgui.util.bash import concat_bash_args - logger = logging.getLogger(APP_NAME) @@ -275,3 +278,112 @@ def gen_shortcut() -> None: def enqueue_mod(client: str, mod: str, appid: int) -> None: client_args = concat_bash_args(client) subprocess.Popen([*client_args, "+workshop_download_item", str(appid), mod]) + + +@deprecated("Cf. https://github.com/ValveSoftware/steam-for-linux/issues/9672") +def get_registry() -> dict[str, Any] | None: + home = os.getenv("HOME") + try: + with open(f"{home}/.steam/registry.vdf") as f: + registry = vdf.load(f) + return registry + except Exception: + logger.critical(e) + return None + + +def _is_dayz_running() -> bool: + registry = get_registry() + if registry is None: + return False + apps = registry["Registry"]["HKCU"]["Software"]["Valve"]["Steam"]["apps"].items() + for app in apps: + k, v = app + if k in (APPID_DAYZ, APPID_DAYZ_EXP): + try: + state = v["Running"] + # NOTE: 0 denotes False + return bool(int(state)) + except Exception as e: + logger.critical(e) + return False + return False + + +def _get_running_app() -> int | None: + registry = get_registry() + if registry is None: + return None + try: + return registry["Registry"]["HKCU"]["Software"]["Valve"]["Steam"][ + "RunningAppID" + ] + except Exception: + return None + + +# TODO: write tests +def get_running_app() -> int | None: + PROC_NAME = "steam" + SUBPROC_NAME = "reaper" + FLAG = "AppId" + + for proc in psutil.process_iter(): + if proc.name() == PROC_NAME: + subprocs = proc.children() + filtered = (proc for proc in subprocs if proc.name() == SUBPROC_NAME) + try: + proc = next(filtered) + except StopIteration: + return None + args = proc.cmdline() + appid = (row for row in args if FLAG in row) + try: + return next(appid).split("=")[1] + except StopIteration: + return None + return None + + +# TODO: write tests +def get_app_allows_downloads(path: Path, appid: int) -> bool: + # TODO: move PeFile.get_app_path() + # TODO: root_path = PeFile.get_app_path(Preferences.DEFAULT, appid) + # acf = "{root_path}/appmanifest_{aid}.acf" + flag = ACF(acf).get_allows_downloads() + match flag: + # NOTE: adheres to global client setting + case 0: + return get_client_allows_downloads(Preferences.DEFAULT) + # NOTE: always allow + case 1: + return True + # NOTE: never allow + case 2: + return False + case _: + return True + + +def get_client_allows_downloads(path: Path) -> bool: + config = path.joinpath("config/config.vdf") + try: + with open(config) as f: + settings = vdf.load(f) + # NOTE: "1" denotes "allow" + allow = bool( + int( + settings["InstallConfigStore"]["Software"]["Valve"]["Steam"][ + "AllowDownloadsDuringGameplay" + ] + ) + ) + return allow + except Exception as e: + logger.critical(e) + return True + + +def is_dayz_running() -> bool: + appid = get_running_app() + return appid in (APPID_DAYZ, APPID_DAYZ_EXP) From 09ef0f4f9a4414412aa79bdfc55abf37572da290 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:11:40 +0900 Subject: [PATCH 05/16] chore: add reminder --- dzgui/init/proc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dzgui/init/proc.py b/dzgui/init/proc.py index 1c97f74..77aefcd 100644 --- a/dzgui/init/proc.py +++ b/dzgui/init/proc.py @@ -71,6 +71,7 @@ def has_cmd(cmd: str) -> bool: return False +# TODO: drop, completely superseded by new methods @deprecated("dropped in favor of Gtk native methods") def foreground(cmd: str, pid: int) -> None: if cmd == "wmctrl": From d2042a3dde44769524897517953d86a404b6033e Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:38:14 +0900 Subject: [PATCH 06/16] fix: clear typehinting errors --- dzgui/api/steam.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index f7214d6..4527398 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -287,7 +287,7 @@ def get_registry() -> dict[str, Any] | None: with open(f"{home}/.steam/registry.vdf") as f: registry = vdf.load(f) return registry - except Exception: + except Exception as e: logger.critical(e) return None @@ -339,7 +339,7 @@ def get_running_app() -> int | None: args = proc.cmdline() appid = (row for row in args if FLAG in row) try: - return next(appid).split("=")[1] + return str(next(appid).split("=")[1]) except StopIteration: return None return None From 3ed4747d08321ca9214b528f92dafdeb9d12d734 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:38:25 +0900 Subject: [PATCH 07/16] chore: drop unused import --- tests/test_bare_conf_files.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_bare_conf_files.py b/tests/test_bare_conf_files.py index f55765e..052b829 100644 --- a/tests/test_bare_conf_files.py +++ b/tests/test_bare_conf_files.py @@ -1,6 +1,5 @@ import pytest import tempfile -import os from dzgui.app_init import copy_bare_configs from dzgui.config.xdg import get_xdg_paths, parse_filepaths From f4aa6c749ef8d7b37b848c95c6090ec892f7f47f Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 7 Jul 2026 19:36:44 +0900 Subject: [PATCH 08/16] chore: move get_app_path() --- dzgui/api/mods.py | 4 ++-- dzgui/api/pefile.py | 23 ++------------------ dzgui/api/steam.py | 50 +++++++++++++++++++++++++++++++++++++++++--- dzgui/init/dayz.py | 4 ++-- tests/test_pefile.py | 6 +++--- 5 files changed, 56 insertions(+), 31 deletions(-) diff --git a/dzgui/api/mods.py b/dzgui/api/mods.py index 4089859..b1b5a71 100644 --- a/dzgui/api/mods.py +++ b/dzgui/api/mods.py @@ -7,7 +7,7 @@ from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass from pathlib import Path -import dzgui.api.pefile as PeFile +from dzgui.api.steam import get_app_path from dzgui.api.servers import get_rules, fqip_to_record from dzgui.const.constants import ( APP_NAME, @@ -40,7 +40,7 @@ def get_local_mod_ids(steam_path: Path) -> list[int]: def get_local_mod_path(steam_path: Path) -> Path: - p = PeFile.get_app_path(steam_path / Path(LIBRARYFOLDERS_PATH), APPID_DAYZ) + p = get_app_path(steam_path / Path(LIBRARYFOLDERS_PATH), APPID_DAYZ) workshop_path = p / WORKSHOP_PATH return workshop_path diff --git a/dzgui/api/pefile.py b/dzgui/api/pefile.py index d98bf3a..d651689 100644 --- a/dzgui/api/pefile.py +++ b/dzgui/api/pefile.py @@ -14,7 +14,7 @@ from dzgui.const.constants import ( DAYZ_BINARY, LIBRARYFOLDERS_PATH, ) -from dzgui.api.steam import vdf2json +from dzgui.api.steam import get_app_path # https://learn.microsoft.com/en-us/windows/win32/debug/pe-format endian = "<" @@ -257,25 +257,6 @@ class PeFileError(Exception): pass - -class AppNotInstalledError(Exception): - """App not present in user's libraryfolders""" - - pass - - -class AppMovedError(Exception): - """VDF points to a nonexistent location on disk""" - - pass - - -class VDFLoadError(Exception): - """Malformed VDF or JSON conversion""" - - pass - - def parse_version_number(data: BinaryIO) -> FileVersion: # https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo minor = struct.unpack("> 16 & 0xFFFF @@ -425,7 +406,7 @@ def get_pefile_path(steam_path: Path, appid: int) -> Path: # TODO: move to dzgui.api.steam -def get_app_path(folders_path: Path, appid: int) -> Path: +def _get_app_path(folders_path: Path, appid: int) -> Path: app_path = None try: diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index 4527398..a6ef4a3 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -31,6 +31,24 @@ from dzgui.util.bash import concat_bash_args logger = logging.getLogger(APP_NAME) +class AppNotInstalledError(Exception): + """App not present in user's libraryfolders""" + + pass + + +class AppMovedError(Exception): + """VDF points to a nonexistent location on disk""" + + pass + + +class VDFLoadError(Exception): + """Malformed VDF or JSON conversion""" + + pass + + def get_steam_paths() -> list[tuple[Path, str]]: paths = [] if has_steam_client(): @@ -347,9 +365,8 @@ def get_running_app() -> int | None: # TODO: write tests def get_app_allows_downloads(path: Path, appid: int) -> bool: - # TODO: move PeFile.get_app_path() - # TODO: root_path = PeFile.get_app_path(Preferences.DEFAULT, appid) - # acf = "{root_path}/appmanifest_{aid}.acf" + root_path = get_app_path(Preferences.DEFAULT, appid) + acf = f"{root_path}/appmanifest_{aid}.acf" flag = ACF(acf).get_allows_downloads() match flag: # NOTE: adheres to global client setting @@ -387,3 +404,30 @@ def get_client_allows_downloads(path: Path) -> bool: def is_dayz_running() -> bool: appid = get_running_app() return appid in (APPID_DAYZ, APPID_DAYZ_EXP) + + +def get_app_path(folders_path: Path, appid: int) -> Path: + app_path = None + + try: + with open(folders_path) as f: + folders = vdf.load(f) + except Exception: + raise VDFLoadError("Failed to parse libraryfolders") + + for obj in folders["libraryfolders"]: + if str(appid) in folders["libraryfolders"][obj]["apps"]: + app_path = folders["libraryfolders"][obj]["path"] + if Path(app_path).exists(): + break + + if app_path is None: + raise AppNotInstalledError( + f"Failed to find a libraryfolder for the appid {appid}" + ) + if Path(app_path).exists() is False: + raise AppMovedError( + f"The location '{app_path}' pointed to by '{appid}' no longer exists and may have been changed on the disk." + ) + + return Path(app_path) diff --git a/dzgui/init/dayz.py b/dzgui/init/dayz.py index 66f60d9..b83fb45 100644 --- a/dzgui/init/dayz.py +++ b/dzgui/init/dayz.py @@ -2,11 +2,11 @@ import logging from pathlib import Path +from dzgui.api.steam import get_app_path from dzgui.config.query import lookup from dzgui.const.constants import APPID_DAYZ, APP_NAME, LIBRARYFOLDERS_PATH from dzgui.const.enum import Preferences -import dzgui.api.pefile as PeFile logger = logging.getLogger(APP_NAME) @@ -14,7 +14,7 @@ logger = logging.getLogger(APP_NAME) def is_dayz_installed(config: Path) -> None: try: path = lookup(config, Preferences.DEFAULT) - PeFile.get_app_path(Path(path) / LIBRARYFOLDERS_PATH, APPID_DAYZ) + get_app_path(Path(path) / LIBRARYFOLDERS_PATH, APPID_DAYZ) except Exception as e: logger.critical(e) raise e diff --git a/tests/test_pefile.py b/tests/test_pefile.py index 47edc2e..6f02557 100644 --- a/tests/test_pefile.py +++ b/tests/test_pefile.py @@ -2,7 +2,7 @@ import pytest from pathlib import Path import dzgui.api.pefile as PeFile -from dzgui.api.pefile import VDFLoadError, AppNotInstalledError, AppMovedError +from dzgui.api.steam import VDFLoadError, AppNotInstalledError, AppMovedError, get_app_path from dzgui.config.query import get_config from dzgui.config.xdg import get_xdg_paths, parse_filepaths @@ -56,11 +56,11 @@ def test_not_in_library(fixture, exception): fixture = fixture_path(fixture) with pytest.raises(exception): try: - PeFile.get_app_path(fixture, APPID_DAYZ) + get_app_path(fixture, APPID_DAYZ) except Exception as e: raise e def test_on_second_drive(second_drive): - path = PeFile.get_app_path(second_drive, APPID_DAYZ) + path = get_app_path(second_drive, APPID_DAYZ) assert path == Path("/tmp") From e8d991ccb5c93201c43d14c82c6d7a47b704ecb0 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 7 Jul 2026 19:43:00 +0900 Subject: [PATCH 09/16] chore: drop legacy functions --- dzgui/api/pefile.py | 26 ---------------- dzgui/api/steam.py | 57 +++++++----------------------------- dzgui/init/proc.py | 38 ------------------------ dzgui/managers/connection.py | 3 +- dzgui/managers/offline.py | 3 +- 5 files changed, 14 insertions(+), 113 deletions(-) diff --git a/dzgui/api/pefile.py b/dzgui/api/pefile.py index d651689..e2bf676 100644 --- a/dzgui/api/pefile.py +++ b/dzgui/api/pefile.py @@ -405,32 +405,6 @@ def get_pefile_path(steam_path: Path, appid: int) -> Path: return pe_path -# TODO: move to dzgui.api.steam -def _get_app_path(folders_path: Path, appid: int) -> Path: - app_path = None - - try: - j = json.loads(vdf2json(folders_path)) - except Exception: - raise VDFLoadError("Failed to parse libraryfolders") - - for obj in j["libraryfolders"]: - if str(appid) in j["libraryfolders"][obj]["apps"]: - app_path = j["libraryfolders"][obj]["path"] - if Path(app_path).exists(): - break - - if app_path is None: - raise AppNotInstalledError( - f"Failed to find a libraryfolder for the appid {appid}" - ) - if Path(app_path).exists() is False: - raise AppMovedError( - f"The location '{app_path}' pointed to by '{appid}' no longer exists and may have been changed on the disk." - ) - - return Path(app_path) - def get_pretty_version(steam_path: Path, appid: int) -> str | None: try: diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index a6ef4a3..c53fb44 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -204,57 +204,17 @@ def launch_offline( def find_user_id(path: Path) -> str | None: resolved_path = path / "config" / "loginusers.vdf" try: - vdf = vdf2json(resolved_path) - j = json.loads(vdf) - for user in j["users"]: - if j["users"][user]["MostRecent"] == "1": - return str(user) - return None + with open(resolved_path, "r") as f: + v = vdf.load(f) + for user in v["users"]: + if v["users"][user]["MostRecent"] == "1": + return str(user) + return None except Exception as e: logger.warn(e) return None -def vdf2json(path: Path) -> str: - def _istr(indent: int, string: str) -> str: - return (indent * " ") + string - - jbuf = "{\n" - indent = 1 - - with open(path, "r") as f: - st = f.read() - lex = shlex(st) - - while True: - tok = lex.get_token() - if not tok: - return jbuf + "}\n" - if tok == "}": - indent -= 1 - jbuf += _istr(indent, "}") - ntok = lex.get_token() - if ntok is not None: - lex.push_token(ntok) - if ntok and ntok != "}": - jbuf += "," - jbuf += "\n" - else: - ntok = lex.get_token() - if ntok == "{": - jbuf += _istr(indent, tok + ": {\n") - indent += 1 - else: - if ntok is not None: - jbuf += _istr(indent, tok + ": " + ntok) - ntok = lex.get_token() - if ntok is not None: - lex.push_token(ntok) - if ntok != "}": - jbuf += "," - jbuf += "\n" - - def update_workshop(key: str, mod: int, endpoint: str) -> None: payload: dict[str, Union[int, str]] = { "publishedfileid": mod, @@ -341,6 +301,7 @@ def _get_running_app() -> int | None: # TODO: write tests +# TODO: consider moving to proc module def get_running_app() -> int | None: PROC_NAME = "steam" SUBPROC_NAME = "reaper" @@ -431,3 +392,7 @@ def get_app_path(folders_path: Path, appid: int) -> Path: ) return Path(app_path) + + +p = Path("/home/ncase/.local/share/Steam") +print(find_user_id(p)) diff --git a/dzgui/init/proc.py b/dzgui/init/proc.py index 77aefcd..51678fd 100644 --- a/dzgui/init/proc.py +++ b/dzgui/init/proc.py @@ -20,21 +20,6 @@ from dzgui.util.format import format_exception logger = logging.getLogger(APP_NAME) -# TODO: move to util.proc -def is_dayz_running() -> bool: - """Subprocesses spawned from Steam will not show up in regular process tree""" - procs = [] - substring = DAYZ_BINARY - for proc in psutil.process_iter(): - try: - procs.append(proc.cmdline()) - except Exception as e: - msg = format_exception(e) - logger.warning(msg) - continue - return any(substring in item for sublist in procs for item in sublist) - - def is_steam_running(cmd: str) -> bool: if cmd == STEAM_CMD: if has_cmd(STEAM_CMD) is False: @@ -69,26 +54,3 @@ def has_cmd(cmd: str) -> bool: if shutil.which(cmd) is not None: return True return False - - -# TODO: drop, completely superseded by new methods -@deprecated("dropped in favor of Gtk native methods") -def foreground(cmd: str, pid: int) -> None: - if cmd == "wmctrl": - proc = subprocess.run(["wmctrl", "-ilp"], capture_output=True, text=True) - lines = proc.stdout.splitlines() - for line in lines: - els = line.split(" ") - if str(pid) in els: - wid = els[0] - break - subprocess.run(["wmctrl", "-ia", wid]) - elif cmd == "xdotool": - args = [cmd, "search", "--pid", str(pid)] - proc = subprocess.run([*args], capture_output=True, text=True) - lines = proc.stdout.splitlines() - ## NOTE: some forked subprocesses may fail, so skip over them - for line in lines: - subprocess.run( - ["xdotool", "windowactivate", line], stderr=subprocess.DEVNULL - ) diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index 643dffd..1f28172 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -32,8 +32,9 @@ from dzgui.const.constants import ( APPNAME_DAYZ, APPNAME_DAYZ_EXP_HUMAN, ) +from dzgui.api.steam import is_dayz_running from dzgui.const.enum import NotebookPage, Preferences -from dzgui.init.proc import is_dayz_running, is_steam_running +from dzgui.init.proc import is_steam_running from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.strings.dialogs import ( waiting_for_launch, diff --git a/dzgui/managers/offline.py b/dzgui/managers/offline.py index a63d42b..cf23527 100644 --- a/dzgui/managers/offline.py +++ b/dzgui/managers/offline.py @@ -5,10 +5,9 @@ from typing import Callable, TYPE_CHECKING, Union import dzgui.api.pefile as PeFile from dzgui.api.mods import is_mission, get_custom_mods -from dzgui.api.steam import launch_offline +from dzgui.api.steam import is_dayz_running, launch_offline from dzgui.const.constants import APP_NAME, APPID_DAYZ_EXP from dzgui.const.enum import Preferences -from dzgui.init.proc import is_dayz_running from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager from dzgui.model.model_factory import ModelFactory from dzgui.strings import dialogs From b731fb5566aaf5116044baf703bf551599c2f0b1 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 7 Jul 2026 19:43:41 +0900 Subject: [PATCH 10/16] chore: drop test code --- dzgui/api/steam.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index c53fb44..fdfe29b 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -392,7 +392,3 @@ def get_app_path(folders_path: Path, appid: int) -> Path: ) return Path(app_path) - - -p = Path("/home/ncase/.local/share/Steam") -print(find_user_id(p)) From a2d1f9b0e185c80b2f023b8db49f84ed7517432c Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Tue, 7 Jul 2026 19:52:44 +0900 Subject: [PATCH 11/16] chore: simplify path concatenation logic --- dzgui/api/mods.py | 3 +-- dzgui/api/pefile.py | 3 +-- dzgui/api/steam.py | 9 +++++---- dzgui/init/dayz.py | 4 ++-- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/dzgui/api/mods.py b/dzgui/api/mods.py index b1b5a71..c6a076d 100644 --- a/dzgui/api/mods.py +++ b/dzgui/api/mods.py @@ -13,7 +13,6 @@ from dzgui.const.constants import ( APP_NAME, APPID_DAYZ, DAYZ_COMMUNITY_ROOT, - LIBRARYFOLDERS_PATH, WORKSHOP_PATH, ) @@ -40,7 +39,7 @@ def get_local_mod_ids(steam_path: Path) -> list[int]: def get_local_mod_path(steam_path: Path) -> Path: - p = get_app_path(steam_path / Path(LIBRARYFOLDERS_PATH), APPID_DAYZ) + p = get_app_path(steam_path, APPID_DAYZ) workshop_path = p / WORKSHOP_PATH return workshop_path diff --git a/dzgui/api/pefile.py b/dzgui/api/pefile.py index e2bf676..4e77522 100644 --- a/dzgui/api/pefile.py +++ b/dzgui/api/pefile.py @@ -12,7 +12,6 @@ from dzgui.const.constants import ( APPNAME_DAYZ, APPNAME_DAYZ_EXP, DAYZ_BINARY, - LIBRARYFOLDERS_PATH, ) from dzgui.api.steam import get_app_path @@ -400,7 +399,7 @@ def get_pefile_path(steam_path: Path, appid: int) -> Path: name = identifier[appid] binary = DAYZ_BINARY - app_path = get_app_path(steam_path / LIBRARYFOLDERS_PATH, appid) + app_path = get_app_path(steam_path, appid) pe_path = app_path / f"steamapps/common/{name}/{binary}" return pe_path diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index fdfe29b..d452c81 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -20,6 +20,7 @@ from dzgui.const.constants import ( DEBIAN_STEAM_PATH, DEFAULT_STEAM_PATH, FLATPAK_STEAM_PATH, + LIBRARYFOLDERS_PATH, UBUNTU_STEAM_PATH, REQUEST_TIMEOUT, VDF_PATH, @@ -326,13 +327,13 @@ def get_running_app() -> int | None: # TODO: write tests def get_app_allows_downloads(path: Path, appid: int) -> bool: - root_path = get_app_path(Preferences.DEFAULT, appid) - acf = f"{root_path}/appmanifest_{aid}.acf" + root_path = get_app_path(path, appid) + acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf") flag = ACF(acf).get_allows_downloads() match flag: # NOTE: adheres to global client setting case 0: - return get_client_allows_downloads(Preferences.DEFAULT) + return get_client_allows_downloads(path) # NOTE: always allow case 1: return True @@ -371,7 +372,7 @@ def get_app_path(folders_path: Path, appid: int) -> Path: app_path = None try: - with open(folders_path) as f: + with open(folders_path / LIBRARYFOLDERS_PATH) as f: folders = vdf.load(f) except Exception: raise VDFLoadError("Failed to parse libraryfolders") diff --git a/dzgui/init/dayz.py b/dzgui/init/dayz.py index b83fb45..01bc271 100644 --- a/dzgui/init/dayz.py +++ b/dzgui/init/dayz.py @@ -4,7 +4,7 @@ from pathlib import Path from dzgui.api.steam import get_app_path from dzgui.config.query import lookup -from dzgui.const.constants import APPID_DAYZ, APP_NAME, LIBRARYFOLDERS_PATH +from dzgui.const.constants import APPID_DAYZ, APP_NAME from dzgui.const.enum import Preferences @@ -14,7 +14,7 @@ logger = logging.getLogger(APP_NAME) def is_dayz_installed(config: Path) -> None: try: path = lookup(config, Preferences.DEFAULT) - get_app_path(Path(path) / LIBRARYFOLDERS_PATH, APPID_DAYZ) + get_app_path(Path(path), APPID_DAYZ) except Exception as e: logger.critical(e) raise e From eff31e2bd3b6f9fe98a15f832d33d25b5eecd19e Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:07:16 +0900 Subject: [PATCH 12/16] chore: clear typehinting errors --- dzgui/api/pefile.py | 2 -- dzgui/api/steam.py | 12 +++++------- dzgui/init/proc.py | 5 ----- dzgui/managers/mods.py | 2 +- 4 files changed, 6 insertions(+), 15 deletions(-) diff --git a/dzgui/api/pefile.py b/dzgui/api/pefile.py index 4e77522..7c86987 100644 --- a/dzgui/api/pefile.py +++ b/dzgui/api/pefile.py @@ -1,4 +1,3 @@ -import json import struct from dataclasses import dataclass @@ -404,7 +403,6 @@ def get_pefile_path(steam_path: Path, appid: int) -> Path: return pe_path - def get_pretty_version(steam_path: Path, appid: int) -> str | None: try: pe_file_path = get_pefile_path(steam_path, appid) diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index d452c81..9c4a941 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -1,12 +1,10 @@ -import json import logging import os import psutil import requests import subprocess -import vdf +import vdf # type: ignore -from shlex import shlex from pathlib import Path from typing import Any, Union from warnings import deprecated @@ -260,7 +258,7 @@ def enqueue_mod(client: str, mod: str, appid: int) -> None: @deprecated("Cf. https://github.com/ValveSoftware/steam-for-linux/issues/9672") -def get_registry() -> dict[str, Any] | None: +def get_registry() -> Any | None: home = os.getenv("HOME") try: with open(f"{home}/.steam/registry.vdf") as f: @@ -294,9 +292,9 @@ def _get_running_app() -> int | None: if registry is None: return None try: - return registry["Registry"]["HKCU"]["Software"]["Valve"]["Steam"][ + return int(registry["Registry"]["HKCU"]["Software"]["Valve"]["Steam"][ "RunningAppID" - ] + ]) except Exception: return None @@ -319,7 +317,7 @@ def get_running_app() -> int | None: args = proc.cmdline() appid = (row for row in args if FLAG in row) try: - return str(next(appid).split("=")[1]) + return int(next(appid).split("=")[1]) except StopIteration: return None return None diff --git a/dzgui/init/proc.py b/dzgui/init/proc.py index 51678fd..faca556 100644 --- a/dzgui/init/proc.py +++ b/dzgui/init/proc.py @@ -3,11 +3,8 @@ import subprocess import shutil import logging -from warnings import deprecated - from dzgui.const.constants import ( APP_NAME, - DAYZ_BINARY, STEAM_CMD, FLATPAK_APPID, FLATPAK_CMD, @@ -15,8 +12,6 @@ from dzgui.const.constants import ( FLATPAK_SANDBOX, ) -from dzgui.util.format import format_exception - logger = logging.getLogger(APP_NAME) diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py index d609378..a28ce28 100644 --- a/dzgui/managers/mods.py +++ b/dzgui/managers/mods.py @@ -141,7 +141,7 @@ class ModManager: app_path_exp = PeFile.get_nested_app_path(steam_path, APPID_DAYZ_EXP) symlink = app_path_exp / md5 symlink.unlink() - except PeFile.AppNotInstalledError: + except Exception: pass time.sleep(API_RATE_LIMIT) From 8652259053c499ce2ca43ec0ff625768bd266e8d Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:58:12 +0900 Subject: [PATCH 13/16] feat: convert appid to name --- dzgui/api/steam.py | 26 ++++++++++++++++++++++---- dzgui/const/endpoints.py | 1 + 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index 9c4a941..2c6799a 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -23,9 +23,15 @@ from dzgui.const.constants import ( REQUEST_TIMEOUT, VDF_PATH, ) -from dzgui.const.endpoints import SUB_ENDPOINT, STEAM_PUBLISHED_FILES, UNSUB_ENDPOINT +from dzgui.const.endpoints import ( + APP_DETAILS, + SUB_ENDPOINT, + STEAM_PUBLISHED_FILES, + UNSUB_ENDPOINT, +) from dzgui.strings import wizard from dzgui.util.bash import concat_bash_args +from dzgui.util.strings import unknown logger = logging.getLogger(APP_NAME) @@ -292,9 +298,9 @@ def _get_running_app() -> int | None: if registry is None: return None try: - return int(registry["Registry"]["HKCU"]["Software"]["Valve"]["Steam"][ - "RunningAppID" - ]) + return int( + registry["Registry"]["HKCU"]["Software"]["Valve"]["Steam"]["RunningAppID"] + ) except Exception: return None @@ -391,3 +397,15 @@ def get_app_path(folders_path: Path, appid: int) -> Path: ) return Path(app_path) + + +def get_app_name(appid: int) -> str: + payload = {"appids": [appid]} + res = requests.get(APP_DETAILS, params=payload) + if res.status_code != 200: + return unknown + try: + return res.json()[str(appid)]["data"]["name"] + except Exception as e: + logger.debug(e) + return unknown diff --git a/dzgui/const/endpoints.py b/dzgui/const/endpoints.py index 9d15b02..26a50f8 100644 --- a/dzgui/const/endpoints.py +++ b/dzgui/const/endpoints.py @@ -5,6 +5,7 @@ STEAM_PUBLISHED_FILES = ( STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1" SUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Subscribe/v1" UNSUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/v1" +APP_DETAILS = "https://store.steampowered.com/api/appdetails?" BM_SERVERS = "https://api.battlemetrics.com/servers?" GITHUB = "https://github.com/aclist" From 6a3b4b2bfb72f2a76d55865d7cf3bfbb0b1996bc Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:58:40 +0900 Subject: [PATCH 14/16] fix: cast result to str --- dzgui/api/steam.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index 2c6799a..51197e5 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -405,7 +405,7 @@ def get_app_name(appid: int) -> str: if res.status_code != 200: return unknown try: - return res.json()[str(appid)]["data"]["name"] + return str(res.json()[str(appid)]["data"]["name"]) except Exception as e: logger.debug(e) return unknown From fc86b75ca1809bcadf6cbd7cb6674a89fd1fc817 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 9 Jul 2026 12:37:09 +0900 Subject: [PATCH 15/16] fix: convert str to Path --- dzgui/managers/mods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py index a28ce28..da31385 100644 --- a/dzgui/managers/mods.py +++ b/dzgui/managers/mods.py @@ -60,7 +60,7 @@ class ModManager: @call_on_thread(dialogs.fetching_mods) def load_mods(self) -> None: - mods = get_delimited_mods(self.path) + mods = get_delimited_mods(Path(self.path)) if len(mods) < 1: msg = self.format_mod_statusbar() func = StoredFunc(lambda: self.emitter.emit("mods_updated", msg, 0)) From aa43b017d100999d0f3e744f61bf1e3d83ca0fda Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Thu, 9 Jul 2026 12:37:23 +0900 Subject: [PATCH 16/16] chore: add note --- dzgui/api/steam.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index 51197e5..10f1ea6 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -344,6 +344,7 @@ def get_app_allows_downloads(path: Path, appid: int) -> bool: # NOTE: never allow case 2: return False + # NOTE: no other known values at this time case _: return True