Files
dzgui/dzgui/api/mods.py
T
2026-11-11 07:00:17 +09:00

212 lines
5.7 KiB
Python

import hashlib
import logging
import re
from concurrent.futures import ThreadPoolExecutor, wait
from pathlib import Path
from typing import Any
from dzgui.api.steam import get_app_path
from dzgui.api.servers import get_rules, fqip_to_record
from dzgui.const.constants import (
APP_NAME,
APPID_DAYZ,
DAYZ_COMMUNITY_ROOT,
WORKSHOP_PATH,
)
from dzgui.config.query import lookup
from dzgui.const.enum import Preferences
from dzgui.init.migrate import clean_legacy_versions_file
logger = logging.getLogger(APP_NAME)
def get_local_mod_ids(steam_path: Path) -> list[int]:
workshop_path = get_local_mod_path(steam_path)
mods = get_local_mods(workshop_path)
return [int(mod.name) for mod in mods]
def get_local_mod_path(steam_path: Path) -> Path:
p = get_app_path(steam_path, APPID_DAYZ)
workshop_path = p / WORKSHOP_PATH
return workshop_path
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
if parent != DAYZ_COMMUNITY_ROOT:
return False
file = path / "init.c"
return file.exists()
def tokenize(mod: Path) -> dict[str, Any] | None:
file = mod.joinpath("meta.cpp")
delimiter = r"\s*=\s*"
modmeta = {}
try:
with open(file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip().rstrip(";")
if not line:
continue
els = re.split(delimiter, line, maxsplit=1)
if len(els) == 2:
key, value = els
modmeta[key.strip()] = value.strip('"')
return modmeta
except Exception as e:
logger.critical(e)
return None
def get_mod_size(path: Path) -> float:
s = 0
for f in path.rglob("*"):
s += f.stat().st_size
size = round(s / (1024**2), 3)
return size
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)
def parse_mods(mods: list[Path], use_custom: bool = False) -> list[Any]:
clean = []
for mod in mods:
mod_dir = mod.name
symlink = _hash(mod_dir, use_custom)
meta = tokenize(mod)
if meta is None:
continue
size = get_mod_size(mod)
# NOTE: final col is cell renderer highlight toggle
clean.append([meta["name"], symlink, mod_dir, size, False])
clean.sort(key=lambda row: str(row[0]).casefold())
return clean
def get_delimited_mods(steam_path: Path) -> list[Any]:
workshop_path = get_local_mod_path(steam_path)
mods = get_local_mods(workshop_path)
return parse_mods(mods)
def _hash(uid: str, use_custom: bool = False) -> str:
md5 = hashlib.md5()
md5.update(uid.encode("ascii"))
prefix = "@C" if use_custom else "@"
return prefix + md5.hexdigest()[:8]
def remove_stale_signatures(
config: Path, versions: Path, ids: list[int] | None = None
) -> None:
if versions.is_file() is False:
logger.debug("Creating new version signatures file")
versions.touch()
return
clean_legacy_versions_file(versions)
path = lookup(config, Preferences.DEFAULT)
steam_path = Path(path)
if ids is None:
local_ids = get_local_mod_ids(steam_path)
with open(versions, "r") as f:
lines = f.readlines()
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)
def find_stale_mods(config: Path) -> list[int]:
def push_record(rec: str) -> list[int]:
record = fqip_to_record(rec)
if record is None:
return []
try:
mods = get_rules(record)
except Exception:
return []
return [mod.workshop_id for mod in mods]
steam = lookup(config, Preferences.DEFAULT)
steam_path = Path(steam)
local = get_local_mod_ids(steam_path)
records = lookup(config, Preferences.IP_LIST)
remote_mods = []
with ThreadPoolExecutor() as executor:
futures = [executor.submit(push_record, record) for record in records]
wait(futures)
for future in futures:
res = future.result()
remote_mods.extend(res)
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:
"""
This is not a guarantee of parity, as user may adjusted contents of local mods,
so upstream epoch time is still used
"""
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
def version_file_to_dict(file: Path) -> dict[str, str]:
versions = file.read_text().splitlines()
d: dict[str, str] = {}
for version in versions:
line = version.split(",")
mod = line[0]
stamp = line[1]
d[mod] = stamp
return d
def update_signatures(
mods: list[tuple[str, str, int, int]], version_file: Path
) -> None:
d = version_file_to_dict(version_file)
for _title, mod, stamp, size in mods:
d[mod] = str(stamp)
versions: list[str] = []
for k, v in d.items():
row = ",".join([k, v])
versions.append(row)
with open(version_file, "w") as f:
for version in versions:
f.write(f"{version}\n")