From 831bd9145db0a3fcc0c53f6307b6de34721d7dca Mon Sep 17 00:00:00 2001 From: aclist <92275929+aclist@users.noreply.github.com> Date: Sun, 7 Jun 2026 20:17:58 +0900 Subject: [PATCH] feat: launch offline (WIP)) --- dzgui/api/steam.py | 18 +++++++++++++++++- dzgui/const/enum.py | 1 + dzgui/controllers/mc.py | 12 +++++++++++- dzgui/managers/connection.py | 2 +- dzgui/util/strings.py | 2 ++ dzgui/views/base.py | 5 +++++ dzgui/views/components/statusbar.py | 2 +- dzgui/views/dialogs/filepicker.py | 25 +++++++++++++++++++++++++ dzgui/views/pages/mods.py | 21 +++++++++++++++++++-- 9 files changed, 82 insertions(+), 6 deletions(-) diff --git a/dzgui/api/steam.py b/dzgui/api/steam.py index 0fa126d..b322f76 100644 --- a/dzgui/api/steam.py +++ b/dzgui/api/steam.py @@ -146,7 +146,7 @@ def connect(client: str, addr: str, appid: int, name: str, mods: list[str]) -> i return proc.returncode -def load_to_menu(client: str, addr: str, appid: int, name: str, mods: list[str]) -> int: +def load_to_menu(client: str, appid: int, name: str, mods: list[str]) -> int: """Loads to the menu screen with the selected mods; used for AFK/pre-joining""" concat = concat_mods(mods) client_args = concat_bash_args(client) @@ -162,6 +162,22 @@ def load_to_menu(client: str, addr: str, appid: int, name: str, mods: list[str]) proc = subprocess.run([*client_args, *params]) return proc.returncode +def launch_offline(client: str, addr: str, appid: int, name: str, mods: list[str], mission: str) -> int: + """Launch offline with specific mods/missions""" + concat = concat_mods(mods) + client_args = concat_bash_args(client) + params = [ + "-applaunch", + str(appid), + "-nolauncher", + "-nosplash", + "-skipintro", + f"-name={name}", + f"-mod={concat}", + f"-mission={mission}" + ] + proc = subprocess.run([*client_args, *params]) + return proc.returncode def find_user_id(path: Path) -> str | None: resolved_path = path / "config" / "loginusers.vdf" diff --git a/dzgui/const/enum.py b/dzgui/const/enum.py index aad7698..c14f6fe 100644 --- a/dzgui/const/enum.py +++ b/dzgui/const/enum.py @@ -103,6 +103,7 @@ class Preferences(EnumWithAttrs): class NotebookPage(EnumWithAttrs): + OFFLINE = {"crumbs": strings.crumbs.offline, "statusbar": True} CHANGELOG = {"crumbs": strings.crumbs.changelog, "statusbar": True} DEVELOPERS = {"crumbs": strings.crumbs.developers, "statusbar": False} HELP = {"crumbs": strings.crumbs._help, "statusbar": True} diff --git a/dzgui/controllers/mc.py b/dzgui/controllers/mc.py index e0c57c0..074e842 100644 --- a/dzgui/controllers/mc.py +++ b/dzgui/controllers/mc.py @@ -26,7 +26,7 @@ from dzgui.model.servers import ServerModelManager from dzgui.util.diag import write_diagnostic from dzgui.util.format import 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.filepicker import FilePicker, FolderPicker from dzgui.views.dialogs.generic import ExceptionDialog import gi @@ -47,6 +47,7 @@ if TYPE_CHECKING: from dzgui.views.components.filter_panel import FilterPanel from dzgui.views.components.right_panel import RightPanel from dzgui.views.components.statusbar import Statusbar + from dzgui.views.pages.offline import OfflineLoader from dzgui.views.pages.options import Options from dzgui.views.pages.preconnect import PreConnectionAssistant from dzgui.views.pages.servers import ServerNotebook @@ -72,6 +73,7 @@ class AppNavigation: filters: "FilterPanel" preconnect: "PreConnectionAssistant" options: "Options" + offline_loader: "OfflineLoader" class Controller(GObject.GObject): @@ -288,6 +290,10 @@ class Controller(GObject.GObject): dialog = ExceptionDialog(self, str(e)) dialog.run() + def set_custom_folder(self) -> None: + picker = FolderPicker(self.mediator.window) + return picker.pick_folder() + def update_api_key(self, key: Preferences, text: str) -> None: self.config_man.update_api_key(key, text) @@ -533,3 +539,7 @@ class Controller(GObject.GObject): self.clear_cancel_event() return True return False + + def open_offline(self, mods) -> None: + self.open_page(NotebookPage.OFFLINE) + self.mediator.offline_loader.populate(mods) diff --git a/dzgui/managers/connection.py b/dzgui/managers/connection.py index 7570dfd..035ba46 100644 --- a/dzgui/managers/connection.py +++ b/dzgui/managers/connection.py @@ -267,7 +267,7 @@ class ConnectionManager: if menu_only: rc = load_to_menu(client, addr, self.appid, playername, self.remote_mod_ids) else: - rc = connect(client, addr, self.appid, playername, self.remote_mod_ids) + rc = connect(client, self.appid, playername, self.remote_mod_ids) if rc != 0: # TODO: log/pop the error func = StoredFunc(self.controller.update_status) diff --git a/dzgui/util/strings.py b/dzgui/util/strings.py index dad40fa..cbff3d7 100644 --- a/dzgui/util/strings.py +++ b/dzgui/util/strings.py @@ -273,6 +273,7 @@ class Crumbs: thanks: str developers: str default: str + offline: str @dataclass(slots=True, frozen=True) @@ -462,6 +463,7 @@ crumbs = Crumbs( thanks="Help > Special thanks", developers="Options > Developers", default="Servers > ", + offline="Mods > Play offline" ) no_mods = "No local mods found." diff --git a/dzgui/views/base.py b/dzgui/views/base.py index f9b7a00..c540d13 100644 --- a/dzgui/views/base.py +++ b/dzgui/views/base.py @@ -110,8 +110,13 @@ class Notebook(ScrollableMixin, Gtk.Notebook): # type: ignore self.connection = PreConnectionAssistant(MainController) + from dzgui.views.pages.offline import OfflineLoader + + self.offline = OfflineLoader(MainController) + self.pages = { self.help: NotebookPage.HELP, + self.offline: NotebookPage.OFFLINE, self.clog: NotebookPage.CHANGELOG, self.keys: NotebookPage.KEYS, self.settings: NotebookPage.OPTIONS, diff --git a/dzgui/views/components/statusbar.py b/dzgui/views/components/statusbar.py index a6c1519..f7bc8fa 100644 --- a/dzgui/views/components/statusbar.py +++ b/dzgui/views/components/statusbar.py @@ -1,5 +1,4 @@ from typing import Self, Union, TYPE_CHECKING -from warnings import deprecated from dzgui.const.enum import NotebookPage, ServerTab from dzgui.util.strings import esc_to_return, question_to_return @@ -70,6 +69,7 @@ class Statusbar(Gtk.Grid): NotebookPage.CHANGELOG, NotebookPage.LOG, NotebookPage.CONNECTION, + NotebookPage.OFFLINE, ): self.set_by_context(enum, esc_to_return) return diff --git a/dzgui/views/dialogs/filepicker.py b/dzgui/views/dialogs/filepicker.py index 0691bc8..d8d8316 100644 --- a/dzgui/views/dialogs/filepicker.py +++ b/dzgui/views/dialogs/filepicker.py @@ -33,3 +33,28 @@ class FilePicker(Gtk.FileChooserDialog): self.destroy() return Path(file) return None + + +class FolderPicker(Gtk.FileChooserDialog): + def __init__(self, parent: Gtk.Window) -> None: + super().__init__( + title=picker.title, + action=Gtk.FileChooserAction.SELECT_FOLDER, + parent=parent, + resizable=True, + ) + self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL) + self.add_buttons("_OK", Gtk.ResponseType.OK) + self.set_default_response(Gtk.ResponseType.OK) + + def pick_folder(self) -> Path | None: + res = self.run() + if res in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT): + self.destroy() + return None + if res == Gtk.ResponseType.OK: + folder = self.get_current_folder() + if folder is not None: + self.destroy() + return Path(folder) + return None diff --git a/dzgui/views/pages/mods.py b/dzgui/views/pages/mods.py index f87d993..5d7c575 100644 --- a/dzgui/views/pages/mods.py +++ b/dzgui/views/pages/mods.py @@ -11,21 +11,38 @@ if TYPE_CHECKING: from dzgui.controllers.mc import Controller - class Mods(NoOverlayScrolledWindow): def __init__(self, controller: "Controller") -> None: super().__init__() + self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.controller = controller self.emitter = controller.get_emitter() self.tree = ModTreeView(controller) + self.tree.set_vexpand(True) - self.add(self.tree) + self.offline_button = Gtk.Button( + label="Play offline", + halign=Gtk.Align.START, + margin_top=10, + margin_bottom=10, + ) + self.offline_button.connect("clicked", self._on_offline_clicked) + + self.box.add(self.offline_button) + self.box.add(self.tree) + + self.add(self.box) self.controller.register_widget("modtreeview", self.tree) self.connect("map", self._on_map) self.connect("unmap", self._on_unmap) + def _on_offline_clicked(self, button: Gtk.Button) -> None: + self.controller.open_offline(self.tree.get_model()) + # TODO: add offline page + def _on_unmap(self, widget: Self) -> None: self.emitter.emit("mod_page_toggled", False)