initial commit

This commit is contained in:
2026-06-22 06:44:43 -04:00
commit 28da4d386e
7 changed files with 15428 additions and 0 deletions
+142
View File
@@ -0,0 +1,142 @@
# SymlinkModels
Replace unwanted Sven Co-op player models with a neutral placeholder by symlinking them to `helmet.mdl`. This keeps your client from rendering anime, NSFW, or other models you don't want to see, while still letting you connect to any server that uses them.
## How it works
For each model ID, the scripts create a relative symlink:
```
svencoop_addon/models/player/<id>/<id>.mdl
-> ../../../../svencoop/models/player/helmet/helmet.mdl
```
Every suppressed model renders as the default helmet instead.
## Prerequisites
- Sven Co-op installed (scripts must run from the game's root folder, alongside `tags.json`)
- Python 3.8+ **or** PowerShell 5.1+
- **Windows only:** Developer Mode enabled *(Settings → For developers)* **or** run as Administrator — both Windows symlink scripts check for this and warn if neither condition is met
## Files
| File | Description |
|---|---|
| `tags.json` | Model ID lists grouped by category (`anime`, `nsfw`, `vehicle`) |
| `symlink_models.py` | Python — symlink models from `tags.json`, a server path, or a text file |
| `symlink_models.ps1` | PowerShell equivalent of `symlink_models.py` |
| `symlink_downloads.py` | Python — symlink downloaded models that match `tags.json`, then optionally clean up `svencoop_downloads` |
| `symlink_downloads.ps1` | PowerShell equivalent of `symlink_downloads.py` |
| `server_models.txt` | Example model ID dump from a server, usable with `--from-file` |
## Usage
Place the scripts and `tags.json` in your **Sven Co-op root folder** (the directory that contains `svencoop/` and `svencoop_addon/`).
### symlink_models — bulk symlink by source
Symlinks every model ID from the selected source.
**Python:**
```bash
# Default: all IDs in tags.json
python symlink_models.py
# Scan a server's player/ directory
python symlink_models.py --server /path/to/svencoop_addon/models/player
# Read IDs from a text file (one ID per line)
python symlink_models.py --from-file server_models.txt
# Preview changes without writing anything
python symlink_models.py --dry-run
```
**PowerShell:**
```powershell
# Default: all IDs in tags.json
.\symlink_models.ps1
# Scan a server's player/ directory
.\symlink_models.ps1 -Server "C:\path\to\svencoop_addon\models\player"
# Read IDs from a text file
.\symlink_models.ps1 -FromFile server_models.txt
# Preview changes without writing anything
.\symlink_models.ps1 -DryRun
```
### symlink_downloads — process already-downloaded models
Scans `svencoop_downloads/models/player/` for `.mdl` files whose names match IDs in `tags.json`. For each match it creates the symlink in `svencoop_addon`, then lists the source folders and prompts whether to delete them.
**Python:**
```bash
python symlink_downloads.py
# Preview without writing or deleting
python symlink_downloads.py --dry-run
```
**PowerShell:**
```powershell
.\symlink_downloads.py
# Preview without writing or deleting
.\symlink_downloads.ps1 -DryRun
```
The `svencoop_downloads/models/player/` root itself is never deleted — only matched sub-folders.
### Dumping model IDs from a server
If you have access to a server's files (directly or via a network mount), you can dump its player model IDs to a text file and feed it to `symlink_models`:
**Linux/macOS:**
```bash
find /path/to/svencoop_addon/models/player \
-mindepth 2 -maxdepth 2 -name "*.mdl" -printf "%f\n" \
| sed 's/\.mdl$//' > server_models.txt
```
**PowerShell:**
```powershell
Get-ChildItem -Path "C:\path\to\svencoop_addon\models\player" -Recurse -Filter "*.mdl" |
Where-Object { $_.DirectoryName -ne "...\player" } |
ForEach-Object { $_.BaseName } | Sort-Object -Unique | Set-Content server_models.txt
```
## tags.json
The tag file is a flat JSON object with three category arrays:
```json
{
"anime": ["2d_ab_kanna_coco", "sagas_trunks_ssj", ...],
"nsfw": [...],
"vehicle": [...]
}
```
- **anime** (~2,129 entries) — anime character models
- **nsfw** (~579 entries) — adult content models
- **vehicle** (~73 entries) — vehicle models
Model IDs are lowercase with underscores or hyphens. Version suffixes like `_v2` and `_hd` are treated as separate entries. To add or remove a model, edit the relevant array directly. Existing entries are not fully sorted, but try to insert near alphabetical neighbours.
## Output
Each script prints one line per model processed and a summary on exit:
```
[anime] — 2129 models
create : 2d_ab_kanna_coco
skipped : 2d_ab_kratos
replace : sagas_trunks_ssj
147 symlinks written (147 new, 0 replaced), 1982 already correct
```
Exit codes are 0 on success, non-zero on a fatal error (missing `helmet.mdl`, bad path, etc.).
+11835
View File
File diff suppressed because it is too large Load Diff
+190
View File
@@ -0,0 +1,190 @@
<#
.SYNOPSIS
symlink_downloads.ps1 -- place in the "Sven Co-op" root folder alongside tags.json.
.DESCRIPTION
1. Scans svencoop_downloads\models\player\ for .mdl files whose name matches a
model ID in tags.json (case-insensitive).
2. Creates a relative symlink in svencoop_addon\models\player\ for each match:
<id>\<id>.mdl -> ..\..\..\..\svencoop\models\player\helmet\helmet.mdl
3. Lists matched folders and prompts to delete them from svencoop_downloads.
The player\ root itself is never deleted.
.NOTES
Requires PowerShell 5.1+ and either:
- Developer Mode enabled (Settings > For developers)
- Running as Administrator
#>
[CmdletBinding()]
param(
[switch]$DryRun
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# --- Paths ---
$scriptDir = $PSScriptRoot
$tagsPath = Join-Path $scriptDir "tags.json"
$helmetAbs = Join-Path $scriptDir "svencoop\models\player\helmet\helmet.mdl"
$dlRoot = Join-Path $scriptDir "svencoop_downloads\models\player"
$addonRoot = Join-Path $scriptDir "svencoop_addon\models\player"
$symlinkTarget = "..\..\..\..\svencoop\models\player\helmet\helmet.mdl"
# --- Pre-flight checks ---
if (-not (Test-Path $tagsPath)) {
Write-Error "tags.json not found next to script ($tagsPath)"
exit 1
}
if (-not (Test-Path $helmetAbs)) {
Write-Error "helmet.mdl not found: $helmetAbs`nRun from the Sven Co-op root folder."
exit 1
}
if (-not (Test-Path $dlRoot)) {
Write-Error "Downloads folder not found: $dlRoot"
exit 1
}
# Check for symlink privilege on Windows
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
[Security.Principal.WindowsBuiltInRole]::Administrator
)
if (-not $isAdmin) {
$devMode = $false
try {
$dm = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" -ErrorAction SilentlyContinue
$devMode = $dm.AllowDevelopmentWithoutDevLicense -eq 1
} catch {}
if (-not $devMode) {
Write-Warning @"
Symlink creation may fail. To fix, either:
- Enable Developer Mode (Settings > For developers > Developer Mode)
- Re-run this script as Administrator
"@
}
}
# --- Build case-insensitive tag lookup: mdl stem -> canonical ID ---
$tags = Get-Content $tagsPath -Raw -Encoding UTF8 | ConvertFrom-Json
$tagLookup = @{}
foreach ($category in $tags.PSObject.Properties) {
foreach ($id in $category.Value) {
$tagLookup[$id] = $id
}
}
# --- Scan downloads for matching .mdl files ---
$folderMdls = @{}
$matched = [System.Collections.Generic.List[hashtable]]::new()
Get-ChildItem -Path $dlRoot -Recurse -Filter "*.mdl" | ForEach-Object {
$dir = $_.DirectoryName
if ($dir -eq $dlRoot) { return }
if (-not $folderMdls.ContainsKey($dir)) { $folderMdls[$dir] = @() }
$folderMdls[$dir] += $_
$tagsId = $tagLookup[$_.BaseName]
if ($tagsId) {
$matched.Add(@{ File = $_; TagsId = $tagsId })
}
}
if ($matched.Count -eq 0) {
Write-Host "No downloaded .mdl files matched any model ID in tags.json."
exit 0
}
# --- Symlink pass ---
$prefix = if ($DryRun) { "[DRY RUN] " } else { "" }
Write-Host "${prefix}Symlinking $($matched.Count) matched model(s):`n"
$counts = @{ created = 0; replaced = 0; ok = 0; error = 0 }
foreach ($m in $matched) {
$tagsId = $m.TagsId
$addonMdl = Join-Path $addonRoot "$tagsId\$tagsId.mdl"
$addonDir = Split-Path $addonMdl -Parent
if ($DryRun) {
$link = Get-Item $addonMdl -ErrorAction SilentlyContinue
if ($link -and $link.LinkType -and $link.Target -eq $symlinkTarget) {
$status = "ok"
} else {
$status = "would link"
}
Write-Host (" {0,-12} {1} (from {2})" -f $status, $tagsId, $m.File.FullName.Substring($dlRoot.Length + 1))
$counts.created++
continue
}
try {
if (Test-Path $addonMdl) { Remove-Item $addonMdl -Force }
if (-not (Test-Path $addonDir)) { New-Item -ItemType Directory -Path $addonDir -Force | Out-Null }
$existing = Get-Item $addonMdl -ErrorAction SilentlyContinue
if ($existing -and $existing.LinkType -and $existing.Target -eq $symlinkTarget) {
Write-Host " already ok $tagsId"
$counts.ok++
} else {
New-Item -ItemType SymbolicLink -Path $addonMdl -Target $symlinkTarget | Out-Null
Write-Host " linked $tagsId"
$counts.created++
}
} catch {
Write-Host " ERROR ${tagsId}: $_"
$counts.error++
}
}
if (-not $DryRun) {
Write-Host ("`n $($counts.created) created, $($counts.replaced) replaced, $($counts.ok) already correct, $($counts.error) errors")
}
# --- Cleanup prompt ---
$foldersToClean = @{}
foreach ($m in $matched) {
$dir = $m.File.DirectoryName
if (-not $foldersToClean.ContainsKey($dir)) { $foldersToClean[$dir] = @() }
$foldersToClean[$dir] += $m.File
}
Write-Host "`nFolders to delete from svencoop_downloads ($($foldersToClean.Count)):`n"
foreach ($dir in ($foldersToClean.Keys | Sort-Object)) {
$matchedHere = $foldersToClean[$dir]
$matchedPaths = $matchedHere | ForEach-Object { $_.FullName }
$other = $folderMdls[$dir] | Where-Object { $_.FullName -notin $matchedPaths }
$relDir = $dir.Substring($scriptDir.Length + 1)
Write-Host " $relDir\"
foreach ($f in $matchedHere) { Write-Host " [tagged] $($f.Name)" }
foreach ($f in $other) { Write-Host " [extra ] $($f.Name)" }
}
Write-Host ""
if ($DryRun) {
Write-Host "[DRY RUN] Skipping delete prompt."
exit 0
}
$answer = Read-Host "Delete these folders from svencoop_downloads? [y/N]"
if ($answer -ne "y") {
Write-Host "Skipped cleanup."
exit 0
}
$deleted = 0
foreach ($dir in ($foldersToClean.Keys | Sort-Object)) {
if ($dir -eq $dlRoot) {
Write-Host " SKIP (is player\ root): $dir"
continue
}
try {
Remove-Item -Path $dir -Recurse -Force
Write-Host " deleted $($dir.Substring($scriptDir.Length + 1))\"
$deleted++
} catch {
Write-Host " ERROR $([System.IO.Path]::GetFileName($dir)): $_"
}
}
Write-Host "`nDeleted $deleted folder(s)."
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""
symlink_downloads.py — place in the "Sven Co-op" root folder alongside tags.json.
Workflow:
1. Scan svencoop_downloads/models/player/ for .mdl files whose stem matches a
model ID in tags.json (case-insensitive).
2. For each match, create a relative symlink in svencoop_addon/models/player/:
<id>/<id>.mdl -> ../../../../svencoop/models/player/helmet/helmet.mdl
3. Show matched folders, then prompt to delete them from svencoop_downloads.
The player/ root itself is never deleted.
"""
import json
import os
import shutil
import sys
from collections import defaultdict
from pathlib import Path
HELMET_REL = Path("svencoop") / "models" / "player" / "helmet" / "helmet.mdl"
ADDON_PLAYERS = Path("svencoop_addon") / "models" / "player"
DL_PLAYERS = Path("svencoop_downloads") / "models" / "player"
SYMLINK_TARGET = Path("..") / ".." / ".." / ".." / HELMET_REL
def write_symlink(mdl_path: Path) -> str:
"""Create or update the symlink. Returns 'created', 'replaced', or 'ok'."""
target_str = str(SYMLINK_TARGET)
if mdl_path.is_symlink():
if os.readlink(mdl_path) == target_str:
return "ok"
mdl_path.unlink()
elif mdl_path.exists():
mdl_path.unlink()
mdl_path.parent.mkdir(parents=True, exist_ok=True)
os.symlink(target_str, mdl_path)
return "created"
def main():
dry_run = "--dry-run" in sys.argv
script_dir = Path(__file__).resolve().parent
tags_path = script_dir / "tags.json"
if not tags_path.exists():
sys.exit(f"tags.json not found next to script ({tags_path})")
helmet_abs = script_dir / HELMET_REL
if not helmet_abs.exists():
sys.exit(f"helmet.mdl not found: {helmet_abs}\nRun from the Sven Co-op root folder.")
dl_root = script_dir / DL_PLAYERS
if not dl_root.is_dir():
sys.exit(f"Downloads folder not found: {dl_root}")
addon_root = script_dir / ADDON_PLAYERS
with open(tags_path, encoding="utf-8") as f:
tags: dict[str, list[str]] = json.load(f)
# Build case-insensitive lookup: lowercase stem -> canonical tags.json ID
tag_lookup: dict[str, str] = {
model_id.lower(): model_id
for model_ids in tags.values()
for model_id in model_ids
}
# Scan downloads: collect all .mdl paths and identify matches
# folder_mdls: folder -> all .mdl files in that folder
# matched: list of (dl_mdl_path, canonical_tags_id)
folder_mdls: dict[Path, list[Path]] = defaultdict(list)
matched: list[tuple[Path, str]] = []
for mdl_path in sorted(dl_root.rglob("*.mdl")):
parent = mdl_path.parent
if parent == dl_root:
continue # skip .mdl files sitting directly in player/ (no subfolder to delete)
folder_mdls[parent].append(mdl_path)
tags_id = tag_lookup.get(mdl_path.stem.lower())
if tags_id:
matched.append((mdl_path, tags_id))
if not matched:
print("No downloaded .mdl files matched any model ID in tags.json.")
return
# --- Symlink pass ---
print(f"{'[DRY RUN] ' if dry_run else ''}Symlinking {len(matched)} matched model(s):\n")
counts = {"created": 0, "replaced": 0, "ok": 0, "error": 0}
for dl_mdl, tags_id in matched:
addon_mdl = addon_root / tags_id / f"{tags_id}.mdl"
if dry_run:
existing = "ok" if (addon_mdl.is_symlink() and os.readlink(addon_mdl) == str(SYMLINK_TARGET)) else "would link"
print(f" {existing:<12} {tags_id} (from {dl_mdl.relative_to(dl_root)})")
counts["created"] += 1
else:
try:
result = write_symlink(addon_mdl)
counts[result] += 1
label = {"created": "linked ", "replaced": "replaced ", "ok": "already ok"}.get(result, result)
print(f" {label} {tags_id}")
except OSError as exc:
counts["error"] += 1
print(f" ERROR {tags_id}: {exc}")
if not dry_run:
print(
f"\n {counts['created']} created, {counts['replaced']} replaced, "
f"{counts['ok']} already correct, {counts['error']} errors"
)
# --- Cleanup prompt ---
# Any folder containing at least one tagged .mdl is deleted entirely.
# The player/ root itself is never deleted.
folders_to_clean: dict[Path, list[Path]] = defaultdict(list)
for dl_mdl, _ in matched:
folders_to_clean[dl_mdl.parent].append(dl_mdl)
print(f"\nFolders to delete from svencoop_downloads ({len(folders_to_clean)}):\n")
for folder in sorted(folders_to_clean):
matched_here = folders_to_clean[folder]
other = [p for p in folder_mdls[folder] if p not in matched_here]
print(f" {folder.relative_to(script_dir)}/")
for m in matched_here:
print(f" [tagged] {m.name}")
for o in other:
print(f" [extra ] {o.name}")
print()
if dry_run:
print("[DRY RUN] Skipping delete prompt.")
return
try:
answer = input("Delete these folders from svencoop_downloads? [y/N] ").strip().lower()
except (EOFError, KeyboardInterrupt):
print("\nAborted.")
return
if answer != "y":
print("Skipped cleanup.")
return
deleted = 0
for folder in sorted(folders_to_clean):
if folder == dl_root:
print(f" SKIP (is player/ root): {folder}")
continue
try:
shutil.rmtree(folder)
print(f" deleted {folder.relative_to(script_dir)}/")
deleted += 1
except OSError as exc:
print(f" ERROR {folder.name}: {exc}")
print(f"\nDeleted {deleted} folder(s).")
if __name__ == "__main__":
main()
+154
View File
@@ -0,0 +1,154 @@
<#
.SYNOPSIS
symlink_models.ps1 -- place in the "Sven Co-op" root folder alongside tags.json.
.DESCRIPTION
Creates a relative symlink for every model:
svencoop_addon\models\player\<id>\<id>.mdl -> ..\..\..\..\ svencoop\models\player\helmet\helmet.mdl
Sources (pick one, or default to tags.json):
default -- model IDs from tags.json, grouped by category
-Server PATH -- scan any svencoop_addon\models\player\ directory for .mdl files
-FromFile FILE -- read one model ID per line from a text file
Dump server model IDs to a file (run on the server):
Get-ChildItem -Path "C:\path\to\svencoop_addon\models\player" -Recurse -Filter "*.mdl" |
Where-Object { $_.DirectoryName -ne "...\player" } |
ForEach-Object { $_.BaseName } | Sort-Object -Unique | Set-Content server_models.txt
.NOTES
Requires PowerShell 5.1+ and either:
- Developer Mode enabled (Settings > For developers)
- Running as Administrator
#>
[CmdletBinding()]
param(
[switch]$DryRun,
[string]$Server,
[string]$FromFile
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# --- Paths ---
$scriptDir = $PSScriptRoot
$helmetAbs = Join-Path $scriptDir "svencoop\models\player\helmet\helmet.mdl"
$addonRoot = Join-Path $scriptDir "svencoop_addon\models\player"
$symlinkTarget = "..\..\..\..\svencoop\models\player\helmet\helmet.mdl"
# --- Pre-flight ---
if (-not (Test-Path $helmetAbs)) {
Write-Error "helmet.mdl not found: $helmetAbs`nRun from the Sven Co-op root folder."
exit 1
}
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
[Security.Principal.WindowsBuiltInRole]::Administrator
)
if (-not $isAdmin) {
$devMode = $false
try {
$dm = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" -ErrorAction SilentlyContinue
$devMode = $dm.AllowDevelopmentWithoutDevLicense -eq 1
} catch {}
if (-not $devMode) {
Write-Warning @"
Symlink creation may fail. To fix, either:
- Enable Developer Mode (Settings > For developers > Developer Mode)
- Re-run this script as Administrator
"@
}
}
# --- Shared: symlink one model ID ---
function Invoke-Symlink {
param([string]$ModelId)
$modelDir = Join-Path $addonRoot $ModelId
$mdlPath = Join-Path $modelDir "$ModelId.mdl"
if (-not (Test-Path $modelDir -PathType Container)) {
if (-not $DryRun) { New-Item -ItemType Directory -Path $modelDir -Force | Out-Null }
}
$item = Get-Item $mdlPath -ErrorAction SilentlyContinue
if ($item -and $item.LinkType) {
if ($item.Target -eq $symlinkTarget) { return "skipped" }
if (-not $DryRun) { Remove-Item $mdlPath -Force }
Write-Host " replace : $ModelId"
$result = "replaced"
} elseif ($item) {
if (-not $DryRun) { Remove-Item $mdlPath -Force }
Write-Host " replace : $ModelId (was real file)"
$result = "replaced"
} else {
Write-Host " create : $ModelId"
$result = "created"
}
if (-not $DryRun) {
try {
New-Item -ItemType SymbolicLink -Path $mdlPath -Target $symlinkTarget | Out-Null
} catch {
Write-Host " ERROR : $ModelId -- $_"
return "error"
}
}
return $result
}
function Write-Summary {
param([hashtable]$Counts)
$total = $Counts.created + $Counts.replaced
$prefix = if ($DryRun) { "[DRY RUN] " } else { "" }
$verb = if ($DryRun) { "would be written" } else { "written" }
$errors = if ($Counts.error) { ", $($Counts.error) errors" } else { "" }
Write-Host "`n${prefix}$total symlinks $verb ($($Counts.created) new, $($Counts.replaced) replaced), $($Counts.skipped) already correct${errors}"
}
$counts = @{ created = 0; replaced = 0; skipped = 0; error = 0 }
# --- -Server: scan a player/ directory ---
if ($Server) {
if (-not (Test-Path $Server -PathType Container)) {
Write-Error "Server path not found: $Server"
exit 1
}
$modelIds = Get-ChildItem -Path $Server -Recurse -Filter "*.mdl" |
Where-Object { $_.DirectoryName -ne $Server } |
ForEach-Object { $_.BaseName } |
Sort-Object -Unique
Write-Host "`n[server] -- $($modelIds.Count) models found in $Server`n"
foreach ($id in $modelIds) { $counts[(Invoke-Symlink $id)]++ }
Write-Summary $counts
return
}
# --- -FromFile: read model IDs from a text file ---
if ($FromFile) {
if (-not (Test-Path $FromFile)) {
Write-Error "File not found: $FromFile"
exit 1
}
$modelIds = Get-Content $FromFile | Where-Object { $_.Trim() -ne "" } | ForEach-Object { $_.Trim() }
Write-Host "`n[from file] -- $($modelIds.Count) models in $FromFile`n"
foreach ($id in $modelIds) { $counts[(Invoke-Symlink $id)]++ }
Write-Summary $counts
return
}
# --- Default: tags.json ---
$tagsPath = Join-Path $scriptDir "tags.json"
if (-not (Test-Path $tagsPath)) {
Write-Error "tags.json not found next to script ($tagsPath)"
exit 1
}
$tags = Get-Content $tagsPath -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($category in $tags.PSObject.Properties) {
Write-Host "`n[$($category.Name)] -- $($category.Value.Count) models"
foreach ($id in $category.Value) { $counts[(Invoke-Symlink $id)]++ }
}
Write-Summary $counts
+155
View File
@@ -0,0 +1,155 @@
#!/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()
Executable
+2789
View File
File diff suppressed because it is too large Load Diff