Compare commits

...

15 Commits

Author SHA1 Message Date
aclist
d305f47bb9
Merge branch 'dzgui7' into feat/launch-offline
Some checks failed
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
20dab38fff
Merge pull request #351 from aclist/fix/enqueue-mods
Some checks failed
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
12 changed files with 82 additions and 34 deletions

View File

@ -12,6 +12,7 @@ from dzgui.api.servers import get_rules, fqip_to_record
from dzgui.const.constants import (
APP_NAME,
APPID_DAYZ,
DAYZ_COMMUNITY_ROOT,
LIBRARYFOLDERS_PATH,
WORKSHOP_PATH,
)
@ -50,6 +51,9 @@ def get_local_mods(workshop_path: Path) -> list[Path]:
def is_mission(path: Path) -> bool:
# TODO: parse integrity of other files
parent = path.parent.name
if parent != DAYZ_COMMUNITY_ROOT:
return False
file = path / "init.c"
return file.exists()
@ -81,6 +85,7 @@ def get_mod_size(path: Path) -> float:
def get_custom_mods(path: Path) -> list[Any]:
# TODO: rename this function for clarity ("get local vs. get custom")
mods = get_local_mods(path)
# TODO: error handling
return parse_mods(mods, use_custom=True)

View File

@ -73,14 +73,9 @@ def get_local_signatures(version_file: Path) -> dict[str, int]:
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 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(

View File

@ -83,3 +83,5 @@ LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG")
TMP_PATH = "/tmp"
TMP_TARBALL = "/tmp/dzgui.tar.gz"
TMP_EXE = "/tmp/dzgui/dzgui"
DAYZ_COMMUNITY_ROOT = "Missions"

View File

@ -88,6 +88,7 @@ class ConnectionManager:
self.record: Record
self.workshop: Path
self.client: str
self.remote_mod_ids: list[str] = []
self.missing_mods: list[tuple[str, str, int, int]] = []
@ -171,6 +172,7 @@ class ConnectionManager:
client = self.controller.query_config(Preferences.CLIENT)
running = is_steam_running(client)
steam_proc = SteamProcess(client_name, running)
self.client = client
game_mode = prefs.is_game_mode
@ -263,11 +265,10 @@ class ConnectionManager:
def _connect_steam(self, menu_only: bool) -> None:
addr = f"{self.record.ip}:{self.record.gameport}"
playername = self.controller.query_config(Preferences.NAME)
client = self.controller.query_config(Preferences.CLIENT)
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:
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:
# TODO: log/pop the error
func = StoredFunc(self.controller.update_status)
@ -299,7 +300,8 @@ class ConnectionManager:
for title, mod, stamp, size in self.missing_mods:
if self.controller.is_cancel_pending():
return
enqueue_mod(mod, self.appid)
enqueue_mod(self.client, mod, self.appid)
# NOTE: prevents rate limiting
time.sleep(3)
if raise_window is True:

View File

@ -66,6 +66,9 @@ class ModManager:
func = StoredFunc(self._on_mods_loaded)
self.thread_man.set_cleanup_func(func)
def set_store(self, store: "FastInsertListStore") -> None:
self.store = store
def _on_mods_loaded(self) -> None:
self.treeview.set_model(self.store)
if self.store is None:

View File

@ -95,6 +95,7 @@ class OfflineManager:
)
combined_mods.extend(new_symlinks)
relative_link = ""
if len(mission) > 0:
relative_link = symlink_mission(Path(steam_path), mission)

View File

@ -10,6 +10,7 @@ custom_button = "Set custom mod folder"
local_frame = "Installed mods"
no_local_mods = "No local mods found"
no_mods = "No valid mods found"
no_mission = "Not a valid mission"

View File

