Compare commits

..
93 Commits
Author SHA1 Message Date
aclist 1154789f8b Merge pull request #389 from aclist/fix/defer-import
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
fix: defer flock action until GUI is loaded
2026-06-25 00:05:27 +09:00
aclist cbccd4c5f6 fix: defer flock action until GUI is loaded 2026-06-25 00:04:18 +09:00
aclist 8cac752a0d Merge pull request #388 from aclist/prerelease/7.0.0b14
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
chore: bump version
2026-06-24 22:07:38 +09:00
aclist 580273053c chore: bump version 2026-06-24 21:43:45 +09:00
aclist 8d75c1f7b2 Merge pull request #386 from aclist/feat/libgi-preboot
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
feat: libgi preboot test
2026-06-24 01:06:51 +09:00
aclist 320c2f692e chore: more explicit library name 2026-06-24 01:05:12 +09:00
aclist 413d84b2b5 chore: reword comment 2026-06-24 01:04:07 +09:00
aclist c751b277d0 fix: unhandled exception 2026-06-24 00:59:56 +09:00
aclist 7f36a3acad fix: mislabeled keybindings 2026-06-23 23:02:39 +09:00
aclist 5780d3daff feat: abort gracefully if libgirepository is missing 2026-06-23 23:02:18 +09:00
aclist e692e1d139 Merge pull request #385 from aclist/fix/maps-combo-unloaded
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
fix: maps combo not updating on unloaded model
2026-06-23 19:30:36 +09:00
aclist 38d44a7952 chore: drop unused string 2026-06-23 19:29:23 +09:00
aclist 611e3e6e58 fix: drop extraneous chained thread 2026-06-23 19:26:57 +09:00
aclist d5b415316e fix: update dialog string 2026-06-23 19:17:47 +09:00
aclist 0eeec490c2 chore: clear typehinting errors 2026-06-23 19:13:15 +09:00
aclist 782157006a fix: regression introduced in #382 2026-06-23 19:12:30 +09:00
aclist 3be7bc0e02 fix: reinit map store on empty model (#359) 2026-06-23 19:12:06 +09:00
aclist 9e7102aaba fix: wipe cache on refresh action (#384) 2026-06-23 19:11:30 +09:00
aclist 6516e7ae3c Merge pull request #383 from aclist/fix/block-refresh
fix: do not trigger decrement if tree is unloaded
2026-06-23 18:29:55 +09:00
aclist 46c1da232e fix: do not trigger decrement if tree is unloaded (#360) 2026-06-23 18:27:34 +09:00
aclist 5280121e43 Merge pull request #382 from aclist/fix/needs-update-column
fix: use unified mod list logic
2026-06-23 18:26:45 +09:00
aclist 17c63cf85f chore: drop unused code 2026-06-23 18:26:18 +09:00
aclist 488d98800f chore: clear typehinting errors 2026-06-23 18:14:37 +09:00
aclist 4eac5055cb fix: use unified logic for modlist queries (#354) 2026-06-23 18:09:57 +09:00
aclist 81168d3952 Merge pull request #381 from aclist/fix/modpanel-sens
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
fix: toggle mod panel sensitivity on map
2026-06-23 00:37:40 +09:00
aclist ee90be42ee fix: toggle mod panel sensitivity on map 2026-06-23 00:18:02 +09:00
aclist 48f341bae2 Merge pull request #379 from aclist/fix/synch-icons
fix: synch icons
2026-06-22 22:59:38 +09:00
aclist 0653f064b2 chore: add tests for mod signatures
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-06-22 22:55:44 +09:00
aclist 525dc24508 fix: missing import 2026-06-22 17:02:39 +09:00
aclist 4e7c831a53 fix: pass mods and iters to callback 2026-06-22 17:02:27 +09:00
aclist e41bef7b00 fix: use list comprehension 2026-06-22 17:01:45 +09:00
aclist 27136a0906 Merge pull request #378 from aclist/chore/log-exception
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
chore: log exception when launch process cannot be found
2026-06-22 00:02:05 +09:00
aclist cd95a71782 chore: log exception when launch process cannot be found 2026-06-21 23:58:44 +09:00
aclist 0c014a7859 Merge pull request #377 from aclist/fix/update-symlinks
fix: rebuild symlinks on load
2026-06-21 23:58:07 +09:00
aclist 65c159505f Merge branch 'dzgui7' into fix/update-symlinks 2026-06-21 23:54:32 +09:00
aclist e056ac924a Merge pull request #376 from aclist/fix/missing-config-key
fix: missing config key
2026-06-21 23:52:53 +09:00
aclist d540680330 Merge pull request #374 from aclist/fix/show-mod-progress
fix: show mod progress
2026-06-21 23:52:30 +09:00
aclist 98c45afe7f chore: drop print statement
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-06-21 23:51:42 +09:00
aclist e86040747f fix: rebuild symlinks on load 2026-06-21 23:50:18 +09:00
aclist a559485d0a chore: change structure of fixtures 2026-06-21 22:20:38 +09:00
aclist 8695793af4 chore: add config test 2026-06-21 22:18:35 +09:00
aclist a2aeac0242 fix: workaround for circular import in tests 2026-06-21 22:18:02 +09:00
aclist 879d0650d4 fix: insert missing values prior to serialization 2026-06-21 22:17:38 +09:00
aclist 5c85c746f1 fix: update button label conditionally
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-06-20 23:28:48 +09:00
aclist fa8792ba69 chore: add logging when opening options page 2026-06-20 19:45:27 +09:00
aclist 5e3392ddec chore: drop unused import 2026-06-20 18:30:35 +09:00
aclist 796f6b8435 feat: call emitter from ThreadingManager 2026-06-20 18:30:08 +09:00
aclist 780402d14f change: ease rate limit 2026-06-20 18:29:54 +09:00
aclist 3ae0c8428a change: ease rate limit 2026-06-20 18:29:39 +09:00
aclist ed20a7cc84 fix: update table checkmarks when mods are synched (#370) 2026-06-20 18:28:59 +09:00
aclist 9225f3bc06 chore: update strings 2026-06-20 18:27:46 +09:00
aclist 075a2e5a74 chore: update rate limit consts 2026-06-20 18:26:49 +09:00
aclist 58a7878f60 fix: append mission param dynamically (#372) 2026-06-20 18:26:34 +09:00
aclist f839001097 Merge pull request #368 from aclist/fix/offline-symlinks
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: rebuild symlinks when launching offline
2026-06-18 01:08:10 +09:00
aclist ab2b651b6c chore: bump version 2026-06-18 01:07:00 +09:00
aclist e5934848c7 fix: rebuild symlinks when launching 2026-06-18 01:06:49 +09:00
aclist cab10efc96 Merge pull request #367 from aclist/prerelease/7.0.0b12
Prerelease/7.0.0b12
2026-06-17 22:22:04 +09:00
aclist 0a4efba905 docs: add user to thanks 2026-06-17 22:16:00 +09:00
aclist 1e4e09e52c docs: update changelog 2026-06-17 22:14:56 +09:00
aclist 910c068109 chore: bump version 2026-06-17 22:14:30 +09:00
aclist 1dd773a9bf Merge pull request #366 from aclist/fix/validate-offline-mods
fix: validate custom folders before launch
2026-06-17 22:13:56 +09:00
aclist f4d065d766 fix: typo 2026-06-17 22:13:17 +09:00
aclist 1fe66076c6 fix: validate custom folders before launch 2026-06-17 22:12:12 +09:00
aclist 668ec1d7a0 Merge pull request #365 from aclist/fix/unsub
Fix/unsub
2026-06-17 21:43:11 +09:00
aclist 412f0d3193 chore: clear typehinting errors
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-06-17 21:42:02 +09:00
aclist 894a9b106d chore: move HBox, VBox classes 2026-06-17 21:41:34 +09:00
aclist 1b0a9c9a69 chore: drop foreground logic 2026-06-17 21:33:49 +09:00
aclist c4818d2097 fix: name collision between function signature and variable 2026-06-17 21:33:28 +09:00
aclist 69135150c7 chore: add deprecaiton warning
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-06-17 03:16:44 +09:00
aclist 4d8949427b feat: acf parser 2026-06-17 03:15:57 +09:00
aclist 0139f6bfba fix: cast str to int 2026-06-17 02:57:50 +09:00
aclist 8d77d46eda feat: unsubscribe from mods gracefully 2026-06-17 02:56:26 +09:00
aclist 179f92234a chore: clear typehinting errors 2026-06-17 02:55:13 +09:00
aclist 650a5ecd7b chore: drop unused imports 2026-06-17 02:54:50 +09:00
aclist 8cebb362e9 chore: clear typehinting errors 2026-06-17 02:53:32 +09:00
aclist bfd1d3e0f9 change: move workshop button to mods page 2026-06-17 02:52:57 +09:00
aclist e8b18d83a5 Merge pull request #354 from aclist/feat/launch-offline
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
feat: launch offline
2026-06-14 22:03:17 +09:00
aclist d305f47bb9 Merge branch 'dzgui7' into feat/launch-offline
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-06-14 22:03:02 +09:00
aclist 43f6c17c4b chore: port changes from dzgui7 branch 2026-06-14 22:02:21 +09:00
aclist 38bfef3836 chore: clear typehinting errors 2026-06-14 21:56:49 +09:00
aclist c818945a28 chore: add comment 2026-06-14 21:54:59 +09:00
aclist 5cb4f69500 fix: initialize string 2026-06-14 19:45:48 +09:00
aclist cee9052dea fix: append store to mod manager 2026-06-14 19:31:52 +09:00
aclist b3b98aa407 fix: parse parent dir 2026-06-14 19:11:35 +09:00
aclist b8230cbf43 feat: make symlink to mission path
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
2026-06-14 01:54:08 +09:00
aclist 20dab38fff Merge pull request #351 from aclist/fix/enqueue-mods
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
fix: enqueue mods
2026-06-07 21:59:49 +09:00
aclist 1ddc4a5ff1 chore: bump version 2026-06-07 21:58:44 +09:00
aclist d605447fca chore: clear typehinting errors 2026-06-07 21:51:55 +09:00
aclist 94fe3f9dff fix: suppress search popup (#353) 2026-06-07 21:49:41 +09:00
aclist a966993999 fix: invoke steam client directly (#348) 2026-06-07 21:21:49 +09:00
aclist 8ee3b36816 fix: restore mod statusbar when returning from other pages 2026-06-07 21:20:56 +09:00
aclist 3af53775cd fix: grab focus on tree 2026-06-07 21:20:32 +09:00
aclist e2d6a462db fix: suppress headers if no model is loaded 2026-06-07 20:23:13 +09:00
54 changed files with 785 additions and 329 deletions

No files matched your search

+1
View File
@@ -57,6 +57,7 @@
- Disable overlay scrollbars on server tables - Disable overlay scrollbars on server tables
- Reduce size of geolocation DB on disk (~100MB) - Reduce size of geolocation DB on disk (~100MB)
- 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
## Dropped ## Dropped
- Debug mode - Debug mode
+96
View File
@@ -0,0 +1,96 @@
import re
from collections.abc import Iterator
from typing import Any
from warnings import deprecated
@deprecated("Use dzgui.api.steam.unsubscribe()")
class WorkshopACF:
def __init__(self, file: str) -> None:
super().__init__()
self.dict: dict[str, Any]
self.load(file)
def as_dict(self) -> dict[str, Any]:
return self.dict
def load(self, file: str) -> None:
delimiter = r"\t\t"
lines = []
with open(file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
els = re.split(delimiter, line, maxsplit=1)
lines.append(els)
self.dict = self.parse(iter(lines))
def parse(self, lines: Iterator[list[str]]) -> dict[str, str]:
acf: dict[str, Any] = {}
try:
while True:
line = next(lines)
if len(line) == 1:
key = line[0]
if key == "{":
continue
return acf
elif key == "}":
return acf
else:
key = self.dequote(key)
acf[key] = self.parse(lines)
elif len(line) == 2:
k, v = line
k = self.dequote(k)
v = self.dequote(v)
try:
n = list(acf.keys())[-1]
acf[n][k] = v
except Exception:
acf[k] = v
except StopIteration:
return acf
def unpack(self, d: dict, lines: list[Any] = []) -> str:
t1 = "AppWorkshop"
t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails")
for k, v in d.items():
if type(v) is dict:
if k in t1:
self.indent = 0
elif k in t2:
self.indent = 1
else:
self.indent = 2
pref = "\t" * self.indent
lines.append(pref + self.enquote(k))
lines.append(pref + "{")
self.unpack(v, lines)
lines.append(pref + "}")
else:
pref = "\t" * (self.indent + 1)
lines.append(pref + self.enquote(k) + "\t\t" + self.enquote(v))
s = ""
for line in lines:
s += line + "\n"
return s
def to_file(self, file: str) -> None:
s = self.unpack(self.dict)
with open(file, "w") as f:
f.write(s)
@classmethod
def enquote(cls, s: str) -> str:
return f'"{s}"'
@classmethod
def dequote(cls, s: str) -> str:
return s.rstrip('"').lstrip('"')
def delete(self, modid: int) -> None:
del self.dict["AppWorkshop"]["WorkshopItemsInstalled"][modid]
del self.dict["AppWorkshop"]["WorkshopItemDetails"][modid]
+22 -7
View File
@@ -12,6 +12,7 @@ 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,
LIBRARYFOLDERS_PATH, LIBRARYFOLDERS_PATH,
WORKSHOP_PATH, WORKSHOP_PATH,
) )
@@ -48,14 +49,19 @@ def get_local_mods(workshop_path: Path) -> list[Path]:
mods = [file for file in workshop_path.iterdir() if file.is_dir()] mods = [file for file in workshop_path.iterdir() if file.is_dir()]
return mods return mods
def is_mission(path: Path) -> bool: def is_mission(path: Path) -> bool:
# TODO: parse integrity of other files # TODO: parse integrity of other files
parent = path.parent.name
if parent != DAYZ_COMMUNITY_ROOT:
return False
file = path / "init.c" file = path / "init.c"
return file.exists() return file.exists()
def tokenize(mod: Path) -> dict[str, Any] | None: def tokenize(mod: Path) -> dict[str, Any] | None:
file = mod.joinpath("meta.cpp") file = mod.joinpath("meta.cpp")
delimiter=r"\s*=\s*" delimiter = r"\s*=\s*"
modmeta = {} modmeta = {}
try: try:
with open(file, "r", encoding="utf-8") as f: with open(file, "r", encoding="utf-8") as f:
@@ -72,6 +78,7 @@ def tokenize(mod: Path) -> dict[str, Any] | None:
logger.critical(e) logger.critical(e)
return None return None
def get_mod_size(path: Path) -> float: def get_mod_size(path: Path) -> float:
s = 0 s = 0
for f in path.rglob("*"): for f in path.rglob("*"):
@@ -81,6 +88,7 @@ def get_mod_size(path: Path) -> float:
def get_custom_mods(path: Path) -> list[Any]: def get_custom_mods(path: Path) -> list[Any]:
# TODO: rename this function for clarity ("get local vs. get custom")
mods = get_local_mods(path) mods = get_local_mods(path)
# TODO: error handling # TODO: error handling
return parse_mods(mods, use_custom=True) return parse_mods(mods, use_custom=True)
@@ -118,20 +126,27 @@ def _hash(uid: str, use_custom: bool = False) -> str:
return prefix + md5.hexdigest()[:8] return prefix + md5.hexdigest()[:8]
def remove_stale_signatures(config: Path, versions: Path) -> None: def remove_stale_signatures(
config: Path, versions: Path, ids: list[int] | None = None
) -> None:
if versions.is_file() is False: if versions.is_file() is False:
logger.warning("Creating new version signatures file") logger.warning("Creating new version signatures file")
versions.touch() versions.touch()
return return
path = lookup(config, Preferences.DEFAULT) path = lookup(config, Preferences.DEFAULT)
steam_path = Path(path) steam_path = Path(path)
ids = get_local_mod_ids(steam_path) if ids is None:
local_ids = get_local_mod_ids(steam_path)
with open(versions, "r") as f: with open(versions, "r") as f:
lines = f.readlines() lines = f.readlines()
for line in lines:
uid = int(line.split(",")[0]) if ids is None:
if uid not in ids: lines = [line for line in lines if int(line.split(",")[0]) in local_ids]
lines.remove(line) else:
lines = [line for line in lines if int(line.split(",")[0]) not in ids]
with open(versions, "w") as f: with open(versions, "w") as f:
for line in lines: for line in lines:
f.write(line) f.write(line)
+2 -1
View File
@@ -289,13 +289,14 @@ def query_direct(ip: str, qport: int, timeout: float = 3.0) -> dict[str, Any] |
def get_details(record: Record) -> Details: def get_details(record: Record) -> Details:
# TODO: consolidate try/except statements
ip = record.ip ip = record.ip
qport = record.qport qport = record.qport
default_str = strings.none_provided default_str = strings.none_provided
try: try:
info = a2s.info((ip, qport)) info = a2s.info((ip, qport))
except TimeoutError: except (TimeoutError, a2s.exceptions.BrokenMessageError):
return Details(None, default_str, False) return Details(None, default_str, False)
try: try:
rules = dayzquery.dayz_rules((ip, qport)) rules = dayzquery.dayz_rules((ip, qport))
+47 -21
View File
@@ -3,20 +3,24 @@ import logging
import os import os
import requests import requests
import subprocess import subprocess
from typing import Union
from warnings import deprecated
from shlex import shlex from shlex import shlex
from pathlib import Path 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,
APP_NAME, APP_NAME,
DEBIAN_STEAM_PATH, DEBIAN_STEAM_PATH,
DEFAULT_STEAM_PATH, DEFAULT_STEAM_PATH,
FLATPAK_STEAM_PATH, FLATPAK_STEAM_PATH,
UBUNTU_STEAM_PATH, UBUNTU_STEAM_PATH,
REQUEST_TIMEOUT,
VDF_PATH, VDF_PATH,
) )
from dzgui.const.endpoints import STEAM_PUBLISHED_FILES from dzgui.const.endpoints import 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
@@ -53,7 +57,8 @@ def get_steam_paths() -> list[tuple[Path, str]]:
def concat_mods(mods: list[str]) -> str: def concat_mods(mods: list[str]) -> str:
from dzgui.util.symlink import _hash # TODO: circular import workaround
from dzgui.api.mods import _hash
hashes = [] hashes = []
for mod in mods: for mod in mods:
@@ -68,31 +73,21 @@ def get_local_signatures(version_file: Path) -> dict[str, int]:
for line in lines: for line in lines:
data = line.split(",") data = line.split(",")
_id = data[0] _id = data[0]
_hash = int(data[1]) mod_hash = int(data[1])
hashes[_id] = _hash hashes[_id] = mod_hash
return hashes return hashes
def enqueue_mod(mod: str, appid: int) -> None:
args = [
"steam",
f"steam://url/CommunityFilePage/{mod}+workshop_download_item",
str(appid),
mod,
]
subprocess.Popen(["/usr/bin/env", "bash", *args])
def get_needs_update( def get_needs_update(
version_file: Path, remote_hashes: list[tuple[str, str, int, int]] version_file: Path, remote_hashes: list[tuple[str, str, int, int]]
) -> list[tuple[str, str, int, int]]: ) -> list[tuple[str, str, int, int]]:
local_hashes = get_local_signatures(version_file) local_stamps = get_local_signatures(version_file)
needs_update: list[tuple[str, str, int, int]] = [] needs_update: list[tuple[str, str, int, int]] = []
for title, _id, _hash, size in remote_hashes: for title, _id, stamp, size in remote_hashes:
if _id not in local_hashes: if _id not in local_stamps:
needs_update.append((title, _id, _hash, size)) needs_update.append((title, _id, stamp, size))
elif _hash != local_hashes[_id]: elif stamp != local_stamps[_id]:
needs_update.append((title, _id, _hash, size)) needs_update.append((title, _id, stamp, size))
else: else:
continue continue
return needs_update return needs_update
@@ -177,8 +172,10 @@ def launch_offline(
"-skipintro", "-skipintro",
f"-name={name}", f"-name={name}",
f"-mod={symlinks}", f"-mod={symlinks}",
f"-mission={mission}",
] ]
if len(mission) > 0:
arg = f"-mission={mission}"
params.append(arg)
proc = subprocess.run([*client_args, *params]) proc = subprocess.run([*client_args, *params])
return proc.returncode return proc.returncode
@@ -237,6 +234,29 @@ def vdf2json(path: Path) -> str:
jbuf += "\n" jbuf += "\n"
def update_workshop(key: str, mod: int, endpoint: str) -> None:
payload: dict[str, Union[int, str]] = {
"publishedfileid": mod,
"appid": APPID_DAYZ,
"key": key,
"list_type": 1,
"notify_client": 1,
}
try:
res = requests.post(endpoint, params=payload, timeout=REQUEST_TIMEOUT)
res.raise_for_status()
except Exception as e:
logger.critical(e)
def subscribe(key: str, mod: int) -> None:
update_workshop(key, mod, SUB_ENDPOINT)
def unsubscribe(key: str, mod: int) -> None:
update_workshop(key, mod, UNSUB_ENDPOINT)
def gen_shortcut() -> None: def gen_shortcut() -> None:
# TODO: # TODO:
""" """
@@ -249,3 +269,9 @@ def gen_shortcut() -> None:
# or get right-most 32 bits # or get right-most 32 bits
# STEAMID_64 & 0xFFFFFFFF # STEAMID_64 & 0xFFFFFFFF
pass pass
@deprecated("Use subscribe()")
def enqueue_mod(client: str, mod: str, appid: int) -> None:
client_args = concat_bash_args(client)
subprocess.Popen([*client_args, "+workshop_download_item", str(appid), mod])
+109
View File
@@ -0,0 +1,109 @@
import logging
import os
from typing import TYPE_CHECKING
from dzgui.const.constants import APP_NAME
from dzgui.const.enum import Preferences
from dzgui.config.query import lookup
from dzgui.config.userprefs import UserPrefs
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
from dzgui.init.migrate import (
has_new_config,
copy_state_files,
migrate_cols_file,
)
from dzgui.init.flock import lock_acquire
from dzgui.init.prereqs import has_steam_client
from dzgui.strings import boot
# from dzgui.util.map_count import get_map_count
from dzgui.util.deck import is_steam_deck, is_game_mode
from dzgui.util.localize import set_locale
from dzgui.util.strings import init
from dzgui.views.base import App
from dzgui.views.dialogs.boot import BootWindow
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
from dzgui.views.dialogs.wizard import SetupWizard
if TYPE_CHECKING:
from pathlib import Path
logger = logging.getLogger(APP_NAME)
# 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:
_format = (
"%(asctime)s␞%(levelname)s␞%(filename)s::%(funcName)s::%(lineno)s␞%(message)s"
)
fh = logging.FileHandler(log_path)
formatter = logging.Formatter(_format)
fh.setFormatter(formatter)
fh.setLevel(logging.DEBUG)
logger.setLevel(logging.DEBUG)
logger.addHandler(fh)
def load_gui(version: str, is_debug: bool) -> None:
lock = lock_acquire() # noqa
set_locale()
# NOTE: consider aborting this check if steam deck
xdg_paths = get_xdg_paths()
XDG = parse_filepaths(xdg_paths)
if XDG.resolution.parent.is_dir() is False:
make_parents(XDG.resolution)
if XDG.debug.is_file() is False:
make_parents(XDG.debug)
_is_steam_deck = is_steam_deck()
_is_game_mode = is_game_mode() if _is_steam_deck else False
if _is_game_mode:
# NOTE: this may no longer be necessary on newer versions of SteamOS
del os.environ["GTK_IM_MODULE"]
if has_new_config(XDG.config) is False:
migrate_cols_file(XDG.columns)
copy_state_files(xdg_paths["XDG_STATE_HOME"])
# TODO: add logging inside wizard
SetupWizard(_is_steam_deck, XDG.config)
# NOTE: implies that setup wizard failed or was closed
if has_new_config(XDG.config) is False:
return
setup_logger(XDG.debug)
with open(XDG.debug, "w") as f:
f.truncate(0)
if _is_steam_deck is False:
# TODO: sudo escalation dialog
# count = get_map_count()
# TODO: move into module
if has_steam_client() is False:
EarlyAlertDialog(init.requires_steam)
bootwin = BootWindow(XDG, version)
local_coords, latest_release = bootwin.get_results()
use_miles = lookup(XDG.config, Preferences.DIST)
prefs = UserPrefs(
is_steam_deck=_is_steam_deck,
is_game_mode=_is_game_mode,
is_debug=is_debug,
coords=local_coords,
version=version,
paths=XDG,
latest_release=latest_release,
use_miles=use_miles,
)
print(boot.all_ok)
App(prefs)
+6
View File
@@ -3,6 +3,8 @@ import shlex
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from dzgui.const.boilerplate import config_boilerplate
""" """
Convert legacy dztuirc to config.json Convert legacy dztuirc to config.json
@@ -89,4 +91,8 @@ def rc2json(file: Path) -> str:
keys["ip_list"] = ips keys["ip_list"] = ips
keys["use_miles"] = False keys["use_miles"] = False
keys["start_tab"] = 0 keys["start_tab"] = 0
# NOTE: workaround for DZGUI 6 logic (see #375)
for key in config_boilerplate.keys():
if key not in keys:
keys[key] = config_boilerplate[key]
return json.dumps(keys, indent=2) return json.dumps(keys, indent=2)
+4
View File
@@ -4,6 +4,8 @@ UDP_PORT = 27016
VM_FILE = "/proc/sys/vm/max_map_count" VM_FILE = "/proc/sys/vm/max_map_count"
MIN_COUNT = 1048576 MIN_COUNT = 1048576
API_RATE_LIMIT = 1
CLIENT_RATE_LIMIT = 3
REQUEST_TIMEOUT = 10 REQUEST_TIMEOUT = 10
APPNAME_DAYZ = "DayZ" APPNAME_DAYZ = "DayZ"
@@ -83,3 +85,5 @@ LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG")
TMP_PATH = "/tmp" TMP_PATH = "/tmp"
TMP_TARBALL = "/tmp/dzgui.tar.gz" TMP_TARBALL = "/tmp/dzgui.tar.gz"
TMP_EXE = "/tmp/dzgui/dzgui" TMP_EXE = "/tmp/dzgui/dzgui"
DAYZ_COMMUNITY_ROOT = "Missions"
+7 -2
View File
@@ -1,6 +1,11 @@
# internal # internal
STEAM_PUBLISHED_FILES = "https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/?format=json" STEAM_PUBLISHED_FILES = (
STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1/?" "https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1"
)
STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1"
SUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Subscribe/v1"
UNSUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/v1"
BM_SERVERS = "https://api.battlemetrics.com/servers?" BM_SERVERS = "https://api.battlemetrics.com/servers?"
GITHUB = "https://github.com/aclist" GITHUB = "https://github.com/aclist"
GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest" GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest"
+5 -5
View File
@@ -168,7 +168,7 @@ class ContextMenu(EnumWithAttrs):
COPY_LOG_CLIPBOARD = {"label": strings.copy_log} COPY_LOG_CLIPBOARD = {"label": strings.copy_log}
COPY_SERVER_IP = {"label": strings.copy_ip} COPY_SERVER_IP = {"label": strings.copy_ip}
COPY_SERVER_NAME = {"label": strings.copy_name} COPY_SERVER_NAME = {"label": strings.copy_name}
DELETE_MOD = {"label": strings.delete_mod} UNSUB_MOD = {"label": strings.unsub_mod}
OPEN_WORKSHOP = {"label": strings.open_workshop} OPEN_WORKSHOP = {"label": strings.open_workshop}
REFRESH_PLAYERS = {"label": strings.refresh_players} REFRESH_PLAYERS = {"label": strings.refresh_players}
REMOVE_HISTORY = {"label": strings.remove_history} REMOVE_HISTORY = {"label": strings.remove_history}
@@ -184,7 +184,7 @@ class ContextMenuGroup(Enum):
""" """
SERVER_MOD = (ContextMenu.OPEN_WORKSHOP,) SERVER_MOD = (ContextMenu.OPEN_WORKSHOP,)
MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.DELETE_MOD) MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.UNSUB_MOD)
MOD_OFFLINE = (None,) MOD_OFFLINE = (None,)
LOG = (ContextMenu.COPY_LOG_CLIPBOARD,) LOG = (ContextMenu.COPY_LOG_CLIPBOARD,)
SERVER_BROWSER = ( SERVER_BROWSER = (
@@ -250,9 +250,9 @@ class ModButton(EnumWithAttrs):
"label": strings.mod_panel.unhighlight_stale, "label": strings.mod_panel.unhighlight_stale,
"tooltip": strings.mod_panel.unhighlight_stale_tooltip, "tooltip": strings.mod_panel.unhighlight_stale_tooltip,
} }
DELETE_SELECTED = { UNSUB_SELECTED = {
"label": strings.mod_panel.delete_selected, "label": strings.mod_panel.unsub_selected,
"tooltip": strings.mod_panel.delete_selected_tooltip, "tooltip": strings.mod_panel.unsub_selected_tooltip,
} }
SELECT_STALE = { SELECT_STALE = {
"label": strings.mod_panel.select_stale, "label": strings.mod_panel.select_stale,
+6 -2
View File
@@ -81,8 +81,8 @@ class Emitter(GObject.GObject):
def distcalc_started(self) -> None: def distcalc_started(self) -> None:
pass pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(bool,)) @GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(bool, int))
def mod_page_toggled(self, state: bool) -> None: def mod_page_toggled(self, state: bool, mods: int) -> None:
pass pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(bool,)) @GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(bool,))
@@ -172,3 +172,7 @@ class Emitter(GObject.GObject):
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=()) @GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
def invalid_custom_mods(self) -> None: def invalid_custom_mods(self) -> None:
pass pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
def all_mods_synched(self) -> None:
pass
+10 -8
View File
@@ -23,7 +23,7 @@ from dzgui.managers.contextmenu import ContextMenuManager
from dzgui.managers.notes import NoteManager from dzgui.managers.notes import NoteManager
from dzgui.model.servers import ServerModelManager from dzgui.model.servers import ServerModelManager
from dzgui.util.diag import write_diagnostic from dzgui.util.diag import write_diagnostic
from dzgui.util.format import format_player_count from dzgui.util.format import format_exception, format_player_count
from dzgui.util.open_links import open_user_workshop, open_workshop_page from dzgui.util.open_links import open_user_workshop, open_workshop_page
from dzgui.views.dialogs.filepicker import FilePicker from dzgui.views.dialogs.filepicker import FilePicker
from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.views.dialogs.generic import ExceptionDialog
@@ -203,7 +203,9 @@ class Controller(GObject.GObject):
try: try:
# TODO: where to put config file check # TODO: where to put config file check
self.mediator.grid.notebook.settings.populate_settings() self.mediator.grid.notebook.settings.populate_settings()
except Exception: except Exception as e:
msg = format_exception(e)
logger.critical(msg)
return return
case ButtonType.MODS: case ButtonType.MODS:
self.load_mods() self.load_mods()
@@ -233,7 +235,7 @@ class Controller(GObject.GObject):
mod_man = self.mediator.modtreeview.get_mod_man() mod_man = self.mediator.modtreeview.get_mod_man()
mod_man.toggle_mod_selection(state) mod_man.toggle_mod_selection(state)
def delete_mods( def unsub_mods(
self, treeview: Union["ModTreeView", "OfflineModTreeView", None] = None self, treeview: Union["ModTreeView", "OfflineModTreeView", None] = None
) -> None: ) -> None:
if treeview is None: if treeview is None:
@@ -241,7 +243,7 @@ class Controller(GObject.GObject):
else: else:
view = treeview view = treeview
mod_man = view.get_mod_man() mod_man = view.get_mod_man()
mod_man.delete_mods() mod_man.unsub_mods()
def get_mod_store(self) -> Gtk.TreeModel | None: def get_mod_store(self) -> Gtk.TreeModel | None:
return self.mediator.modtreeview.get_model() return self.mediator.modtreeview.get_model()
@@ -523,11 +525,11 @@ class Controller(GObject.GObject):
ind = self.config_man.get_start_tab() ind = self.config_man.get_start_tab()
self.get_servers().notebook.set_current_page(ind) self.get_servers().notebook.set_current_page(ind)
def update_and_load_to_menu(self, raise_window: bool) -> None: def update_and_load_to_menu(self) -> None:
self.connection_man.update_and_connect(raise_window, menu_only=True) self.connection_man.update_and_connect(menu_only=True)
def update_and_connect(self, raise_window: bool) -> None: def update_and_connect(self) -> None:
self.connection_man.update_and_connect(raise_window) self.connection_man.update_and_connect()
def update_status(self) -> None: def update_status(self) -> None:
self.mediator.preconnect.mark_finished() self.mediator.preconnect.mark_finished()
+2
View File
@@ -40,6 +40,7 @@
- Preboot progress dialog - Preboot progress dialog
- 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)
## Changed ## Changed
- Conform to PEP 440 versioning for beta versions - Conform to PEP 440 versioning for beta versions
@@ -56,6 +57,7 @@
- Disable overlay scrollbars on server tables - Disable overlay scrollbars on server tables
- Reduce size of geolocation DB on disk (~100MB) - Reduce size of geolocation DB on disk (~100MB)
- 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
## Dropped ## Dropped
- Debug mode - Debug mode
+1 -1
View File
@@ -44,7 +44,7 @@ def has_libgi() -> bool:
return exists return exists
def test_missing_lib() -> bool: def test_libgi_missing() -> None:
msg = f"System is missing the required library '{LIB}'. Install it via your system package manager." msg = f"System is missing the required library '{LIB}'. Install it via your system package manager."
if has_libgi() is False: if has_libgi() is False:
print(msg) print(msg)
+9 -1
View File
@@ -1,10 +1,12 @@
import psutil import psutil
import subprocess import subprocess
import shutil import shutil
import logging
from warnings import deprecated from warnings import deprecated
from dzgui.const.constants import ( from dzgui.const.constants import (
APP_NAME,
DAYZ_BINARY, DAYZ_BINARY,
STEAM_CMD, STEAM_CMD,
FLATPAK_APPID, FLATPAK_APPID,
@@ -13,6 +15,10 @@ from dzgui.const.constants import (
FLATPAK_SANDBOX, FLATPAK_SANDBOX,
) )
from dzgui.util.format import format_exception
logger = logging.getLogger(APP_NAME)
# TODO: move to util.proc # TODO: move to util.proc
def is_dayz_running() -> bool: def is_dayz_running() -> bool:
@@ -22,7 +28,9 @@ def is_dayz_running() -> bool:
for proc in psutil.process_iter(): for proc in psutil.process_iter():
try: try:
procs.append(proc.cmdline()) procs.append(proc.cmdline())
except Exception: except Exception as e:
msg = format_exception(e)
logger.warning(msg)
continue continue
return any(substring in item for sublist in procs for item in sublist) return any(substring in item for sublist in procs for item in sublist)
+6 -103
View File
@@ -1,40 +1,11 @@
import argparse import argparse
import logging
import os
import sys import sys
import warnings import warnings
from typing import TYPE_CHECKING
from dzgui.const.constants import APP_NAME from dzgui.const.constants import APP_NAME
from dzgui.const.enum import Preferences from dzgui.init.libgi import test_libgi_missing
from dzgui.config.query import lookup
from dzgui.config.userprefs import UserPrefs
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
from dzgui.init.flock import lock_acquire
from dzgui.init.migrate import (
has_new_config,
copy_state_files,
migrate_cols_file,
)
from dzgui.init.prefix import get_version from dzgui.init.prefix import get_version
from dzgui.init.prereqs import has_steam_client from dzgui.util.strings import flags
from dzgui.strings import boot
# from dzgui.util.map_count import get_map_count
from dzgui.util.deck import is_steam_deck, is_game_mode
from dzgui.util.localize import set_locale
from dzgui.util.strings import init, flags
from dzgui.views.base import App
from dzgui.views.dialogs.boot import BootWindow
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
from dzgui.views.dialogs.wizard import SetupWizard
if TYPE_CHECKING:
from pathlib import Path
logger = logging.getLogger(APP_NAME)
parser = argparse.ArgumentParser(description=flags.description) parser = argparse.ArgumentParser(description=flags.description)
parser.add_argument("-v", "--version", action="store_true", help=flags.version) parser.add_argument("-v", "--version", action="store_true", help=flags.version)
@@ -43,11 +14,6 @@ parser.add_argument("-d", "--debug", action="store_true", help=flags.debug)
args = parser.parse_args() args = parser.parse_args()
# TODO: profile load time
def make_parents(path: "Path") -> None:
path.parent.mkdir(parents=True, exist_ok=True)
def uninstall() -> None: def uninstall() -> None:
# TODO: uninstall data files (-u) # TODO: uninstall data files (-u)
# -u removes state, log, freedesktop # -u removes state, log, freedesktop
@@ -57,21 +23,7 @@ def uninstall() -> None:
pass pass
def setup_logger(log_path: "Path") -> None:
_format = (
"%(asctime)s␞%(levelname)s␞%(filename)s::%(funcName)s::%(lineno)s␞%(message)s"
)
fh = logging.FileHandler(log_path)
formatter = logging.Formatter(_format)
fh.setFormatter(formatter)
fh.setLevel(logging.DEBUG)
logger.setLevel(logging.DEBUG)
logger.addHandler(fh)
def main() -> None: def main() -> None:
lock = lock_acquire() # noqa
if args.version is True: if args.version is True:
print(get_version()) print(get_version())
sys.exit(0) sys.exit(0)
@@ -82,60 +34,11 @@ def main() -> None:
warnings.filterwarnings("default", category=DeprecationWarning) warnings.filterwarnings("default", category=DeprecationWarning)
version = get_version() version = get_version()
set_locale()
print(f"{APP_NAME} {version}") print(f"{APP_NAME} {version}")
test_libgi_missing()
# NOTE: consider aborting this check if steam deck # NOTE: only import ligbirepository modules after the above check
xdg_paths = get_xdg_paths() from dzgui.app_init import load_gui
XDG = parse_filepaths(xdg_paths)
if XDG.resolution.parent.is_dir() is False: load_gui(version, args.debug)
make_parents(XDG.resolution)
if XDG.debug.is_file() is False:
make_parents(XDG.debug)
_is_steam_deck = is_steam_deck()
_is_game_mode = is_game_mode() if _is_steam_deck else False
if _is_game_mode:
# NOTE: this may no longer be necessary on newer versions of SteamOS
del os.environ["GTK_IM_MODULE"]
if has_new_config(XDG.config) is False:
migrate_cols_file(XDG.columns)
copy_state_files(xdg_paths["XDG_STATE_HOME"])
# TODO: add logging inside wizard
SetupWizard(_is_steam_deck, XDG.config)
# NOTE: implies that setup wizard failed or was closed
if has_new_config(XDG.config) is False:
return
setup_logger(XDG.debug)
with open(XDG.debug, "w") as f:
f.truncate(0)
if _is_steam_deck is False:
# TODO: sudo escalation dialog
# count = get_map_count()
# TODO: move into module
if has_steam_client() is False:
EarlyAlertDialog(init.requires_steam)
bootwin = BootWindow(XDG, version)
local_coords, latest_release = bootwin.get_results()
use_miles = lookup(XDG.config, Preferences.DIST)
prefs = UserPrefs(
is_steam_deck=_is_steam_deck,
is_game_mode=_is_game_mode,
is_debug=args.debug,
coords=local_coords,
version=version,
paths=XDG,
latest_release=latest_release,
use_miles=use_miles,
)
print(boot.all_ok)
App(prefs)
+44 -29
View File
@@ -12,10 +12,10 @@ import dzgui.api.servers as Servers
from dzgui.api.steam import ( from dzgui.api.steam import (
connect, connect,
enqueue_mod,
get_remote_signatures, get_remote_signatures,
get_needs_update, get_needs_update,
load_to_menu, load_to_menu,
subscribe,
) )
from dzgui.api.mods import ( from dzgui.api.mods import (
@@ -25,6 +25,7 @@ from dzgui.api.mods import (
update_signatures, update_signatures,
) )
from dzgui.const.constants import ( from dzgui.const.constants import (
API_RATE_LIMIT,
APP_NAME, APP_NAME,
APPID_DAYZ, APPID_DAYZ,
APPID_DAYZ_EXP, APPID_DAYZ_EXP,
@@ -34,7 +35,11 @@ from dzgui.const.constants import (
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_dayz_running, 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 waiting_for_launch, waiting_for_mods from dzgui.strings.dialogs import (
waiting_for_launch,
waiting_for_mods,
waiting_for_directories,
)
from dzgui.strings.server_mods import checkmark, resync from dzgui.strings.server_mods import checkmark, resync
from dzgui.util.format import format_mib from dzgui.util.format import format_mib
from dzgui.util.strings import dialog, server_timeout from dzgui.util.strings import dialog, server_timeout
@@ -88,6 +93,7 @@ class ConnectionManager:
self.record: Record self.record: Record
self.workshop: Path self.workshop: Path
self.client: str
self.remote_mod_ids: list[str] = [] self.remote_mod_ids: list[str] = []
self.missing_mods: list[tuple[str, str, int, int]] = [] self.missing_mods: list[tuple[str, str, int, int]] = []
@@ -142,21 +148,14 @@ class ConnectionManager:
remote_mods: list[list[str]] = [] remote_mods: list[list[str]] = []
if res.is_modded(): if res.is_modded():
try: try:
remote_mods = self._query_modlist(record) remote_mods, missing_mods = self._query_modlist(record)
self.missing_mods = missing_mods
self.remote_mod_ids = [mod[1] for mod in remote_mods] self.remote_mod_ids = [mod[1] for mod in remote_mods]
except Exception as e: except Exception as e:
logger.warning(e) logger.warning(e)
self.thread_man.set_cleanup_func(failure_func, destroy_first=True) self.thread_man.set_cleanup_func(failure_func, destroy_first=True)
return return
hashes = get_remote_signatures(self.remote_mod_ids)
version_file = prefs.paths.version
self.missing_mods = get_needs_update(version_file, hashes)
for mod in remote_mods:
if any(mod[1] in tuple for tuple in self.missing_mods):
mod[2] = resync
if local_version is not None: if local_version is not None:
pefile_path = PeFile.get_pefile_path(steam_path, info.game_id) pefile_path = PeFile.get_pefile_path(steam_path, info.game_id)
total, used, free = shutil.disk_usage(pefile_path) total, used, free = shutil.disk_usage(pefile_path)
@@ -171,6 +170,7 @@ class ConnectionManager:
client = self.controller.query_config(Preferences.CLIENT) client = self.controller.query_config(Preferences.CLIENT)
running = is_steam_running(client) running = is_steam_running(client)
steam_proc = SteamProcess(client_name, running) steam_proc = SteamProcess(client_name, running)
self.client = client
game_mode = prefs.is_game_mode game_mode = prefs.is_game_mode
@@ -219,10 +219,13 @@ class ConnectionManager:
StoredFunc(self._present_details_dialog, details), destroy_first=True StoredFunc(self._present_details_dialog, details), destroy_first=True
) )
def _query_modlist(self, record: Servers.Record) -> list[list[str]]: def _query_modlist(
self, record: Servers.Record
) -> 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(steam_path)
alpha_mods: list[list[str]] = [ alpha_mods: list[list[str]] = [
[ [
mod.name, mod.name,
@@ -232,12 +235,23 @@ class ConnectionManager:
for mod in mods for mod in mods
] ]
alpha_mods.sort(key=lambda x: x[0]) alpha_mods.sort(key=lambda x: x[0])
return alpha_mods
prefs = self.controller.get_prefs()
version_file = prefs.paths.version
remote_mod_ids = [mod[1] for mod in alpha_mods]
hashes = get_remote_signatures(remote_mod_ids)
missing_mods = get_needs_update(version_file, hashes)
for mod in alpha_mods:
if any(mod[1] in tuple for tuple in missing_mods):
mod[2] = resync
return alpha_mods, missing_mods
@call_on_thread(dialog.querying) @call_on_thread(dialog.querying)
def query_modlist_and_present(self, record: Servers.Record) -> None: def query_modlist_and_present(self, record: Servers.Record) -> None:
try: try:
mods = self._query_modlist(record) mods, missing_mods = self._query_modlist(record)
except Exception: except Exception:
self.thread_man.set_cleanup_func( self.thread_man.set_cleanup_func(
StoredFunc(self._server_timeout), destroy_first=True StoredFunc(self._server_timeout), destroy_first=True
@@ -263,20 +277,19 @@ class ConnectionManager:
def _connect_steam(self, menu_only: bool) -> None: def _connect_steam(self, menu_only: bool) -> None:
addr = f"{self.record.ip}:{self.record.gameport}" addr = f"{self.record.ip}:{self.record.gameport}"
playername = self.controller.query_config(Preferences.NAME) playername = self.controller.query_config(Preferences.NAME)
client = self.controller.query_config(Preferences.CLIENT)
if menu_only: if menu_only:
rc = load_to_menu(client, self.appid, playername, self.remote_mod_ids) rc = load_to_menu(self.client, self.appid, playername, self.remote_mod_ids)
else: else:
rc = connect(client, addr, self.appid, playername, self.remote_mod_ids) rc = connect(self.client, addr, self.appid, playername, self.remote_mod_ids)
if rc != 0: if rc != 0:
# TODO: log/pop the error # TODO: log/pop the error
func = StoredFunc(self.controller.update_status) func = StoredFunc(self.controller.update_status)
self.thread_man.set_cleanup_func(func) self.thread_man.set_cleanup_func(func)
return return
self.thread_man.show_cancel(False)
self.thread_man.update_dialog(waiting_for_launch) self.thread_man.update_dialog(waiting_for_launch)
while True: while True:
# FIXME: cancel should not be visible here per setting above
if self.controller.is_cancel_pending(): if self.controller.is_cancel_pending():
# TODO: some facility to also close spawned steam process # TODO: some facility to also close spawned steam process
return return
@@ -291,20 +304,18 @@ class ConnectionManager:
self.controller.add_to_history(self.history, self.record) self.controller.add_to_history(self.history, self.record)
self.controller.open_page(NotebookPage.SERVERS) self.controller.open_page(NotebookPage.SERVERS)
def _update_mods(self, raise_window: bool, menu_only: bool = False) -> None: def _update_mods(self, menu_only: bool = False) -> None:
# NOTE: fast enqueue all mods in auto mode
prefs = self.controller.get_prefs() prefs = self.controller.get_prefs()
config_man = self.controller.get_config_man()
key = config_man.lookup(Preferences.STEAM)
for title, mod, stamp, size in self.missing_mods: for title, mod, stamp, size in self.missing_mods:
if self.controller.is_cancel_pending(): if self.controller.is_cancel_pending():
return return
enqueue_mod(mod, self.appid) subscribe(key, int(mod))
time.sleep(3) time.sleep(API_RATE_LIMIT)
if raise_window is True:
logger.info("Bringing window to foreground")
GLib.idle_add(self.controller.present_window)
self.thread_man.update_dialog(waiting_for_directories)
for title, mod, stamp, size in self.missing_mods: for title, mod, stamp, size in self.missing_mods:
mod_path = self.workshop / mod mod_path = self.workshop / mod
@@ -322,13 +333,17 @@ class ConnectionManager:
time.sleep(1) time.sleep(1)
update_signatures(self.missing_mods, prefs.paths.version) update_signatures(self.missing_mods, prefs.paths.version)
# TODO: just push steam path directly
rebuild_symlinks(prefs.paths.config) rebuild_symlinks(prefs.paths.config)
# NOTE: update table status in main loop
self.thread_man.update_emitter("all_mods_synched")
# TODO: just push steam path directly
self._connect_steam(menu_only) self._connect_steam(menu_only)
@call_on_thread(waiting_for_mods, show_cancel=True) @call_on_thread(waiting_for_mods, show_cancel=True)
def update_and_connect(self, raise_window: bool, menu_only: bool = False) -> None: def update_and_connect(self, menu_only: bool = False) -> None:
if len(self.missing_mods) > 0: if len(self.missing_mods) > 0:
self._update_mods(raise_window, menu_only) self._update_mods(menu_only)
else: else:
self._connect_steam(menu_only) self._connect_steam(menu_only)
+2 -2
View File
@@ -80,8 +80,8 @@ class ContextMenuManager:
if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)): if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)):
match action: match action:
case ContextMenu.DELETE_MOD: case ContextMenu.UNSUB_MOD:
self.controller.delete_mods(self.treeview) self.controller.unsub_mods(self.treeview)
case ContextMenu.OPEN_WORKSHOP: case ContextMenu.OPEN_WORKSHOP:
self.open_mod_page() self.open_mod_page()
+3 -1
View File
@@ -80,6 +80,7 @@ class FilterManager:
if maps is None: if maps is None:
return return
if len(maps) < 1: if len(maps) < 1:
self.reinit_map_store()
return return
self.reinit_map_store() self.reinit_map_store()
@@ -89,8 +90,9 @@ class FilterManager:
continue continue
self.append_map([m]) self.append_map([m])
# TODO: currently unused
def get_unique_maps(self) -> list[str]: def get_unique_maps(self) -> list[str]:
return [row[0] for row in self.map_store if row != "All maps"] return [row[0] for row in self.map_store if row[0] != "All maps"]
def get_all_filters(self) -> tuple: def get_all_filters(self) -> tuple:
map_name = self.get_active_map_name() map_name = self.get_active_map_name()
+43 -20
View File
@@ -1,23 +1,29 @@
import logging import logging
import shutil import time
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dzgui.api.steam import unsubscribe
from dzgui.api.mods import ( from dzgui.api.mods import (
get_delimited_mods, get_delimited_mods,
get_local_mod_path,
find_stale_mods, find_stale_mods,
_hash, _hash,
remove_stale_signatures, remove_stale_signatures,
) )
from dzgui.const.constants import APP_NAME, APPID_DAYZ, APPID_DAYZ_EXP from dzgui.const.constants import (
API_RATE_LIMIT,
APP_NAME,
APPID_DAYZ,
APPID_DAYZ_EXP,
)
from dzgui.const.enum import Preferences from dzgui.const.enum import Preferences
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 FastInsertListStore, ModelFactory from dzgui.model.model_factory import FastInsertListStore, ModelFactory
from dzgui.strings import dialogs from dzgui.strings import dialogs
from dzgui.util.format import format_mods from dzgui.util.format import format_mods
from dzgui.util.strings import server_timeout from dzgui.util.strings import server_timeout
from dzgui.util.symlink import rebuild_symlinks
from dzgui.views.dialogs.generic import ExceptionDialog from dzgui.views.dialogs.generic import ExceptionDialog
@@ -61,11 +67,18 @@ class ModManager:
self.thread_man.set_cleanup_func(func) self.thread_man.set_cleanup_func(func)
return return
prefs = self.controller.get_prefs()
rebuild_symlinks(prefs.paths.config)
self.store = ModelFactory().make_mod_store() self.store = ModelFactory().make_mod_store()
self.store.extend(mods) self.store.extend(mods)
func = StoredFunc(self._on_mods_loaded) func = StoredFunc(self._on_mods_loaded)
self.thread_man.set_cleanup_func(func) self.thread_man.set_cleanup_func(func)
def set_store(self, store: "FastInsertListStore") -> None:
self.store = store
def _on_mods_loaded(self) -> None: def _on_mods_loaded(self) -> None:
self.treeview.set_model(self.store) self.treeview.set_model(self.store)
if self.store is None: if self.store is None:
@@ -74,7 +87,7 @@ class ModManager:
total_mods = len(self.store) total_mods = len(self.store)
self.emitter.emit("mods_updated", msg, total_mods) self.emitter.emit("mods_updated", msg, total_mods)
def delete_mods(self) -> None: def unsub_mods(self) -> None:
sel = self.treeview.get_selection() sel = self.treeview.get_selection()
model, pathlist = sel.get_selected_rows() model, pathlist = sel.get_selected_rows()
# NOTE: reverse when multiple selection # NOTE: reverse when multiple selection
@@ -85,7 +98,8 @@ class ModManager:
continue continue
mod, _iter = res mod, _iter = res
mods.append((mod, _iter)) mods.append((mod, _iter))
self.delete_mods_on_system(mods) self.thread_man.set_job_count(len(mods))
self.unsub_all_mods(mods)
def get_mod_from_tree_path( def get_mod_from_tree_path(
self, tree_path: Gtk.TreePath self, tree_path: Gtk.TreePath
@@ -98,24 +112,29 @@ class ModManager:
return mod, tree_iter return mod, tree_iter
@call_on_thread(dialogs.deleting_mods) @call_on_thread(dialogs.deleting_mods)
def delete_mods_on_system(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None: def unsub_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
for mod, _iter in mods: for mod, _iter in mods:
self.delete_single_mod(mod) self.unsub_atomic_mod(mod)
self.thread_man.increment_dialog()
time.sleep(API_RATE_LIMIT)
iters = [_iter for mod, _iter in mods] func = StoredFunc(self._on_mods_unsubbed, mods)
func = StoredFunc(self._on_mods_deleted, iters)
self.thread_man.set_cleanup_func(func) self.thread_man.set_cleanup_func(func)
def delete_single_mod(self, mod: str) -> None: def unsub_atomic_mod(self, mod: str) -> None:
config_man = self.controller.get_config_man()
key = config_man.lookup(Preferences.STEAM)
unsubscribe(key, int(mod))
steam_path = Path(self.path) steam_path = Path(self.path)
mods_path = get_local_mod_path(steam_path)
app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ) app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
md5 = _hash(mod) try:
symlink = app_path / md5 md5 = _hash(mod)
symlink.unlink() symlink = app_path / md5
shutil.rmtree(mods_path / mod) symlink.unlink()
except Exception as e:
logger.warning(e)
# NOTE: second pass to unlink DAYZ_EXP mods # NOTE: second pass to unlink DAYZ_EXP mods
# TODO: test this with working APPID_DAYZ_EXP installation # TODO: test this with working APPID_DAYZ_EXP installation
try: try:
@@ -124,19 +143,23 @@ class ModManager:
symlink.unlink() symlink.unlink()
except PeFile.AppNotInstalledError: except PeFile.AppNotInstalledError:
pass pass
time.sleep(API_RATE_LIMIT)
def _on_mods_deleted(self, iters: list[Gtk.TreeIter]) -> None: def _on_mods_unsubbed(self, mod_iter: list[tuple[str, Gtk.TreeIter]]) -> None:
mods = [int(mod) for mod, _iter in mod_iter]
iters = [_iter for mod, _iter in mod_iter]
if self.store is None: if self.store is None:
return return
for _iter in iters: for _iter in iters:
self.store.remove(_iter) self.store.remove(_iter)
remove_stale_signatures(self.prefs.paths.config, self.prefs.paths.version) # TODO: process config path in called function
remove_stale_signatures(self.prefs.paths.config, self.prefs.paths.version, mods)
model = self.treeview.get_model() model = self.treeview.get_model()
if model is None: if model is None:
return return
mods = len(model) total_mods = len(model)
msg = self.format_mod_statusbar() msg = self.format_mod_statusbar()
self.emitter.emit("mods_updated", msg, mods) self.emitter.emit("mods_updated", msg, total_mods)
def uncolorize_mods(self) -> None: def uncolorize_mods(self) -> None:
model = self.treeview.get_model() model = self.treeview.get_model()
+24 -3
View File
@@ -1,4 +1,5 @@
import logging import logging
import time
from pathlib import Path from pathlib import Path
from typing import Callable, TYPE_CHECKING, Union from typing import Callable, TYPE_CHECKING, Union
@@ -7,10 +8,11 @@ from dzgui.api.mods import is_mission, get_custom_mods
from dzgui.api.steam import launch_offline from dzgui.api.steam import 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
from dzgui.util.symlink import create_custom_symlinks from dzgui.util.symlink import create_custom_symlinks, rebuild_symlinks, symlink_mission
from dzgui.views.dialogs.filepicker import FolderPicker from dzgui.views.dialogs.filepicker import FolderPicker
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -32,10 +34,18 @@ class OfflineManager:
self.thread_man = ThreadingManager(controller) self.thread_man = ThreadingManager(controller)
self.appid: int self.appid: int
self.mission_folder: str
self.local_mods: list[str] | None self.local_mods: list[str] | None
self.custom_mods: list[str] | None self.custom_mods: list[str] | None
# TODO: more robust test
def is_custom_folder_valid(self, folder: str) -> bool:
return Path(folder).exists()
# TODO: set properties as members of offline manager on demand and check them here
# rather than passing strings again
def is_mission_valid(self, folder: str) -> bool:
return is_mission(Path(folder))
def get_mission(self) -> None: def get_mission(self) -> None:
folder = self.open_folderpicker(dialogs.mission_dialog) folder = self.open_folderpicker(dialogs.mission_dialog)
if folder is None: if folder is None:
@@ -86,13 +96,24 @@ class OfflineManager:
if len(local_mods) > 0: if len(local_mods) > 0:
combined_mods.extend(local_mods) combined_mods.extend(local_mods)
config = self.controller.get_prefs().paths.config
rebuild_symlinks(config)
if len(custom_mods) > 0: if len(custom_mods) > 0:
new_symlinks = create_custom_symlinks( new_symlinks = create_custom_symlinks(
Path(steam_path), Path(custom_folder), custom_mods Path(steam_path), Path(custom_folder), custom_mods
) )
combined_mods.extend(new_symlinks) combined_mods.extend(new_symlinks)
launch_offline(client, appid, name, combined_mods, mission)
relative_link = ""
if len(mission) > 0:
relative_link = symlink_mission(Path(steam_path), mission)
launch_offline(client, appid, name, combined_mods, relative_link)
while True:
if is_dayz_running():
break
time.sleep(1)
def open_folderpicker(self, title: str) -> Union["Path", None]: def open_folderpicker(self, title: str) -> Union["Path", None]:
picker = FolderPicker(self.controller.get_window(), title) picker = FolderPicker(self.controller.get_window(), title)
+5
View File
@@ -81,6 +81,11 @@ class ThreadingManager:
def set_job_count(self, jobs: int) -> None: def set_job_count(self, jobs: int) -> None:
self.jobs = jobs self.jobs = jobs
def update_emitter(self, signal: str) -> None:
if self.controller:
emitter = self.controller.get_emitter()
GLib.idle_add(lambda: emitter.emit(signal))
def update_dialog(self, msg: str) -> None: def update_dialog(self, msg: str) -> None:
GLib.idle_add(lambda: self.wait_dialog.update_text(msg)) GLib.idle_add(lambda: self.wait_dialog.update_text(msg))
+4 -2
View File
@@ -374,7 +374,7 @@ class ServerModelManager:
proxy = self._get_proxy_man().get_proxy_model() proxy = self._get_proxy_man().get_proxy_model()
self.tv.set_model(proxy) self.tv.set_model(proxy)
if self.controller.get_active_treeview().get_enum == ServerTab.SAVED: if self.controller.get_active_treeview().get_enum() == ServerTab.SAVED:
self.emitter.emit("servers_loaded", self.enum) self.emitter.emit("servers_loaded", self.enum)
filter_man = self.tv.get_filter_man() filter_man = self.tv.get_filter_man()
@@ -395,8 +395,10 @@ class ServerModelManager:
self.first_iteration = False self.first_iteration = False
def _cleanup_on_success(self) -> None: def _cleanup_on_success(self) -> None:
proxy = self._get_proxy_man().get_proxy_model() proxy = self.proxy_man.get_proxy_model()
self.tv.set_model(proxy) self.tv.set_model(proxy)
if self.preserve_on_fail is True:
self.proxy_man.wipe_cache()
self.emitter.emit("servers_loaded", self.enum) self.emitter.emit("servers_loaded", self.enum)
if self.first_iteration: if self.first_iteration:
+3 -2
View File
@@ -1,5 +1,6 @@
waiting_for_launch = "Waiting for DayZ to launch" waiting_for_launch = "Waiting for DayZ to launch"
waiting_for_mods = "Waiting for Steam to update mods" waiting_for_mods = "Queueing up mods (step 1/2)"
waiting_for_directories = "Steam is staging mods (step 2/2)"
fetching_update = "Fetching update" fetching_update = "Fetching update"
failed_to_update = "Failed to update DZGUI executable" failed_to_update = "Failed to update DZGUI executable"
@@ -7,7 +8,7 @@ update_success = "Updated successfully. Please exit and relaunch."
load_error_lan = "Failed to find any servers on your network.\nCheck the server query port or your firewall settings." load_error_lan = "Failed to find any servers on your network.\nCheck the server query port or your firewall settings."
fetching_mods = "Fetching mod metadata" fetching_mods = "Fetching mod metadata"
deleting_mods = "Deleting mods" deleting_mods = "Unsubscribing mods"
scanning_mods = "Scanning mods" scanning_mods = "Scanning mods"
parsing_mods = "Parsing mods" parsing_mods = "Parsing mods"
+2
View File
@@ -10,8 +10,10 @@ custom_button = "Set custom mod folder"
local_frame = "Installed mods" local_frame = "Installed mods"
no_local_mods = "No local mods found"
no_mods = "No valid mods found" no_mods = "No valid mods found"
no_mission = "Not a valid mission" no_mission = "Not a valid mission"
folder_changed = "Folder missing or changed on disk"
custom_eventbox = "Set the root folder. Mods must be in atomic folders and have a meta.cpp file at a minimum." custom_eventbox = "Set the root folder. Mods must be in atomic folders and have a meta.cpp file at a minimum."
mission_eventbox = "Select a single mission folder containing an init.c file." mission_eventbox = "Select a single mission folder containing an init.c file."
+1
View File
@@ -10,6 +10,7 @@ warnings = "Warnings"
errors = "Errors" errors = "Errors"
mods = "Mods" mods = "Mods"
total_mods = "Total mods: " total_mods = "Total mods: "
up_to_date = "All mods are up to date."
all_updated = "All mods updated." all_updated = "All mods updated."
"If you recently installed {build} or moved it to a different drive, " "If you recently installed {build} or moved it to a different drive, "
"restart Steam to allow these changes to synchronize, then try again." "restart Steam to allow these changes to synchronize, then try again."
+9 -9
View File
@@ -52,7 +52,7 @@ show_mods = "Show server-side mods"
show_details = "Show server details" show_details = "Show server details"
refresh_players = "Refresh player count" refresh_players = "Refresh player count"
open_workshop = "Open in Steam Workshop" open_workshop = "Open in Steam Workshop"
delete_mod = "Delete mod" unsub_mod = "Unsubscribe mod"
copy_name = "Copy name to clipboard" copy_name = "Copy name to clipboard"
copy_ip = "Copy IP to clipboard" copy_ip = "Copy IP to clipboard"
copy_log = "Copy record(s) to clipboard" copy_log = "Copy record(s) to clipboard"
@@ -130,7 +130,6 @@ servers = {
"Enter/space/double-click": "connect to server", "Enter/space/double-click": "connect to server",
"Right-click/Ctrl-l": "additional context menus", "Right-click/Ctrl-l": "additional context menus",
"Ctrl-r": "refresh players", "Ctrl-r": "refresh players",
"Ctrl-p": "refresh ping",
"Ctrl-f": "jump to keyword search field", "Ctrl-f": "jump to keyword search field",
"Ctrl-m": "jump to maps field", "Ctrl-m": "jump to maps field",
"Ctrl-d": "select default LAN port", "Ctrl-d": "select default LAN port",
@@ -148,8 +147,8 @@ servers = {
vim = { vim = {
"j": "Move down a row/scroll up", "j": "Move down a row/scroll up",
"k": "Move up a row/scroll down", "k": "Move up a row/scroll down",
"l": "Jump to main area from sidebar", "h": "Jump to main area from sidebar",
"h": "Jump to sidebar from main area", "l": "Jump to sidebar from main area",
"g": "Jump to first row/top of page", "g": "Jump to first row/top of page",
"G": "Jump to last row/bottom of page", "G": "Jump to last row/bottom of page",
} }
@@ -215,8 +214,8 @@ class ModPanelStrings:
unhighlight_stale_tooltip: str unhighlight_stale_tooltip: str
highlight_stale: str highlight_stale: str
highlight_stale_tooltip: str highlight_stale_tooltip: str
delete_selected: str unsub_selected: str
delete_selected_tooltip: str unsub_selected_tooltip: str
unselect_all: str unselect_all: str
unselect_all_tooltip: str unselect_all_tooltip: str
select_all: str select_all: str
@@ -337,8 +336,8 @@ mod_panel = ModPanelStrings(
"Shows locally-installed mods which are not used by any server " "Shows locally-installed mods which are not used by any server "
"in your Saved Servers" "in your Saved Servers"
), ),
delete_selected="Delete selected", unsub_selected="Unsubscribe selected",
delete_selected_tooltip="Deletes selected mods from the system", unsub_selected_tooltip="Unsubscribes from selected mods",
unselect_all="Unselect all", unselect_all="Unselect all",
unselect_all_tooltip="Bulk unselects all mods", unselect_all_tooltip="Bulk unselects all mods",
select_all="Select all", select_all="Select all",
@@ -367,6 +366,7 @@ thanks = Thanks(
"Johnofwrong", "Johnofwrong",
"MatheusLasserr", "MatheusLasserr",
"nolan-perez", "nolan-perez",
"OnniSaarni",
"scandalouss", "scandalouss",
"SnackSBR", "SnackSBR",
"StevelDusa", "StevelDusa",
@@ -463,7 +463,7 @@ crumbs = Crumbs(
thanks="Help > Special thanks", thanks="Help > Special thanks",
developers="Options > Developers", developers="Options > Developers",
default="Servers > ", default="Servers > ",
offline="Mods > Play offline" offline="Mods > Play offline",
) )
no_mods = "No local mods found." no_mods = "No local mods found."
+27 -7
View File
@@ -2,7 +2,7 @@ import logging
from pathlib import Path from pathlib import Path
from dzgui.api.mods import get_local_mod_path, get_local_mod_ids, _hash from dzgui.api.mods import get_local_mod_path, get_local_mod_ids, _hash
from dzgui.const.constants import APPID_DAYZ, APPID_DAYZ_EXP, APP_NAME from dzgui.const.constants import APPID_DAYZ, APPID_DAYZ_EXP, APP_NAME, DAYZ_COMMUNITY_ROOT
from dzgui.const.enum import Preferences from dzgui.const.enum import Preferences
from dzgui.config.query import lookup from dzgui.config.query import lookup
@@ -11,18 +11,23 @@ import dzgui.api.pefile as PeFile
logger = logging.getLogger(APP_NAME) logger = logging.getLogger(APP_NAME)
def expunge_link(file: Path) -> None:
if not file.is_symlink():
return
# NOTE: unlink stale symlinks
if file.exists() is False:
file.unlink()
if str(file.stem)[:2] == "@C":
file.unlink()
def rebuild_symlinks(config: Path) -> None: def rebuild_symlinks(config: Path) -> None:
# TODO: pass direct path as argument # TODO: pass direct path as argument
path = lookup(config, Preferences.DEFAULT) path = lookup(config, Preferences.DEFAULT)
steam_path = Path(path) steam_path = Path(path)
dayz_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ) dayz_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
for file in dayz_path.iterdir(): for file in dayz_path.iterdir():
# NOTE: unlink stale symlinks expunge_link(file)
if file.is_symlink() and file.exists() is False:
file.unlink()
# NOTE: expunge ephemeral (custom) symlinks
if file.is_symlink() and str(file.stem)[:2] == "@C":
file.unlink()
workshop = get_local_mod_path(steam_path) workshop = get_local_mod_path(steam_path)
# NOTE: create symlinks for missing mods # NOTE: create symlinks for missing mods
for mod_id in get_local_mod_ids(steam_path): for mod_id in get_local_mod_ids(steam_path):
@@ -50,6 +55,21 @@ def create_custom_symlinks(
return links return links
def symlink_mission(steam_path: Path, target: str) -> str:
dayz_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
for file in dayz_path.iterdir():
expunge_link(file)
path = Path(target)
stem = path.name
parent = path.parent
source = dayz_path.joinpath(DAYZ_COMMUNITY_ROOT)
try:
source.symlink_to(parent)
except Exception:
return ""
return f"{DAYZ_COMMUNITY_ROOT}/{stem}"
def clone_symlinks(steam_path: Path) -> None: def clone_symlinks(steam_path: Path) -> None:
""" """
Shares symlinks between builds. Used after any symlink operation Shares symlinks between builds. Used after any symlink operation
+25
View File
@@ -0,0 +1,25 @@
from typing import Sequence
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
class GenericBox(Gtk.Box):
def __init__(self, orientation: Gtk.Orientation, spacing: int = 0) -> None:
super().__init__(orientation=orientation, spacing=spacing)
def extend(self, els: Sequence[Gtk.Widget]) -> None:
for el in els:
self.add(el)
class HBox(GenericBox):
def __init__(self, spacing: int = 0) -> None:
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=spacing)
class VBox(GenericBox):
def __init__(self, spacing: int = 0) -> None:
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=spacing)
+4
View File
@@ -141,6 +141,8 @@ class RefreshButton(IconTextButton):
def _on_refresh_clicked(self, button: Self) -> None: def _on_refresh_clicked(self, button: Self) -> None:
"""Spawned in a thread""" """Spawned in a thread"""
if self.controller.get_active_treeview().is_loaded() is False:
return
self.is_clicked = True self.is_clicked = True
self.controller.refresh_tree() self.controller.refresh_tree()
# TODO: get server tab enum # TODO: get server tab enum
@@ -208,6 +210,8 @@ class SteamWorkshopButton(SteamTextButton):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__(label=buttons.workshop) super().__init__(label=buttons.workshop)
self.set_tooltip_text(buttons.workshop_tooltip) self.set_tooltip_text(buttons.workshop_tooltip)
self.set_margin_top(10)
self.set_margin_bottom(10)
class AddButton(IconTextButton): class AddButton(IconTextButton):
+2 -1
View File
@@ -20,7 +20,8 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk # noqa E402 from gi.repository import Gtk, Gdk # noqa E402
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.controllers.mc import Controller, Emitter from dzgui.controllers.mc import Controller
from dzgui.controllers.emitter import Emitter
COLS = 1 COLS = 1
ROWS = 1 ROWS = 1
+13 -8
View File
@@ -11,7 +11,8 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa E402 from gi.repository import Gtk # noqa E402
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.controllers.mc import Controller, Emitter from dzgui.controllers.mc import Controller
from dzgui.controllers.emitter import Emitter
class EnumeratedModButton(Gtk.Button): class EnumeratedModButton(Gtk.Button):
@@ -48,7 +49,7 @@ class ModSelectionPanel(Gtk.Box):
buttons = ( buttons = (
ModButton.SELECT_ALL, ModButton.SELECT_ALL,
ModButton.UNSELECT_ALL, ModButton.UNSELECT_ALL,
ModButton.DELETE_SELECTED, ModButton.UNSUB_SELECTED,
) )
for button in buttons: for button in buttons:
b = EnumeratedModButton(button) b = EnumeratedModButton(button)
@@ -79,14 +80,18 @@ class ModSelectionPanel(Gtk.Box):
self.highlight_stale_button.set_sensitive(True) self.highlight_stale_button.set_sensitive(True)
def _on_mods_updated(self, emitter: "Emitter", msg: str, mods: int) -> None: def _on_mods_updated(self, emitter: "Emitter", msg: str, mods: int) -> None:
if mods < 1: self._toggle_panel_sensitivity(mods)
self.main_panel.set_sensitive(False)
self.stale_panel.set_sensitive(False) def _toggle_panel_sensitivity(self, mods: int) -> None:
state = bool(mods)
for el in self.main_panel, self.stale_panel:
el.set_sensitive(state)
def _on_mods_highlighted(self, emitter: "Emitter") -> None: def _on_mods_highlighted(self, emitter: "Emitter") -> None:
self.swap_sensitive(True) self.swap_sensitive(True)
def _on_mod_page_toggled(self, emitter: "Emitter", state: bool) -> None: def _on_mod_page_toggled(self, emitter: "Emitter", state: bool, mods: int) -> None:
self._toggle_panel_sensitivity(mods)
self.set_visible(state) self.set_visible(state)
def swap_sensitive(self, state: bool) -> None: def swap_sensitive(self, state: bool) -> None:
@@ -100,8 +105,8 @@ class ModSelectionPanel(Gtk.Box):
self.controller.toggle_mod_selection(True) self.controller.toggle_mod_selection(True)
case ModButton.UNSELECT_ALL: case ModButton.UNSELECT_ALL:
self.controller.toggle_mod_selection(False) self.controller.toggle_mod_selection(False)
case ModButton.DELETE_SELECTED: case ModButton.UNSUB_SELECTED:
self.controller.delete_mods() self.controller.unsub_mods()
case ModButton.HIGHLIGHT_STALE: case ModButton.HIGHLIGHT_STALE:
self.controller.highlight_stale() self.controller.highlight_stale()
case ModButton.UNHIGHLIGHT_STALE: case ModButton.UNHIGHLIGHT_STALE:
+11 -1
View File
@@ -25,6 +25,8 @@ class Statusbar(Gtk.Grid):
self.controller.register_widget("statusbar", self) self.controller.register_widget("statusbar", self)
self.emitter = controller.get_emitter() self.emitter = controller.get_emitter()
self.prior_enum: NotebookPage
self.prior_status: str
self.playercount = "" self.playercount = ""
self.statusbar = Gtk.Statusbar() self.statusbar = Gtk.Statusbar()
@@ -71,6 +73,7 @@ class Statusbar(Gtk.Grid):
NotebookPage.CONNECTION, NotebookPage.CONNECTION,
NotebookPage.OFFLINE, NotebookPage.OFFLINE,
): ):
self.prior_enum = enum
self.set_by_context(enum, esc_to_return) self.set_by_context(enum, esc_to_return)
return return
@@ -80,8 +83,9 @@ class Statusbar(Gtk.Grid):
case NotebookPage.KEYS: case NotebookPage.KEYS:
bar = question_to_return bar = question_to_return
case _: case _:
return bar = self.prior_status
self.prior_enum = enum
self.set_by_context(enum, bar) self.set_by_context(enum, bar)
def _on_server_row_changed(self, statusbar: Self) -> None: def _on_server_row_changed(self, statusbar: Self) -> None:
@@ -97,6 +101,10 @@ class Statusbar(Gtk.Grid):
context: Union["ServerTab", NotebookPage], context: Union["ServerTab", NotebookPage],
) -> None: ) -> None:
self.spinner.stop() self.spinner.stop()
if type(context) is NotebookPage:
self.prior_enum = context
self.prior_status = ""
# FIXME: CalcDist is being called when table is not loaded # FIXME: CalcDist is being called when table is not loaded
if dist is None: if dist is None:
self.set_by_context(context, "") self.set_by_context(context, "")
@@ -122,6 +130,8 @@ class Statusbar(Gtk.Grid):
def set_by_context( def set_by_context(
self, context: Union[NotebookPage, "ServerTab"], string: str self, context: Union[NotebookPage, "ServerTab"], string: str
) -> None: ) -> None:
if context != NotebookPage.KEYS:
self.prior_status = string
meta = self.statusbar.get_context_id(str(context)) meta = self.statusbar.get_context_id(str(context))
self.statusbar.push(meta, string) self.statusbar.push(meta, string)
self.set_cache(string) self.set_cache(string)
+28 -4
View File
@@ -1,5 +1,10 @@
from pathlib import Path
from typing import Self, TYPE_CHECKING from typing import Self, TYPE_CHECKING
from dzgui.const.enum import NotebookPage from dzgui.api.steam import find_user_id
from dzgui.const.enum import NotebookPage, Preferences
from dzgui.views.components.box import HBox
from dzgui.views.components.buttons import SteamWorkshopButton
from dzgui.views.components.scrollable import NoOverlayScrolledWindow from dzgui.views.components.scrollable import NoOverlayScrolledWindow
from dzgui.views.trees.tree_mods import ModTreeView from dzgui.views.trees.tree_mods import ModTreeView
@@ -26,6 +31,17 @@ class Mods(Gtk.Box):
self.controller.register_widget("modtreeview", self.tree) self.controller.register_widget("modtreeview", self.tree)
self.emitter = controller.get_emitter() self.emitter = controller.get_emitter()
# TODO: move
default_steam_path = self.controller.query_config(Preferences.DEFAULT)
steam_path = Path(default_steam_path)
uid = find_user_id(steam_path)
pretty_uid = "" if uid is None else uid
hbox = HBox(spacing=10)
workshop_button = SteamWorkshopButton()
workshop_button.connect(
"clicked", lambda _: self.controller.open_user_workshop(pretty_uid)
)
self.offline_button = Gtk.Button( self.offline_button = Gtk.Button(
label="Play offline", label="Play offline",
halign=Gtk.Align.START, halign=Gtk.Align.START,
@@ -34,7 +50,10 @@ class Mods(Gtk.Box):
) )
self.offline_button.connect("clicked", self._on_offline_clicked) self.offline_button.connect("clicked", self._on_offline_clicked)
self.add(self.offline_button) hbox.add(workshop_button)
hbox.add(self.offline_button)
self.add(hbox)
self.add(self.box) self.add(self.box)
self.connect("map", self._on_map) self.connect("map", self._on_map)
@@ -45,10 +64,15 @@ class Mods(Gtk.Box):
self.controller.open_offline() self.controller.open_offline()
def _on_unmap(self, widget: Self) -> None: def _on_unmap(self, widget: Self) -> None:
self.emitter.emit("mod_page_toggled", False) self.emitter.emit("mod_page_toggled", False, 0)
def _on_map(self, widget: Self) -> None: def _on_map(self, widget: Self) -> None:
self.emitter.emit("mod_page_toggled", True) model = self.tree.get_model()
if model is None:
mods = 0
else:
mods = len(model)
self.emitter.emit("mod_page_toggled", True, mods)
# TODO: delegation # TODO: delegation
# NOTE: handles going back from offline mods page # NOTE: handles going back from offline mods page
# more generic cache restoration method # more generic cache restoration method
+54 -21
View File
@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import Enum
from typing import Self, Sequence, TYPE_CHECKING, Union from typing import Self, TYPE_CHECKING, Union
from dzgui.util import css from dzgui.util import css
from dzgui.const.constants import ( from dzgui.const.constants import (
@@ -11,10 +11,12 @@ from dzgui.const.constants import (
EDIT_DELETE, EDIT_DELETE,
ERROR, ERROR,
FOLDER, FOLDER,
WARNING,
) )
from dzgui.const.enum import ContextMenuGroup, NotebookPage from dzgui.const.enum import ContextMenuGroup, NotebookPage
from dzgui.managers.offline import OfflineManager from dzgui.managers.offline import OfflineManager
from dzgui.strings import generic, offline from dzgui.strings import generic, offline
from dzgui.views.components.box import HBox, VBox
from dzgui.views.components.buttons import Icon, IconTextButton from dzgui.views.components.buttons import Icon, IconTextButton
from dzgui.views.components.eventbox import InfoEventBox from dzgui.views.components.eventbox import InfoEventBox
from dzgui.views.components.frame import HeadingFrame from dzgui.views.components.frame import HeadingFrame
@@ -36,25 +38,7 @@ if TYPE_CHECKING:
class FolderError(Enum): class FolderError(Enum):
NO_VALID_MODS = 1 NO_VALID_MODS = 1
NO_VALID_MISSION = 2 NO_VALID_MISSION = 2
FOLDER_CHANGED = 3
class GenericBox(Gtk.Box):
def __init__(self, orientation: Gtk.Orientation, spacing: int = 0) -> None:
super().__init__(orientation=orientation, spacing=spacing)
def extend(self, els: Sequence[Gtk.Widget]) -> None:
for el in els:
self.add(el)
class HBox(GenericBox):
def __init__(self, spacing: int = 0) -> None:
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=spacing)
class VBox(GenericBox):
def __init__(self, spacing: int = 0) -> None:
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=spacing)
class PageHeading(Gtk.Label): class PageHeading(Gtk.Label):
@@ -82,6 +66,10 @@ class ErrorPopover(Gtk.Popover):
prefix = offline.no_mods prefix = offline.no_mods
case FolderError.NO_VALID_MISSION: case FolderError.NO_VALID_MISSION:
prefix = offline.no_mission prefix = offline.no_mission
case FolderError.FOLDER_CHANGED:
prefix = offline.folder_changed
self.label.set_label(prefix)
return
self.label.set_label(f"{prefix}: '{msg}'") self.label.set_label(f"{prefix}: '{msg}'")
@@ -122,6 +110,11 @@ class FolderHBox(HBox):
self.pop.set_relative_to(self.button) self.pop.set_relative_to(self.button)
self.pop.connect("unmap", lambda _: self.grab_focus()) self.pop.connect("unmap", lambda _: self.grab_focus())
self.sidepop = ErrorPopover()
self.sidepop.set_position(Gtk.PositionType.BOTTOM)
self.sidepop.set_relative_to(self.scrolled_label)
self.sidepop.connect("unmap", lambda _: self.grab_focus())
self.connect("map", self._on_map) self.connect("map", self._on_map)
self.connect("unmap", self._on_unmap) self.connect("unmap", self._on_unmap)
@@ -167,12 +160,17 @@ class FolderHBox(HBox):
self.unset_button.show() self.unset_button.show()
def present_error(self, error: FolderError, msg: str) -> None: def present_error(self, error: FolderError, msg: str) -> None:
if error == FolderError.FOLDER_CHANGED:
self.sidepop.set_label(error, msg)
self.sidepop.popup()
return
self.folder = "" self.folder = ""
self.label.set_text("") self.label.set_text("")
self.unset_button.hide() self.unset_button.hide()
self.pop.set_label(error, msg) self.pop.set_label(error, msg)
self.pop.popup() self.pop.popup()
class ModFrame(HeadingFrame): class ModFrame(HeadingFrame):
def __init__( def __init__(
self, parent: OfflineLoader, controller: "Controller", label: str self, parent: OfflineLoader, controller: "Controller", label: str
@@ -198,20 +196,39 @@ class ModFrame(HeadingFrame):
self.tree_vbox = VBox() self.tree_vbox = VBox()
self.tree_vbox.extend([self.scrolled, self.status]) self.tree_vbox.extend([self.scrolled, self.status])
self.no_mods = HBox(spacing=10)
no_mods_icon = Icon(WARNING, margin_start=10)
no_mods_label = Gtk.Label(label=offline.no_local_mods, halign=Gtk.Align.START)
self.no_mods.extend([no_mods_icon, no_mods_label])
self.vbox.pack_end(self.no_mods, expand=True, fill=True, padding=3)
self.vbox.pack_end(self.tree_vbox, expand=True, fill=True, padding=3) self.vbox.pack_end(self.tree_vbox, expand=True, fill=True, padding=3)
self.frame.add(self.vbox) self.frame.add(self.vbox)
sel = self.tree.get_selection() sel = self.tree.get_selection()
sel.connect("changed", self._on_selection_changed) sel.connect("changed", self._on_selection_changed)
self.connect("map", self._on_map)
self.connect("unmap", self._on_unmap) self.connect("unmap", self._on_unmap)
def start_empty(self) -> None:
self.tree_vbox.hide()
self.no_mods.show()
def _on_map(self, widget: Self) -> None:
self.no_mods.hide()
self.tree.set_model(None)
self.tree_vbox.hide()
def _on_unmap(self, widget: Self) -> None: def _on_unmap(self, widget: Self) -> None:
self.tree.set_model(None) self.tree.set_model(None)
self.tree_vbox.hide() self.tree_vbox.hide()
self.no_mods.hide()
def start(self, store: "FastInsertListStore") -> None: def start(self, store: "FastInsertListStore") -> None:
# TODO: this manipulates mod man store out of band
self.tree.set_model(store) self.tree.set_model(store)
self.tree.mod_man.set_store(store)
self.show_tree() self.show_tree()
def get_mods(self) -> list[str]: def get_mods(self) -> list[str]:
@@ -219,6 +236,7 @@ class ModFrame(HeadingFrame):
if model is None: if model is None:
return [] return []
if type(self) is CustomModFrame: if type(self) is CustomModFrame:
# NOTE: custom mod dirs to be hashed
return [model[_iter][2] for _iter in treeiters] return [model[_iter][2] for _iter in treeiters]
else: else:
# NOTE: pre-existing, canonical symlinks to published mods # NOTE: pre-existing, canonical symlinks to published mods
@@ -294,6 +312,10 @@ class CustomModFrame(ModFrame):
self.custom_hbox.hide_label() self.custom_hbox.hide_label()
self.tree.set_model(None) self.tree.set_model(None)
self.tree_vbox.hide() self.tree_vbox.hide()
self.no_mods.hide()
def present_folder_changed(self, folder: str) -> None:
self.custom_hbox.present_error(FolderError.FOLDER_CHANGED, folder)
def present_error(self, folder: str) -> None: def present_error(self, folder: str) -> None:
self.hide_tree() self.hide_tree()
@@ -358,6 +380,9 @@ class MissionFrame(HeadingFrame):
def get_mission(self) -> str: def get_mission(self) -> str:
return self.mission_hbox.get_folder() return self.mission_hbox.get_folder()
def present_folder_changed(self, folder: str) -> None:
self.mission_hbox.present_error(FolderError.FOLDER_CHANGED, folder)
class RadioFrame(HeadingFrame): class RadioFrame(HeadingFrame):
def __init__(self, parent: OfflineLoader, controller: "Controller") -> None: def __init__(self, parent: OfflineLoader, controller: "Controller") -> None:
@@ -459,12 +484,12 @@ class OfflineLoader(Gtk.Box):
self.ok.set_sensitive(True) self.ok.set_sensitive(True)
def _on_keypress(self, widget: Self, event: Gdk.EventKey) -> None: def _on_keypress(self, widget: Self, event: Gdk.EventKey) -> None:
# FIXME: widget is not always in focus
if event.keyval == Gdk.KEY_Escape: if event.keyval == Gdk.KEY_Escape:
self.back.emit("clicked") self.back.emit("clicked")
def populate(self, store: Union["FastInsertListStore", None]) -> None: def populate(self, store: Union["FastInsertListStore", None]) -> None:
if store is None: if store is None:
self.local_frame.start_empty()
return return
self.local_frame.start(store) self.local_frame.start(store)
@@ -477,4 +502,12 @@ class OfflineLoader(Gtk.Box):
local_mods = self.local_frame.get_mods() local_mods = self.local_frame.get_mods()
custom_folder = self.custom_frame.get_folder() custom_folder = self.custom_frame.get_folder()
custom_mods = self.custom_frame.get_mods() custom_mods = self.custom_frame.get_mods()
if custom_folder and not self.offline_man.is_custom_folder_valid(custom_folder):
self.custom_frame.present_folder_changed(mission)
return
if mission and not self.offline_man.is_mission_valid(mission):
self.mission_frame.present_folder_changed(mission)
return
self.offline_man.launch(appid, mission, local_mods, custom_folder, custom_mods) self.offline_man.launch(appid, mission, local_mods, custom_folder, custom_mods)
+3 -19
View File
@@ -2,7 +2,7 @@ from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dzgui.api import pefile as PeFile from dzgui.api import pefile as PeFile
from dzgui.api.steam import find_user_id
from dzgui.config import query from dzgui.config import query
from dzgui.const.constants import ( from dzgui.const.constants import (
APPID_DAYZ, APPID_DAYZ,
@@ -19,9 +19,8 @@ from dzgui.const.endpoints import STEAM_API_SETUP, BM_API_SETUP
from dzgui.const.enum import Preferences, ServerTab from dzgui.const.enum import Preferences, ServerTab
from dzgui.strings import errors, options from dzgui.strings import errors, options
from dzgui.util import strings, css, open_links from dzgui.util import strings, css, open_links
from dzgui.views.components.buttons import SteamWorkshopButton
from dzgui.views.components.labels import LeftLabel from dzgui.views.components.labels import LeftLabel
from dzgui.views.components.eventbox import InfoEventBox
from dzgui.views.components.buttons import WebButton from dzgui.views.components.buttons import WebButton
from dzgui.views.components.frame import HeadingFrame from dzgui.views.components.frame import HeadingFrame
from dzgui.views.components.misc import ClientCombo from dzgui.views.components.misc import ClientCombo
@@ -134,16 +133,6 @@ class Options(Gtk.Box):
[LeftLabel(strings.options.name), self.player_box], [LeftLabel(strings.options.name), self.player_box],
] ]
eb = InfoEventBox(options.workshop_eventbox, controller)
workshop_button = SteamWorkshopButton()
workshop_button.connect(
"clicked", lambda _: self.controller.open_user_workshop(self.uid)
)
mod_rows = [
[LeftLabel(options.workshop_label), workshop_button, eb],
]
self.dayz_version_label = Gtk.Label(label=strings.null) self.dayz_version_label = Gtk.Label(label=strings.null)
self.dayz_exp_version_label = Gtk.Label(label=strings.null) self.dayz_exp_version_label = Gtk.Label(label=strings.null)
@@ -169,7 +158,6 @@ class Options(Gtk.Box):
api_box.add(api_links_box) api_box.add(api_links_box)
prefs_grid = self._make_grid(pref_rows) prefs_grid = self._make_grid(pref_rows)
mods_grid = self._make_grid(mod_rows)
version_grid = self._make_grid(version_rows) version_grid = self._make_grid(version_rows)
col = 1 col = 1
@@ -190,7 +178,6 @@ class Options(Gtk.Box):
for pair in [ for pair in [
(api_box, strings.options.api_keys), (api_box, strings.options.api_keys),
(prefs_grid, strings.options.prefs), (prefs_grid, strings.options.prefs),
(mods_grid, strings.options.mods),
(version_grid, strings.options.version), (version_grid, strings.options.version),
]: ]:
@@ -405,7 +392,7 @@ class Options(Gtk.Box):
if prefs.paths.config.is_file() is False: if prefs.paths.config.is_file() is False:
dialog = ExceptionDialog(self.controller, strings.config_not_found) dialog = ExceptionDialog(self.controller, strings.config_not_found)
dialog.run() dialog.run()
raise Exception raise OSError(f"Config file '{prefs.paths.config}' not found")
config = query.get_config(prefs.paths.config) config = query.get_config(prefs.paths.config)
@@ -416,9 +403,6 @@ class Options(Gtk.Box):
bm = self.controller.query_config(Preferences.BM) bm = self.controller.query_config(Preferences.BM)
steam_path = Path(default_steam_path) steam_path = Path(default_steam_path)
# NOTE: this is a best effort guess at the most recent user
uid = find_user_id(steam_path)
self.uid = "" if uid is None else uid
self.old_steam = steam self.old_steam = steam
self.old_bm = bm self.old_bm = bm
+14 -30
View File
@@ -23,6 +23,7 @@ from gi.repository import Gdk, Gtk # noqa E402
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.managers.connection import Prerequisites from dzgui.managers.connection import Prerequisites
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
from dzgui.controllers.emitter import Emitter
@dataclass @dataclass
@@ -112,18 +113,6 @@ class PreConnectionAssistant(Gtk.Box):
tooltip_text=preconnect.connect_last_tooltip, tooltip_text=preconnect.connect_last_tooltip,
) )
# TODO: abstract
self.raise_window = Gtk.CheckButton(
label="Foreground DZGUI while downloading",
halign=Gtk.Align.END,
hexpand=True,
valign=Gtk.Align.END,
visible=False,
has_tooltip=True,
sensitive=False,
tooltip_text="Foreground the DZGUI window after mod downloads are queued",
active=True,
)
self.button_box = Gtk.Box( self.button_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL, orientation=Gtk.Orientation.VERTICAL,
valign=Gtk.Align.END, valign=Gtk.Align.END,
@@ -133,8 +122,7 @@ class PreConnectionAssistant(Gtk.Box):
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
for button in self.back, self.ok, self.connect_last: for button in self.back, self.ok, self.connect_last:
box.add(button) box.add(button)
for el in self.raise_window, box: self.button_box.add(box)
self.button_box.add(el)
self.back.connect("clicked", self._on_back_clicked) self.back.connect("clicked", self._on_back_clicked)
self.ok.connect("clicked", self._on_ok_clicked) self.ok.connect("clicked", self._on_ok_clicked)
@@ -204,9 +192,14 @@ class PreConnectionAssistant(Gtk.Box):
self.add(self.scrolled_box) self.add(self.scrolled_box)
self.add(self.button_box) self.add(self.button_box)
emitter = self.controller.get_emitter()
emitter.connect("all_mods_synched", self._on_mods_synched)
self.connect("key-press-event", self._on_keypress) self.connect("key-press-event", self._on_keypress)
self.connect("map", self._on_map) self.connect("map", self._on_map)
def _on_mods_synched(self, emitter: "Emitter") -> None:
self.tree.mark_mods_synched()
def _on_map(self, widget: Self) -> None: def _on_map(self, widget: Self) -> None:
widgets = ( widgets = (
self.tree_frame, self.tree_frame,
@@ -217,8 +210,6 @@ class PreConnectionAssistant(Gtk.Box):
for child in widgets: for child in widgets:
child.set_visible(True) child.set_visible(True)
self.raise_window.set_visible(False)
self.raise_window.set_sensitive(False)
self.ok.set_sensitive(True) self.ok.set_sensitive(True)
self.ok.set_label(preconnect.update_mods) self.ok.set_label(preconnect.update_mods)
@@ -230,10 +221,10 @@ class PreConnectionAssistant(Gtk.Box):
self.ok.emit("clicked") self.ok.emit("clicked")
def _on_connect_last_clicked(self, button: Gtk.Button) -> None: def _on_connect_last_clicked(self, button: Gtk.Button) -> None:
self.controller.update_and_load_to_menu(self.raise_window.get_active()) self.controller.update_and_load_to_menu()
def _on_ok_clicked(self, button: Gtk.Button) -> None: def _on_ok_clicked(self, button: Gtk.Button) -> None:
self.controller.update_and_connect(self.raise_window.get_active()) self.controller.update_and_connect()
def _on_back_clicked(self, button: Gtk.Button) -> None: def _on_back_clicked(self, button: Gtk.Button) -> None:
self.controller.open_page(NotebookPage.SERVERS) self.controller.open_page(NotebookPage.SERVERS)
@@ -312,20 +303,16 @@ class PreConnectionAssistant(Gtk.Box):
self._hide_mod_area() self._hide_mod_area()
else: else:
self._show_mod_area() self._show_mod_area()
msg = "All mods are up to date." msg = preconnect.up_to_date
self.mod_count.set_text(msg) self.mod_count.set_text(msg)
if prereqs.required_space == 0: if prereqs.required_space != 0:
self.ok.set_label(preconnect.connect)
self.raise_window.set_visible(False)
else:
self.raise_window.set_visible(True)
self.raise_window.set_sensitive(True)
pretty = number(prereqs.required_space) pretty = number(prereqs.required_space)
suffix = f" Need to download {pretty} MiB of mod updates." suffix = f" Need to download {pretty} MiB of mod updates."
prefix = preconnect.total_mods prefix = preconnect.total_mods
self.mod_count.set_text(f"{prefix}{str(total_mods)}.{suffix}") self.mod_count.set_text(f"{prefix}{str(total_mods)}.{suffix}")
else:
self.ok.set_label(preconnect.connect)
if prereqs.is_last_server: if prereqs.is_last_server:
self.connect_last.show() self.connect_last.show()
@@ -333,10 +320,7 @@ class PreConnectionAssistant(Gtk.Box):
self.connect_last.hide() self.connect_last.hide()
self._process_warnings(prereqs) self._process_warnings(prereqs)
if self.tree.is_visible(): self.tree.grab_focus()
self.tree.grab_focus()
else:
self.grab_focus()
def mark_finished(self) -> None: def mark_finished(self) -> None:
self.mod_count.set_label(preconnect.all_updated) self.mod_count.set_label(preconnect.all_updated)
+1 -1
View File
@@ -16,7 +16,7 @@ logger = logging.getLogger(APP_NAME)
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.controllers.mc import Controller from dzgui.controllers.mc import Controller
from dzgui.controllers.mc import Emitter from dzgui.controllers.emitter import Emitter
class ScrollableTree(Gtk.ScrolledWindow): class ScrollableTree(Gtk.ScrolledWindow):
+4
View File
@@ -48,6 +48,10 @@ class ServerModTreeView(ContextMixin, TreeView): # type: ignore
case _: case _:
pass pass
def mark_mods_synched(self) -> None:
for row in self.mod_store:
row[2] = server_mods.checkmark
# TODO: could be problematic if user downloads mods out of band # TODO: could be problematic if user downloads mods out of band
def _on_row_activated( def _on_row_activated(
self, self,
+2 -1
View File
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
authors = [ authors = [
{name = "aclist"} {name = "aclist"}
] ]
version = "7.0.0b12" version = "7.0.0b14"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
license-files = ["LICENSE"] license-files = ["LICENSE"]
readme = "README.md" readme = "README.md"
@@ -107,6 +107,7 @@ config-settings-package = { pygobject-stubs = { config = "Gtk3,Gdk3,Soup2" } }
[tool.pytest.ini_options] [tool.pytest.ini_options]
markers = [ markers = [
"config: config file keys/values", "config: config file keys/values",
"mods: tests mod metadata/link creation",
"pefile: validate PE files", "pefile: validate PE files",
"post_install: requires a completed installation", "post_install: requires a completed installation",
"slow: long-running tests", "slow: long-running tests",
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
-3
View File
@@ -1,3 +0,0 @@
from dzgui.api.mods import tokenize
print(tokenize("meta4.cpp"))
+8
View File
@@ -0,0 +1,8 @@
1559212036,1771519119
1564026768,1770917948
2545327648,1780174455
2276010135,1756744083
1654462998,1780501511
3410710885,1752864732
2918418331,1780413260
3739934289,1780855136
File renamed without changes.
File renamed without changes.
+42
View File
@@ -0,0 +1,42 @@
#Path to DayZ installation
steam_path="/tmp"
#Battlemetrics API key
api_key=""
#Favorited server IP:PORT array
ip_list=(
)
#Favorite server to fast-connect to (limit one)
fav_server=""
#Favorite server label (human readable)
fav_label=""
#Custom player name (optional, required by some servers)
name=""
#Set to 1 to perform dry-run and print launch options
debug="1"
#Toggle stable/testing branch
branch="testing"
#Steam API key
steam_api=""
#Auto-install mods
auto_install="2"
#Automod staging directory
staging_dir="/tmp"
#Path to default Steam client
default_steam_path="/tmp"
#Preferred Steam launch command (for Flatpak support)
preferred_client="steam"
#DZGUI source path
src_path=""
+15 -3
View File
@@ -1,6 +1,7 @@
import json import json
import pytest import pytest
from dzgui.const.boilerplate import config_boilerplate
from dzgui.config.query import get_config from dzgui.config.query import get_config
from dzgui.config.xdg import get_xdg_paths, parse_filepaths from dzgui.config.xdg import get_xdg_paths, parse_filepaths
from dzgui.config import convert from dzgui.config import convert
@@ -11,7 +12,11 @@ pytestmark = pytest.mark.config
@pytest.fixture @pytest.fixture
def legacy_config(): def legacy_config():
return fixture_path("dztuirc_one") return fixture_path("dztuirc_1")
@pytest.fixture
def unset_values():
return fixture_path("dztuirc_3")
@pytest.fixture @pytest.fixture
@@ -60,8 +65,8 @@ def test_contains_invalid_values(keys, config):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"fixture, expect", "fixture, expect",
[ [
("dztuirc_one", (False, True, True)), ("dztuirc_1", (False, True, True)),
("dztuirc_two", (False, False, False)), ("dztuirc_2", (False, False, False)),
], ],
) )
def test_bool_conversion(fixture, expect): def test_bool_conversion(fixture, expect):
@@ -86,5 +91,12 @@ def test_key_conversion(legacy_config):
for key in keys: for key in keys:
assert key not in j assert key not in j
def test_missing_values(unset_values):
j = convert.rc2json(unset_values)
j = json.loads(j)
assert j.keys() == config_boilerplate.keys()
# TODO: test that when a config file is created from scratch, it contains all values # TODO: test that when a config file is created from scratch, it contains all values
+52
View File
@@ -0,0 +1,52 @@
import pytest
import shutil
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING
import dzgui.api.mods
import dzgui.config.query
from tests.fixtures import fixture_path
if TYPE_CHECKING:
from dzgui.const.enums import Preferences
pytestmark = pytest.mark.mods
@pytest.fixture
def versions() -> str:
return fixture_path("dzg.versions")
@pytest.fixture
def tmp() -> str:
with tempfile.NamedTemporaryFile(delete=False) as f:
tmp = f.name
return tmp
def mock_local_ids(path: Path) -> list[int]:
return [3410710885, 3739934289]
def mock_lookup(path: Path, prefs: "Preferences") -> str:
return ""
def test_signatures(monkeypatch, tmp: str, versions: str) -> None:
ids = [1559212036, 1654462998]
shutil.copyfile(versions, tmp)
path = Path(tmp)
monkeypatch.setattr("dzgui.api.mods.lookup", mock_lookup)
dzgui.api.mods.remove_stale_signatures(path, path, ids)
with open(tmp, "r") as f:
lines = f.readlines()
assert ids not in lines
def test_signatures_with_no_ids(monkeypatch, tmp: str, versions: str) -> None:
shutil.copyfile(versions, tmp)
path = Path(tmp)
monkeypatch.setattr("dzgui.api.mods.get_local_mod_ids", mock_local_ids)
monkeypatch.setattr("dzgui.api.mods.lookup", mock_lookup)
dzgui.api.mods.remove_stale_signatures(path, path)
with open(tmp, "r") as f:
lines = f.readlines()
assert lines == ["3410710885,1752864732\n", "3739934289,1780855136\n"]
+7 -11
View File
@@ -1,17 +1,13 @@
import pytest import pytest
from pathlib import Path
from dzgui.api.mods import tokenize from dzgui.api.mods import tokenize
from tests.fixtures import fixture_path from tests.fixtures import fixture_path
@pytest.mark.mods @pytest.mark.mods
@pytest.mark.parametrize("fixture", [ @pytest.mark.parametrize("i", range(1, 7))
fixture_path("cpp/meta1.cpp"), def test_modmeta(i: int) -> None:
fixture_path("cpp/meta2.cpp"), fixture = fixture_path(f"cpp/mod{i}")
fixture_path("cpp/meta3.cpp"), path = Path(fixture)
fixture_path("cpp/meta4.cpp"), meta = tokenize(path)
fixture_path("cpp/meta5.cpp"),
fixture_path("cpp/meta6.cpp"),
]
)
def test_modmeta(fixture: str) -> None:
meta = tokenize(fixture)
assert meta["name"] == "ModName" assert meta["name"] == "ModName"