Something went wrong. Try again.
Steam Deck Doom modding setup automation for Doom Runner, UZDoom, Brutal Doom, and Project Brutality.
brutal-doom moddb python doom-runner linux-gaming project-brutality uzdoom doom steam-deck
Something went wrong. Try again.
7.8 kB · 213 lines
Python
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214"""Managed mod installation use cases."""from __future__ import annotations
import jsonimport loggingimport shutilimport zipfilefrom pathlib import Pathfrom typing import Iterable, Mapping
from doomdeck.domain.models import DoomDeckErrorfrom doomdeck.domain.mods import BRUTAL_DOOM_MOD, PROJECT_BRUTALITY_MOD, InstalledModMetadata, ModSourcefrom doomdeck.infrastructure.archives import ( choose_payload_member, common_zip_toplevel, normalized_zip_member_name, zip_contains_markers,)from doomdeck.infrastructure.downloads import sha256_filefrom doomdeck.infrastructure.files import atomic_write_text, backup_path
SourceMetadata = Mapping[str, str] | ModSource
def metadata_matches(metadata_path: Path, expected: dict[str, str], keys: Iterable[str]) -> bool: if not metadata_path.exists(): return False try: metadata = json.loads(metadata_path.read_text(encoding="utf-8")) except (json.JSONDecodeError, OSError): return False return all(str(metadata.get(key, "")) == str(expected.get(key, "")) for key in keys)
def installed_payload_matches(installed_path: Path, metadata_path: Path) -> bool: if not installed_path.is_file() or not metadata_path.is_file(): return False try: metadata = json.loads(metadata_path.read_text(encoding="utf-8")) expected_sha = metadata.get("installed_sha256") if isinstance(metadata, dict) else None return isinstance(expected_sha, str) and bool(expected_sha) and sha256_file(installed_path) == expected_sha except (json.JSONDecodeError, OSError, UnicodeError): return False
def _reuse_current_payload( dest: Path, metadata_path: Path, source_sha: str, source: SourceMetadata, logger: logging.Logger, label: str,) -> bool: try: metadata = json.loads(metadata_path.read_text(encoding="utf-8")) except (json.JSONDecodeError, OSError, UnicodeError): return False if ( not isinstance(metadata, dict) or metadata.get("source_sha256") != source_sha or not installed_payload_matches(dest, metadata_path) ): return False
source_metadata = source.as_metadata() if isinstance(source, ModSource) else dict(source) if any(str(metadata.get(key, "")) != str(value) for key, value in source_metadata.items()): metadata.update(source_metadata) atomic_write_text(metadata_path, json.dumps(metadata, indent=2) + "\n", False, logger) logger.info("Refresh %s source metadata: %s", label, metadata_path) logger.info("%s already current: %s", label, dest) return True
def install_project_brutality_archive( src: Path, dest: Path, backups_dir: Path, metadata_path: Path, dry_run: bool, logger: logging.Logger, source: SourceMetadata, force: bool = False,) -> Path: if not src.exists() and not dry_run: raise DoomDeckError(f"Project Brutality download is missing: {src}") source_sha = sha256_file(src) if src.exists() else "" if ( dest.exists() and metadata_path.exists() and not force and _reuse_current_payload(dest, metadata_path, source_sha, source, logger, "Project Brutality") ): return dest
logger.info("Install Project Brutality archive: %s -> %s", src, dest) if dry_run: return dest dest.parent.mkdir(parents=True, exist_ok=True) if dest.exists(): backup_path(dest, backups_dir, dry_run, logger, label=dest.name)
if src.suffix.lower() == ".wad" or not zipfile.is_zipfile(src): shutil.copy2(src, dest) else: _repack_project_brutality_zip(src, dest)
metadata = InstalledModMetadata( mod=PROJECT_BRUTALITY_MOD, installed=dest, installed_sha256=sha256_file(dest), source_sha256=source_sha, source=source, ).as_json_object() atomic_write_text(metadata_path, json.dumps(metadata, indent=2) + "\n", dry_run, logger) return dest
def _repack_project_brutality_zip(src: Path, dest: Path) -> None: tmp = dest.with_suffix(dest.suffix + ".tmp") try: with zipfile.ZipFile(src) as source_zip: infos = [info for info in source_zip.infolist() if not info.is_dir()] if not infos: raise DoomDeckError(f"Project Brutality archive has no files: {src}") prefix = common_zip_toplevel(info.filename for info in infos) with zipfile.ZipFile(tmp, "w", compression=zipfile.ZIP_DEFLATED, allowZip64=True) as out_zip: written = 0 for info in infos: new_name = normalized_zip_member_name(info.filename, prefix) if not new_name: continue out_info = zipfile.ZipInfo(new_name, date_time=info.date_time) out_info.external_attr = info.external_attr out_info.compress_type = zipfile.ZIP_DEFLATED out_zip.writestr(out_info, source_zip.read(info)) written += 1 if written == 0: raise DoomDeckError(f"Project Brutality archive had no safe file entries: {src}") tmp.replace(dest) except Exception: if tmp.exists(): tmp.unlink() raise
def install_brutal_doom_archive( src: Path, dest: Path, backups_dir: Path, metadata_path: Path, dry_run: bool, logger: logging.Logger, source: SourceMetadata, force: bool = False,) -> Path: if not src.exists() and not dry_run: raise DoomDeckError(f"Brutal Doom download is missing: {src}") source_sha = sha256_file(src) if src.exists() else "" if ( dest.exists() and metadata_path.exists() and not force and _reuse_current_payload(dest, metadata_path, source_sha, source, logger, "Brutal Doom") ): return dest
logger.info("Install Brutal Doom archive: %s -> %s", src, dest) if dry_run: return dest dest.parent.mkdir(parents=True, exist_ok=True) if dest.exists() or dest.is_symlink(): backup_path(dest, backups_dir, dry_run, logger, label=dest.name)
payload_member = _install_brutal_doom_payload(src, dest)
metadata = InstalledModMetadata( mod=BRUTAL_DOOM_MOD, installed=dest, installed_sha256=sha256_file(dest), payload_member=payload_member, source_sha256=source_sha, source=source, ).as_json_object() atomic_write_text(metadata_path, json.dumps(metadata, indent=2) + "\n", dry_run, logger) return dest
def _install_brutal_doom_payload(src: Path, dest: Path) -> str: payload_member = "" tmp = dest.with_suffix(dest.suffix + ".tmp") try: if src.suffix.lower() in {".pk3", ".wad"}: shutil.copy2(src, tmp) elif zipfile.is_zipfile(src): with zipfile.ZipFile(src) as source_zip: infos = [info for info in source_zip.infolist() if not info.is_dir()] chosen = choose_payload_member(infos) if chosen: payload_member = chosen.filename with source_zip.open(chosen) as source_handle, tmp.open("wb") as dest_handle: shutil.copyfileobj(source_handle, dest_handle) elif zip_contains_markers(src, {"gameinfo.txt"}): shutil.copy2(src, tmp) else: raise DoomDeckError(f"Brutal Doom archive has no .pk3/.wad payload: {src}") else: raise DoomDeckError(f"Brutal Doom file should be a .pk3/.wad/.zip, got: {src}") tmp.replace(dest) except Exception: if tmp.exists(): tmp.unlink() raise return payload_member