<# .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: \.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)."