From a4ac357c8fe80cf78c97857319b02477db879a55 Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:44:30 +0900 Subject: [PATCH] feat: validate atomic mods --- dzgui/api/steam.py | 13 ++++++++++--- dzgui/const/endpoints.py | 1 + dzgui/const/enum.py | 7 ++++++- dzgui/controllers/mc.py | 10 ++++++++++ dzgui/managers/contextmenu.py | 8 ++++++-- dzgui/managers/mods.py | 28 ++++++++++++++++++++++++++-- dzgui/strings/dialogs.py | 1 + dzgui/util/strings.py | 5 +++++ dzgui/views/components/mod_panel.py | 3 +++ 9 files changed, 68 insertions(+), 8 deletions(-) diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index be691b4..8c67bcb 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -12,6 +12,7 @@ from typing import Any, Generator, Union from warnings import deprecated from dzgui.api.acf import ACF +from dzgui.const.endpoints import VALIDATE from dzgui.init.prereqs import has_any_client from dzgui.init.prereqs import has_flatpak_client, has_steam_client from dzgui.const.constants import ( @@ -316,10 +317,9 @@ def unsubscribe(key: str, mod: int) -> None: update_workshop(key, mod, UNSUB_ENDPOINT) -@deprecated("Use subscribe()") -def enqueue_mod(client: str, mod: str, appid: int) -> None: +def validate_mod(client: str, mod: str) -> None: client_args = concat_bash_args(client) - subprocess.Popen([*client_args, "+workshop_download_item", str(appid), mod]) + subprocess.Popen([*client_args, "+workshop_download_item", str(APPID_DAYZ), mod]) @deprecated("Cf. https://github.com/ValveSoftware/steam-for-linux/issues/9672") @@ -500,3 +500,10 @@ def get_app_name(appid: int) -> str | None: except Exception as e: logger.debug(e) return None + + +@deprecated("Currently unused") +def validate_dayz(client: str) -> None: + client_args = concat_bash_args(client) + uri = VALIDATE + str(APPID_DAYZ) + subprocess.Popen([*client_args, uri]) diff --git a/dzgui/const/endpoints.py b/dzgui/const/endpoints.py index 5fc007b..7616ff0 100644 --- a/dzgui/const/endpoints.py +++ b/dzgui/const/endpoints.py @@ -18,6 +18,7 @@ COORDS_API = "http://ip-api.com/json" # steam URIs WORKSHOP = "steam://url/CommunityFilePage/" OPEN_URL = "steam://openurl/" +VALIDATE = "steam://validate/" # user facing GITHUB_PAGES = "https://aclist.github.io/dzgui/index" diff --git a/dzgui/const/enum.py b/dzgui/const/enum.py index 05a9763..b1728f9 100644 --- a/dzgui/const/enum.py +++ b/dzgui/const/enum.py @@ -157,6 +157,7 @@ class ContextMenu(EnumWithAttrs): COPY_SERVER_IP = {"label": strings.copy_ip} COPY_SERVER_NAME = {"label": strings.copy_name} UNSUB_MOD = {"label": strings.unsub_mod} + VALIDATE_MOD = {"label": strings.validate_mod} OPEN_WORKSHOP = {"label": strings.open_workshop} REFRESH_PLAYERS = {"label": strings.refresh_players} REMOVE_HISTORY = {"label": strings.remove_history} @@ -172,7 +173,7 @@ class ContextMenuGroup(Enum): """ SERVER_MOD = (ContextMenu.OPEN_WORKSHOP,) - MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.UNSUB_MOD) + MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.UNSUB_MOD, ContextMenu.VALIDATE_MOD) MOD_OFFLINE = (None,) LOG = (ContextMenu.COPY_LOG_CLIPBOARD,) SERVER_BROWSER = ( @@ -246,6 +247,10 @@ class ModButton(EnumWithAttrs): "label": strings.mod_panel.select_stale, "tooltip": strings.mod_panel.select_stale_tooltip, } + VALIDATE = { + "label": strings.mod_panel.validate, + "tooltip": strings.mod_panel.validate_tooltip, + } class ButtonType(EnumWithAttrs): diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index c7f3f02..1a05955 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -102,6 +102,16 @@ class Controller(GObject.GObject): except AttributeError: logger.critical(f"{attr} is not a valid AppNavigation attribute.") + def validate_mods( + self, treeview: Union["ModTreeView", "OfflineModTreeView", None] = None + ) -> None: + if treeview is None: + view = self.mediator.modtreeview + else: + view = treeview + mod_man = view.get_mod_man() + mod_man.validate_mods() + def get_prefs(self) -> UserPrefs: return self.prefs diff --git a/dzgui/managers/contextmenu.py b/dzgui/managers/contextmenu.py index 8b362eb..016ce7b 100644 --- a/dzgui/managers/contextmenu.py +++ b/dzgui/managers/contextmenu.py @@ -75,10 +75,14 @@ class ContextMenuManager: if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)): match action: - case ContextMenu.UNSUB_MOD: - self.controller.unsub_mods(self.treeview) case ContextMenu.OPEN_WORKSHOP: self.open_mod_page() + case ContextMenu.UNSUB_MOD: + self.controller.unsub_mods(self.treeview) + case ContextMenu.VALIDATE_MOD: + self.controller.validate_mods(self.treeview) + case _: + pass if isinstance(self.treeview, LogTreeView): match action: diff --git a/dzgui/managers/mods.py b/dzgui/managers/mods.py index f55781a..6a98e02 100644 --- a/dzgui/managers/mods.py +++ b/dzgui/managers/mods.py @@ -4,7 +4,7 @@ import time from pathlib import Path from typing import TYPE_CHECKING -from dzgui.api.steam import unsubscribe +from dzgui.api.steam import unsubscribe, validate_mod from dzgui.api.mods import ( get_delimited_mods, find_stale_mods, @@ -85,7 +85,7 @@ class ModManager: total_mods = len(self.store) self.emitter.emit("mods_updated", msg, total_mods) - def unsub_mods(self) -> None: + def get_selected_mods(self) -> list[tuple[str, Gtk.TreeIter]]: sel = self.treeview.get_selection() model, pathlist = sel.get_selected_rows() # NOTE: reverse when multiple selection @@ -96,6 +96,15 @@ class ModManager: continue mod, _iter = res mods.append((mod, _iter)) + return mods + + def validate_mods(self) -> None: + mods = self.get_selected_mods() + self.thread_man.set_job_count(len(mods)) + self.validate_all_mods(mods) + + def unsub_mods(self) -> None: + mods = self.get_selected_mods() self.thread_man.set_job_count(len(mods)) self.unsub_all_mods(mods) @@ -109,6 +118,21 @@ class ModManager: mod = model.get_value(tree_iter, 2) return mod, tree_iter + def validate_atomic_mod(self, mod: str) -> None: + # NOTE: propagation to dayz exp will happen during + # subsequent symlink phase on connect + client = self.controller.query_config(Preferences.CLIENT) + validate_mod(client, mod) + + @call_on_thread(dialogs.validating_mods) + def validate_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None: + for mod, _iter in mods: + self.validate_atomic_mod(mod) + self.thread_man.increment_dialog() + time.sleep(API_RATE_LIMIT) + func = StoredFunc(lambda: None) + self.thread_man.set_cleanup_func(func) + @call_on_thread(dialogs.deleting_mods) def unsub_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None: for mod, _iter in mods: diff --git a/dzgui/strings/dialogs.py b/dzgui/strings/dialogs.py index 64fba5f..b895ba0 100644 --- a/dzgui/strings/dialogs.py +++ b/dzgui/strings/dialogs.py @@ -9,6 +9,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 = "Unsubscribing mods" +validating_mods = "Validating mods" scanning_mods = "Scanning mods" parsing_mods = "Parsing mods" diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index 91c41ea..931ca88 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -31,6 +31,7 @@ show_details = "Show server details" refresh_players = "Refresh player count" open_workshop = "Open in Steam Workshop" unsub_mod = "Unsubscribe mod" +validate_mod = "Validate mod" copy_name = "Copy name to clipboard" copy_ip = "Copy IP to clipboard" copy_log = "Copy record(s) to clipboard" @@ -200,6 +201,8 @@ class ModPanelStrings: clear_highlights: str select_stale: str select_stale_tooltip: str + validate: str + validate_tooltip: str @dataclass(slots=True, frozen=True) @@ -319,6 +322,8 @@ mod_panel = ModPanelStrings( clear_highlights="Clears highlights and reverts the table to a default state", select_stale="Select stale", select_stale_tooltip="Only selects highlighted stale mods", + validate="Validate selected", + validate_tooltip="Validates selected mods and updates them if necessary", ) diff --git a/dzgui/views/components/mod_panel.py b/dzgui/views/components/mod_panel.py index 047fd2d..7c913c4 100644 --- a/dzgui/views/components/mod_panel.py +++ b/dzgui/views/components/mod_panel.py @@ -50,6 +50,7 @@ class ModSelectionPanel(Gtk.Box): ModButton.SELECT_ALL, ModButton.UNSELECT_ALL, ModButton.UNSUB_SELECTED, + ModButton.VALIDATE, ) for button in buttons: b = EnumeratedModButton(button) @@ -114,3 +115,5 @@ class ModSelectionPanel(Gtk.Box): self.swap_sensitive(False) case ModButton.SELECT_STALE: self.controller.select_colorized() + case ModButton.VALIDATE: + self.controller.validate_mods()