#!/usr/bin/env python3 """Copy workshop mods from SERVER.xml into LocalMods/ with readable names.""" import shutil import sys import os import xml.etree.ElementTree as ET WORKSHOP_DIR = "/mnt/nvme/B/SteamLibrary/steamapps/workshop/content/602960" MODLIST_PATH = "/mnt/nvme/B/SteamLibrary/steamapps/common/Barotrauma/ModLists/SERVER.xml" LOCALMODS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "LocalMods") def log_ok(msg: str, indent: int = 0): prefix = " " * indent + "✓" print(f"\033[92m{prefix}\033[0m {msg}") def log_info(msg: str, indent: int = 0): prefix = " " * indent + "•" print(f"{prefix} {msg}") def log_warn(msg: str, indent: int = 0): prefix = " " * indent + "⚠" print(f"\033[93m{prefix}\033[0m {msg}") def log_err(msg: str, indent: int = 0): prefix = " " * indent + "✗" print(f"\033[91m{prefix}\033[0m {msg}", file=sys.stderr) def sanitize_folder_name(name: str) -> str: forbidden = '/\\:*?"<>|' for ch in forbidden: name = name.replace(ch, "_") return name.strip() def get_mod_name_from_filelist(folder_path: str) -> str | None: fl_path = os.path.join(folder_path, "filelist.xml") if not os.path.isfile(fl_path): return None try: root = ET.parse(fl_path).getroot() return root.get("name") except Exception as e: log_warn(f"Failed to parse filelist.xml: {e}", indent=2) return None def copy_mod(src: str, dst: str) -> bool: if os.path.exists(dst): log_info(f"Removing existing folder: {dst}", indent=2) shutil.rmtree(dst) log_info(f"Copying: {src} → {dst}", indent=2) shutil.copytree(src, dst, symlinks=True) return True def main(): print() print("╔══════════════════════════════════════════╗") print("║ Prepare Local Mods from SERVER ║") print("╚══════════════════════════════════════════╝") print() if not os.path.isfile(MODLIST_PATH): log_err(f"Mod list not found: {MODLIST_PATH}") sys.exit(1) log_info(f"Reading mod list: {MODLIST_PATH}") try: tree = ET.parse(MODLIST_PATH) root = tree.getroot() except Exception as e: log_err(f"Failed to parse mod list: {e}") sys.exit(1) mods = root.findall("Workshop") if not mods: log_warn("No entries found in mod list") sys.exit(0) log_info(f"Found {len(mods)} workshop mod(s)") print() copied = 0 skipped = 0 errors = 0 for mod in mods: mod_id = mod.get("id", "").strip() mod_name = mod.get("name", "").strip() if not mod_id: log_warn(f"Skipping entry with no id: name='{mod_name}'", indent=1) skipped += 1 continue print(f"[{mod_id}] {mod_name}") log_info(f"Processing mod: {mod_name}", indent=1) src_dir = os.path.join(WORKSHOP_DIR, mod_id) if not os.path.isdir(src_dir): log_err(f"Workshop folder not found: {src_dir}", indent=2) errors += 1 continue display_name = get_mod_name_from_filelist(src_dir) if not display_name: log_warn("No name found in filelist.xml, using Steam name", indent=2) display_name = mod_name safe_name = sanitize_folder_name(display_name) if not safe_name: log_warn("Sanitized name is empty, using mod ID as fallback", indent=2) safe_name = mod_id dst_dir = os.path.join(LOCALMODS_DIR, safe_name) try: copy_mod(src_dir, dst_dir) log_ok(f"Copied as: {safe_name}", indent=2) copied += 1 except Exception as e: log_err(f"Copy failed: {e}", indent=2) errors += 1 print() print("─" * 50) log_ok(f"Copied: {copied}") log_info(f"Skipped: {skipped}") if errors: log_err(f"Errors: {errors}") local_mods = root.findall("Local") if local_mods: print() log_info(f"Checking {len(local_mods)} local mod reference(s)…") for lm in local_mods: lm_name = lm.get("name", "").strip() if not lm_name: continue lm_path = os.path.join(LOCALMODS_DIR, sanitize_folder_name(lm_name)) if os.path.isdir(lm_path): log_ok(f"Local mod present: {lm_name}", indent=1) else: log_warn(f"Local mod NOT found: {lm_name}", indent=1) print() if not errors and skipped == 0: print() log_ok("All mods prepared successfully!") print() if __name__ == "__main__": main()