mirror of
https://github.com/aclist/dztui.git
synced 2026-08-26 17:57:06 +02:00
feat: mod enqueue logic (WIP)
This commit is contained in:
parent
0e6caed64d
commit
fbbf0d9e66
@ -160,3 +160,13 @@ def find_stale_mods(config: Path) -> list[int]:
|
||||
unique_mods = set(remote_mods)
|
||||
stale = [mod for mod in local if mod not in unique_mods]
|
||||
return stale
|
||||
|
||||
|
||||
def get_mod_dir_size(path: Path) -> int:
|
||||
size = 0
|
||||
for i in Path(path).iterdir():
|
||||
if i.is_file():
|
||||
size += i.stat().st_size
|
||||
elif i.is_dir():
|
||||
size += get_mod_dir_size(i)
|
||||
return size
|
||||
|
||||
@ -8,50 +8,12 @@ from pathlib import Path
|
||||
|
||||
from dzgui.const.constants import APP_NAME
|
||||
from dzgui.const.endpoints import STEAM_PUBLISHED_FILES
|
||||
from dzgui.util.bash import concat_bash_args
|
||||
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
|
||||
def query_defunct() -> None:
|
||||
pass
|
||||
|
||||
|
||||
# TODO: unimplemented
|
||||
# cf.
|
||||
#!/usr/bin/env bash
|
||||
# query_defunct(){
|
||||
# readarray -t modlist <<< "$@"
|
||||
# local max=${#modlist[@]}
|
||||
# concat(){
|
||||
# for ((i=0;i<$max;i++)); do
|
||||
# echo "publishedfileids[$i]=${modlist[$i]}&"
|
||||
# done | awk '{print}' ORS=''
|
||||
# }
|
||||
# payload(){
|
||||
# echo -e "itemcount=${max}&$(concat)"
|
||||
# }
|
||||
# post(){
|
||||
# curl -s \
|
||||
# -X POST \
|
||||
# -H "Content-Type:application/x-www-form-urlencoded"\
|
||||
# -d "$(payload)" 'https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/?format=json'
|
||||
# }
|
||||
# post | jq
|
||||
# return
|
||||
# local result=$(post | jq -r '
|
||||
# .[].publishedfiledetails[]
|
||||
# | select(.result==1)
|
||||
# | select(.filename|contains("screenshot")|not)
|
||||
# | "\(.file_size) \(.publishedfileid)"')
|
||||
# <<< "$result" awk '{print $2}'
|
||||
# }
|
||||
#
|
||||
# query_defunct "3576065083"
|
||||
|
||||
|
||||
def concat_mods(mods: list[int]) -> str:
|
||||
def concat_mods(mods: list[str]) -> str:
|
||||
for mod in mods:
|
||||
mods[mod] = f"@{mod}"
|
||||
return ";".join(mods)
|
||||
@ -68,22 +30,32 @@ 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 get_needs_update(
|
||||
version_file: Path, remote_hashes: list[tuple[str, int, str]]
|
||||
) -> list[tuple[str, int, str]]:
|
||||
version_file: Path, remote_hashes: list[tuple[str, str, int, int]]
|
||||
) -> list[tuple[str, str, int, int]]:
|
||||
local_hashes = get_local_signatures(version_file)
|
||||
needs_update: list[tuple[str, str]] = []
|
||||
for _id, _hash, size in remote_hashes:
|
||||
for title, _id, _hash, size in remote_hashes:
|
||||
if _id not in local_hashes:
|
||||
needs_update.append((_id, _hash, size))
|
||||
needs_update.append((title, _id, _hash, size))
|
||||
elif _hash != local_hashes[_id]:
|
||||
needs_update.append((_id, _hash, size))
|
||||
needs_update.append((title, _id, _hash, size))
|
||||
else:
|
||||
continue
|
||||
return needs_update
|
||||
|
||||
|
||||
def get_remote_signatures(mods: list[str]) -> list[tuple[str, int]]:
|
||||
def get_remote_signatures(mods: list[str]) -> list[tuple[str, str, int, int]]:
|
||||
"""
|
||||
Attempts to continue connecting even if signatures are empty
|
||||
"""
|
||||
@ -99,24 +71,26 @@ def get_remote_signatures(mods: list[str]) -> list[tuple[str, int]]:
|
||||
if r.status_code != 200:
|
||||
return []
|
||||
|
||||
hashes: list[tuple[str, int, str]] = []
|
||||
hashes: list[tuple[str, int, int]] = []
|
||||
j = r.json()
|
||||
rows = j["response"]["publishedfiledetails"]
|
||||
for row in rows:
|
||||
title = row["title"]
|
||||
_id = row["publishedfileid"]
|
||||
time = row["time_updated"]
|
||||
size = row["file_size"]
|
||||
hashes.append((_id, time, size))
|
||||
size = int(row["file_size"])
|
||||
hashes.append((title, _id, time, size))
|
||||
return hashes
|
||||
|
||||
|
||||
# TEST: set config to name=user, use official server and no mods,
|
||||
# ensure that formatted string is identical to fixture
|
||||
def connect(addr: str, appid: int, name: str, mods: list[int]) -> None:
|
||||
# TODO: get name from configs
|
||||
# TODO: concat_mods(mods):
|
||||
# @<mod>;@<mod>;
|
||||
# TODO:: set config to name=user, use official server and no mods,
|
||||
# ensure that formatted string is identical to fixture with same hash
|
||||
def connect(addr: str, appid: int, name: str, mods: list[str]) -> None:
|
||||
concat = concat_mods(mods)
|
||||
params = [
|
||||
"steam",
|
||||
"-applaunch",
|
||||
appid,
|
||||
f"-connect={addr}",
|
||||
"-nolauncher",
|
||||
"-nosplash",
|
||||
@ -124,10 +98,7 @@ def connect(addr: str, appid: int, name: str, mods: list[int]) -> None:
|
||||
f"-name={name}",
|
||||
f"-mod={concat}",
|
||||
]
|
||||
# TODO: get steam launch command from configs
|
||||
# args = concat_bash_args()
|
||||
#
|
||||
proc = subprocess.Popen([*args, "-applaunch", appid, *params])
|
||||
proc = subprocess.Popen([*params])
|
||||
# check proc.returncode
|
||||
|
||||
|
||||
|
||||
@ -112,7 +112,7 @@ class NotebookPage(EnumWithAttrs):
|
||||
OPTIONS = {"crumbs": strings.crumbs.options, "statusbar": False}
|
||||
SERVERS = {"crumbs": strings.crumbs.servers, "statusbar": True}
|
||||
THANKS = {"crumbs": strings.crumbs.thanks, "statusbar": True}
|
||||
CONNECTION = {"crumbs": "Connect", "statusbar": False}
|
||||
CONNECTION = {"crumbs": "Connect", "statusbar": True}
|
||||
|
||||
|
||||
class RowType(EnumWithAttrs):
|
||||
|
||||
@ -20,6 +20,7 @@ from dzgui.controllers.emitter import Emitter
|
||||
from dzgui.managers.config import ConfigManager
|
||||
from dzgui.managers.connection import ConnectionManager
|
||||
from dzgui.managers.contextmenu import ContextMenuManager
|
||||
from dzgui.managers.mods import ModManager
|
||||
from dzgui.managers.notes import NoteManager
|
||||
from dzgui.model.servers import ServerModelManager
|
||||
from dzgui.util.diag import write_diagnostic
|
||||
@ -215,8 +216,6 @@ class Controller(GObject.GObject):
|
||||
open_user_workshop(uid, client)
|
||||
|
||||
def load_mods(self) -> None:
|
||||
from dzgui.managers.mods import ModManager
|
||||
|
||||
self.mod_man = ModManager(self)
|
||||
|
||||
def uncolorize_mods(self) -> None:
|
||||
@ -473,3 +472,6 @@ class Controller(GObject.GObject):
|
||||
|
||||
def update_and_connect(self) -> None:
|
||||
self.connection_man.update_and_connect()
|
||||
|
||||
def update_status(self, mod: str, mark_finished: bool = False) -> None:
|
||||
self.mediator.preconnect.update_mod(mod, mark_finished)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import shutil
|
||||
import time
|
||||
|
||||
from dataclasses import dataclass
|
||||
from packaging.version import Version
|
||||
@ -8,9 +9,9 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import dzgui.api.pefile as PeFile
|
||||
import dzgui.api.servers as Servers
|
||||
from dzgui.api.steam import get_remote_signatures, get_needs_update
|
||||
from dzgui.api.steam import enqueue_mod, get_remote_signatures, get_needs_update
|
||||
|
||||
from dzgui.api.mods import get_local_mod_ids
|
||||
from dzgui.api.mods import get_mod_dir_size, get_local_mod_ids, get_local_mod_path
|
||||
from dzgui.const.constants import (
|
||||
APP_NAME,
|
||||
APPID_DAYZ,
|
||||
@ -29,7 +30,7 @@ from dzgui.views.dialogs.servers import ServerDetailsDialog, ServerModDialog
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
from gi.repository import Gtk, GLib # noqa E402
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.api.servers import A2SInfo, Record
|
||||
@ -62,6 +63,7 @@ class ConnectionManager:
|
||||
|
||||
self.appid: int
|
||||
self.record: Record
|
||||
self.workshop: Path
|
||||
|
||||
self.remote_mod_ids: list[str] = []
|
||||
self.missing_mods: list[str] = []
|
||||
@ -101,6 +103,7 @@ class ConnectionManager:
|
||||
free_mib = 0.0
|
||||
|
||||
steam_path = Path(self.controller.query_config(Preferences.DEFAULT))
|
||||
self.workshop = get_local_mod_path(steam_path)
|
||||
local_version = PeFile.get_pretty_version(steam_path, info.game_id)
|
||||
if local_version is None:
|
||||
local_version = "0.0.0"
|
||||
@ -124,13 +127,12 @@ class ConnectionManager:
|
||||
pefile_path = PeFile.get_pefile_path(steam_path, info.game_id)
|
||||
total, used, free = shutil.disk_usage(pefile_path)
|
||||
if len(self.missing_mods) > 0:
|
||||
required_size = sum(int(row[2]) for row in self.missing_mods)
|
||||
required_size = sum(row[2] for row in self.missing_mods)
|
||||
required_mib = format_mib(required_size)
|
||||
free_mib = format_mib(required_size)
|
||||
|
||||
dayz_running = is_dayz_running()
|
||||
steam_running = is_steam_running()
|
||||
# TODO: is dayz downloading
|
||||
|
||||
prereqs = Prerequisites(
|
||||
name=info.server_name,
|
||||
@ -207,17 +209,38 @@ class ConnectionManager:
|
||||
print(self.record.ip)
|
||||
print(self.record.gameport)
|
||||
print(self.appid)
|
||||
# TODO: convert mod ids to symlink hashes
|
||||
# TODO: add to history file and list store
|
||||
# steam api, concat mods
|
||||
|
||||
# TODO: custom threading with glib idle callback
|
||||
@call_on_thread("Waiting for Steam to update mods")
|
||||
def update_mods(self) -> None:
|
||||
print(self.missing_mods)
|
||||
# TODO: when downloading mods, create symlinks if missing
|
||||
# TODO: pack a final PreReq struct with pre-processed values
|
||||
# self.needs_update
|
||||
# then connect
|
||||
pass
|
||||
# NOTE: fast enqueue all mods in auto mode
|
||||
for title, mod, stamp, size in self.missing_mods:
|
||||
# TODO: check for cancel event
|
||||
enqueue_mod(mod, self.appid)
|
||||
time.sleep(2)
|
||||
|
||||
for title, mod, stamp, size in self.missing_mods:
|
||||
mod_path = self.workshop / mod
|
||||
|
||||
while mod_path.is_dir() is False:
|
||||
time.sleep(1)
|
||||
while True:
|
||||
# NOTE: mods will finish at the same time
|
||||
# TODO: check for cancel event
|
||||
cur_size = get_mod_dir_size(mod_path)
|
||||
if cur_size == size:
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
# TODO: update tree checkmarks
|
||||
func = StoredFunc(self.controller.update_status, "All mods updated.")
|
||||
self.thread_man.set_cleanup_func(func)
|
||||
# TODO: after downloading mods, create all symlinks (and clone)
|
||||
# GLib.idle_add(self._mark_finished)
|
||||
|
||||
# def _mark_finished(self) -> None:
|
||||
# self.controller.update_status("All mods updated.", mark_finished=True)
|
||||
|
||||
def update_and_connect(self) -> None:
|
||||
if len(self.missing_mods) > 0:
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import shlex
|
||||
|
||||
|
||||
def concat_bash_args(command: str) -> list[str]:
|
||||
return shlex.split(command)
|
||||
|
||||
@ -76,6 +76,7 @@ class Statusbar(Gtk.Grid):
|
||||
NotebookPage.THANKS,
|
||||
NotebookPage.CHANGELOG,
|
||||
NotebookPage.LOG,
|
||||
NotebookPage.CONNECTION,
|
||||
):
|
||||
self.set_by_context(enum, esc_to_return)
|
||||
return
|
||||
|
||||
@ -5,6 +5,7 @@ from dzgui.const.constants import (
|
||||
ERROR,
|
||||
WARNING,
|
||||
)
|
||||
from dzgui.const.enum import NotebookPage
|
||||
from dzgui.util.css import add_class
|
||||
from dzgui.util.localize import number
|
||||
from dzgui.strings import preconnect
|
||||
@ -99,20 +100,33 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
|
||||
|
||||
self.controller.register_widget("preconnect", self)
|
||||
|
||||
self.back = Gtk.Button(label=preconnect.back, halign=Gtk.Align.START)
|
||||
self.cancel = Gtk.Button(
|
||||
label=preconnect.cancel, halign=Gtk.Align.END, sensitive=False, hexpand=True
|
||||
self.back = Gtk.Button(
|
||||
label=preconnect.back, halign=Gtk.Align.END, hexpand=True
|
||||
)
|
||||
self.ok = Gtk.Button(label=preconnect.update_mods, halign=Gtk.Align.END)
|
||||
|
||||
# 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="This option is available if wmctrl or xdotool is installed on the system",
|
||||
)
|
||||
self.button_box = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL,
|
||||
orientation=Gtk.Orientation.VERTICAL,
|
||||
valign=Gtk.Align.END,
|
||||
vexpand=True,
|
||||
spacing=5,
|
||||
)
|
||||
for button in self.back, self.cancel, self.ok:
|
||||
self.button_box.add(button)
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
||||
for button in self.back, self.ok:
|
||||
box.add(button)
|
||||
for el in self.raise_window, box:
|
||||
self.button_box.add(el)
|
||||
|
||||
self.back.connect("clicked", self._on_back_clicked)
|
||||
self.ok.connect("clicked", self._on_ok_clicked)
|
||||
@ -128,6 +142,21 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
|
||||
margin_top=10,
|
||||
margin_bottom=10,
|
||||
)
|
||||
self.spinner = Gtk.Spinner()
|
||||
self.progress_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=15)
|
||||
self.progress_box.add(self.mod_count)
|
||||
self.progress_box.add(self.spinner)
|
||||
self.cancel = Gtk.Button(
|
||||
label=preconnect.cancel,
|
||||
halign=Gtk.Align.START,
|
||||
hexpand=True,
|
||||
margin_bottom=5,
|
||||
margin_top=5,
|
||||
)
|
||||
self.progress_box.add(self.cancel)
|
||||
self.cancel.connect("clicked", self._on_cancel_clicked)
|
||||
self.cancel.set_visible(False)
|
||||
|
||||
# TODO: live count of remaining downloads
|
||||
# "Steam is downloading: {mod_name}"
|
||||
# mention whether manual or auto mod is active
|
||||
@ -138,7 +167,7 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
|
||||
|
||||
self.tree_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
self.tree_box.add(self.scrolled)
|
||||
self.tree_box.add(self.mod_count)
|
||||
self.tree_box.add(self.progress_box)
|
||||
|
||||
# TODO: strings
|
||||
self.mods_placeholder = Placeholder("This server has no mods.")
|
||||
@ -180,12 +209,18 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
|
||||
self.grab_focus()
|
||||
widgets = (
|
||||
self.tree_frame,
|
||||
self.mod_count,
|
||||
self.progress_box,
|
||||
self.error_placeholder,
|
||||
self.warning_placeholder,
|
||||
)
|
||||
for widget in widgets:
|
||||
widget.set_visible(True)
|
||||
self.raise_window.set_visible(False)
|
||||
|
||||
# TODO: enable button if wmctrl or xdotool is available
|
||||
# TODO: check this at boot time and pass in via connection manager
|
||||
|
||||
self.cancel.set_visible(False)
|
||||
self.ok.set_sensitive(True)
|
||||
self.ok.set_label(preconnect.update_mods)
|
||||
|
||||
@ -193,18 +228,24 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
|
||||
if event.keyval == Gdk.KEY_Escape:
|
||||
self.back.emit("clicked")
|
||||
|
||||
def _on_cancel_clicked(self, button: Gtk.Button) -> None:
|
||||
print("user canceled")
|
||||
|
||||
def _on_ok_clicked(self, button: Gtk.Button) -> None:
|
||||
# TODO: update mod store in place with spinner/toast
|
||||
# no dialog
|
||||
# TODO: cancel mod downloads
|
||||
# TODO: add to history file and list store
|
||||
# TODO: concat mods
|
||||
# sets some kind of global event listener
|
||||
if self.mod_count.get_visible():
|
||||
# TODO: set ready mode
|
||||
# TODO: strings
|
||||
self.mod_count.set_label("Enqueuing downloads")
|
||||
self.spinner.start()
|
||||
self.cancel.set_visible(True)
|
||||
self.ok.set_label(preconnect.connect)
|
||||
self.controller.update_and_connect()
|
||||
pass
|
||||
|
||||
def _on_back_clicked(self, button: Gtk.Button) -> None:
|
||||
page = self.controller.get_prior_page()
|
||||
self.controller.open_page(page)
|
||||
self.controller.open_page(NotebookPage.SERVERS)
|
||||
|
||||
def _process_warnings(self, prereqs: "Prerequisites") -> None:
|
||||
warnings: list[str] = []
|
||||
@ -256,16 +297,17 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
|
||||
|
||||
if total_mods < 1:
|
||||
self.scrolled.set_visible(False)
|
||||
self.mod_count.set_visible(False)
|
||||
self.progress_box.set_visible(False)
|
||||
self.mods_placeholder.set_visible(True)
|
||||
self.ok.set_label(preconnect.connect)
|
||||
else:
|
||||
self.scrolled.set_visible(True)
|
||||
self.mod_count.set_visible(True)
|
||||
self.progress_box.set_visible(True)
|
||||
self.raise_window.set_visible(True)
|
||||
self.mods_placeholder.set_visible(False)
|
||||
|
||||
# TODO: print no. of mods that need updating
|
||||
suffix = ""
|
||||
# TODO: strings
|
||||
suffix = "All mods are up to date."
|
||||
if prereqs.required_space > 0:
|
||||
pretty = number(prereqs.required_space)
|
||||
suffix = f" Need to download {pretty} MiB of mod updates."
|
||||
@ -274,6 +316,12 @@ class PreConnectionAssistant(Gtk.ScrolledWindow):
|
||||
|
||||
self._process_warnings(prereqs)
|
||||
|
||||
def update_mod(self, text: str, mark_finished: bool = False) -> None:
|
||||
self.mod_count.set_label(text)
|
||||
if mark_finished:
|
||||
self.cancel.set_visible(False)
|
||||
self.spinner.stop()
|
||||
|
||||
def add_errors(self, errors: list[str]) -> None:
|
||||
self.error_tree.extend(errors)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user