156 lines
5.7 KiB
Python
156 lines
5.7 KiB
Python
|
|
#!/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()
|