mirror of
https://github.com/aclist/dztui.git
synced 2026-11-06 10:00:54 +01:00
Compare commits
86
Commits
e8b18d83a5
...
7.0.0b15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c56f16633 | ||
|
|
2b87272b52 | ||
|
|
4de5147899 | ||
|
|
4c31cd7ecd | ||
|
|
61f90d6016 | ||
|
|
cd1fd1b179 | ||
|
|
1b25c8fadb | ||
|
|
18146bf66f | ||
|
|
8ae54d436a | ||
|
|
4d94551f87 | ||
|
|
1154789f8b | ||
|
|
cbccd4c5f6 | ||
|
|
8cac752a0d | ||
|
|
580273053c | ||
|
|
8d75c1f7b2 | ||
|
|
320c2f692e | ||
|
|
413d84b2b5 | ||
|
|
c751b277d0 | ||
|
|
7f36a3acad | ||
|
|
5780d3daff | ||
|
|
e692e1d139 | ||
|
|
38d44a7952 | ||
|
|
611e3e6e58 | ||
|
|
d5b415316e | ||
|
|
0eeec490c2 | ||
|
|
782157006a | ||
|
|
3be7bc0e02 | ||
|
|
9e7102aaba | ||
|
|
6516e7ae3c | ||
|
|
46c1da232e | ||
|
|
5280121e43 | ||
|
|
17c63cf85f | ||
|
|
488d98800f | ||
|
|
4eac5055cb | ||
|
|
81168d3952 | ||
|
|
ee90be42ee | ||
|
|
48f341bae2 | ||
|
|
0653f064b2 | ||
|
|
525dc24508 | ||
|
|
4e7c831a53 | ||
|
|
e41bef7b00 | ||
|
|
27136a0906 | ||
|
|
cd95a71782 | ||
|
|
0c014a7859 | ||
|
|
65c159505f | ||
|
|
e056ac924a | ||
|
|
d540680330 | ||
|
|
98c45afe7f | ||
|
|
e86040747f | ||
|
|
a559485d0a | ||
|
|
8695793af4 | ||
|
|
a2aeac0242 | ||
|
|
879d0650d4 | ||
|
|
5c85c746f1 | ||
|
|
fa8792ba69 | ||
|
|
5e3392ddec | ||
|
|
796f6b8435 | ||
|
|
780402d14f | ||
|
|
3ae0c8428a | ||
|
|
ed20a7cc84 | ||
|
|
9225f3bc06 | ||
|
|
075a2e5a74 | ||
|
|
58a7878f60 | ||
|
|
f839001097 | ||
|
|
ab2b651b6c | ||
|
|
e5934848c7 | ||
|
|
cab10efc96 | ||
|
|
0a4efba905 | ||
|
|
1e4e09e52c | ||
|
|
910c068109 | ||
|
|
1dd773a9bf | ||
|
|
f4d065d766 | ||
|
|
1fe66076c6 | ||
|
|
668ec1d7a0 | ||
|
|
412f0d3193 | ||
|
|
894a9b106d | ||
|
|
1b0a9c9a69 | ||
|
|
c4818d2097 | ||
|
|
69135150c7 | ||
|
|
4d8949427b | ||
|
|
0139f6bfba | ||
|
|
8d77d46eda | ||
|
|
179f92234a | ||
|
|
650a5ecd7b | ||
|
|
8cebb362e9 | ||
|
|
bfd1d3e0f9 |
No files matched your search
@@ -57,6 +57,7 @@
|
||||
- Disable overlay scrollbars on server tables
|
||||
- Reduce size of geolocation DB on disk (~100MB)
|
||||
- Enable LAN page Empty/Full filters on first run of app
|
||||
- Propagate subscribed mods to Steam client
|
||||
|
||||
## Dropped
|
||||
- Debug mode
|
||||
|
||||
@@ -26,7 +26,7 @@ You can install it via the following set of commands:
|
||||
.. code:: console
|
||||
|
||||
sudo apt update
|
||||
sudo apt install libgirepository-2.0
|
||||
sudo apt install libgirepository-2.0-0
|
||||
|
||||
Next steps
|
||||
---------------
|
||||
|
||||
@@ -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]
|
||||
+17
-7
@@ -49,6 +49,7 @@ def get_local_mods(workshop_path: Path) -> list[Path]:
|
||||
mods = [file for file in workshop_path.iterdir() if file.is_dir()]
|
||||
return mods
|
||||
|
||||
|
||||
def is_mission(path: Path) -> bool:
|
||||
# TODO: parse integrity of other files
|
||||
parent = path.parent.name
|
||||
@@ -57,9 +58,10 @@ def is_mission(path: Path) -> bool:
|
||||
file = path / "init.c"
|
||||
return file.exists()
|
||||
|
||||
|
||||
def tokenize(mod: Path) -> dict[str, Any] | None:
|
||||
file = mod.joinpath("meta.cpp")
|
||||
delimiter=r"\s*=\s*"
|
||||
delimiter = r"\s*=\s*"
|
||||
modmeta = {}
|
||||
try:
|
||||
with open(file, "r", encoding="utf-8") as f:
|
||||
@@ -76,6 +78,7 @@ def tokenize(mod: Path) -> dict[str, Any] | None:
|
||||
logger.critical(e)
|
||||
return None
|
||||
|
||||
|
||||
def get_mod_size(path: Path) -> float:
|
||||
s = 0
|
||||
for f in path.rglob("*"):
|
||||
@@ -123,20 +126,27 @@ def _hash(uid: str, use_custom: bool = False) -> str:
|
||||
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:
|
||||
logger.warning("Creating new version signatures file")
|
||||
versions.touch()
|
||||
return
|
||||
|
||||
path = lookup(config, Preferences.DEFAULT)
|
||||
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:
|
||||
lines = f.readlines()
|
||||
for line in lines:
|
||||
uid = int(line.split(",")[0])
|
||||
if uid not in ids:
|
||||
lines.remove(line)
|
||||
|
||||
if ids is None:
|
||||
lines = [line for line in lines if int(line.split(",")[0]) in local_ids]
|
||||
else:
|
||||
lines = [line for line in lines if int(line.split(",")[0]) not in ids]
|
||||
|
||||
with open(versions, "w") as f:
|
||||
for line in lines:
|
||||
f.write(line)
|
||||
|
||||
@@ -289,13 +289,14 @@ def query_direct(ip: str, qport: int, timeout: float = 3.0) -> dict[str, Any] |
|
||||
|
||||
|
||||
def get_details(record: Record) -> Details:
|
||||
# TODO: consolidate try/except statements
|
||||
ip = record.ip
|
||||
qport = record.qport
|
||||
default_str = strings.none_provided
|
||||
|
||||
try:
|
||||
info = a2s.info((ip, qport))
|
||||
except TimeoutError:
|
||||
except (TimeoutError, a2s.exceptions.BrokenMessageError):
|
||||
return Details(None, default_str, False)
|
||||
try:
|
||||
rules = dayzquery.dayz_rules((ip, qport))
|
||||
|
||||
+47
-16
@@ -3,20 +3,24 @@ import logging
|
||||
import os
|
||||
import requests
|
||||
import subprocess
|
||||
from typing import Union
|
||||
from warnings import deprecated
|
||||
|
||||
from shlex import shlex
|
||||
from pathlib import Path
|
||||
|
||||
from dzgui.init.prereqs import has_steam_client
|
||||
from dzgui.const.constants import (
|
||||
APPID_DAYZ,
|
||||
APP_NAME,
|
||||
DEBIAN_STEAM_PATH,
|
||||
DEFAULT_STEAM_PATH,
|
||||
FLATPAK_STEAM_PATH,
|
||||
UBUNTU_STEAM_PATH,
|
||||
REQUEST_TIMEOUT,
|
||||
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.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:
|
||||
from dzgui.util.symlink import _hash
|
||||
# TODO: circular import workaround
|
||||
from dzgui.api.mods import _hash
|
||||
|
||||
hashes = []
|
||||
for mod in mods:
|
||||
@@ -68,26 +73,21 @@ def get_local_signatures(version_file: Path) -> dict[str, int]:
|
||||
for line in lines:
|
||||
data = line.split(",")
|
||||
_id = data[0]
|
||||
_hash = int(data[1])
|
||||
hashes[_id] = _hash
|
||||
mod_hash = int(data[1])
|
||||
hashes[_id] = mod_hash
|
||||
return hashes
|
||||
|
||||
|
||||
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])
|
||||
|
||||
|
||||
def get_needs_update(
|
||||
version_file: Path, remote_hashes: 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]] = []
|
||||
for title, _id, _hash, size in remote_hashes:
|
||||
if _id not in local_hashes:
|
||||
needs_update.append((title, _id, _hash, size))
|
||||
elif _hash != local_hashes[_id]:
|
||||
needs_update.append((title, _id, _hash, size))
|
||||
for title, _id, stamp, size in remote_hashes:
|
||||
if _id not in local_stamps:
|
||||
needs_update.append((title, _id, stamp, size))
|
||||
elif stamp != local_stamps[_id]:
|
||||
needs_update.append((title, _id, stamp, size))
|
||||
else:
|
||||
continue
|
||||
return needs_update
|
||||
@@ -172,8 +172,10 @@ def launch_offline(
|
||||
"-skipintro",
|
||||
f"-name={name}",
|
||||
f"-mod={symlinks}",
|
||||
f"-mission={mission}",
|
||||
]
|
||||
if len(mission) > 0:
|
||||
arg = f"-mission={mission}"
|
||||
params.append(arg)
|
||||
proc = subprocess.run([*client_args, *params])
|
||||
return proc.returncode
|
||||
|
||||
@@ -232,6 +234,29 @@ def vdf2json(path: Path) -> str:
|
||||
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:
|
||||
# TODO:
|
||||
"""
|
||||
@@ -244,3 +269,9 @@ def gen_shortcut() -> None:
|
||||
# or get right-most 32 bits
|
||||
# STEAMID_64 & 0xFFFFFFFF
|
||||
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])
|
||||
@@ -0,0 +1,152 @@
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER
|
||||
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 copy_bare_configs(config: "Path", resolution: "Path") -> tuple[bool, bool]:
|
||||
# NOTE: temporary workaround for #375
|
||||
conf = "config.json"
|
||||
state = [
|
||||
"dzg.history",
|
||||
"dzg.versions",
|
||||
"dzg.res.json",
|
||||
"dzg.filters.json",
|
||||
"dzg.columns.json",
|
||||
"dzg.notes.json",
|
||||
"ips.csv",
|
||||
".month",
|
||||
]
|
||||
config_changed = False
|
||||
state_changed = False
|
||||
if config.parent.exists() is False:
|
||||
new_file = config
|
||||
old_file = config.parent.parent / conf
|
||||
if old_file.is_file():
|
||||
try:
|
||||
make_parents(new_file)
|
||||
shutil.copy(old_file, new_file)
|
||||
config_changed = True
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
if resolution.parent.exists() is False:
|
||||
state_path = resolution.parent
|
||||
for state_file in state:
|
||||
old_file = state_path.parent / state_file
|
||||
if old_file.is_file():
|
||||
try:
|
||||
new_file = state_path / state_file
|
||||
make_parents(new_file)
|
||||
shutil.copy(old_file, new_file)
|
||||
state_changed = True
|
||||
except Exception as e:
|
||||
logger.critical(e)
|
||||
return (config_changed, state_changed)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
copy_bare_configs(XDG.config, XDG.resolution)
|
||||
|
||||
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)
|
||||
@@ -3,6 +3,8 @@ import shlex
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from dzgui.const.boilerplate import config_boilerplate
|
||||
|
||||
|
||||
"""
|
||||
Convert legacy dztuirc to config.json
|
||||
@@ -89,4 +91,8 @@ def rc2json(file: Path) -> str:
|
||||
keys["ip_list"] = ips
|
||||
keys["use_miles"] = False
|
||||
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)
|
||||
+6
-5
@@ -29,9 +29,9 @@ def is_writeable(path_str: str) -> bool:
|
||||
if not path.exists():
|
||||
try:
|
||||
path.mkdir(parents=True)
|
||||
path.unlink()
|
||||
except OSError:
|
||||
return False
|
||||
path.unlink()
|
||||
return True
|
||||
|
||||
try:
|
||||
@@ -55,11 +55,12 @@ def get_xdg_paths() -> dict:
|
||||
|
||||
resolved_paths = {}
|
||||
for path in xdg_paths:
|
||||
rp = os.environ.get(path)
|
||||
if rp is not None and is_writeable(rp):
|
||||
resolved_paths[path] = Path(rp)
|
||||
real_path = os.environ.get(path)
|
||||
if real_path is not None and is_writeable(real_path):
|
||||
new_path = Path(real_path)
|
||||
else:
|
||||
resolved_paths[path] = xdg_paths[path] / APP_NAME_LOWER
|
||||
new_path = xdg_paths[path]
|
||||
resolved_paths[path] = new_path / APP_NAME_LOWER
|
||||
return resolved_paths
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ UDP_PORT = 27016
|
||||
VM_FILE = "/proc/sys/vm/max_map_count"
|
||||
MIN_COUNT = 1048576
|
||||
|
||||
API_RATE_LIMIT = 1
|
||||
CLIENT_RATE_LIMIT = 3
|
||||
REQUEST_TIMEOUT = 10
|
||||
|
||||
APPNAME_DAYZ = "DayZ"
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
# internal
|
||||
STEAM_PUBLISHED_FILES = "https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/?format=json"
|
||||
STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1/?"
|
||||
STEAM_PUBLISHED_FILES = (
|
||||
"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?"
|
||||
GITHUB = "https://github.com/aclist"
|
||||
GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest"
|
||||
|
||||
+5
-5
@@ -168,7 +168,7 @@ class ContextMenu(EnumWithAttrs):
|
||||
COPY_LOG_CLIPBOARD = {"label": strings.copy_log}
|
||||
COPY_SERVER_IP = {"label": strings.copy_ip}
|
||||
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}
|
||||
REFRESH_PLAYERS = {"label": strings.refresh_players}
|
||||
REMOVE_HISTORY = {"label": strings.remove_history}
|
||||
@@ -184,7 +184,7 @@ class ContextMenuGroup(Enum):
|
||||
"""
|
||||
|
||||
SERVER_MOD = (ContextMenu.OPEN_WORKSHOP,)
|
||||
MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.DELETE_MOD)
|
||||
MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.UNSUB_MOD)
|
||||
MOD_OFFLINE = (None,)
|
||||
LOG = (ContextMenu.COPY_LOG_CLIPBOARD,)
|
||||
SERVER_BROWSER = (
|
||||
@@ -250,9 +250,9 @@ class ModButton(EnumWithAttrs):
|
||||
"label": strings.mod_panel.unhighlight_stale,
|
||||
"tooltip": strings.mod_panel.unhighlight_stale_tooltip,
|
||||
}
|
||||
DELETE_SELECTED = {
|
||||
"label": strings.mod_panel.delete_selected,
|
||||
"tooltip": strings.mod_panel.delete_selected_tooltip,
|
||||
UNSUB_SELECTED = {
|
||||
"label": strings.mod_panel.unsub_selected,
|
||||
"tooltip": strings.mod_panel.unsub_selected_tooltip,
|
||||
}
|
||||
SELECT_STALE = {
|
||||
"label": strings.mod_panel.select_stale,
|
||||
|
||||
@@ -81,8 +81,8 @@ class Emitter(GObject.GObject):
|
||||
def distcalc_started(self) -> None:
|
||||
pass
|
||||
|
||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(bool,))
|
||||
def mod_page_toggled(self, state: bool) -> None:
|
||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(bool, int))
|
||||
def mod_page_toggled(self, state: bool, mods: int) -> None:
|
||||
pass
|
||||
|
||||
@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=())
|
||||
def invalid_custom_mods(self) -> None:
|
||||
pass
|
||||
|
||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
|
||||
def all_mods_synched(self) -> None:
|
||||
pass
|
||||
+10
-8
@@ -23,7 +23,7 @@ from dzgui.managers.contextmenu import ContextMenuManager
|
||||
from dzgui.managers.notes import NoteManager
|
||||
from dzgui.model.servers import ServerModelManager
|
||||
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.views.dialogs.filepicker import FilePicker
|
||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||
@@ -203,7 +203,9 @@ class Controller(GObject.GObject):
|
||||
try:
|
||||
# TODO: where to put config file check
|
||||
self.mediator.grid.notebook.settings.populate_settings()
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
msg = format_exception(e)
|
||||
logger.critical(msg)
|
||||
return
|
||||
case ButtonType.MODS:
|
||||
self.load_mods()
|
||||
@@ -233,7 +235,7 @@ class Controller(GObject.GObject):
|
||||
mod_man = self.mediator.modtreeview.get_mod_man()
|
||||
mod_man.toggle_mod_selection(state)
|
||||
|
||||
def delete_mods(
|
||||
def unsub_mods(
|
||||
self, treeview: Union["ModTreeView", "OfflineModTreeView", None] = None
|
||||
) -> None:
|
||||
if treeview is None:
|
||||
@@ -241,7 +243,7 @@ class Controller(GObject.GObject):
|
||||
else:
|
||||
view = treeview
|
||||
mod_man = view.get_mod_man()
|
||||
mod_man.delete_mods()
|
||||
mod_man.unsub_mods()
|
||||
|
||||
def get_mod_store(self) -> Gtk.TreeModel | None:
|
||||
return self.mediator.modtreeview.get_model()
|
||||
@@ -523,11 +525,11 @@ class Controller(GObject.GObject):
|
||||
ind = self.config_man.get_start_tab()
|
||||
self.get_servers().notebook.set_current_page(ind)
|
||||
|
||||
def update_and_load_to_menu(self, raise_window: bool) -> None:
|
||||
self.connection_man.update_and_connect(raise_window, menu_only=True)
|
||||
def update_and_load_to_menu(self) -> None:
|
||||
self.connection_man.update_and_connect(menu_only=True)
|
||||
|
||||
def update_and_connect(self, raise_window: bool) -> None:
|
||||
self.connection_man.update_and_connect(raise_window)
|
||||
def update_and_connect(self) -> None:
|
||||
self.connection_man.update_and_connect()
|
||||
|
||||
def update_status(self) -> None:
|
||||
self.mediator.preconnect.mark_finished()
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
- Preboot progress dialog
|
||||
- Choose to jump into splash screen instead of server
|
||||
- Collapsible connection panel
|
||||
- Play offline (load mods directly)
|
||||
|
||||
## Changed
|
||||
- Conform to PEP 440 versioning for beta versions
|
||||
@@ -56,6 +57,7 @@
|
||||
- Disable overlay scrollbars on server tables
|
||||
- Reduce size of geolocation DB on disk (~100MB)
|
||||
- Enable LAN page Empty/Full filters on first run of app
|
||||
- Propagate subscribed mods to Steam client
|
||||
|
||||
## Dropped
|
||||
- Debug mode
|
||||
|
||||
+4
-5
@@ -1,11 +1,12 @@
|
||||
import platform
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
from ctypes.util import find_library
|
||||
from warnings import deprecated
|
||||
|
||||
LIB = "libgirepository-2.0"
|
||||
|
||||
|
||||
@deprecated("currently unused")
|
||||
def is_debian() -> bool:
|
||||
"""
|
||||
@@ -39,12 +40,10 @@ def is_debian() -> bool:
|
||||
|
||||
|
||||
def has_libgi() -> bool:
|
||||
parent_dir = Path("/usr/lib64")
|
||||
exists = any(LIB in subdir.name for subdir in parent_dir.iterdir())
|
||||
return exists
|
||||
return bool(find_library("girepository-2.0"))
|
||||
|
||||
|
||||
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."
|
||||
if has_libgi() is False:
|
||||
print(msg)
|
||||
|
||||
+9
-1
@@ -1,10 +1,12 @@
|
||||
import psutil
|
||||
import subprocess
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
from warnings import deprecated
|
||||
|
||||
from dzgui.const.constants import (
|
||||
APP_NAME,
|
||||
DAYZ_BINARY,
|
||||
STEAM_CMD,
|
||||
FLATPAK_APPID,
|
||||
@@ -13,6 +15,10 @@ from dzgui.const.constants import (
|
||||
FLATPAK_SANDBOX,
|
||||
)
|
||||
|
||||
from dzgui.util.format import format_exception
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
|
||||
# TODO: move to util.proc
|
||||
def is_dayz_running() -> bool:
|
||||
@@ -22,7 +28,9 @@ def is_dayz_running() -> bool:
|
||||
for proc in psutil.process_iter():
|
||||
try:
|
||||
procs.append(proc.cmdline())
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
msg = format_exception(e)
|
||||
logger.warning(msg)
|
||||
continue
|
||||
return any(substring in item for sublist in procs for item in sublist)
|
||||
|
||||
|
||||
+6
-103
@@ -1,40 +1,11 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
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.flock import lock_acquire
|
||||
from dzgui.init.migrate import (
|
||||
has_new_config,
|
||||
copy_state_files,
|
||||
migrate_cols_file,
|
||||
)
|
||||
from dzgui.init.libgi import test_libgi_missing
|
||||
from dzgui.init.prefix import get_version
|
||||
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, 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)
|
||||
from dzgui.util.strings import flags
|
||||
|
||||
parser = argparse.ArgumentParser(description=flags.description)
|
||||
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()
|
||||
|
||||
|
||||
# TODO: profile load time
|
||||
def make_parents(path: "Path") -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def uninstall() -> None:
|
||||
# TODO: uninstall data files (-u)
|
||||
# -u removes state, log, freedesktop
|
||||
@@ -57,21 +23,7 @@ def uninstall() -> None:
|
||||
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:
|
||||
lock = lock_acquire() # noqa
|
||||
|
||||
if args.version is True:
|
||||
print(get_version())
|
||||
sys.exit(0)
|
||||
@@ -82,60 +34,11 @@ def main() -> None:
|
||||
warnings.filterwarnings("default", category=DeprecationWarning)
|
||||
|
||||
version = get_version()
|
||||
set_locale()
|
||||
|
||||
print(f"{APP_NAME} {version}")
|
||||
test_libgi_missing()
|
||||
|
||||
# NOTE: consider aborting this check if steam deck
|
||||
xdg_paths = get_xdg_paths()
|
||||
XDG = parse_filepaths(xdg_paths)
|
||||
# NOTE: only import ligbirepository modules after the above check
|
||||
from dzgui.app_init import load_gui
|
||||
|
||||
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=args.debug,
|
||||
coords=local_coords,
|
||||
version=version,
|
||||
paths=XDG,
|
||||
latest_release=latest_release,
|
||||
use_miles=use_miles,
|
||||
)
|
||||
print(boot.all_ok)
|
||||
App(prefs)
|
||||
load_gui(version, args.debug)
|
||||
@@ -12,10 +12,10 @@ import dzgui.api.servers as Servers
|
||||
|
||||
from dzgui.api.steam import (
|
||||
connect,
|
||||
enqueue_mod,
|
||||
get_remote_signatures,
|
||||
get_needs_update,
|
||||
load_to_menu,
|
||||
subscribe,
|
||||
)
|
||||
|
||||
from dzgui.api.mods import (
|
||||
@@ -25,6 +25,7 @@ from dzgui.api.mods import (
|
||||
update_signatures,
|
||||
)
|
||||
from dzgui.const.constants import (
|
||||
API_RATE_LIMIT,
|
||||
APP_NAME,
|
||||
APPID_DAYZ,
|
||||
APPID_DAYZ_EXP,
|
||||
@@ -34,7 +35,11 @@ from dzgui.const.constants import (
|
||||
from dzgui.const.enum import NotebookPage, Preferences
|
||||
from dzgui.init.proc import is_dayz_running, is_steam_running
|
||||
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.util.format import format_mib
|
||||
from dzgui.util.strings import dialog, server_timeout
|
||||
@@ -143,21 +148,14 @@ class ConnectionManager:
|
||||
remote_mods: list[list[str]] = []
|
||||
if res.is_modded():
|
||||
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]
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
self.thread_man.set_cleanup_func(failure_func, destroy_first=True)
|
||||
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:
|
||||
pefile_path = PeFile.get_pefile_path(steam_path, info.game_id)
|
||||
total, used, free = shutil.disk_usage(pefile_path)
|
||||
@@ -221,10 +219,13 @@ class ConnectionManager:
|
||||
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)
|
||||
steam_path = self.controller.query_config(Preferences.DEFAULT)
|
||||
local = get_local_mod_ids(steam_path)
|
||||
|
||||
alpha_mods: list[list[str]] = [
|
||||
[
|
||||
mod.name,
|
||||
@@ -234,12 +235,23 @@ class ConnectionManager:
|
||||
for mod in mods
|
||||
]
|
||||
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)
|
||||
def query_modlist_and_present(self, record: Servers.Record) -> None:
|
||||
try:
|
||||
mods = self._query_modlist(record)
|
||||
mods, missing_mods = self._query_modlist(record)
|
||||
except Exception:
|
||||
self.thread_man.set_cleanup_func(
|
||||
StoredFunc(self._server_timeout), destroy_first=True
|
||||
@@ -275,7 +287,6 @@ class ConnectionManager:
|
||||
self.thread_man.set_cleanup_func(func)
|
||||
return
|
||||
|
||||
self.thread_man.show_cancel(False)
|
||||
self.thread_man.update_dialog(waiting_for_launch)
|
||||
while True:
|
||||
# FIXME: cancel should not be visible here per setting above
|
||||
@@ -293,21 +304,18 @@ class ConnectionManager:
|
||||
self.controller.add_to_history(self.history, self.record)
|
||||
self.controller.open_page(NotebookPage.SERVERS)
|
||||
|
||||
def _update_mods(self, raise_window: bool, menu_only: bool = False) -> None:
|
||||
# NOTE: fast enqueue all mods in auto mode
|
||||
def _update_mods(self, menu_only: bool = False) -> None:
|
||||
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:
|
||||
if self.controller.is_cancel_pending():
|
||||
return
|
||||
enqueue_mod(self.client, mod, self.appid)
|
||||
# NOTE: prevents rate limiting
|
||||
time.sleep(3)
|
||||
|
||||
if raise_window is True:
|
||||
logger.info("Bringing window to foreground")
|
||||
GLib.idle_add(self.controller.present_window)
|
||||
subscribe(key, int(mod))
|
||||
time.sleep(API_RATE_LIMIT)
|
||||
|
||||
self.thread_man.update_dialog(waiting_for_directories)
|
||||
for title, mod, stamp, size in self.missing_mods:
|
||||
mod_path = self.workshop / mod
|
||||
|
||||
@@ -325,13 +333,17 @@ class ConnectionManager:
|
||||
time.sleep(1)
|
||||
|
||||
update_signatures(self.missing_mods, prefs.paths.version)
|
||||
# TODO: just push steam path directly
|
||||
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)
|
||||
|
||||
@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:
|
||||
self._update_mods(raise_window, menu_only)
|
||||
self._update_mods(menu_only)
|
||||
else:
|
||||
self._connect_steam(menu_only)
|
||||
@@ -80,8 +80,8 @@ class ContextMenuManager:
|
||||
|
||||
if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)):
|
||||
match action:
|
||||
case ContextMenu.DELETE_MOD:
|
||||
self.controller.delete_mods(self.treeview)
|
||||
case ContextMenu.UNSUB_MOD:
|
||||
self.controller.unsub_mods(self.treeview)
|
||||
case ContextMenu.OPEN_WORKSHOP:
|
||||
self.open_mod_page()
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ class FilterManager:
|
||||
if maps is None:
|
||||
return
|
||||
if len(maps) < 1:
|
||||
self.reinit_map_store()
|
||||
return
|
||||
|
||||
self.reinit_map_store()
|
||||
@@ -89,8 +90,9 @@ class FilterManager:
|
||||
continue
|
||||
self.append_map([m])
|
||||
|
||||
# TODO: currently unused
|
||||
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:
|
||||
map_name = self.get_active_map_name()
|
||||
|
||||
+40
-20
@@ -1,23 +1,29 @@
|
||||
import logging
|
||||
import shutil
|
||||
import time
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dzgui.api.steam import unsubscribe
|
||||
from dzgui.api.mods import (
|
||||
get_delimited_mods,
|
||||
get_local_mod_path,
|
||||
find_stale_mods,
|
||||
_hash,
|
||||
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.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||
from dzgui.model.model_factory import FastInsertListStore, ModelFactory
|
||||
from dzgui.strings import dialogs
|
||||
from dzgui.util.format import format_mods
|
||||
from dzgui.util.strings import server_timeout
|
||||
from dzgui.util.symlink import rebuild_symlinks
|
||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||
|
||||
|
||||
@@ -61,9 +67,13 @@ class ModManager:
|
||||
self.thread_man.set_cleanup_func(func)
|
||||
return
|
||||
|
||||
prefs = self.controller.get_prefs()
|
||||
rebuild_symlinks(prefs.paths.config)
|
||||
|
||||
self.store = ModelFactory().make_mod_store()
|
||||
self.store.extend(mods)
|
||||
func = StoredFunc(self._on_mods_loaded)
|
||||
|
||||
self.thread_man.set_cleanup_func(func)
|
||||
|
||||
def set_store(self, store: "FastInsertListStore") -> None:
|
||||
@@ -77,7 +87,7 @@ class ModManager:
|
||||
total_mods = len(self.store)
|
||||
self.emitter.emit("mods_updated", msg, total_mods)
|
||||
|
||||
def delete_mods(self) -> None:
|
||||
def unsub_mods(self) -> None:
|
||||
sel = self.treeview.get_selection()
|
||||
model, pathlist = sel.get_selected_rows()
|
||||
# NOTE: reverse when multiple selection
|
||||
@@ -88,7 +98,8 @@ class ModManager:
|
||||
continue
|
||||
mod, _iter = res
|
||||
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(
|
||||
self, tree_path: Gtk.TreePath
|
||||
@@ -101,24 +112,29 @@ class ModManager:
|
||||
return mod, tree_iter
|
||||
|
||||
@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:
|
||||
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_deleted, iters)
|
||||
func = StoredFunc(self._on_mods_unsubbed, mods)
|
||||
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)
|
||||
mods_path = get_local_mod_path(steam_path)
|
||||
app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
|
||||
|
||||
md5 = _hash(mod)
|
||||
symlink = app_path / md5
|
||||
symlink.unlink()
|
||||
shutil.rmtree(mods_path / mod)
|
||||
|
||||
try:
|
||||
md5 = _hash(mod)
|
||||
symlink = app_path / md5
|
||||
symlink.unlink()
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
# NOTE: second pass to unlink DAYZ_EXP mods
|
||||
# TODO: test this with working APPID_DAYZ_EXP installation
|
||||
try:
|
||||
@@ -127,19 +143,23 @@ class ModManager:
|
||||
symlink.unlink()
|
||||
except PeFile.AppNotInstalledError:
|
||||
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:
|
||||
return
|
||||
for _iter in iters:
|
||||
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()
|
||||
if model is None:
|
||||
return
|
||||
mods = len(model)
|
||||
total_mods = len(model)
|
||||
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:
|
||||
model = self.treeview.get_model()
|
||||
|
||||
@@ -12,7 +12,7 @@ from dzgui.init.proc import is_dayz_running
|
||||
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||
from dzgui.model.model_factory import ModelFactory
|
||||
from dzgui.strings import dialogs
|
||||
from dzgui.util.symlink import create_custom_symlinks, symlink_mission
|
||||
from dzgui.util.symlink import create_custom_symlinks, rebuild_symlinks, symlink_mission
|
||||
from dzgui.views.dialogs.filepicker import FolderPicker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -34,10 +34,18 @@ class OfflineManager:
|
||||
self.thread_man = ThreadingManager(controller)
|
||||
|
||||
self.appid: int
|
||||
self.mission_folder: str
|
||||
self.local_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:
|
||||
folder = self.open_folderpicker(dialogs.mission_dialog)
|
||||
if folder is None:
|
||||
@@ -88,6 +96,8 @@ class OfflineManager:
|
||||
|
||||
if len(local_mods) > 0:
|
||||
combined_mods.extend(local_mods)
|
||||
config = self.controller.get_prefs().paths.config
|
||||
rebuild_symlinks(config)
|
||||
|
||||
if len(custom_mods) > 0:
|
||||
new_symlinks = create_custom_symlinks(
|
||||
|
||||
@@ -81,6 +81,11 @@ class ThreadingManager:
|
||||
def set_job_count(self, jobs: int) -> None:
|
||||
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:
|
||||
GLib.idle_add(lambda: self.wait_dialog.update_text(msg))
|
||||
|
||||
|
||||
@@ -374,7 +374,7 @@ class ServerModelManager:
|
||||
proxy = self._get_proxy_man().get_proxy_model()
|
||||
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)
|
||||
|
||||
filter_man = self.tv.get_filter_man()
|
||||
@@ -395,8 +395,10 @@ class ServerModelManager:
|
||||
self.first_iteration = False
|
||||
|
||||
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)
|
||||
if self.preserve_on_fail is True:
|
||||
self.proxy_man.wipe_cache()
|
||||
|
||||
self.emitter.emit("servers_loaded", self.enum)
|
||||
if self.first_iteration:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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"
|
||||
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."
|
||||
fetching_mods = "Fetching mod metadata"
|
||||
deleting_mods = "Deleting mods"
|
||||
deleting_mods = "Unsubscribing mods"
|
||||
scanning_mods = "Scanning mods"
|
||||
parsing_mods = "Parsing mods"
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ local_frame = "Installed mods"
|
||||
no_local_mods = "No local mods found"
|
||||
no_mods = "No valid mods found"
|
||||
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."
|
||||
mission_eventbox = "Select a single mission folder containing an init.c file."
|
||||
|
||||
@@ -10,6 +10,7 @@ warnings = "Warnings"
|
||||
errors = "Errors"
|
||||
mods = "Mods"
|
||||
total_mods = "Total mods: "
|
||||
up_to_date = "All mods are up to date."
|
||||
all_updated = "All mods updated."
|
||||
"If you recently installed {build} or moved it to a different drive, "
|
||||
"restart Steam to allow these changes to synchronize, then try again."
|
||||
@@ -52,7 +52,7 @@ show_mods = "Show server-side mods"
|
||||
show_details = "Show server details"
|
||||
refresh_players = "Refresh player count"
|
||||
open_workshop = "Open in Steam Workshop"
|
||||
delete_mod = "Delete mod"
|
||||
unsub_mod = "Unsubscribe mod"
|
||||
copy_name = "Copy name to clipboard"
|
||||
copy_ip = "Copy IP to clipboard"
|
||||
copy_log = "Copy record(s) to clipboard"
|
||||
@@ -130,7 +130,6 @@ servers = {
|
||||
"Enter/space/double-click": "connect to server",
|
||||
"Right-click/Ctrl-l": "additional context menus",
|
||||
"Ctrl-r": "refresh players",
|
||||
"Ctrl-p": "refresh ping",
|
||||
"Ctrl-f": "jump to keyword search field",
|
||||
"Ctrl-m": "jump to maps field",
|
||||
"Ctrl-d": "select default LAN port",
|
||||
@@ -148,8 +147,8 @@ servers = {
|
||||
vim = {
|
||||
"j": "Move down a row/scroll up",
|
||||
"k": "Move up a row/scroll down",
|
||||
"l": "Jump to main area from sidebar",
|
||||
"h": "Jump to sidebar from main area",
|
||||
"h": "Jump to main area from sidebar",
|
||||
"l": "Jump to sidebar from main area",
|
||||
"g": "Jump to first row/top of page",
|
||||
"G": "Jump to last row/bottom of page",
|
||||
}
|
||||
@@ -215,8 +214,8 @@ class ModPanelStrings:
|
||||
unhighlight_stale_tooltip: str
|
||||
highlight_stale: str
|
||||
highlight_stale_tooltip: str
|
||||
delete_selected: str
|
||||
delete_selected_tooltip: str
|
||||
unsub_selected: str
|
||||
unsub_selected_tooltip: str
|
||||
unselect_all: str
|
||||
unselect_all_tooltip: str
|
||||
select_all: str
|
||||
@@ -337,8 +336,8 @@ mod_panel = ModPanelStrings(
|
||||
"Shows locally-installed mods which are not used by any server "
|
||||
"in your Saved Servers"
|
||||
),
|
||||
delete_selected="Delete selected",
|
||||
delete_selected_tooltip="Deletes selected mods from the system",
|
||||
unsub_selected="Unsubscribe selected",
|
||||
unsub_selected_tooltip="Unsubscribes from selected mods",
|
||||
unselect_all="Unselect all",
|
||||
unselect_all_tooltip="Bulk unselects all mods",
|
||||
select_all="Select all",
|
||||
@@ -367,6 +366,7 @@ thanks = Thanks(
|
||||
"Johnofwrong",
|
||||
"MatheusLasserr",
|
||||
"nolan-perez",
|
||||
"OnniSaarni",
|
||||
"scandalouss",
|
||||
"SnackSBR",
|
||||
"StevelDusa",
|
||||
@@ -463,7 +463,7 @@ crumbs = Crumbs(
|
||||
thanks="Help > Special thanks",
|
||||
developers="Options > Developers",
|
||||
default="Servers > ",
|
||||
offline="Mods > Play offline"
|
||||
offline="Mods > Play offline",
|
||||
)
|
||||
|
||||
no_mods = "No local mods found."
|
||||
|
||||
@@ -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)
|
||||
@@ -141,6 +141,8 @@ class RefreshButton(IconTextButton):
|
||||
|
||||
def _on_refresh_clicked(self, button: Self) -> None:
|
||||
"""Spawned in a thread"""
|
||||
if self.controller.get_active_treeview().is_loaded() is False:
|
||||
return
|
||||
self.is_clicked = True
|
||||
self.controller.refresh_tree()
|
||||
# TODO: get server tab enum
|
||||
@@ -208,6 +210,8 @@ class SteamWorkshopButton(SteamTextButton):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(label=buttons.workshop)
|
||||
self.set_tooltip_text(buttons.workshop_tooltip)
|
||||
self.set_margin_top(10)
|
||||
self.set_margin_bottom(10)
|
||||
|
||||
|
||||
class AddButton(IconTextButton):
|
||||
|
||||
@@ -20,7 +20,8 @@ gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk # noqa E402
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller, Emitter
|
||||
from dzgui.controllers.mc import Controller
|
||||
from dzgui.controllers.emitter import Emitter
|
||||
|
||||
COLS = 1
|
||||
ROWS = 1
|
||||
|
||||
@@ -11,7 +11,8 @@ gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
|
||||
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):
|
||||
@@ -48,7 +49,7 @@ class ModSelectionPanel(Gtk.Box):
|
||||
buttons = (
|
||||
ModButton.SELECT_ALL,
|
||||
ModButton.UNSELECT_ALL,
|
||||
ModButton.DELETE_SELECTED,
|
||||
ModButton.UNSUB_SELECTED,
|
||||
)
|
||||
for button in buttons:
|
||||
b = EnumeratedModButton(button)
|
||||
@@ -79,14 +80,18 @@ class ModSelectionPanel(Gtk.Box):
|
||||
self.highlight_stale_button.set_sensitive(True)
|
||||
|
||||
def _on_mods_updated(self, emitter: "Emitter", msg: str, mods: int) -> None:
|
||||
if mods < 1:
|
||||
self.main_panel.set_sensitive(False)
|
||||
self.stale_panel.set_sensitive(False)
|
||||
self._toggle_panel_sensitivity(mods)
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
def swap_sensitive(self, state: bool) -> None:
|
||||
@@ -100,8 +105,8 @@ class ModSelectionPanel(Gtk.Box):
|
||||
self.controller.toggle_mod_selection(True)
|
||||
case ModButton.UNSELECT_ALL:
|
||||
self.controller.toggle_mod_selection(False)
|
||||
case ModButton.DELETE_SELECTED:
|
||||
self.controller.delete_mods()
|
||||
case ModButton.UNSUB_SELECTED:
|
||||
self.controller.unsub_mods()
|
||||
case ModButton.HIGHLIGHT_STALE:
|
||||
self.controller.highlight_stale()
|
||||
case ModButton.UNHIGHLIGHT_STALE:
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
from pathlib import Path
|
||||
|
||||
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.trees.tree_mods import ModTreeView
|
||||
|
||||
@@ -26,6 +31,17 @@ class Mods(Gtk.Box):
|
||||
self.controller.register_widget("modtreeview", self.tree)
|
||||
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(
|
||||
label="Play offline",
|
||||
halign=Gtk.Align.START,
|
||||
@@ -34,7 +50,10 @@ class Mods(Gtk.Box):
|
||||
)
|
||||
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.connect("map", self._on_map)
|
||||
@@ -45,10 +64,15 @@ class Mods(Gtk.Box):
|
||||
self.controller.open_offline()
|
||||
|
||||
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:
|
||||
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
|
||||
# NOTE: handles going back from offline mods page
|
||||
# more generic cache restoration method
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
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.const.constants import (
|
||||
@@ -16,6 +16,7 @@ from dzgui.const.constants import (
|
||||
from dzgui.const.enum import ContextMenuGroup, NotebookPage
|
||||
from dzgui.managers.offline import OfflineManager
|
||||
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.eventbox import InfoEventBox
|
||||
from dzgui.views.components.frame import HeadingFrame
|
||||
@@ -37,25 +38,7 @@ if TYPE_CHECKING:
|
||||
class FolderError(Enum):
|
||||
NO_VALID_MODS = 1
|
||||
NO_VALID_MISSION = 2
|
||||
|
||||
|
||||
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)
|
||||
FOLDER_CHANGED = 3
|
||||
|
||||
|
||||
class PageHeading(Gtk.Label):
|
||||
@@ -83,6 +66,10 @@ class ErrorPopover(Gtk.Popover):
|
||||
prefix = offline.no_mods
|
||||
case FolderError.NO_VALID_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}'")
|
||||
|
||||
|
||||
@@ -123,6 +110,11 @@ class FolderHBox(HBox):
|
||||
self.pop.set_relative_to(self.button)
|
||||
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("unmap", self._on_unmap)
|
||||
|
||||
@@ -168,6 +160,10 @@ class FolderHBox(HBox):
|
||||
self.unset_button.show()
|
||||
|
||||
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.label.set_text("")
|
||||
self.unset_button.hide()
|
||||
@@ -318,6 +314,9 @@ class CustomModFrame(ModFrame):
|
||||
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:
|
||||
self.hide_tree()
|
||||
self.custom_hbox.present_error(FolderError.NO_VALID_MODS, folder)
|
||||
@@ -381,6 +380,9 @@ class MissionFrame(HeadingFrame):
|
||||
def get_mission(self) -> str:
|
||||
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):
|
||||
def __init__(self, parent: OfflineLoader, controller: "Controller") -> None:
|
||||
@@ -500,4 +502,12 @@ class OfflineLoader(Gtk.Box):
|
||||
local_mods = self.local_frame.get_mods()
|
||||
custom_folder = self.custom_frame.get_folder()
|
||||
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)
|
||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dzgui.api import pefile as PeFile
|
||||
from dzgui.api.steam import find_user_id
|
||||
|
||||
from dzgui.config import query
|
||||
from dzgui.const.constants import (
|
||||
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.strings import errors, options
|
||||
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.eventbox import InfoEventBox
|
||||
from dzgui.views.components.buttons import WebButton
|
||||
from dzgui.views.components.frame import HeadingFrame
|
||||
from dzgui.views.components.misc import ClientCombo
|
||||
@@ -134,16 +133,6 @@ class Options(Gtk.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_exp_version_label = Gtk.Label(label=strings.null)
|
||||
|
||||
@@ -169,7 +158,6 @@ class Options(Gtk.Box):
|
||||
api_box.add(api_links_box)
|
||||
|
||||
prefs_grid = self._make_grid(pref_rows)
|
||||
mods_grid = self._make_grid(mod_rows)
|
||||
version_grid = self._make_grid(version_rows)
|
||||
|
||||
col = 1
|
||||
@@ -190,7 +178,6 @@ class Options(Gtk.Box):
|
||||
for pair in [
|
||||
(api_box, strings.options.api_keys),
|
||||
(prefs_grid, strings.options.prefs),
|
||||
(mods_grid, strings.options.mods),
|
||||
(version_grid, strings.options.version),
|
||||
]:
|
||||
|
||||
@@ -405,7 +392,7 @@ class Options(Gtk.Box):
|
||||
if prefs.paths.config.is_file() is False:
|
||||
dialog = ExceptionDialog(self.controller, strings.config_not_found)
|
||||
dialog.run()
|
||||
raise Exception
|
||||
raise OSError(f"Config file '{prefs.paths.config}' not found")
|
||||
|
||||
config = query.get_config(prefs.paths.config)
|
||||
|
||||
@@ -416,9 +403,6 @@ class Options(Gtk.Box):
|
||||
bm = self.controller.query_config(Preferences.BM)
|
||||
|
||||
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_bm = bm
|
||||
|
||||
@@ -23,6 +23,7 @@ from gi.repository import Gdk, Gtk # noqa E402
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.managers.connection import Prerequisites
|
||||
from dzgui.controllers.mc import Controller
|
||||
from dzgui.controllers.emitter import Emitter
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -112,18 +113,6 @@ class PreConnectionAssistant(Gtk.Box):
|
||||
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(
|
||||
orientation=Gtk.Orientation.VERTICAL,
|
||||
valign=Gtk.Align.END,
|
||||
@@ -133,8 +122,7 @@ class PreConnectionAssistant(Gtk.Box):
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
||||
for button in self.back, self.ok, self.connect_last:
|
||||
box.add(button)
|
||||
for el in self.raise_window, box:
|
||||
self.button_box.add(el)
|
||||
self.button_box.add(box)
|
||||
|
||||
self.back.connect("clicked", self._on_back_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.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("map", self._on_map)
|
||||
|
||||
def _on_mods_synched(self, emitter: "Emitter") -> None:
|
||||
self.tree.mark_mods_synched()
|
||||
|
||||
def _on_map(self, widget: Self) -> None:
|
||||
widgets = (
|
||||
self.tree_frame,
|
||||
@@ -217,8 +210,6 @@ class PreConnectionAssistant(Gtk.Box):
|
||||
for child in widgets:
|
||||
child.set_visible(True)
|
||||
|
||||
self.raise_window.set_visible(False)
|
||||
self.raise_window.set_sensitive(False)
|
||||
self.ok.set_sensitive(True)
|
||||
self.ok.set_label(preconnect.update_mods)
|
||||
|
||||
@@ -230,10 +221,10 @@ class PreConnectionAssistant(Gtk.Box):
|
||||
self.ok.emit("clicked")
|
||||
|
||||
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:
|
||||
self.controller.update_and_connect(self.raise_window.get_active())
|
||||
self.controller.update_and_connect()
|
||||
|
||||
def _on_back_clicked(self, button: Gtk.Button) -> None:
|
||||
self.controller.open_page(NotebookPage.SERVERS)
|
||||
@@ -312,20 +303,16 @@ class PreConnectionAssistant(Gtk.Box):
|
||||
self._hide_mod_area()
|
||||
else:
|
||||
self._show_mod_area()
|
||||
msg = "All mods are up to date."
|
||||
msg = preconnect.up_to_date
|
||||
self.mod_count.set_text(msg)
|
||||
|
||||
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)
|
||||
|
||||
if prereqs.required_space != 0:
|
||||
pretty = number(prereqs.required_space)
|
||||
suffix = f" Need to download {pretty} MiB of mod updates."
|
||||
prefix = preconnect.total_mods
|
||||
self.mod_count.set_text(f"{prefix}{str(total_mods)}.{suffix}")
|
||||
else:
|
||||
self.ok.set_label(preconnect.connect)
|
||||
|
||||
if prereqs.is_last_server:
|
||||
self.connect_last.show()
|
||||
|
||||
@@ -16,7 +16,7 @@ logger = logging.getLogger(APP_NAME)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.controllers.mc import Controller
|
||||
from dzgui.controllers.mc import Emitter
|
||||
from dzgui.controllers.emitter import Emitter
|
||||
|
||||
|
||||
class ScrollableTree(Gtk.ScrolledWindow):
|
||||
|
||||
@@ -48,6 +48,10 @@ class ServerModTreeView(ContextMixin, TreeView): # type: ignore
|
||||
case _:
|
||||
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
|
||||
def _on_row_activated(
|
||||
self,
|
||||
|
||||
+2
-1
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
|
||||
authors = [
|
||||
{name = "aclist"}
|
||||
]
|
||||
version = "7.0.0b11"
|
||||
version = "7.0.0b15"
|
||||
license = "GPL-3.0-or-later"
|
||||
license-files = ["LICENSE"]
|
||||
readme = "README.md"
|
||||
@@ -107,6 +107,7 @@ config-settings-package = { pygobject-stubs = { config = "Gtk3,Gdk3,Soup2" } }
|
||||
[tool.pytest.ini_options]
|
||||
markers = [
|
||||
"config: config file keys/values",
|
||||
"mods: tests mod metadata/link creation",
|
||||
"pefile: validate PE files",
|
||||
"post_install: requires a completed installation",
|
||||
"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.
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
from dzgui.api.mods import tokenize
|
||||
|
||||
print(tokenize("meta4.cpp"))
|
||||
Vendored
+8
@@ -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.
Vendored
+42
@@ -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=""
|
||||
@@ -0,0 +1,87 @@
|
||||
import pytest
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
from dzgui.app_init import copy_bare_configs
|
||||
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
CONF_STRING = "DZGUI_CONF\n"
|
||||
STATE_STRING = "DZGUI_STATE\n"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def state_files():
|
||||
state = [
|
||||
"dzg.history",
|
||||
"dzg.versions",
|
||||
"dzg.res.json",
|
||||
"dzg.filters.json",
|
||||
"dzg.columns.json",
|
||||
"dzg.notes.json",
|
||||
"ips.csv",
|
||||
".month",
|
||||
]
|
||||
return state
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def xdg_paths():
|
||||
paths = []
|
||||
routes = {
|
||||
"XDG_CONFIG_HOME": "",
|
||||
"XDG_STATE_HOME": "",
|
||||
"XDG_DATA_HOME": "",
|
||||
"XDG_CACHE_HOME": "",
|
||||
}
|
||||
for route in routes:
|
||||
tmp = tempfile.TemporaryDirectory(delete=False)
|
||||
routes[route] = tmp.name
|
||||
for k, v in routes.items():
|
||||
os.environ[k] = v
|
||||
env = get_xdg_paths()
|
||||
return parse_filepaths(env)
|
||||
|
||||
|
||||
@pytest.mark.mods
|
||||
def test_config_file_import(xdg_paths, state_files):
|
||||
|
||||
# NOTE: write bare files in root
|
||||
tmp_conf = xdg_paths.config.parent.parent
|
||||
tmp_conf_file = tmp_conf / xdg_paths.config.name
|
||||
tmp_conf_file.write_text(CONF_STRING)
|
||||
|
||||
tmp_state = xdg_paths.resolution.parent.parent
|
||||
for file in state_files:
|
||||
tmp_state.joinpath(file).write_text(STATE_STRING)
|
||||
# NOTE: function should move files into "dzgui" subdirectory
|
||||
config_changed, state_changed = copy_bare_configs(
|
||||
xdg_paths.config, xdg_paths.resolution
|
||||
)
|
||||
|
||||
assert xdg_paths.config.read_text() == CONF_STRING
|
||||
for file in state_files:
|
||||
expected = xdg_paths.resolution.parent.joinpath(file)
|
||||
assert expected.read_text() == STATE_STRING
|
||||
|
||||
assert config_changed is True
|
||||
assert state_changed is True
|
||||
|
||||
|
||||
@pytest.mark.mods
|
||||
def test_config_file_no_import(xdg_paths, state_files):
|
||||
for subdir in xdg_paths.config.parent, xdg_paths.resolution.parent:
|
||||
subdir.mkdir()
|
||||
|
||||
tmp_conf_file = xdg_paths.config
|
||||
tmp_conf_file.write_text(CONF_STRING)
|
||||
|
||||
for file in state_files:
|
||||
xdg_paths.resolution.parent.joinpath(file).write_text(STATE_STRING)
|
||||
|
||||
config_changed, state_changed = copy_bare_configs(
|
||||
xdg_paths.config, xdg_paths.resolution
|
||||
)
|
||||
assert config_changed is False
|
||||
assert state_changed is False
|
||||
+15
-3
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import pytest
|
||||
|
||||
from dzgui.const.boilerplate import config_boilerplate
|
||||
from dzgui.config.query import get_config
|
||||
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
|
||||
from dzgui.config import convert
|
||||
@@ -11,7 +12,11 @@ pytestmark = pytest.mark.config
|
||||
|
||||
@pytest.fixture
|
||||
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
|
||||
@@ -60,8 +65,8 @@ def test_contains_invalid_values(keys, config):
|
||||
@pytest.mark.parametrize(
|
||||
"fixture, expect",
|
||||
[
|
||||
("dztuirc_one", (False, True, True)),
|
||||
("dztuirc_two", (False, False, False)),
|
||||
("dztuirc_1", (False, True, True)),
|
||||
("dztuirc_2", (False, False, False)),
|
||||
],
|
||||
)
|
||||
def test_bool_conversion(fixture, expect):
|
||||
@@ -86,5 +91,12 @@ def test_key_conversion(legacy_config):
|
||||
for key in keys:
|
||||
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
|
||||
@@ -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
@@ -1,17 +1,13 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
from dzgui.api.mods import tokenize
|
||||
from tests.fixtures import fixture_path
|
||||
|
||||
@pytest.mark.mods
|
||||
@pytest.mark.parametrize("fixture", [
|
||||
fixture_path("cpp/meta1.cpp"),
|
||||
fixture_path("cpp/meta2.cpp"),
|
||||
fixture_path("cpp/meta3.cpp"),
|
||||
fixture_path("cpp/meta4.cpp"),
|
||||
fixture_path("cpp/meta5.cpp"),
|
||||
fixture_path("cpp/meta6.cpp"),
|
||||
]
|
||||
)
|
||||
def test_modmeta(fixture: str) -> None:
|
||||
meta = tokenize(fixture)
|
||||
@pytest.mark.parametrize("i", range(1, 7))
|
||||
def test_modmeta(i: int) -> None:
|
||||
fixture = fixture_path(f"cpp/mod{i}")
|
||||
path = Path(fixture)
|
||||
meta = tokenize(path)
|
||||
assert meta["name"] == "ModName"
|
||||
Reference in new issue
Block a user