Files
SymlinkModels/symlink_models.py
T
2026-06-22 06:44:43 -04:00

156 lines
5.7 KiB
Python
Executable File

#!/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/<id>/<id>.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()