#!/usr/bin/env python3 """ symlink_models.py — place this file and tags.json in the "Sven Co-op" root folder. Creates a relative symlink for every model: svencoop_addon/models/player//.mdl -> ../../../../svencoop/models/player/helmet/helmet.mdl Sources (pick one, or combine): default — model IDs from tags.json, grouped by category --server PATH — scan any svencoop_addon/models/player/ directory for .mdl files --from-file FILE — read one model ID per line from a text file Windows: requires Developer Mode or Administrator privileges to create symlinks. Dump server model IDs to a file (run on the server or over a mount): find /home/svenserver/serverfiles/svencoop_addon/models/player \\ -mindepth 2 -maxdepth 2 -name "*.mdl" -printf "%f\\n" | sed 's/\\.mdl$//' > server_models.txt """ import argparse import json import os import sys from pathlib import Path HELMET_REL = Path("svencoop") / "models" / "player" / "helmet" / "helmet.mdl" MODELS_REL = Path("svencoop_addon") / "models" / "player" SYMLINK_TARGET = Path("..") / ".." / ".." / ".." / HELMET_REL def check_windows_symlink_privilege(): if os.name != "nt": return True import ctypes try: return ctypes.windll.shell32.IsUserAnAdmin() != 0 except Exception: return False def symlink_one(model_id: str, models_root: Path, target_str: str, dry_run: bool) -> str: """Create/update symlink for one model_id. Returns 'created', 'replaced', 'skipped', 'error'.""" model_dir = models_root / model_id mdl_path = model_dir / f"{model_id}.mdl" if not model_dir.is_dir() and not dry_run: model_dir.mkdir(parents=True, exist_ok=True) if mdl_path.is_symlink(): if os.readlink(mdl_path) == target_str: return "skipped" if not dry_run: mdl_path.unlink() print(f" replace : {model_id}") return "replaced" elif mdl_path.exists(): if not dry_run: mdl_path.unlink() print(f" replace : {model_id} (was real file)") return "replaced" else: print(f" create : {model_id}") if not dry_run: try: os.symlink(target_str, mdl_path) except OSError as exc: print(f" ERROR : {model_id} — {exc}") return "error" return "created" def print_summary(counts: dict, dry_run: bool): total = counts["created"] + counts["replaced"] prefix = "[DRY RUN] " if dry_run else "" verb = "would be written" if dry_run else "written" print( f"\n{prefix}{total} symlinks {verb} " f"({counts['created']} new, {counts['replaced']} replaced), " f"{counts['skipped']} already correct" + (f", {counts['error']} errors" if counts["error"] else "") ) def main(): parser = argparse.ArgumentParser(description="Symlink Sven Co-op player models to helmet.mdl") parser.add_argument("--dry-run", action="store_true", help="Show what would happen without making changes") parser.add_argument("--server", metavar="PATH", help="Scan a server's svencoop_addon/models/player/ directory") parser.add_argument("--from-file", metavar="FILE", help="Read model IDs (one per line) from a text file") args = parser.parse_args() script_dir = Path(__file__).resolve().parent helmet_abs = script_dir / HELMET_REL if not helmet_abs.exists(): sys.exit(f"helmet.mdl not found at {helmet_abs}\nAre you running from the Sven Co-op root?") if os.name == "nt" and not check_windows_symlink_privilege(): print( "WARNING: On Windows, symlink creation requires either:\n" " • Developer Mode enabled (Settings > For developers)\n" " • Running this script as Administrator\n" ) models_root = script_dir / MODELS_REL target_str = str(SYMLINK_TARGET) counts = {"created": 0, "replaced": 0, "skipped": 0, "error": 0} # --- --server: scan a player/ directory for all .mdl files --- if args.server: server_root = Path(args.server) if not server_root.is_dir(): sys.exit(f"Server path not found: {server_root}") mdl_files = sorted(server_root.rglob("*.mdl")) model_ids = sorted({f.stem for f in mdl_files if f.parent != server_root}) print(f"\n[server] — {len(model_ids)} models found in {server_root}\n") for model_id in model_ids: result = symlink_one(model_id, models_root, target_str, args.dry_run) counts[result] += 1 print_summary(counts, args.dry_run) return # --- --from-file: read model IDs from a text file --- if args.from_file: file_path = Path(args.from_file) if not file_path.exists(): sys.exit(f"File not found: {file_path}") model_ids = [line.strip() for line in file_path.read_text().splitlines() if line.strip()] print(f"\n[from file] — {len(model_ids)} models in {file_path}\n") for model_id in model_ids: result = symlink_one(model_id, models_root, target_str, args.dry_run) counts[result] += 1 print_summary(counts, args.dry_run) return # --- default: tags.json --- tags_path = script_dir / "tags.json" if not tags_path.exists(): sys.exit(f"tags.json not found next to script ({tags_path})") with open(tags_path, encoding="utf-8") as f: tags: dict[str, list[str]] = json.load(f) for category, model_ids in tags.items(): print(f"\n[{category}] — {len(model_ids)} models") for model_id in model_ids: result = symlink_one(model_id, models_root, target_str, args.dry_run) counts[result] += 1 print_summary(counts, args.dry_run) if __name__ == "__main__": main()