@ -2,7 +2,7 @@ import logging
from pathlib import Path
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.config.query import lookup
@ -11,18 +11,23 @@ import dzgui.api.pefile as PeFile
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:
# TODO: pass direct path as argument
path = lookup(config, Preferences.DEFAULT)
steam_path = Path(path)
dayz_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
for file in dayz_path.iterdir():
# NOTE: unlink stale symlinks
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()
expunge_link(file)
workshop = get_local_mod_path(steam_path)
# NOTE: create symlinks for missing mods
for mod_id in get_local_mod_ids(steam_path):
@ -49,16 +54,20 @@ def create_custom_symlinks(
clone_symlinks(steam_path)
return links
def symlink_mission(steam_path: Path, target: str) -> None:
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():
if file.is_symlink() and str(file.stem)[:2] == "@C":
file.unlink()
stem = Path(target).name
suffix = _hash(stem, use_custom=True)
source = dayz_path.joinpath(suffix)
source.symlink_to(Path(target))
return suffix
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:

View File

@ -25,6 +25,8 @@ class Statusbar(Gtk.Grid):
self.controller.register_widget("statusbar", self)
self.emitter = controller.get_emitter()
self.prior_enum: NotebookPage
self.prior_status: str
self.playercount = ""
self.statusbar = Gtk.Statusbar()
@ -71,6 +73,7 @@ class Statusbar(Gtk.Grid):
NotebookPage.CONNECTION,
NotebookPage.OFFLINE,
):
self.prior_enum = enum
self.set_by_context(enum, esc_to_return)
return
@ -80,8 +83,9 @@ class Statusbar(Gtk.Grid):
case NotebookPage.KEYS:
bar = question_to_return
case _:
return
bar = self.prior_status
self.prior_enum = enum
self.set_by_context(enum, bar)
def _on_server_row_changed(self, statusbar: Self) -> None:
@ -97,6 +101,10 @@ class Statusbar(Gtk.Grid):
context: Union["ServerTab", NotebookPage],
) -> None:
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
if dist is None:
self.set_by_context(context, "")
@ -122,6 +130,8 @@ class Statusbar(Gtk.Grid):
def set_by_context(
self, context: Union[NotebookPage, "ServerTab"], string: str
) -> None:
if context != NotebookPage.KEYS:
self.prior_status = string
meta = self.statusbar.get_context_id(str(context))
self.statusbar.push(meta, string)
self.set_cache(string)

View File

@ -11,6 +11,7 @@ from dzgui.const.constants import (
EDIT_DELETE,
ERROR,
FOLDER,
WARNING,
)
from dzgui.const.enum import ContextMenuGroup, NotebookPage
from dzgui.managers.offline import OfflineManager
@ -173,6 +174,7 @@ class FolderHBox(HBox):
self.pop.set_label(error, msg)
self.pop.popup()
class ModFrame(HeadingFrame):
def __init__(
self, parent: OfflineLoader, controller: "Controller", label: str
@ -198,20 +200,39 @@ class ModFrame(HeadingFrame):
self.tree_vbox = VBox()
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.frame.add(self.vbox)
sel = self.tree.get_selection()
sel.connect("changed", self._on_selection_changed)
self.connect("map", self._on_map)
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:
self.tree.set_model(None)
self.tree_vbox.hide()
self.no_mods.hide()
def start(self, store: "FastInsertListStore") -> None:
# TODO: this manipulates mod man store out of band
self.tree.set_model(store)
self.tree.mod_man.set_store(store)
self.show_tree()
def get_mods(self) -> list[str]:
@ -219,6 +240,7 @@ class ModFrame(HeadingFrame):
if model is None:
return []
if type(self) is CustomModFrame:
# NOTE: custom mod dirs to be hashed
return [model[_iter][2] for _iter in treeiters]
else:
# NOTE: pre-existing, canonical symlinks to published mods
@ -294,6 +316,7 @@ class CustomModFrame(ModFrame):
self.custom_hbox.hide_label()
self.tree.set_model(None)
self.tree_vbox.hide()
self.no_mods.hide()
def present_error(self, folder: str) -> None:
self.hide_tree()
@ -459,12 +482,12 @@ class OfflineLoader(Gtk.Box):
self.ok.set_sensitive(True)
def _on_keypress(self, widget: Self, event: Gdk.EventKey) -> None:
# FIXME: widget is not always in focus
if event.keyval == Gdk.KEY_Escape:
self.back.emit("clicked")
def populate(self, store: Union["FastInsertListStore", None]) -> None:
if store is None:
self.local_frame.start_empty()
return
self.local_frame.start(store)

View File

@ -333,10 +333,7 @@ class PreConnectionAssistant(Gtk.Box):
self.connect_last.hide()
self._process_warnings(prereqs)
if self.tree.is_visible():
self.tree.grab_focus()
else:
self.grab_focus()
self.tree.grab_focus()
def mark_finished(self) -> None:
self.mod_count.set_label(preconnect.all_updated)

View File

@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
authors = [
{name = "aclist"}
]
version = "7.0.0b12"
version = "7.0.0b11"
license = "GPL-3.0-or-later"
license-files = ["LICENSE"]
readme = "README.md"