mirror of
https://github.com/aclist/dztui.git
synced 2026-08-30 11:47:17 +02:00
Compare commits
54 Commits
e92df8c062
...
e9d4054e76
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9d4054e76 | ||
|
|
1c9e026f70 | ||
|
|
eb076b7dcd | ||
|
|
8629e02670 | ||
|
|
3ba38f30ad | ||
|
|
7c37fd8b6e | ||
|
|
1a065123bb | ||
|
|
2586306323 | ||
|
|
1ccb99d935 | ||
|
|
054fe968d1 | ||
|
|
1760e6e928 | ||
|
|
a2a39e9927 | ||
|
|
e664f68624 | ||
|
|
2e41856348 | ||
|
|
27d444ea6f | ||
|
|
26a2362172 | ||
|
|
50cf347164 | ||
|
|
dd69bc88b1 | ||
|
|
5c84b5ae0a | ||
|
|
6fdf5547dc | ||
|
|
fc1311579c | ||
|
|
da993db2e2 | ||
|
|
207d8fd407 | ||
|
|
72b10ad543 | ||
|
|
35680b1373 | ||
|
|
5121e6d005 | ||
|
|
2f61cc7ff7 | ||
|
|
8733094267 | ||
|
|
9d14b7f219 | ||
|
|
0084af4959 | ||
|
|
caf071f997 | ||
|
|
b50d061e1f | ||
|
|
e913c95c70 | ||
|
|
d6bf63886e | ||
|
|
ec8f6a3d36 | ||
|
|
b8fbbbb1cd | ||
|
|
fb2772cde2 | ||
|
|
6c36b741f4 | ||
|
|
aa43b017d1 | ||
|
|
fc86b75ca1 | ||
|
|
6a3b4b2bfb | ||
|
|
8652259053 | ||
|
|
eff31e2bd3 | ||
|
|
a2d1f9b0e1 | ||
|
|
b731fb5566 | ||
|
|
e8d991ccb5 | ||
|
|
f4aa6c749e | ||
|
|
3ed4747d08 | ||
|
|
d2042a3dde | ||
|
|
09ef0f4f9a | ||
|
|
a91ce17501 | ||
|
|
48f0f3913c | ||
|
|
33e367182e | ||
|
|
0ce3ab3915 |
59
CHANGELOG.md
59
CHANGELOG.md
@ -1,6 +1,7 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Added
|
## [7.0.0] Unreleased
|
||||||
|
### Added
|
||||||
- Setup wizard
|
- Setup wizard
|
||||||
- Changelog text wrapping and formatting
|
- Changelog text wrapping and formatting
|
||||||
- Changelog ships with source
|
- Changelog ships with source
|
||||||
@ -41,8 +42,10 @@
|
|||||||
- Choose to jump into splash screen instead of server
|
- Choose to jump into splash screen instead of server
|
||||||
- Collapsible connection panel
|
- Collapsible connection panel
|
||||||
- Play offline (load mods directly)
|
- Play offline (load mods directly)
|
||||||
|
- Generate Steam shortcuts and cover art
|
||||||
|
- Warn user if background downloads are disabled
|
||||||
|
|
||||||
## Changed
|
### Changed
|
||||||
- Conform to PEP 440 versioning for beta versions
|
- Conform to PEP 440 versioning for beta versions
|
||||||
- Reduce padding on keys button
|
- Reduce padding on keys button
|
||||||
- Boldface breadcrumbs
|
- Boldface breadcrumbs
|
||||||
@ -59,58 +62,58 @@
|
|||||||
- Enable LAN page Empty/Full filters on first run of app
|
- Enable LAN page Empty/Full filters on first run of app
|
||||||
- Propagate subscribed mods to Steam client
|
- Propagate subscribed mods to Steam client
|
||||||
|
|
||||||
## Dropped
|
### Dropped
|
||||||
- Debug mode
|
- Debug mode
|
||||||
- Branch switching
|
- Branch switching
|
||||||
- Manual mod install mode (describe rationale)
|
- Manual mod install mode (describe rationale)
|
||||||
- Force update mods
|
- Force update mods
|
||||||
|
|
||||||
## Fixed
|
### Fixed
|
||||||
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
|
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
|
||||||
- Center server title text on server dialogs
|
- Center server title text on server dialogs
|
||||||
- Rare segfaults when changing maps (threading)
|
- Rare segfaults when changing maps (threading)
|
||||||
- Moved dialogs out of threads
|
- Moved dialogs out of threads
|
||||||
|
|
||||||
## Unreleased
|
### Unreleased
|
||||||
- Load offline mods
|
- Load offline mods
|
||||||
- Local documentation
|
- Local documentation
|
||||||
- Raw debug command in context menu
|
- Raw debug command in context menu
|
||||||
|
|
||||||
## Developer-facing
|
### Developer-facing
|
||||||
- Add pyproject.toml file
|
- Add pyproject.toml file
|
||||||
- Show deprecation warnings
|
- Show deprecation warnings
|
||||||
- Options -> Dev page
|
- Options -> Dev page
|
||||||
|
|
||||||
## [6.0.5] 2026-05-17
|
## [6.0.5] 2026-05-17
|
||||||
## Changed
|
### Changed
|
||||||
- Update symlink hash logic to align with DZGUI 7 beta
|
- Update symlink hash logic to align with DZGUI 7 beta
|
||||||
- Update A2S helper to latest commit
|
- Update A2S helper to latest commit
|
||||||
|
|
||||||
## [6.0.4] 2026-04-13
|
## [6.0.4] 2026-04-13
|
||||||
## Fixed
|
### Fixed
|
||||||
- Update A2S module checksums to support servers with missing description field
|
- Update A2S module checksums to support servers with missing description field
|
||||||
|
|
||||||
## [6.0.3] 2026-04-12
|
## [6.0.3] 2026-04-12
|
||||||
## Fixed
|
### Fixed
|
||||||
- Use GLibUnix namespace where available when setting up SIGINT callback
|
- Use GLibUnix namespace where available when setting up SIGINT callback
|
||||||
|
|
||||||
## [6.0.2] 2026-01-26
|
## [6.0.2] 2026-01-26
|
||||||
## Fixed
|
### Fixed
|
||||||
- Explicitly use Python 3.13 when calling subprocesses
|
- Explicitly use Python 3.13 when calling subprocesses
|
||||||
- Raise error correctly from module
|
- Raise error correctly from module
|
||||||
- Do not spawn dialogs from inside of thread when fetching prereqs
|
- Do not spawn dialogs from inside of thread when fetching prereqs
|
||||||
## Changed
|
### Changed
|
||||||
- Optimize time complexity of startup checks
|
- Optimize time complexity of startup checks
|
||||||
- Optimize coordinate calculation by using local records first
|
- Optimize coordinate calculation by using local records first
|
||||||
- Rename some internal functions
|
- Rename some internal functions
|
||||||
- Wrap entire startup process in dialog
|
- Wrap entire startup process in dialog
|
||||||
|
|
||||||
## [6.0.1] 2026-01-16
|
## [6.0.1] 2026-01-16
|
||||||
## Fixed
|
### Fixed
|
||||||
- Explicitly use Python 3.13 when calling subprocesses
|
- Explicitly use Python 3.13 when calling subprocesses
|
||||||
|
|
||||||
## [6.0.0] 2026-01-14
|
## [6.0.0] 2026-01-14
|
||||||
## Added
|
### Added
|
||||||
- Support DayZ Experimental
|
- Support DayZ Experimental
|
||||||
- Ping display on server tables
|
- Ping display on server tables
|
||||||
- Save descriptive text notes on a per-server basis
|
- Save descriptive text notes on a per-server basis
|
||||||
@ -136,7 +139,7 @@
|
|||||||
- Commandline usage help text (GaryBlackbourne)
|
- Commandline usage help text (GaryBlackbourne)
|
||||||
- More descriptive help dialogs when initial dependencies are missing
|
- More descriptive help dialogs when initial dependencies are missing
|
||||||
|
|
||||||
## Fixed
|
### Fixed
|
||||||
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
|
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
|
||||||
- Key stickiness when quickly navigating through entries in tables
|
- Key stickiness when quickly navigating through entries in tables
|
||||||
- Servers returning malformed A2S_INFO blocking server browser from loading
|
- Servers returning malformed A2S_INFO blocking server browser from loading
|
||||||
@ -175,7 +178,7 @@
|
|||||||
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
||||||
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
||||||
|
|
||||||
## Changed
|
### Changed
|
||||||
- Require Python 3.13
|
- Require Python 3.13
|
||||||
- Reduced global API cooldown from 60s to 30s
|
- Reduced global API cooldown from 60s to 30s
|
||||||
- Clarify dialog messages when DayZ path could not be found
|
- Clarify dialog messages when DayZ path could not be found
|
||||||
@ -187,50 +190,50 @@
|
|||||||
- Rewrote distance calculation module (GaryBlackbourne)
|
- Rewrote distance calculation module (GaryBlackbourne)
|
||||||
- Changed preferred client setting from radio toggle to combobox
|
- Changed preferred client setting from radio toggle to combobox
|
||||||
|
|
||||||
## Dropped
|
### Dropped
|
||||||
- Ping readout in statusbar
|
- Ping readout in statusbar
|
||||||
- Extraneous information from right statusbar
|
- Extraneous information from right statusbar
|
||||||
|
|
||||||
## [5.8.3] 2026-01-04
|
## [5.8.3] 2026-01-04
|
||||||
## Fixed
|
### Fixed
|
||||||
- Normalize checksum numbers and dates
|
- Normalize checksum numbers and dates
|
||||||
|
|
||||||
## [5.8.2] 2026-01-04
|
## [5.8.2] 2026-01-04
|
||||||
## Fixed
|
### Fixed
|
||||||
- Fix regression in symlink routine introduced by previous hotfix
|
- Fix regression in symlink routine introduced by previous hotfix
|
||||||
|
|
||||||
## [5.8.1] 2025-12-27
|
## [5.8.1] 2025-12-27
|
||||||
## Fixed
|
### Fixed
|
||||||
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
||||||
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
||||||
|
|
||||||
## [5.8.0] 2025-07-06
|
## [5.8.0] 2025-07-06
|
||||||
## Added
|
### Added
|
||||||
- Filter servers by official/unofficial status
|
- Filter servers by official/unofficial status
|
||||||
- Automatically fetch geolocation records
|
- Automatically fetch geolocation records
|
||||||
## Changed
|
### Changed
|
||||||
- Updated internal versioning of helper files
|
- Updated internal versioning of helper files
|
||||||
## Fixed
|
### Fixed
|
||||||
- Corrected erroneous 2024 date in prior changelog entries
|
- Corrected erroneous 2024 date in prior changelog entries
|
||||||
|
|
||||||
## [5.7.0] 2025-04-17
|
## [5.7.0] 2025-04-17
|
||||||
## Added
|
### Added
|
||||||
- Save application dimensions when quitting and restore on subsequent boot
|
- Save application dimensions when quitting and restore on subsequent boot
|
||||||
## Fixed
|
### Fixed
|
||||||
- Issues with the window exceeding the bounds of the screen when displaying some table contexts
|
- Issues with the window exceeding the bounds of the screen when displaying some table contexts
|
||||||
## Changed
|
### Changed
|
||||||
- More direct server ping query method that should return marginally more accurate times
|
- More direct server ping query method that should return marginally more accurate times
|
||||||
|
|
||||||
## [5.6.7] 2025-04-04
|
## [5.6.7] 2025-04-04
|
||||||
## Dropped
|
### Dropped
|
||||||
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
|
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
|
||||||
|
|
||||||
## [5.6.6] 2025-03-16
|
## [5.6.6] 2025-03-16
|
||||||
## Changed
|
### Changed
|
||||||
- Update IP database records for 2025-03
|
- Update IP database records for 2025-03
|
||||||
|
|
||||||
## [5.6.5] 2025-03-04
|
## [5.6.5] 2025-03-04
|
||||||
## Fixed
|
### Fixed
|
||||||
- Livonia server results being dropped from batch queries
|
- Livonia server results being dropped from batch queries
|
||||||
|
|
||||||
## [5.6.4] 2025-02-10
|
## [5.6.4] 2025-02-10
|
||||||
|
|||||||
@ -8,7 +8,7 @@ If you are a developer, you can skip to the end.
|
|||||||
# Requesting help
|
# Requesting help
|
||||||
|
|
||||||
If you encounter a problem with DZGUI, you can submit tickets on the GitHub
|
If you encounter a problem with DZGUI, you can submit tickets on the GitHub
|
||||||
(issue tracker)[https://github.com/aclist/dztui/issues] under the
|
[issue tracker](https://github.com/aclist/dztui/issues) under the
|
||||||
`troubleshooting` tag.
|
`troubleshooting` tag.
|
||||||
|
|
||||||
# How can I help the project?
|
# How can I help the project?
|
||||||
@ -24,7 +24,7 @@ accordingly.
|
|||||||
|
|
||||||
## Submitting a ticket
|
## Submitting a ticket
|
||||||
|
|
||||||
Navigate to the GitHub (issue tracker)[https://github.com/aclist/dztui/issues].
|
Navigate to the GitHub [issue tracker](https://github.com/aclist/dztui/issues).
|
||||||
From there, follow the onscreen prompts. You will be asked questions such as:
|
From there, follow the onscreen prompts. You will be asked questions such as:
|
||||||
|
|
||||||
- What version are you using?
|
- What version are you using?
|
||||||
|
|||||||
19
docs/licenses/vdf/LICENSE
Normal file
19
docs/licenses/vdf/LICENSE
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2015 Rossen Georgiev <rossen@rgp.io>
|
||||||
|
|
||||||
|
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.
|
||||||
@ -1,12 +1,18 @@
|
|||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from warnings import deprecated
|
from warnings import deprecated
|
||||||
|
|
||||||
|
from dzgui.const.constants import APP_NAME
|
||||||
|
|
||||||
@deprecated("Use dzgui.api.steam.unsubscribe()")
|
logger = logging.getLogger(APP_NAME)
|
||||||
class WorkshopACF:
|
|
||||||
def __init__(self, file: str) -> None:
|
|
||||||
|
class ACF:
|
||||||
|
def __init__(self, file: Path) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.dict: dict[str, Any]
|
self.dict: dict[str, Any]
|
||||||
@ -15,9 +21,10 @@ class WorkshopACF:
|
|||||||
def as_dict(self) -> dict[str, Any]:
|
def as_dict(self) -> dict[str, Any]:
|
||||||
return self.dict
|
return self.dict
|
||||||
|
|
||||||
def load(self, file: str) -> None:
|
def load(self, file: Path) -> None:
|
||||||
delimiter = r"\t\t"
|
delimiter = r"\t\t"
|
||||||
lines = []
|
lines = []
|
||||||
|
# TODO: illegal file handling
|
||||||
with open(file, "r", encoding="utf-8") as f:
|
with open(file, "r", encoding="utf-8") as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
@ -27,6 +34,14 @@ class WorkshopACF:
|
|||||||
lines.append(els)
|
lines.append(els)
|
||||||
self.dict = self.parse(iter(lines))
|
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]:
|
def parse(self, lines: Iterator[list[str]]) -> dict[str, str]:
|
||||||
acf: dict[str, Any] = {}
|
acf: dict[str, Any] = {}
|
||||||
try:
|
try:
|
||||||
@ -54,6 +69,23 @@ class WorkshopACF:
|
|||||||
except StopIteration:
|
except StopIteration:
|
||||||
return acf
|
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)
|
||||||
|
|
||||||
def unpack(self, d: dict, lines: list[Any] = []) -> str:
|
def unpack(self, d: dict, lines: list[Any] = []) -> str:
|
||||||
t1 = "AppWorkshop"
|
t1 = "AppWorkshop"
|
||||||
t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails")
|
t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails")
|
||||||
@ -78,18 +110,9 @@ class WorkshopACF:
|
|||||||
s += line + "\n"
|
s += line + "\n"
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def to_file(self, file: str) -> None:
|
def to_file(self, file: Path) -> None:
|
||||||
s = self.unpack(self.dict)
|
s = self.unpack(self.dict)
|
||||||
with open(file, "w") as f:
|
file.write_text(s)
|
||||||
f.write(s)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def enquote(cls, s: str) -> str:
|
|
||||||
return f'"{s}"'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def dequote(cls, s: str) -> str:
|
|
||||||
return s.rstrip('"').lstrip('"')
|
|
||||||
|
|
||||||
def delete(self, modid: int) -> None:
|
def delete(self, modid: int) -> None:
|
||||||
del self.dict["AppWorkshop"]["WorkshopItemsInstalled"][modid]
|
del self.dict["AppWorkshop"]["WorkshopItemsInstalled"][modid]
|
||||||
|
|||||||
@ -7,13 +7,12 @@ from concurrent.futures import ThreadPoolExecutor
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
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.api.servers import get_rules, fqip_to_record
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
APPID_DAYZ,
|
APPID_DAYZ,
|
||||||
DAYZ_COMMUNITY_ROOT,
|
DAYZ_COMMUNITY_ROOT,
|
||||||
LIBRARYFOLDERS_PATH,
|
|
||||||
WORKSHOP_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:
|
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, APPID_DAYZ)
|
||||||
workshop_path = p / WORKSHOP_PATH
|
workshop_path = p / WORKSHOP_PATH
|
||||||
return workshop_path
|
return workshop_path
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import json
|
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@ -12,9 +11,8 @@ from dzgui.const.constants import (
|
|||||||
APPNAME_DAYZ,
|
APPNAME_DAYZ,
|
||||||
APPNAME_DAYZ_EXP,
|
APPNAME_DAYZ_EXP,
|
||||||
DAYZ_BINARY,
|
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
|
# https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
|
||||||
endian = "<"
|
endian = "<"
|
||||||
@ -257,25 +255,6 @@ class PeFileError(Exception):
|
|||||||
|
|
||||||
pass
|
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:
|
def parse_version_number(data: BinaryIO) -> FileVersion:
|
||||||
# https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
|
# https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
|
||||||
minor = struct.unpack("<L", data.read(4))[0] >> 16 & 0xFFFF
|
minor = struct.unpack("<L", data.read(4))[0] >> 16 & 0xFFFF
|
||||||
@ -419,37 +398,11 @@ def get_pefile_path(steam_path: Path, appid: int) -> Path:
|
|||||||
name = identifier[appid]
|
name = identifier[appid]
|
||||||
binary = DAYZ_BINARY
|
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}"
|
pe_path = app_path / f"steamapps/common/{name}/{binary}"
|
||||||
return pe_path
|
return pe_path
|
||||||
|
|
||||||
|
|
||||||
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:
|
def get_pretty_version(steam_path: Path, appid: int) -> str | None:
|
||||||
try:
|
try:
|
||||||
pe_file_path = get_pefile_path(steam_path, appid)
|
pe_file_path = get_pefile_path(steam_path, appid)
|
||||||
|
|||||||
173
dzgui/api/shortcuts.py
Normal file
173
dzgui/api/shortcuts.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import binascii
|
||||||
|
import ctypes
|
||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
import vdf # type: ignore
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from importlib import resources
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from dzgui.api.steam import find_user_id_32
|
||||||
|
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, IMAGES_PATH
|
||||||
|
from dzgui.util.dirs import copy_dzgui_to_xdg_data
|
||||||
|
|
||||||
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class ShortcutMetadata:
|
||||||
|
appname: str
|
||||||
|
appid: int
|
||||||
|
exe_path: str
|
||||||
|
start_dir: str
|
||||||
|
icon: str
|
||||||
|
|
||||||
|
|
||||||
|
class Shortcuts:
|
||||||
|
def __init__(self, steam_path: Path) -> None:
|
||||||
|
self.user_config_path: Path
|
||||||
|
self.shortcuts_path = self.find_shortcuts_path(steam_path)
|
||||||
|
self._load_shortcuts(self.shortcuts_path)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def gen_bpid(cls, uid: str) -> int:
|
||||||
|
u = uid.encode()
|
||||||
|
encoded = binascii.crc32(u)
|
||||||
|
return encoded | 0x80000000
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def gen_signed_appid(cls, appid: int) -> int:
|
||||||
|
return ctypes.c_int(appid).value
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def gen_exe_uid(cls, appname: str, exe: Path) -> str:
|
||||||
|
wrapped_exe = f'"{exe}"'
|
||||||
|
return appname + wrapped_exe
|
||||||
|
|
||||||
|
def get_shortcuts(self) -> Any:
|
||||||
|
return self.shortcuts
|
||||||
|
|
||||||
|
def _load_shortcuts(self, path: Path) -> None:
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
shortcuts = vdf.binary_load(f)
|
||||||
|
self.shortcuts = shortcuts
|
||||||
|
logger.debug("Loaded shortcuts file")
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def find_shortcuts_path(self, steam_path: Path) -> Path:
|
||||||
|
uid = find_user_id_32(steam_path)
|
||||||
|
self.user_config_path = steam_path.joinpath(f"userdata/{uid}/config")
|
||||||
|
return self.user_config_path.joinpath("shortcuts.vdf")
|
||||||
|
|
||||||
|
def add_shortcut(
|
||||||
|
self, appname: str, start_dir: Path, exe_path: Path, icon: Path
|
||||||
|
) -> None:
|
||||||
|
uid = self.gen_exe_uid(appname, exe_path)
|
||||||
|
self.bpid = self.gen_bpid(uid)
|
||||||
|
self.signed_appid = self.gen_signed_appid(self.bpid)
|
||||||
|
|
||||||
|
meta = ShortcutMetadata(
|
||||||
|
appname, self.signed_appid, str(exe_path), str(start_dir), str(icon)
|
||||||
|
)
|
||||||
|
entry = self._create_entry(meta)
|
||||||
|
self._insert_at_last_index(entry)
|
||||||
|
|
||||||
|
def add_grid_images(self, images: Path) -> None:
|
||||||
|
"""
|
||||||
|
{BPID}_hero.png: hero (splash)
|
||||||
|
{BPID}p.ping: portrait boxart in library
|
||||||
|
{BPID}_logo.png: logo overlay on top of hero
|
||||||
|
{BPID}.png: legacy Big Picture/landscape image
|
||||||
|
"""
|
||||||
|
|
||||||
|
grid_path = self.user_config_path.joinpath("grid")
|
||||||
|
for img in ("_hero", "p", "_logo"):
|
||||||
|
filename = str(self.bpid) + img
|
||||||
|
dest = grid_path.joinpath(filename).with_suffix(".png")
|
||||||
|
b = images.joinpath(img).with_suffix(".png").read_bytes()
|
||||||
|
dest.write_bytes(b)
|
||||||
|
|
||||||
|
# NOTE: legacy Big Picture header
|
||||||
|
b = images.joinpath("_hero.png").read_bytes()
|
||||||
|
dest = grid_path.joinpath(str(self.bpid)).with_suffix(".png")
|
||||||
|
dest.write_bytes(b)
|
||||||
|
|
||||||
|
def _insert_at_last_index(self, entry: dict[str, Any]) -> None:
|
||||||
|
try:
|
||||||
|
last = list(self.shortcuts["shortcuts"].keys())[-1]
|
||||||
|
n = int(last) + 1
|
||||||
|
except Exception:
|
||||||
|
n = 0
|
||||||
|
self.shortcuts["shortcuts"][str(n)] = entry
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_entry(cls, meta: ShortcutMetadata) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
appid: signed int CRC
|
||||||
|
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["StartDir"] = f"{meta.start_dir}"
|
||||||
|
NEW_ENTRY["icon"] = meta.icon
|
||||||
|
NEW_ENTRY["ShortcutPath"] = ""
|
||||||
|
NEW_ENTRY["LaunchOptions"] = ""
|
||||||
|
NEW_ENTRY["IsHidden"] = 0
|
||||||
|
NEW_ENTRY["AllowDesktopConfig"] = 1
|
||||||
|
NEW_ENTRY["AllowOverlay"] = 1
|
||||||
|
NEW_ENTRY["openvr"] = 0
|
||||||
|
NEW_ENTRY["Devkit"] = 0
|
||||||
|
NEW_ENTRY["DevkitGameID"] = ""
|
||||||
|
NEW_ENTRY["DevkitOverrideAppID"] = 0
|
||||||
|
NEW_ENTRY["LastPlayTime"] = 0
|
||||||
|
NEW_ENTRY["FlatpakAppID"] = ""
|
||||||
|
NEW_ENTRY["sortas"] = ""
|
||||||
|
NEW_ENTRY["tags"] = {}
|
||||||
|
return NEW_ENTRY
|
||||||
|
|
||||||
|
def save_shortcuts(self) -> None:
|
||||||
|
try:
|
||||||
|
backup = self.shortcuts_path.with_suffix(".vdf.bak")
|
||||||
|
shutil.copy(self.shortcuts_path, backup)
|
||||||
|
with open(self.shortcuts_path, "wb") as f:
|
||||||
|
vdf.binary_dump(self.shortcuts, f)
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
|
||||||
|
|
||||||
|
def add_steam_shortcut(steam_path: Path, exe_path: Path) -> None:
|
||||||
|
try:
|
||||||
|
start_dir = exe_path.parent
|
||||||
|
|
||||||
|
traversable = resources.files(APP_NAME_LOWER).joinpath(IMAGES_PATH)
|
||||||
|
images = Path(str(traversable))
|
||||||
|
icon = images.joinpath("icon.png")
|
||||||
|
|
||||||
|
shortcuts = Shortcuts(Path(steam_path))
|
||||||
|
shortcuts.add_shortcut(APP_NAME, start_dir, exe_path, icon)
|
||||||
|
shortcuts.save_shortcuts()
|
||||||
|
shortcuts.add_grid_images(images)
|
||||||
|
|
||||||
|
copy_dzgui_to_xdg_data(exe_path)
|
||||||
|
logger.debug("Finished creating Steam shortcut")
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
@ -1,33 +1,57 @@
|
|||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import psutil
|
||||||
import requests
|
import requests
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Union
|
import vdf # type: ignore
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Union
|
||||||
from warnings import deprecated
|
from warnings import deprecated
|
||||||
|
|
||||||
from shlex import shlex
|
from dzgui.api.acf import ACF
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from dzgui.init.prereqs import has_steam_client
|
from dzgui.init.prereqs import has_steam_client
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
APPID_DAYZ,
|
APPID_DAYZ,
|
||||||
|
APPID_DAYZ_EXP,
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
|
DAYZ_BINARY,
|
||||||
DEBIAN_STEAM_PATH,
|
DEBIAN_STEAM_PATH,
|
||||||
DEFAULT_STEAM_PATH,
|
DEFAULT_STEAM_PATH,
|
||||||
FLATPAK_STEAM_PATH,
|
FLATPAK_STEAM_PATH,
|
||||||
|
LIBRARYFOLDERS_PATH,
|
||||||
UBUNTU_STEAM_PATH,
|
UBUNTU_STEAM_PATH,
|
||||||
REQUEST_TIMEOUT,
|
REQUEST_TIMEOUT,
|
||||||
VDF_PATH,
|
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.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):
|
||||||
|
"""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]]:
|
def get_steam_paths() -> list[tuple[Path, str]]:
|
||||||
paths = []
|
paths = []
|
||||||
if has_steam_client():
|
if has_steam_client():
|
||||||
@ -183,10 +207,10 @@ def launch_offline(
|
|||||||
def find_user_id(path: Path) -> str | None:
|
def find_user_id(path: Path) -> str | None:
|
||||||
resolved_path = path / "config" / "loginusers.vdf"
|
resolved_path = path / "config" / "loginusers.vdf"
|
||||||
try:
|
try:
|
||||||
vdf = vdf2json(resolved_path)
|
with open(resolved_path, "r") as f:
|
||||||
j = json.loads(vdf)
|
v = vdf.load(f)
|
||||||
for user in j["users"]:
|
for user in v["users"]:
|
||||||
if j["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:
|
||||||
@ -194,44 +218,11 @@ def find_user_id(path: Path) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def vdf2json(path: Path) -> str:
|
def find_user_id_32(path: Path) -> int:
|
||||||
def _istr(indent: int, string: str) -> str:
|
uid = find_user_id(path)
|
||||||
return (indent * " ") + string
|
if uid is None:
|
||||||
|
raise ValueError("Failed to parse a valid Steam32 ID")
|
||||||
jbuf = "{\n"
|
return int(uid) & 0xFFFFFFFF
|
||||||
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:
|
def update_workshop(key: str, mod: int, endpoint: str) -> None:
|
||||||
@ -257,21 +248,168 @@ def unsubscribe(key: str, mod: int) -> None:
|
|||||||
update_workshop(key, mod, UNSUB_ENDPOINT)
|
update_workshop(key, mod, UNSUB_ENDPOINT)
|
||||||
|
|
||||||
|
|
||||||
def gen_shortcut() -> None:
|
|
||||||
# TODO:
|
|
||||||
"""
|
|
||||||
during setup, prompt user to select matching user id from loginusers
|
|
||||||
show user account name, select outer steam id
|
|
||||||
steam/userdata/<steamid_32>/config/shortcuts.vdf
|
|
||||||
"""
|
|
||||||
# STEAMID_MAGIC = 76561197960265728
|
|
||||||
# STEAMID_64 - STEAMID_MAGIC = STEAMID32
|
|
||||||
# or get right-most 32 bits
|
|
||||||
# STEAMID_64 & 0xFFFFFFFF
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@deprecated("Use subscribe()")
|
@deprecated("Use subscribe()")
|
||||||
def enqueue_mod(client: str, mod: str, appid: int) -> None:
|
def enqueue_mod(client: str, mod: str, appid: int) -> None:
|
||||||
client_args = concat_bash_args(client)
|
client_args = concat_bash_args(client)
|
||||||
subprocess.Popen([*client_args, "+workshop_download_item", str(appid), mod])
|
subprocess.Popen([*client_args, "+workshop_download_item", str(appid), mod])
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated("Cf. https://github.com/ValveSoftware/steam-for-linux/issues/9672")
|
||||||
|
def get_registry() -> Any | None:
|
||||||
|
home = os.getenv("HOME")
|
||||||
|
try:
|
||||||
|
with open(f"{home}/.steam/registry.vdf") as f:
|
||||||
|
registry = vdf.load(f)
|
||||||
|
return registry
|
||||||
|
except Exception as e:
|
||||||
|
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 int(
|
||||||
|
registry["Registry"]["HKCU"]["Software"]["Valve"]["Steam"]["RunningAppID"]
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_running_app() -> int | None:
|
||||||
|
PROC_NAME = "steam"
|
||||||
|
SUBPROC_NAME = "reaper"
|
||||||
|
FLAG = "AppId"
|
||||||
|
# NOTE: may cause conflicts if multiple apps are running
|
||||||
|
try:
|
||||||
|
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 int(next(appid).split("=")[1])
|
||||||
|
except StopIteration:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_allows_downloads(path: Path, appid: int) -> bool:
|
||||||
|
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(path)
|
||||||
|
# NOTE: always allow
|
||||||
|
case 1:
|
||||||
|
return True
|
||||||
|
# NOTE: never allow
|
||||||
|
case 2:
|
||||||
|
return False
|
||||||
|
# NOTE: no other known values at this time, fallback (assume permits)
|
||||||
|
case _:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_config(path: Path) -> Path:
|
||||||
|
return path.joinpath("config/config.vdf")
|
||||||
|
|
||||||
|
|
||||||
|
def get_client_allows_downloads(path: Path) -> bool:
|
||||||
|
config = get_config(path)
|
||||||
|
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:
|
||||||
|
"""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:
|
||||||
|
logger.warning(e)
|
||||||
|
continue
|
||||||
|
return any(substring in item for sublist in procs for item in sublist)
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_path(folders_path: Path, appid: int) -> Path:
|
||||||
|
app_path = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(folders_path / LIBRARYFOLDERS_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)
|
||||||
|
|
||||||
|
|
||||||
|
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 str(res.json()[str(appid)]["data"]["name"])
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(e)
|
||||||
|
return unknown
|
||||||
|
|||||||
@ -20,6 +20,7 @@ from dzgui.strings import boot
|
|||||||
|
|
||||||
# from dzgui.util.map_count import get_map_count
|
# from dzgui.util.map_count import get_map_count
|
||||||
from dzgui.util.deck import is_steam_deck, is_game_mode
|
from dzgui.util.deck import is_steam_deck, is_game_mode
|
||||||
|
from dzgui.util.dirs import make_parents
|
||||||
from dzgui.util.localize import set_locale
|
from dzgui.util.localize import set_locale
|
||||||
from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS
|
from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS
|
||||||
from dzgui.util.strings import init
|
from dzgui.util.strings import init
|
||||||
@ -34,10 +35,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
# TODO: profile load time
|
# TODO: profile load time
|
||||||
def make_parents(path: "Path") -> None:
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
def setup_logger(log_path: "Path") -> None:
|
def setup_logger(log_path: "Path") -> None:
|
||||||
@ -124,10 +122,9 @@ def load_gui(version: str, is_debug: bool) -> None:
|
|||||||
migrate_cols_file(XDG.columns)
|
migrate_cols_file(XDG.columns)
|
||||||
copy_state_files(xdg_paths["XDG_STATE_HOME"])
|
copy_state_files(xdg_paths["XDG_STATE_HOME"])
|
||||||
# TODO: add logging inside wizard
|
# TODO: add logging inside wizard
|
||||||
SetupWizard(_is_steam_deck, XDG.config)
|
wizard = SetupWizard(_is_steam_deck, XDG)
|
||||||
|
|
||||||
# NOTE: implies that setup wizard failed or was closed
|
if not wizard.is_setup_complete():
|
||||||
if has_new_config(XDG.config) is False:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
setup_logger(XDG.debug)
|
setup_logger(XDG.debug)
|
||||||
|
|||||||
@ -1,18 +1,43 @@
|
|||||||
|
import stat
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dzgui.const.constants import APP_NAME
|
||||||
|
from dzgui.util.dirs import copy_dzgui_to_xdg_data, find_icon_resource, make_parents
|
||||||
|
|
||||||
|
|
||||||
|
def get_share_path(exe_path: Path) -> Path:
|
||||||
|
return exe_path.parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def write_desktop_file(exe_path: Path) -> Path:
|
||||||
|
icon = find_icon_resource()
|
||||||
|
|
||||||
def write_desktop_file(share_path: Path) -> None:
|
|
||||||
template = f"""\
|
template = f"""\
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Version=1.0
|
Version=1.0
|
||||||
Type=Application
|
Type=Application
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Exec={share_path}/launch.sh
|
Exec={exe_path}
|
||||||
Name=DZGUI
|
Name=DZGUI
|
||||||
Comment=dzgui
|
Comment=dzgui
|
||||||
Icon={share_path}/dzgui
|
Icon={icon}
|
||||||
Categories=Game"""
|
Categories=Game"""
|
||||||
|
|
||||||
file = share_path / "dzgui.desktop"
|
copy_dzgui_to_xdg_data(exe_path)
|
||||||
file.write_text(textwrap.dedent(template))
|
|
||||||
|
share_path = get_share_path(exe_path)
|
||||||
|
desktop_file = share_path.joinpath("applications/dzgui.desktop")
|
||||||
|
make_parents(desktop_file)
|
||||||
|
desktop_file.write_text(textwrap.dedent(template))
|
||||||
|
desktop_file.chmod(desktop_file.stat().st_mode | stat.S_IEXEC)
|
||||||
|
return desktop_file
|
||||||
|
|
||||||
|
|
||||||
|
def write_desktop_shortcut(desktop_file: Path) -> None:
|
||||||
|
# NOTE: necessarily depends on the above (UI blocks creation without XDG entry first)
|
||||||
|
link = Path.home().joinpath(f"Desktop/{APP_NAME}.desktop")
|
||||||
|
if link.exists():
|
||||||
|
link.unlink()
|
||||||
|
link.symlink_to(desktop_file)
|
||||||
|
|||||||
@ -18,6 +18,7 @@ class Xdg:
|
|||||||
debug: Path
|
debug: Path
|
||||||
ips: Path
|
ips: Path
|
||||||
filters: Path
|
filters: Path
|
||||||
|
shortcut: Path
|
||||||
|
|
||||||
|
|
||||||
def is_writeable(path_str: str) -> bool:
|
def is_writeable(path_str: str) -> bool:
|
||||||
@ -67,6 +68,7 @@ def get_xdg_paths() -> dict:
|
|||||||
def parse_filepaths(xdg: dict) -> Xdg:
|
def parse_filepaths(xdg: dict) -> Xdg:
|
||||||
config = xdg["XDG_CONFIG_HOME"]
|
config = xdg["XDG_CONFIG_HOME"]
|
||||||
state = xdg["XDG_STATE_HOME"]
|
state = xdg["XDG_STATE_HOME"]
|
||||||
|
share = xdg["XDG_DATA_HOME"]
|
||||||
|
|
||||||
config = config / "config.json"
|
config = config / "config.json"
|
||||||
|
|
||||||
@ -81,6 +83,8 @@ def parse_filepaths(xdg: dict) -> Xdg:
|
|||||||
ips = state / "ips.csv"
|
ips = state / "ips.csv"
|
||||||
filters = state / "dzg.filters.json"
|
filters = state / "dzg.filters.json"
|
||||||
|
|
||||||
|
shortcut = share / APP_NAME_LOWER
|
||||||
|
|
||||||
return Xdg(
|
return Xdg(
|
||||||
config,
|
config,
|
||||||
columns,
|
columns,
|
||||||
@ -91,5 +95,6 @@ def parse_filepaths(xdg: dict) -> Xdg:
|
|||||||
system,
|
system,
|
||||||
debug,
|
debug,
|
||||||
ips,
|
ips,
|
||||||
filters
|
filters,
|
||||||
|
shortcut,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -72,7 +72,9 @@ LEGACY_IPS_PATH = ".local/share/dzgui/helpers/ips.csv"
|
|||||||
DEBUG_LOG = f"{APP_NAME}_DEBUG.LOG"
|
DEBUG_LOG = f"{APP_NAME}_DEBUG.LOG"
|
||||||
SYSTEM_LOG = f"{APP_NAME}_SYSTEM.LOG"
|
SYSTEM_LOG = f"{APP_NAME}_SYSTEM.LOG"
|
||||||
CHANGELOG_PATH = "data/CHANGELOG.md"
|
CHANGELOG_PATH = "data/CHANGELOG.md"
|
||||||
HERO_PATH = "data/images/hero.png"
|
IMAGES_PATH = "data/images"
|
||||||
|
|
||||||
|
HERO_PATH = "data/images/_hero.png"
|
||||||
CSS_PATH = "data/app.css"
|
CSS_PATH = "data/app.css"
|
||||||
VDF_PATH = "steamapps/libraryfolders.vdf"
|
VDF_PATH = "steamapps/libraryfolders.vdf"
|
||||||
DEFAULT_STEAM_PATH = ".local/share/Steam"
|
DEFAULT_STEAM_PATH = ".local/share/Steam"
|
||||||
|
|||||||
@ -5,6 +5,7 @@ STEAM_PUBLISHED_FILES = (
|
|||||||
STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1"
|
STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1"
|
||||||
SUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Subscribe/v1"
|
SUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Subscribe/v1"
|
||||||
UNSUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/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?"
|
BM_SERVERS = "https://api.battlemetrics.com/servers?"
|
||||||
GITHUB = "https://github.com/aclist"
|
GITHUB = "https://github.com/aclist"
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Added
|
## [7.0.0] Unreleased
|
||||||
|
### Added
|
||||||
- Setup wizard
|
- Setup wizard
|
||||||
- Changelog text wrapping and formatting
|
- Changelog text wrapping and formatting
|
||||||
- Changelog ships with source
|
- Changelog ships with source
|
||||||
@ -41,8 +42,10 @@
|
|||||||
- Choose to jump into splash screen instead of server
|
- Choose to jump into splash screen instead of server
|
||||||
- Collapsible connection panel
|
- Collapsible connection panel
|
||||||
- Play offline (load mods directly)
|
- Play offline (load mods directly)
|
||||||
|
- Generate Steam shortcuts and cover art
|
||||||
|
- Warn user if background downloads are disabled
|
||||||
|
|
||||||
## Changed
|
### Changed
|
||||||
- Conform to PEP 440 versioning for beta versions
|
- Conform to PEP 440 versioning for beta versions
|
||||||
- Reduce padding on keys button
|
- Reduce padding on keys button
|
||||||
- Boldface breadcrumbs
|
- Boldface breadcrumbs
|
||||||
@ -59,58 +62,58 @@
|
|||||||
- Enable LAN page Empty/Full filters on first run of app
|
- Enable LAN page Empty/Full filters on first run of app
|
||||||
- Propagate subscribed mods to Steam client
|
- Propagate subscribed mods to Steam client
|
||||||
|
|
||||||
## Dropped
|
### Dropped
|
||||||
- Debug mode
|
- Debug mode
|
||||||
- Branch switching
|
- Branch switching
|
||||||
- Manual mod install mode (describe rationale)
|
- Manual mod install mode (describe rationale)
|
||||||
- Force update mods
|
- Force update mods
|
||||||
|
|
||||||
## Fixed
|
### Fixed
|
||||||
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
|
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
|
||||||
- Center server title text on server dialogs
|
- Center server title text on server dialogs
|
||||||
- Rare segfaults when changing maps (threading)
|
- Rare segfaults when changing maps (threading)
|
||||||
- Moved dialogs out of threads
|
- Moved dialogs out of threads
|
||||||
|
|
||||||
## Unreleased
|
### Unreleased
|
||||||
- Load offline mods
|
- Load offline mods
|
||||||
- Local documentation
|
- Local documentation
|
||||||
- Raw debug command in context menu
|
- Raw debug command in context menu
|
||||||
|
|
||||||
## Developer-facing
|
### Developer-facing
|
||||||
- Add pyproject.toml file
|
- Add pyproject.toml file
|
||||||
- Show deprecation warnings
|
- Show deprecation warnings
|
||||||
- Options -> Dev page
|
- Options -> Dev page
|
||||||
|
|
||||||
## [6.0.5] 2026-05-17
|
## [6.0.5] 2026-05-17
|
||||||
## Changed
|
### Changed
|
||||||
- Update symlink hash logic to align with DZGUI 7 beta
|
- Update symlink hash logic to align with DZGUI 7 beta
|
||||||
- Update A2S helper to latest commit
|
- Update A2S helper to latest commit
|
||||||
|
|
||||||
## [6.0.4] 2026-04-13
|
## [6.0.4] 2026-04-13
|
||||||
## Fixed
|
### Fixed
|
||||||
- Update A2S module checksums to support servers with missing description field
|
- Update A2S module checksums to support servers with missing description field
|
||||||
|
|
||||||
## [6.0.3] 2026-04-12
|
## [6.0.3] 2026-04-12
|
||||||
## Fixed
|
### Fixed
|
||||||
- Use GLibUnix namespace where available when setting up SIGINT callback
|
- Use GLibUnix namespace where available when setting up SIGINT callback
|
||||||
|
|
||||||
## [6.0.2] 2026-01-26
|
## [6.0.2] 2026-01-26
|
||||||
## Fixed
|
### Fixed
|
||||||
- Explicitly use Python 3.13 when calling subprocesses
|
- Explicitly use Python 3.13 when calling subprocesses
|
||||||
- Raise error correctly from module
|
- Raise error correctly from module
|
||||||
- Do not spawn dialogs from inside of thread when fetching prereqs
|
- Do not spawn dialogs from inside of thread when fetching prereqs
|
||||||
## Changed
|
### Changed
|
||||||
- Optimize time complexity of startup checks
|
- Optimize time complexity of startup checks
|
||||||
- Optimize coordinate calculation by using local records first
|
- Optimize coordinate calculation by using local records first
|
||||||
- Rename some internal functions
|
- Rename some internal functions
|
||||||
- Wrap entire startup process in dialog
|
- Wrap entire startup process in dialog
|
||||||
|
|
||||||
## [6.0.1] 2026-01-16
|
## [6.0.1] 2026-01-16
|
||||||
## Fixed
|
### Fixed
|
||||||
- Explicitly use Python 3.13 when calling subprocesses
|
- Explicitly use Python 3.13 when calling subprocesses
|
||||||
|
|
||||||
## [6.0.0] 2026-01-14
|
## [6.0.0] 2026-01-14
|
||||||
## Added
|
### Added
|
||||||
- Support DayZ Experimental
|
- Support DayZ Experimental
|
||||||
- Ping display on server tables
|
- Ping display on server tables
|
||||||
- Save descriptive text notes on a per-server basis
|
- Save descriptive text notes on a per-server basis
|
||||||
@ -136,7 +139,7 @@
|
|||||||
- Commandline usage help text (GaryBlackbourne)
|
- Commandline usage help text (GaryBlackbourne)
|
||||||
- More descriptive help dialogs when initial dependencies are missing
|
- More descriptive help dialogs when initial dependencies are missing
|
||||||
|
|
||||||
## Fixed
|
### Fixed
|
||||||
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
|
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
|
||||||
- Key stickiness when quickly navigating through entries in tables
|
- Key stickiness when quickly navigating through entries in tables
|
||||||
- Servers returning malformed A2S_INFO blocking server browser from loading
|
- Servers returning malformed A2S_INFO blocking server browser from loading
|
||||||
@ -175,7 +178,7 @@
|
|||||||
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
||||||
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
||||||
|
|
||||||
## Changed
|
### Changed
|
||||||
- Require Python 3.13
|
- Require Python 3.13
|
||||||
- Reduced global API cooldown from 60s to 30s
|
- Reduced global API cooldown from 60s to 30s
|
||||||
- Clarify dialog messages when DayZ path could not be found
|
- Clarify dialog messages when DayZ path could not be found
|
||||||
@ -187,50 +190,50 @@
|
|||||||
- Rewrote distance calculation module (GaryBlackbourne)
|
- Rewrote distance calculation module (GaryBlackbourne)
|
||||||
- Changed preferred client setting from radio toggle to combobox
|
- Changed preferred client setting from radio toggle to combobox
|
||||||
|
|
||||||
## Dropped
|
### Dropped
|
||||||
- Ping readout in statusbar
|
- Ping readout in statusbar
|
||||||
- Extraneous information from right statusbar
|
- Extraneous information from right statusbar
|
||||||
|
|
||||||
## [5.8.3] 2026-01-04
|
## [5.8.3] 2026-01-04
|
||||||
## Fixed
|
### Fixed
|
||||||
- Normalize checksum numbers and dates
|
- Normalize checksum numbers and dates
|
||||||
|
|
||||||
## [5.8.2] 2026-01-04
|
## [5.8.2] 2026-01-04
|
||||||
## Fixed
|
### Fixed
|
||||||
- Fix regression in symlink routine introduced by previous hotfix
|
- Fix regression in symlink routine introduced by previous hotfix
|
||||||
|
|
||||||
## [5.8.1] 2025-12-27
|
## [5.8.1] 2025-12-27
|
||||||
## Fixed
|
### Fixed
|
||||||
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
||||||
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
||||||
|
|
||||||
## [5.8.0] 2025-07-06
|
## [5.8.0] 2025-07-06
|
||||||
## Added
|
### Added
|
||||||
- Filter servers by official/unofficial status
|
- Filter servers by official/unofficial status
|
||||||
- Automatically fetch geolocation records
|
- Automatically fetch geolocation records
|
||||||
## Changed
|
### Changed
|
||||||
- Updated internal versioning of helper files
|
- Updated internal versioning of helper files
|
||||||
## Fixed
|
### Fixed
|
||||||
- Corrected erroneous 2024 date in prior changelog entries
|
- Corrected erroneous 2024 date in prior changelog entries
|
||||||
|
|
||||||
## [5.7.0] 2025-04-17
|
## [5.7.0] 2025-04-17
|
||||||
## Added
|
### Added
|
||||||
- Save application dimensions when quitting and restore on subsequent boot
|
- Save application dimensions when quitting and restore on subsequent boot
|
||||||
## Fixed
|
### Fixed
|
||||||
- Issues with the window exceeding the bounds of the screen when displaying some table contexts
|
- Issues with the window exceeding the bounds of the screen when displaying some table contexts
|
||||||
## Changed
|
### Changed
|
||||||
- More direct server ping query method that should return marginally more accurate times
|
- More direct server ping query method that should return marginally more accurate times
|
||||||
|
|
||||||
## [5.6.7] 2025-04-04
|
## [5.6.7] 2025-04-04
|
||||||
## Dropped
|
### Dropped
|
||||||
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
|
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
|
||||||
|
|
||||||
## [5.6.6] 2025-03-16
|
## [5.6.6] 2025-03-16
|
||||||
## Changed
|
### Changed
|
||||||
- Update IP database records for 2025-03
|
- Update IP database records for 2025-03
|
||||||
|
|
||||||
## [5.6.5] 2025-03-04
|
## [5.6.5] 2025-03-04
|
||||||
## Fixed
|
### Fixed
|
||||||
- Livonia server results being dropped from batch queries
|
- Livonia server results being dropped from batch queries
|
||||||
|
|
||||||
## [5.6.4] 2025-02-10
|
## [5.6.4] 2025-02-10
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 81 B After Width: | Height: | Size: 81 B |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
@ -2,11 +2,11 @@ import logging
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dzgui.api.steam import get_app_path
|
||||||
from dzgui.config.query import lookup
|
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
|
from dzgui.const.enum import Preferences
|
||||||
|
|
||||||
import dzgui.api.pefile as PeFile
|
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ logger = logging.getLogger(APP_NAME)
|
|||||||
def is_dayz_installed(config: Path) -> None:
|
def is_dayz_installed(config: Path) -> None:
|
||||||
try:
|
try:
|
||||||
path = lookup(config, Preferences.DEFAULT)
|
path = lookup(config, Preferences.DEFAULT)
|
||||||
PeFile.get_app_path(Path(path) / LIBRARYFOLDERS_PATH, APPID_DAYZ)
|
get_app_path(Path(path), APPID_DAYZ)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
raise e
|
raise e
|
||||||
|
|||||||
@ -3,11 +3,8 @@ import subprocess
|
|||||||
import shutil
|
import shutil
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from warnings import deprecated
|
|
||||||
|
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
DAYZ_BINARY,
|
|
||||||
STEAM_CMD,
|
STEAM_CMD,
|
||||||
FLATPAK_APPID,
|
FLATPAK_APPID,
|
||||||
FLATPAK_CMD,
|
FLATPAK_CMD,
|
||||||
@ -15,26 +12,9 @@ from dzgui.const.constants import (
|
|||||||
FLATPAK_SANDBOX,
|
FLATPAK_SANDBOX,
|
||||||
)
|
)
|
||||||
|
|
||||||
from dzgui.util.format import format_exception
|
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
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:
|
def is_steam_running(cmd: str) -> bool:
|
||||||
if cmd == STEAM_CMD:
|
if cmd == STEAM_CMD:
|
||||||
if has_cmd(STEAM_CMD) is False:
|
if has_cmd(STEAM_CMD) is False:
|
||||||
@ -69,25 +49,3 @@ def has_cmd(cmd: str) -> bool:
|
|||||||
if shutil.which(cmd) is not None:
|
if shutil.which(cmd) is not None:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@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
|
|
||||||
)
|
|
||||||
|
|||||||
@ -12,8 +12,11 @@ import dzgui.api.servers as Servers
|
|||||||
|
|
||||||
from dzgui.api.steam import (
|
from dzgui.api.steam import (
|
||||||
connect,
|
connect,
|
||||||
get_remote_signatures,
|
get_app_allows_downloads,
|
||||||
|
get_app_name,
|
||||||
get_needs_update,
|
get_needs_update,
|
||||||
|
get_remote_signatures,
|
||||||
|
get_running_app,
|
||||||
load_to_menu,
|
load_to_menu,
|
||||||
subscribe,
|
subscribe,
|
||||||
)
|
)
|
||||||
@ -32,8 +35,9 @@ from dzgui.const.constants import (
|
|||||||
APPNAME_DAYZ,
|
APPNAME_DAYZ,
|
||||||
APPNAME_DAYZ_EXP_HUMAN,
|
APPNAME_DAYZ_EXP_HUMAN,
|
||||||
)
|
)
|
||||||
|
from dzgui.api.steam import is_dayz_running
|
||||||
from dzgui.const.enum import NotebookPage, Preferences
|
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.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||||
from dzgui.strings.dialogs import (
|
from dzgui.strings.dialogs import (
|
||||||
waiting_for_launch,
|
waiting_for_launch,
|
||||||
@ -81,6 +85,7 @@ class Prerequisites:
|
|||||||
mods: list[list[str]]
|
mods: list[list[str]]
|
||||||
game_mode: bool
|
game_mode: bool
|
||||||
is_last_server: bool
|
is_last_server: bool
|
||||||
|
allows_downloads: tuple[bool, str]
|
||||||
|
|
||||||
|
|
||||||
class ConnectionManager:
|
class ConnectionManager:
|
||||||
@ -166,6 +171,15 @@ class ConnectionManager:
|
|||||||
|
|
||||||
dayz_running = is_dayz_running()
|
dayz_running = is_dayz_running()
|
||||||
|
|
||||||
|
running_app = get_running_app()
|
||||||
|
if running_app is not None:
|
||||||
|
allows_dl = get_app_allows_downloads(steam_path, running_app)
|
||||||
|
running_app_name = get_app_name(running_app)
|
||||||
|
allows_downloads = (allows_dl, running_app_name)
|
||||||
|
else:
|
||||||
|
allows_downloads = (True, "")
|
||||||
|
|
||||||
|
|
||||||
client_name = self.controller.get_steam_client_name()
|
client_name = self.controller.get_steam_client_name()
|
||||||
client = self.controller.query_config(Preferences.CLIENT)
|
client = self.controller.query_config(Preferences.CLIENT)
|
||||||
running = is_steam_running(client)
|
running = is_steam_running(client)
|
||||||
@ -191,6 +205,7 @@ class ConnectionManager:
|
|||||||
mods=remote_mods,
|
mods=remote_mods,
|
||||||
game_mode=game_mode,
|
game_mode=game_mode,
|
||||||
is_last_server=is_last,
|
is_last_server=is_last,
|
||||||
|
allows_downloads=allows_downloads,
|
||||||
)
|
)
|
||||||
|
|
||||||
func = StoredFunc(self.controller.open_connection_assistant, prereqs)
|
func = StoredFunc(self.controller.open_connection_assistant, prereqs)
|
||||||
@ -224,7 +239,7 @@ class ConnectionManager:
|
|||||||
) -> tuple[list[list[str]], list[tuple[str, str, int, int]]]:
|
) -> tuple[list[list[str]], list[tuple[str, str, int, int]]]:
|
||||||
mods = Servers.get_rules(record)
|
mods = Servers.get_rules(record)
|
||||||
steam_path = self.controller.query_config(Preferences.DEFAULT)
|
steam_path = self.controller.query_config(Preferences.DEFAULT)
|
||||||
local = get_local_mod_ids(steam_path)
|
local = get_local_mod_ids(Path(steam_path))
|
||||||
|
|
||||||
alpha_mods: list[list[str]] = [
|
alpha_mods: list[list[str]] = [
|
||||||
[
|
[
|
||||||
|
|||||||
@ -60,7 +60,7 @@ class ModManager:
|
|||||||
|
|
||||||
@call_on_thread(dialogs.fetching_mods)
|
@call_on_thread(dialogs.fetching_mods)
|
||||||
def load_mods(self) -> None:
|
def load_mods(self) -> None:
|
||||||
mods = get_delimited_mods(self.path)
|
mods = get_delimited_mods(Path(self.path))
|
||||||
if len(mods) < 1:
|
if len(mods) < 1:
|
||||||
msg = self.format_mod_statusbar()
|
msg = self.format_mod_statusbar()
|
||||||
func = StoredFunc(lambda: self.emitter.emit("mods_updated", msg, 0))
|
func = StoredFunc(lambda: self.emitter.emit("mods_updated", msg, 0))
|
||||||
@ -141,7 +141,7 @@ class ModManager:
|
|||||||
app_path_exp = PeFile.get_nested_app_path(steam_path, APPID_DAYZ_EXP)
|
app_path_exp = PeFile.get_nested_app_path(steam_path, APPID_DAYZ_EXP)
|
||||||
symlink = app_path_exp / md5
|
symlink = app_path_exp / md5
|
||||||
symlink.unlink()
|
symlink.unlink()
|
||||||
except PeFile.AppNotInstalledError:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
time.sleep(API_RATE_LIMIT)
|
time.sleep(API_RATE_LIMIT)
|
||||||
|
|
||||||
|
|||||||
@ -5,10 +5,9 @@ from typing import Callable, TYPE_CHECKING, Union
|
|||||||
|
|
||||||
import dzgui.api.pefile as PeFile
|
import dzgui.api.pefile as PeFile
|
||||||
from dzgui.api.mods import is_mission, get_custom_mods
|
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.constants import APP_NAME, APPID_DAYZ_EXP
|
||||||
from dzgui.const.enum import Preferences
|
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.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||||
from dzgui.model.model_factory import ModelFactory
|
from dzgui.model.model_factory import ModelFactory
|
||||||
from dzgui.strings import dialogs
|
from dzgui.strings import dialogs
|
||||||
@ -77,7 +76,7 @@ class OfflineManager:
|
|||||||
window.set_sensitive(True)
|
window.set_sensitive(True)
|
||||||
self.emitter.emit("custom_mods_loaded", store, folder)
|
self.emitter.emit("custom_mods_loaded", store, folder)
|
||||||
|
|
||||||
@call_on_thread(dialogs.waiting_for_launch)
|
@call_on_thread(dialogs.waiting_for_launch, show_cancel=True)
|
||||||
def launch(
|
def launch(
|
||||||
self,
|
self,
|
||||||
appid: int,
|
appid: int,
|
||||||
|
|||||||
@ -64,6 +64,25 @@ radio_mi = "mi (miles)"
|
|||||||
label_dist = "Distance display"
|
label_dist = "Distance display"
|
||||||
label_client = "Steam client"
|
label_client = "Steam client"
|
||||||
|
|
||||||
|
### Shortcuts page
|
||||||
|
heading_shortcuts = "Create shortcuts"
|
||||||
|
blurb_shortcuts = (
|
||||||
|
"You can optionally create shortcuts to facilitate launching DZGUI faster."
|
||||||
|
)
|
||||||
|
button_shortcuts = "Create shortcuts"
|
||||||
|
checkbox_steam_shortcut = (
|
||||||
|
"Add DZGUI to Steam library",
|
||||||
|
(
|
||||||
|
"This creates a standalone copy of DZGUI and adds it to Steam. "
|
||||||
|
"You <b>must restart Steam</b> for the shortcut to appear. This will not replace or update existing shortcuts. "
|
||||||
|
"If you update DZGUI when it is launched via Steam, updates will be applied to the Steam version."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
checkbox_start_menu = (
|
||||||
|
"Add a start menu shortcut",
|
||||||
|
"Depends on local window manager settings. Will work on Steam Deck.",
|
||||||
|
)
|
||||||
|
checkbox_desktop_shortcut = ("Add a desktop shortcut", "Depends on the option above.")
|
||||||
|
|
||||||
### Completion page
|
### Completion page
|
||||||
heading_completion = "Setup complete"
|
heading_completion = "Setup complete"
|
||||||
|
|||||||
@ -41,10 +41,10 @@ def print_mods(mods: list[int]) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def write_diagnostic(config: Path, outfile: Path) -> None:
|
def write_diagnostic(config: Path, outfile: Path) -> None:
|
||||||
# TODO: test availability on other distros
|
|
||||||
date = datetime.now().isoformat()
|
date = datetime.now().isoformat()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# FIXME: ID_LIKE not available on CachyOS
|
||||||
distro = platform.freedesktop_os_release()["ID_LIKE"]
|
distro = platform.freedesktop_os_release()["ID_LIKE"]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(e)
|
logger.warn(e)
|
||||||
|
|||||||
32
dzgui/util/dirs.py
Normal file
32
dzgui/util/dirs.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from importlib import resources
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, IMAGES_PATH
|
||||||
|
|
||||||
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
def make_parents(path: "Path") -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_dzgui_to_xdg_data(exe_path: Path) -> None:
|
||||||
|
dzgui = os.getenv("PYAPP")
|
||||||
|
if dzgui is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
make_parents(exe_path)
|
||||||
|
shutil.copy(dzgui, exe_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
|
||||||
|
|
||||||
|
def find_icon_resource() -> "Path":
|
||||||
|
traversable = resources.files(APP_NAME_LOWER).joinpath(IMAGES_PATH)
|
||||||
|
images = Path(str(traversable))
|
||||||
|
icon = images.joinpath("icon.png")
|
||||||
|
return icon
|
||||||
@ -1,11 +1,13 @@
|
|||||||
|
import os
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from importlib import resources
|
from importlib import resources
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Self
|
from typing import Any, Callable, Self, TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.api.probe import test_steam_api, test_bm_api
|
from dzgui.api.probe import test_steam_api, test_bm_api
|
||||||
|
from dzgui.api.shortcuts import add_steam_shortcut
|
||||||
from dzgui.api.steam import get_steam_paths
|
from dzgui.api.steam import get_steam_paths
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
@ -15,6 +17,9 @@ from dzgui.const.constants import (
|
|||||||
)
|
)
|
||||||
from dzgui.const.boilerplate import config_boilerplate
|
from dzgui.const.boilerplate import config_boilerplate
|
||||||
from dzgui.const.endpoints import BM_API_SETUP, STEAM_API_SETUP
|
from dzgui.const.endpoints import BM_API_SETUP, STEAM_API_SETUP
|
||||||
|
from dzgui.const.enum import Preferences
|
||||||
|
from dzgui.config import freedesktop
|
||||||
|
from dzgui.config.query import lookup
|
||||||
from dzgui.init.migrate import migrate_legacy_conf
|
from dzgui.init.migrate import migrate_legacy_conf
|
||||||
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||||
from dzgui.strings import wizard
|
from dzgui.strings import wizard
|
||||||
@ -30,6 +35,9 @@ import gi
|
|||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gdk, Gtk, GLib, GObject, GdkPixbuf # noqa E402
|
from gi.repository import Gdk, Gtk, GLib, GObject, GdkPixbuf # noqa E402
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from dzgui.config.xdg import Xdg
|
||||||
|
|
||||||
|
|
||||||
class PageNum(Enum):
|
class PageNum(Enum):
|
||||||
INTRO = 1
|
INTRO = 1
|
||||||
@ -38,7 +46,8 @@ class PageNum(Enum):
|
|||||||
STEAM_API = 4
|
STEAM_API = 4
|
||||||
BM_API = 5
|
BM_API = 5
|
||||||
USER_PREFS = 6
|
USER_PREFS = 6
|
||||||
FINAL = 7
|
SHORTCUTS = 7
|
||||||
|
FINAL = 8
|
||||||
|
|
||||||
|
|
||||||
class DescriptionArea(Gtk.Box):
|
class DescriptionArea(Gtk.Box):
|
||||||
@ -58,7 +67,7 @@ class Progress(Gtk.ProgressBar):
|
|||||||
|
|
||||||
class ScrolledWizardPage(Gtk.ScrolledWindow):
|
class ScrolledWizardPage(Gtk.ScrolledWindow):
|
||||||
def __init__(self, enum: PageNum, heading: str, description: str):
|
def __init__(self, enum: PageNum, heading: str, description: str):
|
||||||
super().__init__()
|
super().__init__(overlay_scrolling=False)
|
||||||
|
|
||||||
self.enum = enum
|
self.enum = enum
|
||||||
self.page_type: Gtk.AssistantPageType
|
self.page_type: Gtk.AssistantPageType
|
||||||
@ -141,6 +150,10 @@ class NotificationFrame(Gtk.Frame):
|
|||||||
if error:
|
if error:
|
||||||
add_class(self, "error-frame")
|
add_class(self, "error-frame")
|
||||||
|
|
||||||
|
def set_text(self, text: str) -> None:
|
||||||
|
wrapped = textwrap.fill(text, width=80)
|
||||||
|
self.label.set_markup(wrapped)
|
||||||
|
|
||||||
|
|
||||||
class APIValidationPage(ScrolledWizardPage):
|
class APIValidationPage(ScrolledWizardPage):
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -339,6 +352,7 @@ class ConfigMigrationPage(ScrolledWizardPage):
|
|||||||
def _on_import_clicked(self, button: Gtk.Button) -> None:
|
def _on_import_clicked(self, button: Gtk.Button) -> None:
|
||||||
self.grid.set_sensitive(False)
|
self.grid.set_sensitive(False)
|
||||||
try:
|
try:
|
||||||
|
# TODO: this could be deferred to the final page (prevents accidental destruction of dialog via ESC)
|
||||||
migrate_legacy_conf(self.config)
|
migrate_legacy_conf(self.config)
|
||||||
self.migrated = True
|
self.migrated = True
|
||||||
self.success_box.set_visible(True)
|
self.success_box.set_visible(True)
|
||||||
@ -423,24 +437,27 @@ class CompletionPage(ScrolledWizardPage):
|
|||||||
|
|
||||||
|
|
||||||
class Assistant(Gtk.Assistant):
|
class Assistant(Gtk.Assistant):
|
||||||
def __init__(self, is_deck: bool, config: Path):
|
def __init__(self, is_deck: bool, XDG: "Xdg"):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if is_deck:
|
if is_deck:
|
||||||
self.fullscreen()
|
self.fullscreen()
|
||||||
else:
|
else:
|
||||||
self.set_default_size(1500, 900)
|
self.set_default_size(1500, 900)
|
||||||
|
|
||||||
self.config_path = config
|
self.config_path = XDG.config
|
||||||
|
|
||||||
self.config_values: dict[str, Any] = config_boilerplate
|
self.config_values: dict[str, Any] = config_boilerplate
|
||||||
|
|
||||||
|
self.setup_complete = False
|
||||||
|
|
||||||
self.page1 = IntroductionPage()
|
self.page1 = IntroductionPage()
|
||||||
self.page2 = ConfigMigrationPage(config)
|
self.page2 = ConfigMigrationPage(XDG.config)
|
||||||
self.page3 = SteamPathPage()
|
self.page3 = SteamPathPage()
|
||||||
self.page4 = SteamValidationPage()
|
self.page4 = SteamValidationPage()
|
||||||
self.page5 = BMValidationPage()
|
self.page5 = BMValidationPage()
|
||||||
self.page6 = PreferencesPage()
|
self.page6 = PreferencesPage()
|
||||||
self.page7 = CompletionPage()
|
self.page7 = ShortcutCreationPage(XDG.shortcut)
|
||||||
|
self.page8 = CompletionPage()
|
||||||
|
|
||||||
self.set_forward_page_func(self._advance_page)
|
self.set_forward_page_func(self._advance_page)
|
||||||
|
|
||||||
@ -458,9 +475,16 @@ class Assistant(Gtk.Assistant):
|
|||||||
self.page5,
|
self.page5,
|
||||||
self.page6,
|
self.page6,
|
||||||
self.page7,
|
self.page7,
|
||||||
|
self.page8,
|
||||||
):
|
):
|
||||||
# NOTE: skip config migration page if no legacy config file
|
# NOTE: skip config migration page if no legacy config file
|
||||||
if page == self.page2 and self.has_legacy_config is False:
|
if (
|
||||||
|
isinstance(page, ConfigMigrationPage)
|
||||||
|
and self.has_legacy_config is False
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
# NOTE: disabled for now on system-provided packages
|
||||||
|
if isinstance(page, ShortcutCreationPage) and os.getenv("PYAPP") is None:
|
||||||
continue
|
continue
|
||||||
self._add_page(page, page.get_page_type())
|
self._add_page(page, page.get_page_type())
|
||||||
|
|
||||||
@ -475,13 +499,15 @@ class Assistant(Gtk.Assistant):
|
|||||||
|
|
||||||
def _advance_page(self, index: int) -> int:
|
def _advance_page(self, index: int) -> int:
|
||||||
page = self.get_nth_page(index)
|
page = self.get_nth_page(index)
|
||||||
# TODO: use enums
|
# TODO: use enums/isinstance
|
||||||
match page:
|
match page:
|
||||||
case self.page1:
|
case self.page1:
|
||||||
pass
|
pass
|
||||||
case self.page2:
|
case self.page2:
|
||||||
if self.page2.is_migrated():
|
if self.page2.is_migrated():
|
||||||
return self.get_n_pages() - 1
|
steam_path = lookup(self.config_path, Preferences.DEFAULT)
|
||||||
|
self.page7.set_steam_path(steam_path)
|
||||||
|
return self.get_n_pages() - 2
|
||||||
case self.page3:
|
case self.page3:
|
||||||
self.config_values["default_steam_path"] = page.get_path_from_radio()
|
self.config_values["default_steam_path"] = page.get_path_from_radio()
|
||||||
case self.page4:
|
case self.page4:
|
||||||
@ -495,10 +521,17 @@ class Assistant(Gtk.Assistant):
|
|||||||
self.config_values["use_miles"] = use_miles
|
self.config_values["use_miles"] = use_miles
|
||||||
self.config_values["client"] = client
|
self.config_values["client"] = client
|
||||||
self.write_config()
|
self.write_config()
|
||||||
|
self.page7.set_steam_path(self.config_values["default_steam_path"])
|
||||||
|
case self.page7:
|
||||||
|
self.page7.create_shortcuts()
|
||||||
|
self.setup_complete = True
|
||||||
case _:
|
case _:
|
||||||
raise AttributeError("Trying to advance a non-canonical page")
|
raise AttributeError("Trying to advance a non-canonical page")
|
||||||
return index + 1
|
return index + 1
|
||||||
|
|
||||||
|
def is_setup_complete(self) -> bool:
|
||||||
|
return self.setup_complete
|
||||||
|
|
||||||
def destroy_and_quit(self, widget: Self) -> None:
|
def destroy_and_quit(self, widget: Self) -> None:
|
||||||
self.destroy()
|
self.destroy()
|
||||||
Gtk.main_quit()
|
Gtk.main_quit()
|
||||||
@ -543,6 +576,90 @@ class Assistant(Gtk.Assistant):
|
|||||||
EMITTER.emit("step_pending")
|
EMITTER.emit("step_pending")
|
||||||
|
|
||||||
|
|
||||||
|
class CheckboxWithLabel(Gtk.Box):
|
||||||
|
def __init__(self, text: str, blurb_text: str) -> None:
|
||||||
|
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=5)
|
||||||
|
|
||||||
|
self.button = Gtk.CheckButton(label=text)
|
||||||
|
self.button.set_active(True)
|
||||||
|
label = Gtk.Label(label="", halign=Gtk.Align.START, margin_start=20)
|
||||||
|
wrapped = textwrap.fill(blurb_text, width=100)
|
||||||
|
label.set_markup(f"- {wrapped}")
|
||||||
|
|
||||||
|
for el in self.button, label:
|
||||||
|
self.add(el)
|
||||||
|
|
||||||
|
def get_checkbox(self) -> Gtk.CheckButton:
|
||||||
|
return self.button
|
||||||
|
|
||||||
|
def get_active(self) -> bool:
|
||||||
|
return self.button.get_active()
|
||||||
|
|
||||||
|
def set_active(self, state: bool) -> None:
|
||||||
|
self.button.set_active(state)
|
||||||
|
|
||||||
|
|
||||||
|
class ShortcutCreationPage(ScrolledWizardPage):
|
||||||
|
def __init__(self, shortcut: Path) -> None:
|
||||||
|
super().__init__(
|
||||||
|
enum=PageNum.SHORTCUTS,
|
||||||
|
heading=wizard.heading_shortcuts,
|
||||||
|
description=wizard.blurb_shortcuts,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.steam_path: Path
|
||||||
|
self.shortcut_path = shortcut
|
||||||
|
self.checks_area = Gtk.Box(
|
||||||
|
orientation=Gtk.Orientation.VERTICAL, spacing=10, margin_top=20
|
||||||
|
)
|
||||||
|
self.page_type = Gtk.AssistantPageType.INTRO
|
||||||
|
|
||||||
|
label, blurb = wizard.checkbox_steam_shortcut
|
||||||
|
self.steam_checkbox = CheckboxWithLabel(label, blurb)
|
||||||
|
|
||||||
|
label, blurb = wizard.checkbox_start_menu
|
||||||
|
self.start_menu_checkbox = CheckboxWithLabel(label, blurb)
|
||||||
|
cb = self.start_menu_checkbox.get_checkbox()
|
||||||
|
cb.connect("toggled", self._on_start_menu_toggled)
|
||||||
|
|
||||||
|
label, blurb = wizard.checkbox_desktop_shortcut
|
||||||
|
self.desktop_checkbox = CheckboxWithLabel(label, blurb)
|
||||||
|
|
||||||
|
for el in (
|
||||||
|
self.steam_checkbox,
|
||||||
|
self.start_menu_checkbox,
|
||||||
|
self.desktop_checkbox,
|
||||||
|
):
|
||||||
|
self.checks_area.add(el)
|
||||||
|
|
||||||
|
self.add_start(self.checks_area)
|
||||||
|
self.show_all()
|
||||||
|
self.connect("map", self._on_map)
|
||||||
|
|
||||||
|
def _on_start_menu_toggled(self, button: Gtk.CheckButton) -> None:
|
||||||
|
state = button.get_active()
|
||||||
|
if not state:
|
||||||
|
self.desktop_checkbox.set_active(state)
|
||||||
|
self.desktop_checkbox.set_sensitive(state)
|
||||||
|
|
||||||
|
def _on_map(self, page: "ScrolledWizardPage") -> None:
|
||||||
|
EMITTER.emit("step_complete")
|
||||||
|
|
||||||
|
def set_steam_path(self, path: Path) -> None:
|
||||||
|
self.steam_path = path
|
||||||
|
|
||||||
|
def create_shortcuts(self) -> None:
|
||||||
|
# NOTE: best-effort, permissive even on failure (page is already marked as complete)
|
||||||
|
if self.steam_checkbox.get_active():
|
||||||
|
add_steam_shortcut(self.steam_path, self.shortcut_path)
|
||||||
|
|
||||||
|
if self.start_menu_checkbox.get_active():
|
||||||
|
desktop_file = freedesktop.write_desktop_file(self.shortcut_path)
|
||||||
|
|
||||||
|
if self.desktop_checkbox.get_active():
|
||||||
|
freedesktop.write_desktop_shortcut(desktop_file)
|
||||||
|
|
||||||
|
|
||||||
class SteamPathPage(ScrolledWizardPage):
|
class SteamPathPage(ScrolledWizardPage):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@ -599,17 +716,20 @@ class SteamPathPage(ScrolledWizardPage):
|
|||||||
|
|
||||||
|
|
||||||
class SetupWizard(Gtk.Application):
|
class SetupWizard(Gtk.Application):
|
||||||
def __init__(self, is_deck: bool, config: Path) -> None:
|
def __init__(self, is_deck: bool, XDG: "Xdg") -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
GLib.set_prgname(APP_NAME)
|
GLib.set_prgname(APP_NAME)
|
||||||
Window(is_deck, config)
|
self.win = Window(is_deck, XDG)
|
||||||
Gtk.main()
|
Gtk.main()
|
||||||
|
|
||||||
|
def is_setup_complete(self) -> int:
|
||||||
|
return self.win.assistant.is_setup_complete()
|
||||||
|
|
||||||
|
|
||||||
class Window(Gtk.Window):
|
class Window(Gtk.Window):
|
||||||
def __init__(self, is_deck: bool, config: Path) -> None:
|
def __init__(self, is_deck: bool, XDG: "Xdg") -> None:
|
||||||
super().__init__(title=APP_NAME, icon_name=APP_NAME)
|
super().__init__(title=APP_NAME, icon_name=APP_NAME)
|
||||||
Assistant(is_deck, config)
|
self.assistant = Assistant(is_deck, XDG)
|
||||||
|
|
||||||
|
|
||||||
class Emitter(GObject.GObject):
|
class Emitter(GObject.GObject):
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from importlib import resources
|
from importlib import resources
|
||||||
@ -32,15 +33,63 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
|||||||
|
|
||||||
# TODO: should long text be wrapped?
|
# TODO: should long text be wrapped?
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
formatted = format_pango(changelog)
|
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10)
|
||||||
self.changelog_label = Gtk.Label(valign=Gtk.Align.START, margin=15)
|
|
||||||
self.changelog_label.set_markup(formatted)
|
|
||||||
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
||||||
self.box.add(self.changelog_label)
|
|
||||||
self.add(self.box)
|
self.add(self.box)
|
||||||
|
|
||||||
self.connect("key-press-event", self._on_keypress)
|
self.connect("key-press-event", self._on_keypress)
|
||||||
self.connect("key-press-event", self._on_esc_keypress)
|
self.connect("key-press-event", self._on_esc_keypress)
|
||||||
|
|
||||||
|
changes = self._parse(changelog)
|
||||||
|
self._generate_nodes(changes)
|
||||||
|
self.show_all()
|
||||||
|
|
||||||
def grab_content_area(self) -> None:
|
def grab_content_area(self) -> None:
|
||||||
self.grab_focus()
|
self.grab_focus()
|
||||||
|
|
||||||
|
def _parse_version(self, version: str) -> tuple[str, str]:
|
||||||
|
pattern = r"(^##\s\[)(.+)(\] )(.+$)"
|
||||||
|
match = re.match(pattern, version)
|
||||||
|
if match:
|
||||||
|
version = match.group(2)
|
||||||
|
date = match.group(4)
|
||||||
|
return version, date
|
||||||
|
raise ValueError("Changelog parse error")
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_nodes(self, releases: list[tuple[str, list[str]]]) -> None:
|
||||||
|
for header, changes in releases:
|
||||||
|
for ind in (0, -1):
|
||||||
|
if changes[ind] == "":
|
||||||
|
del changes[ind]
|
||||||
|
text = "\n".join(changes)
|
||||||
|
formatted = format_pango(text)
|
||||||
|
|
||||||
|
label = Gtk.Label(valign=Gtk.Align.START, margin=15, halign=Gtk.Align.START)
|
||||||
|
label.set_markup(formatted)
|
||||||
|
|
||||||
|
version, date = self._parse_version(header)
|
||||||
|
button_text = f"{version} ({date})"
|
||||||
|
button = Gtk.Button(label=button_text, margin_start=5)
|
||||||
|
|
||||||
|
expander = Gtk.Expander()
|
||||||
|
expander.set_label_widget(button)
|
||||||
|
expander.add(label)
|
||||||
|
self.box.add(expander)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse(self, changelog: str) -> list[tuple[str, list[str]]]:
|
||||||
|
release = ""
|
||||||
|
releases: list[tuple[str, list[str]]] = []
|
||||||
|
release_notes: list[str] = []
|
||||||
|
for line in changelog.splitlines():
|
||||||
|
if line.startswith("# "):
|
||||||
|
continue
|
||||||
|
if line.startswith("## "):
|
||||||
|
if release != "":
|
||||||
|
releases.append((release, release_notes))
|
||||||
|
release_notes = []
|
||||||
|
release = ""
|
||||||
|
release = line
|
||||||
|
continue
|
||||||
|
release_notes.append(line.rstrip())
|
||||||
|
return releases
|
||||||
|
|||||||
@ -272,6 +272,15 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
"It looks like DayZ is already running in the background. Exit DayZ before connecting."
|
"It looks like DayZ is already running in the background. Exit DayZ before connecting."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
allows_dl, running_app = prereqs.allows_downloads
|
||||||
|
if allows_dl is False:
|
||||||
|
msg = (
|
||||||
|
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"
|
||||||
|
"Otherwise, mods may be queued for download but never update."
|
||||||
|
)
|
||||||
|
warnings.append(msg)
|
||||||
|
|
||||||
self.add_warnings(warnings)
|
self.add_warnings(warnings)
|
||||||
self.add_errors(errors)
|
self.add_errors(errors)
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
|
|||||||
authors = [
|
authors = [
|
||||||
{name = "aclist"}
|
{name = "aclist"}
|
||||||
]
|
]
|
||||||
version = "7.0.0b15"
|
version = "7.0.0b16"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@ -27,6 +27,7 @@ dependencies = [
|
|||||||
"psutil==7.1.3",
|
"psutil==7.1.3",
|
||||||
"python-a2s",
|
"python-a2s",
|
||||||
"requests==2.32.5",
|
"requests==2.32.5",
|
||||||
|
"vdf==3.4"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
13
tests/fixtures/api/client_allows_downloads.vdf
vendored
Normal file
13
tests/fixtures/api/client_allows_downloads.vdf
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
"InstallConfigStore"
|
||||||
|
{
|
||||||
|
"Software"
|
||||||
|
{
|
||||||
|
"Valve"
|
||||||
|
{
|
||||||
|
"Steam"
|
||||||
|
{
|
||||||
|
"AllowDownloadsDuringGameplay" "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
tests/fixtures/api/client_disallows_downloads.vdf
vendored
Normal file
13
tests/fixtures/api/client_disallows_downloads.vdf
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
"InstallConfigStore"
|
||||||
|
{
|
||||||
|
"Software"
|
||||||
|
{
|
||||||
|
"Valve"
|
||||||
|
{
|
||||||
|
"Steam"
|
||||||
|
{
|
||||||
|
"AllowDownloadsDuringGameplay" "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
tests/fixtures/api/no_shortcuts.vdf
vendored
Normal file
BIN
tests/fixtures/api/no_shortcuts.vdf
vendored
Normal file
Binary file not shown.
53
tests/fixtures/api/steamapps/appmanifest_111.acf
vendored
Normal file
53
tests/fixtures/api/steamapps/appmanifest_111.acf
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
"AppState"
|
||||||
|
{
|
||||||
|
"appid" "111"
|
||||||
|
"Universe" "1"
|
||||||
|
"name" "TEST APP"
|
||||||
|
"StateFlags" "4"
|
||||||
|
"installdir" "TESTAPP"
|
||||||
|
"LastUpdated" "0"
|
||||||
|
"LastPlayed" "0"
|
||||||
|
"SizeOnDisk" "0"
|
||||||
|
"StagingSize" "0"
|
||||||
|
"buildid" "0"
|
||||||
|
"LastOwner" "0"
|
||||||
|
"DownloadType" "2"
|
||||||
|
"UpdateResult" "0"
|
||||||
|
"BytesToDownload" "0"
|
||||||
|
"BytesDownloaded" "0"
|
||||||
|
"BytesToStage" "0"
|
||||||
|
"BytesStaged" "0"
|
||||||
|
"TargetBuildID" "0"
|
||||||
|
"AutoUpdateBehavior" "0"
|
||||||
|
"AllowOtherDownloadsWhileRunning" "1"
|
||||||
|
"ScheduledAutoUpdate" "0"
|
||||||
|
"FullValidateAfterNextUpdate" "1"
|
||||||
|
"InstalledDepots"
|
||||||
|
{
|
||||||
|
"1110"
|
||||||
|
{
|
||||||
|
"manifest" "0"
|
||||||
|
"size" "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"SharedDepots"
|
||||||
|
{
|
||||||
|
"228983" "228980"
|
||||||
|
"228985" "228980"
|
||||||
|
"228988" "228980"
|
||||||
|
"228990" "228980"
|
||||||
|
"229004" "228980"
|
||||||
|
}
|
||||||
|
"UserConfig"
|
||||||
|
{
|
||||||
|
"language" "english"
|
||||||
|
"platform_override_dest" "linux"
|
||||||
|
"platform_override_source" "windows"
|
||||||
|
}
|
||||||
|
"MountedConfig"
|
||||||
|
{
|
||||||
|
"language" "english"
|
||||||
|
"platform_override_dest" "linux"
|
||||||
|
"platform_override_source" "windows"
|
||||||
|
}
|
||||||
|
}
|
||||||
53
tests/fixtures/api/steamapps/appmanifest_222.acf
vendored
Normal file
53
tests/fixtures/api/steamapps/appmanifest_222.acf
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
"AppState"
|
||||||
|
{
|
||||||
|
"appid" "222"
|
||||||
|
"Universe" "1"
|
||||||
|
"name" "TEST APP"
|
||||||
|
"StateFlags" "4"
|
||||||
|
"installdir" "TESTAPP"
|
||||||
|
"LastUpdated" "0"
|
||||||
|
"LastPlayed" "0"
|
||||||
|
"SizeOnDisk" "0"
|
||||||
|
"StagingSize" "0"
|
||||||
|
"buildid" "0"
|
||||||
|
"LastOwner" "0"
|
||||||
|
"DownloadType" "2"
|
||||||
|
"UpdateResult" "0"
|
||||||
|
"BytesToDownload" "0"
|
||||||
|
"BytesDownloaded" "0"
|
||||||
|
"BytesToStage" "0"
|
||||||
|
"BytesStaged" "0"
|
||||||
|
"TargetBuildID" "0"
|
||||||
|
"AutoUpdateBehavior" "0"
|
||||||
|
"AllowOtherDownloadsWhileRunning" "2"
|
||||||
|
"ScheduledAutoUpdate" "0"
|
||||||
|
"FullValidateAfterNextUpdate" "1"
|
||||||
|
"InstalledDepots"
|
||||||
|
{
|
||||||
|
"2220"
|
||||||
|
{
|
||||||
|
"manifest" "0"
|
||||||
|
"size" "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"SharedDepots"
|
||||||
|
{
|
||||||
|
"228983" "228980"
|
||||||
|
"228985" "228980"
|
||||||
|
"228988" "228980"
|
||||||
|
"228990" "228980"
|
||||||
|
"229004" "228980"
|
||||||
|
}
|
||||||
|
"UserConfig"
|
||||||
|
{
|
||||||
|
"language" "english"
|
||||||
|
"platform_override_dest" "linux"
|
||||||
|
"platform_override_source" "windows"
|
||||||
|
}
|
||||||
|
"MountedConfig"
|
||||||
|
{
|
||||||
|
"language" "english"
|
||||||
|
"platform_override_dest" "linux"
|
||||||
|
"platform_override_source" "windows"
|
||||||
|
}
|
||||||
|
}
|
||||||
53
tests/fixtures/api/steamapps/appmanifest_333.acf
vendored
Normal file
53
tests/fixtures/api/steamapps/appmanifest_333.acf
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
"AppState"
|
||||||
|
{
|
||||||
|
"appid" "333"
|
||||||
|
"Universe" "1"
|
||||||
|
"name" "TEST APP"
|
||||||
|
"StateFlags" "4"
|
||||||
|
"installdir" "TESTAPP"
|
||||||
|
"LastUpdated" "0"
|
||||||
|
"LastPlayed" "0"
|
||||||
|
"SizeOnDisk" "0"
|
||||||
|
"StagingSize" "0"
|
||||||
|
"buildid" "0"
|
||||||
|
"LastOwner" "0"
|
||||||
|
"DownloadType" "2"
|
||||||
|
"UpdateResult" "0"
|
||||||
|
"BytesToDownload" "0"
|
||||||
|
"BytesDownloaded" "0"
|
||||||
|
"BytesToStage" "0"
|
||||||
|
"BytesStaged" "0"
|
||||||
|
"TargetBuildID" "0"
|
||||||
|
"AutoUpdateBehavior" "0"
|
||||||
|
"AllowOtherDownloadsWhileRunning" "0"
|
||||||
|
"ScheduledAutoUpdate" "0"
|
||||||
|
"FullValidateAfterNextUpdate" "1"
|
||||||
|
"InstalledDepots"
|
||||||
|
{
|
||||||
|
"3330"
|
||||||
|
{
|
||||||
|
"manifest" "0"
|
||||||
|
"size" "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"SharedDepots"
|
||||||
|
{
|
||||||
|
"228983" "228980"
|
||||||
|
"228985" "228980"
|
||||||
|
"228988" "228980"
|
||||||
|
"228990" "228980"
|
||||||
|
"229004" "228980"
|
||||||
|
}
|
||||||
|
"UserConfig"
|
||||||
|
{
|
||||||
|
"language" "english"
|
||||||
|
"platform_override_dest" "linux"
|
||||||
|
"platform_override_source" "windows"
|
||||||
|
}
|
||||||
|
"MountedConfig"
|
||||||
|
{
|
||||||
|
"language" "english"
|
||||||
|
"platform_override_dest" "linux"
|
||||||
|
"platform_override_source" "windows"
|
||||||
|
}
|
||||||
|
}
|
||||||
60
tests/test_app_download_settings.py
Normal file
60
tests/test_app_download_settings.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dzgui.api.steam import get_app_allows_downloads
|
||||||
|
from tests.fixtures import fixture_path
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.apitest
|
||||||
|
|
||||||
|
|
||||||
|
def mock_path(p: Path, appid: int) -> Path:
|
||||||
|
return Path(fixture_path("api"))
|
||||||
|
|
||||||
|
|
||||||
|
def mock_config_allows(p: Path) -> Path:
|
||||||
|
return Path(fixture_path("api/client_allows_downloads.vdf"))
|
||||||
|
|
||||||
|
|
||||||
|
def mock_config_disallows(p: Path) -> Path:
|
||||||
|
return Path(fixture_path("api/client_disallows_downloads.vdf"))
|
||||||
|
|
||||||
|
|
||||||
|
def mock_config_missing(p: Path) -> Path:
|
||||||
|
return Path(fixture_path(""))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client_allows() -> Path:
|
||||||
|
return Path(fixture_path("api/client_allows_downloads.vdf"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def steam_path() -> Path:
|
||||||
|
return Path(fixture_path("api"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module", autouse=True)
|
||||||
|
def patch_api() -> None:
|
||||||
|
mp = MonkeyPatch()
|
||||||
|
mp.setattr("dzgui.api.steam.get_app_path", mock_path)
|
||||||
|
yield
|
||||||
|
mp.undo()
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_allows_downloads(steam_path) -> None:
|
||||||
|
assert get_app_allows_downloads(steam_path, 111) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_disallows_downloads(steam_path):
|
||||||
|
assert get_app_allows_downloads(steam_path, 222) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_delegates_downloads(monkeypatch, steam_path, client_allows):
|
||||||
|
monkeypatch.setattr("dzgui.api.steam.get_config", mock_config_disallows)
|
||||||
|
assert get_app_allows_downloads(steam_path, 333) is False
|
||||||
|
monkeypatch.setattr("dzgui.api.steam.get_config", mock_config_allows)
|
||||||
|
assert get_app_allows_downloads(steam_path, 333) is True
|
||||||
|
monkeypatch.setattr("dzgui.api.steam.get_config", mock_config_missing)
|
||||||
|
assert get_app_allows_downloads(steam_path, 333) is True
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import tempfile
|
import tempfile
|
||||||
import os
|
|
||||||
|
|
||||||
from dzgui.app_init import copy_bare_configs
|
from dzgui.app_init import copy_bare_configs
|
||||||
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
|
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import pytest
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import dzgui.api.pefile as PeFile
|
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.query import get_config
|
||||||
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
|
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)
|
fixture = fixture_path(fixture)
|
||||||
with pytest.raises(exception):
|
with pytest.raises(exception):
|
||||||
try:
|
try:
|
||||||
PeFile.get_app_path(fixture, APPID_DAYZ)
|
get_app_path(fixture, APPID_DAYZ)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
def test_on_second_drive(second_drive):
|
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")
|
assert path == Path("/tmp")
|
||||||
|
|||||||
67
tests/test_shortcuts.py
Normal file
67
tests/test_shortcuts.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import pytest
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
|
|
||||||
|
from dzgui.api.shortcuts import Shortcuts
|
||||||
|
from tests.fixtures import fixture_path
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.apitest
|
||||||
|
|
||||||
|
|
||||||
|
def mock_find_shortcuts(self, steam_path: Path) -> Path:
|
||||||
|
return Path(fixture_path("api/no_shortcuts.vdf"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module", autouse=True)
|
||||||
|
def patch_api() -> None:
|
||||||
|
mp = MonkeyPatch()
|
||||||
|
mp.setattr("dzgui.api.shortcuts.Shortcuts.find_shortcuts_path", mock_find_shortcuts)
|
||||||
|
yield
|
||||||
|
mp.undo()
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_shortcuts(monkeypatch) -> None:
|
||||||
|
s = Shortcuts(Path(""))
|
||||||
|
assert len(s.shortcuts["shortcuts"]) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dummy_app() -> None:
|
||||||
|
d = {
|
||||||
|
"appname": "TEST APP",
|
||||||
|
"StartDir": "TEST_DIR",
|
||||||
|
"exe": "TEST_DIR/TEST_EXE.EXE",
|
||||||
|
"icon": "IMAGES_DIR/TEST_IMAGE.PNG",
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
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] == '"'
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_shortcut(dummy_app) -> None:
|
||||||
|
s = Shortcuts(Path(""))
|
||||||
|
s.add_shortcut(*dummy_app.values())
|
||||||
|
new = s.shortcuts["shortcuts"]
|
||||||
|
ind = str(len(new) - 1)
|
||||||
|
for k, v in dummy_app.items():
|
||||||
|
if k == "exe":
|
||||||
|
v = f'"{v}"'
|
||||||
|
assert new[ind][k] == v
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_shortcut(dummy_app) -> None:
|
||||||
|
s = Shortcuts(Path(""))
|
||||||
|
s.add_shortcut(*dummy_app.values())
|
||||||
|
with tempfile.NamedTemporaryFile() as f:
|
||||||
|
tmp = Path(f.name)
|
||||||
|
s.shortcuts_path = tmp
|
||||||
|
s.save_shortcuts()
|
||||||
|
s._load_shortcuts(tmp)
|
||||||
|
assert len(s.shortcuts["shortcuts"]) == 1
|
||||||
Loading…
Reference in New Issue
Block a user